mirror of
https://github.com/Athou/commafeed.git
synced 2026-03-21 21:37:29 +00:00
Compare commits
345 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74bce1308c | ||
|
|
98cfa6d2c8 | ||
|
|
99a02a2186 | ||
|
|
3431a813b1 | ||
|
|
e9e0e8d32b | ||
|
|
2d14409d35 | ||
|
|
a8200e5c58 | ||
|
|
79a8df8b06 | ||
|
|
061a5f0262 | ||
|
|
821bdb3b0f | ||
|
|
606dfa9299 | ||
|
|
131357c616 | ||
|
|
f6d3493bad | ||
|
|
0c6104e25b | ||
|
|
d73735a35d | ||
|
|
e725d2d6b6 | ||
|
|
f0e1279d68 | ||
|
|
c74c74d2c4 | ||
|
|
aa70cf5dcd | ||
|
|
1055259627 | ||
|
|
302d37b6ef | ||
|
|
8532a73d94 | ||
|
|
ffafb272cb | ||
|
|
22e0171a34 | ||
|
|
2b410f040c | ||
|
|
259e8ad4e5 | ||
|
|
21244dd9f5 | ||
|
|
bc6206180d | ||
|
|
6e22d21358 | ||
|
|
95bdb4e700 | ||
|
|
9b7dbc68ab | ||
|
|
dc86c9b0db | ||
|
|
cb92ed753f | ||
|
|
10a085e24e | ||
|
|
3400a39edf | ||
|
|
21efffa345 | ||
|
|
e2e80ba7e5 | ||
|
|
d988dba66e | ||
|
|
403201fbff | ||
|
|
3cc93b51bb | ||
|
|
6a7d83bb45 | ||
|
|
19c8db8b31 | ||
|
|
0d75688ec8 | ||
|
|
e01dcb2f5b | ||
|
|
57757e2c14 | ||
|
|
779cd2fcfe | ||
|
|
94919f22e4 | ||
|
|
d5cf690703 | ||
|
|
191574dace | ||
|
|
ee7c6792c9 | ||
|
|
e2962dc2eb | ||
|
|
8c335cb8fd | ||
|
|
461c18a591 | ||
|
|
363837ab26 | ||
|
|
a184485421 | ||
|
|
f992c3f1a6 | ||
|
|
3219a9e313 | ||
|
|
4717c31058 | ||
|
|
693844828b | ||
|
|
ef4b479638 | ||
|
|
8eefb1bcfb | ||
|
|
ada9a5039b | ||
|
|
cca2d49cc3 | ||
|
|
f4a43e9950 | ||
|
|
9a89b39b62 | ||
|
|
2dba844b6c | ||
|
|
3101dc91de | ||
|
|
83cacd97f3 | ||
|
|
aa179c21f8 | ||
|
|
31cf4d8bb2 | ||
|
|
19bcc2c0da | ||
|
|
ca803ff7ce | ||
|
|
0e26d975aa | ||
|
|
86a3cb67f2 | ||
|
|
6297463445 | ||
|
|
1a3a3076b1 | ||
|
|
7fb9cfeaf1 | ||
|
|
5c7dbe6304 | ||
|
|
c41fd9216a | ||
|
|
91a9ad79f0 | ||
|
|
906458dc96 | ||
|
|
2f4fddf539 | ||
|
|
a8157143b9 | ||
|
|
92576e28e9 | ||
|
|
a6e34a2960 | ||
|
|
306cf7aab7 | ||
|
|
f3b806686d | ||
|
|
6634cfb991 | ||
|
|
9930bb68b2 | ||
|
|
37722131e5 | ||
|
|
5f2d213419 | ||
|
|
e119941762 | ||
|
|
ba496c1395 | ||
|
|
9c3fc84542 | ||
|
|
b017ce936a | ||
|
|
d696b0581b | ||
|
|
e4b2880f2b | ||
|
|
e071049969 | ||
|
|
5e1f592951 | ||
|
|
231f82da28 | ||
|
|
9a28bc7334 | ||
|
|
00907e92ff | ||
|
|
5669798881 | ||
|
|
f3a62a5f75 | ||
|
|
3b20ed222c | ||
|
|
1f40f3f59c | ||
|
|
a8d890524f | ||
|
|
b635799e80 | ||
|
|
50ea66620d | ||
|
|
46581d0e22 | ||
|
|
a3562867a6 | ||
|
|
c0117ada93 | ||
|
|
a3dcb2c03e | ||
|
|
8f8aaa5a1d | ||
|
|
85482422fd | ||
|
|
643c969faf | ||
|
|
85f9469d6d | ||
|
|
0df0d50695 | ||
|
|
5e9256c197 | ||
|
|
b67e1a92f5 | ||
|
|
d250e4bc26 | ||
|
|
dcf1f41f2d | ||
|
|
3df6ba1457 | ||
|
|
b89928f6c6 | ||
|
|
2e014484e3 | ||
|
|
3b2b18fd2e | ||
|
|
ebf1e592ff | ||
|
|
88404b91d8 | ||
|
|
9cbb60313c | ||
|
|
b95d417f5e | ||
|
|
994f1fb121 | ||
|
|
e533c1fa4b | ||
|
|
d0d946ffe9 | ||
|
|
e3bcc824c7 | ||
|
|
357e4e207f | ||
|
|
2aee961600 | ||
|
|
3aa1987319 | ||
|
|
ae15f61fc2 | ||
|
|
e58f92a812 | ||
|
|
46383924b1 | ||
|
|
071920e864 | ||
|
|
012238e6a9 | ||
|
|
a565566c50 | ||
|
|
550804c666 | ||
|
|
f7a4a33f5e | ||
|
|
f1b19ebae3 | ||
|
|
4049fa2e17 | ||
|
|
28808cf4f5 | ||
|
|
870b46cf9d | ||
|
|
9c20dea99c | ||
|
|
63c7679067 | ||
|
|
764c1a6430 | ||
|
|
bb6578bdd0 | ||
|
|
748c8531ad | ||
|
|
a734fe68d2 | ||
|
|
cc5ebc55a4 | ||
|
|
aa396c1e1c | ||
|
|
fbf87ff291 | ||
|
|
e9f3ffddf4 | ||
|
|
695518d68b | ||
|
|
5d96c1e12b | ||
|
|
3a72a1cc04 | ||
|
|
54f5714108 | ||
|
|
04811c7eca | ||
|
|
6856736ddb | ||
|
|
db6c43993a | ||
|
|
508a22576a | ||
|
|
8fb012b3a1 | ||
|
|
133781d314 | ||
|
|
50cb12896e | ||
|
|
79a4315941 | ||
|
|
33a2f76521 | ||
|
|
d4041a1d88 | ||
|
|
09f2f56446 | ||
|
|
a0c3eda506 | ||
|
|
84de3199cc | ||
|
|
a7e8309d63 | ||
|
|
7e74d2f6f4 | ||
|
|
dc25d53dc0 | ||
|
|
ac1a927836 | ||
|
|
b50b69adb2 | ||
|
|
b112e912af | ||
|
|
ece55727d3 | ||
|
|
181dd24b57 | ||
|
|
10008ca0e8 | ||
|
|
134c4621a8 | ||
|
|
51f15bf487 | ||
|
|
49ae2c88ad | ||
|
|
43a628fc55 | ||
|
|
7f71f95f7c | ||
|
|
e8d5eab419 | ||
|
|
de3a6b1f20 | ||
|
|
849742e19a | ||
|
|
b6392b114c | ||
|
|
4db0c775ff | ||
|
|
ff9374f1ed | ||
|
|
ea86c9bb1f | ||
|
|
e6dd088abe | ||
|
|
c039d8f3a4 | ||
|
|
bffa6329fd | ||
|
|
b88e5d2847 | ||
|
|
0fc4fcd406 | ||
|
|
f04ca21394 | ||
|
|
a82fca130f | ||
|
|
70d494798c | ||
|
|
cf02bf221b | ||
|
|
9eb03d7455 | ||
|
|
12c8fdeec2 | ||
|
|
851babfe2a | ||
|
|
859490806b | ||
|
|
2c828b50da | ||
|
|
ede7834cb8 | ||
|
|
3627ee369d | ||
|
|
c4c41d1494 | ||
|
|
c577e77f8f | ||
|
|
9218f19832 | ||
|
|
ecbc2133a4 | ||
|
|
e38ca66c51 | ||
|
|
2395a2670e | ||
|
|
e7748d787f | ||
|
|
012ce71134 | ||
|
|
1b1a3f49c1 | ||
|
|
5b77860189 | ||
|
|
b333e8d90a | ||
|
|
ab6457ef3f | ||
|
|
5c69daec08 | ||
|
|
1bfa3ebb8e | ||
|
|
2694fea211 | ||
|
|
720eddeb66 | ||
|
|
ab334a7bc6 | ||
|
|
214dfe580a | ||
|
|
4ef53eab3a | ||
|
|
2f51547f0d | ||
|
|
da910ac336 | ||
|
|
643954f7c9 | ||
|
|
63061482d0 | ||
|
|
86d4f5a670 | ||
|
|
815093f1c6 | ||
|
|
47d39831d3 | ||
|
|
c18ed829aa | ||
|
|
e757e61b79 | ||
|
|
d612d83874 | ||
|
|
e170dfe60b | ||
|
|
69cd90edd8 | ||
|
|
f506f722c2 | ||
|
|
857736adad | ||
|
|
a92df774bd | ||
|
|
f2c6734c79 | ||
|
|
77b6cf75a5 | ||
|
|
3b56496196 | ||
|
|
aabbf0a5d1 | ||
|
|
9a43fd434f | ||
|
|
21ce9db4b0 | ||
|
|
044694487d | ||
|
|
3af8485326 | ||
|
|
f7adef0648 | ||
|
|
dc16e43154 | ||
|
|
78a5267198 | ||
|
|
04af355e0c | ||
|
|
89405009ec | ||
|
|
6b0aa32da2 | ||
|
|
aaf237d111 | ||
|
|
1fd48a0a40 | ||
|
|
09e0a51b46 | ||
|
|
cc32f8ad16 | ||
|
|
2f6ddf0e70 | ||
|
|
c3973755da | ||
|
|
42537a65b9 | ||
|
|
906c92e54f | ||
|
|
cc69968d78 | ||
|
|
dcde2083ec | ||
|
|
7469784059 | ||
|
|
c13a693456 | ||
|
|
e3c482d664 | ||
|
|
1fd33a5585 | ||
|
|
0742778e6a | ||
|
|
152479c888 | ||
|
|
a297f8c0c8 | ||
|
|
92aeee0572 | ||
|
|
050756517e | ||
|
|
0bb46f291a | ||
|
|
1eecabf105 | ||
|
|
da1bd8d32e | ||
|
|
124983a396 | ||
|
|
43613688da | ||
|
|
43aa69cd18 | ||
|
|
780b7666c5 | ||
|
|
70b4534e14 | ||
|
|
24666fd7fc | ||
|
|
de80aa6bb3 | ||
|
|
6c7e2ea847 | ||
|
|
6ea318acd3 | ||
|
|
2f4ee7cff8 | ||
|
|
9d9d758fa6 | ||
|
|
a071b7c265 | ||
|
|
3a57b68fa3 | ||
|
|
f2f36baf1b | ||
|
|
1aaf9e747a | ||
|
|
92611772a9 | ||
|
|
fb159dc46b | ||
|
|
78ea0873f2 | ||
|
|
faabc01dbc | ||
|
|
5acfe9e92a | ||
|
|
4388a8b6ce | ||
|
|
7414bd15b0 | ||
|
|
52d6021f3c | ||
|
|
f7acc27fcb | ||
|
|
175a293327 | ||
|
|
21dd6519b0 | ||
|
|
72f55c34b7 | ||
|
|
1b1d3c791b | ||
|
|
159c2c01a7 | ||
|
|
272f5b42f9 | ||
|
|
2395d0782e | ||
|
|
da81830e43 | ||
|
|
63a602cf8a | ||
|
|
0244b5c3e3 | ||
|
|
9592e86fa9 | ||
|
|
e6840bb50c | ||
|
|
b6890378a1 | ||
|
|
ba72ed0b93 | ||
|
|
e2fb576858 | ||
|
|
608b099b4d | ||
|
|
c2e0c81f7e | ||
|
|
7071d01a59 | ||
|
|
30cd2b9b53 | ||
|
|
abc498b09c | ||
|
|
31081e1089 | ||
|
|
4a16b8d072 | ||
|
|
9c04095292 | ||
|
|
643f98d59e | ||
|
|
f4da19183e | ||
|
|
de40f253b5 | ||
|
|
f6543e407a | ||
|
|
4d462a8e9e | ||
|
|
018ee1f3e6 | ||
|
|
752268fed1 | ||
|
|
8fe9a6cc3a | ||
|
|
b17a17ba10 | ||
|
|
b3545b60ea | ||
|
|
e6da3f693d | ||
|
|
4ab09da434 | ||
|
|
5e8daf29bf | ||
|
|
024a1067bb | ||
|
|
c427da72b9 |
@@ -1,8 +1 @@
|
||||
# ignore everything
|
||||
*
|
||||
|
||||
# allow only what we need
|
||||
!commafeed-server/target/commafeed.jar
|
||||
!commafeed-server/config.docker-warmup.yml
|
||||
!commafeed-server/config.yml.example
|
||||
|
||||
commafeed-client
|
||||
7
.github/FUNDING.yml
vendored
7
.github/FUNDING.yml
vendored
@@ -1,2 +1,5 @@
|
||||
github: [athou]
|
||||
custom: ['https://www.paypal.com/donate/?business=9CNQHMJG2ZJVY&no_recurring=0&item_name=CommaFeed¤cy_code=EUR']
|
||||
github: [ athou ]
|
||||
custom: [
|
||||
'https://github.com/sponsors/Athou',
|
||||
'https://www.paypal.com/donate/?business=9CNQHMJG2ZJVY&no_recurring=0&item_name=CommaFeed¤cy_code=EUR'
|
||||
]
|
||||
|
||||
3
.github/ISSUE_TEMPLATE/bug_report.md
vendored
3
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -25,7 +25,8 @@ If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Environment (please complete the following information):**
|
||||
|
||||
- CommaFeed version (or "commafeed.com"): 3.2.1
|
||||
- commafeed.com or self-hosted:
|
||||
- If self-hosted, CommaFeed version [e.g. 5.1.0 (a3dcb2c)]:
|
||||
- Browser [e.g. chrome, firefox]:
|
||||
- Device [e.g. desktop, mobile]:
|
||||
|
||||
|
||||
121
.github/workflows/build.yml
vendored
121
.github/workflows/build.yml
vendored
@@ -1,121 +0,0 @@
|
||||
name: Java CI
|
||||
|
||||
on: [ push ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
java: [ "17", "21" ]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# Setup
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: ${{ matrix.java }}
|
||||
distribution: "temurin"
|
||||
cache: "maven"
|
||||
|
||||
# Build & Test
|
||||
- name: Build with Maven
|
||||
run: mvn --batch-mode --no-transfer-progress install
|
||||
env:
|
||||
TEST_DATABASE: h2
|
||||
|
||||
- name: Run integration tests on PostgreSQL
|
||||
run: mvn --batch-mode --no-transfer-progress failsafe:integration-test failsafe:verify
|
||||
env:
|
||||
TEST_DATABASE: postgresql
|
||||
|
||||
- name: Run integration tests on MySQL
|
||||
run: mvn --batch-mode --no-transfer-progress failsafe:integration-test failsafe:verify
|
||||
env:
|
||||
TEST_DATABASE: mysql
|
||||
|
||||
- name: Run integration tests on MariaDB
|
||||
run: mvn --batch-mode --no-transfer-progress failsafe:integration-test failsafe:verify
|
||||
env:
|
||||
TEST_DATABASE: mariadb
|
||||
|
||||
- name: Run integration tests with Redis cache enabled
|
||||
run: mvn --batch-mode --no-transfer-progress failsafe:integration-test failsafe:verify
|
||||
env:
|
||||
TEST_DATABASE: h2
|
||||
REDIS: true
|
||||
|
||||
# Upload artifacts
|
||||
- name: Upload JAR
|
||||
uses: actions/upload-artifact@v4
|
||||
if: ${{ matrix.java == '17' }}
|
||||
with:
|
||||
name: commafeed.jar
|
||||
path: commafeed-server/target/commafeed.jar
|
||||
|
||||
- name: Upload Playwright artifacts
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: playwright-artifacts
|
||||
path: |
|
||||
**/target/playwright-artifacts/
|
||||
|
||||
# Docker
|
||||
- name: Login to Container Registry
|
||||
uses: docker/login-action@v3
|
||||
if: ${{ matrix.java == '17' && (github.ref_type == 'tag' || github.ref_name == 'master') }}
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Docker build and push tag
|
||||
uses: docker/build-push-action@v6
|
||||
if: ${{ matrix.java == '17' && github.ref_type == 'tag' }}
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
tags: |
|
||||
athou/commafeed:latest
|
||||
athou/commafeed:${{ github.ref_name }}
|
||||
|
||||
- name: Docker build and push master
|
||||
uses: docker/build-push-action@v6
|
||||
if: ${{ matrix.java == '17' && github.ref_name == 'master' }}
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
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 == '17' && github.ref_type == 'tag' }}
|
||||
id: changelog_reader
|
||||
with:
|
||||
version: ${{ github.ref_name }}
|
||||
|
||||
- name: Create GitHub release
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: ${{ matrix.java == '17' && 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
|
||||
183
.github/workflows/ci.yml
vendored
Normal file
183
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,183 @@
|
||||
name: ci
|
||||
|
||||
on: [ push ]
|
||||
|
||||
env:
|
||||
JAVA_VERSION: 21
|
||||
DOCKER_BUILD_SUMMARY: false
|
||||
|
||||
jobs:
|
||||
build-linux:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
database: [ "h2", "postgresql", "mysql", "mariadb" ]
|
||||
|
||||
steps:
|
||||
# Checkout
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# Setup
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Set up GraalVM
|
||||
uses: graalvm/setup-graalvm@v1
|
||||
with:
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
distribution: "graalvm"
|
||||
cache: "maven"
|
||||
|
||||
# Build & Test
|
||||
- name: Build with Maven
|
||||
run: mvn --batch-mode --no-transfer-progress install -Pnative -P${{ matrix.database }}
|
||||
|
||||
# Upload artifacts
|
||||
- name: Upload cross-platform app
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: commafeed-${{ matrix.database }}-jvm
|
||||
path: commafeed-server/target/commafeed-*.zip
|
||||
|
||||
- name: Upload native executable
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: commafeed-${{ matrix.database }}-${{ runner.os }}-${{ runner.arch }}
|
||||
path: commafeed-server/target/commafeed-*-runner
|
||||
|
||||
# Docker
|
||||
- name: Login to Container Registry
|
||||
uses: docker/login-action@v3
|
||||
if: ${{ github.ref_type == 'tag' || github.ref_name == 'master' }}
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
## tags
|
||||
- name: Docker build and push tag - native
|
||||
uses: docker/build-push-action@v6
|
||||
if: ${{ github.ref_type == 'tag' }}
|
||||
with:
|
||||
context: .
|
||||
file: commafeed-server/src/main/docker/Dockerfile.native
|
||||
push: true
|
||||
platforms: linux/amd64
|
||||
tags: |
|
||||
athou/commafeed:latest-${{ matrix.database }}
|
||||
athou/commafeed:${{ github.ref_name }}-${{ matrix.database }}
|
||||
|
||||
- name: Docker build and push tag - jvm
|
||||
uses: docker/build-push-action@v6
|
||||
if: ${{ github.ref_type == 'tag' }}
|
||||
with:
|
||||
context: .
|
||||
file: commafeed-server/src/main/docker/Dockerfile.jvm
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
tags: |
|
||||
athou/commafeed:latest-${{ matrix.database }}-jvm
|
||||
athou/commafeed:${{ github.ref_name }}-${{ matrix.database }}-jvm
|
||||
|
||||
## master
|
||||
- name: Docker build and push master - native
|
||||
uses: docker/build-push-action@v6
|
||||
if: ${{ github.ref_name == 'master' }}
|
||||
with:
|
||||
context: .
|
||||
file: commafeed-server/src/main/docker/Dockerfile.native
|
||||
push: true
|
||||
platforms: linux/amd64
|
||||
tags: athou/commafeed:master-${{ matrix.database }}
|
||||
|
||||
- name: Docker build and push master - jvm
|
||||
uses: docker/build-push-action@v6
|
||||
if: ${{ github.ref_name == 'master' }}
|
||||
with:
|
||||
context: .
|
||||
file: commafeed-server/src/main/docker/Dockerfile.jvm
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
tags: athou/commafeed:master-${{ matrix.database }}-jvm
|
||||
|
||||
build-windows:
|
||||
runs-on: windows-latest
|
||||
strategy:
|
||||
matrix:
|
||||
database: [ "h2", "postgresql", "mysql", "mariadb" ]
|
||||
|
||||
steps:
|
||||
# Checkout
|
||||
- name: Configure git to checkout as-is
|
||||
run: git config --global core.autocrlf false
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# Setup
|
||||
- name: Set up GraalVM
|
||||
uses: graalvm/setup-graalvm@v1
|
||||
with:
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
distribution: "graalvm"
|
||||
cache: "maven"
|
||||
|
||||
# Build & Test
|
||||
- name: Build with Maven
|
||||
run: mvn --batch-mode --no-transfer-progress install -Pnative -P${{ matrix.database }} -DskipTests=${{ matrix.database != 'h2' }}
|
||||
|
||||
# Upload artifacts
|
||||
- name: Upload native executable
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: commafeed-${{ matrix.database }}-${{ runner.os }}-${{ runner.arch }}
|
||||
path: commafeed-server/target/commafeed-*-runner.exe
|
||||
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build-linux
|
||||
- build-windows
|
||||
if: github.ref_type == 'tag'
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: commafeed-*
|
||||
path: ./artifacts
|
||||
merge-multiple: true
|
||||
|
||||
- name: Extract Changelog Entry
|
||||
uses: mindsers/changelog-reader-action@v2
|
||||
id: changelog_reader
|
||||
with:
|
||||
version: ${{ github.ref_name }}
|
||||
|
||||
- name: Create GitHub release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
name: CommaFeed ${{ github.ref_name }}
|
||||
body: ${{ steps.changelog_reader.outputs.changes }}
|
||||
artifacts: ./artifacts/*
|
||||
|
||||
- name: Update Docker Hub Description
|
||||
uses: peter-evans/dockerhub-description@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
repository: athou/commafeed
|
||||
short-description: ${{ github.event.repository.description }}
|
||||
readme-filepath: commafeed-server/src/main/docker/README.md
|
||||
2
.mvn/wrapper/maven-wrapper.properties
vendored
2
.mvn/wrapper/maven-wrapper.properties
vendored
@@ -15,4 +15,4 @@
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
distributionType=only-script
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.8/apache-maven-3.9.8-bin.zip
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
|
||||
|
||||
63
CHANGELOG.md
63
CHANGELOG.md
@@ -1,5 +1,68 @@
|
||||
# Changelog
|
||||
|
||||
## [5.3.1]
|
||||
|
||||
- Fixed an issue that could cause some HTTP feeds to return a 400 error (#1572)
|
||||
|
||||
## [5.3.0]
|
||||
|
||||
- Added a setting to set a cooldown on the "fetch all my feeds" action, disabled by default (#1556)
|
||||
- Fixed an issue that could cause entries to not correctly load when using the "next" header button (#1557)
|
||||
|
||||
## [5.2.0]
|
||||
|
||||
- Added an option to keep a number of entries above the selected entry when scrolling
|
||||
- Added a cache to the HTTP client to reduce the number of requests made to feeds when subscribing (#1431)
|
||||
- Feeds are no longer refreshed between the moment its last user unsubscribes and the moment the feed is cleaned up (every hour)
|
||||
- Fixed an issue that could cause entries to not correctly load when using keyboard navigation (#1557)
|
||||
|
||||
## [5.1.1]
|
||||
|
||||
- Fixed database migration issue when upgrading from 5.0.0 to 5.1.0 on MariaDB (#1544)
|
||||
- When feeds without unread entries are hidden from the tree, the feed is displayed in the tree until another one is selected (#1543)
|
||||
|
||||
## [5.1.0]
|
||||
|
||||
- Added a setting for showing/hiding unread count in the browser's tab title/favicon (#1518)
|
||||
- Fixed an issue that could prevent the app from starting on some systems (#1532)
|
||||
- Added a cache busting filter for the webapp index.html and openapi documentation to make sure they are always up to date
|
||||
- Reduced database cleanup log verbosity
|
||||
|
||||
## [5.0.2]
|
||||
|
||||
- Fix favicon fetching for Youtube channels in native mode when Google auth key is set
|
||||
- Fix an error that appears in the logs when fetching some favicons
|
||||
|
||||
## [5.0.1]
|
||||
|
||||
- Configure native compilation to support older CPU architectures (#1524)
|
||||
|
||||
## [5.0.0]
|
||||
|
||||
CommaFeed is now powered by Quarkus instead of Dropwizard. Read the rationale behind this change in
|
||||
the [announcement](https://github.com/Athou/commafeed/discussions/1517).
|
||||
The gist of it is that CommaFeed can now be compiled to a native binary, resulting in blazing fast startup times (around
|
||||
0.3s) and very low memory footprint (< 50M).
|
||||
|
||||
- CommaFeed now has a different package for each supported database.
|
||||
- If you are deploying CommaFeed with a precompiled package, please
|
||||
read [this section of the README](https://github.com/Athou/commafeed/tree/master?tab=readme-ov-file#download-a-precompiled-package).
|
||||
- If you are building CommaFeed from sources, please
|
||||
read [this section of the README](https://github.com/Athou/commafeed/tree/master?tab=readme-ov-file#build-from-sources).
|
||||
- If you are using the Docker image, please read the instructions on
|
||||
the [Docker Hub page](https://hub.docker.com/r/athou/commafeed).
|
||||
- Due to the switch to Quarkus, the way CommaFeed is configured is very different (the `config.yml` file is gone).
|
||||
Please
|
||||
read [this section of the README](https://github.com/Athou/commafeed/tree/master?tab=readme-ov-file#configuration).
|
||||
Note that a lot of configuration elements have been removed or renamed and are now nested/grouped by feature.
|
||||
- Added a setting to prevent parsing large feeds to avoid out of memory errors. The default is 5MB.
|
||||
- Use a different icon for filtering unread entries and marking an entry as read (#1506)
|
||||
- Added various HTML attributes to ease custom JS/CSS customization (#1507)
|
||||
- The Redis cache has been removed. There have been multiple enhancements to the feed refresh engine and it is no longer
|
||||
needed, even for instances with a large number of feeds.
|
||||
- The H2 migration tool that automatically upgrades H2 databases from format 2 to 3 has been removed. If you're using
|
||||
the H2 embedded database, please upgrade to at least version 4.3.0 before upgrading to CommaFeed 5.0.0.
|
||||
|
||||
## [4.6.0]
|
||||
|
||||
- switched from Temurin to OpenJ9 as the JVM used in the Docker image, resulting in memory usage reduction by up to 50%
|
||||
|
||||
19
Dockerfile
19
Dockerfile
@@ -1,19 +0,0 @@
|
||||
FROM ibm-semeru-runtimes:open-21-jre
|
||||
|
||||
EXPOSE 8082
|
||||
|
||||
RUN mkdir -p /commafeed/data
|
||||
VOLUME /commafeed/data
|
||||
|
||||
RUN apt update && apt install -y wait-for-it && apt clean
|
||||
|
||||
ENV JAVA_TOOL_OPTIONS -Djava.net.preferIPv4Stack=true -Xtune:virtualized -Xminf0.05 -Xmaxf0.1
|
||||
|
||||
COPY commafeed-server/config.docker-warmup.yml .
|
||||
COPY commafeed-server/config.yml.example config.yml
|
||||
COPY commafeed-server/target/commafeed.jar .
|
||||
|
||||
# build openj9 shared classes cache to improve startup time
|
||||
RUN sh -c 'java -Xshareclasses -jar commafeed.jar server config.docker-warmup.yml &' ; wait-for-it -t 600 localhost:8088 -- pkill java ; rm -rf config.warmup.yml
|
||||
|
||||
CMD ["java", "-Xshareclasses", "-jar", "commafeed.jar", "server", "config.yml"]
|
||||
101
README.md
101
README.md
@@ -1,6 +1,6 @@
|
||||
# CommaFeed
|
||||
|
||||
Google Reader inspired self-hosted RSS reader, based on Dropwizard and React/TypeScript.
|
||||
Google Reader inspired self-hosted RSS reader, based on Quarkus and React/TypeScript.
|
||||
|
||||

|
||||
|
||||
@@ -8,14 +8,22 @@ Google Reader inspired self-hosted RSS reader, based on Dropwizard and React/Typ
|
||||
|
||||
- 4 different layouts
|
||||
- Light/Dark theme
|
||||
- Fully responsive
|
||||
- Fully responsive, works great on both mobile and desktop
|
||||
- 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 and a Fever-compatible API for native mobile apps
|
||||
- REST API
|
||||
- Fever-compatible API for native mobile apps
|
||||
- Can automatically mark articles as read based on user-defined rules
|
||||
- [Browser extension](https://github.com/Athou/commafeed-browser-extension)
|
||||
- Compiles to native code for blazing fast startup and low memory usage
|
||||
- Supports 4 databases
|
||||
- H2 (embedded database)
|
||||
- PostgreSQL
|
||||
- MySQL
|
||||
- MariaDB
|
||||
|
||||
## Deployment
|
||||
|
||||
@@ -33,28 +41,80 @@ PikaPods shares 20% of the revenue back to CommaFeed.
|
||||
|
||||
[](https://www.pikapods.com/pods?run=commafeed)
|
||||
|
||||
### Download precompiled package
|
||||
### Download a precompiled package
|
||||
|
||||
mkdir commafeed && cd commafeed
|
||||
wget https://github.com/Athou/commafeed/releases/latest/download/commafeed.jar
|
||||
wget https://github.com/Athou/commafeed/releases/latest/download/config.yml.example -O config.yml
|
||||
java -Djava.net.preferIPv4Stack=true -jar commafeed.jar server config.yml
|
||||
Go to the [release page](https://github.com/Athou/commafeed/releases) and download the latest version for your operating
|
||||
system and database of choice.
|
||||
|
||||
The server will listen on http://localhost:8082. The default
|
||||
user is `admin` and the default password is `admin`.
|
||||
There are two types of packages:
|
||||
|
||||
- The `linux-x86_64` and `windows-x86_64` packages are compiled natively and contain an executable that can be run
|
||||
directly.
|
||||
- The `jvm` package is a zip file containing all `.jar` files required to run the application. This package works on all
|
||||
platforms and is started with `java -jar quarkus-run.jar`.
|
||||
|
||||
If available for your operating system, the native package is recommended because it has a faster startup time and lower
|
||||
memory usage.
|
||||
|
||||
### Build from sources
|
||||
|
||||
git clone https://github.com/Athou/commafeed.git
|
||||
cd commafeed
|
||||
./mvnw clean package
|
||||
cp commafeed-server/config.yml.example config.yml
|
||||
java -Djava.net.preferIPv4Stack=true -jar commafeed-server/target/commafeed.jar server config.yml
|
||||
./mvnw clean package [-P<database>] [-Pnative] [-DskipTests]
|
||||
|
||||
The server will listen on http://localhost:8082. The default
|
||||
user is `admin` and the default password is `admin`.
|
||||
- `<database>` can be one of `h2`, `postgresql`, `mysql` or `mariadb`. The default is `h2`.
|
||||
- `-Pnative` compiles the application to native code. This requires GraalVM to be installed (`GRAALVM_HOME` environment
|
||||
variable pointing to a GraalVM installation).
|
||||
- `-DskipTests` to speed up the build process by skipping tests.
|
||||
|
||||
### Memory management
|
||||
When the build is complete:
|
||||
|
||||
- a zip containing all jars required to run the application is located at
|
||||
`commafeed-server/target/commafeed-<version>-<database>-jvm.zip`. Extract it and run the application with
|
||||
`java -jar quarkus-run.jar`
|
||||
- if you used the native profile, the executable is located at
|
||||
`commafeed-server/target/commafeed-<version>-<database>-<platform>-<arch>-runner[.exe]`
|
||||
|
||||
## Configuration
|
||||
|
||||
CommaFeed doesn't require any configuration to run with its embedded database (H2). The database file will be stored in
|
||||
the `data` directory of the current directory.
|
||||
|
||||
To use a different database, you will need to configure the following properties:
|
||||
|
||||
- `quarkus.datasource.jdbc.url`
|
||||
- e.g. for H2: `jdbc:h2:./data/db;DEFRAG_ALWAYS=TRUE`
|
||||
- e.g. for PostgreSQL: `jdbc:postgresql://localhost:5432/commafeed`
|
||||
- e.g. for MySQL:
|
||||
`jdbc:mysql://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC`
|
||||
- e.g. for MariaDB:
|
||||
`jdbc:mariadb://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC`
|
||||
- `quarkus.datasource.username`
|
||||
- `quarkus.datasource.password`
|
||||
|
||||
There are multiple ways to configure CommaFeed:
|
||||
|
||||
- a `config/application.properties` [properties](https://en.wikipedia.org/wiki/.properties) file relative to the working
|
||||
directory (keys in kebab-case)
|
||||
- Command line arguments prefixed with `-D` (keys in kebab-case)
|
||||
- Environment variables (keys in UPPER_CASE)
|
||||
- a `.env` file in the working directory (keys in UPPER_CASE)
|
||||
|
||||
The properties file is recommended because CommaFeed will be able to warn about invalid properties and typos.
|
||||
|
||||
All [CommaFeed settings](commafeed-server/doc/commafeed.adoc) are optional and have sensible default values.
|
||||
|
||||
When logging in, credentials are stored in an encrypted cookie. The encryption key is randomly generated at startup,
|
||||
meaning that you will have to log back in after each restart of the application. To prevent this, you can set the
|
||||
`quarkus.http.auth.session.encryption-key` property to a fixed value (min. 16 characters).
|
||||
All other Quarkus settings can be found [here](https://quarkus.io/guides/all-config).
|
||||
|
||||
When started, the server will listen on http://localhost:8082.
|
||||
The default user is `admin` and the default password is `admin`.
|
||||
|
||||
### Updates
|
||||
|
||||
When CommaFeed is up and running, you can subscribe to [this feed](https://github.com/Athou/commafeed/releases.atom) to be notified of new releases.
|
||||
|
||||
### Memory management (`jvm` package only)
|
||||
|
||||
The Java Virtual Machine (JVM) is rather greedy by default and will not release unused memory to the
|
||||
operating system. This is because acquiring memory from the operating system is a relatively expensive operation.
|
||||
@@ -84,7 +144,8 @@ slightly slower throughput.
|
||||
|
||||
IBM provides precompiled binaries for OpenJ9
|
||||
named [Semeru](https://developer.ibm.com/languages/java/semeru-runtimes/downloads/).
|
||||
This is the JVM used in the [Docker image](https://github.com/Athou/commafeed/blob/master/Dockerfile).
|
||||
This is the JVM used in
|
||||
the [Docker image](https://github.com/Athou/commafeed/blob/master/commafeed-server/src/main/docker/Dockerfile.jvm).
|
||||
|
||||
## Translation
|
||||
|
||||
@@ -108,7 +169,7 @@ two-letters [ISO-639-1 language code](http://en.wikipedia.org/wiki/List_of_ISO_6
|
||||
|
||||
- Open `commafeed-server` in your preferred Java IDE.
|
||||
- CommaFeed uses Lombok, you need the Lombok plugin for your IDE.
|
||||
- Start `CommaFeedApplication.java` in debug mode with `server config.dev.yml` as arguments
|
||||
- run `./mvnw quarkus:dev`
|
||||
|
||||
### Frontend
|
||||
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
{
|
||||
"locales": [
|
||||
"ar",
|
||||
"ca",
|
||||
"cs",
|
||||
"cy",
|
||||
"da",
|
||||
"de",
|
||||
"en",
|
||||
"es",
|
||||
"fa",
|
||||
"fi",
|
||||
"fr",
|
||||
"gl",
|
||||
"hu",
|
||||
"id",
|
||||
"it",
|
||||
"ja",
|
||||
"ko",
|
||||
"ms",
|
||||
"nb",
|
||||
"nl",
|
||||
"nn",
|
||||
"pl",
|
||||
"pt",
|
||||
"ru",
|
||||
"sk",
|
||||
"sv",
|
||||
"tr",
|
||||
"zh"
|
||||
],
|
||||
"catalogs": [
|
||||
{
|
||||
"path": "src/locales/{locale}/messages",
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"exclude": [
|
||||
"src/locales/**"
|
||||
]
|
||||
}
|
||||
],
|
||||
"format": "po",
|
||||
"formatOptions": {
|
||||
"origins": true,
|
||||
"lineNumbers": false
|
||||
},
|
||||
"sourceLocale": "en",
|
||||
"fallbackLocales": {
|
||||
"default": "en"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.8.3/schema.json",
|
||||
"$schema": "https://biomejs.dev/schemas/1.9.3/schema.json",
|
||||
"formatter": {
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 4,
|
||||
|
||||
52
commafeed-client/lingui.config.ts
Normal file
52
commafeed-client/lingui.config.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import type { LinguiConfig } from "@lingui/conf"
|
||||
|
||||
const config: LinguiConfig = {
|
||||
locales: [
|
||||
"ar",
|
||||
"ca",
|
||||
"cs",
|
||||
"cy",
|
||||
"da",
|
||||
"de",
|
||||
"en",
|
||||
"es",
|
||||
"fa",
|
||||
"fi",
|
||||
"fr",
|
||||
"gl",
|
||||
"hu",
|
||||
"id",
|
||||
"it",
|
||||
"ja",
|
||||
"ko",
|
||||
"ms",
|
||||
"nb",
|
||||
"nl",
|
||||
"nn",
|
||||
"pl",
|
||||
"pt",
|
||||
"ru",
|
||||
"sk",
|
||||
"sv",
|
||||
"tr",
|
||||
"zh",
|
||||
],
|
||||
catalogs: [
|
||||
{
|
||||
path: "src/locales/{locale}/messages",
|
||||
include: ["src"],
|
||||
exclude: ["src/locales/**"],
|
||||
},
|
||||
],
|
||||
format: "po",
|
||||
formatOptions: {
|
||||
origins: true,
|
||||
lineNumbers: false,
|
||||
},
|
||||
sourceLocale: "en",
|
||||
fallbackLocales: {
|
||||
default: "en",
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
||||
2303
commafeed-client/package-lock.json
generated
2303
commafeed-client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -15,24 +15,24 @@
|
||||
"i18n:extract": "lingui extract --clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.11.4",
|
||||
"@fontsource/open-sans": "^5.0.28",
|
||||
"@lingui/core": "^4.11.2",
|
||||
"@lingui/macro": "^4.11.2",
|
||||
"@lingui/react": "^4.11.2",
|
||||
"@mantine/core": "^7.11.2",
|
||||
"@mantine/form": "^7.11.2",
|
||||
"@mantine/hooks": "^7.11.2",
|
||||
"@mantine/modals": "^7.11.2",
|
||||
"@mantine/notifications": "^7.11.2",
|
||||
"@mantine/spotlight": "^7.11.2",
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@fontsource/open-sans": "^5.1.0",
|
||||
"@lingui/core": "^4.11.4",
|
||||
"@lingui/macro": "^4.11.4",
|
||||
"@lingui/react": "^4.11.4",
|
||||
"@mantine/core": "^7.13.2",
|
||||
"@mantine/form": "^7.13.2",
|
||||
"@mantine/hooks": "^7.13.2",
|
||||
"@mantine/modals": "^7.13.2",
|
||||
"@mantine/notifications": "^7.13.2",
|
||||
"@mantine/spotlight": "^7.13.2",
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@reduxjs/toolkit": "^2.2.6",
|
||||
"axios": "^1.7.2",
|
||||
"dayjs": "^1.11.11",
|
||||
"@reduxjs/toolkit": "^2.2.7",
|
||||
"axios": "^1.7.7",
|
||||
"dayjs": "^1.11.13",
|
||||
"escape-string-regexp": "^5.0.0",
|
||||
"interweave": "^13.1.0",
|
||||
"monaco-editor": "^0.50.0",
|
||||
"monaco-editor": "^0.52.0",
|
||||
"mousetrap": "^1.6.5",
|
||||
"react": "^18.3.1",
|
||||
"react-async-hook": "^4.0.0",
|
||||
@@ -42,38 +42,38 @@
|
||||
"react-draggable": "^4.4.6",
|
||||
"react-ga4": "^2.1.0",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-icons": "^5.2.1",
|
||||
"react-icons": "^5.3.0",
|
||||
"react-infinite-scroller": "^1.2.6",
|
||||
"react-redux": "^9.1.2",
|
||||
"react-router-dom": "^6.24.1",
|
||||
"react-router-dom": "^6.26.2",
|
||||
"react-swipeable": "^7.0.1",
|
||||
"redoc": "^2.1.5",
|
||||
"throttle-debounce": "^5.0.2",
|
||||
"tinycon": "^0.6.8",
|
||||
"tss-react": "^4.9.10",
|
||||
"use-local-storage": "^3.0.0",
|
||||
"vite-plugin-biome": "^1.0.12",
|
||||
"tss-react": "^4.9.13",
|
||||
"websocket-heartbeat-js": "^1.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.8.3",
|
||||
"@lingui/cli": "^4.11.2",
|
||||
"@lingui/vite-plugin": "^4.11.2",
|
||||
"@biomejs/biome": "^1.9.3",
|
||||
"@lingui/cli": "^4.11.4",
|
||||
"@lingui/vite-plugin": "^4.11.4",
|
||||
"@types/mousetrap": "^1.6.15",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/react-helmet": "^6.1.11",
|
||||
"@types/react-infinite-scroller": "^1.2.5",
|
||||
"@types/swagger-ui-react": "^4.18.3",
|
||||
"@types/throttle-debounce": "^5.0.2",
|
||||
"@types/tinycon": "^0.6.5",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"@vitejs/plugin-react": "^4.3.2",
|
||||
"babel-plugin-macros": "^3.1.0",
|
||||
"jsdom": "^25.0.1",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"typescript": "^5.5.3",
|
||||
"vite": "^5.3.3",
|
||||
"vite-tsconfig-paths": "^4.3.2",
|
||||
"vitest": "^2.0.2",
|
||||
"vitest-mock-extended": "^1.3.1"
|
||||
"typescript": "^5.6.2",
|
||||
"vite": "^5.4.8",
|
||||
"vite-plugin-checker": "^0.8.0",
|
||||
"vite-tsconfig-paths": "^5.0.1",
|
||||
"vitest": "^2.1.2",
|
||||
"vitest-mock-extended": "^2.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,16 +6,16 @@
|
||||
<parent>
|
||||
<groupId>com.commafeed</groupId>
|
||||
<artifactId>commafeed</artifactId>
|
||||
<version>4.6.0</version>
|
||||
<version>5.3.1</version>
|
||||
</parent>
|
||||
<artifactId>commafeed-client</artifactId>
|
||||
<name>CommaFeed Client</name>
|
||||
|
||||
<properties>
|
||||
<!-- renovate: datasource=node-version depName=node -->
|
||||
<node.version>v20.15.1</node.version>
|
||||
<node.version>v20.18.0</node.version>
|
||||
<!-- renovate: datasource=npm depName=npm -->
|
||||
<npm.version>10.8.2</npm.version>
|
||||
<npm.version>10.9.0</npm.version>
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
@@ -23,7 +23,7 @@
|
||||
<plugin>
|
||||
<groupId>com.github.eirslett</groupId>
|
||||
<artifactId>frontend-maven-plugin</artifactId>
|
||||
<version>1.15.0</version>
|
||||
<version>1.15.1</version>
|
||||
<?m2e ignore?>
|
||||
<executions>
|
||||
<execution>
|
||||
@@ -80,7 +80,7 @@
|
||||
<goal>copy-resources</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<outputDirectory>${project.build.directory}/classes/assets</outputDirectory>
|
||||
<outputDirectory>${project.build.directory}/classes/META-INF/resources</outputDirectory>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>dist</directory>
|
||||
|
||||
@@ -71,7 +71,7 @@ function Providers(props: { children: React.ReactNode }) {
|
||||
)
|
||||
}
|
||||
|
||||
// swagger-ui is very large, load only on-demand
|
||||
// api documentation page is very large, load only on-demand
|
||||
const ApiDocumentationPage = React.lazy(async () => await import("pages/app/ApiDocumentationPage"))
|
||||
|
||||
function AppRoutes() {
|
||||
@@ -142,16 +142,18 @@ function GoogleAnalyticsHandler() {
|
||||
return null
|
||||
}
|
||||
|
||||
function FaviconHandler() {
|
||||
const root = useAppSelector(state => state.tree.rootCategory)
|
||||
function UnreadCountTitleHandler({ unreadCount, enabled }: { unreadCount: number; enabled?: boolean }) {
|
||||
return <Helmet title={enabled && unreadCount > 0 ? `(${unreadCount}) CommaFeed` : "CommaFeed"} />
|
||||
}
|
||||
|
||||
function UnreadCountFaviconHandler({ unreadCount, enabled }: { unreadCount: number; enabled?: boolean }) {
|
||||
useEffect(() => {
|
||||
const unreadCount = categoryUnreadCount(root)
|
||||
if (unreadCount === 0) {
|
||||
Tinycon.reset()
|
||||
} else {
|
||||
if (enabled && unreadCount > 0) {
|
||||
Tinycon.setBubble(unreadCount)
|
||||
} else {
|
||||
Tinycon.reset()
|
||||
}
|
||||
}, [root])
|
||||
}, [unreadCount, enabled])
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -179,8 +181,13 @@ function CustomCode() {
|
||||
|
||||
export function App() {
|
||||
useI18n()
|
||||
const root = useAppSelector(state => state.tree.rootCategory)
|
||||
const unreadCountTitle = useAppSelector(state => state.user.settings?.unreadCountTitle)
|
||||
const unreadCountFavicon = useAppSelector(state => state.user.settings?.unreadCountFavicon)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const unreadCount = categoryUnreadCount(root)
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(reloadServerInfos())
|
||||
}, [dispatch])
|
||||
@@ -188,7 +195,8 @@ export function App() {
|
||||
return (
|
||||
<Providers>
|
||||
<>
|
||||
<FaviconHandler />
|
||||
<UnreadCountTitleHandler unreadCount={unreadCount} enabled={unreadCountTitle} />
|
||||
<UnreadCountFaviconHandler unreadCount={unreadCount} enabled={unreadCountFavicon} />
|
||||
<BrowserExtensionBadgeUnreadCountHandler />
|
||||
<HashRouter>
|
||||
<GoogleAnalyticsHandler />
|
||||
|
||||
@@ -81,7 +81,17 @@ export const client = {
|
||||
},
|
||||
},
|
||||
user: {
|
||||
login: async (req: LoginRequest) => await axiosInstance.post("user/login", req),
|
||||
login: async (req: LoginRequest) => {
|
||||
const formData = new URLSearchParams()
|
||||
formData.append("j_username", req.name)
|
||||
formData.append("j_password", req.password)
|
||||
return await axiosInstance.post("j_security_check", formData, {
|
||||
baseURL: ".",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
})
|
||||
},
|
||||
register: async (req: RegistrationRequest) => await axiosInstance.post("user/register", req),
|
||||
passwordReset: async (req: PasswordResetRequest) => await axiosInstance.post("user/passwordReset", req),
|
||||
getSettings: async () => await axiosInstance.get<Settings>("user/settings"),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { t } from "@lingui/macro"
|
||||
import type { IconType } from "react-icons"
|
||||
import { FaAt } from "react-icons/fa"
|
||||
import { SiBuffer, SiFacebook, SiGmail, SiInstapaper, SiPocket, SiTumblr, SiTwitter } from "react-icons/si"
|
||||
import { SiBuffer, SiFacebook, SiGmail, SiInstapaper, SiPocket, SiTumblr, SiX } from "react-icons/si"
|
||||
import type { Category, Entry, SharingSettings } from "./types"
|
||||
|
||||
const categories: Record<string, Category> = {
|
||||
@@ -50,10 +50,10 @@ const sharing: {
|
||||
url: url => `https://www.facebook.com/sharer/sharer.php?u=${url}`,
|
||||
},
|
||||
twitter: {
|
||||
label: "Twitter",
|
||||
icon: SiTwitter,
|
||||
color: "#1D9BF0",
|
||||
url: (url, desc) => `https://twitter.com/share?text=${desc}&url=${url}`,
|
||||
label: "X",
|
||||
icon: SiX,
|
||||
color: "#000000",
|
||||
url: (url, desc) => `https://x.com/share?text=${desc}&url=${url}`,
|
||||
},
|
||||
tumblr: {
|
||||
label: "Tumblr",
|
||||
|
||||
@@ -166,22 +166,34 @@ export const selectEntry = createAppAsyncThunk(
|
||||
})
|
||||
|
||||
if (arg.scrollToEntry) {
|
||||
const viewMode = state.user.localSettings.viewMode
|
||||
|
||||
const entryIndex = state.entries.entries.indexOf(entry)
|
||||
const entriesToKeepOnTopWhenScrolling =
|
||||
viewMode === "expanded" ? 0 : Math.min(state.user.settings?.entriesToKeepOnTopWhenScrolling ?? 0, entryIndex)
|
||||
const entryToScrollTo = state.entries.entries[entryIndex - entriesToKeepOnTopWhenScrolling]
|
||||
|
||||
const entryElement = document.getElementById(Constants.dom.entryId(entry))
|
||||
if (entryElement) {
|
||||
const entryElementToScrollTo = document.getElementById(Constants.dom.entryId(entryToScrollTo))
|
||||
if (entryElement && entryElementToScrollTo) {
|
||||
const scrollMode = state.user.settings?.scrollMode
|
||||
const entryEntirelyVisible = Constants.layout.isTopVisible(entryElement) && Constants.layout.isBottomVisible(entryElement)
|
||||
const entryEntirelyVisible =
|
||||
Constants.layout.isTopVisible(entryElementToScrollTo) && Constants.layout.isBottomVisible(entryElement)
|
||||
if (scrollMode === "always" || (scrollMode === "if_needed" && !entryEntirelyVisible)) {
|
||||
const scrollSpeed = state.user.settings?.scrollSpeed
|
||||
const margin = viewMode === "detailed" ? 8 : 3
|
||||
thunkApi.dispatch(entriesSlice.actions.setScrollingToEntry(true))
|
||||
scrollToEntry(entryElement, scrollSpeed, () => thunkApi.dispatch(entriesSlice.actions.setScrollingToEntry(false)))
|
||||
scrollToEntry(entryElementToScrollTo, margin, scrollSpeed, () =>
|
||||
thunkApi.dispatch(entriesSlice.actions.setScrollingToEntry(false))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
const scrollToEntry = (entryElement: HTMLElement, scrollSpeed: number | undefined, onScrollEnded: () => void) => {
|
||||
const scrollToEntry = (entryElement: HTMLElement, margin: number, scrollSpeed: number | undefined, onScrollEnded: () => void) => {
|
||||
const header = document.getElementById(Constants.dom.headerId)?.getBoundingClientRect()
|
||||
const offset = (header?.bottom ?? 0) + 3
|
||||
const offset = (header?.bottom ?? 0) + margin
|
||||
scrollToWithCallback({
|
||||
options: {
|
||||
top: entryElement.offsetTop - offset,
|
||||
@@ -218,7 +230,7 @@ export const selectPreviousEntry = createAppAsyncThunk(
|
||||
)
|
||||
export const selectNextEntry = createAppAsyncThunk(
|
||||
"entries/entry/selectNext",
|
||||
(
|
||||
async (
|
||||
arg: {
|
||||
expand: boolean
|
||||
markAsRead: boolean
|
||||
@@ -227,12 +239,20 @@ export const selectNextEntry = createAppAsyncThunk(
|
||||
thunkApi
|
||||
) => {
|
||||
const state = thunkApi.getState()
|
||||
const { entries } = state.entries
|
||||
const { entries, hasMore, loading } = state.entries
|
||||
const nextIndex = entries.findIndex(e => e.id === state.entries.selectedEntryId) + 1
|
||||
if (nextIndex < entries.length) {
|
||||
|
||||
// load more entries if needed
|
||||
// this can happen if the last entry is too large to fit on the screen and the infinite loader doesn't trigger
|
||||
if (nextIndex >= entries.length && hasMore && !loading) {
|
||||
await thunkApi.dispatch(loadMoreEntries())
|
||||
}
|
||||
|
||||
const entriesAfterLoading = thunkApi.getState().entries.entries
|
||||
if (nextIndex < entriesAfterLoading.length) {
|
||||
thunkApi.dispatch(
|
||||
selectEntry({
|
||||
entry: entries[nextIndex],
|
||||
entry: entriesAfterLoading[nextIndex],
|
||||
expand: arg.expand,
|
||||
markAsRead: arg.markAsRead,
|
||||
scrollToEntry: arg.scrollToEntry,
|
||||
|
||||
@@ -3,7 +3,8 @@ import { entriesSlice } from "app/entries/slice"
|
||||
import { redirectSlice } from "app/redirect/slice"
|
||||
import { serverSlice } from "app/server/slice"
|
||||
import { treeSlice } from "app/tree/slice"
|
||||
import { userSlice } from "app/user/slice"
|
||||
import type { LocalSettings } from "app/types"
|
||||
import { initialLocalSettings, userSlice } from "app/user/slice"
|
||||
import { type TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"
|
||||
|
||||
export const reducers = {
|
||||
@@ -14,7 +15,36 @@ export const reducers = {
|
||||
user: userSlice.reducer,
|
||||
}
|
||||
|
||||
export const store = configureStore({ reducer: reducers })
|
||||
const loadLocalSettings = (): LocalSettings => {
|
||||
const json = localStorage.getItem("commafeed-local-settings")
|
||||
if (json) {
|
||||
return JSON.parse(json)
|
||||
}
|
||||
|
||||
// load old settings
|
||||
const viewMode = localStorage.getItem("view-mode")
|
||||
const sidebarWidth = localStorage.getItem("sidebar-width")
|
||||
const announcementHash = localStorage.getItem("announcement-hash")
|
||||
return {
|
||||
...initialLocalSettings,
|
||||
viewMode: viewMode ? JSON.parse(viewMode) : initialLocalSettings.viewMode,
|
||||
sidebarWidth: sidebarWidth ? JSON.parse(sidebarWidth) : initialLocalSettings.sidebarWidth,
|
||||
announcementHash: announcementHash ? JSON.parse(announcementHash) : initialLocalSettings.announcementHash,
|
||||
}
|
||||
}
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: reducers,
|
||||
preloadedState: {
|
||||
user: {
|
||||
localSettings: loadLocalSettings(),
|
||||
},
|
||||
},
|
||||
})
|
||||
store.subscribe(() => {
|
||||
const localSettings = store.getState().user.localSettings
|
||||
localStorage.setItem("commafeed-local-settings", JSON.stringify(localSettings))
|
||||
})
|
||||
|
||||
export type RootState = ReturnType<typeof store.getState>
|
||||
export type AppDispatch = typeof store.dispatch
|
||||
|
||||
@@ -220,6 +220,7 @@ export interface ServerInfo {
|
||||
websocketEnabled: boolean
|
||||
websocketPingInterval: number
|
||||
treeReloadInterval: number
|
||||
forceRefreshCooldownDuration: number
|
||||
}
|
||||
|
||||
export interface SharingSettings {
|
||||
@@ -243,14 +244,23 @@ export interface Settings {
|
||||
customJs?: string
|
||||
scrollSpeed: number
|
||||
scrollMode: ScrollMode
|
||||
entriesToKeepOnTopWhenScrolling: number
|
||||
starIconDisplayMode: IconDisplayMode
|
||||
externalLinkIconDisplayMode: IconDisplayMode
|
||||
markAllAsReadConfirmation: boolean
|
||||
customContextMenu: boolean
|
||||
mobileFooter: boolean
|
||||
unreadCountTitle: boolean
|
||||
unreadCountFavicon: boolean
|
||||
sharingSettings: SharingSettings
|
||||
}
|
||||
|
||||
export interface LocalSettings {
|
||||
viewMode: ViewMode
|
||||
sidebarWidth: number
|
||||
announcementHash: string
|
||||
}
|
||||
|
||||
export interface StarRequest {
|
||||
id: string
|
||||
feedId: number
|
||||
@@ -278,6 +288,7 @@ export interface UserModel {
|
||||
created: number
|
||||
lastLogin?: number
|
||||
admin: boolean
|
||||
lastForceRefresh?: number
|
||||
}
|
||||
|
||||
export interface AdminSaveUserRequest {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { t } from "@lingui/macro"
|
||||
import { showNotification } from "@mantine/notifications"
|
||||
import { createSlice, isAnyOf } from "@reduxjs/toolkit"
|
||||
import type { Settings, UserModel } from "app/types"
|
||||
import { type PayloadAction, createSlice, isAnyOf } from "@reduxjs/toolkit"
|
||||
import type { LocalSettings, Settings, UserModel, ViewMode } from "app/types"
|
||||
import {
|
||||
changeCustomContextMenu,
|
||||
changeEntriesToKeepOnTopWhenScrolling,
|
||||
changeExternalLinkIconDisplayMode,
|
||||
changeLanguage,
|
||||
changeMarkAllAsReadConfirmation,
|
||||
@@ -16,6 +17,8 @@ import {
|
||||
changeSharingSetting,
|
||||
changeShowRead,
|
||||
changeStarIconDisplayMode,
|
||||
changeUnreadCountFavicon,
|
||||
changeUnreadCountTitle,
|
||||
reloadProfile,
|
||||
reloadSettings,
|
||||
reloadTags,
|
||||
@@ -23,16 +26,35 @@ import {
|
||||
|
||||
interface UserState {
|
||||
settings?: Settings
|
||||
localSettings: LocalSettings
|
||||
profile?: UserModel
|
||||
tags?: string[]
|
||||
}
|
||||
|
||||
const initialState: UserState = {}
|
||||
export const initialLocalSettings: LocalSettings = {
|
||||
viewMode: "detailed",
|
||||
sidebarWidth: 360,
|
||||
announcementHash: "no-hash",
|
||||
}
|
||||
|
||||
const initialState: UserState = {
|
||||
localSettings: initialLocalSettings,
|
||||
}
|
||||
|
||||
export const userSlice = createSlice({
|
||||
name: "user",
|
||||
initialState,
|
||||
reducers: {},
|
||||
reducers: {
|
||||
setViewMode: (state, action: PayloadAction<ViewMode>) => {
|
||||
state.localSettings.viewMode = action.payload
|
||||
},
|
||||
setSidebarWidth: (state, action: PayloadAction<number>) => {
|
||||
state.localSettings.sidebarWidth = action.payload
|
||||
},
|
||||
setAnnouncementHash: (state, action: PayloadAction<string>) => {
|
||||
state.localSettings.announcementHash = action.payload
|
||||
},
|
||||
},
|
||||
extraReducers: builder => {
|
||||
builder.addCase(reloadSettings.fulfilled, (state, action) => {
|
||||
state.settings = action.payload
|
||||
@@ -71,6 +93,10 @@ export const userSlice = createSlice({
|
||||
if (!state.settings) return
|
||||
state.settings.scrollMode = action.meta.arg
|
||||
})
|
||||
builder.addCase(changeEntriesToKeepOnTopWhenScrolling.pending, (state, action) => {
|
||||
if (!state.settings) return
|
||||
state.settings.entriesToKeepOnTopWhenScrolling = action.meta.arg
|
||||
})
|
||||
builder.addCase(changeStarIconDisplayMode.pending, (state, action) => {
|
||||
if (!state.settings) return
|
||||
state.settings.starIconDisplayMode = action.meta.arg
|
||||
@@ -91,6 +117,14 @@ export const userSlice = createSlice({
|
||||
if (!state.settings) return
|
||||
state.settings.mobileFooter = action.meta.arg
|
||||
})
|
||||
builder.addCase(changeUnreadCountTitle.pending, (state, action) => {
|
||||
if (!state.settings) return
|
||||
state.settings.unreadCountTitle = action.meta.arg
|
||||
})
|
||||
builder.addCase(changeUnreadCountFavicon.pending, (state, action) => {
|
||||
if (!state.settings) return
|
||||
state.settings.unreadCountFavicon = action.meta.arg
|
||||
})
|
||||
builder.addCase(changeSharingSetting.pending, (state, action) => {
|
||||
if (!state.settings) return
|
||||
state.settings.sharingSettings[action.meta.arg.site] = action.meta.arg.value
|
||||
@@ -102,11 +136,14 @@ export const userSlice = createSlice({
|
||||
changeShowRead.fulfilled,
|
||||
changeScrollMarks.fulfilled,
|
||||
changeScrollMode.fulfilled,
|
||||
changeEntriesToKeepOnTopWhenScrolling.fulfilled,
|
||||
changeStarIconDisplayMode.fulfilled,
|
||||
changeExternalLinkIconDisplayMode.fulfilled,
|
||||
changeMarkAllAsReadConfirmation.fulfilled,
|
||||
changeCustomContextMenu.fulfilled,
|
||||
changeMobileFooter.fulfilled,
|
||||
changeUnreadCountTitle.fulfilled,
|
||||
changeUnreadCountFavicon.fulfilled,
|
||||
changeSharingSetting.fulfilled
|
||||
),
|
||||
() => {
|
||||
@@ -118,3 +155,5 @@ export const userSlice = createSlice({
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export const { setViewMode, setSidebarWidth, setAnnouncementHash } = userSlice.actions
|
||||
|
||||
@@ -43,6 +43,14 @@ export const changeScrollMode = createAppAsyncThunk("settings/scrollMode", (scro
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, scrollMode })
|
||||
})
|
||||
export const changeEntriesToKeepOnTopWhenScrolling = createAppAsyncThunk(
|
||||
"settings/entriesToKeepOnTopWhenScrolling",
|
||||
(entriesToKeepOnTopWhenScrolling: number, thunkApi) => {
|
||||
const { settings } = thunkApi.getState().user
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, entriesToKeepOnTopWhenScrolling })
|
||||
}
|
||||
)
|
||||
export const changeStarIconDisplayMode = createAppAsyncThunk(
|
||||
"settings/starIconDisplayMode",
|
||||
(starIconDisplayMode: IconDisplayMode, thunkApi) => {
|
||||
@@ -77,6 +85,16 @@ export const changeMobileFooter = createAppAsyncThunk("settings/mobileFooter", (
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, mobileFooter })
|
||||
})
|
||||
export const changeUnreadCountTitle = createAppAsyncThunk("settings/unreadCountTitle", (unreadCountTitle: boolean, thunkApi) => {
|
||||
const { settings } = thunkApi.getState().user
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, unreadCountTitle })
|
||||
})
|
||||
export const changeUnreadCountFavicon = createAppAsyncThunk("settings/unreadCountFavicon", (unreadCountFavicon: boolean, thunkApi) => {
|
||||
const { settings } = thunkApi.getState().user
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, unreadCountFavicon })
|
||||
})
|
||||
export const changeSharingSetting = createAppAsyncThunk(
|
||||
"settings/sharingSetting",
|
||||
(
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { MessageDescriptor } from "@lingui/core"
|
||||
import { useLingui } from "@lingui/react"
|
||||
import { ActionIcon, Button, type ButtonVariant, Tooltip, useMantineTheme } from "@mantine/core"
|
||||
import type { ActionIconVariant } from "@mantine/core/lib/components/ActionIcon/ActionIcon"
|
||||
import { Constants } from "app/constants"
|
||||
@@ -7,7 +9,7 @@ import { type MouseEventHandler, type ReactNode, forwardRef } from "react"
|
||||
interface ActionButtonProps {
|
||||
className?: string
|
||||
icon?: ReactNode
|
||||
label: ReactNode
|
||||
label?: string | MessageDescriptor
|
||||
onClick?: MouseEventHandler
|
||||
variant?: ActionIconVariant & ButtonVariant
|
||||
hideLabelOnDesktop?: boolean
|
||||
@@ -20,17 +22,35 @@ interface ActionButtonProps {
|
||||
export const ActionButton = forwardRef<HTMLButtonElement, ActionButtonProps>((props: ActionButtonProps, ref) => {
|
||||
const { mobile } = useActionButton()
|
||||
const theme = useMantineTheme()
|
||||
const { _ } = useLingui()
|
||||
|
||||
const label = typeof props.label === "string" ? props.label : props.label && _(props.label)
|
||||
const variant = props.variant ?? "subtle"
|
||||
const iconOnly = (mobile && !props.showLabelOnMobile) || (!mobile && props.hideLabelOnDesktop)
|
||||
return iconOnly ? (
|
||||
<Tooltip label={props.label} openDelay={Constants.tooltip.delay}>
|
||||
<ActionIcon ref={ref} color={theme.primaryColor} variant={variant} className={props.className} onClick={props.onClick}>
|
||||
<Tooltip label={label} openDelay={Constants.tooltip.delay}>
|
||||
<ActionIcon
|
||||
ref={ref}
|
||||
color={theme.primaryColor}
|
||||
variant={variant}
|
||||
className={props.className}
|
||||
onClick={props.onClick}
|
||||
aria-label={label}
|
||||
>
|
||||
{props.icon}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Button ref={ref} variant={variant} size="xs" className={props.className} leftSection={props.icon} onClick={props.onClick}>
|
||||
{props.label}
|
||||
<Button
|
||||
ref={ref}
|
||||
variant={variant}
|
||||
size="xs"
|
||||
className={props.className}
|
||||
leftSection={props.icon}
|
||||
onClick={props.onClick}
|
||||
aria-label={label}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Trans } from "@lingui/macro"
|
||||
import { Box, Dialog, Text } from "@mantine/core"
|
||||
import { useAppSelector } from "app/store"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import { setAnnouncementHash } from "app/user/slice"
|
||||
import { Content } from "components/content/Content"
|
||||
import { useAsync } from "react-async-hook"
|
||||
import useLocalStorage from "use-local-storage"
|
||||
|
||||
const sha256Hex = async (input: string | undefined) => {
|
||||
const data = new TextEncoder().encode(input)
|
||||
@@ -15,10 +15,11 @@ const sha256Hex = async (input: string | undefined) => {
|
||||
export function AnnouncementDialog() {
|
||||
const announcement = useAppSelector(state => state.server.serverInfos?.announcement)
|
||||
const announcementHash = useAsync(sha256Hex, [announcement]).result
|
||||
const [localStorageHash, setLocalStorageHash] = useLocalStorage("announcement-hash", "no-hash")
|
||||
const existingAnnouncementHash = useAppSelector(state => state.user.localSettings.announcementHash)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const opened = !!announcementHash && announcementHash !== localStorageHash
|
||||
const onClosed = () => setLocalStorageHash(announcementHash)
|
||||
const opened = !!announcementHash && announcementHash !== existingAnnouncementHash
|
||||
const onClosed = () => announcementHash && dispatch(setAnnouncementHash(announcementHash))
|
||||
|
||||
if (!announcement) return null
|
||||
return (
|
||||
|
||||
@@ -150,9 +150,7 @@ export function KeyboardShortcutsHelp() {
|
||||
<Trans>Navigate to a subscription by entering its name</Trans>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Kbd>
|
||||
<Trans>{isMacOS ? "Cmd" : "Ctrl"}</Trans>
|
||||
</Kbd>
|
||||
<Kbd>{isMacOS ? <Trans>Cmd</Trans> : <Trans>Ctrl</Trans>}</Kbd>
|
||||
<span> + </span>
|
||||
<Kbd>K</Kbd>
|
||||
<span>, </span>
|
||||
|
||||
@@ -2,14 +2,10 @@ import { Trans } from "@lingui/macro"
|
||||
import { Tooltip } from "@mantine/core"
|
||||
import { Constants } from "app/constants"
|
||||
import dayjs from "dayjs"
|
||||
import { useEffect, useState } from "react"
|
||||
import { useNow } from "hooks/useNow"
|
||||
|
||||
export function RelativeDate(props: { date: Date | number | undefined }) {
|
||||
const [now, setNow] = useState(new Date())
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => setNow(new Date()), 60 * 1000)
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
const now = useNow(60 * 1000)
|
||||
|
||||
if (!props.date) return <Trans>N/A</Trans>
|
||||
const date = dayjs(props.date)
|
||||
|
||||
@@ -20,7 +20,6 @@ import { KeyboardShortcutsHelp } from "components/KeyboardShortcutsHelp"
|
||||
import { Loader } from "components/Loader"
|
||||
import { useBrowserExtension } from "hooks/useBrowserExtension"
|
||||
import { useMousetrap } from "hooks/useMousetrap"
|
||||
import { useViewMode } from "hooks/useViewMode"
|
||||
import { useEffect } from "react"
|
||||
import { useContextMenu } from "react-contexify"
|
||||
import InfiniteScroll from "react-infinite-scroller"
|
||||
@@ -38,7 +37,7 @@ export function FeedEntries() {
|
||||
const scrollingToEntry = useAppSelector(state => state.entries.scrollingToEntry)
|
||||
const sidebarVisible = useAppSelector(state => state.tree.sidebarVisible)
|
||||
const customContextMenu = useAppSelector(state => state.user.settings?.customContextMenu)
|
||||
const { viewMode } = useViewMode()
|
||||
const viewMode = useAppSelector(state => state.user.localSettings.viewMode)
|
||||
const dispatch = useAppDispatch()
|
||||
const { openLinkInBackgroundTab } = useBrowserExtension()
|
||||
|
||||
@@ -305,11 +304,12 @@ export function FeedEntries() {
|
||||
loader={<Box key={0}>{loading && <Loader />}</Box>}
|
||||
>
|
||||
{entries.map(entry => (
|
||||
<div
|
||||
<article
|
||||
key={entry.id}
|
||||
ref={el => {
|
||||
if (el) el.id = Constants.dom.entryId(entry)
|
||||
}}
|
||||
data-id={entry.id}
|
||||
>
|
||||
<FeedEntry
|
||||
entry={entry}
|
||||
@@ -322,7 +322,7 @@ export function FeedEntries() {
|
||||
onBodyClick={() => bodyClicked(entry)}
|
||||
onSwipedLeft={async () => await swipedLeft(entry)}
|
||||
/>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</InfiniteScroll>
|
||||
)
|
||||
|
||||
@@ -5,7 +5,6 @@ import type { Entry, ViewMode } from "app/types"
|
||||
import { FeedEntryCompactHeader } from "components/content/header/FeedEntryCompactHeader"
|
||||
import { FeedEntryHeader } from "components/content/header/FeedEntryHeader"
|
||||
import { useMobile } from "hooks/useMobile"
|
||||
import { useViewMode } from "hooks/useViewMode"
|
||||
import type React from "react"
|
||||
import { useSwipeable } from "react-swipeable"
|
||||
import { tss } from "tss"
|
||||
@@ -95,7 +94,7 @@ const useStyles = tss
|
||||
})
|
||||
|
||||
export function FeedEntry(props: FeedEntryProps) {
|
||||
const { viewMode } = useViewMode()
|
||||
const viewMode = useAppSelector(state => state.user.localSettings.viewMode)
|
||||
const { classes, cx } = useStyles({
|
||||
read: props.entry.read,
|
||||
expanded: props.expanded,
|
||||
|
||||
@@ -9,7 +9,7 @@ import { truncate } from "app/utils"
|
||||
import { useBrowserExtension } from "hooks/useBrowserExtension"
|
||||
import { useColorScheme } from "hooks/useColorScheme"
|
||||
import { Item, Menu, Separator } from "react-contexify"
|
||||
import { TbArrowBarToDown, TbExternalLink, TbEyeCheck, TbEyeOff, TbRss, TbStar, TbStarOff } from "react-icons/tb"
|
||||
import { TbArrowBarToDown, TbExternalLink, TbMail, TbMailOpened, TbRss, TbStar, TbStarOff } from "react-icons/tb"
|
||||
import { tss } from "tss"
|
||||
|
||||
interface FeedEntryContextMenuProps {
|
||||
@@ -70,7 +70,7 @@ export function FeedEntryContextMenu(props: FeedEntryContextMenuProps) {
|
||||
{props.entry.markable && (
|
||||
<Item onClick={async () => await dispatch(markEntry({ entry: props.entry, read: !props.entry.read }))}>
|
||||
<Group>
|
||||
{props.entry.read ? <TbEyeOff size={iconSize} /> : <TbEyeCheck size={iconSize} />}
|
||||
{props.entry.read ? <TbMail size={iconSize} /> : <TbMailOpened size={iconSize} />}
|
||||
{props.entry.read ? <Trans>Keep unread</Trans> : <Trans>Mark as read</Trans>}
|
||||
</Group>
|
||||
</Item>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Trans, t } from "@lingui/macro"
|
||||
import { msg } from "@lingui/macro"
|
||||
import { useLingui } from "@lingui/react"
|
||||
import { Group, Indicator, Popover, TagsInput } from "@mantine/core"
|
||||
import { markEntriesUpToEntry, markEntry, starEntry, tagEntry } from "app/entries/thunks"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
@@ -6,7 +7,7 @@ import type { Entry } from "app/types"
|
||||
import { ActionButton } from "components/ActionButton"
|
||||
import { useActionButton } from "hooks/useActionButton"
|
||||
import { useMobile } from "hooks/useMobile"
|
||||
import { TbArrowBarToDown, TbExternalLink, TbEyeCheck, TbEyeOff, TbShare, TbStar, TbStarOff, TbTag } from "react-icons/tb"
|
||||
import { TbArrowBarToDown, TbExternalLink, TbMail, TbMailOpened, TbShare, TbStar, TbStarOff, TbTag } from "react-icons/tb"
|
||||
import { ShareButtons } from "./ShareButtons"
|
||||
|
||||
interface FeedEntryFooterProps {
|
||||
@@ -18,6 +19,7 @@ export function FeedEntryFooter(props: FeedEntryFooterProps) {
|
||||
const mobile = useMobile()
|
||||
const { spacing } = useActionButton()
|
||||
const dispatch = useAppDispatch()
|
||||
const { _ } = useLingui()
|
||||
|
||||
const readStatusButtonClicked = async () =>
|
||||
await dispatch(
|
||||
@@ -39,14 +41,14 @@ export function FeedEntryFooter(props: FeedEntryFooterProps) {
|
||||
<Group gap={spacing}>
|
||||
{props.entry.markable && (
|
||||
<ActionButton
|
||||
icon={props.entry.read ? <TbEyeOff size={18} /> : <TbEyeCheck size={18} />}
|
||||
label={props.entry.read ? <Trans>Keep unread</Trans> : <Trans>Mark as read</Trans>}
|
||||
icon={props.entry.read ? <TbMail size={18} /> : <TbMailOpened size={18} />}
|
||||
label={props.entry.read ? msg`Keep unread` : msg`Mark as read`}
|
||||
onClick={readStatusButtonClicked}
|
||||
/>
|
||||
)}
|
||||
<ActionButton
|
||||
icon={props.entry.starred ? <TbStarOff size={18} /> : <TbStar size={18} />}
|
||||
label={props.entry.starred ? <Trans>Unstar</Trans> : <Trans>Star</Trans>}
|
||||
label={props.entry.starred ? msg`Unstar` : msg`Star`}
|
||||
onClick={async () =>
|
||||
await dispatch(
|
||||
starEntry({
|
||||
@@ -59,7 +61,7 @@ export function FeedEntryFooter(props: FeedEntryFooterProps) {
|
||||
|
||||
<Popover withArrow withinPortal shadow="md" closeOnClickOutside={!mobile}>
|
||||
<Popover.Target>
|
||||
<ActionButton icon={<TbShare size={18} />} label={<Trans>Share</Trans>} />
|
||||
<ActionButton icon={<TbShare size={18} />} label={msg`Share`} />
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown>
|
||||
<ShareButtons url={props.entry.url} description={props.entry.title} />
|
||||
@@ -70,12 +72,12 @@ export function FeedEntryFooter(props: FeedEntryFooterProps) {
|
||||
<Popover withArrow shadow="md" closeOnClickOutside={!mobile}>
|
||||
<Popover.Target>
|
||||
<Indicator label={props.entry.tags.length} disabled={props.entry.tags.length === 0} inline size={16}>
|
||||
<ActionButton icon={<TbTag size={18} />} label={<Trans>Tags</Trans>} />
|
||||
<ActionButton icon={<TbTag size={18} />} label={msg`Tags`} />
|
||||
</Indicator>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown>
|
||||
<TagsInput
|
||||
placeholder={t`Tags`}
|
||||
placeholder={_(msg`Tags`)}
|
||||
data={tags}
|
||||
value={props.entry.tags}
|
||||
onChange={onTagsChange}
|
||||
@@ -88,13 +90,13 @@ export function FeedEntryFooter(props: FeedEntryFooterProps) {
|
||||
)}
|
||||
|
||||
<a href={props.entry.url} target="_blank" rel="noreferrer">
|
||||
<ActionButton icon={<TbExternalLink size={18} />} label={<Trans>Open link</Trans>} />
|
||||
<ActionButton icon={<TbExternalLink size={18} />} label={msg`Open link`} />
|
||||
</a>
|
||||
</Group>
|
||||
|
||||
<ActionButton
|
||||
icon={<TbArrowBarToDown size={18} />}
|
||||
label={<Trans>Mark as read up to here</Trans>}
|
||||
label={msg`Mark as read up to here`}
|
||||
onClick={async () => await dispatch(markEntriesUpToEntry(props.entry))}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Trans, t } from "@lingui/macro"
|
||||
import { Trans, msg } from "@lingui/macro"
|
||||
import { useLingui } from "@lingui/react"
|
||||
import { Box, Button, Group, Stack, TextInput } from "@mantine/core"
|
||||
import { useForm } from "@mantine/form"
|
||||
import { client, errorToStrings } from "app/client"
|
||||
@@ -13,6 +14,7 @@ import { CategorySelect } from "./CategorySelect"
|
||||
|
||||
export function AddCategory() {
|
||||
const dispatch = useAppDispatch()
|
||||
const { _ } = useLingui()
|
||||
|
||||
const form = useForm<AddCategoryRequest>()
|
||||
|
||||
@@ -33,7 +35,7 @@ export function AddCategory() {
|
||||
|
||||
<form onSubmit={form.onSubmit(addCategory.execute)}>
|
||||
<Stack>
|
||||
<TextInput label={<Trans>Category</Trans>} placeholder={t`Category`} {...form.getInputProps("name")} required />
|
||||
<TextInput label={<Trans>Category</Trans>} placeholder={_(msg`Category`)} {...form.getInputProps("name")} required />
|
||||
<CategorySelect label={<Trans>Parent</Trans>} {...form.getInputProps("parentId")} clearable />
|
||||
<Group justify="center">
|
||||
<Button variant="default" onClick={async () => await dispatch(redirectToSelectedSource())}>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { t } from "@lingui/macro"
|
||||
import { msg } from "@lingui/macro"
|
||||
import { useLingui } from "@lingui/react"
|
||||
import { Select, type SelectProps } from "@mantine/core"
|
||||
import type { ComboboxItem } from "@mantine/core/lib/components/Combobox/Combobox.types"
|
||||
import { Constants } from "app/constants"
|
||||
@@ -13,6 +14,8 @@ type CategorySelectProps = Partial<SelectProps> & {
|
||||
|
||||
export function CategorySelect(props: CategorySelectProps) {
|
||||
const rootCategory = useAppSelector(state => state.tree.rootCategory)
|
||||
const { _ } = useLingui()
|
||||
|
||||
const categories = rootCategory && flattenCategoryTree(rootCategory)
|
||||
const categoriesById = categories?.reduce((map, c) => {
|
||||
map.set(c.id, c)
|
||||
@@ -43,7 +46,7 @@ export function CategorySelect(props: CategorySelectProps) {
|
||||
.sort((c1, c2) => c1.label.localeCompare(c2.label))
|
||||
if (props.withAll) {
|
||||
selectData?.unshift({
|
||||
label: t`All`,
|
||||
label: _(msg`All`),
|
||||
value: Constants.categories.all.id,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Trans, t } from "@lingui/macro"
|
||||
import { Trans, msg } from "@lingui/macro"
|
||||
import { useLingui } from "@lingui/react"
|
||||
import { Box, Button, FileInput, Group, Stack } from "@mantine/core"
|
||||
import { isNotEmpty, useForm } from "@mantine/form"
|
||||
import { client, errorToStrings } from "app/client"
|
||||
@@ -11,10 +12,11 @@ import { TbFileImport } from "react-icons/tb"
|
||||
|
||||
export function ImportOpml() {
|
||||
const dispatch = useAppDispatch()
|
||||
const { _ } = useLingui()
|
||||
|
||||
const form = useForm<{ file: File }>({
|
||||
validate: {
|
||||
file: isNotEmpty(t`OPML file is required`),
|
||||
file: isNotEmpty(_(msg`OPML file is required`)),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -38,7 +40,7 @@ export function ImportOpml() {
|
||||
<FileInput
|
||||
label={<Trans>OPML file</Trans>}
|
||||
leftSection={<TbFileImport />}
|
||||
placeholder={t`OPML file`}
|
||||
placeholder={_(msg`OPML file`)}
|
||||
description={
|
||||
<Trans>
|
||||
An opml file is an XML file containing feed URLs and categories. You can get an OPML file by exporting your
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Trans, t } from "@lingui/macro"
|
||||
import { msg } from "@lingui/macro"
|
||||
import { useLingui } from "@lingui/react"
|
||||
import { Box, Center, CloseButton, Divider, Group, Indicator, Popover, TextInput } from "@mantine/core"
|
||||
import { useForm } from "@mantine/form"
|
||||
import { reloadEntries, search, selectNextEntry, selectPreviousEntry } from "app/entries/thunks"
|
||||
@@ -57,10 +58,11 @@ export function Header() {
|
||||
const searchFromStore = useAppSelector(state => state.entries.search)
|
||||
const { isBrowserExtensionPopup, openSettingsPage, openAppInNewTab } = useBrowserExtension()
|
||||
const dispatch = useAppDispatch()
|
||||
const { _ } = useLingui()
|
||||
|
||||
const searchForm = useForm<{ search: string }>({
|
||||
validate: {
|
||||
search: value => (value.length > 0 && value.length < 3 ? t`Search requires at least 3 characters` : null),
|
||||
search: value => (value.length > 0 && value.length < 3 ? _(msg`Search requires at least 3 characters`) : null),
|
||||
},
|
||||
})
|
||||
const { setValues } = searchForm
|
||||
@@ -77,7 +79,7 @@ export function Header() {
|
||||
<HeaderToolbar>
|
||||
<ActionButton
|
||||
icon={<TbArrowUp size={iconSize} />}
|
||||
label={<Trans>Previous</Trans>}
|
||||
label={msg`Previous`}
|
||||
onClick={async () =>
|
||||
await dispatch(
|
||||
selectPreviousEntry({
|
||||
@@ -90,7 +92,7 @@ export function Header() {
|
||||
/>
|
||||
<ActionButton
|
||||
icon={<TbArrowDown size={iconSize} />}
|
||||
label={<Trans>Next</Trans>}
|
||||
label={msg`Next`}
|
||||
onClick={async () =>
|
||||
await dispatch(
|
||||
selectNextEntry({
|
||||
@@ -106,7 +108,7 @@ export function Header() {
|
||||
|
||||
<ActionButton
|
||||
icon={<TbRefresh size={iconSize} />}
|
||||
label={<Trans>Refresh</Trans>}
|
||||
label={msg`Refresh`}
|
||||
onClick={async () => await dispatch(reloadEntries())}
|
||||
/>
|
||||
<MarkAllAsReadButton iconSize={iconSize} />
|
||||
@@ -115,25 +117,25 @@ export function Header() {
|
||||
|
||||
<ActionButton
|
||||
icon={settings.readingMode === "all" ? <TbEye size={iconSize} /> : <TbEyeOff size={iconSize} />}
|
||||
label={settings.readingMode === "all" ? <Trans>All</Trans> : <Trans>Unread</Trans>}
|
||||
label={settings.readingMode === "all" ? msg`All` : msg`Unread`}
|
||||
onClick={async () => await dispatch(changeReadingMode(settings.readingMode === "all" ? "unread" : "all"))}
|
||||
/>
|
||||
<ActionButton
|
||||
icon={settings.readingOrder === "asc" ? <TbSortAscending size={iconSize} /> : <TbSortDescending size={iconSize} />}
|
||||
label={settings.readingOrder === "asc" ? <Trans>Asc</Trans> : <Trans>Desc</Trans>}
|
||||
label={settings.readingOrder === "asc" ? msg`Asc` : msg`Desc`}
|
||||
onClick={async () => await dispatch(changeReadingOrder(settings.readingOrder === "asc" ? "desc" : "asc"))}
|
||||
/>
|
||||
|
||||
<Popover>
|
||||
<Popover.Target>
|
||||
<Indicator disabled={!searchFromStore}>
|
||||
<ActionButton icon={<TbSearch size={iconSize} />} label={<Trans>Search</Trans>} />
|
||||
<ActionButton icon={<TbSearch size={iconSize} />} label={msg`Search`} />
|
||||
</Indicator>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown>
|
||||
<form onSubmit={searchForm.onSubmit(async values => await dispatch(search(values.search)))}>
|
||||
<TextInput
|
||||
placeholder={t`Search`}
|
||||
placeholder={_(msg`Search`)}
|
||||
{...searchForm.getInputProps("search")}
|
||||
leftSection={<TbSearch size={iconSize} />}
|
||||
rightSection={<CloseButton onClick={async () => await (searchFromStore && dispatch(search("")))} />}
|
||||
@@ -153,12 +155,12 @@ export function Header() {
|
||||
|
||||
<ActionButton
|
||||
icon={<TbSettings size={iconSize} />}
|
||||
label={<Trans>Extension options</Trans>}
|
||||
label={msg`Extension options`}
|
||||
onClick={() => openSettingsPage()}
|
||||
/>
|
||||
<ActionButton
|
||||
icon={<TbExternalLink size={iconSize} />}
|
||||
label={<Trans>Open CommaFeed</Trans>}
|
||||
label={msg`Open CommaFeed`}
|
||||
onClick={() => openAppInNewTab()}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Trans } from "@lingui/macro"
|
||||
import { Trans, msg } from "@lingui/macro"
|
||||
|
||||
import { Button, Code, Group, Modal, Slider, Stack, Text } from "@mantine/core"
|
||||
import { markAllEntries } from "app/entries/thunks"
|
||||
@@ -91,7 +91,7 @@ export function MarkAllAsReadButton(props: { iconSize: number }) {
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
<ActionButton icon={<TbChecks size={props.iconSize} />} label={<Trans>Mark all as read</Trans>} onClick={buttonClicked} />
|
||||
<ActionButton icon={<TbChecks size={props.iconSize} />} label={msg`Mark all as read`} onClick={buttonClicked} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,10 @@ import { client } from "app/client"
|
||||
import { redirectToAbout, redirectToAdminUsers, redirectToDonate, redirectToMetrics, redirectToSettings } from "app/redirect/thunks"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import type { ViewMode } from "app/types"
|
||||
import { useViewMode } from "hooks/useViewMode"
|
||||
import { setViewMode } from "app/user/slice"
|
||||
import { reloadProfile } from "app/user/thunks"
|
||||
import dayjs from "dayjs"
|
||||
import { useNow } from "hooks/useNow"
|
||||
import { type ReactNode, useState } from "react"
|
||||
import {
|
||||
TbChartLine,
|
||||
@@ -92,12 +95,19 @@ const viewModeData: ViewModeControlItem[] = [
|
||||
|
||||
export function ProfileMenu(props: ProfileMenuProps) {
|
||||
const [opened, setOpened] = useState(false)
|
||||
const { viewMode, setViewMode } = useViewMode()
|
||||
const now = useNow()
|
||||
const profile = useAppSelector(state => state.user.profile)
|
||||
const admin = useAppSelector(state => state.user.profile?.admin)
|
||||
const viewMode = useAppSelector(state => state.user.localSettings.viewMode)
|
||||
const forceRefreshCooldownDuration = useAppSelector(state => state.server.serverInfos?.forceRefreshCooldownDuration)
|
||||
const dispatch = useAppDispatch()
|
||||
const { colorScheme, setColorScheme } = useMantineColorScheme()
|
||||
|
||||
const nextAvailableForceRefresh = profile?.lastForceRefresh
|
||||
? profile.lastForceRefresh + (forceRefreshCooldownDuration ?? 0)
|
||||
: now.getTime()
|
||||
const forceRefreshEnabled = nextAvailableForceRefresh <= now.getTime()
|
||||
|
||||
const logout = () => {
|
||||
window.location.href = "logout"
|
||||
}
|
||||
@@ -118,18 +128,32 @@ export function ProfileMenu(props: ProfileMenuProps) {
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
leftSection={<TbWorldDownload size={iconSize} />}
|
||||
onClick={async () =>
|
||||
await client.feed.refreshAll().then(() => {
|
||||
disabled={!forceRefreshEnabled}
|
||||
onClick={async () => {
|
||||
setOpened(false)
|
||||
|
||||
try {
|
||||
await client.feed.refreshAll()
|
||||
|
||||
// reload profile to update last force refresh timestamp
|
||||
await dispatch(reloadProfile())
|
||||
|
||||
showNotification({
|
||||
message: <Trans>Your feeds have been queued for refresh.</Trans>,
|
||||
color: "green",
|
||||
autoClose: 1000,
|
||||
})
|
||||
setOpened(false)
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
showNotification({
|
||||
message: <Trans>Force fetching feeds is not yet available.</Trans>,
|
||||
color: "red",
|
||||
autoClose: 2000,
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Trans>Fetch all my feeds now</Trans>
|
||||
{!forceRefreshEnabled && <span> ({dayjs.duration(nextAvailableForceRefresh - now.getTime()).format("HH:mm:ss")})</span>}
|
||||
</Menu.Item>
|
||||
|
||||
<Divider />
|
||||
@@ -156,7 +180,7 @@ export function ProfileMenu(props: ProfileMenuProps) {
|
||||
orientation="vertical"
|
||||
data={viewModeData}
|
||||
value={viewMode}
|
||||
onChange={e => setViewMode(e as ViewMode)}
|
||||
onChange={e => dispatch(setViewMode(e as ViewMode))}
|
||||
mb="xs"
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { NumberFormatter } from "@mantine/core"
|
||||
import type { MetricGauge } from "app/types"
|
||||
|
||||
interface MeterProps {
|
||||
@@ -5,5 +6,5 @@ interface MeterProps {
|
||||
}
|
||||
|
||||
export function Gauge(props: MeterProps) {
|
||||
return <span>{props.gauge.value}</span>
|
||||
return <NumberFormatter value={props.gauge.value} thousandSeparator />
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { Trans, t } from "@lingui/macro"
|
||||
import { Divider, Group, Radio, Select, SimpleGrid, Stack, Switch } from "@mantine/core"
|
||||
import { Trans, msg } from "@lingui/macro"
|
||||
import { useLingui } from "@lingui/react"
|
||||
import { Divider, Group, NumberInput, Radio, Select, SimpleGrid, Stack, Switch } from "@mantine/core"
|
||||
import type { ComboboxData } from "@mantine/core/lib/components/Combobox/Combobox.types"
|
||||
import { Constants } from "app/constants"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import type { IconDisplayMode, ScrollMode, SharingSettings } from "app/types"
|
||||
import {
|
||||
changeCustomContextMenu,
|
||||
changeEntriesToKeepOnTopWhenScrolling,
|
||||
changeExternalLinkIconDisplayMode,
|
||||
changeLanguage,
|
||||
changeMarkAllAsReadConfirmation,
|
||||
@@ -16,6 +18,8 @@ import {
|
||||
changeSharingSetting,
|
||||
changeShowRead,
|
||||
changeStarIconDisplayMode,
|
||||
changeUnreadCountFavicon,
|
||||
changeUnreadCountTitle,
|
||||
} from "app/user/thunks"
|
||||
import { locales } from "i18n"
|
||||
import type { ReactNode } from "react"
|
||||
@@ -26,13 +30,17 @@ export function DisplaySettings() {
|
||||
const showRead = useAppSelector(state => state.user.settings?.showRead)
|
||||
const scrollMarks = useAppSelector(state => state.user.settings?.scrollMarks)
|
||||
const scrollMode = useAppSelector(state => state.user.settings?.scrollMode)
|
||||
const entriesToKeepOnTop = useAppSelector(state => state.user.settings?.entriesToKeepOnTopWhenScrolling)
|
||||
const starIconDisplayMode = useAppSelector(state => state.user.settings?.starIconDisplayMode)
|
||||
const externalLinkIconDisplayMode = useAppSelector(state => state.user.settings?.externalLinkIconDisplayMode)
|
||||
const markAllAsReadConfirmation = useAppSelector(state => state.user.settings?.markAllAsReadConfirmation)
|
||||
const customContextMenu = useAppSelector(state => state.user.settings?.customContextMenu)
|
||||
const mobileFooter = useAppSelector(state => state.user.settings?.mobileFooter)
|
||||
const unreadCountTitle = useAppSelector(state => state.user.settings?.unreadCountTitle)
|
||||
const unreadCountFavicon = useAppSelector(state => state.user.settings?.unreadCountFavicon)
|
||||
const sharingSettings = useAppSelector(state => state.user.settings?.sharingSettings)
|
||||
const dispatch = useAppDispatch()
|
||||
const { _ } = useLingui()
|
||||
|
||||
const scrollModeOptions: Record<ScrollMode, ReactNode> = {
|
||||
always: <Trans>Always</Trans>,
|
||||
@@ -43,19 +51,19 @@ export function DisplaySettings() {
|
||||
const displayModeData: ComboboxData = [
|
||||
{
|
||||
value: "always",
|
||||
label: t`Always`,
|
||||
label: _(msg`Always`),
|
||||
},
|
||||
{
|
||||
value: "on_desktop",
|
||||
label: t`On desktop`,
|
||||
label: _(msg`On desktop`),
|
||||
},
|
||||
{
|
||||
value: "on_mobile",
|
||||
label: t`On mobile`,
|
||||
label: _(msg`On mobile`),
|
||||
},
|
||||
{
|
||||
value: "never",
|
||||
label: t`Never`,
|
||||
label: _(msg`Never`),
|
||||
},
|
||||
]
|
||||
|
||||
@@ -89,6 +97,20 @@ export function DisplaySettings() {
|
||||
onChange={async e => await dispatch(changeMobileFooter(e.currentTarget.checked))}
|
||||
/>
|
||||
|
||||
<Divider label={<Trans>Browser tab</Trans>} labelPosition="center" />
|
||||
|
||||
<Switch
|
||||
label={<Trans>Show unread count in tab title</Trans>}
|
||||
checked={unreadCountTitle}
|
||||
onChange={async e => await dispatch(changeUnreadCountTitle(e.currentTarget.checked))}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label={<Trans>Show unread count in tab favicon</Trans>}
|
||||
checked={unreadCountFavicon}
|
||||
onChange={async e => await dispatch(changeUnreadCountFavicon(e.currentTarget.checked))}
|
||||
/>
|
||||
|
||||
<Divider label={<Trans>Entry headers</Trans>} labelPosition="center" />
|
||||
|
||||
<Select
|
||||
@@ -125,6 +147,14 @@ export function DisplaySettings() {
|
||||
</Group>
|
||||
</Radio.Group>
|
||||
|
||||
<NumberInput
|
||||
label={<Trans>Entries to keep above the selected entry when scrolling</Trans>}
|
||||
description={<Trans>Only applies to compact, cozy and detailed modes</Trans>}
|
||||
min={0}
|
||||
value={entriesToKeepOnTop}
|
||||
onChange={async value => await dispatch(changeEntriesToKeepOnTopWhenScrolling(+value))}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label={<Trans>Scroll smoothly when navigating between entries</Trans>}
|
||||
checked={scrollSpeed ? scrollSpeed > 0 : false}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Trans, t } from "@lingui/macro"
|
||||
import { Trans, msg } from "@lingui/macro"
|
||||
import { useLingui } from "@lingui/react"
|
||||
import { Anchor, Box, Button, Checkbox, Divider, Group, Input, PasswordInput, Stack, Text, TextInput } from "@mantine/core"
|
||||
import { useForm } from "@mantine/form"
|
||||
import { openConfirmModal } from "@mantine/modals"
|
||||
@@ -19,10 +20,11 @@ interface FormData extends ProfileModificationRequest {
|
||||
export function ProfileSettings() {
|
||||
const profile = useAppSelector(state => state.user.profile)
|
||||
const dispatch = useAppDispatch()
|
||||
const { _ } = useLingui()
|
||||
|
||||
const form = useForm<FormData>({
|
||||
validate: {
|
||||
newPasswordConfirmation: (value, values) => (value !== values.newPassword ? t`Passwords do not match` : null),
|
||||
newPasswordConfirmation: (value, values) => (value !== values.newPassword ? _(msg`Passwords do not match`) : null),
|
||||
},
|
||||
})
|
||||
const { setValues } = form
|
||||
|
||||
@@ -35,6 +35,21 @@ export function Tree() {
|
||||
const showRead = useAppSelector(state => state.user.settings?.showRead)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const isFeedDisplayed = (feed: Subscription) => {
|
||||
const isCurrentFeed = source.type === "feed" && source.id === String(feed.id)
|
||||
return isCurrentFeed || feed.unread > 0 || showRead
|
||||
}
|
||||
|
||||
const isCategoryDisplayed = (category: Category): boolean => {
|
||||
const isCurrentCategory = source.type === "category" && source.id === category.id
|
||||
return (
|
||||
isCurrentCategory ||
|
||||
showRead ||
|
||||
category.children.some(c => isCategoryDisplayed(c)) ||
|
||||
category.feeds.some(f => isFeedDisplayed(f))
|
||||
)
|
||||
}
|
||||
|
||||
const feedClicked = (e: React.MouseEvent, id: string) => {
|
||||
if (e.detail === 2) {
|
||||
dispatch(redirectToFeedDetails(id))
|
||||
@@ -70,6 +85,7 @@ export function Tree() {
|
||||
const allCategoryNode = () => (
|
||||
<TreeNode
|
||||
id={Constants.categories.all.id}
|
||||
type="category"
|
||||
name={<Trans>All</Trans>}
|
||||
icon={allIcon}
|
||||
unread={categoryUnreadCount(root)}
|
||||
@@ -83,6 +99,7 @@ export function Tree() {
|
||||
const starredCategoryNode = () => (
|
||||
<TreeNode
|
||||
id={Constants.categories.starred.id}
|
||||
type="category"
|
||||
name={<Trans>Starred</Trans>}
|
||||
icon={starredIcon}
|
||||
unread={0}
|
||||
@@ -95,16 +112,16 @@ export function Tree() {
|
||||
)
|
||||
|
||||
const categoryNode = (category: Category, level = 0) => {
|
||||
const unreadCount = categoryUnreadCount(category)
|
||||
if (unreadCount === 0 && !showRead) return null
|
||||
if (!isCategoryDisplayed(category)) return null
|
||||
|
||||
const hasError = !category.expanded && flattenCategoryTree(category).some(c => c.feeds.some(f => f.errorCount > errorThreshold))
|
||||
return (
|
||||
<TreeNode
|
||||
id={category.id}
|
||||
type="category"
|
||||
name={category.name}
|
||||
icon={category.expanded ? expandedIcon : collapsedIcon}
|
||||
unread={unreadCount}
|
||||
unread={categoryUnreadCount(category)}
|
||||
selected={source.type === "category" && source.id === category.id}
|
||||
expanded={category.expanded}
|
||||
level={level}
|
||||
@@ -117,11 +134,12 @@ export function Tree() {
|
||||
}
|
||||
|
||||
const feedNode = (feed: Subscription, level = 0) => {
|
||||
if (feed.unread === 0 && !showRead) return null
|
||||
if (!isFeedDisplayed(feed)) return null
|
||||
|
||||
return (
|
||||
<TreeNode
|
||||
id={String(feed.id)}
|
||||
type="feed"
|
||||
name={feed.name}
|
||||
icon={feed.iconUrl}
|
||||
unread={feed.unread}
|
||||
@@ -137,6 +155,7 @@ export function Tree() {
|
||||
const tagNode = (tag: string) => (
|
||||
<TreeNode
|
||||
id={tag}
|
||||
type="tag"
|
||||
name={tag}
|
||||
icon={tagIcon}
|
||||
unread={0}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Box, Center } from "@mantine/core"
|
||||
import type { EntrySourceType } from "app/entries/slice"
|
||||
import { FeedFavicon } from "components/content/FeedFavicon"
|
||||
import type React from "react"
|
||||
import { tss } from "tss"
|
||||
@@ -6,6 +7,7 @@ import { UnreadCount } from "./UnreadCount"
|
||||
|
||||
interface TreeNodeProps {
|
||||
id: string
|
||||
type: EntrySourceType
|
||||
name: React.ReactNode
|
||||
icon: React.ReactNode
|
||||
unread: number
|
||||
@@ -63,7 +65,15 @@ export function TreeNode(props: TreeNodeProps) {
|
||||
hasUnread: props.unread > 0,
|
||||
})
|
||||
return (
|
||||
<Box py={1} pl={props.level * 20} className={classes.node} onClick={(e: React.MouseEvent) => props.onClick(e, props.id)}>
|
||||
<Box
|
||||
py={1}
|
||||
pl={props.level * 20}
|
||||
className={classes.node}
|
||||
onClick={(e: React.MouseEvent) => props.onClick(e, props.id)}
|
||||
data-id={props.id}
|
||||
data-type={props.type}
|
||||
data-unread-count={props.unread}
|
||||
>
|
||||
<Box mr={6} onClick={(e: React.MouseEvent) => props.onIconClick?.(e, props.id)}>
|
||||
<Center>{typeof props.icon === "string" ? <FeedFavicon url={props.icon} /> : props.icon}</Center>
|
||||
</Box>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Trans, t } from "@lingui/macro"
|
||||
import { Box, Center, Kbd, TextInput } from "@mantine/core"
|
||||
import { useOs } from "@mantine/hooks"
|
||||
import { Trans, msg } from "@lingui/macro"
|
||||
import { useLingui } from "@lingui/react"
|
||||
import { TextInput } from "@mantine/core"
|
||||
import { Spotlight, type SpotlightActionData, spotlight } from "@mantine/spotlight"
|
||||
import { redirectToFeed } from "app/redirect/thunks"
|
||||
import { useAppDispatch } from "app/store"
|
||||
@@ -15,7 +15,8 @@ export interface TreeSearchProps {
|
||||
|
||||
export function TreeSearch(props: TreeSearchProps) {
|
||||
const dispatch = useAppDispatch()
|
||||
const isMacOS = useOs() === "macos"
|
||||
const { _ } = useLingui()
|
||||
|
||||
const actions: SpotlightActionData[] = props.feeds
|
||||
.map(f => ({
|
||||
id: `${f.id}`,
|
||||
@@ -26,13 +27,6 @@ export function TreeSearch(props: TreeSearchProps) {
|
||||
.sort((f1, f2) => f1.label.localeCompare(f2.label))
|
||||
|
||||
const searchIcon = <TbSearch size={18} />
|
||||
const rightSection = (
|
||||
<Center style={{ cursor: "pointer" }} onClick={() => spotlight.open()}>
|
||||
<Kbd>{isMacOS ? "Cmd" : "Ctrl"}</Kbd>
|
||||
<Box mx={5}>+</Box>
|
||||
<Kbd>K</Kbd>
|
||||
</Center>
|
||||
)
|
||||
|
||||
// additional keyboard shortcut used by commafeed v1
|
||||
useMousetrap("g u", () => spotlight.open())
|
||||
@@ -40,10 +34,9 @@ export function TreeSearch(props: TreeSearchProps) {
|
||||
return (
|
||||
<>
|
||||
<TextInput
|
||||
placeholder={t`Search`}
|
||||
placeholder={_(msg`Search`)}
|
||||
leftSection={searchIcon}
|
||||
rightSectionWidth={100}
|
||||
rightSection={rightSection}
|
||||
styles={{
|
||||
input: {
|
||||
cursor: "pointer",
|
||||
@@ -60,7 +53,7 @@ export function TreeSearch(props: TreeSearchProps) {
|
||||
shortcut="mod+k"
|
||||
searchProps={{
|
||||
leftSection: searchIcon,
|
||||
placeholder: t`Search`,
|
||||
placeholder: _(msg`Search`),
|
||||
}}
|
||||
nothingFound={<Trans>Nothing found</Trans>}
|
||||
/>
|
||||
|
||||
@@ -18,7 +18,7 @@ export function UnreadCount(props: { unreadCount: number }) {
|
||||
const count = props.unreadCount >= 10000 ? "10k+" : props.unreadCount
|
||||
return (
|
||||
<Tooltip label={props.unreadCount} disabled={props.unreadCount === count} openDelay={Constants.tooltip.delay}>
|
||||
<Badge className={classes.badge} variant="light">
|
||||
<Badge className={classes.badge} variant="light" fullWidth>
|
||||
{count}
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { t } from "@lingui/macro"
|
||||
import { msg } from "@lingui/macro"
|
||||
import { useLingui } from "@lingui/react"
|
||||
import { useAppSelector } from "app/store"
|
||||
|
||||
interface Step {
|
||||
@@ -11,22 +12,23 @@ export const useAppLoading = () => {
|
||||
const settings = useAppSelector(state => state.user.settings)
|
||||
const rootCategory = useAppSelector(state => state.tree.rootCategory)
|
||||
const tags = useAppSelector(state => state.user.tags)
|
||||
const { _ } = useLingui()
|
||||
|
||||
const steps: Step[] = [
|
||||
{
|
||||
label: t`Loading settings...`,
|
||||
label: _(msg`Loading settings...`),
|
||||
done: !!settings,
|
||||
},
|
||||
{
|
||||
label: t`Loading profile...`,
|
||||
label: _(msg`Loading profile...`),
|
||||
done: !!profile,
|
||||
},
|
||||
{
|
||||
label: t`Loading subscriptions...`,
|
||||
label: _(msg`Loading subscriptions...`),
|
||||
done: !!rootCategory,
|
||||
},
|
||||
{
|
||||
label: t`Loading tags...`,
|
||||
label: _(msg`Loading tags...`),
|
||||
done: !!tags,
|
||||
},
|
||||
]
|
||||
|
||||
10
commafeed-client/src/hooks/useNow.ts
Normal file
10
commafeed-client/src/hooks/useNow.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
export const useNow = (interval = 1000): Date => {
|
||||
const [time, setTime] = useState(new Date())
|
||||
useEffect(() => {
|
||||
const t = setInterval(() => setTime(new Date()), interval)
|
||||
return () => clearInterval(t)
|
||||
}, [interval])
|
||||
return time
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import type { ViewMode } from "app/types"
|
||||
import useLocalStorage from "use-local-storage"
|
||||
|
||||
export function useViewMode() {
|
||||
const [viewMode, setViewMode] = useLocalStorage<ViewMode>("view-mode", "detailed")
|
||||
return { viewMode, setViewMode }
|
||||
}
|
||||
@@ -10,7 +10,7 @@ interface Locale {
|
||||
}
|
||||
|
||||
// add an object to the array to add a new locale
|
||||
// don't forget to also add it to the 'locales' array in .linguirc
|
||||
// don't forget to also add it to the 'locales' array in lingui.config.ts
|
||||
export const locales: Locale[] = [
|
||||
{ key: "ar", label: "العربية", dayjsImportFn: async () => await import("dayjs/locale/ar") },
|
||||
{ key: "ca", label: "Català", dayjsImportFn: async () => await import("dayjs/locale/ca") },
|
||||
|
||||
@@ -140,6 +140,10 @@ msgstr ""
|
||||
msgid "Browser extention"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Browser tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -176,6 +180,10 @@ msgstr "تأكد من عمل الخلاصة"
|
||||
msgid "Close menu"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Cmd"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
@@ -313,6 +321,10 @@ msgstr "دخول"
|
||||
msgid "Enter your current password to change profile settings"
|
||||
msgstr "أدخل كلمة المرور الحالية لتغيير إعدادات ملف التعريف"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entries to keep above the selected entry when scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entry headers"
|
||||
msgstr ""
|
||||
@@ -364,6 +376,10 @@ msgstr ""
|
||||
msgid "Filtering expression"
|
||||
msgstr "تصفية التعبير"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Force fetching feeds is not yet available."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Forgot password?"
|
||||
msgstr "هل نسيت كلمة المرور؟"
|
||||
@@ -600,6 +616,10 @@ msgstr ""
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Only applies to compact, cozy and detailed modes"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "اوووه!"
|
||||
@@ -822,6 +842,14 @@ msgstr ""
|
||||
msgid "Show star icon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab favicon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab title"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
|
||||
@@ -140,6 +140,10 @@ msgstr "Extensió del navegador necessària per a Chrome"
|
||||
msgid "Browser extention"
|
||||
msgstr "Extensió del navegador"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Browser tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -176,6 +180,10 @@ msgstr "Comproveu que el canal funciona"
|
||||
msgid "Close menu"
|
||||
msgstr "Tanca el menu"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Cmd"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr "Versió de l'extensió del navegador CommaFeed {browserExtensionVersion}."
|
||||
@@ -313,6 +321,10 @@ msgstr "Entra"
|
||||
msgid "Enter your current password to change profile settings"
|
||||
msgstr "introduïu la vostra contrasenya actual per canviar la configuració del perfil"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entries to keep above the selected entry when scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entry headers"
|
||||
msgstr ""
|
||||
@@ -364,6 +376,10 @@ msgstr ""
|
||||
msgid "Filtering expression"
|
||||
msgstr "Expressió de filtratge"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Force fetching feeds is not yet available."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Forgot password?"
|
||||
msgstr "Heu oblidat la contrasenya?"
|
||||
@@ -600,6 +616,10 @@ msgstr ""
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr "Al mòbil, mostra els botons d'acció a la part inferior de la pantalla"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Only applies to compact, cozy and detailed modes"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Vaja!"
|
||||
@@ -822,6 +842,14 @@ msgstr "Mostra el menú natiu (escriptori)"
|
||||
msgid "Show star icon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab favicon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab title"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
|
||||
@@ -140,6 +140,10 @@ msgstr ""
|
||||
msgid "Browser extention"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Browser tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -176,6 +180,10 @@ msgstr "Zkontrolujte, zda zdroj funguje"
|
||||
msgid "Close menu"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Cmd"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
@@ -313,6 +321,10 @@ msgstr "Vstupte"
|
||||
msgid "Enter your current password to change profile settings"
|
||||
msgstr "Zadejte své aktuální heslo pro změnu nastavení profilu"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entries to keep above the selected entry when scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entry headers"
|
||||
msgstr ""
|
||||
@@ -364,6 +376,10 @@ msgstr ""
|
||||
msgid "Filtering expression"
|
||||
msgstr "Filtrování výrazu"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Force fetching feeds is not yet available."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Forgot password?"
|
||||
msgstr "Zapomněli jste heslo?"
|
||||
@@ -600,6 +616,10 @@ msgstr ""
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Only applies to compact, cozy and detailed modes"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Jejda!"
|
||||
@@ -822,6 +842,14 @@ msgstr ""
|
||||
msgid "Show star icon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab favicon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab title"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
|
||||
@@ -140,6 +140,10 @@ msgstr ""
|
||||
msgid "Browser extention"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Browser tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -176,6 +180,10 @@ msgstr "Gwiriwch fod y porthiant yn gweithio"
|
||||
msgid "Close menu"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Cmd"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
@@ -313,6 +321,10 @@ msgstr "Ewch i mewn"
|
||||
msgid "Enter your current password to change profile settings"
|
||||
msgstr "Rhowch eich cyfrinair presennol i newid gosodiadau proffil"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entries to keep above the selected entry when scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entry headers"
|
||||
msgstr ""
|
||||
@@ -364,6 +376,10 @@ msgstr ""
|
||||
msgid "Filtering expression"
|
||||
msgstr "Hidlo mynegiant"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Force fetching feeds is not yet available."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Forgot password?"
|
||||
msgstr "Wedi anghofio cyfrinair?"
|
||||
@@ -600,6 +616,10 @@ msgstr ""
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Only applies to compact, cozy and detailed modes"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Wps!"
|
||||
@@ -822,6 +842,14 @@ msgstr ""
|
||||
msgid "Show star icon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab favicon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab title"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
|
||||
@@ -140,6 +140,10 @@ msgstr ""
|
||||
msgid "Browser extention"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Browser tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -176,6 +180,10 @@ msgstr "Tjek, at foderet virker"
|
||||
msgid "Close menu"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Cmd"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
@@ -313,6 +321,10 @@ msgstr ""
|
||||
msgid "Enter your current password to change profile settings"
|
||||
msgstr "Indtast din nuværende adgangskode for at ændre profilindstillinger"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entries to keep above the selected entry when scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entry headers"
|
||||
msgstr ""
|
||||
@@ -364,6 +376,10 @@ msgstr ""
|
||||
msgid "Filtering expression"
|
||||
msgstr "Filtrerende udtryk"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Force fetching feeds is not yet available."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Forgot password?"
|
||||
msgstr "Glemt adgangskode?"
|
||||
@@ -600,6 +616,10 @@ msgstr ""
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Only applies to compact, cozy and detailed modes"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Hovsa!"
|
||||
@@ -822,6 +842,14 @@ msgstr ""
|
||||
msgid "Show star icon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab favicon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab title"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
|
||||
@@ -140,6 +140,10 @@ msgstr "Browser-Erweiterung für Chrome benötigt"
|
||||
msgid "Browser extention"
|
||||
msgstr "Browser-Erweiterung"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Browser tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -176,6 +180,10 @@ msgstr "Überprüfe, ob der Feed funktioniert"
|
||||
msgid "Close menu"
|
||||
msgstr "Menü schließen"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Cmd"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr "CommaFeed Browser Erweiterung Version {browserExtensionVersion}."
|
||||
@@ -313,6 +321,10 @@ msgstr "Eintreten"
|
||||
msgid "Enter your current password to change profile settings"
|
||||
msgstr "Geben Sie Ihr aktuelles Passwort ein, um die Profileinstellungen zu ändern"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entries to keep above the selected entry when scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entry headers"
|
||||
msgstr ""
|
||||
@@ -364,6 +376,10 @@ msgstr ""
|
||||
msgid "Filtering expression"
|
||||
msgstr "Filterausdruck"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Force fetching feeds is not yet available."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Forgot password?"
|
||||
msgstr "Passwort vergessen?"
|
||||
@@ -600,6 +616,10 @@ msgstr ""
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr "Auf mobilen Geräten die Aktion-Buttons am unteren Ende des Bildschirms anzeigen"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Only applies to compact, cozy and detailed modes"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Ups!"
|
||||
@@ -822,6 +842,14 @@ msgstr "Natives Menü anzeigen (Desktop)"
|
||||
msgid "Show star icon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab favicon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab title"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
|
||||
@@ -140,6 +140,10 @@ msgstr "Browser extension required for Chrome"
|
||||
msgid "Browser extention"
|
||||
msgstr "Browser extention"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Browser tab"
|
||||
msgstr "Browser tab"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -176,6 +180,10 @@ msgstr "Check that the feed is working"
|
||||
msgid "Close menu"
|
||||
msgstr "Close menu"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Cmd"
|
||||
msgstr "Cmd"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
@@ -313,6 +321,10 @@ msgstr "Enter"
|
||||
msgid "Enter your current password to change profile settings"
|
||||
msgstr "Enter your current password to change profile settings"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entries to keep above the selected entry when scrolling"
|
||||
msgstr "Entries to keep above the selected entry when scrolling"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entry headers"
|
||||
msgstr "Entry headers"
|
||||
@@ -364,6 +376,10 @@ msgstr "Fever API URL"
|
||||
msgid "Filtering expression"
|
||||
msgstr "Filtering expression"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Force fetching feeds is not yet available."
|
||||
msgstr "Force fetching feeds is not yet available."
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Forgot password?"
|
||||
msgstr "Forgot password?"
|
||||
@@ -600,6 +616,10 @@ msgstr "On mobile"
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr "On mobile, show action buttons at the bottom of the screen"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Only applies to compact, cozy and detailed modes"
|
||||
msgstr "Only applies to compact, cozy and detailed modes"
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Oops!"
|
||||
@@ -822,6 +842,14 @@ msgstr "Show native menu (desktop)"
|
||||
msgid "Show star icon"
|
||||
msgstr "Show star icon"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab favicon"
|
||||
msgstr "Show unread count in tab favicon"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab title"
|
||||
msgstr "Show unread count in tab title"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
# SPDX-FileCopyrightText: 2024 victorhck <victorhck@mailbox.org>
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"POT-Creation-Date: 2022-10-28 13:47+0200\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: @lingui/cli\n"
|
||||
"X-Generator: Lokalize 24.08.0\n"
|
||||
"Language: es\n"
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"PO-Revision-Date: 2024-09-13 17:17+0200\n"
|
||||
"Last-Translator: victorhck <victorhck@mailbox.org>\n"
|
||||
"Language-Team: Spanish <kde-i18n-doc@kde.org>\n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr ""
|
||||
msgstr "<0>CommaFeed es un proyecto de código abierto. El código fuente está hospedado en </0><1>GitHub</1>."
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1>."
|
||||
msgstr ""
|
||||
msgstr "<0>La sintaxis completa está disponible </0><1>aquí</1>."
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "<0>Have an account?</0><1>Log in!</1>"
|
||||
@@ -27,7 +28,7 @@ msgstr "<0>¿Tienes una cuenta?</0><1>¡Inicia sesión!</1>"
|
||||
|
||||
#: src/pages/app/DonatePage.tsx
|
||||
msgid "<0>Hey,</0><1>I'm Jérémie from Belgium and I've been working on CommaFeed in my free time for over 10 years now. Thanks for taking an interest in helping me continue supporting CommaFeed.</1>"
|
||||
msgstr ""
|
||||
msgstr "<0>Hola,</0><1>Soy Jérémie de Bélgica y he estado trabajando en CommaFeed en mi tiempo libre desde hace 10 años. Gracias por interesarte en ayudarme a seguir apoyando a CommaFeed.</1>"
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
@@ -36,7 +37,7 @@ msgstr "<0>¿Necesitas una cuenta?</0><1>¡Regístrate!</1>"
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "About"
|
||||
msgstr "Sobre"
|
||||
msgstr "Acerca de"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Actions"
|
||||
@@ -44,7 +45,7 @@ msgstr "Acciones"
|
||||
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
msgid "Add"
|
||||
msgstr "Agregar"
|
||||
msgstr "Añadir"
|
||||
|
||||
#: src/pages/app/AddPage.tsx
|
||||
msgid "Add category"
|
||||
@@ -70,55 +71,55 @@ msgstr "Todo"
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always"
|
||||
msgstr ""
|
||||
msgstr "Siempre"
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "An email has been sent if this address was registered. Check your inbox."
|
||||
msgstr "Se ha enviado un correo electrónico si se registró esta dirección. "
|
||||
msgstr "Se ha enviado un correo electrónico si esta dirección estaba registrada. Revisa tu bandeja de entrada."
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "An opml file is an XML file containing feed URLs and categories. You can get an OPML file by exporting your data from other feed reading services."
|
||||
msgstr "Un archivo opml es un archivo XML que contiene categorías y direcciones URL de fuentes. "
|
||||
msgstr "Un archivo opml es un archivo XML que contiene categorías y las URL de los feeds. Puedes obtener un archivo OPML exportando tus datos desde otros servicios de lectura de feeds."
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Analyze feed"
|
||||
msgstr "Analizar alimentación"
|
||||
msgstr "Analizar feed"
|
||||
|
||||
#: src/components/AnnouncementDialog.tsx
|
||||
msgid "Announcement"
|
||||
msgstr ""
|
||||
msgstr "Anuncio"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "API key"
|
||||
msgstr "clave API"
|
||||
msgstr "Clave API"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Are you sure you want to delete category <0>{categoryName}</0>?"
|
||||
msgstr "¿Está seguro de que desea eliminar la categoría <0>{categoryName}</0>?"
|
||||
msgstr "¿Estás seguro de que deseas eliminar la categoría <0>{categoryName}</0>?"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Are you sure you want to delete user <0>{userName}</0> ?"
|
||||
msgstr "¿Está seguro de que desea eliminar el usuario <0>{userName}</0> ?"
|
||||
msgstr "¿Estás seguro de que deseas eliminar el usuario <0>{userName}</0> ?"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Are you sure you want to delete your account? There's no turning back!"
|
||||
msgstr "¿Está seguro de que desea eliminar su cuenta? "
|
||||
msgstr "¿Estás seguro de que quieres eliminar tu cuenta? ¡No hay vuelta atrás!"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
msgid "Are you sure you want to mark all entries of <0>{sourceLabel}</0> as read?"
|
||||
msgstr "¿Está seguro de que desea marcar todas las entradas de <0>{sourceLabel}</0> como leídas?"
|
||||
msgstr "¿Estás seguro de que deseas marcar todas las entradas de <0>{sourceLabel}</0> como leídas?"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
msgid "Are you sure you want to mark entries older than {threshold} days of <0>{sourceLabel}</0> as read?"
|
||||
msgstr "¿Está seguro de que desea marcar las entradas anteriores a {threshold} días de <0>{sourceLabel}</0> como leídas?"
|
||||
msgstr "¿Estás seguro de que deseas marcar las entradas anteriores a {threshold} días de <0>{sourceLabel}</0> como leídas?"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Are you sure you want to unsubscribe from <0>{feedName}</0>?"
|
||||
msgstr "¿Está seguro de que desea darse de baja de <0>{feedName}</0>?"
|
||||
msgstr "¿Estás seguro de que deseas darte de baja de <0>{feedName}</0>?"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Asc"
|
||||
msgstr "ASC"
|
||||
msgstr "Asc"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Available variables are 'title', 'content', 'url' 'author' and 'categories' and their content is converted to lower case to ease string comparison."
|
||||
@@ -134,11 +135,15 @@ msgstr "Volver a iniciar sesión"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Browser extension required for Chrome"
|
||||
msgstr ""
|
||||
msgstr "Se requiere extensión de navegador para Chrome"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Browser extention"
|
||||
msgstr ""
|
||||
msgstr "Extensión del navegador"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Browser tab"
|
||||
msgstr "Pestaña del navegador"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
@@ -170,19 +175,23 @@ msgstr "Cambiar la contraseña generará una nueva clave API"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Check that the feed is working"
|
||||
msgstr "Compruebe que el feed funciona"
|
||||
msgstr "Comprueba que el feed funciona"
|
||||
|
||||
#: src/pages/app/Layout.tsx
|
||||
msgid "Close menu"
|
||||
msgstr ""
|
||||
msgstr "Cerrar menú"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Cmd"
|
||||
msgstr "Cmd"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
msgstr "Versión de la extensión del navegador CommaFeed {browserExtensionVersion}."
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||
msgstr ""
|
||||
msgstr "CommaFeed es compatible con Fever API. Utilice la siguiente URL en su cliente móvil compatible con Fever. Inicie sesión con su nombre de usuario y su <0>clave API</0>."
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed next unread item"
|
||||
@@ -190,7 +199,7 @@ msgstr "CommaFeed siguiente elemento no leído"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed version {version} ({revision})."
|
||||
msgstr ""
|
||||
msgstr "Versión de CommaFeed {version} ({revision})."
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Compact"
|
||||
@@ -214,7 +223,7 @@ msgstr "Acogedor"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Ctrl"
|
||||
msgstr ""
|
||||
msgstr "Ctrl"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Current password"
|
||||
@@ -222,19 +231,19 @@ msgstr "Contraseña actual"
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Custom code"
|
||||
msgstr ""
|
||||
msgstr "Código personalizado"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "Custom CSS rules that will be applied"
|
||||
msgstr ""
|
||||
msgstr "Reglas CSS personalizadas que se aplicarán"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "Custom JS code that will be executed on page load"
|
||||
msgstr ""
|
||||
msgstr "Código JS personalizado que se ejecutará al cargar la página"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Dark"
|
||||
msgstr ""
|
||||
msgstr "Oscuro"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Date created"
|
||||
@@ -259,25 +268,25 @@ msgstr "Borrar usuario"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Desc"
|
||||
msgstr ""
|
||||
msgstr "Desc"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Detailed"
|
||||
msgstr ""
|
||||
msgstr "Detallado"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Display"
|
||||
msgstr "Pantalla"
|
||||
msgstr "Mostrar"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/app/DonatePage.tsx
|
||||
msgid "Donate"
|
||||
msgstr ""
|
||||
msgstr "Donar"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Download"
|
||||
msgstr "descargar"
|
||||
msgstr "Descargar"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Drag link to bookmark bar"
|
||||
@@ -294,7 +303,7 @@ msgstr "Correo electrónico"
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "E-mail address"
|
||||
msgstr "dirección de correo electrónico"
|
||||
msgstr "Dirección de correo electrónico"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Edit user"
|
||||
@@ -311,15 +320,19 @@ msgstr "Entrar"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Enter your current password to change profile settings"
|
||||
msgstr "Ingrese su contraseña actual para cambiar la configuración del perfil"
|
||||
msgstr "Ingresa tu contraseña actual para cambiar la configuración del perfil"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entries to keep above the selected entry when scrolling"
|
||||
msgstr "Entradas para mantener encima de la entrada seleccionada al desplazarse"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entry headers"
|
||||
msgstr ""
|
||||
msgstr "Encabezados de las entradas"
|
||||
|
||||
#: src/components/Alert.tsx
|
||||
msgid "Error"
|
||||
msgstr ""
|
||||
msgstr "Error"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Example: {example}."
|
||||
@@ -331,39 +344,43 @@ msgstr "Expandido"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services"
|
||||
msgstr "Exporte sus suscripciones y categorías como un archivo OPML que se puede importar en otros servicios de lectura de feeds"
|
||||
msgstr "Exporta tus suscripciones y categorías como un archivo OPML que se puede importar en otros servicios de lectura de feeds"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
msgid "Extension options"
|
||||
msgstr ""
|
||||
msgstr "Opciones de la extensión"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Feed name"
|
||||
msgstr "Nombre de alimentación"
|
||||
msgstr "Nombre del feed"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Feed URL"
|
||||
msgstr "URL de fuente"
|
||||
msgstr "URL del feed"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Fetch all my feeds now"
|
||||
msgstr ""
|
||||
msgstr "Obtener todos mis feeds ahora"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Fever API"
|
||||
msgstr ""
|
||||
msgstr "API de Fever"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Fever API URL"
|
||||
msgstr ""
|
||||
msgstr "URL de la API de Fever"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Filtering expression"
|
||||
msgstr "Expresión de filtrado"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Force fetching feeds is not yet available."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Forgot password?"
|
||||
msgstr "¿Olvidaste la contraseña?"
|
||||
@@ -386,7 +403,7 @@ msgstr "URL del feed generado"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Go to {0}"
|
||||
msgstr ""
|
||||
msgstr "Ir a {0}"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Go to the All view"
|
||||
@@ -398,7 +415,7 @@ msgstr "Ir a la documentación de la API."
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Goodies"
|
||||
msgstr "golosinas"
|
||||
msgstr "Golosinas"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Id"
|
||||
@@ -406,15 +423,15 @@ msgstr "Identificación"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
|
||||
msgstr "Si no está vacío, una expresión que se evalúa como 'verdadero' o 'falso'. "
|
||||
msgstr "Si no está vacía, una expresión que se evalúa como \"verdadera\" o \"falso\". Si es falso, las nuevas entradas de este feed se marcarán como leídas automáticamente."
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
msgstr ""
|
||||
msgstr "Si la entrada no cabe completamente en la pantalla"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "If you encounter an issue, please report it on the issues page of the GitHub project."
|
||||
msgstr "Si encuentra un problema, infórmelo en la página de problemas del proyecto GitHub."
|
||||
msgstr "Si encuentras un problema, informa sobre ello en la página de problemas del proyecto en GitHub."
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "Import"
|
||||
@@ -422,7 +439,7 @@ msgstr "Importar"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "In expanded view, scrolling through entries mark them as read"
|
||||
msgstr "En la vista ampliada, al desplazarse por las entradas, márquelas como leídas"
|
||||
msgstr "En la vista ampliada, al desplazarse por las entradas marcarlas como leídas"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
@@ -432,7 +449,7 @@ msgstr "Mantener sin leer"
|
||||
#: src/components/content/FeedEntries.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Keyboard shortcuts"
|
||||
msgstr "atajos de teclado"
|
||||
msgstr "Atajos de teclado"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Language"
|
||||
@@ -440,7 +457,7 @@ msgstr "Idioma"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Last login date"
|
||||
msgstr "fecha del último inicio de sesión"
|
||||
msgstr "Fecha del último inicio de sesión"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Last refresh"
|
||||
@@ -452,7 +469,7 @@ msgstr "Último mensaje de actualización"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Light"
|
||||
msgstr ""
|
||||
msgstr "Claro"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
@@ -484,11 +501,11 @@ msgstr "Iniciar sesión"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Logout"
|
||||
msgstr "Salir"
|
||||
msgstr "Cerrar sesión"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Long press"
|
||||
msgstr ""
|
||||
msgstr "Pulsación larga"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
@@ -520,7 +537,7 @@ msgstr "Métricas"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Middle click"
|
||||
msgstr ""
|
||||
msgstr "Clic central"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Move the page down"
|
||||
@@ -544,12 +561,12 @@ msgstr "Nombre"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Navegar a una suscripción ingresando su nombre"
|
||||
msgstr "Navegar a una suscripción introduciendo su nombre"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
msgstr ""
|
||||
msgstr "Nunca"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "New password"
|
||||
@@ -557,7 +574,7 @@ msgstr "Nueva contraseña"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Newest first"
|
||||
msgstr "más reciente primero"
|
||||
msgstr "Las más recientes primero"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
@@ -570,7 +587,7 @@ msgstr "Próxima actualización"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Next unread item bookmarklet"
|
||||
msgstr "Bookmarklet del siguiente elemento no leído"
|
||||
msgstr "Siguiente elemento no leído de los marcadores"
|
||||
|
||||
#: src/pages/app/FeedEntriesPage.tsx
|
||||
msgid "No more entries"
|
||||
@@ -578,7 +595,7 @@ msgstr "No más entradas"
|
||||
|
||||
#: src/components/content/ShareButtons.tsx
|
||||
msgid "No sharing options available."
|
||||
msgstr ""
|
||||
msgstr "No hay opciones para compartir disponibles."
|
||||
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
msgid "Nothing found"
|
||||
@@ -586,19 +603,23 @@ msgstr "Nada encontrado"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Oldest first"
|
||||
msgstr "más antigua primero"
|
||||
msgstr "Las más antiguas primero"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On desktop"
|
||||
msgstr ""
|
||||
msgstr "En el escritorio"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On mobile"
|
||||
msgstr ""
|
||||
msgstr "En dispositivos móviles"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr ""
|
||||
msgstr "En dispositivos móviles, mostrar los botones de acción en la parte inferior de la pantalla"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Only applies to compact, cozy and detailed modes"
|
||||
msgstr "Sólo se aplica a los modos compacto, acogedor y detallado"
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
@@ -606,7 +627,7 @@ msgstr "¡Ups!"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Open CommaFeed"
|
||||
msgstr ""
|
||||
msgstr "Abrir Commafeed"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open current entry in a new tab"
|
||||
@@ -623,15 +644,15 @@ msgstr "Abrir enlace"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Open link in new background tab"
|
||||
msgstr ""
|
||||
msgstr "Abrir enlace en una nueva pestaña en segundo plano"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Open link in new tab"
|
||||
msgstr ""
|
||||
msgstr "Abrir el enlace en una nueva pestaña"
|
||||
|
||||
#: src/pages/app/Layout.tsx
|
||||
msgid "Open menu"
|
||||
msgstr ""
|
||||
msgstr "Abrir menú"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open next entry"
|
||||
@@ -647,20 +668,20 @@ msgstr "Abrir/cerrar entrada actual"
|
||||
|
||||
#: src/pages/app/AddPage.tsx
|
||||
msgid "OPML"
|
||||
msgstr ""
|
||||
msgstr "OPML"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "OPML export"
|
||||
msgstr "Exportación OPML"
|
||||
msgstr "Exportar OPML"
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "OPML file"
|
||||
msgstr "archivo OPML"
|
||||
msgstr "Archivo OPML"
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "OPML file is required"
|
||||
msgstr ""
|
||||
msgstr "Es necesario un archivo OPML"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Order"
|
||||
@@ -697,7 +718,7 @@ msgstr "Posición"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Previous"
|
||||
msgstr ""
|
||||
msgstr "Previo"
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Profile"
|
||||
@@ -723,7 +744,7 @@ msgstr "API REST"
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Right click"
|
||||
msgstr ""
|
||||
msgstr "Clic derecho"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
@@ -735,7 +756,7 @@ msgstr "Guardar"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll selected entry to the top of the page"
|
||||
msgstr ""
|
||||
msgstr "Desplazar la entrada seleccionada hasta la parte superior de la página"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll smoothly when navigating between entries"
|
||||
@@ -743,7 +764,7 @@ msgstr "Desplazarse suavemente al navegar entre entradas"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scrolling"
|
||||
msgstr ""
|
||||
msgstr "Desplazarse"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
@@ -758,15 +779,15 @@ msgstr "La búsqueda requiere al menos 3 caracteres"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Set focus on next entry without opening it"
|
||||
msgstr "Establezca el foco en la siguiente entrada sin abrirla"
|
||||
msgstr "Establecer el foco en la siguiente entrada sin abrirla"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Set focus on previous entry without opening it"
|
||||
msgstr "Poner el foco en la entrada anterior sin abrirla"
|
||||
msgstr "Establecer el foco en la entrada anterior sin abrirla"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Settings"
|
||||
msgstr "Configuraciones"
|
||||
msgstr "Ajustes"
|
||||
|
||||
#: src/app/user/slice.ts
|
||||
msgid "Settings saved."
|
||||
@@ -784,27 +805,27 @@ msgstr "Compartir sitios"
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Shift"
|
||||
msgstr "Cambio"
|
||||
msgstr "Shift"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show CommaFeed's own context menu on right click"
|
||||
msgstr ""
|
||||
msgstr "Mostrar el menú contextual propio de CommaFeed al hacer clic derecho"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show confirmation when marking all entries as read"
|
||||
msgstr ""
|
||||
msgstr "Mostrar confirmación al marcar todas las entradas como leídas"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show entry menu (desktop)"
|
||||
msgstr ""
|
||||
msgstr "Mostrar menú de entrada (escritorio)"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show entry menu (mobile)"
|
||||
msgstr ""
|
||||
msgstr "Mostrar menú de entrada (móvil)"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show external link icon"
|
||||
msgstr ""
|
||||
msgstr "Mostrar icono de enlace externo"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show feeds and categories with no unread entries"
|
||||
@@ -812,15 +833,23 @@ msgstr "Mostrar feeds y categorías sin entradas no leídas"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show keyboard shortcut help"
|
||||
msgstr "Mostrar ayuda de atajo de teclado"
|
||||
msgstr "Mostrar la ayuda de los atajos de teclado"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show native menu (desktop)"
|
||||
msgstr ""
|
||||
msgstr "Mostrar menú nativo (escritorio)"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show star icon"
|
||||
msgstr ""
|
||||
msgstr "Mostrar el icono de la estrella"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab favicon"
|
||||
msgstr "Mostrar recuento de no leídos en la pestaña favicon"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab title"
|
||||
msgstr "Mostrar recuento de no leídos en el título de la pestaña"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
@@ -841,7 +870,7 @@ msgstr "Espacio"
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
#: src/components/content/header/Star.tsx
|
||||
msgid "Star"
|
||||
msgstr "estrella"
|
||||
msgstr "Estrella"
|
||||
|
||||
#: src/app/constants.ts
|
||||
#: src/components/sidebar/Tree.tsx
|
||||
@@ -856,7 +885,7 @@ msgstr "Suscribirse"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Subscribe to the feed"
|
||||
msgstr "Suscríbete a la fuente"
|
||||
msgstr "Suscríbete al feed"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Subscribe URL"
|
||||
@@ -868,7 +897,7 @@ msgstr "Éxito"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Swipe header to the left"
|
||||
msgstr ""
|
||||
msgstr "Desliza el encabezado hacia la izquierda"
|
||||
|
||||
#: src/pages/WelcomePage.tsx
|
||||
msgid "Switch to dark theme"
|
||||
@@ -880,7 +909,7 @@ msgstr "Cambiar a tema claro"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "System"
|
||||
msgstr ""
|
||||
msgstr "Sistema"
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
@@ -889,7 +918,7 @@ msgstr "Etiquetas"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "The URL for the feed you want to subscribe to. You can also use the website's url directly and CommaFeed will try to find the feed in the page."
|
||||
msgstr "La URL de la fuente a la que desea suscribirse. "
|
||||
msgstr "La URL del feed al que desea suscribirse. También puede utilizar la URL del sitio web directamente y CommaFeed intentará encontrar el feed en la página."
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Theme"
|
||||
@@ -897,7 +926,7 @@ msgstr "Tema"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
||||
msgstr ""
|
||||
msgstr "Esta es su clave API. Se puede utilizar para algunas operaciones API de solo lectura y otorga acceso a Fever API. Utilice el formulario en la parte inferior de la página para generar una nueva clave API"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle read status of current entry"
|
||||
@@ -905,19 +934,19 @@ msgstr "Alternar estado de lectura de la entrada actual"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle sidebar"
|
||||
msgstr ""
|
||||
msgstr "Alternar barra lateral"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle starred status of current entry"
|
||||
msgstr ""
|
||||
msgstr "Alternar estado destacado de la entrada actual"
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||
msgstr "Pruebe CommaFeed con la cuenta demo: demo/demo"
|
||||
msgstr "Prueba CommaFeed con la cuenta de demostración: demo/demo"
|
||||
|
||||
#: src/pages/WelcomePage.tsx
|
||||
msgid "Try the demo!"
|
||||
msgstr ""
|
||||
msgstr "¡Prueba la demostración!"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Unread"
|
||||
@@ -953,8 +982,8 @@ msgstr "Sitio web"
|
||||
|
||||
#: src/pages/app/FeedEntriesPage.tsx
|
||||
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 "Aún no tienes ninguna suscripción. ¿Por qué no intentas agregar una haciendo clic en el signo + en la parte superior de la página?"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Your feeds have been queued for refresh."
|
||||
msgstr ""
|
||||
msgstr "Tus feeds se han puesto en cola para actualizarse."
|
||||
|
||||
@@ -140,6 +140,10 @@ msgstr ""
|
||||
msgid "Browser extention"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Browser tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -176,6 +180,10 @@ msgstr "بررسی کنید که خوراک کار می کند"
|
||||
msgid "Close menu"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Cmd"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
@@ -313,6 +321,10 @@ msgstr "وارد شوید"
|
||||
msgid "Enter your current password to change profile settings"
|
||||
msgstr "رمز عبور فعلی خود را برای تغییر تنظیمات نمایه وارد کنید"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entries to keep above the selected entry when scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entry headers"
|
||||
msgstr ""
|
||||
@@ -364,6 +376,10 @@ msgstr ""
|
||||
msgid "Filtering expression"
|
||||
msgstr "بیان فیلتر"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Force fetching feeds is not yet available."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Forgot password?"
|
||||
msgstr "رمز عبور را فراموش کرده اید؟"
|
||||
@@ -600,6 +616,10 @@ msgstr ""
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Only applies to compact, cozy and detailed modes"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "اوه!"
|
||||
@@ -822,6 +842,14 @@ msgstr ""
|
||||
msgid "Show star icon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab favicon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab title"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
|
||||
@@ -140,6 +140,10 @@ msgstr ""
|
||||
msgid "Browser extention"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Browser tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -176,6 +180,10 @@ msgstr "Tarkista, että syöttö toimii"
|
||||
msgid "Close menu"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Cmd"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
@@ -313,6 +321,10 @@ msgstr ""
|
||||
msgid "Enter your current password to change profile settings"
|
||||
msgstr "Anna nykyinen salasanasi muuttaaksesi profiiliasetuksia"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entries to keep above the selected entry when scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entry headers"
|
||||
msgstr ""
|
||||
@@ -364,6 +376,10 @@ msgstr ""
|
||||
msgid "Filtering expression"
|
||||
msgstr "Suodattava lauseke"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Force fetching feeds is not yet available."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Forgot password?"
|
||||
msgstr "Unohditko salasanan?"
|
||||
@@ -600,6 +616,10 @@ msgstr ""
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Only applies to compact, cozy and detailed modes"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Hups!"
|
||||
@@ -822,6 +842,14 @@ msgstr ""
|
||||
msgid "Show star icon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab favicon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab title"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
|
||||
@@ -140,6 +140,10 @@ msgstr "L'extension navigateur est nécessaire sur Chrome"
|
||||
msgid "Browser extention"
|
||||
msgstr "Extension navigateur"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Browser tab"
|
||||
msgstr "Onglet navigateur"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -176,6 +180,10 @@ msgstr "Vérifie que le flux fonctionne"
|
||||
msgid "Close menu"
|
||||
msgstr "Fermer le menu"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Cmd"
|
||||
msgstr "Cmd"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr "Extension CommaFeed pour navigateur version {browserExtensionVersion}."
|
||||
@@ -313,9 +321,13 @@ msgstr "Entrer"
|
||||
msgid "Enter your current password to change profile settings"
|
||||
msgstr "Entrez votre mot de passe actuel pour changer les paramètres du profil"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entries to keep above the selected entry when scrolling"
|
||||
msgstr "Nombre d'entrées à conserver au-dessus de l'entrée sélectionnée lors d'un défilement"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entry headers"
|
||||
msgstr ""
|
||||
msgstr "En-têtes de l'entrée"
|
||||
|
||||
#: src/components/Alert.tsx
|
||||
msgid "Error"
|
||||
@@ -364,6 +376,10 @@ msgstr "URL API Fever"
|
||||
msgid "Filtering expression"
|
||||
msgstr "Expression de filtrage"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Force fetching feeds is not yet available."
|
||||
msgstr "La récupération forcée des flux n'est pas encore disponible."
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Forgot password?"
|
||||
msgstr "Mot de passe oublié ?"
|
||||
@@ -578,7 +594,7 @@ msgstr "Fin de la liste"
|
||||
|
||||
#: src/components/content/ShareButtons.tsx
|
||||
msgid "No sharing options available."
|
||||
msgstr ""
|
||||
msgstr "Aucune option de partage disponible"
|
||||
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
msgid "Nothing found"
|
||||
@@ -590,16 +606,20 @@ msgstr "Du plus ancien au plus récent"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On desktop"
|
||||
msgstr ""
|
||||
msgstr "Version PC"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On mobile"
|
||||
msgstr ""
|
||||
msgstr "Version mobile"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr "Sur mobile, afficher les boutons d'action en bas de l'écran"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Only applies to compact, cozy and detailed modes"
|
||||
msgstr "Ne fonctionne que dans les modes Compact, Cozy, et Vue détaillée"
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Oups !"
|
||||
@@ -660,7 +680,7 @@ msgstr "Fichier OPML"
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "OPML file is required"
|
||||
msgstr ""
|
||||
msgstr "Vous devez fournir un fichier OPML"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Order"
|
||||
@@ -804,7 +824,7 @@ msgstr "Afficher les options de l'entrée (mobile)"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show external link icon"
|
||||
msgstr ""
|
||||
msgstr "Afficher l'icône du lien distant"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show feeds and categories with no unread entries"
|
||||
@@ -820,7 +840,15 @@ msgstr "Afficher les options du navigateur (ordinateur)"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show star icon"
|
||||
msgstr ""
|
||||
msgstr "Afficher l'icône Favori"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab favicon"
|
||||
msgstr "Afficher le nombre d'entrées non lues dans la favicône de l'onglet"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab title"
|
||||
msgstr "Afficher le nombre d'entrées non lues dans le titre de l'onglet"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
|
||||
@@ -140,6 +140,10 @@ msgstr ""
|
||||
msgid "Browser extention"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Browser tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -176,6 +180,10 @@ msgstr "Comproba que a fonte funciona"
|
||||
msgid "Close menu"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Cmd"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
@@ -313,6 +321,10 @@ msgstr "Entra"
|
||||
msgid "Enter your current password to change profile settings"
|
||||
msgstr "Introduce o teu contrasinal actual para cambiar a configuración do perfil"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entries to keep above the selected entry when scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entry headers"
|
||||
msgstr ""
|
||||
@@ -364,6 +376,10 @@ msgstr ""
|
||||
msgid "Filtering expression"
|
||||
msgstr "Expresión de filtrado"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Force fetching feeds is not yet available."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Forgot password?"
|
||||
msgstr "Esqueceches o contrasinal?"
|
||||
@@ -600,6 +616,10 @@ msgstr ""
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Only applies to compact, cozy and detailed modes"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Vaia!"
|
||||
@@ -822,6 +842,14 @@ msgstr ""
|
||||
msgid "Show star icon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab favicon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab title"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
|
||||
@@ -140,6 +140,10 @@ msgstr ""
|
||||
msgid "Browser extention"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Browser tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -176,6 +180,10 @@ msgstr "Ellenőrizze, hogy a feed működik-e"
|
||||
msgid "Close menu"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Cmd"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
@@ -313,6 +321,10 @@ msgstr ""
|
||||
msgid "Enter your current password to change profile settings"
|
||||
msgstr "Adja meg jelenlegi jelszavát a profilbeállítások módosításához"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entries to keep above the selected entry when scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entry headers"
|
||||
msgstr ""
|
||||
@@ -364,6 +376,10 @@ msgstr ""
|
||||
msgid "Filtering expression"
|
||||
msgstr "Szűrő kifejezés"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Force fetching feeds is not yet available."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Forgot password?"
|
||||
msgstr "Elfelejtette a jelszavát?"
|
||||
@@ -600,6 +616,10 @@ msgstr ""
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Only applies to compact, cozy and detailed modes"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Hoppá!"
|
||||
@@ -822,6 +842,14 @@ msgstr ""
|
||||
msgid "Show star icon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab favicon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab title"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
|
||||
@@ -140,6 +140,10 @@ msgstr ""
|
||||
msgid "Browser extention"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Browser tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -176,6 +180,10 @@ msgstr "Periksa apakah umpannya berfungsi"
|
||||
msgid "Close menu"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Cmd"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
@@ -313,6 +321,10 @@ msgstr "Masuk"
|
||||
msgid "Enter your current password to change profile settings"
|
||||
msgstr "Masukkan kata sandi Anda saat ini untuk mengubah pengaturan profil"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entries to keep above the selected entry when scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entry headers"
|
||||
msgstr ""
|
||||
@@ -364,6 +376,10 @@ msgstr ""
|
||||
msgid "Filtering expression"
|
||||
msgstr "Memfilter ekspresi"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Force fetching feeds is not yet available."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Forgot password?"
|
||||
msgstr "Lupa kata sandi?"
|
||||
@@ -600,6 +616,10 @@ msgstr ""
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Only applies to compact, cozy and detailed modes"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Ups!"
|
||||
@@ -822,6 +842,14 @@ msgstr ""
|
||||
msgid "Show star icon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab favicon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab title"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
|
||||
@@ -140,6 +140,10 @@ msgstr ""
|
||||
msgid "Browser extention"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Browser tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -176,6 +180,10 @@ msgstr "Verifica che il feed funzioni"
|
||||
msgid "Close menu"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Cmd"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
@@ -313,6 +321,10 @@ msgstr "Invio"
|
||||
msgid "Enter your current password to change profile settings"
|
||||
msgstr "Inserisci la tua password attuale per modificare le impostazioni del profilo"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entries to keep above the selected entry when scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entry headers"
|
||||
msgstr ""
|
||||
@@ -364,6 +376,10 @@ msgstr ""
|
||||
msgid "Filtering expression"
|
||||
msgstr "Espressione filtrante"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Force fetching feeds is not yet available."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Forgot password?"
|
||||
msgstr "Password dimenticata?"
|
||||
@@ -600,6 +616,10 @@ msgstr ""
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Only applies to compact, cozy and detailed modes"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Ops!"
|
||||
@@ -822,6 +842,14 @@ msgstr ""
|
||||
msgid "Show star icon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab favicon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab title"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
|
||||
@@ -9,17 +9,17 @@ msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: \n"
|
||||
"Last-Translator: https://github.com/dai\n"
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr ""
|
||||
msgstr "<0>CommaFeed はオープンソースのプロジェクトです。 ソースは以下でホストされています </0><1>GitHub</1>。"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1>."
|
||||
msgstr ""
|
||||
msgstr "<0>完全な syntax </0><1>こちら</1>で利用可能です。"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "<0>Have an account?</0><1>Log in!</1>"
|
||||
@@ -27,7 +27,7 @@ msgstr "<0>アカウントをお持ちですか?</0><1>ログインしてくだ
|
||||
|
||||
#: src/pages/app/DonatePage.tsx
|
||||
msgid "<0>Hey,</0><1>I'm Jérémie from Belgium and I've been working on CommaFeed in my free time for over 10 years now. Thanks for taking an interest in helping me continue supporting CommaFeed.</1>"
|
||||
msgstr ""
|
||||
msgstr "<0>こんにちは、</0><1>私はベルギーのジェレミーです。私はこれまで 10 年以上、CommaFeed のオープンソースプロジェクトを無料で開発してきました。あなたの関心に感謝します。</1>"
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
@@ -36,7 +36,7 @@ msgstr "<0>アカウントが必要ですか?</0><1>サインアップ!</1>"
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "About"
|
||||
msgstr "約"
|
||||
msgstr "About"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Actions"
|
||||
@@ -44,7 +44,7 @@ msgstr "アクション"
|
||||
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
msgid "Add"
|
||||
msgstr "追記"
|
||||
msgstr "追加"
|
||||
|
||||
#: src/pages/app/AddPage.tsx
|
||||
msgid "Add category"
|
||||
@@ -58,27 +58,27 @@ msgstr "ユーザー追加"
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Admin"
|
||||
msgstr "管理人"
|
||||
msgstr "管理者"
|
||||
|
||||
#: src/app/constants.ts
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/Tree.tsx
|
||||
msgid "All"
|
||||
msgstr "全員"
|
||||
msgstr "すべて"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always"
|
||||
msgstr ""
|
||||
msgstr "常に"
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "An email has been sent if this address was registered. Check your inbox."
|
||||
msgstr "このアドレスが登録されていれば、メールが送信されました。"
|
||||
msgstr "このアドレスに確認メールを送信しました。受信箱を確認してください。"
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "An opml file is an XML file containing feed URLs and categories. You can get an OPML file by exporting your data from other feed reading services."
|
||||
msgstr "opml ファイルは、フィードの URL とカテゴリを含む XML ファイルです。"
|
||||
msgstr "opmlファイルは、フィードのURLとカテゴリを含むXMLファイルです。OPMLファイルは他のフィードサービスからエクスポートして取得することができます"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Analyze feed"
|
||||
@@ -86,7 +86,7 @@ msgstr "フィードを分析する"
|
||||
|
||||
#: src/components/AnnouncementDialog.tsx
|
||||
msgid "Announcement"
|
||||
msgstr ""
|
||||
msgstr "お知らせ"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "API key"
|
||||
@@ -94,27 +94,27 @@ msgstr "APIキー"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Are you sure you want to delete category <0>{categoryName}</0>?"
|
||||
msgstr "カテゴリ <0>{categoryName}</0> を削除してもよろしいですか?"
|
||||
msgstr "カテゴリ <0>{categoryName}</0> を削除してもよろしいですか?"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Are you sure you want to delete user <0>{userName}</0> ?"
|
||||
msgstr "ユーザー <0>{userName}</0> を削除してもよろしいですか?"
|
||||
msgstr "ユーザー <0>{userName}</0> を削除してもよろしいですか?"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Are you sure you want to delete your account? There's no turning back!"
|
||||
msgstr "本当にアカウントを削除しますか?"
|
||||
msgstr "本当にアカウントを削除しますか?元には戻せません!"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
msgid "Are you sure you want to mark all entries of <0>{sourceLabel}</0> as read?"
|
||||
msgstr "<0>{sourceLabel}</0> のすべてのエントリを既読にしますか?"
|
||||
msgstr "<0>{sourceLabel}</0> のすべてのエントリーを既読にしますか?"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
msgid "Are you sure you want to mark entries older than {threshold} days of <0>{sourceLabel}</0> as read?"
|
||||
msgstr "<0>{sourceLabel}</0> の {threshold} 日より前のエントリを既読としてマークしてもよろしいですか?"
|
||||
msgstr "<0>{sourceLabel}</0> の {threshold} 日より前のエントリーを既読としてマークしてもよろしいですか?"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Are you sure you want to unsubscribe from <0>{feedName}</0>?"
|
||||
msgstr "<0>{feedName}</0> の登録を解除してもよろしいですか?"
|
||||
msgstr "<0>{feedName}</0> の登録を解除してもよろしいですか?"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Asc"
|
||||
@@ -126,7 +126,7 @@ msgstr "使用可能な変数は「title」、「content」、「url」、「aut
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Back"
|
||||
msgstr "裏"
|
||||
msgstr "戻る"
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "Back to log in"
|
||||
@@ -134,11 +134,15 @@ msgstr "ログインに戻る"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Browser extension required for Chrome"
|
||||
msgstr ""
|
||||
msgstr "Chromeのブラウザー拡張が必要です"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Browser extention"
|
||||
msgstr ""
|
||||
msgstr "ブラウザー拡張"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Browser tab"
|
||||
msgstr "ブラウザータブ"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
@@ -166,7 +170,7 @@ msgstr "カテゴリー"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Changing password will generate a new API key"
|
||||
msgstr "パスワードを変更すると、新しい API キーが生成されます"
|
||||
msgstr "パスワードを変更すると、新しいAPIキーが生成されます"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Check that the feed is working"
|
||||
@@ -174,23 +178,27 @@ msgstr "フィードが動作していることを確認してください"
|
||||
|
||||
#: src/pages/app/Layout.tsx
|
||||
msgid "Close menu"
|
||||
msgstr ""
|
||||
msgstr "メニューを閉じる"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Cmd"
|
||||
msgstr "Cmd"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
msgstr "CommaFeed ブラウザー拡張機能のバージョンは {browserExtensionVersion} です。"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||
msgstr ""
|
||||
msgstr "CommaFeedはFever APIと互換性があります。Fever互換のモバイルクライアントで次のURLを使用してください。ユーザー名と <0>API キー</0> でログインしてください。"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed next unread item"
|
||||
msgstr "次の未読アイテムをカンマフィード"
|
||||
msgstr "CommaFeed 次の未読アイテム"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed version {version} ({revision})."
|
||||
msgstr ""
|
||||
msgstr "CommaFeed バージョン {version} ({revision})。"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Compact"
|
||||
@@ -210,11 +218,11 @@ msgstr "パスワード確認"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Cozy"
|
||||
msgstr "コージー"
|
||||
msgstr "Cozy"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Ctrl"
|
||||
msgstr "コントロール"
|
||||
msgstr "Ctrl"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Current password"
|
||||
@@ -222,19 +230,19 @@ msgstr "現在のパスワード"
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Custom code"
|
||||
msgstr ""
|
||||
msgstr "カスタムコード"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "Custom CSS rules that will be applied"
|
||||
msgstr ""
|
||||
msgstr "適用されるカスタムCSSルール"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "Custom JS code that will be executed on page load"
|
||||
msgstr ""
|
||||
msgstr "ページ読み込み時に実行されるカスタムJSコード"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Dark"
|
||||
msgstr ""
|
||||
msgstr "ダーク"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Date created"
|
||||
@@ -251,7 +259,7 @@ msgstr "アカウント削除"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Delete Category"
|
||||
msgstr "カテゴリを削除"
|
||||
msgstr "カテゴリーを削除"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Delete user"
|
||||
@@ -263,7 +271,7 @@ msgstr "説明"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Detailed"
|
||||
msgstr ""
|
||||
msgstr "詳細"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
@@ -273,7 +281,7 @@ msgstr "ディスプレイ"
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/app/DonatePage.tsx
|
||||
msgid "Donate"
|
||||
msgstr ""
|
||||
msgstr "寄付"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Download"
|
||||
@@ -298,7 +306,7 @@ msgstr "メールアドレス"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Edit user"
|
||||
msgstr "ユーザー編集"
|
||||
msgstr "ユーザーの編集"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
@@ -313,9 +321,13 @@ msgstr "入力"
|
||||
msgid "Enter your current password to change profile settings"
|
||||
msgstr "プロファイル設定を変更するには、現在のパスワードを入力してください"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entries to keep above the selected entry when scrolling"
|
||||
msgstr "エントリーを選択したとき、読みやすさに応じたスクロール調整を行います。"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entry headers"
|
||||
msgstr ""
|
||||
msgstr "エントリーヘッダー"
|
||||
|
||||
#: src/components/Alert.tsx
|
||||
msgid "Error"
|
||||
@@ -323,7 +335,7 @@ msgstr "エラー"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Example: {example}."
|
||||
msgstr "例: {例}."
|
||||
msgstr "例: {example}."
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Expanded"
|
||||
@@ -336,7 +348,7 @@ msgstr "サブスクリプションとカテゴリを、他のフィード読み
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
msgid "Extension options"
|
||||
msgstr ""
|
||||
msgstr "拡張機能オプション"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Feed name"
|
||||
@@ -350,43 +362,47 @@ msgstr "フィード URL"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Fetch all my feeds now"
|
||||
msgstr ""
|
||||
msgstr "すべてのフィードを今すぐ取得"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Fever API"
|
||||
msgstr ""
|
||||
msgstr "Fever API"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Fever API URL"
|
||||
msgstr ""
|
||||
msgstr "Fever API URL"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Filtering expression"
|
||||
msgstr "フィルタリング式"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Force fetching feeds is not yet available."
|
||||
msgstr "フィードの強制フェッチはまだ利用できません。"
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Forgot password?"
|
||||
msgstr "パスワードをお忘れですか?"
|
||||
msgstr "パスワードをお忘れですか?"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
msgid "Generate an API key in your profile first."
|
||||
msgstr "最初にプロファイルで API キーを生成します。"
|
||||
msgstr "最初にプロファイルでAPIキーを生成します。"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Generate new API key"
|
||||
msgstr "新しい API キーを生成する"
|
||||
msgstr "新しいAPIキーを生成する"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
msgid "Generated feed url"
|
||||
msgstr "生成されたフィード URL"
|
||||
msgstr "生成されたフィードURL"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Go to {0}"
|
||||
msgstr ""
|
||||
msgstr "{0} に移動"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Go to the All view"
|
||||
@@ -394,11 +410,11 @@ msgstr "すべてのビューに移動"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Go to the API documentation."
|
||||
msgstr "API ドキュメントに移動します。"
|
||||
msgstr "APIドキュメントに移動します。"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Goodies"
|
||||
msgstr "グッディーズ"
|
||||
msgstr "グッズ"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Id"
|
||||
@@ -406,15 +422,15 @@ msgstr "ID"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
|
||||
msgstr "空でない場合は、'true' または 'false' に評価される式。 "
|
||||
msgstr "空でない場合は、'true' または 'false' に評価される式。 'false' の場合、このフィードの新しいエントリーは自動的に既読としてマークされます。"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
msgstr ""
|
||||
msgstr "エントリーが画面に完全に収まらない場合"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "If you encounter an issue, please report it on the issues page of the GitHub project."
|
||||
msgstr "問題が発生した場合は、GitHub プロジェクトの問題ページで報告してください。"
|
||||
msgstr "問題が発生した場合は、GitHubプロジェクトのissuesページで報告してください。"
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "Import"
|
||||
@@ -422,7 +438,7 @@ msgstr "インポート"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "In expanded view, scrolling through entries mark them as read"
|
||||
msgstr "展開ビューでエントリをスクロールすると、それらが既読としてマークされます"
|
||||
msgstr "展開ビューでエントリーをスクロールすると、それらが既読としてマークされます"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
@@ -452,7 +468,7 @@ msgstr "最終更新メッセージ"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Light"
|
||||
msgstr ""
|
||||
msgstr "ライト"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
@@ -488,7 +504,7 @@ msgstr "ログアウト"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Long press"
|
||||
msgstr ""
|
||||
msgstr "長押し"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
@@ -502,7 +518,7 @@ msgstr "すべて既読にする"
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Mark all entries as read"
|
||||
msgstr "すべてのエントリを既読にする"
|
||||
msgstr "すべてのエントリーを既読にする"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
@@ -516,11 +532,11 @@ msgstr "ここまで既読にする"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Metrics"
|
||||
msgstr "メトリクス"
|
||||
msgstr "メトリックス"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Middle click"
|
||||
msgstr ""
|
||||
msgstr "中クリック"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Move the page down"
|
||||
@@ -549,7 +565,7 @@ msgstr "名前を入力してサブスクリプションに移動します"
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
msgstr ""
|
||||
msgstr "しない"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "New password"
|
||||
@@ -574,11 +590,11 @@ msgstr "次の未読アイテムのブックマークレット"
|
||||
|
||||
#: src/pages/app/FeedEntriesPage.tsx
|
||||
msgid "No more entries"
|
||||
msgstr "これ以上エントリはありません"
|
||||
msgstr "これ以上エントリーはありません"
|
||||
|
||||
#: src/components/content/ShareButtons.tsx
|
||||
msgid "No sharing options available."
|
||||
msgstr ""
|
||||
msgstr "共有オプションは利用できません。"
|
||||
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
msgid "Nothing found"
|
||||
@@ -590,15 +606,19 @@ msgstr "古い順"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On desktop"
|
||||
msgstr ""
|
||||
msgstr "デスクトップ"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On mobile"
|
||||
msgstr ""
|
||||
msgstr "モバイル"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr ""
|
||||
msgstr "モバイルでは、画面の下部にアクションボタンを表示します"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Only applies to compact, cozy and detailed modes"
|
||||
msgstr "これはコンパクト/cozy/詳細モードでのみ適用されます"
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
@@ -606,15 +626,15 @@ msgstr "おっと!"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Open CommaFeed"
|
||||
msgstr ""
|
||||
msgstr "CommaFeedを開く"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open current entry in a new tab"
|
||||
msgstr "現在のエントリを新しいタブで開く"
|
||||
msgstr "現在のエントリーを新しいタブで開く"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open current entry in a new tab in the background"
|
||||
msgstr "現在のエントリをバックグラウンドで新しいタブで開く"
|
||||
msgstr "現在のエントリーを新しいバックグラウンドタブで開く"
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
#: src/components/content/header/OpenExternalLink.tsx
|
||||
@@ -623,31 +643,31 @@ msgstr "リンクを開く"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Open link in new background tab"
|
||||
msgstr ""
|
||||
msgstr "リンクを新しいバックグラウンドタブで開く"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Open link in new tab"
|
||||
msgstr ""
|
||||
msgstr "リンクを新しいタブで開く"
|
||||
|
||||
#: src/pages/app/Layout.tsx
|
||||
msgid "Open menu"
|
||||
msgstr ""
|
||||
msgstr "メニューを開く"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open next entry"
|
||||
msgstr "次のエントリを開く"
|
||||
msgstr "次のエントリーを開く"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open previous entry"
|
||||
msgstr "前のエントリを開く"
|
||||
msgstr "前のエントリーを開く"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open/close current entry"
|
||||
msgstr "現在のエントリを開く/閉じる"
|
||||
msgstr "現在のエントリーを開く/閉じる"
|
||||
|
||||
#: src/pages/app/AddPage.tsx
|
||||
msgid "OPML"
|
||||
msgstr ""
|
||||
msgstr "OPML"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "OPML export"
|
||||
@@ -660,7 +680,7 @@ msgstr "OPMLファイル"
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "OPML file is required"
|
||||
msgstr ""
|
||||
msgstr "OPMLファイルは必要です"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Order"
|
||||
@@ -697,7 +717,7 @@ msgstr "位置"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Previous"
|
||||
msgstr ""
|
||||
msgstr "前へ"
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Profile"
|
||||
@@ -714,16 +734,16 @@ msgstr "リフレッシュ"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "Registrations are closed on this CommaFeed instance"
|
||||
msgstr "この CommaFeed インスタンスの登録は終了しています"
|
||||
msgstr "このCommaFeedインスタンスの登録は終了しています"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "REST API"
|
||||
msgstr ""
|
||||
msgstr "REST API"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Right click"
|
||||
msgstr ""
|
||||
msgstr "右クリック"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
@@ -735,15 +755,15 @@ msgstr "保存"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll selected entry to the top of the page"
|
||||
msgstr ""
|
||||
msgstr "エントリーを選択したときのスクロール調整"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll smoothly when navigating between entries"
|
||||
msgstr "エントリ間を移動するときにスムーズにスクロールする"
|
||||
msgstr "エントリー間を移動するときにスムーズにスクロールする"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scrolling"
|
||||
msgstr ""
|
||||
msgstr "スクロール"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
@@ -754,15 +774,15 @@ msgstr "検索"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Search requires at least 3 characters"
|
||||
msgstr "検索には少なくとも 3 文字が必要です"
|
||||
msgstr "検索には少なくとも3文字が必要です"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Set focus on next entry without opening it"
|
||||
msgstr "次のエントリを開かずにフォーカスを設定する"
|
||||
msgstr "次のエントリーを開かずにフォーカスする"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Set focus on previous entry without opening it"
|
||||
msgstr "前のエントリを開かずにフォーカスを設定する"
|
||||
msgstr "前のエントリーを開かずにフォーカスする"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Settings"
|
||||
@@ -784,31 +804,31 @@ msgstr "共有サイト"
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Shift"
|
||||
msgstr "シフト"
|
||||
msgstr "Shift"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show CommaFeed's own context menu on right click"
|
||||
msgstr ""
|
||||
msgstr "右クリックでCommaFeedのコンテキストメニューを表示する"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show confirmation when marking all entries as read"
|
||||
msgstr ""
|
||||
msgstr "すべてのエントリーを既読にするときに確認を表示する"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show entry menu (desktop)"
|
||||
msgstr ""
|
||||
msgstr "エントリーメニューを表示する(デスクトップ)"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show entry menu (mobile)"
|
||||
msgstr ""
|
||||
msgstr "エントリーメニューを表示する(モバイル)"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show external link icon"
|
||||
msgstr ""
|
||||
msgstr "外部リンクアイコンを表示する"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show feeds and categories with no unread entries"
|
||||
msgstr "未読エントリのないフィードとカテゴリを表示する"
|
||||
msgstr "未読エントリーのないフィードとカテゴリーを表示する"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show keyboard shortcut help"
|
||||
@@ -816,11 +836,19 @@ msgstr "キーボード ショートカットのヘルプを表示"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show native menu (desktop)"
|
||||
msgstr ""
|
||||
msgstr "ネイティブメニューを表示する(デスクトップ)"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show star icon"
|
||||
msgstr ""
|
||||
msgstr "スターアイコンを表示する"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab favicon"
|
||||
msgstr "未読数をタブのアイコンに表示する"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab title"
|
||||
msgstr "未読数をタブのタイトルに表示する"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
@@ -830,12 +858,12 @@ msgstr "サインアップ"
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Something bad just happened..."
|
||||
msgstr "何か悪いことが起こった..."
|
||||
msgstr "何か悪いことが起きました..."
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Space"
|
||||
msgstr "スペース"
|
||||
msgstr "Space"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
@@ -868,7 +896,7 @@ msgstr "成功"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Swipe header to the left"
|
||||
msgstr ""
|
||||
msgstr "ヘッダーを左にスワイプ"
|
||||
|
||||
#: src/pages/WelcomePage.tsx
|
||||
msgid "Switch to dark theme"
|
||||
@@ -880,7 +908,7 @@ msgstr "ライトテーマに切り替え"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "System"
|
||||
msgstr ""
|
||||
msgstr "システム"
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
@@ -889,7 +917,7 @@ msgstr "タグ"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "The URL for the feed you want to subscribe to. You can also use the website's url directly and CommaFeed will try to find the feed in the page."
|
||||
msgstr "購読したいフィードのURL。 "
|
||||
msgstr "購読したいフィードのURL。ウェブサイトのURLを直接使用して、CommaFeedはページ内のフィードを検索します。"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Theme"
|
||||
@@ -897,27 +925,27 @@ msgstr "テーマ"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
||||
msgstr ""
|
||||
msgstr "これはあなたのAPIキーです。いくつかの読み取り専用API操作に使用できます。これにより、Fever APIへのアクセスが可能になります。ページの下部のフォームを使用して新しいAPIキーを生成します。"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "現在のエントリの読み取りステータスを切り替えます"
|
||||
msgstr "現在のエントリーの読み取りステータスを切り替えます"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle sidebar"
|
||||
msgstr ""
|
||||
msgstr "サイドバーを切り替える"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle starred status of current entry"
|
||||
msgstr ""
|
||||
msgstr "現在のエントリーのスターステータスを切り替える"
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
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 ""
|
||||
msgstr "デモを試す!"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Unread"
|
||||
@@ -953,8 +981,8 @@ msgstr "ウェブサイト"
|
||||
|
||||
#: src/pages/app/FeedEntriesPage.tsx
|
||||
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 "まだサブスクリプションがありません。上部の + 記号をクリックして1つ追加してみませんか?"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Your feeds have been queued for refresh."
|
||||
msgstr ""
|
||||
msgstr "フィードの更新がキューに登録されました。"
|
||||
|
||||
@@ -140,6 +140,10 @@ msgstr ""
|
||||
msgid "Browser extention"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Browser tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -176,6 +180,10 @@ msgstr "피드가 작동하는지 확인"
|
||||
msgid "Close menu"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Cmd"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
@@ -313,6 +321,10 @@ msgstr "입력"
|
||||
msgid "Enter your current password to change profile settings"
|
||||
msgstr "프로필 설정을 변경하려면 현재 비밀번호를 입력하세요."
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entries to keep above the selected entry when scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entry headers"
|
||||
msgstr ""
|
||||
@@ -364,6 +376,10 @@ msgstr ""
|
||||
msgid "Filtering expression"
|
||||
msgstr "필터링 표현식"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Force fetching feeds is not yet available."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Forgot password?"
|
||||
msgstr "비밀번호를 잊으셨나요?"
|
||||
@@ -600,6 +616,10 @@ msgstr ""
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Only applies to compact, cozy and detailed modes"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "앗!"
|
||||
@@ -822,6 +842,14 @@ msgstr ""
|
||||
msgid "Show star icon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab favicon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab title"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
|
||||
@@ -140,6 +140,10 @@ msgstr ""
|
||||
msgid "Browser extention"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Browser tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -176,6 +180,10 @@ msgstr "Semak sama ada suapan berfungsi"
|
||||
msgid "Close menu"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Cmd"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
@@ -313,6 +321,10 @@ msgstr "Masuk"
|
||||
msgid "Enter your current password to change profile settings"
|
||||
msgstr "Masukkan kata laluan semasa anda untuk menukar tetapan profil"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entries to keep above the selected entry when scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entry headers"
|
||||
msgstr ""
|
||||
@@ -364,6 +376,10 @@ msgstr ""
|
||||
msgid "Filtering expression"
|
||||
msgstr "Ungkapan penapisan"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Force fetching feeds is not yet available."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Forgot password?"
|
||||
msgstr "Lupa kata laluan?"
|
||||
@@ -600,6 +616,10 @@ msgstr ""
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Only applies to compact, cozy and detailed modes"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Aduh!"
|
||||
@@ -822,6 +842,14 @@ msgstr ""
|
||||
msgid "Show star icon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab favicon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab title"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
|
||||
@@ -140,6 +140,10 @@ msgstr ""
|
||||
msgid "Browser extention"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Browser tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -176,6 +180,10 @@ msgstr "Sjekk at feeden fungerer"
|
||||
msgid "Close menu"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Cmd"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
@@ -313,6 +321,10 @@ msgstr ""
|
||||
msgid "Enter your current password to change profile settings"
|
||||
msgstr "Skriv inn ditt nåværende passord for å endre profilinnstillinger"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entries to keep above the selected entry when scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entry headers"
|
||||
msgstr ""
|
||||
@@ -364,6 +376,10 @@ msgstr ""
|
||||
msgid "Filtering expression"
|
||||
msgstr "Filtrerende uttrykk"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Force fetching feeds is not yet available."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Forgot password?"
|
||||
msgstr "Glemt passord?"
|
||||
@@ -600,6 +616,10 @@ msgstr ""
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Only applies to compact, cozy and detailed modes"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Beklager!"
|
||||
@@ -822,6 +842,14 @@ msgstr ""
|
||||
msgid "Show star icon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab favicon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab title"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
|
||||
@@ -140,6 +140,10 @@ msgstr ""
|
||||
msgid "Browser extention"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Browser tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -176,6 +180,10 @@ msgstr "Controleer of de feed werkt"
|
||||
msgid "Close menu"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Cmd"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
@@ -313,6 +321,10 @@ msgstr ""
|
||||
msgid "Enter your current password to change profile settings"
|
||||
msgstr "Voer uw huidige wachtwoord in om de profielinstellingen te wijzigen"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entries to keep above the selected entry when scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entry headers"
|
||||
msgstr ""
|
||||
@@ -364,6 +376,10 @@ msgstr ""
|
||||
msgid "Filtering expression"
|
||||
msgstr "Uitdrukking filteren"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Force fetching feeds is not yet available."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Forgot password?"
|
||||
msgstr "Wachtwoord vergeten?"
|
||||
@@ -600,6 +616,10 @@ msgstr ""
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Only applies to compact, cozy and detailed modes"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Oeps!"
|
||||
@@ -822,6 +842,14 @@ msgstr ""
|
||||
msgid "Show star icon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab favicon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab title"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
|
||||
@@ -140,6 +140,10 @@ msgstr ""
|
||||
msgid "Browser extention"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Browser tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -176,6 +180,10 @@ msgstr "Sjekk at feeden fungerer"
|
||||
msgid "Close menu"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Cmd"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
@@ -313,6 +321,10 @@ msgstr ""
|
||||
msgid "Enter your current password to change profile settings"
|
||||
msgstr "Skriv inn ditt nåværende passord for å endre profilinnstillinger"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entries to keep above the selected entry when scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entry headers"
|
||||
msgstr ""
|
||||
@@ -364,6 +376,10 @@ msgstr ""
|
||||
msgid "Filtering expression"
|
||||
msgstr "Filtrerende uttrykk"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Force fetching feeds is not yet available."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Forgot password?"
|
||||
msgstr "Glemt passord?"
|
||||
@@ -600,6 +616,10 @@ msgstr ""
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Only applies to compact, cozy and detailed modes"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Beklager!"
|
||||
@@ -822,6 +842,14 @@ msgstr ""
|
||||
msgid "Show star icon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab favicon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab title"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
|
||||
@@ -140,6 +140,10 @@ msgstr ""
|
||||
msgid "Browser extention"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Browser tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -176,6 +180,10 @@ msgstr "Sprawdź, czy kanał działa"
|
||||
msgid "Close menu"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Cmd"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
@@ -313,6 +321,10 @@ msgstr "Wprowadź"
|
||||
msgid "Enter your current password to change profile settings"
|
||||
msgstr "Wprowadź swoje aktualne hasło, aby zmienić ustawienia profilu"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entries to keep above the selected entry when scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entry headers"
|
||||
msgstr ""
|
||||
@@ -364,6 +376,10 @@ msgstr ""
|
||||
msgid "Filtering expression"
|
||||
msgstr "Wyrażenie filtrujące"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Force fetching feeds is not yet available."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Forgot password?"
|
||||
msgstr "Zapomniałeś hasła?"
|
||||
@@ -600,6 +616,10 @@ msgstr ""
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Only applies to compact, cozy and detailed modes"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Ups!"
|
||||
@@ -822,6 +842,14 @@ msgstr ""
|
||||
msgid "Show star icon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab favicon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab title"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
|
||||
@@ -140,6 +140,10 @@ msgstr ""
|
||||
msgid "Browser extention"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Browser tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -176,6 +180,10 @@ msgstr "Verifique se o feed está funcionando"
|
||||
msgid "Close menu"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Cmd"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
@@ -313,6 +321,10 @@ msgstr "Entrar"
|
||||
msgid "Enter your current password to change profile settings"
|
||||
msgstr "Digite sua senha atual para alterar as configurações do perfil"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entries to keep above the selected entry when scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entry headers"
|
||||
msgstr ""
|
||||
@@ -364,6 +376,10 @@ msgstr ""
|
||||
msgid "Filtering expression"
|
||||
msgstr "Filtrando expressão"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Force fetching feeds is not yet available."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Forgot password?"
|
||||
msgstr "Esqueceu a senha?"
|
||||
@@ -600,6 +616,10 @@ msgstr ""
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Only applies to compact, cozy and detailed modes"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Opa!"
|
||||
@@ -822,6 +842,14 @@ msgstr ""
|
||||
msgid "Show star icon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab favicon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab title"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
|
||||
@@ -140,6 +140,10 @@ msgstr "Для браузера Chrome требуется расширение"
|
||||
msgid "Browser extention"
|
||||
msgstr "Расширение для браузера"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Browser tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -176,6 +180,10 @@ msgstr "Проверьте, работает ли лента."
|
||||
msgid "Close menu"
|
||||
msgstr "Закрыть меню"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Cmd"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr "Версия расширения браузера CommaFeed {browserExtensionVersion}."
|
||||
@@ -313,6 +321,10 @@ msgstr "Ввод"
|
||||
msgid "Enter your current password to change profile settings"
|
||||
msgstr "Введите текущий пароль, чтобы изменить настройки профиля."
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entries to keep above the selected entry when scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entry headers"
|
||||
msgstr ""
|
||||
@@ -364,6 +376,10 @@ msgstr "Ссылка Fever API"
|
||||
msgid "Filtering expression"
|
||||
msgstr "Выражение фильтрации"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Force fetching feeds is not yet available."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Forgot password?"
|
||||
msgstr "Забыли пароль?"
|
||||
@@ -600,6 +616,10 @@ msgstr ""
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr "На мобильных устройствах отображать кнопки действий в нижней части экрана"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Only applies to compact, cozy and detailed modes"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Ой!"
|
||||
@@ -822,6 +842,14 @@ msgstr "Показать родное меню (ПК)"
|
||||
msgid "Show star icon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab favicon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab title"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
|
||||
@@ -140,6 +140,10 @@ msgstr ""
|
||||
msgid "Browser extention"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Browser tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -176,6 +180,10 @@ msgstr "Skontrolujte, či feed funguje"
|
||||
msgid "Close menu"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Cmd"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
@@ -313,6 +321,10 @@ msgstr "Vstúpte"
|
||||
msgid "Enter your current password to change profile settings"
|
||||
msgstr "Ak chcete zmeniť nastavenia profilu, zadajte svoje aktuálne heslo"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entries to keep above the selected entry when scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entry headers"
|
||||
msgstr ""
|
||||
@@ -364,6 +376,10 @@ msgstr ""
|
||||
msgid "Filtering expression"
|
||||
msgstr "Filtrovanie výrazu"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Force fetching feeds is not yet available."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Forgot password?"
|
||||
msgstr "Zabudli ste heslo?"
|
||||
@@ -600,6 +616,10 @@ msgstr ""
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Only applies to compact, cozy and detailed modes"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Ojoj!"
|
||||
@@ -822,6 +842,14 @@ msgstr ""
|
||||
msgid "Show star icon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab favicon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab title"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
|
||||
@@ -140,6 +140,10 @@ msgstr ""
|
||||
msgid "Browser extention"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Browser tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -176,6 +180,10 @@ msgstr "Kontrollera att matningen fungerar"
|
||||
msgid "Close menu"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Cmd"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
@@ -313,6 +321,10 @@ msgstr ""
|
||||
msgid "Enter your current password to change profile settings"
|
||||
msgstr "Ange ditt nuvarande lösenord för att ändra profilinställningar"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entries to keep above the selected entry when scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entry headers"
|
||||
msgstr ""
|
||||
@@ -364,6 +376,10 @@ msgstr ""
|
||||
msgid "Filtering expression"
|
||||
msgstr "Filtrerande uttryck"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Force fetching feeds is not yet available."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Forgot password?"
|
||||
msgstr "Glömt lösenord?"
|
||||
@@ -600,6 +616,10 @@ msgstr ""
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Only applies to compact, cozy and detailed modes"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Hoppsan!"
|
||||
@@ -822,6 +842,14 @@ msgstr ""
|
||||
msgid "Show star icon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab favicon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab title"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
|
||||
@@ -140,6 +140,10 @@ msgstr ""
|
||||
msgid "Browser extention"
|
||||
msgstr "Tarayıcı eklentisi"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Browser tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -176,6 +180,10 @@ msgstr "Feed'in çalışıp çalışmadığını kontrol edin"
|
||||
msgid "Close menu"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Cmd"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr "CommaFeed tarayıcı eklentisi sürüm {browserExtensionVersion}."
|
||||
@@ -313,6 +321,10 @@ msgstr "Girin"
|
||||
msgid "Enter your current password to change profile settings"
|
||||
msgstr "Profil ayarlarını değiştirmek için mevcut şifrenizi girin"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entries to keep above the selected entry when scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entry headers"
|
||||
msgstr ""
|
||||
@@ -364,6 +376,10 @@ msgstr ""
|
||||
msgid "Filtering expression"
|
||||
msgstr "Filtreleme ifadesi"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Force fetching feeds is not yet available."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Forgot password?"
|
||||
msgstr "Parolanızı mı unuttunuz?"
|
||||
@@ -600,6 +616,10 @@ msgstr ""
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Only applies to compact, cozy and detailed modes"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Hata!"
|
||||
@@ -822,6 +842,14 @@ msgstr "Orijinal tarayıcı menüsünü göster (masaüstü)"
|
||||
msgid "Show star icon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab favicon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab title"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
|
||||
@@ -140,6 +140,10 @@ msgstr "浏览器扩展"
|
||||
msgid "Browser extention"
|
||||
msgstr "浏览器扩展"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Browser tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -176,6 +180,10 @@ msgstr "检查信息流是否正常工作"
|
||||
msgid "Close menu"
|
||||
msgstr "关闭菜单"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Cmd"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr "CommaFeed的浏览器扩展版本:{browserExtensionVersion}。"
|
||||
@@ -313,6 +321,10 @@ msgstr "回车"
|
||||
msgid "Enter your current password to change profile settings"
|
||||
msgstr "输入您当前的密码以更改配置文件设置"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entries to keep above the selected entry when scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entry headers"
|
||||
msgstr "条目头部"
|
||||
@@ -364,6 +376,10 @@ msgstr "Fever API 网址"
|
||||
msgid "Filtering expression"
|
||||
msgstr "过滤表达式"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Force fetching feeds is not yet available."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Forgot password?"
|
||||
msgstr "忘记密码?"
|
||||
@@ -600,6 +616,10 @@ msgstr "移动端"
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr "在移动端,显示屏幕底部的操作按钮"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Only applies to compact, cozy and detailed modes"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "哎呀!"
|
||||
@@ -822,6 +842,14 @@ msgstr "显示原生菜单(桌面端)"
|
||||
msgid "Show star icon"
|
||||
msgstr "显示星标图标"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab favicon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab title"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
|
||||
@@ -6,11 +6,13 @@ import "react-contexify/ReactContexify.css"
|
||||
import { App } from "App"
|
||||
import { store } from "app/store"
|
||||
import dayjs from "dayjs"
|
||||
import duration from "dayjs/plugin/duration"
|
||||
import relativeTime from "dayjs/plugin/relativeTime"
|
||||
import ReactDOM from "react-dom/client"
|
||||
import { Provider } from "react-redux"
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
dayjs.extend(duration)
|
||||
|
||||
const root = document.getElementById("root")
|
||||
root &&
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Trans } from "@lingui/macro"
|
||||
import { msg } from "@lingui/macro"
|
||||
import { Anchor, Box, Center, Container, Divider, Group, Image, Space, Title, useMantineColorScheme } from "@mantine/core"
|
||||
import { client } from "app/client"
|
||||
import { redirectToApiDocumentation, redirectToLogin, redirectToRegistration, redirectToRootCategory } from "app/redirect/thunks"
|
||||
@@ -9,7 +9,7 @@ import { ActionButton } from "components/ActionButton"
|
||||
import { useBrowserExtension } from "hooks/useBrowserExtension"
|
||||
import { useMobile } from "hooks/useMobile"
|
||||
import { useAsyncCallback } from "react-async-hook"
|
||||
import { SiGithub, SiTwitter } from "react-icons/si"
|
||||
import { SiGithub, SiX } from "react-icons/si"
|
||||
import { TbClock, TbKey, TbMoon, TbSettings, TbSun, TbUserPlus } from "react-icons/tb"
|
||||
import { PageTitle } from "./PageTitle"
|
||||
|
||||
@@ -38,7 +38,7 @@ export function WelcomePage() {
|
||||
{serverInfos?.demoAccountEnabled && (
|
||||
<Center>
|
||||
<ActionButton
|
||||
label={<Trans>Try the demo!</Trans>}
|
||||
label={msg`Try the demo!`}
|
||||
icon={<TbClock size={iconSize} />}
|
||||
variant="outline"
|
||||
onClick={async () => await login.execute({ name: "demo", password: "demo" })}
|
||||
@@ -96,7 +96,7 @@ function Buttons() {
|
||||
return (
|
||||
<Group gap={14}>
|
||||
<ActionButton
|
||||
label={<Trans>Log in</Trans>}
|
||||
label={msg`Log in`}
|
||||
icon={<TbKey size={iconSize} />}
|
||||
variant="outline"
|
||||
onClick={async () => await dispatch(redirectToLogin())}
|
||||
@@ -104,7 +104,7 @@ function Buttons() {
|
||||
/>
|
||||
{serverInfos?.allowRegistrations && (
|
||||
<ActionButton
|
||||
label={<Trans>Sign up</Trans>}
|
||||
label={msg`Sign up`}
|
||||
icon={<TbUserPlus size={iconSize} />}
|
||||
variant="filled"
|
||||
onClick={async () => await dispatch(redirectToRegistration())}
|
||||
@@ -113,7 +113,7 @@ function Buttons() {
|
||||
)}
|
||||
|
||||
<ActionButton
|
||||
label={dark ? <Trans>Switch to light theme</Trans> : <Trans>Switch to dark theme</Trans>}
|
||||
label={dark ? msg`Switch to light theme` : msg`Switch to dark theme`}
|
||||
icon={colorScheme === "dark" ? <TbSun size={18} /> : <TbMoon size={iconSize} />}
|
||||
onClick={() => toggleColorScheme()}
|
||||
hideLabelOnDesktop
|
||||
@@ -121,7 +121,7 @@ function Buttons() {
|
||||
|
||||
{isBrowserExtensionPopup && (
|
||||
<ActionButton
|
||||
label={<Trans>Extension options</Trans>}
|
||||
label={msg`Extension options`}
|
||||
icon={<TbSettings size={iconSize} />}
|
||||
onClick={() => openSettingsPage()}
|
||||
hideLabelOnDesktop
|
||||
@@ -140,8 +140,8 @@ function Footer() {
|
||||
<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 variant="text" href="https://x.com/CommaFeed" target="_blank" rel="noreferrer">
|
||||
<SiX />
|
||||
</Anchor>
|
||||
</Group>
|
||||
<Box>
|
||||
|
||||
@@ -1,74 +1,65 @@
|
||||
import { Accordion, Box, Tabs } from "@mantine/core"
|
||||
import { Accordion, Box } from "@mantine/core"
|
||||
import { client } from "app/client"
|
||||
import { Loader } from "components/Loader"
|
||||
import { Gauge } from "components/metrics/Gauge"
|
||||
import { Meter } from "components/metrics/Meter"
|
||||
import { MetricAccordionItem } from "components/metrics/MetricAccordionItem"
|
||||
import { Timer } from "components/metrics/Timer"
|
||||
import { useEffect } from "react"
|
||||
import { useAsync } from "react-async-hook"
|
||||
import { TbChartAreaLine, TbClock } from "react-icons/tb"
|
||||
|
||||
const shownMeters: Record<string, string> = {
|
||||
"com.commafeed.backend.feed.FeedRefreshEngine.refill": "Feed queue refill rate",
|
||||
"com.commafeed.backend.feed.FeedRefreshWorker.feedFetched": "Feed fetching rate",
|
||||
"com.commafeed.backend.feed.FeedRefreshUpdater.feedUpdated": "Feed update rate",
|
||||
"com.commafeed.backend.feed.FeedRefreshUpdater.entryCacheHit": "Entry cache hit rate",
|
||||
"com.commafeed.backend.feed.FeedRefreshUpdater.entryCacheMiss": "Entry cache miss rate",
|
||||
"com.commafeed.backend.feed.FeedRefreshUpdater.entryInserted": "Entries inserted",
|
||||
"com.commafeed.backend.service.db.DatabaseCleaningService.entriesDeleted": "Entries deleted",
|
||||
}
|
||||
|
||||
const shownGauges: Record<string, string> = {
|
||||
"com.commafeed.backend.feed.FeedRefreshEngine.queue.size": "Queue size",
|
||||
"com.commafeed.backend.feed.FeedRefreshEngine.worker.active": "Feed Worker active",
|
||||
"com.commafeed.backend.feed.FeedRefreshEngine.updater.active": "Feed Updater active",
|
||||
"com.commafeed.backend.feed.FeedRefreshEngine.queue.size": "Feed Refresh Engine queue size",
|
||||
"com.commafeed.backend.feed.FeedRefreshEngine.worker.active": "Feed Refresh Engine active HTTP workers",
|
||||
"com.commafeed.backend.feed.FeedRefreshEngine.updater.active": "Feed Refresh Engine active database update workers",
|
||||
"com.commafeed.backend.HttpGetter.pool.max": "HttpGetter max pool size",
|
||||
"com.commafeed.backend.HttpGetter.pool.size": "HttpGetter current pool size",
|
||||
"com.commafeed.backend.HttpGetter.pool.leased": "HttpGetter active connections",
|
||||
"com.commafeed.backend.HttpGetter.pool.pending": "HttpGetter waiting for a connection",
|
||||
"com.commafeed.backend.HttpGetter.cache.size": "HttpGetter cached entries",
|
||||
"com.commafeed.backend.HttpGetter.cache.memoryUsage": "HttpGetter cache memory usage",
|
||||
"com.commafeed.frontend.ws.WebSocketSessions.users": "WebSocket users",
|
||||
"com.commafeed.frontend.ws.WebSocketSessions.sessions": "WebSocket sessions",
|
||||
}
|
||||
|
||||
export function MetricsPage() {
|
||||
const query = useAsync(async () => await client.admin.getMetrics(), [])
|
||||
const query = useAsync(async () => await client.admin.getMetrics(), [], {
|
||||
// keep previous results available while a new request is pending
|
||||
setLoading: state => ({ ...state, loading: true }),
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => query.execute(), 2000)
|
||||
return () => clearInterval(interval)
|
||||
}, [query.execute])
|
||||
|
||||
if (!query.result) return <Loader />
|
||||
const { meters, gauges, timers } = query.result.data
|
||||
const { meters, gauges } = query.result.data
|
||||
return (
|
||||
<Tabs defaultValue="stats">
|
||||
<Tabs.List>
|
||||
<Tabs.Tab value="stats" leftSection={<TbChartAreaLine size={14} />}>
|
||||
Stats
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="timers" leftSection={<TbClock size={14} />}>
|
||||
Timers
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
<>
|
||||
<Accordion variant="contained" chevronPosition="left">
|
||||
{Object.keys(shownMeters).map(m => (
|
||||
<MetricAccordionItem key={m} metricKey={m} name={shownMeters[m]} headerValue={meters[m].count}>
|
||||
<Meter meter={meters[m]} />
|
||||
</MetricAccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
|
||||
<Tabs.Panel value="stats" pt="xs">
|
||||
<Accordion variant="contained" chevronPosition="left">
|
||||
{Object.keys(shownMeters).map(m => (
|
||||
<MetricAccordionItem key={m} metricKey={m} name={shownMeters[m]} headerValue={meters[m].count}>
|
||||
<Meter meter={meters[m]} />
|
||||
</MetricAccordionItem>
|
||||
))}
|
||||
</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 value="timers" pt="xs">
|
||||
<Accordion variant="contained" chevronPosition="left">
|
||||
{Object.keys(timers).map(key => (
|
||||
<MetricAccordionItem key={key} metricKey={key} name={key} headerValue={timers[key].count}>
|
||||
<Timer timer={timers[key]} />
|
||||
</MetricAccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
<Box pt="xs">
|
||||
{Object.keys(shownGauges).map(g => (
|
||||
<Box key={g}>
|
||||
<span>{shownGauges[g]}: </span>
|
||||
<Gauge gauge={gauges[g]} />
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Trans, t } from "@lingui/macro"
|
||||
import { Trans, msg } from "@lingui/macro"
|
||||
import { useLingui } from "@lingui/react"
|
||||
import { Anchor, Box, Container, List, NativeSelect, SimpleGrid, Title } from "@mantine/core"
|
||||
import { Constants } from "app/constants"
|
||||
import { redirectToApiDocumentation } from "app/redirect/thunks"
|
||||
@@ -36,6 +37,8 @@ function Section(props: { title: React.ReactNode; icon: React.ReactNode; childre
|
||||
function NextUnreadBookmarklet() {
|
||||
const [categoryId, setCategoryId] = useState(Constants.categories.all.id)
|
||||
const [order, setOrder] = useState("desc")
|
||||
const { _ } = useLingui()
|
||||
|
||||
const baseUrl = window.location.href.substring(0, window.location.href.lastIndexOf("#"))
|
||||
const href = `javascript:window.location.href='${baseUrl}next?category=${categoryId}&order=${order}&t='+new Date().getTime();`
|
||||
|
||||
@@ -44,8 +47,8 @@ function NextUnreadBookmarklet() {
|
||||
<CategorySelect value={categoryId} onChange={c => c && setCategoryId(c)} withAll description={<Trans>Category</Trans>} />
|
||||
<NativeSelect
|
||||
data={[
|
||||
{ value: "desc", label: t`Newest first` },
|
||||
{ value: "asc", label: t`Oldest first` },
|
||||
{ value: "desc", label: _(msg`Newest first`) },
|
||||
{ value: "asc", label: _(msg`Oldest first`) },
|
||||
]}
|
||||
value={order}
|
||||
onChange={e => setOrder(e.target.value)}
|
||||
|
||||
@@ -80,7 +80,7 @@ export function FeedEntriesPage(props: FeedEntriesPageProps) {
|
||||
if (noSubscriptions) return <NoSubscriptionHelp />
|
||||
return (
|
||||
// add some room at the bottom of the page in order to be able to scroll the current entry at the top of the page when expanding
|
||||
<Box mb={viewport.height * 0.75}>
|
||||
<Box mb={viewport.height * 0.7}>
|
||||
<Group gap="xl">
|
||||
{sourceWebsiteUrl && (
|
||||
<a href={sourceWebsiteUrl} target="_blank" rel="noreferrer" className={classes.sourceWebsiteLink}>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Trans } from "@lingui/macro"
|
||||
import { msg } from "@lingui/macro"
|
||||
import { ActionIcon, AppShell, Box, Center, Group, ScrollArea, Title, useMantineTheme } from "@mantine/core"
|
||||
import { Constants } from "app/constants"
|
||||
import { redirectToAdd, redirectToRootCategory } from "app/redirect/thunks"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import { setMobileMenuOpen } from "app/tree/slice"
|
||||
import { reloadTree } from "app/tree/thunks"
|
||||
import { setSidebarWidth } from "app/user/slice"
|
||||
import { reloadProfile, reloadSettings, reloadTags } from "app/user/thunks"
|
||||
import { ActionButton } from "components/ActionButton"
|
||||
import { AnnouncementDialog } from "components/AnnouncementDialog"
|
||||
@@ -17,13 +18,12 @@ import { useBrowserExtension } from "hooks/useBrowserExtension"
|
||||
import { useMobile } from "hooks/useMobile"
|
||||
import { useWebSocket } from "hooks/useWebSocket"
|
||||
import { LoadingPage } from "pages/LoadingPage"
|
||||
import { type ReactNode, Suspense, useEffect } from "react"
|
||||
import { type ReactNode, Suspense, useEffect, useRef } from "react"
|
||||
import Draggable from "react-draggable"
|
||||
import { TbMenu2, TbPlus, TbX } from "react-icons/tb"
|
||||
import { Outlet } from "react-router-dom"
|
||||
import { useSwipeable } from "react-swipeable"
|
||||
import { tss } from "tss"
|
||||
import useLocalStorage from "use-local-storage"
|
||||
|
||||
interface LayoutProps {
|
||||
sidebar: ReactNode
|
||||
@@ -64,21 +64,24 @@ export default function Layout(props: LayoutProps) {
|
||||
const theme = useMantineTheme()
|
||||
const mobile = useMobile()
|
||||
const { isBrowserExtensionPopup } = useBrowserExtension()
|
||||
const [sidebarWidth, setSidebarWidth] = useLocalStorage("sidebar-width", 350)
|
||||
const draggableSeparator = useRef<HTMLDivElement>(null)
|
||||
|
||||
const { loading } = useAppLoading()
|
||||
const mobileMenuOpen = useAppSelector(state => state.tree.mobileMenuOpen)
|
||||
const webSocketConnected = useAppSelector(state => state.server.webSocketConnected)
|
||||
const treeReloadInterval = useAppSelector(state => state.server.serverInfos?.treeReloadInterval)
|
||||
const mobileFooter = useAppSelector(state => state.user.settings?.mobileFooter)
|
||||
const sidebarWidth = useAppSelector(state => state.user.localSettings.sidebarWidth)
|
||||
const headerInFooter = mobile && !isBrowserExtensionPopup && mobileFooter
|
||||
const dispatch = useAppDispatch()
|
||||
useWebSocket()
|
||||
|
||||
const sidebarPadding = theme.spacing.xs
|
||||
const { classes } = useStyles({
|
||||
sidebarWidth,
|
||||
sidebarPadding,
|
||||
sidebarRightBorderWidth: "1px",
|
||||
})
|
||||
const { loading } = useAppLoading()
|
||||
const mobileMenuOpen = useAppSelector(state => state.tree.mobileMenuOpen)
|
||||
const webSocketConnected = useAppSelector(state => state.server.webSocketConnected)
|
||||
const treeReloadInterval = useAppSelector(state => state.server.serverInfos?.treeReloadInterval)
|
||||
const mobileFooter = useAppSelector(state => state.user.settings?.mobileFooter)
|
||||
const headerInFooter = mobile && !isBrowserExtensionPopup && mobileFooter
|
||||
const dispatch = useAppDispatch()
|
||||
useWebSocket()
|
||||
|
||||
useEffect(() => {
|
||||
// load initial data
|
||||
@@ -101,7 +104,7 @@ export default function Layout(props: LayoutProps) {
|
||||
|
||||
const burger = (
|
||||
<ActionButton
|
||||
label={mobileMenuOpen ? <Trans>Close menu</Trans> : <Trans>Open menu</Trans>}
|
||||
label={mobileMenuOpen ? msg`Close menu` : msg`Open menu`}
|
||||
icon={mobileMenuOpen ? <TbX size={18} /> : <TbMenu2 size={18} />}
|
||||
onClick={() => dispatch(setMobileMenuOpen(!mobileMenuOpen))}
|
||||
/>
|
||||
@@ -182,6 +185,7 @@ export default function Layout(props: LayoutProps) {
|
||||
</AppShell.Navbar>
|
||||
<OnDesktop>
|
||||
<Draggable
|
||||
nodeRef={draggableSeparator}
|
||||
axis="x"
|
||||
defaultPosition={{
|
||||
x: sidebarWidth,
|
||||
@@ -192,9 +196,13 @@ export default function Layout(props: LayoutProps) {
|
||||
right: 1000,
|
||||
}}
|
||||
grid={[30, 30]}
|
||||
onDrag={(_e, data) => setSidebarWidth(data.x)}
|
||||
onDrag={(_e, data) => {
|
||||
dispatch(setSidebarWidth(data.x))
|
||||
return
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
ref={draggableSeparator}
|
||||
style={{
|
||||
position: "fixed",
|
||||
height: "100%",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Trans, t } from "@lingui/macro"
|
||||
import { Trans, msg } from "@lingui/macro"
|
||||
import { useLingui } from "@lingui/react"
|
||||
import { Anchor, Box, Button, Center, Container, Group, Paper, PasswordInput, Stack, TextInput, Title } from "@mantine/core"
|
||||
import { useForm } from "@mantine/form"
|
||||
import { client, errorToStrings } from "app/client"
|
||||
@@ -13,6 +14,7 @@ import { Link } from "react-router-dom"
|
||||
export function LoginPage() {
|
||||
const serverInfos = useAppSelector(state => state.server.serverInfos)
|
||||
const dispatch = useAppDispatch()
|
||||
const { _ } = useLingui()
|
||||
|
||||
const form = useForm<LoginRequest>({
|
||||
initialValues: {
|
||||
@@ -43,7 +45,7 @@ export function LoginPage() {
|
||||
<Stack>
|
||||
<TextInput
|
||||
label={<Trans>User Name or E-mail</Trans>}
|
||||
placeholder={t`User Name or E-mail`}
|
||||
placeholder={_(msg`User Name or E-mail`)}
|
||||
{...form.getInputProps("name")}
|
||||
description={
|
||||
serverInfos?.demoAccountEnabled ? <Trans>Try out CommaFeed with the demo account: demo/demo</Trans> : ""
|
||||
@@ -54,7 +56,7 @@ export function LoginPage() {
|
||||
/>
|
||||
<PasswordInput
|
||||
label={<Trans>Password</Trans>}
|
||||
placeholder={t`Password`}
|
||||
placeholder={_(msg`Password`)}
|
||||
{...form.getInputProps("password")}
|
||||
size="md"
|
||||
required
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Trans, t } from "@lingui/macro"
|
||||
import { Trans, msg } from "@lingui/macro"
|
||||
import { useLingui } from "@lingui/react"
|
||||
import { Anchor, Box, Button, Center, Container, Group, Paper, Stack, TextInput, Title } from "@mantine/core"
|
||||
import { useForm } from "@mantine/form"
|
||||
import { client, errorToStrings } from "app/client"
|
||||
@@ -11,6 +12,7 @@ import { Link } from "react-router-dom"
|
||||
|
||||
export function PasswordRecoveryPage() {
|
||||
const [message, setMessage] = useState("")
|
||||
const { _ } = useLingui()
|
||||
|
||||
const form = useForm<PasswordResetRequest>({
|
||||
initialValues: {
|
||||
@@ -20,7 +22,7 @@ export function PasswordRecoveryPage() {
|
||||
|
||||
const recoverPassword = useAsyncCallback(client.user.passwordReset, {
|
||||
onSuccess: () => {
|
||||
setMessage(t`An email has been sent if this address was registered. Check your inbox.`)
|
||||
setMessage(_(msg`An email has been sent if this address was registered. Check your inbox.`))
|
||||
},
|
||||
})
|
||||
|
||||
@@ -54,7 +56,7 @@ export function PasswordRecoveryPage() {
|
||||
<TextInput
|
||||
type="email"
|
||||
label={<Trans>E-mail</Trans>}
|
||||
placeholder={t`E-mail`}
|
||||
placeholder={_(msg`E-mail`)}
|
||||
{...form.getInputProps("email")}
|
||||
size="md"
|
||||
required
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Trans, t } from "@lingui/macro"
|
||||
import { Trans, msg } from "@lingui/macro"
|
||||
import { useLingui } from "@lingui/react"
|
||||
import { Anchor, Box, Button, Center, Container, Group, Paper, PasswordInput, Stack, TextInput, Title } from "@mantine/core"
|
||||
import { useForm } from "@mantine/form"
|
||||
import { client, errorToStrings } from "app/client"
|
||||
@@ -13,6 +14,7 @@ import { Link } from "react-router-dom"
|
||||
export function RegistrationPage() {
|
||||
const serverInfos = useAppSelector(state => state.server.serverInfos)
|
||||
const dispatch = useAppDispatch()
|
||||
const { _ } = useLingui()
|
||||
|
||||
const form = useForm<RegistrationRequest>({
|
||||
initialValues: {
|
||||
@@ -22,12 +24,18 @@ export function RegistrationPage() {
|
||||
},
|
||||
})
|
||||
|
||||
const register = useAsyncCallback(client.user.register, {
|
||||
const login = useAsyncCallback(client.user.login, {
|
||||
onSuccess: () => {
|
||||
dispatch(redirectToRootCategory())
|
||||
},
|
||||
})
|
||||
|
||||
const register = useAsyncCallback(client.user.register, {
|
||||
onSuccess: () => {
|
||||
login.execute(form.values)
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<Container size="xs">
|
||||
<PageTitle />
|
||||
@@ -37,7 +45,7 @@ export function RegistrationPage() {
|
||||
</Title>
|
||||
{serverInfos && !serverInfos.allowRegistrations && (
|
||||
<Box mb="md">
|
||||
<Alert messages={[t`Registrations are closed on this CommaFeed instance`]} />
|
||||
<Alert messages={[_(msg`Registrations are closed on this CommaFeed instance`)]} />
|
||||
</Box>
|
||||
)}
|
||||
{serverInfos?.allowRegistrations && (
|
||||
@@ -48,25 +56,31 @@ export function RegistrationPage() {
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{login.error && (
|
||||
<Box mb="md">
|
||||
<Alert messages={errorToStrings(login.error)} />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<form onSubmit={form.onSubmit(register.execute)}>
|
||||
<Stack>
|
||||
<TextInput label="User Name" placeholder="User Name" {...form.getInputProps("name")} size="md" required />
|
||||
<TextInput
|
||||
type="email"
|
||||
label={<Trans>E-mail address</Trans>}
|
||||
placeholder={t`E-mail address`}
|
||||
placeholder={_(msg`E-mail address`)}
|
||||
{...form.getInputProps("email")}
|
||||
size="md"
|
||||
required
|
||||
/>
|
||||
<PasswordInput
|
||||
label={<Trans>Password</Trans>}
|
||||
placeholder={t`Password`}
|
||||
placeholder={_(msg`Password`)}
|
||||
{...form.getInputProps("password")}
|
||||
size="md"
|
||||
required
|
||||
/>
|
||||
<Button type="submit" loading={register.loading}>
|
||||
<Button type="submit" loading={register.loading || login.loading}>
|
||||
<Trans>Sign up</Trans>
|
||||
</Button>
|
||||
<Center>
|
||||
|
||||
@@ -2,11 +2,11 @@ import { lingui } from "@lingui/vite-plugin"
|
||||
import react from "@vitejs/plugin-react"
|
||||
import { visualizer } from "rollup-plugin-visualizer"
|
||||
import { defineConfig } from "vite"
|
||||
import biomePlugin from "vite-plugin-biome"
|
||||
import checker from "vite-plugin-checker"
|
||||
import tsconfigPaths from "vite-tsconfig-paths"
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(env => ({
|
||||
export default defineConfig(() => ({
|
||||
plugins: [
|
||||
react({
|
||||
babel: {
|
||||
@@ -17,9 +17,11 @@ export default defineConfig(env => ({
|
||||
lingui(),
|
||||
tsconfigPaths(),
|
||||
visualizer(),
|
||||
biomePlugin({
|
||||
mode: "check",
|
||||
failOnError: true,
|
||||
checker({
|
||||
typescript: true,
|
||||
biome: {
|
||||
command: "check",
|
||||
},
|
||||
}),
|
||||
],
|
||||
base: "./",
|
||||
@@ -32,6 +34,7 @@ export default defineConfig(env => ({
|
||||
"/openapi.json": "http://localhost:8083",
|
||||
"/custom_css.css": "http://localhost:8083",
|
||||
"/custom_js.js": "http://localhost:8083",
|
||||
"/j_security_check": "http://localhost:8083",
|
||||
"/logout": "http://localhost:8083",
|
||||
},
|
||||
},
|
||||
@@ -48,4 +51,7 @@ export default defineConfig(env => ({
|
||||
},
|
||||
},
|
||||
},
|
||||
test: {
|
||||
environment: "jsdom",
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
# CommaFeed settings
|
||||
# ------------------
|
||||
app:
|
||||
# url used to access commafeed
|
||||
publicUrl: http://localhost:8082/
|
||||
|
||||
# whether to expose a robots.txt file that disallows web crawlers and search engine indexers
|
||||
hideFromWebCrawlers: true
|
||||
|
||||
# whether to allow user registrations
|
||||
allowRegistrations: true
|
||||
|
||||
# whether to enable strict password validation (1 uppercase char, 1 lowercase char, 1 digit, 1 special char)
|
||||
strictPasswordPolicy: true
|
||||
|
||||
# create a demo account the first time the app starts
|
||||
createDemoAccount: true
|
||||
|
||||
# put your google analytics tracking code here
|
||||
googleAnalyticsTrackingCode:
|
||||
|
||||
# put your google server key (used for youtube favicon fetching)
|
||||
googleAuthKey:
|
||||
|
||||
# number of http threads
|
||||
backgroundThreads: 3
|
||||
|
||||
# number of database updating threads
|
||||
databaseUpdateThreads: 1
|
||||
|
||||
# rows to delete per query while cleaning up old entries
|
||||
databaseCleanupBatchSize: 100
|
||||
|
||||
# settings for sending emails (password recovery)
|
||||
smtpHost: localhost
|
||||
smtpPort: 25
|
||||
smtpTls: false
|
||||
smtpUserName: user
|
||||
smtpPassword: pass
|
||||
smtpFromAddress:
|
||||
|
||||
# Graphite Metric settings
|
||||
# Allows those who use Graphite to have CommaFeed send metrics for graphing (time in seconds)
|
||||
graphiteEnabled: false
|
||||
graphitePrefix: "test.commafeed"
|
||||
graphiteHost: "localhost"
|
||||
graphitePort: 2003
|
||||
graphiteInterval: 60
|
||||
|
||||
# whether this commafeed instance has a lot of feeds to refresh
|
||||
# leave this to false in almost all cases
|
||||
heavyLoad: false
|
||||
|
||||
# minimum amount of time commafeed will wait before refreshing the same feed
|
||||
refreshIntervalMinutes: 5
|
||||
|
||||
# if enabled, images in feed entries will be proxied through the server instead of accessed directly by the browser
|
||||
# useful if commafeed is usually accessed through a restricting proxy
|
||||
imageProxyEnabled: true
|
||||
|
||||
# database query timeout (in milliseconds), 0 to disable
|
||||
queryTimeout: 0
|
||||
|
||||
# time to keep unread statuses (in days), 0 to disable
|
||||
keepStatusDays: 0
|
||||
|
||||
# entries to keep per feed, old entries will be deleted, 0 to disable
|
||||
maxFeedCapacity: 500
|
||||
|
||||
# entries older than this will be deleted, 0 to disable
|
||||
maxEntriesAgeDays: 365
|
||||
|
||||
# limit the number of feeds a user can subscribe to, 0 to disable
|
||||
maxFeedsPerUser: 0
|
||||
|
||||
# cache service to use, possible values are 'noop' and 'redis'
|
||||
cache: noop
|
||||
|
||||
# announcement string displayed on the main page
|
||||
announcement:
|
||||
|
||||
# user-agent string that will be used by the http client, leave empty for the default one
|
||||
userAgent:
|
||||
|
||||
# enable websocket connection so the server can notify the web client that there are new entries for your feeds
|
||||
websocketEnabled: true
|
||||
|
||||
# interval at which the client will send a ping message on the websocket to keep the connection alive
|
||||
websocketPingInterval: 15m
|
||||
|
||||
# if websocket is disabled or the connection is lost, the client will reload the feed tree at this interval
|
||||
treeReloadInterval: 30s
|
||||
|
||||
# Database connection
|
||||
# -------------------
|
||||
# for MariaDB
|
||||
# driverClass is org.mariadb.jdbc.Driver
|
||||
# url is jdbc:mariadb://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC
|
||||
#
|
||||
# for MySQL
|
||||
# driverClass is com.mysql.cj.jdbc.Driver
|
||||
# url is jdbc:mysql://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC
|
||||
#
|
||||
# for PostgreSQL
|
||||
# driverClass is org.postgresql.Driver
|
||||
# url is jdbc:postgresql://localhost:5432/commafeed
|
||||
|
||||
database:
|
||||
driverClass: org.h2.Driver
|
||||
url: jdbc:h2:./target/commafeed
|
||||
user: sa
|
||||
password: sa
|
||||
properties:
|
||||
charSet: UTF-8
|
||||
validationQuery: "/* CommaFeed Health Check */ SELECT 1"
|
||||
|
||||
server:
|
||||
applicationConnectors:
|
||||
- type: http
|
||||
port: 8083
|
||||
adminConnectors:
|
||||
- type: http
|
||||
port: 8084
|
||||
|
||||
logging:
|
||||
level: INFO
|
||||
loggers:
|
||||
com.commafeed: DEBUG
|
||||
liquibase: INFO
|
||||
org.hibernate.SQL: INFO # or ALL for sql debugging
|
||||
org.hibernate.engine.internal.StatisticalLoggingSessionEventListener: WARN
|
||||
appenders:
|
||||
- type: console
|
||||
- type: file
|
||||
currentLogFilename: log/commafeed.log
|
||||
threshold: ALL
|
||||
archive: true
|
||||
archivedLogFilenamePattern: log/commafeed-%d.log
|
||||
archivedFileCount: 5
|
||||
timeZone: UTC
|
||||
|
||||
# Redis pool configuration
|
||||
# (only used if app.cache is 'redis')
|
||||
# -----------------------------------
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
# username is only required when using ACLs
|
||||
username:
|
||||
password:
|
||||
timeout: 2000
|
||||
database: 0
|
||||
maxTotal: 500
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
# CommaFeed settings
|
||||
# ------------------
|
||||
app:
|
||||
# url used to access commafeed
|
||||
publicUrl: http://localhost:8088/
|
||||
|
||||
# whether to expose a robots.txt file that disallows web crawlers and search engine indexers
|
||||
hideFromWebCrawlers: true
|
||||
|
||||
# whether to allow user registrations
|
||||
allowRegistrations: true
|
||||
|
||||
# whether to enable strict password validation (1 uppercase char, 1 lowercase char, 1 digit, 1 special char)
|
||||
strictPasswordPolicy: true
|
||||
|
||||
# create a demo account the first time the app starts
|
||||
createDemoAccount: true
|
||||
|
||||
# put your google analytics tracking code here
|
||||
googleAnalyticsTrackingCode:
|
||||
|
||||
# put your google server key (used for youtube favicon fetching)
|
||||
googleAuthKey:
|
||||
|
||||
# number of http threads
|
||||
backgroundThreads: 3
|
||||
|
||||
# number of database updating threads
|
||||
databaseUpdateThreads: 1
|
||||
|
||||
# rows to delete per query while cleaning up old entries
|
||||
databaseCleanupBatchSize: 100
|
||||
|
||||
# settings for sending emails (password recovery)
|
||||
smtpHost: localhost
|
||||
smtpPort: 25
|
||||
smtpTls: false
|
||||
smtpUserName: user
|
||||
smtpPassword: pass
|
||||
smtpFromAddress:
|
||||
|
||||
# Graphite Metric settings
|
||||
# Allows those who use Graphite to have CommaFeed send metrics for graphing (time in seconds)
|
||||
graphiteEnabled: false
|
||||
graphitePrefix: "test.commafeed"
|
||||
graphiteHost: "localhost"
|
||||
graphitePort: 2003
|
||||
graphiteInterval: 60
|
||||
|
||||
# whether this commafeed instance has a lot of feeds to refresh
|
||||
# leave this to false in almost all cases
|
||||
heavyLoad: false
|
||||
|
||||
# minimum amount of time commafeed will wait before refreshing the same feed
|
||||
refreshIntervalMinutes: 5
|
||||
|
||||
# if enabled, images in feed entries will be proxied through the server instead of accessed directly by the browser
|
||||
# useful if commafeed is usually accessed through a restricting proxy
|
||||
imageProxyEnabled: true
|
||||
|
||||
# database query timeout (in milliseconds), 0 to disable
|
||||
queryTimeout: 0
|
||||
|
||||
# time to keep unread statuses (in days), 0 to disable
|
||||
keepStatusDays: 0
|
||||
|
||||
# entries to keep per feed, old entries will be deleted, 0 to disable
|
||||
maxFeedCapacity: 500
|
||||
|
||||
# entries older than this will be deleted, 0 to disable
|
||||
maxEntriesAgeDays: 365
|
||||
|
||||
# limit the number of feeds a user can subscribe to, 0 to disable
|
||||
maxFeedsPerUser: 0
|
||||
|
||||
# cache service to use, possible values are 'noop' and 'redis'
|
||||
cache: noop
|
||||
|
||||
# announcement string displayed on the main page
|
||||
announcement:
|
||||
|
||||
# user-agent string that will be used by the http client, leave empty for the default one
|
||||
userAgent:
|
||||
|
||||
# enable websocket connection so the server can notify the web client that there are new entries for your feeds
|
||||
websocketEnabled: true
|
||||
|
||||
# interval at which the client will send a ping message on the websocket to keep the connection alive
|
||||
websocketPingInterval: 15m
|
||||
|
||||
# if websocket is disabled or the connection is lost, the client will reload the feed tree at this interval
|
||||
treeReloadInterval: 30s
|
||||
|
||||
# Database connection
|
||||
# -------------------
|
||||
# for MariaDB
|
||||
# driverClass is org.mariadb.jdbc.Driver
|
||||
# url is jdbc:mariadb://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC
|
||||
#
|
||||
# for MySQL
|
||||
# driverClass is com.mysql.cj.jdbc.Driver
|
||||
# url is jdbc:mysql://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC
|
||||
#
|
||||
# for PostgreSQL
|
||||
# driverClass is org.postgresql.Driver
|
||||
# url is jdbc:postgresql://localhost:5432/commafeed
|
||||
|
||||
database:
|
||||
driverClass: org.h2.Driver
|
||||
url: jdbc:h2:mem:commafeed
|
||||
user: sa
|
||||
password: sa
|
||||
properties:
|
||||
charSet: UTF-8
|
||||
validationQuery: "/* CommaFeed Health Check */ SELECT 1"
|
||||
|
||||
server:
|
||||
applicationConnectors:
|
||||
- type: http
|
||||
port: 8088
|
||||
|
||||
logging:
|
||||
level: INFO
|
||||
loggers:
|
||||
com.commafeed: DEBUG
|
||||
liquibase: INFO
|
||||
org.hibernate.SQL: INFO # or ALL for sql debugging
|
||||
org.hibernate.engine.internal.StatisticalLoggingSessionEventListener: WARN
|
||||
appenders:
|
||||
- type: console
|
||||
- type: file
|
||||
currentLogFilename: log/commafeed.log
|
||||
threshold: ALL
|
||||
archive: true
|
||||
archivedLogFilenamePattern: log/commafeed-%d.log
|
||||
archivedFileCount: 5
|
||||
timeZone: UTC
|
||||
|
||||
# Redis pool configuration
|
||||
# (only used if app.cache is 'redis')
|
||||
# -----------------------------------
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
# username is only required when using ACLs
|
||||
username:
|
||||
password:
|
||||
timeout: 2000
|
||||
database: 0
|
||||
maxTotal: 500
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
# CommaFeed settings
|
||||
# ------------------
|
||||
app:
|
||||
# url used to access commafeed
|
||||
publicUrl: http://localhost:8082/
|
||||
|
||||
# whether to expose a robots.txt file that disallows web crawlers and search engine indexers
|
||||
hideFromWebCrawlers: true
|
||||
|
||||
# whether to allow user registrations
|
||||
allowRegistrations: false
|
||||
|
||||
# whether to enable strict password validation (1 uppercase char, 1 lowercase char, 1 digit, 1 special char)
|
||||
strictPasswordPolicy: true
|
||||
|
||||
# create a demo account the first time the app starts
|
||||
createDemoAccount: false
|
||||
|
||||
# put your google analytics tracking code here
|
||||
googleAnalyticsTrackingCode:
|
||||
|
||||
# put your google server key (used for youtube favicon fetching)
|
||||
googleAuthKey:
|
||||
|
||||
# number of http threads
|
||||
backgroundThreads: 3
|
||||
|
||||
# number of database updating threads
|
||||
databaseUpdateThreads: 1
|
||||
|
||||
# rows to delete per query while cleaning up old entries
|
||||
databaseCleanupBatchSize: 100
|
||||
|
||||
# settings for sending emails (password recovery)
|
||||
smtpHost:
|
||||
smtpPort:
|
||||
smtpTls: false
|
||||
smtpUserName:
|
||||
smtpPassword:
|
||||
smtpFromAddress:
|
||||
|
||||
# Graphite Metric settings
|
||||
# Allows those who use Graphite to have CommaFeed send metrics for graphing (time in seconds)
|
||||
graphiteEnabled: false
|
||||
graphitePrefix: "test.commafeed"
|
||||
graphiteHost: "localhost"
|
||||
graphitePort: 2003
|
||||
graphiteInterval: 60
|
||||
|
||||
# whether this commafeed instance has a lot of feeds to refresh
|
||||
# leave this to false in almost all cases
|
||||
heavyLoad: false
|
||||
|
||||
# minimum amount of time commafeed will wait before refreshing the same feed
|
||||
refreshIntervalMinutes: 5
|
||||
|
||||
# if enabled, images in feed entries will be proxied through the server instead of accessed directly by the browser
|
||||
# useful if commafeed is usually accessed through a restricting proxy
|
||||
imageProxyEnabled: false
|
||||
|
||||
# database query timeout (in milliseconds), 0 to disable
|
||||
queryTimeout: 0
|
||||
|
||||
# time to keep unread statuses (in days), 0 to disable
|
||||
keepStatusDays: 0
|
||||
|
||||
# entries to keep per feed, old entries will be deleted, 0 to disable
|
||||
maxFeedCapacity: 500
|
||||
|
||||
# entries older than this will be deleted, 0 to disable
|
||||
maxEntriesAgeDays: 365
|
||||
|
||||
# limit the number of feeds a user can subscribe to, 0 to disable
|
||||
maxFeedsPerUser: 0
|
||||
|
||||
# cache service to use, possible values are 'noop' and 'redis'
|
||||
cache: noop
|
||||
|
||||
# announcement string displayed on the main page
|
||||
announcement:
|
||||
|
||||
# user-agent string that will be used by the http client, leave empty for the default one
|
||||
userAgent:
|
||||
|
||||
# enable websocket connection so the server can notify the web client that there are new entries for your feeds
|
||||
websocketEnabled: true
|
||||
|
||||
# interval at which the client will send a ping message on the websocket to keep the connection alive
|
||||
websocketPingInterval: 15m
|
||||
|
||||
# if websocket is disabled or the connection is lost, the client will reload the feed tree at this interval
|
||||
treeReloadInterval: 30s
|
||||
|
||||
# Database connection
|
||||
# -------------------
|
||||
# for MariaDB
|
||||
# driverClass is org.mariadb.jdbc.Driver
|
||||
# url is jdbc:mariadb://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC
|
||||
#
|
||||
# for MySQL
|
||||
# driverClass is com.mysql.cj.jdbc.Driver
|
||||
# url is jdbc:mysql://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC
|
||||
#
|
||||
# for PostgreSQL
|
||||
# driverClass is org.postgresql.Driver
|
||||
# url is jdbc:postgresql://localhost:5432/commafeed
|
||||
|
||||
database:
|
||||
driverClass: org.h2.Driver
|
||||
url: jdbc:h2:/commafeed/data/db;DEFRAG_ALWAYS=TRUE
|
||||
user: sa
|
||||
password: sa
|
||||
properties:
|
||||
charSet: UTF-8
|
||||
validationQuery: "/* CommaFeed Health Check */ SELECT 1"
|
||||
minSize: 1
|
||||
maxSize: 50
|
||||
maxConnectionAge: 30m
|
||||
|
||||
server:
|
||||
applicationConnectors:
|
||||
- type: http
|
||||
port: 8082
|
||||
adminConnectors: [ ]
|
||||
requestLog:
|
||||
appenders: [ ]
|
||||
|
||||
logging:
|
||||
level: ERROR
|
||||
loggers:
|
||||
com.commafeed: INFO
|
||||
liquibase: INFO
|
||||
io.dropwizard.server.ServerFactory: INFO
|
||||
appenders:
|
||||
- type: console
|
||||
- type: file
|
||||
currentLogFilename: log/commafeed.log
|
||||
threshold: ALL
|
||||
archive: true
|
||||
archivedLogFilenamePattern: log/commafeed-%d.log
|
||||
archivedFileCount: 5
|
||||
timeZone: UTC
|
||||
|
||||
# Redis pool configuration
|
||||
# (only used if app.cache is 'redis')
|
||||
# -----------------------------------
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
# username is only required when using ACLs
|
||||
username:
|
||||
password:
|
||||
timeout: 2000
|
||||
database: 0
|
||||
maxTotal: 500
|
||||
690
commafeed-server/doc/commafeed.adoc
Normal file
690
commafeed-server/doc/commafeed.adoc
Normal file
@@ -0,0 +1,690 @@
|
||||
:summaryTableId: commafeed-server_commafeed
|
||||
[.configuration-legend]
|
||||
icon:lock[title=Fixed at build time] Configuration property fixed at build time - All other configuration properties are overridable at runtime
|
||||
[.configuration-reference.searchable, cols="80,.^10,.^10"]
|
||||
|===
|
||||
|
||||
h|[.header-title]##Configuration property##
|
||||
h|Type
|
||||
h|Default
|
||||
|
||||
a| [[commafeed-server_commafeed-hide-from-web-crawlers]] [.property-path]##link:#commafeed-server_commafeed-hide-from-web-crawlers[`commafeed.hide-from-web-crawlers`]##
|
||||
|
||||
[.description]
|
||||
--
|
||||
Whether to expose a robots.txt file that disallows web crawlers and search engine indexers.
|
||||
|
||||
|
||||
ifdef::add-copy-button-to-env-var[]
|
||||
Environment variable: env_var_with_copy_button:+++COMMAFEED_HIDE_FROM_WEB_CRAWLERS+++[]
|
||||
endif::add-copy-button-to-env-var[]
|
||||
ifndef::add-copy-button-to-env-var[]
|
||||
Environment variable: `+++COMMAFEED_HIDE_FROM_WEB_CRAWLERS+++`
|
||||
endif::add-copy-button-to-env-var[]
|
||||
--
|
||||
|boolean
|
||||
|`true`
|
||||
|
||||
a| [[commafeed-server_commafeed-image-proxy-enabled]] [.property-path]##link:#commafeed-server_commafeed-image-proxy-enabled[`commafeed.image-proxy-enabled`]##
|
||||
|
||||
[.description]
|
||||
--
|
||||
If enabled, images in feed entries will be proxied through the server instead of accessed directly by the browser. This is useful if commafeed is accessed through a restricting proxy that blocks some feeds that are followed.
|
||||
|
||||
|
||||
ifdef::add-copy-button-to-env-var[]
|
||||
Environment variable: env_var_with_copy_button:+++COMMAFEED_IMAGE_PROXY_ENABLED+++[]
|
||||
endif::add-copy-button-to-env-var[]
|
||||
ifndef::add-copy-button-to-env-var[]
|
||||
Environment variable: `+++COMMAFEED_IMAGE_PROXY_ENABLED+++`
|
||||
endif::add-copy-button-to-env-var[]
|
||||
--
|
||||
|boolean
|
||||
|`false`
|
||||
|
||||
a| [[commafeed-server_commafeed-password-recovery-enabled]] [.property-path]##link:#commafeed-server_commafeed-password-recovery-enabled[`commafeed.password-recovery-enabled`]##
|
||||
|
||||
[.description]
|
||||
--
|
||||
Enable password recovery via email. Quarkus mailer will need to be configured.
|
||||
|
||||
|
||||
ifdef::add-copy-button-to-env-var[]
|
||||
Environment variable: env_var_with_copy_button:+++COMMAFEED_PASSWORD_RECOVERY_ENABLED+++[]
|
||||
endif::add-copy-button-to-env-var[]
|
||||
ifndef::add-copy-button-to-env-var[]
|
||||
Environment variable: `+++COMMAFEED_PASSWORD_RECOVERY_ENABLED+++`
|
||||
endif::add-copy-button-to-env-var[]
|
||||
--
|
||||
|boolean
|
||||
|`false`
|
||||
|
||||
a| [[commafeed-server_commafeed-announcement]] [.property-path]##link:#commafeed-server_commafeed-announcement[`commafeed.announcement`]##
|
||||
|
||||
[.description]
|
||||
--
|
||||
Message displayed in a notification at the bottom of the page.
|
||||
|
||||
|
||||
ifdef::add-copy-button-to-env-var[]
|
||||
Environment variable: env_var_with_copy_button:+++COMMAFEED_ANNOUNCEMENT+++[]
|
||||
endif::add-copy-button-to-env-var[]
|
||||
ifndef::add-copy-button-to-env-var[]
|
||||
Environment variable: `+++COMMAFEED_ANNOUNCEMENT+++`
|
||||
endif::add-copy-button-to-env-var[]
|
||||
--
|
||||
|string
|
||||
|
|
||||
|
||||
a| [[commafeed-server_commafeed-google-analytics-tracking-code]] [.property-path]##link:#commafeed-server_commafeed-google-analytics-tracking-code[`commafeed.google-analytics-tracking-code`]##
|
||||
|
||||
[.description]
|
||||
--
|
||||
Google Analytics tracking code.
|
||||
|
||||
|
||||
ifdef::add-copy-button-to-env-var[]
|
||||
Environment variable: env_var_with_copy_button:+++COMMAFEED_GOOGLE_ANALYTICS_TRACKING_CODE+++[]
|
||||
endif::add-copy-button-to-env-var[]
|
||||
ifndef::add-copy-button-to-env-var[]
|
||||
Environment variable: `+++COMMAFEED_GOOGLE_ANALYTICS_TRACKING_CODE+++`
|
||||
endif::add-copy-button-to-env-var[]
|
||||
--
|
||||
|string
|
||||
|
|
||||
|
||||
a| [[commafeed-server_commafeed-google-auth-key]] [.property-path]##link:#commafeed-server_commafeed-google-auth-key[`commafeed.google-auth-key`]##
|
||||
|
||||
[.description]
|
||||
--
|
||||
Google Auth key for fetching Youtube channel favicons.
|
||||
|
||||
|
||||
ifdef::add-copy-button-to-env-var[]
|
||||
Environment variable: env_var_with_copy_button:+++COMMAFEED_GOOGLE_AUTH_KEY+++[]
|
||||
endif::add-copy-button-to-env-var[]
|
||||
ifndef::add-copy-button-to-env-var[]
|
||||
Environment variable: `+++COMMAFEED_GOOGLE_AUTH_KEY+++`
|
||||
endif::add-copy-button-to-env-var[]
|
||||
--
|
||||
|string
|
||||
|
|
||||
|
||||
h|[[commafeed-server_section_commafeed-http-client]] [.section-name.section-level0]##link:#commafeed-server_section_commafeed-http-client[HTTP client configuration]##
|
||||
h|Type
|
||||
h|Default
|
||||
|
||||
a| [[commafeed-server_commafeed-http-client-user-agent]] [.property-path]##link:#commafeed-server_commafeed-http-client-user-agent[`commafeed.http-client.user-agent`]##
|
||||
|
||||
[.description]
|
||||
--
|
||||
User-Agent string that will be used by the http client, leave empty for the default one.
|
||||
|
||||
|
||||
ifdef::add-copy-button-to-env-var[]
|
||||
Environment variable: env_var_with_copy_button:+++COMMAFEED_HTTP_CLIENT_USER_AGENT+++[]
|
||||
endif::add-copy-button-to-env-var[]
|
||||
ifndef::add-copy-button-to-env-var[]
|
||||
Environment variable: `+++COMMAFEED_HTTP_CLIENT_USER_AGENT+++`
|
||||
endif::add-copy-button-to-env-var[]
|
||||
--
|
||||
|string
|
||||
|
|
||||
|
||||
a| [[commafeed-server_commafeed-http-client-connect-timeout]] [.property-path]##link:#commafeed-server_commafeed-http-client-connect-timeout[`commafeed.http-client.connect-timeout`]##
|
||||
|
||||
[.description]
|
||||
--
|
||||
Time to wait for a connection to be established.
|
||||
|
||||
|
||||
ifdef::add-copy-button-to-env-var[]
|
||||
Environment variable: env_var_with_copy_button:+++COMMAFEED_HTTP_CLIENT_CONNECT_TIMEOUT+++[]
|
||||
endif::add-copy-button-to-env-var[]
|
||||
ifndef::add-copy-button-to-env-var[]
|
||||
Environment variable: `+++COMMAFEED_HTTP_CLIENT_CONNECT_TIMEOUT+++`
|
||||
endif::add-copy-button-to-env-var[]
|
||||
--
|
||||
|link:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html[Duration] link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|
||||
|`5S`
|
||||
|
||||
a| [[commafeed-server_commafeed-http-client-ssl-handshake-timeout]] [.property-path]##link:#commafeed-server_commafeed-http-client-ssl-handshake-timeout[`commafeed.http-client.ssl-handshake-timeout`]##
|
||||
|
||||
[.description]
|
||||
--
|
||||
Time to wait for SSL handshake to complete.
|
||||
|
||||
|
||||
ifdef::add-copy-button-to-env-var[]
|
||||
Environment variable: env_var_with_copy_button:+++COMMAFEED_HTTP_CLIENT_SSL_HANDSHAKE_TIMEOUT+++[]
|
||||
endif::add-copy-button-to-env-var[]
|
||||
ifndef::add-copy-button-to-env-var[]
|
||||
Environment variable: `+++COMMAFEED_HTTP_CLIENT_SSL_HANDSHAKE_TIMEOUT+++`
|
||||
endif::add-copy-button-to-env-var[]
|
||||
--
|
||||
|link:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html[Duration] link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|
||||
|`5S`
|
||||
|
||||
a| [[commafeed-server_commafeed-http-client-socket-timeout]] [.property-path]##link:#commafeed-server_commafeed-http-client-socket-timeout[`commafeed.http-client.socket-timeout`]##
|
||||
|
||||
[.description]
|
||||
--
|
||||
Time to wait between two packets before timeout.
|
||||
|
||||
|
||||
ifdef::add-copy-button-to-env-var[]
|
||||
Environment variable: env_var_with_copy_button:+++COMMAFEED_HTTP_CLIENT_SOCKET_TIMEOUT+++[]
|
||||
endif::add-copy-button-to-env-var[]
|
||||
ifndef::add-copy-button-to-env-var[]
|
||||
Environment variable: `+++COMMAFEED_HTTP_CLIENT_SOCKET_TIMEOUT+++`
|
||||
endif::add-copy-button-to-env-var[]
|
||||
--
|
||||
|link:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html[Duration] link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|
||||
|`10S`
|
||||
|
||||
a| [[commafeed-server_commafeed-http-client-response-timeout]] [.property-path]##link:#commafeed-server_commafeed-http-client-response-timeout[`commafeed.http-client.response-timeout`]##
|
||||
|
||||
[.description]
|
||||
--
|
||||
Time to wait for the full response to be received.
|
||||
|
||||
|
||||
ifdef::add-copy-button-to-env-var[]
|
||||
Environment variable: env_var_with_copy_button:+++COMMAFEED_HTTP_CLIENT_RESPONSE_TIMEOUT+++[]
|
||||
endif::add-copy-button-to-env-var[]
|
||||
ifndef::add-copy-button-to-env-var[]
|
||||
Environment variable: `+++COMMAFEED_HTTP_CLIENT_RESPONSE_TIMEOUT+++`
|
||||
endif::add-copy-button-to-env-var[]
|
||||
--
|
||||
|link:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html[Duration] link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|
||||
|`10S`
|
||||
|
||||
a| [[commafeed-server_commafeed-http-client-connection-time-to-live]] [.property-path]##link:#commafeed-server_commafeed-http-client-connection-time-to-live[`commafeed.http-client.connection-time-to-live`]##
|
||||
|
||||
[.description]
|
||||
--
|
||||
Time to live for a connection in the pool.
|
||||
|
||||
|
||||
ifdef::add-copy-button-to-env-var[]
|
||||
Environment variable: env_var_with_copy_button:+++COMMAFEED_HTTP_CLIENT_CONNECTION_TIME_TO_LIVE+++[]
|
||||
endif::add-copy-button-to-env-var[]
|
||||
ifndef::add-copy-button-to-env-var[]
|
||||
Environment variable: `+++COMMAFEED_HTTP_CLIENT_CONNECTION_TIME_TO_LIVE+++`
|
||||
endif::add-copy-button-to-env-var[]
|
||||
--
|
||||
|link:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html[Duration] link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|
||||
|`30S`
|
||||
|
||||
a| [[commafeed-server_commafeed-http-client-idle-connections-eviction-interval]] [.property-path]##link:#commafeed-server_commafeed-http-client-idle-connections-eviction-interval[`commafeed.http-client.idle-connections-eviction-interval`]##
|
||||
|
||||
[.description]
|
||||
--
|
||||
Time between eviction runs for idle connections.
|
||||
|
||||
|
||||
ifdef::add-copy-button-to-env-var[]
|
||||
Environment variable: env_var_with_copy_button:+++COMMAFEED_HTTP_CLIENT_IDLE_CONNECTIONS_EVICTION_INTERVAL+++[]
|
||||
endif::add-copy-button-to-env-var[]
|
||||
ifndef::add-copy-button-to-env-var[]
|
||||
Environment variable: `+++COMMAFEED_HTTP_CLIENT_IDLE_CONNECTIONS_EVICTION_INTERVAL+++`
|
||||
endif::add-copy-button-to-env-var[]
|
||||
--
|
||||
|link:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html[Duration] link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|
||||
|`1M`
|
||||
|
||||
a| [[commafeed-server_commafeed-http-client-max-response-size]] [.property-path]##link:#commafeed-server_commafeed-http-client-max-response-size[`commafeed.http-client.max-response-size`]##
|
||||
|
||||
[.description]
|
||||
--
|
||||
If a feed is larger than this, it will be discarded to prevent memory issues while parsing the feed.
|
||||
|
||||
|
||||
ifdef::add-copy-button-to-env-var[]
|
||||
Environment variable: env_var_with_copy_button:+++COMMAFEED_HTTP_CLIENT_MAX_RESPONSE_SIZE+++[]
|
||||
endif::add-copy-button-to-env-var[]
|
||||
ifndef::add-copy-button-to-env-var[]
|
||||
Environment variable: `+++COMMAFEED_HTTP_CLIENT_MAX_RESPONSE_SIZE+++`
|
||||
endif::add-copy-button-to-env-var[]
|
||||
--
|
||||
|MemorySize link:#memory-size-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the MemorySize format]]
|
||||
|`5M`
|
||||
|
||||
h|[[commafeed-server_section_commafeed-http-client-cache]] [.section-name.section-level1]##link:#commafeed-server_section_commafeed-http-client-cache[HTTP client cache configuration]##
|
||||
h|Type
|
||||
h|Default
|
||||
|
||||
a| [[commafeed-server_commafeed-http-client-cache-enabled]] [.property-path]##link:#commafeed-server_commafeed-http-client-cache-enabled[`commafeed.http-client.cache.enabled`]##
|
||||
|
||||
[.description]
|
||||
--
|
||||
Whether to enable the cache. This cache is used to avoid spamming feeds in short bursts (e.g. when subscribing to a feed for the first time or when clicking "fetch all my feeds now").
|
||||
|
||||
|
||||
ifdef::add-copy-button-to-env-var[]
|
||||
Environment variable: env_var_with_copy_button:+++COMMAFEED_HTTP_CLIENT_CACHE_ENABLED+++[]
|
||||
endif::add-copy-button-to-env-var[]
|
||||
ifndef::add-copy-button-to-env-var[]
|
||||
Environment variable: `+++COMMAFEED_HTTP_CLIENT_CACHE_ENABLED+++`
|
||||
endif::add-copy-button-to-env-var[]
|
||||
--
|
||||
|boolean
|
||||
|`true`
|
||||
|
||||
a| [[commafeed-server_commafeed-http-client-cache-maximum-memory-size]] [.property-path]##link:#commafeed-server_commafeed-http-client-cache-maximum-memory-size[`commafeed.http-client.cache.maximum-memory-size`]##
|
||||
|
||||
[.description]
|
||||
--
|
||||
Maximum amount of memory the cache can use.
|
||||
|
||||
|
||||
ifdef::add-copy-button-to-env-var[]
|
||||
Environment variable: env_var_with_copy_button:+++COMMAFEED_HTTP_CLIENT_CACHE_MAXIMUM_MEMORY_SIZE+++[]
|
||||
endif::add-copy-button-to-env-var[]
|
||||
ifndef::add-copy-button-to-env-var[]
|
||||
Environment variable: `+++COMMAFEED_HTTP_CLIENT_CACHE_MAXIMUM_MEMORY_SIZE+++`
|
||||
endif::add-copy-button-to-env-var[]
|
||||
--
|
||||
|MemorySize link:#memory-size-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the MemorySize format]]
|
||||
|`10M`
|
||||
|
||||
a| [[commafeed-server_commafeed-http-client-cache-expiration]] [.property-path]##link:#commafeed-server_commafeed-http-client-cache-expiration[`commafeed.http-client.cache.expiration`]##
|
||||
|
||||
[.description]
|
||||
--
|
||||
Duration after which an entry is removed from the cache.
|
||||
|
||||
|
||||
ifdef::add-copy-button-to-env-var[]
|
||||
Environment variable: env_var_with_copy_button:+++COMMAFEED_HTTP_CLIENT_CACHE_EXPIRATION+++[]
|
||||
endif::add-copy-button-to-env-var[]
|
||||
ifndef::add-copy-button-to-env-var[]
|
||||
Environment variable: `+++COMMAFEED_HTTP_CLIENT_CACHE_EXPIRATION+++`
|
||||
endif::add-copy-button-to-env-var[]
|
||||
--
|
||||
|link:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html[Duration] link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|
||||
|`1M`
|
||||
|
||||
|
||||
|
||||
h|[[commafeed-server_section_commafeed-feed-refresh]] [.section-name.section-level0]##link:#commafeed-server_section_commafeed-feed-refresh[Feed refresh engine settings]##
|
||||
h|Type
|
||||
h|Default
|
||||
|
||||
a| [[commafeed-server_commafeed-feed-refresh-interval]] [.property-path]##link:#commafeed-server_commafeed-feed-refresh-interval[`commafeed.feed-refresh.interval`]##
|
||||
|
||||
[.description]
|
||||
--
|
||||
Amount of time CommaFeed will wait before refreshing the same feed.
|
||||
|
||||
|
||||
ifdef::add-copy-button-to-env-var[]
|
||||
Environment variable: env_var_with_copy_button:+++COMMAFEED_FEED_REFRESH_INTERVAL+++[]
|
||||
endif::add-copy-button-to-env-var[]
|
||||
ifndef::add-copy-button-to-env-var[]
|
||||
Environment variable: `+++COMMAFEED_FEED_REFRESH_INTERVAL+++`
|
||||
endif::add-copy-button-to-env-var[]
|
||||
--
|
||||
|link:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html[Duration] link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|
||||
|`5M`
|
||||
|
||||
a| [[commafeed-server_commafeed-feed-refresh-interval-empirical]] [.property-path]##link:#commafeed-server_commafeed-feed-refresh-interval-empirical[`commafeed.feed-refresh.interval-empirical`]##
|
||||
|
||||
[.description]
|
||||
--
|
||||
If true, CommaFeed will calculate the next refresh time based on the feed's average time between entries and the time since the last entry was published. The interval will be somewhere between the default refresh interval and 24h. See `FeedRefreshIntervalCalculator` for details.
|
||||
|
||||
|
||||
ifdef::add-copy-button-to-env-var[]
|
||||
Environment variable: env_var_with_copy_button:+++COMMAFEED_FEED_REFRESH_INTERVAL_EMPIRICAL+++[]
|
||||
endif::add-copy-button-to-env-var[]
|
||||
ifndef::add-copy-button-to-env-var[]
|
||||
Environment variable: `+++COMMAFEED_FEED_REFRESH_INTERVAL_EMPIRICAL+++`
|
||||
endif::add-copy-button-to-env-var[]
|
||||
--
|
||||
|boolean
|
||||
|`false`
|
||||
|
||||
a| [[commafeed-server_commafeed-feed-refresh-http-threads]] [.property-path]##link:#commafeed-server_commafeed-feed-refresh-http-threads[`commafeed.feed-refresh.http-threads`]##
|
||||
|
||||
[.description]
|
||||
--
|
||||
Amount of http threads used to fetch feeds.
|
||||
|
||||
|
||||
ifdef::add-copy-button-to-env-var[]
|
||||
Environment variable: env_var_with_copy_button:+++COMMAFEED_FEED_REFRESH_HTTP_THREADS+++[]
|
||||
endif::add-copy-button-to-env-var[]
|
||||
ifndef::add-copy-button-to-env-var[]
|
||||
Environment variable: `+++COMMAFEED_FEED_REFRESH_HTTP_THREADS+++`
|
||||
endif::add-copy-button-to-env-var[]
|
||||
--
|
||||
|int
|
||||
|`3`
|
||||
|
||||
a| [[commafeed-server_commafeed-feed-refresh-database-threads]] [.property-path]##link:#commafeed-server_commafeed-feed-refresh-database-threads[`commafeed.feed-refresh.database-threads`]##
|
||||
|
||||
[.description]
|
||||
--
|
||||
Amount of threads used to insert new entries in the database.
|
||||
|
||||
|
||||
ifdef::add-copy-button-to-env-var[]
|
||||
Environment variable: env_var_with_copy_button:+++COMMAFEED_FEED_REFRESH_DATABASE_THREADS+++[]
|
||||
endif::add-copy-button-to-env-var[]
|
||||
ifndef::add-copy-button-to-env-var[]
|
||||
Environment variable: `+++COMMAFEED_FEED_REFRESH_DATABASE_THREADS+++`
|
||||
endif::add-copy-button-to-env-var[]
|
||||
--
|
||||
|int
|
||||
|`1`
|
||||
|
||||
a| [[commafeed-server_commafeed-feed-refresh-user-inactivity-period]] [.property-path]##link:#commafeed-server_commafeed-feed-refresh-user-inactivity-period[`commafeed.feed-refresh.user-inactivity-period`]##
|
||||
|
||||
[.description]
|
||||
--
|
||||
Duration after which a user is considered inactive. Feeds for inactive users are not refreshed until they log in again. 0 to disable.
|
||||
|
||||
|
||||
ifdef::add-copy-button-to-env-var[]
|
||||
Environment variable: env_var_with_copy_button:+++COMMAFEED_FEED_REFRESH_USER_INACTIVITY_PERIOD+++[]
|
||||
endif::add-copy-button-to-env-var[]
|
||||
ifndef::add-copy-button-to-env-var[]
|
||||
Environment variable: `+++COMMAFEED_FEED_REFRESH_USER_INACTIVITY_PERIOD+++`
|
||||
endif::add-copy-button-to-env-var[]
|
||||
--
|
||||
|link:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html[Duration] link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|
||||
|`0S`
|
||||
|
||||
a| [[commafeed-server_commafeed-feed-refresh-filtering-expression-evaluation-timeout]] [.property-path]##link:#commafeed-server_commafeed-feed-refresh-filtering-expression-evaluation-timeout[`commafeed.feed-refresh.filtering-expression-evaluation-timeout`]##
|
||||
|
||||
[.description]
|
||||
--
|
||||
Duration after which the evaluation of a filtering expresion to mark an entry as read is considered to have timed out.
|
||||
|
||||
|
||||
ifdef::add-copy-button-to-env-var[]
|
||||
Environment variable: env_var_with_copy_button:+++COMMAFEED_FEED_REFRESH_FILTERING_EXPRESSION_EVALUATION_TIMEOUT+++[]
|
||||
endif::add-copy-button-to-env-var[]
|
||||
ifndef::add-copy-button-to-env-var[]
|
||||
Environment variable: `+++COMMAFEED_FEED_REFRESH_FILTERING_EXPRESSION_EVALUATION_TIMEOUT+++`
|
||||
endif::add-copy-button-to-env-var[]
|
||||
--
|
||||
|link:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html[Duration] link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|
||||
|`500MS`
|
||||
|
||||
a| [[commafeed-server_commafeed-feed-refresh-force-refresh-cooldown-duration]] [.property-path]##link:#commafeed-server_commafeed-feed-refresh-force-refresh-cooldown-duration[`commafeed.feed-refresh.force-refresh-cooldown-duration`]##
|
||||
|
||||
[.description]
|
||||
--
|
||||
Duration after which the "Fetch all my feeds now" action is available again after use to avoid spamming feeds.
|
||||
|
||||
|
||||
ifdef::add-copy-button-to-env-var[]
|
||||
Environment variable: env_var_with_copy_button:+++COMMAFEED_FEED_REFRESH_FORCE_REFRESH_COOLDOWN_DURATION+++[]
|
||||
endif::add-copy-button-to-env-var[]
|
||||
ifndef::add-copy-button-to-env-var[]
|
||||
Environment variable: `+++COMMAFEED_FEED_REFRESH_FORCE_REFRESH_COOLDOWN_DURATION+++`
|
||||
endif::add-copy-button-to-env-var[]
|
||||
--
|
||||
|link:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html[Duration] link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|
||||
|`0S`
|
||||
|
||||
|
||||
h|[[commafeed-server_section_commafeed-database]] [.section-name.section-level0]##link:#commafeed-server_section_commafeed-database[Database settings]##
|
||||
h|Type
|
||||
h|Default
|
||||
|
||||
a| [[commafeed-server_commafeed-database-query-timeout]] [.property-path]##link:#commafeed-server_commafeed-database-query-timeout[`commafeed.database.query-timeout`]##
|
||||
|
||||
[.description]
|
||||
--
|
||||
Timeout applied to all database queries. 0 to disable.
|
||||
|
||||
|
||||
ifdef::add-copy-button-to-env-var[]
|
||||
Environment variable: env_var_with_copy_button:+++COMMAFEED_DATABASE_QUERY_TIMEOUT+++[]
|
||||
endif::add-copy-button-to-env-var[]
|
||||
ifndef::add-copy-button-to-env-var[]
|
||||
Environment variable: `+++COMMAFEED_DATABASE_QUERY_TIMEOUT+++`
|
||||
endif::add-copy-button-to-env-var[]
|
||||
--
|
||||
|link:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html[Duration] link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|
||||
|`0S`
|
||||
|
||||
h|[[commafeed-server_section_commafeed-database-cleanup]] [.section-name.section-level1]##link:#commafeed-server_section_commafeed-database-cleanup[Database cleanup settings]##
|
||||
h|Type
|
||||
h|Default
|
||||
|
||||
a| [[commafeed-server_commafeed-database-cleanup-entries-max-age]] [.property-path]##link:#commafeed-server_commafeed-database-cleanup-entries-max-age[`commafeed.database.cleanup.entries-max-age`]##
|
||||
|
||||
[.description]
|
||||
--
|
||||
Maximum age of feed entries in the database. Older entries will be deleted. 0 to disable.
|
||||
|
||||
|
||||
ifdef::add-copy-button-to-env-var[]
|
||||
Environment variable: env_var_with_copy_button:+++COMMAFEED_DATABASE_CLEANUP_ENTRIES_MAX_AGE+++[]
|
||||
endif::add-copy-button-to-env-var[]
|
||||
ifndef::add-copy-button-to-env-var[]
|
||||
Environment variable: `+++COMMAFEED_DATABASE_CLEANUP_ENTRIES_MAX_AGE+++`
|
||||
endif::add-copy-button-to-env-var[]
|
||||
--
|
||||
|link:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html[Duration] link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|
||||
|`365D`
|
||||
|
||||
a| [[commafeed-server_commafeed-database-cleanup-statuses-max-age]] [.property-path]##link:#commafeed-server_commafeed-database-cleanup-statuses-max-age[`commafeed.database.cleanup.statuses-max-age`]##
|
||||
|
||||
[.description]
|
||||
--
|
||||
Maximum age of feed entry statuses (read/unread) in the database. Older statuses will be deleted. 0 to disable.
|
||||
|
||||
|
||||
ifdef::add-copy-button-to-env-var[]
|
||||
Environment variable: env_var_with_copy_button:+++COMMAFEED_DATABASE_CLEANUP_STATUSES_MAX_AGE+++[]
|
||||
endif::add-copy-button-to-env-var[]
|
||||
ifndef::add-copy-button-to-env-var[]
|
||||
Environment variable: `+++COMMAFEED_DATABASE_CLEANUP_STATUSES_MAX_AGE+++`
|
||||
endif::add-copy-button-to-env-var[]
|
||||
--
|
||||
|link:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html[Duration] link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|
||||
|`0S`
|
||||
|
||||
a| [[commafeed-server_commafeed-database-cleanup-max-feed-capacity]] [.property-path]##link:#commafeed-server_commafeed-database-cleanup-max-feed-capacity[`commafeed.database.cleanup.max-feed-capacity`]##
|
||||
|
||||
[.description]
|
||||
--
|
||||
Maximum number of entries per feed to keep in the database. 0 to disable.
|
||||
|
||||
|
||||
ifdef::add-copy-button-to-env-var[]
|
||||
Environment variable: env_var_with_copy_button:+++COMMAFEED_DATABASE_CLEANUP_MAX_FEED_CAPACITY+++[]
|
||||
endif::add-copy-button-to-env-var[]
|
||||
ifndef::add-copy-button-to-env-var[]
|
||||
Environment variable: `+++COMMAFEED_DATABASE_CLEANUP_MAX_FEED_CAPACITY+++`
|
||||
endif::add-copy-button-to-env-var[]
|
||||
--
|
||||
|int
|
||||
|`500`
|
||||
|
||||
a| [[commafeed-server_commafeed-database-cleanup-max-feeds-per-user]] [.property-path]##link:#commafeed-server_commafeed-database-cleanup-max-feeds-per-user[`commafeed.database.cleanup.max-feeds-per-user`]##
|
||||
|
||||
[.description]
|
||||
--
|
||||
Limit the number of feeds a user can subscribe to. 0 to disable.
|
||||
|
||||
|
||||
ifdef::add-copy-button-to-env-var[]
|
||||
Environment variable: env_var_with_copy_button:+++COMMAFEED_DATABASE_CLEANUP_MAX_FEEDS_PER_USER+++[]
|
||||
endif::add-copy-button-to-env-var[]
|
||||
ifndef::add-copy-button-to-env-var[]
|
||||
Environment variable: `+++COMMAFEED_DATABASE_CLEANUP_MAX_FEEDS_PER_USER+++`
|
||||
endif::add-copy-button-to-env-var[]
|
||||
--
|
||||
|int
|
||||
|`0`
|
||||
|
||||
a| [[commafeed-server_commafeed-database-cleanup-batch-size]] [.property-path]##link:#commafeed-server_commafeed-database-cleanup-batch-size[`commafeed.database.cleanup.batch-size`]##
|
||||
|
||||
[.description]
|
||||
--
|
||||
Rows to delete per query while cleaning up old entries.
|
||||
|
||||
|
||||
ifdef::add-copy-button-to-env-var[]
|
||||
Environment variable: env_var_with_copy_button:+++COMMAFEED_DATABASE_CLEANUP_BATCH_SIZE+++[]
|
||||
endif::add-copy-button-to-env-var[]
|
||||
ifndef::add-copy-button-to-env-var[]
|
||||
Environment variable: `+++COMMAFEED_DATABASE_CLEANUP_BATCH_SIZE+++`
|
||||
endif::add-copy-button-to-env-var[]
|
||||
--
|
||||
|int
|
||||
|`100`
|
||||
|
||||
|
||||
|
||||
h|[[commafeed-server_section_commafeed-users]] [.section-name.section-level0]##link:#commafeed-server_section_commafeed-users[Users settings]##
|
||||
h|Type
|
||||
h|Default
|
||||
|
||||
a| [[commafeed-server_commafeed-users-allow-registrations]] [.property-path]##link:#commafeed-server_commafeed-users-allow-registrations[`commafeed.users.allow-registrations`]##
|
||||
|
||||
[.description]
|
||||
--
|
||||
Whether to let users create accounts for themselves.
|
||||
|
||||
|
||||
ifdef::add-copy-button-to-env-var[]
|
||||
Environment variable: env_var_with_copy_button:+++COMMAFEED_USERS_ALLOW_REGISTRATIONS+++[]
|
||||
endif::add-copy-button-to-env-var[]
|
||||
ifndef::add-copy-button-to-env-var[]
|
||||
Environment variable: `+++COMMAFEED_USERS_ALLOW_REGISTRATIONS+++`
|
||||
endif::add-copy-button-to-env-var[]
|
||||
--
|
||||
|boolean
|
||||
|`false`
|
||||
|
||||
a| [[commafeed-server_commafeed-users-strict-password-policy]] [.property-path]##link:#commafeed-server_commafeed-users-strict-password-policy[`commafeed.users.strict-password-policy`]##
|
||||
|
||||
[.description]
|
||||
--
|
||||
Whether to enable strict password validation (1 uppercase char, 1 lowercase char, 1 digit, 1 special char).
|
||||
|
||||
|
||||
ifdef::add-copy-button-to-env-var[]
|
||||
Environment variable: env_var_with_copy_button:+++COMMAFEED_USERS_STRICT_PASSWORD_POLICY+++[]
|
||||
endif::add-copy-button-to-env-var[]
|
||||
ifndef::add-copy-button-to-env-var[]
|
||||
Environment variable: `+++COMMAFEED_USERS_STRICT_PASSWORD_POLICY+++`
|
||||
endif::add-copy-button-to-env-var[]
|
||||
--
|
||||
|boolean
|
||||
|`true`
|
||||
|
||||
a| [[commafeed-server_commafeed-users-create-demo-account]] [.property-path]##link:#commafeed-server_commafeed-users-create-demo-account[`commafeed.users.create-demo-account`]##
|
||||
|
||||
[.description]
|
||||
--
|
||||
Whether to create a demo account the first time the app starts.
|
||||
|
||||
|
||||
ifdef::add-copy-button-to-env-var[]
|
||||
Environment variable: env_var_with_copy_button:+++COMMAFEED_USERS_CREATE_DEMO_ACCOUNT+++[]
|
||||
endif::add-copy-button-to-env-var[]
|
||||
ifndef::add-copy-button-to-env-var[]
|
||||
Environment variable: `+++COMMAFEED_USERS_CREATE_DEMO_ACCOUNT+++`
|
||||
endif::add-copy-button-to-env-var[]
|
||||
--
|
||||
|boolean
|
||||
|`false`
|
||||
|
||||
|
||||
h|[[commafeed-server_section_commafeed-websocket]] [.section-name.section-level0]##link:#commafeed-server_section_commafeed-websocket[Websocket settings]##
|
||||
h|Type
|
||||
h|Default
|
||||
|
||||
a| [[commafeed-server_commafeed-websocket-enabled]] [.property-path]##link:#commafeed-server_commafeed-websocket-enabled[`commafeed.websocket.enabled`]##
|
||||
|
||||
[.description]
|
||||
--
|
||||
Enable websocket connection so the server can notify web clients that there are new entries for feeds.
|
||||
|
||||
|
||||
ifdef::add-copy-button-to-env-var[]
|
||||
Environment variable: env_var_with_copy_button:+++COMMAFEED_WEBSOCKET_ENABLED+++[]
|
||||
endif::add-copy-button-to-env-var[]
|
||||
ifndef::add-copy-button-to-env-var[]
|
||||
Environment variable: `+++COMMAFEED_WEBSOCKET_ENABLED+++`
|
||||
endif::add-copy-button-to-env-var[]
|
||||
--
|
||||
|boolean
|
||||
|`true`
|
||||
|
||||
a| [[commafeed-server_commafeed-websocket-ping-interval]] [.property-path]##link:#commafeed-server_commafeed-websocket-ping-interval[`commafeed.websocket.ping-interval`]##
|
||||
|
||||
[.description]
|
||||
--
|
||||
Interval at which the client will send a ping message on the websocket to keep the connection alive.
|
||||
|
||||
|
||||
ifdef::add-copy-button-to-env-var[]
|
||||
Environment variable: env_var_with_copy_button:+++COMMAFEED_WEBSOCKET_PING_INTERVAL+++[]
|
||||
endif::add-copy-button-to-env-var[]
|
||||
ifndef::add-copy-button-to-env-var[]
|
||||
Environment variable: `+++COMMAFEED_WEBSOCKET_PING_INTERVAL+++`
|
||||
endif::add-copy-button-to-env-var[]
|
||||
--
|
||||
|link:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html[Duration] link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|
||||
|`15M`
|
||||
|
||||
a| [[commafeed-server_commafeed-websocket-tree-reload-interval]] [.property-path]##link:#commafeed-server_commafeed-websocket-tree-reload-interval[`commafeed.websocket.tree-reload-interval`]##
|
||||
|
||||
[.description]
|
||||
--
|
||||
If the websocket connection is disabled or the connection is lost, the client will reload the feed tree at this interval.
|
||||
|
||||
|
||||
ifdef::add-copy-button-to-env-var[]
|
||||
Environment variable: env_var_with_copy_button:+++COMMAFEED_WEBSOCKET_TREE_RELOAD_INTERVAL+++[]
|
||||
endif::add-copy-button-to-env-var[]
|
||||
ifndef::add-copy-button-to-env-var[]
|
||||
Environment variable: `+++COMMAFEED_WEBSOCKET_TREE_RELOAD_INTERVAL+++`
|
||||
endif::add-copy-button-to-env-var[]
|
||||
--
|
||||
|link:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html[Duration] link:#duration-note-anchor-{summaryTableId}[icon:question-circle[title=More information about the Duration format]]
|
||||
|`30S`
|
||||
|
||||
|
||||
|===
|
||||
|
||||
ifndef::no-duration-note[]
|
||||
[NOTE]
|
||||
[id=duration-note-anchor-commafeed-server_commafeed]
|
||||
.About the Duration format
|
||||
====
|
||||
To write duration values, use the standard `java.time.Duration` format.
|
||||
See the link:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html#parse(java.lang.CharSequence)[Duration#parse() Java API documentation] for more information.
|
||||
|
||||
You can also use a simplified format, starting with a number:
|
||||
|
||||
* If the value is only a number, it represents time in seconds.
|
||||
* If the value is a number followed by `ms`, it represents time in milliseconds.
|
||||
|
||||
In other cases, the simplified format is translated to the `java.time.Duration` format for parsing:
|
||||
|
||||
* If the value is a number followed by `h`, `m`, or `s`, it is prefixed with `PT`.
|
||||
* If the value is a number followed by `d`, it is prefixed with `P`.
|
||||
====
|
||||
endif::no-duration-note[]
|
||||
ifndef::no-memory-size-note[]
|
||||
[NOTE]
|
||||
[id=memory-size-note-anchor-commafeed-server_commafeed]
|
||||
.About the MemorySize format
|
||||
====
|
||||
A size configuration option recognizes strings in this format (shown as a regular expression): `[0-9]+[KkMmGgTtPpEeZzYy]?`.
|
||||
|
||||
If no suffix is given, assume bytes.
|
||||
====
|
||||
ifndef::no-memory-size-note[]
|
||||
|
||||
:!summaryTableId:
|
||||
@@ -1,19 +0,0 @@
|
||||
version: "3.1"
|
||||
|
||||
services:
|
||||
mysql:
|
||||
image: mariadb
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=root
|
||||
- MYSQL_DATABASE=commafeed
|
||||
ports:
|
||||
- "3306:3306"
|
||||
|
||||
postgresql:
|
||||
image: postgres
|
||||
environment:
|
||||
- POSTGRES_USER=root
|
||||
- POSTGRES_PASSWORD=root
|
||||
- POSTGRES_DB=commafeed
|
||||
ports:
|
||||
- "5432:5432"
|
||||
@@ -6,33 +6,26 @@
|
||||
<parent>
|
||||
<groupId>com.commafeed</groupId>
|
||||
<artifactId>commafeed</artifactId>
|
||||
<version>4.6.0</version>
|
||||
<version>5.3.1</version>
|
||||
</parent>
|
||||
<artifactId>commafeed-server</artifactId>
|
||||
<name>CommaFeed Server</name>
|
||||
|
||||
<properties>
|
||||
<guice.version>7.0.0</guice.version>
|
||||
<querydsl.version>6.5</querydsl.version>
|
||||
<quarkus.version>3.15.1</quarkus.version>
|
||||
<querydsl.version>6.8</querydsl.version>
|
||||
<rome.version>2.1.0</rome.version>
|
||||
<swagger.version>2.2.25</swagger.version>
|
||||
|
||||
<testcontainers.version>1.19.8</testcontainers.version>
|
||||
<!-- renovate: datasource=docker depName=postgres -->
|
||||
<postgresql.image.version>16.3</postgresql.image.version>
|
||||
<!-- renovate: datasource=docker depName=mysql -->
|
||||
<mysql.image.version>9.0.0</mysql.image.version>
|
||||
<!-- renovate: datasource=docker depName=mariadb -->
|
||||
<mariadb.image.version>11.4.2</mariadb.image.version>
|
||||
<!-- renovate: datasource=docker depName=redis -->
|
||||
<redis.image.version>7.2.5</redis.image.version>
|
||||
<build.database>h2</build.database>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-dependencies</artifactId>
|
||||
<version>4.0.7</version>
|
||||
<groupId>io.quarkus.platform</groupId>
|
||||
<artifactId>quarkus-bom</artifactId>
|
||||
<version>${quarkus.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
@@ -40,31 +33,120 @@
|
||||
</dependencyManagement>
|
||||
|
||||
<build>
|
||||
<finalName>commafeed</finalName>
|
||||
<testResources>
|
||||
<testResource>
|
||||
<directory>src/test/resources</directory>
|
||||
<filtering>false</filtering>
|
||||
</testResource>
|
||||
<testResource>
|
||||
<directory>src/test/resources</directory>
|
||||
<includes>
|
||||
<include>docker-images.properties</include>
|
||||
</includes>
|
||||
<filtering>true</filtering>
|
||||
</testResource>
|
||||
</testResources>
|
||||
|
||||
<extensions>
|
||||
<extension>
|
||||
<groupId>kr.motd.maven</groupId>
|
||||
<artifactId>os-maven-plugin</artifactId>
|
||||
<version>1.7.1</version>
|
||||
</extension>
|
||||
</extensions>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-help-plugin</artifactId>
|
||||
<version>3.5.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>initialize</phase>
|
||||
<goals>
|
||||
<goal>active-profiles</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>properties-maven-plugin</artifactId>
|
||||
<version>1.2.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>set-system-properties</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<properties>
|
||||
<property>
|
||||
<name>quarkus.datasource.db-kind</name>
|
||||
<value>${build.database}</value>
|
||||
</property>
|
||||
</properties>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>io.quarkus.platform</groupId>
|
||||
<artifactId>quarkus-maven-plugin</artifactId>
|
||||
<version>${quarkus.version}</version>
|
||||
<extensions>true</extensions>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>build</goal>
|
||||
<goal>generate-code</goal>
|
||||
<goal>generate-code-tests</goal>
|
||||
<goal>native-image-agent</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<properties>
|
||||
<quarkus.package.output-name>commafeed-${project.version}</quarkus.package.output-name>
|
||||
<quarkus.package.runner-suffix>
|
||||
-${build.database}-${os.detected.name}-${os.detected.arch}-runner
|
||||
</quarkus.package.runner-suffix>
|
||||
</properties>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-config-doc-maven-plugin</artifactId>
|
||||
<version>${quarkus.version}</version>
|
||||
<extensions>true</extensions>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>default-generate-asciidoc</id>
|
||||
<phase>process-test-resources</phase>
|
||||
<goals>
|
||||
<goal>generate-asciidoc</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<version>3.7.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<finalName>commafeed-${project.version}-${build.database}-jvm</finalName>
|
||||
<appendAssemblyId>false</appendAssemblyId>
|
||||
<descriptors>
|
||||
<descriptor>src/main/assembly/zip-quarkus-app.xml</descriptor>
|
||||
</descriptors>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.3.1</version>
|
||||
<version>3.5.0</version>
|
||||
<configuration>
|
||||
<systemPropertyVariables>
|
||||
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
|
||||
<quarkus.datasource.db-kind>${build.database}</quarkus.datasource.db-kind>
|
||||
</systemPropertyVariables>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-failsafe-plugin</artifactId>
|
||||
<version>3.3.1</version>
|
||||
<version>3.5.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
@@ -73,6 +155,37 @@
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<systemPropertyVariables>
|
||||
<native.image.path>${project.build.directory}/${project.build.finalName}-runner
|
||||
</native.image.path>
|
||||
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
|
||||
<quarkus.datasource.db-kind>${build.database}</quarkus.datasource.db-kind>
|
||||
</systemPropertyVariables>
|
||||
</configuration>
|
||||
<!-- failsafe plugin does not seem to be able to pick up dependencies declared in profiles -->
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-jdbc-h2</artifactId>
|
||||
<version>${quarkus.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-jdbc-mysql</artifactId>
|
||||
<version>${quarkus.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-jdbc-mariadb</artifactId>
|
||||
<version>${quarkus.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-jdbc-postgresql</artifactId>
|
||||
<version>${quarkus.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>io.github.git-commit-id</groupId>
|
||||
@@ -93,63 +206,13 @@
|
||||
<failOnUnableToExtractRepoInfo>false</failOnUnableToExtractRepoInfo>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.6.0</version>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.kordamp.shade</groupId>
|
||||
<artifactId>maven-shade-ext-transformers</artifactId>
|
||||
<version>1.4.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<configuration>
|
||||
<createDependencyReducedPom>false</createDependencyReducedPom>
|
||||
<filters>
|
||||
<filter>
|
||||
<artifact>*:*</artifact>
|
||||
<excludes>
|
||||
<exclude>module-info.class</exclude>
|
||||
<exclude>META-INF/*.SF</exclude>
|
||||
<exclude>META-INF/*.DSA</exclude>
|
||||
<exclude>META-INF/*.RSA</exclude>
|
||||
</excludes>
|
||||
</filter>
|
||||
</filters>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<transformers>
|
||||
<transformer
|
||||
implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" />
|
||||
<transformer
|
||||
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||
<mainClass>com.commafeed.CommaFeedApplication</mainClass>
|
||||
</transformer>
|
||||
<transformer implementation="org.kordamp.shade.resources.PropertiesFileTransformer">
|
||||
<paths>
|
||||
<path>rome.properties</path>
|
||||
</paths>
|
||||
<mergeStrategy>append</mergeStrategy>
|
||||
</transformer>
|
||||
</transformers>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>io.swagger.core.v3</groupId>
|
||||
<artifactId>swagger-maven-plugin-jakarta</artifactId>
|
||||
<version>2.2.22</version>
|
||||
<version>${swagger.version}</version>
|
||||
<?m2e ignore?>
|
||||
<configuration>
|
||||
<outputPath>${project.build.directory}/classes/assets</outputPath>
|
||||
<outputPath>${project.build.directory}/classes/META-INF/resources</outputPath>
|
||||
<outputFormat>JSONANDYAML</outputFormat>
|
||||
<resourcePackages>
|
||||
<package>com.commafeed.frontend.resource</package>
|
||||
@@ -167,22 +230,17 @@
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>3.4.2</version>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifest>
|
||||
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
|
||||
</manifest>
|
||||
</archive>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-checkstyle-plugin</artifactId>
|
||||
<version>3.4.0</version>
|
||||
<version>3.5.0</version>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.puppycrawl.tools</groupId>
|
||||
<artifactId>checkstyle</artifactId>
|
||||
<version>10.18.2</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>validate</id>
|
||||
@@ -236,9 +294,10 @@
|
||||
<dependency>
|
||||
<groupId>com.commafeed</groupId>
|
||||
<artifactId>commafeed-client</artifactId>
|
||||
<version>4.6.0</version>
|
||||
<version>5.3.1</version>
|
||||
</dependency>
|
||||
|
||||
<!-- compile-time processors -->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
@@ -246,68 +305,66 @@
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
<groupId>org.kohsuke.metainf-services</groupId>
|
||||
<artifactId>metainf-services</artifactId>
|
||||
<version>1.11</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>jcl-over-slf4j</artifactId>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-extension-processor</artifactId>
|
||||
<version>${quarkus.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- quarkus dependencies -->
|
||||
<dependency>
|
||||
<groupId>com.google.inject</groupId>
|
||||
<artifactId>guice</artifactId>
|
||||
<version>${guice.version}</version>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-arc</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-security</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-hibernate-validator</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-rest</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-rest-jackson</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-websockets</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-mailer</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-hibernate-orm</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-liquibase</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-unix-socket</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-hibernate</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-migrations</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-assets</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-forms</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard.metrics</groupId>
|
||||
<artifactId>metrics-graphite</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard.metrics</groupId>
|
||||
<artifactId>metrics-json</artifactId>
|
||||
<version>4.2.28</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.whitfin</groupId>
|
||||
<artifactId>dropwizard-environment-substitutor</artifactId>
|
||||
<version>1.1.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.websocket</groupId>
|
||||
<artifactId>websocket-jakarta-server</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.swagger.core.v3</groupId>
|
||||
<artifactId>swagger-annotations</artifactId>
|
||||
<version>2.2.22</version>
|
||||
<version>${swagger.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.github.openfeign.querydsl</groupId>
|
||||
<artifactId>querydsl-apt</artifactId>
|
||||
@@ -320,7 +377,10 @@
|
||||
<artifactId>querydsl-jpa</artifactId>
|
||||
<version>${querydsl.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-collections4</artifactId>
|
||||
@@ -345,20 +405,8 @@
|
||||
<dependency>
|
||||
<groupId>org.passay</groupId>
|
||||
<artifactId>passay</artifactId>
|
||||
<version>1.6.4</version>
|
||||
<version>1.6.5</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>redis.clients</groupId>
|
||||
<artifactId>jedis</artifactId>
|
||||
<version>5.1.3</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.sun.mail</groupId>
|
||||
<artifactId>jakarta.mail</artifactId>
|
||||
<version>2.0.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.rometools</groupId>
|
||||
<artifactId>rome</artifactId>
|
||||
@@ -374,7 +422,6 @@
|
||||
<artifactId>rome-opml</artifactId>
|
||||
<version>${rome.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.ahocorasick</groupId>
|
||||
<artifactId>ahocorasick</artifactId>
|
||||
@@ -400,66 +447,27 @@
|
||||
<artifactId>urlcanon</artifactId>
|
||||
<version>0.4.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.gwtproject</groupId>
|
||||
<artifactId>gwt-servlet</artifactId>
|
||||
<version>2.11.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents.client5</groupId>
|
||||
<artifactId>httpclient5</artifactId>
|
||||
<version>5.4</version>
|
||||
</dependency>
|
||||
<!-- add brotli support for httpclient5 -->
|
||||
<dependency>
|
||||
<groupId>org.brotli</groupId>
|
||||
<artifactId>dec</artifactId>
|
||||
<version>0.1.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.hakky54</groupId>
|
||||
<artifactId>sslcontext-kickstart-for-apache5</artifactId>
|
||||
<version>8.3.6</version>
|
||||
<version>8.3.7</version>
|
||||
</dependency>
|
||||
|
||||
<!-- test dependencies -->
|
||||
<dependency>
|
||||
<groupId>com.google.apis</groupId>
|
||||
<artifactId>google-api-services-youtube</artifactId>
|
||||
<version>v3-rev20240514-2.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.manticore-projects.tools</groupId>
|
||||
<artifactId>h2migrationtool</artifactId>
|
||||
<version>1.7</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<version>9.0.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mariadb.jdbc</groupId>
|
||||
<artifactId>mariadb-java-client</artifactId>
|
||||
<version>3.4.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<version>42.7.3</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-engine</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-junit-jupiter</artifactId>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-junit5-mockito</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
@@ -469,14 +477,8 @@
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.icegreen</groupId>
|
||||
<artifactId>greenmail-junit5</artifactId>
|
||||
<version>2.0.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-testing</artifactId>
|
||||
<groupId>io.rest-assured</groupId>
|
||||
<artifactId>rest-assured</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
@@ -487,33 +489,80 @@
|
||||
<dependency>
|
||||
<groupId>com.microsoft.playwright</groupId>
|
||||
<artifactId>playwright</artifactId>
|
||||
<version>1.45.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>testcontainers</artifactId>
|
||||
<version>${testcontainers.version}</version>
|
||||
<version>1.47.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<version>${testcontainers.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>mysql</artifactId>
|
||||
<version>${testcontainers.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>mariadb</artifactId>
|
||||
<version>${testcontainers.version}</version>
|
||||
<groupId>org.reflections</groupId>
|
||||
<artifactId>reflections</artifactId>
|
||||
<version>0.10.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>native</id>
|
||||
<activation>
|
||||
<property>
|
||||
<name>native</name>
|
||||
</property>
|
||||
</activation>
|
||||
<properties>
|
||||
<quarkus.native.enabled>true</quarkus.native.enabled>
|
||||
</properties>
|
||||
</profile>
|
||||
|
||||
<profile>
|
||||
<id>h2</id>
|
||||
<activation>
|
||||
<activeByDefault>true</activeByDefault>
|
||||
</activation>
|
||||
<properties>
|
||||
<build.database>h2</build.database>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-jdbc-h2</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>mysql</id>
|
||||
<properties>
|
||||
<build.database>mysql</build.database>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-jdbc-mysql</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>mariadb</id>
|
||||
<properties>
|
||||
<build.database>mariadb</build.database>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-jdbc-mariadb</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>postgresql</id>
|
||||
<properties>
|
||||
<build.database>postgresql</build.database>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-jdbc-postgresql</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</profile>
|
||||
</profiles>
|
||||
</project>
|
||||
|
||||
21
commafeed-server/src/main/assembly/zip-quarkus-app.xml
Normal file
21
commafeed-server/src/main/assembly/zip-quarkus-app.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.2.0 https://maven.apache.org/xsd/assembly-2.2.0.xsd">
|
||||
|
||||
<id>zip-quarkus-app</id>
|
||||
|
||||
<includeBaseDirectory>true</includeBaseDirectory>
|
||||
<baseDirectory>commafeed-${project.version}-${build.database}</baseDirectory>
|
||||
|
||||
<formats>
|
||||
<format>zip</format>
|
||||
</formats>
|
||||
<fileSets>
|
||||
<fileSet>
|
||||
<directory>${project.build.directory}/quarkus-app</directory>
|
||||
<outputDirectory>/</outputDirectory>
|
||||
<includes>
|
||||
<include>**/*</include>
|
||||
</includes>
|
||||
</fileSet>
|
||||
</fileSets>
|
||||
</assembly>
|
||||
15
commafeed-server/src/main/docker/Dockerfile.jvm
Normal file
15
commafeed-server/src/main/docker/Dockerfile.jvm
Normal file
@@ -0,0 +1,15 @@
|
||||
FROM ibm-semeru-runtimes:open-21.0.4_7-jre
|
||||
EXPOSE 8082
|
||||
|
||||
RUN mkdir -p /commafeed/data
|
||||
VOLUME /commafeed/data
|
||||
|
||||
COPY commafeed-server/target/quarkus-app/ /commafeed
|
||||
WORKDIR /commafeed
|
||||
|
||||
CMD ["java", \
|
||||
"-Xtune:virtualized", \
|
||||
"-Xminf0.05", \
|
||||
"-Xmaxf0.1", \
|
||||
"-jar", \
|
||||
"quarkus-run.jar"]
|
||||
10
commafeed-server/src/main/docker/Dockerfile.native
Normal file
10
commafeed-server/src/main/docker/Dockerfile.native
Normal file
@@ -0,0 +1,10 @@
|
||||
FROM debian:12.7
|
||||
EXPOSE 8082
|
||||
|
||||
RUN mkdir -p /commafeed/data
|
||||
VOLUME /commafeed/data
|
||||
|
||||
COPY commafeed-server/target/commafeed-*-runner /commafeed/application
|
||||
WORKDIR /commafeed
|
||||
|
||||
CMD ["./application"]
|
||||
96
commafeed-server/src/main/docker/README.md
Normal file
96
commafeed-server/src/main/docker/README.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# CommaFeed
|
||||
|
||||
Official docker images for https://github.com/Athou/commafeed/
|
||||
|
||||
## Quickstart
|
||||
|
||||
Start CommaFeed with a H2 embedded database. Then login as `admin/admin` on http://localhost:8082/
|
||||
|
||||
### docker
|
||||
|
||||
`docker run --name commafeed --detach --publish 8082:8082 --restart unless-stopped --volume /path/to/commafeed/db:/commafeed/data --memory 256M athou/commafeed:latest-h2`
|
||||
|
||||
### docker-compose
|
||||
|
||||
```
|
||||
services:
|
||||
commafeed:
|
||||
image: athou/commafeed:latest-h2
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- /path/to/commafeed/db:/commafeed/data
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 256M
|
||||
ports:
|
||||
- 8082:8082
|
||||
```
|
||||
|
||||
## Advanced
|
||||
|
||||
While using the H2 embedded database is perfectly fine for small instances, you may want to have more control over the
|
||||
database. Here's an example that uses PostgreSQL (note the image tag change from `latest-h2` to `latest-postgresql`):
|
||||
|
||||
```
|
||||
services:
|
||||
commafeed:
|
||||
image: athou/commafeed:latest-postgresql
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- QUARKUS_DATASOURCE_JDBC_URL=jdbc:postgresql://postgresql:5432/commafeed
|
||||
- QUARKUS_DATASOURCE_USERNAME=commafeed
|
||||
- QUARKUS_DATASOURCE_PASSWORD=commafeed
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 256M
|
||||
ports:
|
||||
- 8082:8082
|
||||
|
||||
postgresql:
|
||||
image: postgres:latest
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: commafeed
|
||||
POSTGRES_PASSWORD: commafeed
|
||||
POSTGRES_DB: commafeed
|
||||
volumes:
|
||||
- /path/to/commafeed/db:/var/lib/postgresql/data
|
||||
```
|
||||
|
||||
CommaFeed also supports:
|
||||
|
||||
- MySQL:
|
||||
`QUARKUS_DATASOURCE_JDBC_URL=jdbc:mysql://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC`
|
||||
- MariaDB:
|
||||
`QUARKUS_DATASOURCE_JDBC_URL=jdbc:mariadb://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC`
|
||||
|
||||
## Configuration
|
||||
|
||||
All [CommaFeed settings](https://github.com/Athou/commafeed/blob/master/commafeed-server/doc/commafeed.adoc) are
|
||||
optional and have sensible default values.
|
||||
|
||||
Settings are overrideable with environment variables. For instance, `commafeed.feed-refresh.interval-empirical` can be
|
||||
set with the `COMMAFEED_FEED_REFRESH_INTERVAL_EMPIRICAL` variable.
|
||||
|
||||
When logging in, credentials are stored in an encrypted cookie. The encryption key is randomly generated at startup,
|
||||
meaning that you will have to log back in after each restart of the application. To prevent this, you can set the
|
||||
`QUARKUS_HTTP_AUTH_SESSION_ENCRYPTION_KEY` variable to a fixed value (min. 16 characters).
|
||||
All other Quarkus settings can be found [here](https://quarkus.io/guides/all-config).
|
||||
|
||||
### Updates
|
||||
|
||||
When CommaFeed is up and running, you can subscribe to [this feed](https://github.com/Athou/commafeed/releases.atom) to be notified of new releases.
|
||||
|
||||
## Docker tags
|
||||
|
||||
Tags are of the form `<version>-<database>[-jvm]` where:
|
||||
|
||||
- `<version>` is either:
|
||||
- a specific CommaFeed version (e.g. `5.0.0`)
|
||||
- `latest` (always points to the latest version)
|
||||
- `master` (always points to the latest git commit)
|
||||
- `<database>` is the database to use (`h2`, `postgresql`, `mysql` or `mariadb`)
|
||||
- `-jvm` is optional and indicates that CommaFeed is running on a JVM, and not compiled natively. This image supports
|
||||
the arm64 platform which is not yet supported by the native image.
|
||||
@@ -1,277 +1,40 @@
|
||||
package com.commafeed;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.Instant;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jetty.websocket.jakarta.server.config.JakartaWebSocketServletContainerInitializer;
|
||||
import org.hibernate.cfg.AvailableSettings;
|
||||
|
||||
import com.codahale.metrics.json.MetricsModule;
|
||||
import com.commafeed.backend.dao.UserDAO;
|
||||
import com.commafeed.backend.feed.FeedRefreshEngine;
|
||||
import com.commafeed.backend.model.AbstractModel;
|
||||
import com.commafeed.backend.model.Feed;
|
||||
import com.commafeed.backend.model.FeedCategory;
|
||||
import com.commafeed.backend.model.FeedEntry;
|
||||
import com.commafeed.backend.model.FeedEntryContent;
|
||||
import com.commafeed.backend.model.FeedEntryStatus;
|
||||
import com.commafeed.backend.model.FeedEntryTag;
|
||||
import com.commafeed.backend.model.FeedSubscription;
|
||||
import com.commafeed.backend.model.User;
|
||||
import com.commafeed.backend.model.UserRole;
|
||||
import com.commafeed.backend.model.UserSettings;
|
||||
import com.commafeed.backend.service.UserService;
|
||||
import com.commafeed.backend.service.db.DatabaseStartupService;
|
||||
import com.commafeed.backend.service.db.H2MigrationService;
|
||||
import com.commafeed.backend.task.ScheduledTask;
|
||||
import com.commafeed.frontend.auth.PasswordConstraintValidator;
|
||||
import com.commafeed.frontend.auth.SecurityCheckFactoryProvider;
|
||||
import com.commafeed.frontend.resource.AdminREST;
|
||||
import com.commafeed.frontend.resource.CategoryREST;
|
||||
import com.commafeed.frontend.resource.EntryREST;
|
||||
import com.commafeed.frontend.resource.FeedREST;
|
||||
import com.commafeed.frontend.resource.ServerREST;
|
||||
import com.commafeed.frontend.resource.UserREST;
|
||||
import com.commafeed.frontend.resource.fever.FeverREST;
|
||||
import com.commafeed.frontend.servlet.CustomCssServlet;
|
||||
import com.commafeed.frontend.servlet.CustomJsServlet;
|
||||
import com.commafeed.frontend.servlet.LogoutServlet;
|
||||
import com.commafeed.frontend.servlet.NextUnreadServlet;
|
||||
import com.commafeed.frontend.servlet.RobotsTxtDisallowAllServlet;
|
||||
import com.commafeed.frontend.session.SessionHelperFactoryProvider;
|
||||
import com.commafeed.frontend.ws.WebSocketConfigurator;
|
||||
import com.commafeed.frontend.ws.WebSocketEndpoint;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.MapperFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.google.inject.Guice;
|
||||
import com.google.inject.Injector;
|
||||
import com.google.inject.Key;
|
||||
import com.google.inject.TypeLiteral;
|
||||
import com.commafeed.backend.task.TaskScheduler;
|
||||
import com.commafeed.security.password.PasswordConstraintValidator;
|
||||
|
||||
import io.dropwizard.assets.AssetsBundle;
|
||||
import io.dropwizard.configuration.DefaultConfigurationFactoryFactory;
|
||||
import io.dropwizard.configuration.EnvironmentVariableSubstitutor;
|
||||
import io.dropwizard.configuration.SubstitutingSourceProvider;
|
||||
import io.dropwizard.core.Application;
|
||||
import io.dropwizard.core.ConfiguredBundle;
|
||||
import io.dropwizard.core.setup.Bootstrap;
|
||||
import io.dropwizard.core.setup.Environment;
|
||||
import io.dropwizard.db.DataSourceFactory;
|
||||
import io.dropwizard.forms.MultiPartBundle;
|
||||
import io.dropwizard.hibernate.HibernateBundle;
|
||||
import io.dropwizard.migrations.MigrationsBundle;
|
||||
import io.dropwizard.servlets.CacheBustingFilter;
|
||||
import io.whitfin.dropwizard.configuration.EnvironmentSubstitutor;
|
||||
import jakarta.servlet.DispatcherType;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.ServletResponse;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.websocket.server.ServerEndpointConfig;
|
||||
import io.quarkus.runtime.ShutdownEvent;
|
||||
import io.quarkus.runtime.StartupEvent;
|
||||
import jakarta.enterprise.event.Observes;
|
||||
import jakarta.inject.Singleton;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
|
||||
@Singleton
|
||||
@RequiredArgsConstructor
|
||||
public class CommaFeedApplication {
|
||||
|
||||
public static final String USERNAME_ADMIN = "admin";
|
||||
public static final String USERNAME_DEMO = "demo";
|
||||
|
||||
public static final Instant STARTUP_TIME = Instant.now();
|
||||
private final DatabaseStartupService databaseStartupService;
|
||||
private final FeedRefreshEngine feedRefreshEngine;
|
||||
private final TaskScheduler taskScheduler;
|
||||
private final CommaFeedConfiguration config;
|
||||
|
||||
private HibernateBundle<CommaFeedConfiguration> hibernateBundle;
|
||||
public void start(@Observes StartupEvent ev) {
|
||||
PasswordConstraintValidator.setStrict(config.users().strictPasswordPolicy());
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "CommaFeed";
|
||||
databaseStartupService.populateInitialData();
|
||||
|
||||
feedRefreshEngine.start();
|
||||
taskScheduler.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(Bootstrap<CommaFeedConfiguration> bootstrap) {
|
||||
configureEnvironmentSubstitutor(bootstrap);
|
||||
configureObjectMapper(bootstrap.getObjectMapper());
|
||||
|
||||
// run h2 migration as the first bundle because we need to migrate before hibernate is initialized
|
||||
bootstrap.addBundle(new ConfiguredBundle<>() {
|
||||
@Override
|
||||
public void run(CommaFeedConfiguration config, Environment environment) {
|
||||
DataSourceFactory dataSourceFactory = config.getDataSourceFactory();
|
||||
String url = dataSourceFactory.getUrl();
|
||||
if (isFileBasedH2(url)) {
|
||||
Path path = getFilePath(url);
|
||||
String user = dataSourceFactory.getUser();
|
||||
String password = dataSourceFactory.getPassword();
|
||||
new H2MigrationService().migrateIfNeeded(path, user, password);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isFileBasedH2(String url) {
|
||||
return url.startsWith("jdbc:h2:") && !url.startsWith("jdbc:h2:mem:");
|
||||
}
|
||||
|
||||
private Path getFilePath(String url) {
|
||||
String name = url.substring("jdbc:h2:".length()).split(";")[0];
|
||||
return Paths.get(name + ".mv.db");
|
||||
}
|
||||
});
|
||||
|
||||
bootstrap.addBundle(hibernateBundle = new HibernateBundle<>(AbstractModel.class, Feed.class, FeedCategory.class, FeedEntry.class,
|
||||
FeedEntryContent.class, FeedEntryStatus.class, FeedEntryTag.class, FeedSubscription.class, User.class, UserRole.class,
|
||||
UserSettings.class) {
|
||||
@Override
|
||||
public DataSourceFactory getDataSourceFactory(CommaFeedConfiguration configuration) {
|
||||
DataSourceFactory factory = configuration.getDataSourceFactory();
|
||||
|
||||
factory.getProperties().put(AvailableSettings.PREFERRED_POOLED_OPTIMIZER, "pooled-lo");
|
||||
|
||||
factory.getProperties().put(AvailableSettings.STATEMENT_BATCH_SIZE, "50");
|
||||
factory.getProperties().put(AvailableSettings.BATCH_VERSIONED_DATA, "true");
|
||||
factory.getProperties().put(AvailableSettings.ORDER_INSERTS, "true");
|
||||
factory.getProperties().put(AvailableSettings.ORDER_UPDATES, "true");
|
||||
return factory;
|
||||
}
|
||||
});
|
||||
|
||||
bootstrap.addBundle(new MigrationsBundle<>() {
|
||||
@Override
|
||||
public DataSourceFactory getDataSourceFactory(CommaFeedConfiguration configuration) {
|
||||
return configuration.getDataSourceFactory();
|
||||
}
|
||||
});
|
||||
|
||||
bootstrap.addBundle(new AssetsBundle("/assets/", "/", "index.html"));
|
||||
bootstrap.addBundle(new MultiPartBundle());
|
||||
public void stop(@Observes ShutdownEvent ev) {
|
||||
feedRefreshEngine.stop();
|
||||
taskScheduler.stop();
|
||||
}
|
||||
|
||||
private static void configureEnvironmentSubstitutor(Bootstrap<CommaFeedConfiguration> bootstrap) {
|
||||
bootstrap.setConfigurationFactoryFactory(new DefaultConfigurationFactoryFactory<>() {
|
||||
@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));
|
||||
}
|
||||
});
|
||||
|
||||
bootstrap.setConfigurationSourceProvider(buildEnvironmentSubstitutor(bootstrap));
|
||||
}
|
||||
|
||||
private static void configureObjectMapper(ObjectMapper objectMapper) {
|
||||
// read and write instants as milliseconds instead of nanoseconds
|
||||
objectMapper.configure(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, false)
|
||||
.configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, false);
|
||||
|
||||
// add support for serializing metrics
|
||||
objectMapper.registerModule(new MetricsModule(TimeUnit.SECONDS, TimeUnit.SECONDS, false));
|
||||
}
|
||||
|
||||
private static EnvironmentSubstitutor buildEnvironmentSubstitutor(Bootstrap<CommaFeedConfiguration> bootstrap) {
|
||||
// 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
|
||||
return new EnvironmentSubstitutor("CF", substitutingSourceProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(CommaFeedConfiguration config, Environment environment) {
|
||||
PasswordConstraintValidator.setStrict(config.getApplicationSettings().getStrictPasswordPolicy());
|
||||
|
||||
// guice init
|
||||
Injector injector = Guice.createInjector(new CommaFeedModule(hibernateBundle.getSessionFactory(), config, environment.metrics()));
|
||||
|
||||
// session management
|
||||
environment.servlets().setSessionHandler(config.getSessionHandlerFactory().build(config.getDataSourceFactory()));
|
||||
|
||||
// support for "@SecurityCheck User user" injection
|
||||
environment.jersey()
|
||||
.register(new SecurityCheckFactoryProvider.Binder(injector.getInstance(UserDAO.class),
|
||||
injector.getInstance(UserService.class), config));
|
||||
// support for "@Context SessionHelper sessionHelper" injection
|
||||
environment.jersey().register(new SessionHelperFactoryProvider.Binder());
|
||||
|
||||
// REST resources
|
||||
environment.jersey().setUrlPattern("/rest/*");
|
||||
environment.jersey().register(injector.getInstance(AdminREST.class));
|
||||
environment.jersey().register(injector.getInstance(CategoryREST.class));
|
||||
environment.jersey().register(injector.getInstance(EntryREST.class));
|
||||
environment.jersey().register(injector.getInstance(FeedREST.class));
|
||||
environment.jersey().register(injector.getInstance(ServerREST.class));
|
||||
environment.jersey().register(injector.getInstance(UserREST.class));
|
||||
environment.jersey().register(injector.getInstance(FeverREST.class));
|
||||
|
||||
// Servlets
|
||||
environment.servlets().addServlet("next", injector.getInstance(NextUnreadServlet.class)).addMapping("/next");
|
||||
environment.servlets().addServlet("logout", injector.getInstance(LogoutServlet.class)).addMapping("/logout");
|
||||
environment.servlets().addServlet("customCss", injector.getInstance(CustomCssServlet.class)).addMapping("/custom_css.css");
|
||||
environment.servlets().addServlet("customJs", injector.getInstance(CustomJsServlet.class)).addMapping("/custom_js.js");
|
||||
if (Boolean.TRUE.equals(config.getApplicationSettings().getHideFromWebCrawlers())) {
|
||||
environment.servlets()
|
||||
.addServlet("robots.txt", injector.getInstance(RobotsTxtDisallowAllServlet.class))
|
||||
.addMapping("/robots.txt");
|
||||
}
|
||||
|
||||
// WebSocket endpoint
|
||||
JakartaWebSocketServletContainerInitializer.configure(environment.getApplicationContext(), (context, container) -> {
|
||||
container.setDefaultMaxSessionIdleTimeout(config.getApplicationSettings().getWebsocketPingInterval().toMilliseconds() + 10000);
|
||||
|
||||
container.addEndpoint(ServerEndpointConfig.Builder.create(WebSocketEndpoint.class, "/ws")
|
||||
.configurator(injector.getInstance(WebSocketConfigurator.class))
|
||||
.build());
|
||||
});
|
||||
|
||||
// Scheduled tasks
|
||||
Set<ScheduledTask> tasks = injector.getInstance(Key.get(new TypeLiteral<>() {
|
||||
}));
|
||||
ScheduledExecutorService executor = environment.lifecycle()
|
||||
.scheduledExecutorService("task-scheduler", true)
|
||||
.threads(tasks.size())
|
||||
.build();
|
||||
for (ScheduledTask task : tasks) {
|
||||
task.register(executor);
|
||||
}
|
||||
|
||||
// database init/changelogs
|
||||
environment.lifecycle().manage(injector.getInstance(DatabaseStartupService.class));
|
||||
|
||||
// start feed fetching engine
|
||||
environment.lifecycle().manage(injector.getInstance(FeedRefreshEngine.class));
|
||||
|
||||
// prevent caching index.html, so that the webapp is always up to date
|
||||
environment.servlets()
|
||||
.addFilter("index-cache-busting-filter", new CacheBustingFilter())
|
||||
.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/");
|
||||
|
||||
// prevent caching openapi files, so that the documentation is always up to date
|
||||
environment.servlets()
|
||||
.addFilter("openapi-cache-busting-filter", new CacheBustingFilter())
|
||||
.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/openapi.json", "/openapi.yaml");
|
||||
|
||||
// prevent caching REST resources, except for favicons
|
||||
environment.servlets().addFilter("rest-cache-busting-filter", new CacheBustingFilter() {
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
|
||||
String path = ((HttpServletRequest) request).getRequestURI();
|
||||
if (path.contains("/feed/favicon")) {
|
||||
chain.doFilter(request, response);
|
||||
} else {
|
||||
super.doFilter(request, response, chain);
|
||||
}
|
||||
}
|
||||
}).addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/rest/*");
|
||||
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
new CommaFeedApplication().run(args);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,183 +1,321 @@
|
||||
package com.commafeed;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Properties;
|
||||
import java.util.Optional;
|
||||
|
||||
import com.commafeed.backend.cache.RedisPoolFactory;
|
||||
import com.commafeed.frontend.session.SessionHandlerFactory;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.commafeed.backend.feed.FeedRefreshIntervalCalculator;
|
||||
|
||||
import io.dropwizard.core.Configuration;
|
||||
import io.dropwizard.db.DataSourceFactory;
|
||||
import io.dropwizard.util.Duration;
|
||||
import jakarta.validation.Valid;
|
||||
import io.quarkus.runtime.annotations.ConfigDocSection;
|
||||
import io.quarkus.runtime.annotations.ConfigPhase;
|
||||
import io.quarkus.runtime.annotations.ConfigRoot;
|
||||
import io.quarkus.runtime.configuration.MemorySize;
|
||||
import io.smallrye.config.ConfigMapping;
|
||||
import io.smallrye.config.WithDefault;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class CommaFeedConfiguration extends Configuration {
|
||||
/**
|
||||
* CommaFeed configuration
|
||||
*
|
||||
* Default values are for production, they can be overridden in application.properties for other profiles
|
||||
*/
|
||||
@ConfigMapping(prefix = "commafeed")
|
||||
@ConfigRoot(phase = ConfigPhase.RUN_TIME)
|
||||
public interface CommaFeedConfiguration {
|
||||
/**
|
||||
* Whether to expose a robots.txt file that disallows web crawlers and search engine indexers.
|
||||
*/
|
||||
@WithDefault("true")
|
||||
boolean hideFromWebCrawlers();
|
||||
|
||||
public enum CacheType {
|
||||
NOOP, REDIS
|
||||
/**
|
||||
* If enabled, images in feed entries will be proxied through the server instead of accessed directly by the browser.
|
||||
*
|
||||
* This is useful if commafeed is accessed through a restricting proxy that blocks some feeds that are followed.
|
||||
*/
|
||||
@WithDefault("false")
|
||||
boolean imageProxyEnabled();
|
||||
|
||||
/**
|
||||
* Enable password recovery via email.
|
||||
*
|
||||
* Quarkus mailer will need to be configured.
|
||||
*/
|
||||
@WithDefault("false")
|
||||
boolean passwordRecoveryEnabled();
|
||||
|
||||
/**
|
||||
* Message displayed in a notification at the bottom of the page.
|
||||
*/
|
||||
Optional<String> announcement();
|
||||
|
||||
/**
|
||||
* Google Analytics tracking code.
|
||||
*/
|
||||
Optional<String> googleAnalyticsTrackingCode();
|
||||
|
||||
/**
|
||||
* Google Auth key for fetching Youtube channel favicons.
|
||||
*/
|
||||
Optional<String> googleAuthKey();
|
||||
|
||||
/**
|
||||
* HTTP client configuration
|
||||
*/
|
||||
@ConfigDocSection
|
||||
HttpClient httpClient();
|
||||
|
||||
/**
|
||||
* Feed refresh engine settings.
|
||||
*/
|
||||
@ConfigDocSection
|
||||
FeedRefresh feedRefresh();
|
||||
|
||||
/**
|
||||
* Database settings.
|
||||
*/
|
||||
@ConfigDocSection
|
||||
Database database();
|
||||
|
||||
/**
|
||||
* Users settings.
|
||||
*/
|
||||
@ConfigDocSection
|
||||
Users users();
|
||||
|
||||
/**
|
||||
* Websocket settings.
|
||||
*/
|
||||
@ConfigDocSection
|
||||
Websocket websocket();
|
||||
|
||||
interface HttpClient {
|
||||
/**
|
||||
* User-Agent string that will be used by the http client, leave empty for the default one.
|
||||
*/
|
||||
Optional<String> userAgent();
|
||||
|
||||
/**
|
||||
* Time to wait for a connection to be established.
|
||||
*/
|
||||
@WithDefault("5s")
|
||||
Duration connectTimeout();
|
||||
|
||||
/**
|
||||
* Time to wait for SSL handshake to complete.
|
||||
*/
|
||||
@WithDefault("5s")
|
||||
Duration sslHandshakeTimeout();
|
||||
|
||||
/**
|
||||
* Time to wait between two packets before timeout.
|
||||
*/
|
||||
@WithDefault("10s")
|
||||
Duration socketTimeout();
|
||||
|
||||
/**
|
||||
* Time to wait for the full response to be received.
|
||||
*/
|
||||
@WithDefault("10s")
|
||||
Duration responseTimeout();
|
||||
|
||||
/**
|
||||
* Time to live for a connection in the pool.
|
||||
*/
|
||||
@WithDefault("30s")
|
||||
Duration connectionTimeToLive();
|
||||
|
||||
/**
|
||||
* Time between eviction runs for idle connections.
|
||||
*/
|
||||
@WithDefault("1m")
|
||||
Duration idleConnectionsEvictionInterval();
|
||||
|
||||
/**
|
||||
* If a feed is larger than this, it will be discarded to prevent memory issues while parsing the feed.
|
||||
*/
|
||||
@WithDefault("5M")
|
||||
MemorySize maxResponseSize();
|
||||
|
||||
/**
|
||||
* HTTP client cache configuration
|
||||
*/
|
||||
@ConfigDocSection
|
||||
HttpClientCache cache();
|
||||
}
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty("database")
|
||||
private final DataSourceFactory dataSourceFactory = new DataSourceFactory();
|
||||
interface HttpClientCache {
|
||||
/**
|
||||
* Whether to enable the cache. This cache is used to avoid spamming feeds in short bursts (e.g. when subscribing to a feed for the
|
||||
* first time or when clicking "fetch all my feeds now").
|
||||
*/
|
||||
@WithDefault("true")
|
||||
boolean enabled();
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty("redis")
|
||||
private final RedisPoolFactory redisPoolFactory = new RedisPoolFactory();
|
||||
/**
|
||||
* Maximum amount of memory the cache can use.
|
||||
*/
|
||||
@WithDefault("10M")
|
||||
MemorySize maximumMemorySize();
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty("session")
|
||||
private final SessionHandlerFactory sessionHandlerFactory = new SessionHandlerFactory();
|
||||
/**
|
||||
* Duration after which an entry is removed from the cache.
|
||||
*/
|
||||
@WithDefault("1m")
|
||||
Duration expiration();
|
||||
}
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty("app")
|
||||
private ApplicationSettings applicationSettings;
|
||||
interface FeedRefresh {
|
||||
/**
|
||||
* Amount of time CommaFeed will wait before refreshing the same feed.
|
||||
*/
|
||||
@WithDefault("5m")
|
||||
Duration interval();
|
||||
|
||||
private final String version;
|
||||
private final String gitCommit;
|
||||
/**
|
||||
* If true, CommaFeed will calculate the next refresh time based on the feed's average time between entries and the time since the
|
||||
* last entry was published. The interval will be somewhere between the default refresh interval and 24h.
|
||||
*
|
||||
* See {@link FeedRefreshIntervalCalculator} for details.
|
||||
*/
|
||||
@WithDefault("false")
|
||||
boolean intervalEmpirical();
|
||||
|
||||
public CommaFeedConfiguration() {
|
||||
Properties properties = new Properties();
|
||||
try (InputStream stream = getClass().getResourceAsStream("/git.properties")) {
|
||||
if (stream != null) {
|
||||
properties.load(stream);
|
||||
/**
|
||||
* Amount of http threads used to fetch feeds.
|
||||
*/
|
||||
@Min(1)
|
||||
@WithDefault("3")
|
||||
int httpThreads();
|
||||
|
||||
/**
|
||||
* Amount of threads used to insert new entries in the database.
|
||||
*/
|
||||
@Min(1)
|
||||
@WithDefault("1")
|
||||
int databaseThreads();
|
||||
|
||||
/**
|
||||
* Duration after which a user is considered inactive. Feeds for inactive users are not refreshed until they log in again.
|
||||
*
|
||||
* 0 to disable.
|
||||
*/
|
||||
@WithDefault("0")
|
||||
Duration userInactivityPeriod();
|
||||
|
||||
/**
|
||||
* Duration after which the evaluation of a filtering expresion to mark an entry as read is considered to have timed out.
|
||||
*/
|
||||
@WithDefault("500ms")
|
||||
Duration filteringExpressionEvaluationTimeout();
|
||||
|
||||
/**
|
||||
* Duration after which the "Fetch all my feeds now" action is available again after use to avoid spamming feeds.
|
||||
*/
|
||||
@WithDefault("0")
|
||||
Duration forceRefreshCooldownDuration();
|
||||
}
|
||||
|
||||
interface Database {
|
||||
/**
|
||||
* Timeout applied to all database queries.
|
||||
*
|
||||
* 0 to disable.
|
||||
*/
|
||||
@WithDefault("0")
|
||||
Duration queryTimeout();
|
||||
|
||||
/**
|
||||
* Database cleanup settings.
|
||||
*/
|
||||
@ConfigDocSection
|
||||
Cleanup cleanup();
|
||||
|
||||
interface Cleanup {
|
||||
/**
|
||||
* Maximum age of feed entries in the database. Older entries will be deleted.
|
||||
*
|
||||
* 0 to disable.
|
||||
*/
|
||||
@WithDefault("365d")
|
||||
Duration entriesMaxAge();
|
||||
|
||||
/**
|
||||
* Maximum age of feed entry statuses (read/unread) in the database. Older statuses will be deleted.
|
||||
*
|
||||
* 0 to disable.
|
||||
*/
|
||||
@WithDefault("0")
|
||||
Duration statusesMaxAge();
|
||||
|
||||
/**
|
||||
* Maximum number of entries per feed to keep in the database.
|
||||
*
|
||||
* 0 to disable.
|
||||
*/
|
||||
@WithDefault("500")
|
||||
int maxFeedCapacity();
|
||||
|
||||
/**
|
||||
* Limit the number of feeds a user can subscribe to.
|
||||
*
|
||||
* 0 to disable.
|
||||
*/
|
||||
@WithDefault("0")
|
||||
int maxFeedsPerUser();
|
||||
|
||||
/**
|
||||
* Rows to delete per query while cleaning up old entries.
|
||||
*/
|
||||
@Positive
|
||||
@WithDefault("100")
|
||||
int batchSize();
|
||||
|
||||
default Instant statusesInstantThreshold() {
|
||||
return statusesMaxAge().toMillis() > 0 ? Instant.now().minus(statusesMaxAge()) : null;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
this.version = properties.getProperty("git.build.version", "unknown");
|
||||
this.gitCommit = properties.getProperty("git.commit.id.abbrev", "unknown");
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public static class ApplicationSettings {
|
||||
@NotNull
|
||||
@NotBlank
|
||||
@Valid
|
||||
private String publicUrl;
|
||||
interface Users {
|
||||
/**
|
||||
* Whether to let users create accounts for themselves.
|
||||
*/
|
||||
@WithDefault("false")
|
||||
boolean allowRegistrations();
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
private Boolean hideFromWebCrawlers = true;
|
||||
/**
|
||||
* Whether to enable strict password validation (1 uppercase char, 1 lowercase char, 1 digit, 1 special char).
|
||||
*/
|
||||
@WithDefault("true")
|
||||
boolean strictPasswordPolicy();
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
private Boolean allowRegistrations;
|
||||
/**
|
||||
* Whether to create a demo account the first time the app starts.
|
||||
*/
|
||||
@WithDefault("false")
|
||||
boolean createDemoAccount();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
private Boolean strictPasswordPolicy = true;
|
||||
interface Websocket {
|
||||
/**
|
||||
* Enable websocket connection so the server can notify web clients that there are new entries for feeds.
|
||||
*/
|
||||
@WithDefault("true")
|
||||
boolean enabled();
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
private Boolean createDemoAccount;
|
||||
|
||||
private String googleAnalyticsTrackingCode;
|
||||
|
||||
private String googleAuthKey;
|
||||
|
||||
@NotNull
|
||||
@Min(1)
|
||||
@Valid
|
||||
private Integer backgroundThreads;
|
||||
|
||||
@NotNull
|
||||
@Min(1)
|
||||
@Valid
|
||||
private Integer databaseUpdateThreads;
|
||||
|
||||
@NotNull
|
||||
@Positive
|
||||
@Valid
|
||||
private Integer databaseCleanupBatchSize = 100;
|
||||
|
||||
private String smtpHost;
|
||||
private int smtpPort;
|
||||
private boolean smtpTls;
|
||||
private String smtpUserName;
|
||||
private String smtpPassword;
|
||||
private String smtpFromAddress;
|
||||
|
||||
private boolean graphiteEnabled;
|
||||
private String graphitePrefix;
|
||||
private String graphiteHost;
|
||||
private int graphitePort;
|
||||
private int graphiteInterval;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
private Boolean heavyLoad;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
private Boolean imageProxyEnabled;
|
||||
|
||||
@NotNull
|
||||
@Min(0)
|
||||
@Valid
|
||||
private Integer queryTimeout;
|
||||
|
||||
@NotNull
|
||||
@Min(0)
|
||||
@Valid
|
||||
private Integer keepStatusDays;
|
||||
|
||||
@NotNull
|
||||
@Min(0)
|
||||
@Valid
|
||||
private Integer maxFeedCapacity;
|
||||
|
||||
@NotNull
|
||||
@Min(0)
|
||||
@Valid
|
||||
private Integer maxEntriesAgeDays = 0;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
private Integer maxFeedsPerUser = 0;
|
||||
|
||||
@NotNull
|
||||
@Min(0)
|
||||
@Valid
|
||||
private Integer refreshIntervalMinutes;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
private CacheType cache;
|
||||
|
||||
@Valid
|
||||
private String announcement;
|
||||
|
||||
private String userAgent;
|
||||
|
||||
private Boolean websocketEnabled = true;
|
||||
|
||||
private Duration websocketPingInterval = Duration.minutes(15);
|
||||
|
||||
private Duration treeReloadInterval = Duration.seconds(30);
|
||||
|
||||
public Instant getUnreadThreshold() {
|
||||
return getKeepStatusDays() > 0 ? Instant.now().minus(getKeepStatusDays(), ChronoUnit.DAYS) : null;
|
||||
}
|
||||
/**
|
||||
* Interval at which the client will send a ping message on the websocket to keep the connection alive.
|
||||
*/
|
||||
@WithDefault("15m")
|
||||
Duration pingInterval();
|
||||
|
||||
/**
|
||||
* If the websocket connection is disabled or the connection is lost, the client will reload the feed tree at this interval.
|
||||
*/
|
||||
@WithDefault("30s")
|
||||
Duration treeReloadInterval();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
package com.commafeed;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.hibernate.SessionFactory;
|
||||
|
||||
import com.codahale.metrics.MetricFilter;
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.codahale.metrics.graphite.Graphite;
|
||||
import com.codahale.metrics.graphite.GraphiteReporter;
|
||||
import com.commafeed.CommaFeedConfiguration.ApplicationSettings;
|
||||
import com.commafeed.CommaFeedConfiguration.CacheType;
|
||||
import com.commafeed.backend.cache.CacheService;
|
||||
import com.commafeed.backend.cache.NoopCacheService;
|
||||
import com.commafeed.backend.cache.RedisCacheService;
|
||||
import com.commafeed.backend.favicon.AbstractFaviconFetcher;
|
||||
import com.commafeed.backend.favicon.DefaultFaviconFetcher;
|
||||
import com.commafeed.backend.favicon.FacebookFaviconFetcher;
|
||||
import com.commafeed.backend.favicon.YoutubeFaviconFetcher;
|
||||
import com.commafeed.backend.task.DemoAccountCleanupTask;
|
||||
import com.commafeed.backend.task.EntriesExceedingFeedCapacityCleanupTask;
|
||||
import com.commafeed.backend.task.OldEntriesCleanupTask;
|
||||
import com.commafeed.backend.task.OldStatusesCleanupTask;
|
||||
import com.commafeed.backend.task.OrphanedContentsCleanupTask;
|
||||
import com.commafeed.backend.task.OrphanedFeedsCleanupTask;
|
||||
import com.commafeed.backend.task.ScheduledTask;
|
||||
import com.commafeed.backend.urlprovider.FeedURLProvider;
|
||||
import com.commafeed.backend.urlprovider.InPageReferenceFeedURLProvider;
|
||||
import com.commafeed.backend.urlprovider.YoutubeFeedURLProvider;
|
||||
import com.google.inject.AbstractModule;
|
||||
import com.google.inject.Provides;
|
||||
import com.google.inject.multibindings.Multibinder;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class CommaFeedModule extends AbstractModule {
|
||||
|
||||
@Getter(onMethod = @__({ @Provides }))
|
||||
private final SessionFactory sessionFactory;
|
||||
|
||||
@Getter(onMethod = @__({ @Provides }))
|
||||
private final CommaFeedConfiguration config;
|
||||
|
||||
@Getter(onMethod = @__({ @Provides }))
|
||||
private final MetricRegistry metrics;
|
||||
|
||||
@Override
|
||||
protected void configure() {
|
||||
CacheService cacheService = config.getApplicationSettings().getCache() == CacheType.NOOP ? new NoopCacheService()
|
||||
: new RedisCacheService(config.getRedisPoolFactory().build());
|
||||
log.info("using cache {}", cacheService.getClass());
|
||||
bind(CacheService.class).toInstance(cacheService);
|
||||
|
||||
Multibinder<AbstractFaviconFetcher> faviconMultibinder = Multibinder.newSetBinder(binder(), AbstractFaviconFetcher.class);
|
||||
faviconMultibinder.addBinding().to(YoutubeFaviconFetcher.class);
|
||||
faviconMultibinder.addBinding().to(FacebookFaviconFetcher.class);
|
||||
faviconMultibinder.addBinding().to(DefaultFaviconFetcher.class);
|
||||
|
||||
Multibinder<FeedURLProvider> urlProviderMultibinder = Multibinder.newSetBinder(binder(), FeedURLProvider.class);
|
||||
urlProviderMultibinder.addBinding().to(InPageReferenceFeedURLProvider.class);
|
||||
urlProviderMultibinder.addBinding().to(YoutubeFeedURLProvider.class);
|
||||
|
||||
Multibinder<ScheduledTask> taskMultibinder = Multibinder.newSetBinder(binder(), ScheduledTask.class);
|
||||
taskMultibinder.addBinding().to(OldStatusesCleanupTask.class);
|
||||
taskMultibinder.addBinding().to(EntriesExceedingFeedCapacityCleanupTask.class);
|
||||
taskMultibinder.addBinding().to(OldEntriesCleanupTask.class);
|
||||
taskMultibinder.addBinding().to(OrphanedFeedsCleanupTask.class);
|
||||
taskMultibinder.addBinding().to(OrphanedContentsCleanupTask.class);
|
||||
taskMultibinder.addBinding().to(DemoAccountCleanupTask.class);
|
||||
|
||||
ApplicationSettings settings = config.getApplicationSettings();
|
||||
|
||||
if (settings.isGraphiteEnabled()) {
|
||||
final String graphitePrefix = settings.getGraphitePrefix();
|
||||
final String graphiteHost = settings.getGraphiteHost();
|
||||
final int graphitePort = settings.getGraphitePort();
|
||||
final int graphiteInterval = settings.getGraphiteInterval();
|
||||
|
||||
log.info("Graphite Metrics will be sent to host={}, port={}, prefix={}, interval={}sec", graphiteHost, graphitePort,
|
||||
graphitePrefix, graphiteInterval);
|
||||
|
||||
final Graphite graphite = new Graphite(new InetSocketAddress(graphiteHost, graphitePort));
|
||||
final GraphiteReporter reporter = GraphiteReporter.forRegistry(metrics)
|
||||
.prefixedWith(graphitePrefix)
|
||||
.convertRatesTo(TimeUnit.SECONDS)
|
||||
.convertDurationsTo(TimeUnit.MILLISECONDS)
|
||||
.filter(MetricFilter.ALL)
|
||||
.build(graphite);
|
||||
reporter.start(graphiteInterval, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.commafeed;
|
||||
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
|
||||
import jakarta.enterprise.inject.Produces;
|
||||
import jakarta.inject.Singleton;
|
||||
|
||||
@Singleton
|
||||
public class CommaFeedProducers {
|
||||
|
||||
@Produces
|
||||
@Singleton
|
||||
public MetricRegistry metricRegistry() {
|
||||
return new MetricRegistry();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user