forked from Archives/Athou_commafeed
Compare commits
235 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5da94a7ed0 | ||
|
|
dfb3006c47 | ||
|
|
e626f36c0a | ||
|
|
ff81749559 | ||
|
|
34db9baa7b | ||
|
|
541b5ef085 | ||
|
|
a974164ac8 | ||
|
|
9ca8358900 | ||
|
|
2bd7b46f11 | ||
|
|
eafe56a967 | ||
|
|
c18cd62d24 | ||
|
|
180385d6ab | ||
|
|
838eb8b725 | ||
|
|
b7cb3ee3f7 | ||
|
|
0a97e3f8f0 | ||
|
|
0229292b48 | ||
|
|
c87a965ae1 | ||
|
|
baa4122793 | ||
|
|
e9a4cb3432 | ||
|
|
30fc2cb8a4 | ||
|
|
25ccece76c | ||
|
|
2cb9d2285a | ||
|
|
0bb922fb99 | ||
|
|
1c59ec5857 | ||
|
|
f4e97f6350 | ||
|
|
fb355187ee | ||
|
|
89eb4d0535 | ||
|
|
11fe2f9db8 | ||
|
|
86acc3850a | ||
|
|
77bf97c6d6 | ||
|
|
1a96579292 | ||
|
|
caccd3802c | ||
|
|
dce899186b | ||
|
|
5626b39ffa | ||
|
|
5386c99c6b | ||
|
|
c27fae140f | ||
|
|
b8b67132f4 | ||
|
|
5623039084 | ||
|
|
39ddb256de | ||
|
|
71801718dc | ||
|
|
b728e28081 | ||
|
|
bf2de7aecd | ||
|
|
010fb2dccb | ||
|
|
8517c0f4eb | ||
|
|
09737b4d4c | ||
|
|
88e9a2c2e1 | ||
|
|
0d7300c192 | ||
|
|
cb1a00c5cd | ||
|
|
07a07006cc | ||
|
|
bae7f94f8c | ||
|
|
b0832c5917 | ||
|
|
f72e70cb56 | ||
|
|
8cc24e054f | ||
|
|
48e42228b1 | ||
|
|
46c1af65f0 | ||
|
|
2989407d16 | ||
|
|
2401e36486 | ||
|
|
4ee396e667 | ||
|
|
08180dd373 | ||
|
|
561513b7ed | ||
|
|
9cd7053a90 | ||
|
|
72d510bd47 | ||
|
|
1085d6aa7a | ||
|
|
9e0ef9461f | ||
|
|
650acb62d5 | ||
|
|
ff1c8a1eff | ||
|
|
62a4ac46a0 | ||
|
|
fafd4c9d54 | ||
|
|
73b472bc8a | ||
|
|
1c3be67f76 | ||
|
|
2a5988b3e7 | ||
|
|
c5757849f3 | ||
|
|
b6107c3330 | ||
|
|
3efeed6c85 | ||
|
|
be44b0aad1 | ||
|
|
36152dc47f | ||
|
|
32e9cd3e35 | ||
|
|
4bf8b5696d | ||
|
|
0bf44dbc7b | ||
|
|
bda3ba4b5c | ||
|
|
23cff9c1e9 | ||
|
|
9691517335 | ||
|
|
b38bd8c312 | ||
|
|
d8ca58389d | ||
|
|
20a0cd7192 | ||
|
|
9b895328be | ||
|
|
cd39ab5f95 | ||
|
|
a7152a97a6 | ||
|
|
3e6e0a0f00 | ||
|
|
2936dd0d32 | ||
|
|
38a838d210 | ||
|
|
0136fa883d | ||
|
|
b0890df2f3 | ||
|
|
91acad0dbf | ||
|
|
14e7d70106 | ||
|
|
1cc76ba3ee | ||
|
|
206800c091 | ||
|
|
33749c94e3 | ||
|
|
8bce887e4c | ||
|
|
ca4f73fff6 | ||
|
|
26443310c9 | ||
|
|
870593bae8 | ||
|
|
cfd5d0faab | ||
|
|
9391c05968 | ||
|
|
a13c75981b | ||
|
|
a05baf63c1 | ||
|
|
32ce265cff | ||
|
|
b2ad24e7f6 | ||
|
|
fe626ebbe3 | ||
|
|
4431a898a0 | ||
|
|
89bfcfa240 | ||
|
|
d046d26f4e | ||
|
|
26b634b1a3 | ||
|
|
3ca18bbd36 | ||
|
|
7645731fff | ||
|
|
3c116dbabe | ||
|
|
3026fd116c | ||
|
|
63fa725a13 | ||
|
|
ede4e07ff3 | ||
|
|
de6dfbe8b2 | ||
|
|
164a57bef5 | ||
|
|
fd82b8aaee | ||
|
|
facf8b43f2 | ||
|
|
3184dfe178 | ||
|
|
cc584fd8c8 | ||
|
|
0f8fa1f2e1 | ||
|
|
d93f7bd20e | ||
|
|
96aa06d2dd | ||
|
|
fdaff46008 | ||
|
|
71066cd768 | ||
|
|
b9610a9058 | ||
|
|
ca027d5a4d | ||
|
|
1dea51c705 | ||
|
|
8edc89f3cc | ||
|
|
4cbf32cbb8 | ||
|
|
a5dc551b6b | ||
|
|
b1e0dbd0b3 | ||
|
|
58789b15a3 | ||
|
|
c5f58a2fe9 | ||
|
|
253ba5f18b | ||
|
|
ae859178c0 | ||
|
|
942dc0befe | ||
|
|
66c4510fd3 | ||
|
|
feb7de504c | ||
|
|
ec4b809ff9 | ||
|
|
b8d6a5742b | ||
|
|
31c42403a1 | ||
|
|
ead97be3cf | ||
|
|
3ee43f75d6 | ||
|
|
f77b91540d | ||
|
|
34915c93b8 | ||
|
|
365044d205 | ||
|
|
46f84ab29e | ||
|
|
2041823f0d | ||
|
|
ff01d7a87c | ||
|
|
4d905b118a | ||
|
|
6d74b50751 | ||
|
|
f12bdf5841 | ||
|
|
6b89e211d8 | ||
|
|
1b00e5613a | ||
|
|
050a8b24fc | ||
|
|
c5b1ea486c | ||
|
|
7b8c0ac6ff | ||
|
|
d43039cf9f | ||
|
|
3adc043740 | ||
|
|
08b95ff3dd | ||
|
|
e043ce71c3 | ||
|
|
b345319f68 | ||
|
|
4c298df9c9 | ||
|
|
067e01660a | ||
|
|
c649a04891 | ||
|
|
9b9a4f98f4 | ||
|
|
f8b6f2f237 | ||
|
|
371ce0d160 | ||
|
|
a92a7217ff | ||
|
|
e69c230678 | ||
|
|
b82077d3ca | ||
|
|
c624955ea4 | ||
|
|
9354fb8e18 | ||
|
|
664ed317a0 | ||
|
|
5bf121782b | ||
|
|
66c361e6a6 | ||
|
|
0946c0248e | ||
|
|
a8be8f2edf | ||
|
|
99db85328b | ||
|
|
5f29838bd2 | ||
|
|
7d2c0e7576 | ||
|
|
b8211e69e9 | ||
|
|
d7b2c5a6e3 | ||
|
|
18358d5991 | ||
|
|
e9b4895b0f | ||
|
|
c4fbf98200 | ||
|
|
b0aa6ae524 | ||
|
|
11dd151a3b | ||
|
|
874e7dcee6 | ||
|
|
8297edaf71 | ||
|
|
9e4e629a1a | ||
|
|
8b86617f18 | ||
|
|
bbda35f868 | ||
|
|
df68405fef | ||
|
|
65194d948f | ||
|
|
d49297216c | ||
|
|
e3e50f8456 | ||
|
|
e90b3730ef | ||
|
|
7675a24eb6 | ||
|
|
2bf9186135 | ||
|
|
d4ea51c145 | ||
|
|
6e0e99694e | ||
|
|
9ede8d1c46 | ||
|
|
fd0425a2be | ||
|
|
2b976cadeb | ||
|
|
023c27a565 | ||
|
|
69c9988404 | ||
|
|
b1a4debb95 | ||
|
|
5663d619aa | ||
|
|
2ef9e8d274 | ||
|
|
1292018de0 | ||
|
|
039e91414e | ||
|
|
662d0f754f | ||
|
|
7fb7efbdf7 | ||
|
|
a841c80261 | ||
|
|
da4143fa13 | ||
|
|
789857b09f | ||
|
|
ed45746f52 | ||
|
|
deb51f2ccc | ||
|
|
5fec4a4c5f | ||
|
|
7b335e2fd4 | ||
|
|
60b6c69020 | ||
|
|
08ab32c4c2 | ||
|
|
ff24fe4c7c | ||
|
|
50c62fb468 | ||
|
|
201331afc3 | ||
|
|
cf3100081e | ||
|
|
860aab7495 | ||
|
|
b084c8d108 |
24
.github/dependabot.yml
vendored
Normal file
24
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "maven"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 10
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/commafeed-client"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 10
|
||||
groups:
|
||||
mantine:
|
||||
patterns:
|
||||
- "@mantine/*"
|
||||
lingui:
|
||||
patterns:
|
||||
- "@lingui/*"
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 10
|
||||
18
.github/workflows/build.yml
vendored
18
.github/workflows/build.yml
vendored
@@ -11,19 +11,19 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# Setup
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v3
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: ${{ matrix.java }}
|
||||
distribution: "temurin"
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
run: mvn --batch-mode --update-snapshots verify
|
||||
|
||||
- name: Upload JAR
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: ${{ matrix.java == '17' }}
|
||||
with:
|
||||
name: commafeed.jar
|
||||
@@ -42,14 +42,14 @@ jobs:
|
||||
|
||||
# Docker
|
||||
- name: Login to Container Registry
|
||||
uses: docker/login-action@v2
|
||||
if: ${{ matrix.java == '17' }}
|
||||
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@v4
|
||||
uses: docker/build-push-action@v5
|
||||
if: ${{ matrix.java == '17' && github.ref_type == 'tag' }}
|
||||
with:
|
||||
context: .
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
athou/commafeed:${{ github.ref_name }}
|
||||
|
||||
- name: Docker build and push master
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v5
|
||||
if: ${{ matrix.java == '17' && github.ref_name == 'master' }}
|
||||
with:
|
||||
context: .
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -35,5 +35,8 @@ src/main/app/lib
|
||||
# Sublime
|
||||
*.sublime*
|
||||
|
||||
# VSCode
|
||||
.vscode
|
||||
|
||||
# Macs
|
||||
*.DS_Store
|
||||
|
||||
61
CHANGELOG.md
61
CHANGELOG.md
@@ -1,5 +1,61 @@
|
||||
# Changelog
|
||||
|
||||
## [4.3.3]
|
||||
|
||||
- fix OPML import (#1279)
|
||||
|
||||
## [4.3.2]
|
||||
|
||||
- added support for unix sockets (#1278)
|
||||
|
||||
## [4.3.1]
|
||||
|
||||
- fix an issue that prevents new feeds from being added when mysql/mariadb is used as the database and the database
|
||||
timezone is not UTC (#1239)
|
||||
- videos in enclosures can no longer have a width larger than the page (#1240)
|
||||
|
||||
## [4.3.0]
|
||||
|
||||
- h2 (the embedded database) has been upgraded to 2.2.224
|
||||
- this version uses a different file format than 2.1.x, the first time you start CommaFeed with this version, the
|
||||
database will be automatically converted to the new format
|
||||
- add a setting to completely disable scrolling to selected entry (#1157)
|
||||
- add a css class reflecting the current view mode to ease custom css rules (#1232)
|
||||
- fix an issue that prevents new feeds from being added when mysql/mariadb is used as the database (#1239)
|
||||
|
||||
## [4.2.1]
|
||||
|
||||
- fix an issue that caused the tree to show an incorrect unread count after a websocket notification because entries
|
||||
that were already marked as read by a filtering expression were not ignored (#1191)
|
||||
|
||||
## [4.2.0]
|
||||
|
||||
- add a setting to display the action buttons in the footer instead of in the header on mobile (#1121)
|
||||
- the websocket notification now contains everything needed to update the UI, the client no longer needs to make an API
|
||||
call to get the latest data when receiving the notification
|
||||
- add a workaround to the Fever API for the Unread iOS app (#1188)
|
||||
- fix an issue that caused dates to be saved incorrectly if the database server and the application server were in
|
||||
different timezones (#1187)
|
||||
|
||||
## [4.1.0]
|
||||
|
||||
- it is now possible to open the sidebar on mobile by swiping to the right (#1098)
|
||||
- swiping to mark entries as read/unread changed from swiping right to left because swiping right now opens the sidebar
|
||||
- the full hierarchy of categories are now displayed in the category dropdown (#1045)
|
||||
- added a setting `maxEntriesAgeDays` to delete old entries based on their age during database cleanup.
|
||||
The setting is disabled by default for existing installations, except for the docker image where it is enabled and set
|
||||
to 365 days
|
||||
- if user registrations are disabled on your instance which is the default behavior, users are redirected on the login
|
||||
page instead of the welcome page when not logged in (#1185)
|
||||
- the sidebar resizer is no longer shown in the middle of the screen on mobile
|
||||
- when using the system color scheme and the system is using a dark theme, feed entries no longer flicker on load
|
||||
- the demo account (if enabled) cannot register custom javascript code anymore
|
||||
- removed the usage of `toSorted` in the client because older browsers do not support it (#1183)
|
||||
- the openapi documentation is no longer cached by the browser so you always have access to the latest version
|
||||
- added a memory management section to the readme, reading it is recommended if you are running CommaFeed on a server
|
||||
with limited memory
|
||||
- fixed an issue that caused users without an email address set to be unable to edit their profile (#1184)
|
||||
|
||||
## [4.0.0]
|
||||
|
||||
- migrated from dropwizard 2 to dropwizard 4, Java 17+ is now required
|
||||
@@ -11,14 +67,13 @@
|
||||
- custom JS code is now executed when the app is done loading instead of when the page is loaded
|
||||
- the favicon is now correctly returned for feeds that return an invalid content type
|
||||
- the feed refresh engine now uses httpclient5 with connection pooling and no longer creates a new client for each
|
||||
request,
|
||||
reducing CPU usage
|
||||
request, reducing CPU usage
|
||||
- updated UI library Mantine to 7.0, improving performance
|
||||
- the h2 embedded database is now compacted on shutdown to reclaim unused space
|
||||
- the admin connector on port 8084 is now disabled in config.yml.example. Disabling it in your config.yml is
|
||||
recommended (see https://github.com/Athou/commafeed/commit/929df60f09cce56020b0962ab111cd8349b271b0)
|
||||
- migrated documentation from swagger 2 to openapi 3
|
||||
- added a GET method to the fever api to indicate that the endpoint is working correctly when accesed from a browser
|
||||
- added a GET method to the fever api to indicate that the endpoint is working correctly when accessed from a browser
|
||||
- the websocket connection can now be disabled, the websocket ping interval and the tree reload interval can now be
|
||||
configured (see config.yml.example)
|
||||
- the websocket connection now works correctly when the context root of the application is not "/"
|
||||
|
||||
39
README.md
39
README.md
@@ -54,6 +54,29 @@ user is `admin` and the default password is `admin`.
|
||||
The server will listen on http://localhost:8082. The default
|
||||
user is `admin` and the default password is `admin`.
|
||||
|
||||
### Memory management
|
||||
|
||||
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.
|
||||
However, this can be problematic on systems with limited memory.
|
||||
|
||||
#### Hard limit
|
||||
|
||||
The JVM can be configured to use a maximum amount of memory with the `-Xmx` parameter.
|
||||
For example, to limit the JVM to 256MB of memory, use `-Xmx256m`.
|
||||
|
||||
#### Dynamic sizing
|
||||
|
||||
The JVM can be configured to release unused memory to the operating system with the following parameters:
|
||||
|
||||
-Xms20m -XX:+UseG1GC -XX:-ShrinkHeapInSteps -XX:G1PeriodicGCInterval=10000 -XX:-G1PeriodicGCInvokesConcurrent -XX:MinHeapFreeRatio=5 -XX:MaxHeapFreeRatio=10
|
||||
|
||||
This is how the Docker image is configured.
|
||||
See [here](https://docs.oracle.com/en/java/javase/17/gctuning/garbage-first-g1-garbage-collector1.html)
|
||||
and [here](https://docs.oracle.com/en/java/javase/17/gctuning/factors-affecting-garbage-collection-performance.html) for
|
||||
more
|
||||
information.
|
||||
|
||||
## Translation
|
||||
|
||||
Files for internationalization are
|
||||
@@ -86,19 +109,3 @@ two-letters [ISO-639-1 language code](http://en.wikipedia.org/wiki/List_of_ISO_6
|
||||
|
||||
The frontend server is now running at http://localhost:8082 and is proxying REST requests to the backend running on
|
||||
port 8083
|
||||
|
||||
## Copyright and license
|
||||
|
||||
Copyright 2013-2023 CommaFeed.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this work except in compliance with the License.
|
||||
You may obtain a copy of the License in the LICENSE file, or at:
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
@@ -1,41 +1,47 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
browser: true,
|
||||
es2021: true
|
||||
es2021: true,
|
||||
},
|
||||
extends: ["standard-with-typescript", "plugin:react/recommended", "plugin:react-hooks/recommended", "plugin:prettier/recommended"],
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"standard",
|
||||
"plugin:@typescript-eslint/strict-type-checked",
|
||||
"plugin:@typescript-eslint/stylistic-type-checked",
|
||||
"plugin:react/recommended",
|
||||
"plugin:react-hooks/recommended",
|
||||
"plugin:prettier/recommended",
|
||||
],
|
||||
settings: {
|
||||
react: {
|
||||
version: "detect"
|
||||
}
|
||||
version: "detect",
|
||||
},
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
env: {
|
||||
node: true
|
||||
node: true,
|
||||
},
|
||||
files: [".eslintrc.{js,cjs}"],
|
||||
parserOptions: {
|
||||
sourceType: "script"
|
||||
}
|
||||
}
|
||||
sourceType: "script",
|
||||
},
|
||||
},
|
||||
],
|
||||
parserOptions: {
|
||||
project: true,
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module"
|
||||
sourceType: "module",
|
||||
},
|
||||
plugins: ["react"],
|
||||
rules: {
|
||||
"@typescript-eslint/consistent-type-assertions": ["error", { assertionStyle: "as" }],
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/no-confusing-void-expression": ["error", { ignoreArrowShorthand: true }],
|
||||
"@typescript-eslint/no-floating-promises": "off",
|
||||
"@typescript-eslint/no-misused-promises": "off",
|
||||
"@typescript-eslint/prefer-nullish-coalescing": ["error", { ignoreConditionalTests: true }],
|
||||
"@typescript-eslint/strict-boolean-expressions": "off",
|
||||
"@typescript-eslint/unbound-method": "off",
|
||||
"react/no-unescaped-entities": "off",
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"react-hooks/exhaustive-deps": "error"
|
||||
}
|
||||
"react-hooks/exhaustive-deps": "error",
|
||||
},
|
||||
}
|
||||
|
||||
2294
commafeed-client/package-lock.json
generated
2294
commafeed-client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -14,24 +14,24 @@
|
||||
"i18n:extract": "lingui extract --clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.11.3",
|
||||
"@fontsource/open-sans": "^5.0.20",
|
||||
"@lingui/core": "^4.6.0",
|
||||
"@lingui/macro": "^4.6.0",
|
||||
"@lingui/react": "^4.6.0",
|
||||
"@mantine/core": "^7.3.2",
|
||||
"@mantine/form": "^7.3.2",
|
||||
"@mantine/hooks": "^7.3.2",
|
||||
"@mantine/modals": "^7.3.2",
|
||||
"@mantine/notifications": "^7.3.2",
|
||||
"@mantine/spotlight": "^7.3.2",
|
||||
"@emotion/react": "^11.11.4",
|
||||
"@fontsource/open-sans": "^5.0.25",
|
||||
"@lingui/core": "^4.7.1",
|
||||
"@lingui/macro": "^4.7.1",
|
||||
"@lingui/react": "^4.7.1",
|
||||
"@mantine/core": "^7.6.1",
|
||||
"@mantine/form": "^7.6.1",
|
||||
"@mantine/hooks": "^7.6.1",
|
||||
"@mantine/modals": "^7.6.1",
|
||||
"@mantine/notifications": "^7.6.1",
|
||||
"@mantine/spotlight": "^7.6.1",
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@reduxjs/toolkit": "^2.0.1",
|
||||
"axios": "^1.6.3",
|
||||
"@reduxjs/toolkit": "^2.2.1",
|
||||
"axios": "^1.6.7",
|
||||
"dayjs": "^1.11.10",
|
||||
"escape-string-regexp": "^5.0.0",
|
||||
"interweave": "^13.1.0",
|
||||
"monaco-editor": "^0.45.0",
|
||||
"monaco-editor": "^0.46.0",
|
||||
"mousetrap": "^1.6.5",
|
||||
"react": "^18.2.0",
|
||||
"react-async-hook": "^4.0.0",
|
||||
@@ -39,47 +39,44 @@
|
||||
"react-dom": "^18.2.0",
|
||||
"react-draggable": "^4.4.6",
|
||||
"react-ga4": "^2.1.0",
|
||||
"react-icons": "^4.12.0",
|
||||
"react-icons": "^5.0.1",
|
||||
"react-infinite-scroller": "^1.2.6",
|
||||
"react-redux": "^9.0.4",
|
||||
"react-router-dom": "^6.21.1",
|
||||
"react-redux": "^9.1.0",
|
||||
"react-router-dom": "^6.22.2",
|
||||
"react-swipeable": "^7.0.1",
|
||||
"redoc": "^2.1.3",
|
||||
"throttle-debounce": "^5.0.0",
|
||||
"tinycon": "^0.6.8",
|
||||
"tss-react": "^4.9.3",
|
||||
"tss-react": "^4.9.4",
|
||||
"use-local-storage": "^3.0.0",
|
||||
"websocket-heartbeat-js": "^1.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lingui/cli": "^4.6.0",
|
||||
"@lingui/vite-plugin": "^4.6.0",
|
||||
"@lingui/cli": "^4.7.1",
|
||||
"@lingui/vite-plugin": "^4.7.1",
|
||||
"@types/mousetrap": "^1.6.15",
|
||||
"@types/react": "^18.2.46",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/react": "^18.2.61",
|
||||
"@types/react-dom": "^18.2.19",
|
||||
"@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",
|
||||
"@typescript-eslint/eslint-plugin": "^6.16.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.1.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"babel-plugin-macros": "^3.1.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-config-standard-with-typescript": "^43.0.0",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-n": "^16.5.0",
|
||||
"eslint-plugin-prettier": "^5.1.2",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-config-standard": "^17.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-react": "^7.34.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"prettier": "^3.1.1",
|
||||
"prettier": "^3.2.5",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^4.5.1",
|
||||
"vite": "^5.1.4",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
"vite-tsconfig-paths": "^4.2.3",
|
||||
"vitest": "^0.34.6",
|
||||
"vite-tsconfig-paths": "^4.3.1",
|
||||
"vitest": "^1.3.1",
|
||||
"vitest-mock-extended": "^1.3.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<groupId>com.commafeed</groupId>
|
||||
<artifactId>commafeed</artifactId>
|
||||
<version>4.0.0</version>
|
||||
<version>4.3.3</version>
|
||||
</parent>
|
||||
<artifactId>commafeed-client</artifactId>
|
||||
<name>CommaFeed Client</name>
|
||||
@@ -39,16 +39,6 @@
|
||||
<arguments>ci</arguments>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>npm run eslint</id>
|
||||
<goals>
|
||||
<goal>npm</goal>
|
||||
</goals>
|
||||
<phase>compile</phase>
|
||||
<configuration>
|
||||
<arguments>run eslint</arguments>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>npm run test</id>
|
||||
<goals>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import axios from "axios"
|
||||
import axios, { AxiosError } from "axios"
|
||||
import {
|
||||
type AddCategoryRequest,
|
||||
type AdminSaveUserRequest,
|
||||
AuthenticationError,
|
||||
type Category,
|
||||
type CategoryModificationRequest,
|
||||
type CollapseRequest,
|
||||
@@ -31,16 +32,18 @@ const axiosInstance = axios.create({ baseURL: "./rest", withCredentials: true })
|
||||
axiosInstance.interceptors.response.use(
|
||||
response => response,
|
||||
error => {
|
||||
if (
|
||||
(error.response.status === 401 && error.response.data === "Credentials are required to access this resource.") ||
|
||||
(error.response.status === 403 && error.response.data === "You don't have the required role to access this resource.")
|
||||
) {
|
||||
window.location.hash = "/welcome"
|
||||
if (isAuthenticationError(error)) {
|
||||
const data = error.response?.data
|
||||
window.location.hash = data?.allowRegistrations ? "/welcome" : "/login"
|
||||
}
|
||||
throw error
|
||||
}
|
||||
)
|
||||
|
||||
function isAuthenticationError(error: unknown): error is AxiosError<AuthenticationError> {
|
||||
return axios.isAxiosError(error) && !!error.response && [401, 403].includes(error.response.status)
|
||||
}
|
||||
|
||||
export const client = {
|
||||
category: {
|
||||
getRoot: async () => await axiosInstance.get<Category>("category/get"),
|
||||
@@ -106,14 +109,19 @@ export const client = {
|
||||
export const errorToStrings = (err: unknown) => {
|
||||
let strings: string[] = []
|
||||
|
||||
if (axios.isAxiosError(err)) {
|
||||
if (err.response) {
|
||||
const { data } = err.response
|
||||
if (typeof data === "string") strings.push(data)
|
||||
if (typeof data === "object" && data.message) strings.push(data.message as string)
|
||||
if (typeof data === "object" && data.errors) strings = [...strings, ...data.errors]
|
||||
}
|
||||
if (axios.isAxiosError(err) && err.response) {
|
||||
if (typeof err.response.data === "string") strings.push(err.response.data)
|
||||
if (isMessageError(err)) strings.push(err.response.data.message)
|
||||
if (isMessageArrayError(err)) strings = [...strings, ...err.response.data.errors]
|
||||
}
|
||||
|
||||
return strings
|
||||
}
|
||||
|
||||
function isMessageError(err: AxiosError): err is AxiosError<{ message: string }> {
|
||||
return !!err.response && !!err.response.data && typeof err.response.data === "object" && "message" in err.response.data
|
||||
}
|
||||
|
||||
function isMessageArrayError(err: AxiosError): err is AxiosError<{ errors: string[] }> {
|
||||
return !!err.response && !!err.response.data && typeof err.response.data === "object" && "errors" in err.response.data
|
||||
}
|
||||
|
||||
@@ -89,10 +89,18 @@ export const Constants = {
|
||||
mobileBreakpointName: "md",
|
||||
headerHeight: 60,
|
||||
entryMaxWidth: 650,
|
||||
isTopVisible: (div: HTMLElement) => div.getBoundingClientRect().top >= Constants.layout.headerHeight,
|
||||
isBottomVisible: (div: HTMLElement) => div.getBoundingClientRect().bottom <= window.innerHeight,
|
||||
isTopVisible: (div: HTMLElement) => {
|
||||
const header = document.getElementById(Constants.dom.headerId)?.getBoundingClientRect()
|
||||
return div.getBoundingClientRect().top >= (header?.bottom ?? 0)
|
||||
},
|
||||
isBottomVisible: (div: HTMLElement) => {
|
||||
const footer = document.getElementById(Constants.dom.footerId)?.getBoundingClientRect()
|
||||
return div.getBoundingClientRect().bottom <= (footer?.top ?? window.innerHeight)
|
||||
},
|
||||
},
|
||||
dom: {
|
||||
headerId: "header",
|
||||
footerId: "footer",
|
||||
entryId: (entry: Entry) => `entry-id-${entry.id}`,
|
||||
entryContextMenuId: (entry: Entry) => entry.id,
|
||||
},
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable import/first */
|
||||
import { configureStore } from "@reduxjs/toolkit"
|
||||
import { type client } from "app/client"
|
||||
import { loadEntries, loadMoreEntries, markAllEntries, markEntry } from "app/entries/thunks"
|
||||
@@ -90,7 +89,7 @@ describe("entries", () => {
|
||||
expect(store.getState().entries.hasMore).toBe(false)
|
||||
})
|
||||
|
||||
it("marks an entry as read", async () => {
|
||||
it("marks an entry as read", () => {
|
||||
const store = configureStore({
|
||||
reducer: reducers,
|
||||
preloadedState: {
|
||||
@@ -117,7 +116,7 @@ describe("entries", () => {
|
||||
expect(mockClient.entry.mark).toHaveBeenCalledWith({ id: "3", read: true })
|
||||
})
|
||||
|
||||
it("marks all entries as read", async () => {
|
||||
it("marks all entries as read", () => {
|
||||
const store = configureStore({
|
||||
reducer: reducers,
|
||||
preloadedState: {
|
||||
|
||||
@@ -46,11 +46,11 @@ const buildGetEntriesPaginatedRequest = (state: RootState, source: EntrySource,
|
||||
tag: source.type === "tag" ? source.id : undefined,
|
||||
keywords: state.entries.search,
|
||||
})
|
||||
export const reloadEntries = createAppAsyncThunk("entries/reload", async (arg, thunkApi) => {
|
||||
export const reloadEntries = createAppAsyncThunk("entries/reload", (arg, thunkApi) => {
|
||||
const state = thunkApi.getState()
|
||||
thunkApi.dispatch(loadEntries({ source: state.entries.source, clearSearch: false }))
|
||||
})
|
||||
export const search = createAppAsyncThunk("entries/search", async (arg: string, thunkApi) => {
|
||||
export const search = createAppAsyncThunk("entries/search", (arg: string, thunkApi) => {
|
||||
const state = thunkApi.getState()
|
||||
thunkApi.dispatch(setSearch(arg))
|
||||
thunkApi.dispatch(loadEntries({ source: state.entries.source, clearSearch: false }))
|
||||
@@ -84,7 +84,7 @@ export const markMultipleEntries = createAppAsyncThunk(
|
||||
thunkApi.dispatch(reloadTree())
|
||||
}
|
||||
)
|
||||
export const markEntriesUpToEntry = createAppAsyncThunk("entries/entry/upToEntry", async (arg: Entry, thunkApi) => {
|
||||
export const markEntriesUpToEntry = createAppAsyncThunk("entries/entry/upToEntry", (arg: Entry, thunkApi) => {
|
||||
const state = thunkApi.getState()
|
||||
const { entries } = state.entries
|
||||
|
||||
@@ -162,9 +162,9 @@ export const selectEntry = createAppAsyncThunk(
|
||||
if (arg.scrollToEntry) {
|
||||
const entryElement = document.getElementById(Constants.dom.entryId(entry))
|
||||
if (entryElement) {
|
||||
const alwaysScrollToEntry = state.user.settings?.alwaysScrollToEntry
|
||||
const scrollMode = state.user.settings?.scrollMode
|
||||
const entryEntirelyVisible = Constants.layout.isTopVisible(entryElement) && Constants.layout.isBottomVisible(entryElement)
|
||||
if (alwaysScrollToEntry || !entryEntirelyVisible) {
|
||||
if (scrollMode === "always" || (scrollMode === "if_needed" && !entryEntirelyVisible)) {
|
||||
const scrollSpeed = state.user.settings?.scrollSpeed
|
||||
thunkApi.dispatch(entriesSlice.actions.setScrollingToEntry(true))
|
||||
scrollToEntry(entryElement, scrollSpeed, () => thunkApi.dispatch(entriesSlice.actions.setScrollingToEntry(false)))
|
||||
@@ -174,10 +174,11 @@ export const selectEntry = createAppAsyncThunk(
|
||||
}
|
||||
)
|
||||
const scrollToEntry = (entryElement: HTMLElement, scrollSpeed: number | undefined, onScrollEnded: () => void) => {
|
||||
const header = document.getElementById(Constants.dom.headerId)?.getBoundingClientRect()
|
||||
const offset = (header?.bottom ?? 0) + 3
|
||||
scrollToWithCallback({
|
||||
options: {
|
||||
// add a small gap between the top of the content and the top of the page
|
||||
top: entryElement.offsetTop - Constants.layout.headerHeight - 3,
|
||||
top: entryElement.offsetTop - offset,
|
||||
behavior: scrollSpeed && scrollSpeed > 0 ? "smooth" : "auto",
|
||||
},
|
||||
onScrollEnded,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { configureStore } from "@reduxjs/toolkit"
|
||||
import { setupListeners } from "@reduxjs/toolkit/query"
|
||||
import { entriesSlice } from "app/entries/slice"
|
||||
import { redirectSlice } from "app/redirect/slice"
|
||||
import { serverSlice } from "app/server/slice"
|
||||
@@ -17,8 +16,6 @@ export const reducers = {
|
||||
|
||||
export const store = configureStore({ reducer: reducers })
|
||||
|
||||
setupListeners(store.dispatch)
|
||||
|
||||
export type RootState = ReturnType<typeof store.getState>
|
||||
export type AppDispatch = typeof store.dispatch
|
||||
|
||||
|
||||
@@ -26,6 +26,22 @@ export const treeSlice = createSlice({
|
||||
toggleSidebar: state => {
|
||||
state.sidebarVisible = !state.sidebarVisible
|
||||
},
|
||||
incrementUnreadCount: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
feedId: number
|
||||
amount: number
|
||||
}>
|
||||
) => {
|
||||
if (!state.rootCategory) return
|
||||
visitCategoryTree(state.rootCategory, c =>
|
||||
c.feeds
|
||||
.filter(f => f.id === action.payload.feedId)
|
||||
.forEach(f => {
|
||||
f.unread += action.payload.amount
|
||||
})
|
||||
)
|
||||
},
|
||||
},
|
||||
extraReducers: builder => {
|
||||
builder.addCase(reloadTree.fulfilled, (state, action) => {
|
||||
@@ -53,4 +69,4 @@ export const treeSlice = createSlice({
|
||||
},
|
||||
})
|
||||
|
||||
export const { setMobileMenuOpen, toggleSidebar } = treeSlice.actions
|
||||
export const { setMobileMenuOpen, toggleSidebar, incrementUnreadCount } = treeSlice.actions
|
||||
|
||||
@@ -1,8 +1,33 @@
|
||||
export type ReadingMode = "all" | "unread"
|
||||
|
||||
export type ReadingOrder = "asc" | "desc"
|
||||
|
||||
export type ViewMode = "title" | "cozy" | "detailed" | "expanded"
|
||||
|
||||
export type ScrollMode = "always" | "never" | "if_needed"
|
||||
|
||||
export interface AddCategoryRequest {
|
||||
name: string
|
||||
parentId?: string
|
||||
}
|
||||
|
||||
export interface Subscription {
|
||||
id: number
|
||||
name: string
|
||||
message?: string
|
||||
errorCount: number
|
||||
lastRefresh?: number
|
||||
nextRefresh?: number
|
||||
feedUrl: string
|
||||
feedLink: string
|
||||
iconUrl: string
|
||||
unread: number
|
||||
categoryId?: string
|
||||
position: number
|
||||
newestItemTime?: number
|
||||
filter?: string
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
id: string
|
||||
parentId?: string
|
||||
@@ -26,19 +51,6 @@ export interface CollapseRequest {
|
||||
collapse: boolean
|
||||
}
|
||||
|
||||
export interface Entries {
|
||||
name: string
|
||||
message?: string
|
||||
errorCount: number
|
||||
feedLink: string
|
||||
timestamp: number
|
||||
hasMore: boolean
|
||||
offset?: number
|
||||
limit?: number
|
||||
entries: Entry[]
|
||||
ignoredReadStatus: boolean
|
||||
}
|
||||
|
||||
export interface Entry {
|
||||
id: string
|
||||
guid: string
|
||||
@@ -67,6 +79,19 @@ export interface Entry {
|
||||
tags: string[]
|
||||
}
|
||||
|
||||
export interface Entries {
|
||||
name: string
|
||||
message?: string
|
||||
errorCount: number
|
||||
feedLink: string
|
||||
timestamp: number
|
||||
hasMore: boolean
|
||||
offset?: number
|
||||
limit?: number
|
||||
entries: Entry[]
|
||||
ignoredReadStatus: boolean
|
||||
}
|
||||
|
||||
export interface FeedInfo {
|
||||
url: string
|
||||
title: string
|
||||
@@ -196,21 +221,6 @@ export interface ServerInfo {
|
||||
treeReloadInterval: number
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
language: string
|
||||
readingMode: ReadingMode
|
||||
readingOrder: ReadingOrder
|
||||
showRead: boolean
|
||||
scrollMarks: boolean
|
||||
customCss?: string
|
||||
customJs?: string
|
||||
scrollSpeed: number
|
||||
alwaysScrollToEntry: boolean
|
||||
markAllAsReadConfirmation: boolean
|
||||
customContextMenu: boolean
|
||||
sharingSettings: SharingSettings
|
||||
}
|
||||
|
||||
export interface SharingSettings {
|
||||
email: boolean
|
||||
gmail: boolean
|
||||
@@ -222,6 +232,22 @@ export interface SharingSettings {
|
||||
buffer: boolean
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
language: string
|
||||
readingMode: ReadingMode
|
||||
readingOrder: ReadingOrder
|
||||
showRead: boolean
|
||||
scrollMarks: boolean
|
||||
customCss?: string
|
||||
customJs?: string
|
||||
scrollSpeed: number
|
||||
scrollMode: ScrollMode
|
||||
markAllAsReadConfirmation: boolean
|
||||
customContextMenu: boolean
|
||||
mobileFooter: boolean
|
||||
sharingSettings: SharingSettings
|
||||
}
|
||||
|
||||
export interface StarRequest {
|
||||
id: string
|
||||
feedId: number
|
||||
@@ -234,34 +260,11 @@ export interface SubscribeRequest {
|
||||
categoryId?: string
|
||||
}
|
||||
|
||||
export interface Subscription {
|
||||
id: number
|
||||
name: string
|
||||
message?: string
|
||||
errorCount: number
|
||||
lastRefresh?: number
|
||||
nextRefresh?: number
|
||||
feedUrl: string
|
||||
feedLink: string
|
||||
iconUrl: string
|
||||
unread: number
|
||||
categoryId?: string
|
||||
position: number
|
||||
newestItemTime?: number
|
||||
filter?: string
|
||||
}
|
||||
|
||||
export interface TagRequest {
|
||||
entryId: number
|
||||
tags: string[]
|
||||
}
|
||||
|
||||
export interface UnreadCount {
|
||||
feedId?: number
|
||||
unreadCount?: number
|
||||
newestItemTime?: number
|
||||
}
|
||||
|
||||
export interface UserModel {
|
||||
id: number
|
||||
name: string
|
||||
@@ -283,8 +286,7 @@ export interface AdminSaveUserRequest {
|
||||
admin: boolean
|
||||
}
|
||||
|
||||
export type ReadingMode = "all" | "unread"
|
||||
|
||||
export type ReadingOrder = "asc" | "desc"
|
||||
|
||||
export type ViewMode = "title" | "cozy" | "detailed" | "expanded"
|
||||
export interface AuthenticationError {
|
||||
message: string
|
||||
allowRegistrations: boolean
|
||||
}
|
||||
|
||||
@@ -3,13 +3,14 @@ import { showNotification } from "@mantine/notifications"
|
||||
import { createSlice, isAnyOf } from "@reduxjs/toolkit"
|
||||
import { type Settings, type UserModel } from "app/types"
|
||||
import {
|
||||
changeAlwaysScrollToEntry,
|
||||
changeCustomContextMenu,
|
||||
changeLanguage,
|
||||
changeMarkAllAsReadConfirmation,
|
||||
changeMobileFooter,
|
||||
changeReadingMode,
|
||||
changeReadingOrder,
|
||||
changeScrollMarks,
|
||||
changeScrollMode,
|
||||
changeScrollSpeed,
|
||||
changeSharingSetting,
|
||||
changeShowRead,
|
||||
@@ -64,9 +65,9 @@ export const userSlice = createSlice({
|
||||
if (!state.settings) return
|
||||
state.settings.scrollMarks = action.meta.arg
|
||||
})
|
||||
builder.addCase(changeAlwaysScrollToEntry.pending, (state, action) => {
|
||||
builder.addCase(changeScrollMode.pending, (state, action) => {
|
||||
if (!state.settings) return
|
||||
state.settings.alwaysScrollToEntry = action.meta.arg
|
||||
state.settings.scrollMode = action.meta.arg
|
||||
})
|
||||
builder.addCase(changeMarkAllAsReadConfirmation.pending, (state, action) => {
|
||||
if (!state.settings) return
|
||||
@@ -76,6 +77,10 @@ export const userSlice = createSlice({
|
||||
if (!state.settings) return
|
||||
state.settings.customContextMenu = action.meta.arg
|
||||
})
|
||||
builder.addCase(changeMobileFooter.pending, (state, action) => {
|
||||
if (!state.settings) return
|
||||
state.settings.mobileFooter = action.meta.arg
|
||||
})
|
||||
builder.addCase(changeSharingSetting.pending, (state, action) => {
|
||||
if (!state.settings) return
|
||||
state.settings.sharingSettings[action.meta.arg.site] = action.meta.arg.value
|
||||
@@ -86,9 +91,10 @@ export const userSlice = createSlice({
|
||||
changeScrollSpeed.fulfilled,
|
||||
changeShowRead.fulfilled,
|
||||
changeScrollMarks.fulfilled,
|
||||
changeAlwaysScrollToEntry.fulfilled,
|
||||
changeScrollMode.fulfilled,
|
||||
changeMarkAllAsReadConfirmation.fulfilled,
|
||||
changeCustomContextMenu.fulfilled,
|
||||
changeMobileFooter.fulfilled,
|
||||
changeSharingSetting.fulfilled
|
||||
),
|
||||
() => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createAppAsyncThunk } from "app/async-thunk"
|
||||
import { client } from "app/client"
|
||||
import { reloadEntries } from "app/entries/thunks"
|
||||
import type { ReadingMode, ReadingOrder, SharingSettings } from "app/types"
|
||||
import type { ReadingMode, ReadingOrder, ScrollMode, SharingSettings } from "app/types"
|
||||
|
||||
export const reloadSettings = createAppAsyncThunk("settings/reload", async () => await client.user.getSettings().then(r => r.data))
|
||||
export const reloadProfile = createAppAsyncThunk("profile/reload", async () => await client.user.getProfile().then(r => r.data))
|
||||
@@ -38,10 +38,10 @@ export const changeScrollMarks = createAppAsyncThunk("settings/scrollMarks", (sc
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, scrollMarks })
|
||||
})
|
||||
export const changeAlwaysScrollToEntry = createAppAsyncThunk("settings/alwaysScrollToEntry", (alwaysScrollToEntry: boolean, thunkApi) => {
|
||||
export const changeScrollMode = createAppAsyncThunk("settings/scrollMode", (scrollMode: ScrollMode, thunkApi) => {
|
||||
const { settings } = thunkApi.getState().user
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, alwaysScrollToEntry })
|
||||
client.user.saveSettings({ ...settings, scrollMode })
|
||||
})
|
||||
export const changeMarkAllAsReadConfirmation = createAppAsyncThunk(
|
||||
"settings/markAllAsReadConfirmation",
|
||||
@@ -56,6 +56,11 @@ export const changeCustomContextMenu = createAppAsyncThunk("settings/customConte
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, customContextMenu })
|
||||
})
|
||||
export const changeMobileFooter = createAppAsyncThunk("settings/mobileFooter", (mobileFooter: boolean, thunkApi) => {
|
||||
const { settings } = thunkApi.getState().user
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, mobileFooter })
|
||||
})
|
||||
export const changeSharingSetting = createAppAsyncThunk(
|
||||
"settings/sharingSetting",
|
||||
(
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Box, Center, type MantineTheme, useMantineTheme } from "@mantine/core"
|
||||
import { useColorScheme } from "hooks/useColorScheme"
|
||||
import { Box, Center } from "@mantine/core"
|
||||
import { useState } from "react"
|
||||
import { TbPhoto } from "react-icons/tb"
|
||||
import { tss } from "tss"
|
||||
@@ -18,8 +17,6 @@ interface ImageWithPlaceholderWhileLoadingProps {
|
||||
|
||||
const useStyles = tss
|
||||
.withParams<{
|
||||
theme: MantineTheme
|
||||
colorScheme: "light" | "dark"
|
||||
placeholderWidth?: number
|
||||
placeholderHeight?: number
|
||||
placeholderBackgroundColor?: string
|
||||
@@ -46,11 +43,7 @@ export function ImageWithPlaceholderWhileLoading({
|
||||
title,
|
||||
width,
|
||||
}: ImageWithPlaceholderWhileLoadingProps) {
|
||||
const theme = useMantineTheme()
|
||||
const colorScheme = useColorScheme()
|
||||
const { classes } = useStyles({
|
||||
theme,
|
||||
colorScheme,
|
||||
placeholderWidth,
|
||||
placeholderHeight,
|
||||
placeholderBackgroundColor,
|
||||
|
||||
@@ -110,7 +110,7 @@ export function KeyboardShortcutsHelp() {
|
||||
<Table.Td>
|
||||
<Kbd>M</Kbd>
|
||||
<span>, </span>
|
||||
<Trans>Swipe header to the right</Trans>
|
||||
<Trans>Swipe header to the left</Trans>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
<Table.Tr>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { type ReactNode } from "react"
|
||||
interface CodeEditorProps {
|
||||
description?: ReactNode
|
||||
language: "css" | "javascript"
|
||||
value: string
|
||||
value?: string
|
||||
onChange: (value: string | undefined) => void
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ const init = async () => {
|
||||
interface RichCodeEditorProps {
|
||||
height: number | string
|
||||
language: "css" | "javascript"
|
||||
value: string
|
||||
value?: string
|
||||
onChange: (value: string | undefined) => void
|
||||
}
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ class HighlightMatcher extends Matcher {
|
||||
return this.doMatch(string, new RegExp(pattern, "i"), () => ({}))
|
||||
}
|
||||
|
||||
replaceWith(children: ChildrenNode, props: unknown): Node {
|
||||
replaceWith(children: ChildrenNode): Node {
|
||||
return <Mark>{children}</Mark>
|
||||
}
|
||||
|
||||
|
||||
@@ -2,14 +2,14 @@ import { BasicHtmlStyles } from "components/content/BasicHtmlStyles"
|
||||
import { ImageWithPlaceholderWhileLoading } from "components/ImageWithPlaceholderWhileLoading"
|
||||
|
||||
export function Enclosure(props: { enclosureType: string; enclosureUrl: string }) {
|
||||
const hasVideo = props.enclosureType?.startsWith("video")
|
||||
const hasAudio = props.enclosureType?.startsWith("audio")
|
||||
const hasImage = props.enclosureType?.startsWith("image")
|
||||
const hasVideo = props.enclosureType.startsWith("video")
|
||||
const hasAudio = props.enclosureType.startsWith("audio")
|
||||
const hasImage = props.enclosureType.startsWith("image")
|
||||
|
||||
return (
|
||||
<BasicHtmlStyles>
|
||||
{hasVideo && (
|
||||
<video controls>
|
||||
<video controls width="100%">
|
||||
<source src={props.enclosureUrl} type={props.enclosureType} />
|
||||
</video>
|
||||
)}
|
||||
|
||||
@@ -91,7 +91,7 @@ export function FeedEntries() {
|
||||
)
|
||||
}
|
||||
|
||||
const swipedRight = async (entry: ExpendableEntry) => await dispatch(markEntry({ entry, read: !entry.read }))
|
||||
const swipedLeft = async (entry: ExpendableEntry) => await dispatch(markEntry({ entry, read: !entry.read }))
|
||||
|
||||
// close context menu on scroll
|
||||
useEffect(() => {
|
||||
@@ -295,10 +295,10 @@ export function FeedEntries() {
|
||||
})
|
||||
)
|
||||
|
||||
if (!entries) return <Loader />
|
||||
return (
|
||||
<InfiniteScroll
|
||||
id="entries"
|
||||
className={`view-mode-${viewMode}`}
|
||||
initialLoad={false}
|
||||
loadMore={async () => await (!loading && dispatch(loadMoreEntries()))}
|
||||
hasMore={hasMore}
|
||||
@@ -320,7 +320,7 @@ export function FeedEntries() {
|
||||
onHeaderClick={event => headerClicked(entry, event)}
|
||||
onHeaderRightClick={event => headerRightClicked(entry, event)}
|
||||
onBodyClick={() => bodyClicked(entry)}
|
||||
onSwipedRight={async () => await swipedRight(entry)}
|
||||
onSwipedLeft={async () => await swipedLeft(entry)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Box, Divider, type MantineRadius, type MantineSpacing, type MantineTheme, Paper, useMantineTheme } from "@mantine/core"
|
||||
import { Box, Divider, type MantineRadius, type MantineSpacing, Paper } from "@mantine/core"
|
||||
import { Constants } from "app/constants"
|
||||
import { type Entry, type ViewMode } from "app/types"
|
||||
import { useColorScheme } from "hooks/useColorScheme"
|
||||
import { useViewMode } from "hooks/useViewMode"
|
||||
import React from "react"
|
||||
import { useSwipeable } from "react-swipeable"
|
||||
@@ -21,13 +20,11 @@ interface FeedEntryProps {
|
||||
onHeaderClick: (e: React.MouseEvent) => void
|
||||
onHeaderRightClick: (e: React.MouseEvent) => void
|
||||
onBodyClick: (e: React.MouseEvent) => void
|
||||
onSwipedRight: () => void
|
||||
onSwipedLeft: () => void
|
||||
}
|
||||
|
||||
const useStyles = tss
|
||||
.withParams<{
|
||||
theme: MantineTheme
|
||||
colorScheme: "light" | "dark"
|
||||
read: boolean
|
||||
expanded: boolean
|
||||
viewMode: ViewMode
|
||||
@@ -96,12 +93,8 @@ const useStyles = tss
|
||||
})
|
||||
|
||||
export function FeedEntry(props: FeedEntryProps) {
|
||||
const theme = useMantineTheme()
|
||||
const colorScheme = useColorScheme()
|
||||
const { viewMode } = useViewMode()
|
||||
const { classes, cx } = useStyles({
|
||||
theme,
|
||||
colorScheme,
|
||||
read: props.entry.read,
|
||||
expanded: props.expanded,
|
||||
viewMode,
|
||||
@@ -111,7 +104,7 @@ export function FeedEntry(props: FeedEntryProps) {
|
||||
})
|
||||
|
||||
const swipeHandlers = useSwipeable({
|
||||
onSwipedRight: props.onSwipedRight,
|
||||
onSwipedLeft: props.onSwipedLeft,
|
||||
})
|
||||
|
||||
let paddingX: MantineSpacing = "xs"
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Box, Text } from "@mantine/core"
|
||||
import { type Entry } from "app/types"
|
||||
import { RelativeDate } from "components/RelativeDate"
|
||||
import { OnDesktop } from "components/responsive/OnDesktop"
|
||||
import { useColorScheme } from "hooks/useColorScheme"
|
||||
import { tss } from "tss"
|
||||
import { FeedEntryTitle } from "./FeedEntryTitle"
|
||||
import { FeedFavicon } from "./FeedFavicon"
|
||||
@@ -13,7 +12,6 @@ export interface FeedEntryHeaderProps {
|
||||
|
||||
const useStyles = tss
|
||||
.withParams<{
|
||||
colorScheme: "light" | "dark"
|
||||
read: boolean
|
||||
}>()
|
||||
.create(({ colorScheme, read }) => ({
|
||||
@@ -42,9 +40,7 @@ const useStyles = tss
|
||||
}))
|
||||
|
||||
export function FeedEntryCompactHeader(props: FeedEntryHeaderProps) {
|
||||
const colorScheme = useColorScheme()
|
||||
const { classes } = useStyles({
|
||||
colorScheme,
|
||||
read: props.entry.read,
|
||||
})
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Trans } from "@lingui/macro"
|
||||
import { Group, type MantineTheme, useMantineTheme } from "@mantine/core"
|
||||
import { Group } from "@mantine/core"
|
||||
import { Constants } from "app/constants"
|
||||
import { markEntriesUpToEntry, markEntry, starEntry } from "app/entries/thunks"
|
||||
import { redirectToFeed } from "app/redirect/thunks"
|
||||
@@ -17,28 +17,19 @@ interface FeedEntryContextMenuProps {
|
||||
}
|
||||
|
||||
const iconSize = 16
|
||||
const useStyles = tss
|
||||
.withParams<{
|
||||
theme: MantineTheme
|
||||
colorScheme: "light" | "dark"
|
||||
}>()
|
||||
.create(({ theme, colorScheme }) => ({
|
||||
menu: {
|
||||
// apply mantine theme from MenuItem.styles.ts
|
||||
fontSize: theme.fontSizes.sm,
|
||||
"--contexify-item-color": `${colorScheme === "dark" ? theme.colors.dark[0] : theme.black} !important`,
|
||||
"--contexify-activeItem-color": `${colorScheme === "dark" ? theme.colors.dark[0] : theme.black} !important`,
|
||||
"--contexify-activeItem-bgColor": `${colorScheme === "dark" ? theme.colors.dark[4] : theme.colors.gray[1]} !important`,
|
||||
},
|
||||
}))
|
||||
const useStyles = tss.create(({ theme, colorScheme }) => ({
|
||||
menu: {
|
||||
// apply mantine theme from MenuItem.styles.ts
|
||||
fontSize: theme.fontSizes.sm,
|
||||
"--contexify-item-color": `${colorScheme === "dark" ? theme.colors.dark[0] : theme.black} !important`,
|
||||
"--contexify-activeItem-color": `${colorScheme === "dark" ? theme.colors.dark[0] : theme.black} !important`,
|
||||
"--contexify-activeItem-bgColor": `${colorScheme === "dark" ? theme.colors.dark[4] : theme.colors.gray[1]} !important`,
|
||||
},
|
||||
}))
|
||||
|
||||
export function FeedEntryContextMenu(props: FeedEntryContextMenuProps) {
|
||||
const theme = useMantineTheme()
|
||||
const colorScheme = useColorScheme()
|
||||
const { classes } = useStyles({
|
||||
theme,
|
||||
colorScheme,
|
||||
})
|
||||
const { classes } = useStyles()
|
||||
const sourceType = useAppSelector(state => state.entries.source.type)
|
||||
const dispatch = useAppDispatch()
|
||||
const { openLinkInBackgroundTab } = useBrowserExtension()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Box, Space, Text } from "@mantine/core"
|
||||
import { type Entry } from "app/types"
|
||||
import { RelativeDate } from "components/RelativeDate"
|
||||
import { useColorScheme } from "hooks/useColorScheme"
|
||||
import { tss } from "tss"
|
||||
import { FeedEntryTitle } from "./FeedEntryTitle"
|
||||
import { FeedFavicon } from "./FeedFavicon"
|
||||
@@ -13,7 +12,6 @@ export interface FeedEntryHeaderProps {
|
||||
|
||||
const useStyles = tss
|
||||
.withParams<{
|
||||
colorScheme: "light" | "dark"
|
||||
read: boolean
|
||||
}>()
|
||||
.create(({ colorScheme, read }) => ({
|
||||
@@ -28,9 +26,7 @@ const useStyles = tss
|
||||
}))
|
||||
|
||||
export function FeedEntryHeader(props: FeedEntryHeaderProps) {
|
||||
const colorScheme = useColorScheme()
|
||||
const { classes } = useStyles({
|
||||
colorScheme,
|
||||
read: props.entry.read,
|
||||
})
|
||||
return (
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { ActionIcon, Box, type MantineTheme, SimpleGrid, useMantineTheme } from "@mantine/core"
|
||||
import { ActionIcon, Box, SimpleGrid } from "@mantine/core"
|
||||
import { Constants } from "app/constants"
|
||||
import { useAppSelector } from "app/store"
|
||||
import { type SharingSettings } from "app/types"
|
||||
import { useColorScheme } from "hooks/useColorScheme"
|
||||
import { type IconType } from "react-icons"
|
||||
import { tss } from "tss"
|
||||
|
||||
@@ -10,8 +9,6 @@ type Color = `#${string}`
|
||||
|
||||
const useStyles = tss
|
||||
.withParams<{
|
||||
theme: MantineTheme
|
||||
colorScheme: "light" | "dark"
|
||||
color: Color
|
||||
}>()
|
||||
.create(({ theme, colorScheme, color }) => ({
|
||||
@@ -23,11 +20,7 @@ const useStyles = tss
|
||||
}))
|
||||
|
||||
function ShareButton({ url, icon, color }: { url: string; icon: IconType; color: Color }) {
|
||||
const theme = useMantineTheme()
|
||||
const colorScheme = useColorScheme()
|
||||
const { classes } = useStyles({
|
||||
theme,
|
||||
colorScheme,
|
||||
color,
|
||||
})
|
||||
|
||||
@@ -54,7 +47,7 @@ export function ShareButtons(props: { url: string; description: string }) {
|
||||
|
||||
return (
|
||||
<SimpleGrid cols={4}>
|
||||
{(Object.keys(Constants.sharing) as Array<keyof SharingSettings>)
|
||||
{(Object.keys(Constants.sharing) as (keyof SharingSettings)[])
|
||||
.filter(site => sharingSettings?.[site])
|
||||
.map(site => (
|
||||
<ShareButton
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Select, type SelectProps } from "@mantine/core"
|
||||
import { type ComboboxItem } from "@mantine/core/lib/components/Combobox/Combobox.types"
|
||||
import { Constants } from "app/constants"
|
||||
import { useAppSelector } from "app/store"
|
||||
import { type Category } from "app/types"
|
||||
import { flattenCategoryTree } from "app/utils"
|
||||
|
||||
type CategorySelectProps = Partial<SelectProps> & {
|
||||
@@ -13,14 +14,32 @@ type CategorySelectProps = Partial<SelectProps> & {
|
||||
export function CategorySelect(props: CategorySelectProps) {
|
||||
const rootCategory = useAppSelector(state => state.tree.rootCategory)
|
||||
const categories = rootCategory && flattenCategoryTree(rootCategory)
|
||||
const categoriesById = categories?.reduce((map, c) => {
|
||||
map.set(c.id, c)
|
||||
return map
|
||||
}, new Map<string, Category>())
|
||||
const categoryLabel = (cat: Category) => {
|
||||
let label = cat.name
|
||||
|
||||
while (cat.parentId) {
|
||||
const parent = categoriesById?.get(cat.parentId)
|
||||
if (!parent) {
|
||||
break
|
||||
}
|
||||
label = `${parent.name} → ${label}`
|
||||
cat = parent
|
||||
}
|
||||
|
||||
return label
|
||||
}
|
||||
const selectData: ComboboxItem[] | undefined = categories
|
||||
?.filter(c => c.id !== Constants.categories.all.id)
|
||||
.filter(c => !props.withoutCategoryIds?.includes(c.id))
|
||||
.sort((c1, c2) => c1.name.localeCompare(c2.name))
|
||||
.map(c => ({
|
||||
label: c.parentName ? t`${c.name} (in ${c.parentName})` : c.name,
|
||||
label: categoryLabel(c),
|
||||
value: c.id,
|
||||
}))
|
||||
.sort((c1, c2) => c1.label.localeCompare(c2.label))
|
||||
if (props.withAll) {
|
||||
selectData?.unshift({
|
||||
label: t`All`,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { t, Trans } from "@lingui/macro"
|
||||
import { Box, Button, FileInput, Group, Stack } from "@mantine/core"
|
||||
import { useForm } from "@mantine/form"
|
||||
import { isNotEmpty, useForm } from "@mantine/form"
|
||||
import { client, errorToStrings } from "app/client"
|
||||
import { redirectToSelectedSource } from "app/redirect/thunks"
|
||||
import { useAppDispatch } from "app/store"
|
||||
@@ -14,7 +14,7 @@ export function ImportOpml() {
|
||||
|
||||
const form = useForm<{ file: File }>({
|
||||
validate: {
|
||||
file: v => (v ? null : t`file is required`),
|
||||
file: isNotEmpty(t`OPML file is required`),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -38,8 +38,7 @@ export function ImportOpml() {
|
||||
<FileInput
|
||||
label={<Trans>OPML file</Trans>}
|
||||
leftSection={<TbFileImport />}
|
||||
// https://github.com/mantinedev/mantine/issues/5401
|
||||
{...{ placeholder: t`OPML file` }}
|
||||
placeholder={t`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,31 +1,40 @@
|
||||
import { Trans } from "@lingui/macro"
|
||||
import { Divider, Select, SimpleGrid, Stack, Switch } from "@mantine/core"
|
||||
import { Divider, Group, Radio, Select, SimpleGrid, Stack, Switch } from "@mantine/core"
|
||||
import { Constants } from "app/constants"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import { type SharingSettings } from "app/types"
|
||||
import { type ScrollMode, type SharingSettings } from "app/types"
|
||||
import {
|
||||
changeAlwaysScrollToEntry,
|
||||
changeCustomContextMenu,
|
||||
changeLanguage,
|
||||
changeMarkAllAsReadConfirmation,
|
||||
changeMobileFooter,
|
||||
changeScrollMarks,
|
||||
changeScrollMode,
|
||||
changeScrollSpeed,
|
||||
changeSharingSetting,
|
||||
changeShowRead,
|
||||
} from "app/user/thunks"
|
||||
import { locales } from "i18n"
|
||||
import { type ReactNode } from "react"
|
||||
|
||||
export function DisplaySettings() {
|
||||
const language = useAppSelector(state => state.user.settings?.language)
|
||||
const scrollSpeed = useAppSelector(state => state.user.settings?.scrollSpeed)
|
||||
const showRead = useAppSelector(state => state.user.settings?.showRead)
|
||||
const scrollMarks = useAppSelector(state => state.user.settings?.scrollMarks)
|
||||
const alwaysScrollToEntry = useAppSelector(state => state.user.settings?.alwaysScrollToEntry)
|
||||
const scrollMode = useAppSelector(state => state.user.settings?.scrollMode)
|
||||
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 sharingSettings = useAppSelector(state => state.user.settings?.sharingSettings)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const scrollModeOptions: Record<ScrollMode, ReactNode> = {
|
||||
always: <Trans>Always</Trans>,
|
||||
never: <Trans>Never</Trans>,
|
||||
if_needed: <Trans>If the entry doesn't entirely fit on the screen</Trans>,
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Select
|
||||
@@ -38,30 +47,12 @@ export function DisplaySettings() {
|
||||
onChange={async s => await (s && dispatch(changeLanguage(s)))}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label={<Trans>Scroll smoothly when navigating between entries</Trans>}
|
||||
checked={scrollSpeed ? scrollSpeed > 0 : false}
|
||||
onChange={async e => await dispatch(changeScrollSpeed(e.currentTarget.checked))}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label={<Trans>Always scroll selected entry to the top of the page, even if it fits entirely on screen</Trans>}
|
||||
checked={alwaysScrollToEntry}
|
||||
onChange={async e => await dispatch(changeAlwaysScrollToEntry(e.currentTarget.checked))}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label={<Trans>Show feeds and categories with no unread entries</Trans>}
|
||||
checked={showRead}
|
||||
onChange={async e => await dispatch(changeShowRead(e.currentTarget.checked))}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label={<Trans>In expanded view, scrolling through entries mark them as read</Trans>}
|
||||
checked={scrollMarks}
|
||||
onChange={async e => await dispatch(changeScrollMarks(e.currentTarget.checked))}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label={<Trans>Show confirmation when marking all entries as read</Trans>}
|
||||
checked={markAllAsReadConfirmation}
|
||||
@@ -74,10 +65,42 @@ export function DisplaySettings() {
|
||||
onChange={async e => await dispatch(changeCustomContextMenu(e.currentTarget.checked))}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label={<Trans>On mobile, show action buttons at the bottom of the screen</Trans>}
|
||||
checked={mobileFooter}
|
||||
onChange={async e => await dispatch(changeMobileFooter(e.currentTarget.checked))}
|
||||
/>
|
||||
|
||||
<Divider label={<Trans>Scrolling</Trans>} labelPosition="center" />
|
||||
|
||||
<Radio.Group
|
||||
label={<Trans>Scroll selected entry to the top of the page</Trans>}
|
||||
value={scrollMode}
|
||||
onChange={async value => await dispatch(changeScrollMode(value as ScrollMode))}
|
||||
>
|
||||
<Group mt="xs">
|
||||
{Object.entries(scrollModeOptions).map(e => (
|
||||
<Radio key={e[0]} value={e[0]} label={e[1]} />
|
||||
))}
|
||||
</Group>
|
||||
</Radio.Group>
|
||||
|
||||
<Switch
|
||||
label={<Trans>Scroll smoothly when navigating between entries</Trans>}
|
||||
checked={scrollSpeed ? scrollSpeed > 0 : false}
|
||||
onChange={async e => await dispatch(changeScrollSpeed(e.currentTarget.checked))}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label={<Trans>In expanded view, scrolling through entries mark them as read</Trans>}
|
||||
checked={scrollMarks}
|
||||
onChange={async e => await dispatch(changeScrollMarks(e.currentTarget.checked))}
|
||||
/>
|
||||
|
||||
<Divider label={<Trans>Sharing sites</Trans>} labelPosition="center" />
|
||||
|
||||
<SimpleGrid cols={2}>
|
||||
{(Object.keys(Constants.sharing) as Array<keyof SharingSettings>).map(site => (
|
||||
{(Object.keys(Constants.sharing) as (keyof SharingSettings)[]).map(site => (
|
||||
<Switch
|
||||
key={site}
|
||||
label={Constants.sharing[site].label}
|
||||
|
||||
@@ -78,7 +78,17 @@ export function ProfileSettings() {
|
||||
<form onSubmit={form.onSubmit(saveProfile.execute)}>
|
||||
<Stack>
|
||||
<TextInput label={<Trans>User name</Trans>} readOnly value={profile?.name} />
|
||||
<TextInput label={<Trans>API key</Trans>} readOnly value={profile?.apiKey} />
|
||||
<TextInput
|
||||
label={<Trans>API key</Trans>}
|
||||
description={
|
||||
<Trans>
|
||||
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
|
||||
</Trans>
|
||||
}
|
||||
readOnly
|
||||
value={profile?.apiKey}
|
||||
/>
|
||||
|
||||
<Input.Wrapper
|
||||
label={<Trans>OPML export</Trans>}
|
||||
@@ -100,7 +110,7 @@ export function ProfileSettings() {
|
||||
description={
|
||||
<Trans>
|
||||
CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client.
|
||||
The username is your user name and the password is your API key.
|
||||
Login with your username and your <u>API key</u>.
|
||||
</Trans>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Box, Center, type MantineTheme, useMantineTheme } from "@mantine/core"
|
||||
import { Box, Center } from "@mantine/core"
|
||||
import { FeedFavicon } from "components/content/FeedFavicon"
|
||||
import { useColorScheme } from "hooks/useColorScheme"
|
||||
import React, { type ReactNode } from "react"
|
||||
import { tss } from "tss"
|
||||
import { UnreadCount } from "./UnreadCount"
|
||||
@@ -20,8 +19,6 @@ interface TreeNodeProps {
|
||||
|
||||
const useStyles = tss
|
||||
.withParams<{
|
||||
theme: MantineTheme
|
||||
colorScheme: "dark" | "light"
|
||||
selected: boolean
|
||||
hasError: boolean
|
||||
hasUnread: boolean
|
||||
@@ -60,11 +57,7 @@ const useStyles = tss
|
||||
})
|
||||
|
||||
export function TreeNode(props: TreeNodeProps) {
|
||||
const theme = useMantineTheme()
|
||||
const colorScheme = useColorScheme()
|
||||
const { classes } = useStyles({
|
||||
theme,
|
||||
colorScheme,
|
||||
selected: props.selected,
|
||||
hasError: props.hasError,
|
||||
hasUnread: props.unread > 0,
|
||||
|
||||
@@ -16,13 +16,13 @@ export function TreeSearch(props: TreeSearchProps) {
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const actions: SpotlightActionData[] = props.feeds
|
||||
.toSorted((f1, f2) => f1.name.localeCompare(f2.name))
|
||||
.map(f => ({
|
||||
id: `${f.id}`,
|
||||
label: f.name,
|
||||
leftSection: <FeedFavicon url={f.iconUrl} />,
|
||||
onClick: async () => await dispatch(redirectToFeed(f.id)),
|
||||
}))
|
||||
.sort((f1, f2) => f1.label.localeCompare(f2.label))
|
||||
|
||||
const searchIcon = <TbSearch size={18} />
|
||||
const rightSection = (
|
||||
|
||||
@@ -1,4 +1,20 @@
|
||||
import { useComputedColorScheme } from "@mantine/core"
|
||||
|
||||
// the color scheme to use to render components
|
||||
export const useColorScheme = () => useComputedColorScheme("light")
|
||||
import { useMantineColorScheme } from "@mantine/core"
|
||||
import { useMediaQuery } from "@mantine/hooks"
|
||||
|
||||
export const useColorScheme = () => {
|
||||
const systemColorScheme = useMediaQuery(
|
||||
"(prefers-color-scheme: dark)",
|
||||
// passing undefined will use window.matchMedia(query) as default value
|
||||
undefined,
|
||||
{
|
||||
// get initial value synchronously and not in useEffect to avoid flash of light theme
|
||||
getInitialValueInEffect: false,
|
||||
}
|
||||
)
|
||||
? "dark"
|
||||
: "light"
|
||||
|
||||
const { colorScheme } = useMantineColorScheme()
|
||||
return colorScheme === "auto" ? systemColorScheme : colorScheme
|
||||
}
|
||||
|
||||
@@ -1,9 +1,22 @@
|
||||
import { setWebSocketConnected } from "app/server/slice"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import { reloadTree } from "app/tree/thunks"
|
||||
import { type AppDispatch, useAppDispatch, useAppSelector } from "app/store"
|
||||
import { incrementUnreadCount } from "app/tree/slice"
|
||||
import { useEffect } from "react"
|
||||
import WebsocketHeartbeatJs from "websocket-heartbeat-js"
|
||||
|
||||
const handleMessage = (dispatch: AppDispatch, message: string) => {
|
||||
const parts = message.split(":")
|
||||
const type = parts[0]
|
||||
if (type === "new-feed-entries") {
|
||||
dispatch(
|
||||
incrementUnreadCount({
|
||||
feedId: +parts[1],
|
||||
amount: +parts[2],
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const useWebSocket = () => {
|
||||
const websocketEnabled = useAppSelector(state => state.server.serverInfos?.websocketEnabled)
|
||||
const websocketPingInterval = useAppSelector(state => state.server.serverInfos?.websocketPingInterval)
|
||||
@@ -25,9 +38,8 @@ export const useWebSocket = () => {
|
||||
ws.onopen = () => dispatch(setWebSocketConnected(true))
|
||||
ws.onclose = () => dispatch(setWebSocketConnected(false))
|
||||
ws.onmessage = event => {
|
||||
const { data } = event
|
||||
if (typeof data === "string") {
|
||||
if (data.startsWith("new-feed-entries:")) dispatch(reloadTree())
|
||||
if (typeof event.data === "string") {
|
||||
handleMessage(dispatch, event.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,8 +44,8 @@ export const locales: Locale[] = [
|
||||
|
||||
function activateLocale(locale: string) {
|
||||
// lingui
|
||||
import(`./locales/${locale}/messages.po`).then(data => {
|
||||
i18n.load(locale, data.messages as Messages)
|
||||
import(`./locales/${locale}/messages.po`).then((data: { messages: Messages }) => {
|
||||
i18n.load(locale, data.messages)
|
||||
i18n.activate(locale)
|
||||
})
|
||||
|
||||
|
||||
@@ -13,10 +13,6 @@ msgstr ""
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
msgid "{0} (in {1})"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr ""
|
||||
@@ -72,7 +68,7 @@ msgid "All"
|
||||
msgstr "الكل"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgid "Always"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
@@ -184,7 +180,7 @@ msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
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 ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
@@ -411,6 +407,10 @@ msgstr "المرجع نفسه"
|
||||
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 "إذا لم يكن فارغًا ، فسيتم تقييم التعبير إلى \"صواب\" أو \"خطأ\". "
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
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."
|
||||
@@ -545,6 +545,10 @@ msgstr "الاسم"
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "انتقل إلى اشتراك بإدخال اسمه"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "New password"
|
||||
msgstr "كلمة مرور جديدة"
|
||||
@@ -578,6 +582,10 @@ msgstr "لم يتم العثور على شيء"
|
||||
msgid "Oldest first"
|
||||
msgstr "الأقدم أولا"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "اوووه!"
|
||||
@@ -706,10 +714,18 @@ msgstr ""
|
||||
msgid "Save"
|
||||
msgstr "حفظ"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll selected entry to the top of the page"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll smoothly when navigating between entries"
|
||||
msgstr "قم بالتمرير بسلاسة عند التنقل بين الإدخالات"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
@@ -823,7 +839,7 @@ msgid "Success"
|
||||
msgstr "النجاح"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Swipe header to the right"
|
||||
msgid "Swipe header to the left"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/WelcomePage.tsx
|
||||
@@ -851,6 +867,10 @@ msgstr "عنوان URL للتغذية التي تريد الاشتراك فيه
|
||||
msgid "Theme"
|
||||
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 ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "تبديل قراءة حالة الإدخال الحالي"
|
||||
|
||||
@@ -13,17 +13,13 @@ msgstr ""
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
msgid "{0} (in {1})"
|
||||
msgstr ""
|
||||
|
||||
#: 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 és un projecte de codi obert. El codi font està allotjat a </0><1>GitHub</1>."
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1>."
|
||||
msgstr ""
|
||||
msgstr "<0>La sintaxi completa està disponible </0><1>aquí</1>."
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "<0>Have an account?</0><1>Log in!</1>"
|
||||
@@ -31,7 +27,7 @@ msgstr "<0>Teniu un compte?</0><1>Inicieu la sessió!</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>Ei,</0><1> sóc la Jérémie de Bèlgica i fa més de 10 anys que treballo a CommaFeed en el meu temps lliure. Gràcies per interessar-te i ajudar-me a continuar donant suport a CommaFeed.</1>"
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
@@ -72,8 +68,8 @@ msgid "All"
|
||||
msgstr "Tot"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgstr ""
|
||||
msgid "Always"
|
||||
msgstr "Sempre"
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "An email has been sent if this address was registered. Check your inbox."
|
||||
@@ -89,7 +85,7 @@ msgstr "Analitzar el feed"
|
||||
|
||||
#: src/components/AnnouncementDialog.tsx
|
||||
msgid "Announcement"
|
||||
msgstr ""
|
||||
msgstr "Anunci"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "API key"
|
||||
@@ -121,7 +117,7 @@ msgstr "Estàs segur que vols cancel·lar la subscripció a <0>{feedName}</0>?"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Asc"
|
||||
msgstr ""
|
||||
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."
|
||||
@@ -137,11 +133,11 @@ msgstr "Tornar a iniciar sessió"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Browser extension required for Chrome"
|
||||
msgstr ""
|
||||
msgstr "Extensió del navegador necessària per a Chrome"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Browser extention"
|
||||
msgstr ""
|
||||
msgstr "Extensió del navegador"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
@@ -177,15 +173,15 @@ msgstr "Comproveu que el canal funciona"
|
||||
|
||||
#: src/pages/app/Layout.tsx
|
||||
msgid "Close menu"
|
||||
msgstr ""
|
||||
msgstr "Tanca el menu"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
msgstr "Versió de l'extensió 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. The username is your user name and the password is your API key."
|
||||
msgstr ""
|
||||
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 "CommaFeed és compatible amb l'API Fever. Utilitzeu l'URL següent al vostre client mòbil compatible amb Fever. Inicieu sessió amb el vostre nom d'usuari i la vostra <0>clau API</0>."
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed next unread item"
|
||||
@@ -193,7 +189,7 @@ msgstr "CommaFeed següent element no llegit"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed version {version} ({revision})."
|
||||
msgstr ""
|
||||
msgstr "CommaFeed versió {version} ({version})."
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Compact"
|
||||
@@ -205,7 +201,7 @@ msgstr "Compacte"
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Confirm"
|
||||
msgstr "Confirmar"
|
||||
msgstr "Confirma"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Confirm password"
|
||||
@@ -217,7 +213,7 @@ msgstr "Acollidor"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Ctrl"
|
||||
msgstr ""
|
||||
msgstr "Ctrl"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Current password"
|
||||
@@ -225,19 +221,19 @@ msgstr "Contrasenya actual"
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Custom code"
|
||||
msgstr ""
|
||||
msgstr "Codi personalitzat"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "Custom CSS rules that will be applied"
|
||||
msgstr ""
|
||||
msgstr "Regles CSS personalitzades que s'aplicaran"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "Custom JS code that will be executed on page load"
|
||||
msgstr ""
|
||||
msgstr "Codi JS personalitzat que s'executarà en carregar la pàgina"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Dark"
|
||||
msgstr ""
|
||||
msgstr "Fosc"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Date created"
|
||||
@@ -262,11 +258,11 @@ msgstr "Suprimeix l'usuari"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Desc"
|
||||
msgstr ""
|
||||
msgstr "Desc"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Detailed"
|
||||
msgstr ""
|
||||
msgstr "Detallat"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
@@ -276,7 +272,7 @@ msgstr "Mostra"
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/app/DonatePage.tsx
|
||||
msgid "Donate"
|
||||
msgstr ""
|
||||
msgstr "Donar"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Download"
|
||||
@@ -318,7 +314,7 @@ msgstr "introduïu la vostra contrasenya actual per canviar la configuració del
|
||||
|
||||
#: src/components/Alert.tsx
|
||||
msgid "Error"
|
||||
msgstr ""
|
||||
msgstr "Error"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Example: {example}."
|
||||
@@ -335,7 +331,7 @@ msgstr "exporteu les vostres subscripcions i categories com a fitxer OPML que es
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
msgid "Extension options"
|
||||
msgstr ""
|
||||
msgstr "Opcions de l'extensió"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Feed name"
|
||||
@@ -349,7 +345,7 @@ msgstr "URL del canal"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Fetch all my feeds now"
|
||||
msgstr ""
|
||||
msgstr "Carrega tots els meus feeds ara"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Fever API"
|
||||
@@ -389,7 +385,7 @@ msgstr "URL del feed generat"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Go to {0}"
|
||||
msgstr ""
|
||||
msgstr "Vés a {0}"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Go to the All view"
|
||||
@@ -405,12 +401,16 @@ msgstr "Bones"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Id"
|
||||
msgstr ""
|
||||
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 "Si no està buida, una expressió que s'avalua com a \"vertader\" o \"fals\". "
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
msgstr "Si l'entrada no encaixa del tot a 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 trobeu un problema, informeu-lo a la pàgina de problemes del projecte GitHub."
|
||||
@@ -451,7 +451,7 @@ msgstr "últim missatge d'actualització"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Light"
|
||||
msgstr ""
|
||||
msgstr "Clar"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
@@ -519,7 +519,7 @@ msgstr "mètriques"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Middle click"
|
||||
msgstr ""
|
||||
msgstr "Clic central"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Move the page down"
|
||||
@@ -545,6 +545,10 @@ msgstr "Nom"
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Navegueu a una subscripció introduint-ne el nom"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
msgstr "Mai"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "New password"
|
||||
msgstr "Contrasenya nova"
|
||||
@@ -578,13 +582,17 @@ msgstr "No s'ha trobat res"
|
||||
msgid "Oldest first"
|
||||
msgstr "el més vell primer"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
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/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Vaja!"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Open CommaFeed"
|
||||
msgstr ""
|
||||
msgstr "Obre CommaFeed"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open current entry in a new tab"
|
||||
@@ -596,19 +604,19 @@ msgstr "Obre l'entrada actual en una pestanya nova al fons"
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Open link"
|
||||
msgstr "Enllaç obert"
|
||||
msgstr "Obre l'enllaç obert"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Open link in new background tab"
|
||||
msgstr ""
|
||||
msgstr "Obre l'enllaç a una pestanya de fons nova"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Open link in new tab"
|
||||
msgstr ""
|
||||
msgstr "Obre l'enllaç en una pestanya nova"
|
||||
|
||||
#: src/pages/app/Layout.tsx
|
||||
msgid "Open menu"
|
||||
msgstr ""
|
||||
msgstr "Obre el menú"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open next entry"
|
||||
@@ -624,7 +632,7 @@ msgstr "Obrir/tancar l'entrada actual"
|
||||
|
||||
#: src/pages/app/AddPage.tsx
|
||||
msgid "OPML"
|
||||
msgstr ""
|
||||
msgstr "OPML"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "OPML export"
|
||||
@@ -670,7 +678,7 @@ msgstr "Posició"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Previous"
|
||||
msgstr ""
|
||||
msgstr "Anterior"
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Profile"
|
||||
@@ -696,7 +704,7 @@ msgstr "API REST"
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Right click"
|
||||
msgstr ""
|
||||
msgstr "Clic dret"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
@@ -706,10 +714,18 @@ msgstr ""
|
||||
msgid "Save"
|
||||
msgstr "Desa"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll selected entry to the top of the page"
|
||||
msgstr "Desplaceu-vos per l'entrada seleccionada fins a la part superior de la pàgina"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll smoothly when navigating between entries"
|
||||
msgstr "Desplaceu-vos suaument quan navegueu entre entrades"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scrolling"
|
||||
msgstr "Desplaçament"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
@@ -753,19 +769,19 @@ msgstr "canvi"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show CommaFeed's own context menu on right click"
|
||||
msgstr ""
|
||||
msgstr "Mostra el menú contextual de CommaFeed fent clic amb el botó dret"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show confirmation when marking all entries as read"
|
||||
msgstr ""
|
||||
msgstr "Mostra la confirmació en marcar totes les entrades com a llegides"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show entry menu (desktop)"
|
||||
msgstr ""
|
||||
msgstr "Mostra el menú d'entrada (escriptori)"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show entry menu (mobile)"
|
||||
msgstr ""
|
||||
msgstr "Mostra el menú d'entrada (mòbil)"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show feeds and categories with no unread entries"
|
||||
@@ -777,13 +793,13 @@ msgstr "Mostra l'ajuda de la drecera del teclat"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show native menu (desktop)"
|
||||
msgstr ""
|
||||
msgstr "Mostra el menú natiu (escriptori)"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
msgid "Sign up"
|
||||
msgstr "Inscriu-te"
|
||||
msgstr "Registra't"
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Something bad just happened..."
|
||||
@@ -823,8 +839,8 @@ msgid "Success"
|
||||
msgstr "Éxit"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Swipe header to the right"
|
||||
msgstr ""
|
||||
msgid "Swipe header to the left"
|
||||
msgstr "Feu lliscar la capçalera cap a l'esquerra"
|
||||
|
||||
#: src/pages/WelcomePage.tsx
|
||||
msgid "Switch to dark theme"
|
||||
@@ -836,7 +852,7 @@ msgstr "Canvia al tema clar"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "System"
|
||||
msgstr ""
|
||||
msgstr "Sistema"
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
@@ -851,17 +867,21 @@ msgstr "l'URL del canal al qual us voleu subscriure. "
|
||||
msgid "Theme"
|
||||
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 "Aquesta és la vostra clau de l'API. Es pot utilitzar per a algunes operacions de l'API de només lectura i permet accedir a l'API Fever. Utilitzeu el formulari de la part inferior de la pàgina per generar una nova clau d'API."
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "Canvia l'estat de lectura de l'entrada actual"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle sidebar"
|
||||
msgstr ""
|
||||
msgstr "Canvia la barra lateral"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle starred status of current entry"
|
||||
msgstr ""
|
||||
msgstr "Commuta l'estat destacat de l'entrada actual"
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||
@@ -869,7 +889,7 @@ msgstr "Proveu CommaFeed amb el compte de demostració: demo/demo"
|
||||
|
||||
#: src/pages/WelcomePage.tsx
|
||||
msgid "Try the demo!"
|
||||
msgstr ""
|
||||
msgstr "Prova la demostració!"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Unread"
|
||||
@@ -908,4 +928,4 @@ msgstr "Encara no teniu cap subscripció. "
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Your feeds have been queued for refresh."
|
||||
msgstr ""
|
||||
msgstr "Els vostres feeds s'han posat a la cua per actualitzar-los."
|
||||
|
||||
@@ -13,10 +13,6 @@ msgstr ""
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
msgid "{0} (in {1})"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr ""
|
||||
@@ -72,7 +68,7 @@ msgid "All"
|
||||
msgstr "Všechny"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgid "Always"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
@@ -184,7 +180,7 @@ msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
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 ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
@@ -411,6 +407,10 @@ msgstr ""
|
||||
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 "Pokud není prázdný, výraz vyhodnocený jako 'true' nebo 'false'. "
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "If you encounter an issue, please report it on the issues page of the GitHub project."
|
||||
msgstr "Pokud narazíte na problém, nahlaste jej prosím na stránce problémů projektu GitHub."
|
||||
@@ -545,6 +545,10 @@ msgstr "Jméno"
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Přejděte na předplatné zadáním jeho názvu"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "New password"
|
||||
msgstr "Nové heslo"
|
||||
@@ -578,6 +582,10 @@ msgstr "Nic nebylo nalezeno"
|
||||
msgid "Oldest first"
|
||||
msgstr "Nejdříve nejstarší"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Jejda!"
|
||||
@@ -706,10 +714,18 @@ msgstr ""
|
||||
msgid "Save"
|
||||
msgstr "Uložit"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll selected entry to the top of the page"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll smoothly when navigating between entries"
|
||||
msgstr "Posouvejte plynule při navigaci mezi položkami"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
@@ -823,7 +839,7 @@ msgid "Success"
|
||||
msgstr "Úspěch"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Swipe header to the right"
|
||||
msgid "Swipe header to the left"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/WelcomePage.tsx
|
||||
@@ -851,6 +867,10 @@ msgstr "Adresa URL kanálu, k jehož odběru se chcete přihlásit. "
|
||||
msgid "Theme"
|
||||
msgstr "Téma"
|
||||
|
||||
#: 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 ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "Přepne stav čtení aktuálního záznamu"
|
||||
|
||||
@@ -13,10 +13,6 @@ msgstr ""
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
msgid "{0} (in {1})"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr ""
|
||||
@@ -72,7 +68,7 @@ msgid "All"
|
||||
msgstr "Pawb"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgid "Always"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
@@ -184,7 +180,7 @@ msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
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 ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
@@ -411,6 +407,10 @@ msgstr ""
|
||||
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 "Os nad yw'n wag, mynegiad sy'n gwerthuso i 'gwir' neu 'anghywir'. "
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "If you encounter an issue, please report it on the issues page of the GitHub project."
|
||||
msgstr "Os byddwch yn dod ar draws mater, rhowch wybod amdano ar dudalen materion y prosiect GitHub."
|
||||
@@ -545,6 +545,10 @@ msgstr "Enw"
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Llywiwch i danysgrifiad trwy nodi ei enw"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "New password"
|
||||
msgstr "Cyfrinair newydd"
|
||||
@@ -578,6 +582,10 @@ msgstr "Dim wedi'i ddarganfod"
|
||||
msgid "Oldest first"
|
||||
msgstr "Hynaf yn gyntaf"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Wps!"
|
||||
@@ -706,10 +714,18 @@ msgstr ""
|
||||
msgid "Save"
|
||||
msgstr "Arbed"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll selected entry to the top of the page"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll smoothly when navigating between entries"
|
||||
msgstr "Sgroliwch yn esmwyth wrth lywio rhwng cofnodion"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
@@ -823,7 +839,7 @@ msgid "Success"
|
||||
msgstr "Llwyddiant"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Swipe header to the right"
|
||||
msgid "Swipe header to the left"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/WelcomePage.tsx
|
||||
@@ -851,6 +867,10 @@ msgstr "Y URL ar gyfer y porthwr rydych chi am danysgrifio iddo. "
|
||||
msgid "Theme"
|
||||
msgstr "Thema"
|
||||
|
||||
#: 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 ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "Toglo statws darllen y cofnod cyfredol"
|
||||
|
||||
@@ -13,10 +13,6 @@ msgstr ""
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
msgid "{0} (in {1})"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr ""
|
||||
@@ -72,7 +68,7 @@ msgid "All"
|
||||
msgstr "Alle"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgid "Always"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
@@ -184,7 +180,7 @@ msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
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 ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
@@ -411,6 +407,10 @@ msgstr ""
|
||||
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 "Hvis det ikke er tomt, et udtryk, der vurderes til 'sand' eller 'falsk'. "
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "If you encounter an issue, please report it on the issues page of the GitHub project."
|
||||
msgstr "Hvis du støder på et problem, bedes du rapportere det på problemsiden for GitHub-projektet."
|
||||
@@ -545,6 +545,10 @@ msgstr "Navn"
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Naviger til et abonnement ved at indtaste dets navn"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "New password"
|
||||
msgstr "Ny adgangskode"
|
||||
@@ -578,6 +582,10 @@ msgstr "Intet fundet"
|
||||
msgid "Oldest first"
|
||||
msgstr "Ældst først"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Hovsa!"
|
||||
@@ -706,10 +714,18 @@ msgstr ""
|
||||
msgid "Save"
|
||||
msgstr "Gem"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll selected entry to the top of the page"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll smoothly when navigating between entries"
|
||||
msgstr "Rul jævnt, når du navigerer mellem poster"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
@@ -823,7 +839,7 @@ msgid "Success"
|
||||
msgstr "Succes"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Swipe header to the right"
|
||||
msgid "Swipe header to the left"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/WelcomePage.tsx
|
||||
@@ -851,6 +867,10 @@ msgstr "URL'en til det feed, du vil abonnere på. "
|
||||
msgid "Theme"
|
||||
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 ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "Skift læsestatus for den aktuelle post"
|
||||
|
||||
@@ -13,10 +13,6 @@ msgstr ""
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
msgid "{0} (in {1})"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr "CommaFeed ist ein Open Source Projekt. Der Quellcode wird auf auf </0><1>GitHub</1> gehostet."
|
||||
@@ -72,7 +68,7 @@ msgid "All"
|
||||
msgstr "Alle"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgid "Always"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
@@ -184,8 +180,8 @@ msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
msgstr "CommaFeed ist kompatibel zur Fever API. Benutzen Sie folgende URL in Ihrem Fever-kompatiblen Mobilclient. Der Benutzername ist Ihr User Name, das Passwort ist der API-Schlüssel."
|
||||
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 ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed next unread item"
|
||||
@@ -411,6 +407,10 @@ msgstr ""
|
||||
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 "Wenn nicht leer, ein Ausdruck, der als „wahr“ oder „falsch“ ausgewertet wird. "
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "If you encounter an issue, please report it on the issues page of the GitHub project."
|
||||
msgstr "Wenn Sie auf ein Problem stoßen, melden Sie es bitte auf der Problemseite des GitHub-Projekts."
|
||||
@@ -545,6 +545,10 @@ msgstr ""
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Navigieren Sie zu einem Abonnement, indem Sie seinen Namen eingeben"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "New password"
|
||||
msgstr "Neues Passwort"
|
||||
@@ -578,6 +582,10 @@ msgstr "Nichts gefunden"
|
||||
msgid "Oldest first"
|
||||
msgstr "Älteste zuerst"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Ups!"
|
||||
@@ -706,10 +714,18 @@ msgstr "Rechtsklick"
|
||||
msgid "Save"
|
||||
msgstr "Speichern"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll selected entry to the top of the page"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll smoothly when navigating between entries"
|
||||
msgstr "Schnelles Scrollen beim Navigieren zwischen Einträgen"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
@@ -823,7 +839,7 @@ msgid "Success"
|
||||
msgstr "Erfolg"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Swipe header to the right"
|
||||
msgid "Swipe header to the left"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/WelcomePage.tsx
|
||||
@@ -851,6 +867,10 @@ msgstr "Die URL für den Feed, den Sie abonnieren möchten. "
|
||||
msgid "Theme"
|
||||
msgstr "Thema"
|
||||
|
||||
#: 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 ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "Lesestatus des aktuellen Eintrags umschalten"
|
||||
|
||||
@@ -13,10 +13,6 @@ msgstr ""
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
msgid "{0} (in {1})"
|
||||
msgstr "{0} (in {1})"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
@@ -72,8 +68,8 @@ msgid "All"
|
||||
msgstr "All"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgstr "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgid "Always"
|
||||
msgstr "Always"
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "An email has been sent if this address was registered. Check your inbox."
|
||||
@@ -184,8 +180,8 @@ msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
msgstr "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
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 "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>."
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed next unread item"
|
||||
@@ -411,6 +407,10 @@ msgstr "Id"
|
||||
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 "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
msgstr "If the entry doesn't entirely fit on the screen"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "If you encounter an issue, please report it on the issues page of the GitHub project."
|
||||
msgstr "If you encounter an issue, please report it on the issues page of the GitHub project."
|
||||
@@ -545,6 +545,10 @@ msgstr "Name"
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Navigate to a subscription by entering its name"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
msgstr "Never"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "New password"
|
||||
msgstr "New password"
|
||||
@@ -578,6 +582,10 @@ msgstr "Nothing found"
|
||||
msgid "Oldest first"
|
||||
msgstr "Oldest first"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
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/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Oops!"
|
||||
@@ -706,10 +714,18 @@ msgstr "Right click"
|
||||
msgid "Save"
|
||||
msgstr "Save"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll selected entry to the top of the page"
|
||||
msgstr "Scroll selected entry to the top of the page"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll smoothly when navigating between entries"
|
||||
msgstr "Scroll smoothly when navigating between entries"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scrolling"
|
||||
msgstr "Scrolling"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
@@ -823,8 +839,8 @@ msgid "Success"
|
||||
msgstr "Success"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Swipe header to the right"
|
||||
msgstr "Swipe header to the right"
|
||||
msgid "Swipe header to the left"
|
||||
msgstr "Swipe header to the left"
|
||||
|
||||
#: src/pages/WelcomePage.tsx
|
||||
msgid "Switch to dark theme"
|
||||
@@ -851,6 +867,10 @@ msgstr "The URL for the feed you want to subscribe to. You can also use the webs
|
||||
msgid "Theme"
|
||||
msgstr "Theme"
|
||||
|
||||
#: 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 "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"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "Toggle read status of current entry"
|
||||
|
||||
@@ -13,10 +13,6 @@ msgstr ""
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
msgid "{0} (in {1})"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr ""
|
||||
@@ -72,7 +68,7 @@ msgid "All"
|
||||
msgstr "Todo"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgid "Always"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
@@ -184,7 +180,7 @@ msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
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 ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
@@ -411,6 +407,10 @@ msgstr "Identificación"
|
||||
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'. "
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
msgstr ""
|
||||
|
||||
#: 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."
|
||||
@@ -545,6 +545,10 @@ msgstr "Nombre"
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Navegar a una suscripción ingresando su nombre"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "New password"
|
||||
msgstr "Nueva contraseña"
|
||||
@@ -578,6 +582,10 @@ msgstr "Nada encontrado"
|
||||
msgid "Oldest first"
|
||||
msgstr "más antigua primero"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "¡Ups!"
|
||||
@@ -706,10 +714,18 @@ msgstr ""
|
||||
msgid "Save"
|
||||
msgstr "Guardar"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll selected entry to the top of the page"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll smoothly when navigating between entries"
|
||||
msgstr "Desplazarse suavemente al navegar entre entradas"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
@@ -823,7 +839,7 @@ msgid "Success"
|
||||
msgstr "Éxito"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Swipe header to the right"
|
||||
msgid "Swipe header to the left"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/WelcomePage.tsx
|
||||
@@ -851,6 +867,10 @@ msgstr "La URL de la fuente a la que desea suscribirse. "
|
||||
msgid "Theme"
|
||||
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 ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "Alternar estado de lectura de la entrada actual"
|
||||
|
||||
@@ -13,10 +13,6 @@ msgstr ""
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
msgid "{0} (in {1})"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr ""
|
||||
@@ -72,7 +68,7 @@ msgid "All"
|
||||
msgstr "همه"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgid "Always"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
@@ -184,7 +180,7 @@ msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
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 ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
@@ -411,6 +407,10 @@ msgstr "شناسه"
|
||||
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 "اگر خالی نباشد، عبارتی به \"درست\" یا \"نادرست\" ارزیابی می شود. "
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
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 گزارش دهید."
|
||||
@@ -545,6 +545,10 @@ msgstr "نام"
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "با وارد کردن نام اشتراک، به آن بروید"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "New password"
|
||||
msgstr "رمز عبور جدید"
|
||||
@@ -578,6 +582,10 @@ msgstr "چیزی پیدا نشد"
|
||||
msgid "Oldest first"
|
||||
msgstr "قدیمی ترین اول"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "اوه!"
|
||||
@@ -706,10 +714,18 @@ msgstr ""
|
||||
msgid "Save"
|
||||
msgstr "ذخیره کنید"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll selected entry to the top of the page"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll smoothly when navigating between entries"
|
||||
msgstr "هنگام پیمایش بین ورودیها به آرامی حرکت کنید"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
@@ -823,7 +839,7 @@ msgid "Success"
|
||||
msgstr "موفقیت"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Swipe header to the right"
|
||||
msgid "Swipe header to the left"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/WelcomePage.tsx
|
||||
@@ -851,6 +867,10 @@ msgstr "URL فیدی که می خواهید در آن مشترک شوید. "
|
||||
msgid "Theme"
|
||||
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 ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "وضعیت خواندن ورودی فعلی را تغییر دهید"
|
||||
|
||||
@@ -13,10 +13,6 @@ msgstr ""
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
msgid "{0} (in {1})"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr ""
|
||||
@@ -72,7 +68,7 @@ msgid "All"
|
||||
msgstr "Kaikki"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgid "Always"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
@@ -184,7 +180,7 @@ msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
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 ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
@@ -411,6 +407,10 @@ msgstr ""
|
||||
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 "Jos ei tyhjä, lauseke, jonka arvo on \"tosi\" tai \"epätosi\". "
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "If you encounter an issue, please report it on the issues page of the GitHub project."
|
||||
msgstr "Jos kohtaat ongelman, ilmoita siitä GitHub-projektin ongelmasivulla."
|
||||
@@ -545,6 +545,10 @@ msgstr "Nimi"
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Siirry tilaukseen kirjoittamalla sen nimi"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "New password"
|
||||
msgstr "Uusi salasana"
|
||||
@@ -578,6 +582,10 @@ msgstr "Mitään ei löytynyt"
|
||||
msgid "Oldest first"
|
||||
msgstr "Vanhin ensin"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Hups!"
|
||||
@@ -706,10 +714,18 @@ msgstr ""
|
||||
msgid "Save"
|
||||
msgstr "Tallenna"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll selected entry to the top of the page"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll smoothly when navigating between entries"
|
||||
msgstr "Selaa sujuvasti navigoidessasi merkintöjen välillä"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
@@ -823,7 +839,7 @@ msgid "Success"
|
||||
msgstr "Onnistui"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Swipe header to the right"
|
||||
msgid "Swipe header to the left"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/WelcomePage.tsx
|
||||
@@ -851,6 +867,10 @@ msgstr "Sen syötteen URL-osoite, jonka haluat tilata. "
|
||||
msgid "Theme"
|
||||
msgstr "Teema"
|
||||
|
||||
#: 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 ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "Vaihda nykyisen merkinnän lukutila"
|
||||
|
||||
@@ -13,10 +13,6 @@ msgstr ""
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
msgid "{0} (in {1})"
|
||||
msgstr "{0} (sur {1})"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr "<0>CommaFeed est un projet open-source. Les sources sont hébergées sur </0><1>GitHub</1>."
|
||||
@@ -72,8 +68,8 @@ msgid "All"
|
||||
msgstr "Tout"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgstr "Toujours remonter l'entrée sélectionnée en haut de la page, même si elle s'affiche complètement à l'écran"
|
||||
msgid "Always"
|
||||
msgstr "Toujours"
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "An email has been sent if this address was registered. Check your inbox."
|
||||
@@ -184,8 +180,8 @@ msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr "Extension CommaFeed pour navigateur version {browserExtensionVersion}."
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
msgstr "Commafeed est compatible avec l'API Fever, en inscrivant l'URL suivante dans votre client mobile compatible. Entrez votre nom d'utilisateur habituel, et votre clef API comme mot de passe."
|
||||
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 "Commafeed est compatible avec l'API Fever, en inscrivant l'URL suivante dans votre client mobile compatible. Entrez votre nom d'utilisateur habituel, et votre <0>clef API</0> comme mot de passe."
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed next unread item"
|
||||
@@ -411,6 +407,10 @@ msgstr "Identifiant"
|
||||
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 non vide, une expression évaluant à 'vrai' ou 'faux'. Si faux, les nouvelles entrées de ce flux seront marquées comme lues automatiquement."
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
msgstr "Si l'entrée ne tient pas entièrement sur l'écran"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "If you encounter an issue, please report it on the issues page of the GitHub project."
|
||||
msgstr "Si vous rencontrez un problème, merci de le signaler sur la page du projet GitHub."
|
||||
@@ -545,6 +545,10 @@ msgstr "Nom"
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Naviguer vers un abonnement en entrant son nom"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
msgstr "Jamais"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "New password"
|
||||
msgstr "Nouveau mot de passe"
|
||||
@@ -578,6 +582,10 @@ msgstr "Aucun résultat"
|
||||
msgid "Oldest first"
|
||||
msgstr "Du plus ancien au plus récent"
|
||||
|
||||
#: 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/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Oups !"
|
||||
@@ -706,10 +714,18 @@ msgstr "Clic droit"
|
||||
msgid "Save"
|
||||
msgstr "Enregistrer"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll selected entry to the top of the page"
|
||||
msgstr "Faire défiler l'entrée sélectionnée vers le haut de la page"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll smoothly when navigating between entries"
|
||||
msgstr "Défilement animé lors de la navigation entre les entrées"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scrolling"
|
||||
msgstr "Défilement"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
@@ -823,8 +839,8 @@ msgid "Success"
|
||||
msgstr "Succès"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Swipe header to the right"
|
||||
msgstr "Faire glisser le titre vers la droite"
|
||||
msgid "Swipe header to the left"
|
||||
msgstr "Faire glisser le titre vers la gauche"
|
||||
|
||||
#: src/pages/WelcomePage.tsx
|
||||
msgid "Switch to dark theme"
|
||||
@@ -851,6 +867,10 @@ msgstr "L'URL du flux auquel vous souhaitez vous abonner. Vous pouvez aussi util
|
||||
msgid "Theme"
|
||||
msgstr "Thème"
|
||||
|
||||
#: 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 "Ceci est votre clef API. Elle peut être utilisée pour certaines opérations en lecture seule et donne accès à l'API Fever. Utilisez le formulaire en bas de la page pour générer une nouvelle clef API"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "Marquer l'entrée actuelle comme lue/non lue"
|
||||
|
||||
@@ -13,10 +13,6 @@ msgstr ""
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
msgid "{0} (in {1})"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr ""
|
||||
@@ -72,7 +68,7 @@ msgid "All"
|
||||
msgstr "Todos"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgid "Always"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
@@ -184,7 +180,7 @@ msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
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 ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
@@ -411,6 +407,10 @@ msgstr ""
|
||||
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 "Se non está baleira, unha expresión que se avalía como \"verdadeiro\" ou \"falso\". "
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "If you encounter an issue, please report it on the issues page of the GitHub project."
|
||||
msgstr "Se atopas algún problema, infórmao na páxina de problemas do proxecto GitHub."
|
||||
@@ -545,6 +545,10 @@ msgstr "Nome"
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Navega a unha subscrición introducindo o seu nome"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "New password"
|
||||
msgstr "novo contrasinal"
|
||||
@@ -578,6 +582,10 @@ msgstr "Non se atopou nada"
|
||||
msgid "Oldest first"
|
||||
msgstr "O máis vello primeiro"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Vaia!"
|
||||
@@ -706,10 +714,18 @@ msgstr ""
|
||||
msgid "Save"
|
||||
msgstr "Gardar"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll selected entry to the top of the page"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll smoothly when navigating between entries"
|
||||
msgstr "Desprácese suavemente ao navegar entre as entradas"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
@@ -823,7 +839,7 @@ msgid "Success"
|
||||
msgstr "Éxito"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Swipe header to the right"
|
||||
msgid "Swipe header to the left"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/WelcomePage.tsx
|
||||
@@ -851,6 +867,10 @@ msgstr "O URL do feed ao que quere subscribirse. "
|
||||
msgid "Theme"
|
||||
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 ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "alternar o estado de lectura da entrada actual"
|
||||
|
||||
@@ -13,10 +13,6 @@ msgstr ""
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
msgid "{0} (in {1})"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr ""
|
||||
@@ -72,7 +68,7 @@ msgid "All"
|
||||
msgstr "Mind"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgid "Always"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
@@ -184,7 +180,7 @@ msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
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 ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
@@ -411,6 +407,10 @@ msgstr ""
|
||||
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 "Ha nem üres, akkor 'igaz' vagy 'hamis' értékre kiértékelő kifejezés. "
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "If you encounter an issue, please report it on the issues page of the GitHub project."
|
||||
msgstr "Ha problémát tapasztal, kérjük, jelentse azt a GitHub projekt problémák oldalán."
|
||||
@@ -545,6 +545,10 @@ msgstr "Név"
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Navigáljon egy előfizetéshez a nevének megadásával"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "New password"
|
||||
msgstr "Új jelszó"
|
||||
@@ -578,6 +582,10 @@ msgstr "Semmi sem található"
|
||||
msgid "Oldest first"
|
||||
msgstr "A legidősebb első"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Hoppá!"
|
||||
@@ -706,10 +714,18 @@ msgstr ""
|
||||
msgid "Save"
|
||||
msgstr "Mentés"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll selected entry to the top of the page"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll smoothly when navigating between entries"
|
||||
msgstr "Sima görgetés, amikor a bejegyzések között navigál"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
@@ -823,7 +839,7 @@ msgid "Success"
|
||||
msgstr "Siker"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Swipe header to the right"
|
||||
msgid "Swipe header to the left"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/WelcomePage.tsx
|
||||
@@ -851,6 +867,10 @@ msgstr "Az előfizetni kívánt hírcsatorna URL-je. "
|
||||
msgid "Theme"
|
||||
msgstr "Téma"
|
||||
|
||||
#: 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 ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "Az aktuális bejegyzés olvasási állapotának váltása"
|
||||
|
||||
@@ -13,10 +13,6 @@ msgstr ""
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
msgid "{0} (in {1})"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr ""
|
||||
@@ -72,7 +68,7 @@ msgid "All"
|
||||
msgstr "Semua"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgid "Always"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
@@ -184,7 +180,7 @@ msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
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 ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
@@ -411,6 +407,10 @@ msgstr ""
|
||||
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 "Jika tidak kosong, ekspresi mengevaluasi ke 'benar' atau 'salah'. "
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "If you encounter an issue, please report it on the issues page of the GitHub project."
|
||||
msgstr "Jika Anda mengalami masalah, harap laporkan di halaman masalah proyek GitHub."
|
||||
@@ -545,6 +545,10 @@ msgstr "Nama"
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Navigasikan ke langganan dengan memasukkan namanya"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "New password"
|
||||
msgstr "Kata sandi baru"
|
||||
@@ -578,6 +582,10 @@ msgstr "Tidak ada yang ditemukan"
|
||||
msgid "Oldest first"
|
||||
msgstr "Tertua dulu"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Ups!"
|
||||
@@ -706,10 +714,18 @@ msgstr ""
|
||||
msgid "Save"
|
||||
msgstr "Simpan"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll selected entry to the top of the page"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll smoothly when navigating between entries"
|
||||
msgstr "Gulir dengan lancar saat menavigasi antar entri"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
@@ -823,7 +839,7 @@ msgid "Success"
|
||||
msgstr "Sukses"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Swipe header to the right"
|
||||
msgid "Swipe header to the left"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/WelcomePage.tsx
|
||||
@@ -851,6 +867,10 @@ msgstr "URL untuk umpan yang ingin Anda langgani. "
|
||||
msgid "Theme"
|
||||
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 ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "Beralih status baca entri saat ini"
|
||||
|
||||
@@ -13,10 +13,6 @@ msgstr ""
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
msgid "{0} (in {1})"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr ""
|
||||
@@ -72,7 +68,7 @@ msgid "All"
|
||||
msgstr "Tutto"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgid "Always"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
@@ -184,7 +180,7 @@ msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
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 ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
@@ -411,6 +407,10 @@ msgstr ""
|
||||
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 "Se non è vuota, un'espressione valutata come 'vero' o 'falso'. "
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "If you encounter an issue, please report it on the issues page of the GitHub project."
|
||||
msgstr "Se riscontri un problema, segnalalo nella pagina dei problemi del progetto GitHub."
|
||||
@@ -545,6 +545,10 @@ msgstr "Nome"
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Navigare verso un abbonamento inserendo il suo nome"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "New password"
|
||||
msgstr "Nuova password"
|
||||
@@ -578,6 +582,10 @@ msgstr "Non è stato trovato nulla"
|
||||
msgid "Oldest first"
|
||||
msgstr "Il più vecchio prima"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Ops!"
|
||||
@@ -706,10 +714,18 @@ msgstr ""
|
||||
msgid "Save"
|
||||
msgstr "Salva"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll selected entry to the top of the page"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll smoothly when navigating between entries"
|
||||
msgstr "Scorrere senza problemi durante la navigazione tra le voci"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
@@ -823,7 +839,7 @@ msgid "Success"
|
||||
msgstr "Successo"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Swipe header to the right"
|
||||
msgid "Swipe header to the left"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/WelcomePage.tsx
|
||||
@@ -851,6 +867,10 @@ msgstr "L'URL del feed a cui vuoi iscriverti. "
|
||||
msgid "Theme"
|
||||
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 ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "Commuta lo stato di lettura della voce corrente"
|
||||
|
||||
@@ -13,10 +13,6 @@ msgstr ""
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
msgid "{0} (in {1})"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr ""
|
||||
@@ -72,7 +68,7 @@ msgid "All"
|
||||
msgstr "全員"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgid "Always"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
@@ -184,7 +180,7 @@ msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
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 ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
@@ -411,6 +407,10 @@ msgstr "ID"
|
||||
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' に評価される式。 "
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
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 プロジェクトの問題ページで報告してください。"
|
||||
@@ -545,6 +545,10 @@ msgstr "名前"
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "名前を入力してサブスクリプションに移動します"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "New password"
|
||||
msgstr "新しいパスワード"
|
||||
@@ -578,6 +582,10 @@ msgstr "何も見つかりませんでした"
|
||||
msgid "Oldest first"
|
||||
msgstr "古い順"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "おっと!"
|
||||
@@ -706,10 +714,18 @@ msgstr ""
|
||||
msgid "Save"
|
||||
msgstr "保存"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll selected entry to the top of the page"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll smoothly when navigating between entries"
|
||||
msgstr "エントリ間を移動するときにスムーズにスクロールする"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
@@ -823,7 +839,7 @@ msgid "Success"
|
||||
msgstr "成功"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Swipe header to the right"
|
||||
msgid "Swipe header to the left"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/WelcomePage.tsx
|
||||
@@ -851,6 +867,10 @@ msgstr "購読したいフィードのURL。 "
|
||||
msgid "Theme"
|
||||
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 ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "現在のエントリの読み取りステータスを切り替えます"
|
||||
|
||||
@@ -13,10 +13,6 @@ msgstr ""
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
msgid "{0} (in {1})"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr ""
|
||||
@@ -72,7 +68,7 @@ msgid "All"
|
||||
msgstr "전체"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgid "Always"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
@@ -184,7 +180,7 @@ msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
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 ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
@@ -411,6 +407,10 @@ msgstr "아이디"
|
||||
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'로 평가되는 표현식입니다. "
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
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 프로젝트의 문제 페이지에서 보고하세요."
|
||||
@@ -545,6 +545,10 @@ msgstr "이름"
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "이름을 입력하여 구독으로 이동"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "New password"
|
||||
msgstr "새 비밀번호"
|
||||
@@ -578,6 +582,10 @@ msgstr "아무것도 찾을 수 없습니다"
|
||||
msgid "Oldest first"
|
||||
msgstr "가장 오래된 것부터"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "앗!"
|
||||
@@ -706,10 +714,18 @@ msgstr ""
|
||||
msgid "Save"
|
||||
msgstr "저장"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll selected entry to the top of the page"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll smoothly when navigating between entries"
|
||||
msgstr "항목 간 탐색 시 부드럽게 스크롤"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
@@ -823,7 +839,7 @@ msgid "Success"
|
||||
msgstr "성공"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Swipe header to the right"
|
||||
msgid "Swipe header to the left"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/WelcomePage.tsx
|
||||
@@ -851,6 +867,10 @@ msgstr "구독하려는 피드의 URL입니다. "
|
||||
msgid "Theme"
|
||||
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 ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "현재 항목의 읽기 상태 전환"
|
||||
|
||||
@@ -13,10 +13,6 @@ msgstr ""
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
msgid "{0} (in {1})"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr ""
|
||||
@@ -72,7 +68,7 @@ msgid "All"
|
||||
msgstr "Semua"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgid "Always"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
@@ -184,7 +180,7 @@ msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
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 ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
@@ -411,6 +407,10 @@ msgstr ""
|
||||
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 "Jika tidak kosong, ungkapan yang menilai kepada 'benar' atau 'palsu'. "
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "If you encounter an issue, please report it on the issues page of the GitHub project."
|
||||
msgstr "Jika anda menghadapi isu, sila laporkan pada halaman isu projek GitHub."
|
||||
@@ -545,6 +545,10 @@ msgstr "Nama"
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Navigasi ke langganan dengan memasukkan namanya"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "New password"
|
||||
msgstr "Kata laluan baharu"
|
||||
@@ -578,6 +582,10 @@ msgstr "Tiada apa-apa dijumpai"
|
||||
msgid "Oldest first"
|
||||
msgstr "Tertua dahulu"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Aduh!"
|
||||
@@ -706,10 +714,18 @@ msgstr ""
|
||||
msgid "Save"
|
||||
msgstr "Jimat"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll selected entry to the top of the page"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll smoothly when navigating between entries"
|
||||
msgstr "Tatal dengan lancar apabila menavigasi antara entri"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
@@ -823,7 +839,7 @@ msgid "Success"
|
||||
msgstr "Kejayaan"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Swipe header to the right"
|
||||
msgid "Swipe header to the left"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/WelcomePage.tsx
|
||||
@@ -851,6 +867,10 @@ msgstr "URL untuk suapan yang anda ingin langgan. "
|
||||
msgid "Theme"
|
||||
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 ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "Togol status bacaan entri semasa"
|
||||
|
||||
@@ -13,10 +13,6 @@ msgstr ""
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
msgid "{0} (in {1})"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr ""
|
||||
@@ -72,7 +68,7 @@ msgid "All"
|
||||
msgstr "Alle"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgid "Always"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
@@ -184,7 +180,7 @@ msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
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 ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
@@ -411,6 +407,10 @@ msgstr ""
|
||||
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 "Hvis det ikke er tomt, et uttrykk som vurderes til 'sant' eller 'usant'. "
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "If you encounter an issue, please report it on the issues page of the GitHub project."
|
||||
msgstr "Hvis du støter på et problem, vennligst rapporter det på problemsiden til GitHub-prosjektet."
|
||||
@@ -545,6 +545,10 @@ msgstr "Navn"
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Naviger til et abonnement ved å skrive inn navnet"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "New password"
|
||||
msgstr "Nytt passord"
|
||||
@@ -578,6 +582,10 @@ msgstr "Ingenting funnet"
|
||||
msgid "Oldest first"
|
||||
msgstr "Eldste først"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Beklager!"
|
||||
@@ -706,10 +714,18 @@ msgstr ""
|
||||
msgid "Save"
|
||||
msgstr "Lagre"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll selected entry to the top of the page"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll smoothly when navigating between entries"
|
||||
msgstr "Rull jevnt når du navigerer mellom oppføringer"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
@@ -823,7 +839,7 @@ msgid "Success"
|
||||
msgstr "Suksess"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Swipe header to the right"
|
||||
msgid "Swipe header to the left"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/WelcomePage.tsx
|
||||
@@ -851,6 +867,10 @@ msgstr "URL-en til feeden du vil abonnere på. "
|
||||
msgid "Theme"
|
||||
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 ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "Veksle lesestatus for gjeldende oppføring"
|
||||
|
||||
@@ -13,10 +13,6 @@ msgstr ""
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
msgid "{0} (in {1})"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr ""
|
||||
@@ -72,7 +68,7 @@ msgid "All"
|
||||
msgstr "Alles"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgid "Always"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
@@ -184,7 +180,7 @@ msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
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 ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
@@ -411,6 +407,10 @@ msgstr ""
|
||||
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 "Indien niet leeg, een uitdrukking die evalueert naar 'true' of 'false'. "
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "If you encounter an issue, please report it on the issues page of the GitHub project."
|
||||
msgstr "Als je een probleem tegenkomt, meld dit dan op de pagina met problemen van het GitHub-project."
|
||||
@@ -545,6 +545,10 @@ msgstr "Naam"
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Navigeer naar een abonnement door de naam in te voeren"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "New password"
|
||||
msgstr "Nieuw wachtwoord"
|
||||
@@ -578,6 +582,10 @@ msgstr "Niets gevonden"
|
||||
msgid "Oldest first"
|
||||
msgstr "Oudste eerst"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Oeps!"
|
||||
@@ -706,10 +714,18 @@ msgstr ""
|
||||
msgid "Save"
|
||||
msgstr "Opslaan"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll selected entry to the top of the page"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll smoothly when navigating between entries"
|
||||
msgstr "Vloeiend scrollen bij het navigeren tussen items"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
@@ -823,7 +839,7 @@ msgid "Success"
|
||||
msgstr "Succes"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Swipe header to the right"
|
||||
msgid "Swipe header to the left"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/WelcomePage.tsx
|
||||
@@ -851,6 +867,10 @@ msgstr "De URL voor de feed waarop u zich wilt abonneren. "
|
||||
msgid "Theme"
|
||||
msgstr "Thema"
|
||||
|
||||
#: 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 ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "Toggle leesstatus van huidige invoer"
|
||||
|
||||
@@ -13,10 +13,6 @@ msgstr ""
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
msgid "{0} (in {1})"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr ""
|
||||
@@ -72,7 +68,7 @@ msgid "All"
|
||||
msgstr "Alle"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgid "Always"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
@@ -184,7 +180,7 @@ msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
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 ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
@@ -411,6 +407,10 @@ msgstr ""
|
||||
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 "Hvis det ikke er tomt, et uttrykk som vurderes til 'sant' eller 'usant'. "
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "If you encounter an issue, please report it on the issues page of the GitHub project."
|
||||
msgstr "Hvis du støter på et problem, vennligst rapporter det på problemsiden til GitHub-prosjektet."
|
||||
@@ -545,6 +545,10 @@ msgstr "Navn"
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Naviger til et abonnement ved å skrive inn navnet"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "New password"
|
||||
msgstr "Nytt passord"
|
||||
@@ -578,6 +582,10 @@ msgstr "Ingenting funnet"
|
||||
msgid "Oldest first"
|
||||
msgstr "Eldste først"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Beklager!"
|
||||
@@ -706,10 +714,18 @@ msgstr ""
|
||||
msgid "Save"
|
||||
msgstr "Lagre"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll selected entry to the top of the page"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll smoothly when navigating between entries"
|
||||
msgstr "Rull jevnt når du navigerer mellom oppføringer"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
@@ -823,7 +839,7 @@ msgid "Success"
|
||||
msgstr "Suksess"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Swipe header to the right"
|
||||
msgid "Swipe header to the left"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/WelcomePage.tsx
|
||||
@@ -851,6 +867,10 @@ msgstr "URL-en til feeden du vil abonnere på. "
|
||||
msgid "Theme"
|
||||
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 ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "Veksle lesestatus for gjeldende oppføring"
|
||||
|
||||
@@ -13,10 +13,6 @@ msgstr ""
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
msgid "{0} (in {1})"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr ""
|
||||
@@ -72,7 +68,7 @@ msgid "All"
|
||||
msgstr "Wszystkie"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgid "Always"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
@@ -184,7 +180,7 @@ msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
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 ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
@@ -411,6 +407,10 @@ msgstr "Identyfikator"
|
||||
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 "Jeśli nie jest puste, wyrażenie oceniające jako „prawda” lub „fałsz”. "
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "If you encounter an issue, please report it on the issues page of the GitHub project."
|
||||
msgstr "Jeśli napotkasz problem, zgłoś go na stronie problemów projektu GitHub."
|
||||
@@ -545,6 +545,10 @@ msgstr "Nazwa"
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Przejdź do subskrypcji, wpisując jej nazwę"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "New password"
|
||||
msgstr "Nowe hasło"
|
||||
@@ -578,6 +582,10 @@ msgstr "Nic nie znaleziono"
|
||||
msgid "Oldest first"
|
||||
msgstr "Najstarsze jako pierwsze"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Ups!"
|
||||
@@ -706,10 +714,18 @@ msgstr ""
|
||||
msgid "Save"
|
||||
msgstr "Zapisz"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll selected entry to the top of the page"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll smoothly when navigating between entries"
|
||||
msgstr "Przewijaj płynnie podczas nawigowania między wpisami"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
@@ -823,7 +839,7 @@ msgid "Success"
|
||||
msgstr "Sukces"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Swipe header to the right"
|
||||
msgid "Swipe header to the left"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/WelcomePage.tsx
|
||||
@@ -851,6 +867,10 @@ msgstr "URL kanału, który chcesz subskrybować. "
|
||||
msgid "Theme"
|
||||
msgstr "Motyw"
|
||||
|
||||
#: 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 ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "Przełącz stan odczytu bieżącego wpisu"
|
||||
|
||||
@@ -13,10 +13,6 @@ msgstr ""
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
msgid "{0} (in {1})"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr ""
|
||||
@@ -72,7 +68,7 @@ msgid "All"
|
||||
msgstr "Todos"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgid "Always"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
@@ -184,7 +180,7 @@ msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
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 ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
@@ -411,6 +407,10 @@ msgstr "ID"
|
||||
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 "Se não estiver vazio, uma expressão avaliada como 'true' ou 'false'. "
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "If you encounter an issue, please report it on the issues page of the GitHub project."
|
||||
msgstr "Se você encontrar um problema, informe-o na página de problemas do projeto GitHub."
|
||||
@@ -545,6 +545,10 @@ msgstr "Nome"
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Navegue até uma assinatura digitando seu nome"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "New password"
|
||||
msgstr "Nova senha"
|
||||
@@ -578,6 +582,10 @@ msgstr "Nada encontrado"
|
||||
msgid "Oldest first"
|
||||
msgstr "Mais antigo primeiro"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Opa!"
|
||||
@@ -706,10 +714,18 @@ msgstr ""
|
||||
msgid "Save"
|
||||
msgstr "Salvar"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll selected entry to the top of the page"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll smoothly when navigating between entries"
|
||||
msgstr "Rolar suavemente ao navegar entre as entradas"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
@@ -823,7 +839,7 @@ msgid "Success"
|
||||
msgstr "Sucesso"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Swipe header to the right"
|
||||
msgid "Swipe header to the left"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/WelcomePage.tsx
|
||||
@@ -851,6 +867,10 @@ msgstr "A URL do feed que você deseja assinar. "
|
||||
msgid "Theme"
|
||||
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 ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "Alternar o status de leitura da entrada atual"
|
||||
|
||||
@@ -13,10 +13,6 @@ msgstr ""
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
msgid "{0} (in {1})"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr ""
|
||||
@@ -72,7 +68,7 @@ msgid "All"
|
||||
msgstr "Все"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgid "Always"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
@@ -184,7 +180,7 @@ msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
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 ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
@@ -411,6 +407,10 @@ msgstr "идентификатор"
|
||||
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 "Если не пусто, выражение оценивается как «истина» или «ложь». "
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
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."
|
||||
@@ -545,6 +545,10 @@ msgstr "Имя"
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Перейдите к подписке, введя ее имя."
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "New password"
|
||||
msgstr "Новый пароль"
|
||||
@@ -578,6 +582,10 @@ msgstr "Ничего не найдено"
|
||||
msgid "Oldest first"
|
||||
msgstr "Сначала самые старые"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Ой!"
|
||||
@@ -706,10 +714,18 @@ msgstr ""
|
||||
msgid "Save"
|
||||
msgstr "Сохранить"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll selected entry to the top of the page"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll smoothly when navigating between entries"
|
||||
msgstr "Плавная прокрутка при переходе между записями"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
@@ -823,7 +839,7 @@ msgid "Success"
|
||||
msgstr "Успех"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Swipe header to the right"
|
||||
msgid "Swipe header to the left"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/WelcomePage.tsx
|
||||
@@ -851,6 +867,10 @@ msgstr "URL канала, на который вы хотите подписат
|
||||
msgid "Theme"
|
||||
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 ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "Переключить статус чтения текущей записи"
|
||||
|
||||
@@ -13,10 +13,6 @@ msgstr ""
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
msgid "{0} (in {1})"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr ""
|
||||
@@ -72,7 +68,7 @@ msgid "All"
|
||||
msgstr "Všetky"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgid "Always"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
@@ -184,7 +180,7 @@ msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
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 ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
@@ -411,6 +407,10 @@ msgstr ""
|
||||
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 "Ak nie je prázdny, výraz vyhodnotený ako 'pravda' alebo 'nepravda'. "
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "If you encounter an issue, please report it on the issues page of the GitHub project."
|
||||
msgstr "Ak narazíte na problém, nahláste ho na stránke problémov projektu GitHub."
|
||||
@@ -545,6 +545,10 @@ msgstr "Meno"
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Prejdite na predplatné zadaním jeho názvu"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "New password"
|
||||
msgstr "Nové heslo"
|
||||
@@ -578,6 +582,10 @@ msgstr "Nič sa nenašlo"
|
||||
msgid "Oldest first"
|
||||
msgstr "Najprv najstarší"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Ojoj!"
|
||||
@@ -706,10 +714,18 @@ msgstr ""
|
||||
msgid "Save"
|
||||
msgstr "Uložiť"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll selected entry to the top of the page"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll smoothly when navigating between entries"
|
||||
msgstr "Pri navigácii medzi položkami plynulo rolujte"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
@@ -823,7 +839,7 @@ msgid "Success"
|
||||
msgstr "Úspech"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Swipe header to the right"
|
||||
msgid "Swipe header to the left"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/WelcomePage.tsx
|
||||
@@ -851,6 +867,10 @@ msgstr "URL zdroja, na odber ktorého sa chcete prihlásiť. "
|
||||
msgid "Theme"
|
||||
msgstr "Téma"
|
||||
|
||||
#: 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 ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "Prepne stav čítania aktuálneho záznamu"
|
||||
|
||||
@@ -13,10 +13,6 @@ msgstr ""
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
msgid "{0} (in {1})"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr ""
|
||||
@@ -72,7 +68,7 @@ msgid "All"
|
||||
msgstr "Alla"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgid "Always"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
@@ -184,7 +180,7 @@ msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
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 ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
@@ -411,6 +407,10 @@ msgstr ""
|
||||
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 "Om det inte är tomt, ett uttryck som utvärderas till 'sant' eller 'falskt'. "
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "If you encounter an issue, please report it on the issues page of the GitHub project."
|
||||
msgstr "Om du stöter på ett problem, vänligen rapportera det på problemsidan för GitHub-projektet."
|
||||
@@ -545,6 +545,10 @@ msgstr "Namn"
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Navigera till ett abonnemang genom att ange dess namn"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "New password"
|
||||
msgstr "Nytt lösenord"
|
||||
@@ -578,6 +582,10 @@ msgstr "Inget hittades"
|
||||
msgid "Oldest first"
|
||||
msgstr "Äldst först"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Hoppsan!"
|
||||
@@ -706,10 +714,18 @@ msgstr ""
|
||||
msgid "Save"
|
||||
msgstr "Spara"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll selected entry to the top of the page"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll smoothly when navigating between entries"
|
||||
msgstr "Bläddra mjukt när du navigerar mellan poster"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
@@ -823,7 +839,7 @@ msgid "Success"
|
||||
msgstr "Framgång"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Swipe header to the right"
|
||||
msgid "Swipe header to the left"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/WelcomePage.tsx
|
||||
@@ -851,6 +867,10 @@ msgstr "URL:en för flödet du vill prenumerera på. "
|
||||
msgid "Theme"
|
||||
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 ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "Växla lässtatus för aktuell post"
|
||||
|
||||
@@ -13,10 +13,6 @@ msgstr ""
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
msgid "{0} (in {1})"
|
||||
msgstr "{0} ({1} içinde)"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr "<0>CommaFeed açık kaynak kodlu bir proje. Kaynak kodları </0><1>GitHub</1>'da."
|
||||
@@ -72,8 +68,8 @@ msgid "All"
|
||||
msgstr "Tümü"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgstr "Seçilen girişi her zaman sayfanın üstüne kaydır, ekrana tamamen sığsa bile"
|
||||
msgid "Always"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "An email has been sent if this address was registered. Check your inbox."
|
||||
@@ -184,7 +180,7 @@ msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr "CommaFeed tarayıcı eklentisi sürüm {browserExtensionVersion}."
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
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 ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
@@ -411,6 +407,10 @@ msgstr "Kimlik"
|
||||
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 "Boş değilse, 'doğru' veya 'yanlış' olarak değerlendirilen bir ifade. "
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "If you encounter an issue, please report it on the issues page of the GitHub project."
|
||||
msgstr "Bir sorunla karşılaşırsanız lütfen GitHub projesinin sorunlar sayfasında bildirin."
|
||||
@@ -545,6 +545,10 @@ msgstr "İsim"
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Adını girerek bir aboneliğe gidin"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "New password"
|
||||
msgstr "Yeni şifre"
|
||||
@@ -578,6 +582,10 @@ msgstr "Hiçbir şey bulunamadı"
|
||||
msgid "Oldest first"
|
||||
msgstr "Önce en eski"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Hata!"
|
||||
@@ -706,10 +714,18 @@ msgstr "Sağ tık"
|
||||
msgid "Save"
|
||||
msgstr "Kaydet"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll selected entry to the top of the page"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll smoothly when navigating between entries"
|
||||
msgstr "Girişler arasında gezinirken sorunsuz ilerleyin"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
@@ -823,8 +839,8 @@ msgid "Success"
|
||||
msgstr "Başarı"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Swipe header to the right"
|
||||
msgstr "Başlığı sağa kaydır"
|
||||
msgid "Swipe header to the left"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/WelcomePage.tsx
|
||||
msgid "Switch to dark theme"
|
||||
@@ -851,6 +867,10 @@ msgstr "Abone olmak istediğiniz beslemenin URL'si. "
|
||||
msgid "Theme"
|
||||
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 ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "Geçerli girişin okuma durumunu değiştir"
|
||||
|
||||
@@ -13,10 +13,6 @@ msgstr ""
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
msgid "{0} (in {1})"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr ""
|
||||
@@ -72,7 +68,7 @@ msgid "All"
|
||||
msgstr "全部"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgid "Always"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
@@ -184,7 +180,7 @@ msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
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 ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
@@ -411,6 +407,10 @@ msgstr "身份证"
|
||||
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 "如果不为空,则表达式评估为“真”或“假”。"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "If the entry doesn't entirely fit on the screen"
|
||||
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项目的issues页面上报告。"
|
||||
@@ -545,6 +545,10 @@ msgstr "名称"
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "通过输入订阅名称导航到订阅"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "New password"
|
||||
msgstr "新密码"
|
||||
@@ -578,6 +582,10 @@ msgstr "没有找到"
|
||||
msgid "Oldest first"
|
||||
msgstr "最早的优先"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "哎呀!"
|
||||
@@ -706,10 +714,18 @@ msgstr ""
|
||||
msgid "Save"
|
||||
msgstr "保存"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll selected entry to the top of the page"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll smoothly when navigating between entries"
|
||||
msgstr "在条目之间导航时平滑滚动"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
@@ -823,7 +839,7 @@ msgid "Success"
|
||||
msgstr "成功"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Swipe header to the right"
|
||||
msgid "Swipe header to the left"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/WelcomePage.tsx
|
||||
@@ -851,6 +867,10 @@ msgstr "您要订阅的订阅源的 URL。"
|
||||
msgid "Theme"
|
||||
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 ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "切换当前条目的读取状态"
|
||||
|
||||
@@ -1,44 +1,39 @@
|
||||
import { Trans } from "@lingui/macro"
|
||||
import { Box, Button, Container, Group, type MantineTheme, Text, Title, useMantineTheme } from "@mantine/core"
|
||||
import { Box, Button, Container, Group, Text, Title } from "@mantine/core"
|
||||
import { TbRefresh } from "react-icons/tb"
|
||||
import { tss } from "tss"
|
||||
import { PageTitle } from "./PageTitle"
|
||||
|
||||
const useStyles = tss
|
||||
.withParams<{
|
||||
theme: MantineTheme
|
||||
}>()
|
||||
.create(({ theme }) => ({
|
||||
root: {
|
||||
paddingTop: 80,
|
||||
},
|
||||
const useStyles = tss.create(({ theme }) => ({
|
||||
root: {
|
||||
paddingTop: 80,
|
||||
},
|
||||
|
||||
label: {
|
||||
textAlign: "center",
|
||||
fontWeight: "bold",
|
||||
fontSize: 120,
|
||||
lineHeight: 1,
|
||||
marginBottom: `calc(${theme.spacing.xl} * 1.5)`,
|
||||
color: theme.colors[theme.primaryColor][3],
|
||||
},
|
||||
label: {
|
||||
textAlign: "center",
|
||||
fontWeight: "bold",
|
||||
fontSize: 120,
|
||||
lineHeight: 1,
|
||||
marginBottom: `calc(${theme.spacing.xl} * 1.5)`,
|
||||
color: theme.colors[theme.primaryColor][3],
|
||||
},
|
||||
|
||||
title: {
|
||||
textAlign: "center",
|
||||
fontWeight: "bold",
|
||||
fontSize: 32,
|
||||
},
|
||||
title: {
|
||||
textAlign: "center",
|
||||
fontWeight: "bold",
|
||||
fontSize: 32,
|
||||
},
|
||||
|
||||
description: {
|
||||
maxWidth: 540,
|
||||
margin: "auto",
|
||||
marginTop: theme.spacing.xl,
|
||||
marginBottom: `calc(${theme.spacing.xl} * 1.5)`,
|
||||
},
|
||||
}))
|
||||
description: {
|
||||
maxWidth: 540,
|
||||
margin: "auto",
|
||||
marginTop: theme.spacing.xl,
|
||||
marginBottom: `calc(${theme.spacing.xl} * 1.5)`,
|
||||
},
|
||||
}))
|
||||
|
||||
export function ErrorPage(props: { error: Error }) {
|
||||
const theme = useMantineTheme()
|
||||
const { classes } = useStyles({ theme })
|
||||
const { classes } = useStyles()
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
|
||||
@@ -3,8 +3,8 @@ import { Anchor, Box, Center, Container, Divider, Group, Image, Space, Title, us
|
||||
import { client } from "app/client"
|
||||
import { redirectToApiDocumentation, redirectToLogin, redirectToRegistration, redirectToRootCategory } from "app/redirect/thunks"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import welcome_page_dark from "assets/welcome_page_dark.png"
|
||||
import welcome_page_light from "assets/welcome_page_light.png"
|
||||
import welcomePageDark from "assets/welcome_page_dark.png"
|
||||
import welcomePageLight from "assets/welcome_page_light.png"
|
||||
import { ActionButton } from "components/ActionButton"
|
||||
import { useBrowserExtension } from "hooks/useBrowserExtension"
|
||||
import { useMobile } from "hooks/useMobile"
|
||||
@@ -19,7 +19,7 @@ export function WelcomePage() {
|
||||
const serverInfos = useAppSelector(state => state.server.serverInfos)
|
||||
const { colorScheme } = useMantineColorScheme()
|
||||
const dispatch = useAppDispatch()
|
||||
const image = colorScheme === "light" ? welcome_page_light : welcome_page_dark
|
||||
const image = colorScheme === "light" ? welcomePageLight : welcomePageDark
|
||||
|
||||
const login = useAsyncCallback(client.user.login, {
|
||||
onSuccess: () => {
|
||||
|
||||
@@ -108,7 +108,7 @@ export function AdminUsersPage() {
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{users?.map(u => (
|
||||
{users.map(u => (
|
||||
<Table.Tr key={u.id}>
|
||||
<Table.Td>{u.id}</Table.Td>
|
||||
<Table.Td>{u.name}</Table.Td>
|
||||
|
||||
@@ -14,7 +14,7 @@ const shownMeters: Record<string, string> = {
|
||||
"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.service.DatabaseCleaningService.entriesDeleted": "Entries deleted",
|
||||
"com.commafeed.backend.service.db.DatabaseCleaningService.entriesDeleted": "Entries deleted",
|
||||
}
|
||||
|
||||
const shownGauges: Record<string, string> = {
|
||||
|
||||
@@ -3,7 +3,9 @@ import { HistoryService, RedocStandalone } from "redoc"
|
||||
|
||||
// disable redoc url sync because it causes issues with hashrouter
|
||||
Object.defineProperty(HistoryService.prototype, "replace", {
|
||||
value: () => {},
|
||||
value: () => {
|
||||
// do nothing
|
||||
},
|
||||
})
|
||||
|
||||
function ApiDocumentationPage() {
|
||||
|
||||
@@ -49,11 +49,17 @@ export function FeedEntriesPage(props: FeedEntriesPageProps) {
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const titleClicked = () => {
|
||||
if (props.sourceType === "category") {
|
||||
dispatch(redirectToCategoryDetails(id))
|
||||
} else if (props.sourceType === "feed") {
|
||||
dispatch(redirectToFeedDetails(id))
|
||||
} else if (props.sourceType === "tag") dispatch(redirectToTagDetails(id))
|
||||
switch (props.sourceType) {
|
||||
case "category":
|
||||
dispatch(redirectToCategoryDetails(id))
|
||||
break
|
||||
case "feed":
|
||||
dispatch(redirectToFeedDetails(id))
|
||||
break
|
||||
case "tag":
|
||||
dispatch(redirectToTagDetails(id))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@@ -72,7 +78,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 - Constants.layout.headerHeight - 210}>
|
||||
<Box mb={viewport.height * 0.75}>
|
||||
<Group gap="xl">
|
||||
{sourceWebsiteUrl && (
|
||||
<a href={sourceWebsiteUrl} target="_blank" rel="noreferrer" className={classes.sourceWebsiteLink}>
|
||||
|
||||
@@ -13,12 +13,15 @@ import { Logo } from "components/Logo"
|
||||
import { OnDesktop } from "components/responsive/OnDesktop"
|
||||
import { OnMobile } from "components/responsive/OnMobile"
|
||||
import { useAppLoading } from "hooks/useAppLoading"
|
||||
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 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"
|
||||
|
||||
@@ -59,6 +62,8 @@ const useStyles = tss
|
||||
|
||||
export default function Layout(props: LayoutProps) {
|
||||
const theme = useMantineTheme()
|
||||
const mobile = useMobile()
|
||||
const { isBrowserExtensionPopup } = useBrowserExtension()
|
||||
const [sidebarWidth, setSidebarWidth] = useLocalStorage("sidebar-width", 350)
|
||||
const sidebarPadding = theme.spacing.xs
|
||||
const { classes } = useStyles({
|
||||
@@ -70,6 +75,8 @@ export default function Layout(props: LayoutProps) {
|
||||
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()
|
||||
|
||||
@@ -111,81 +118,100 @@ export default function Layout(props: LayoutProps) {
|
||||
</ActionIcon>
|
||||
)
|
||||
|
||||
if (loading) return <LoadingPage />
|
||||
return (
|
||||
<AppShell
|
||||
header={{ height: Constants.layout.headerHeight }}
|
||||
navbar={{
|
||||
width: sidebarWidth,
|
||||
breakpoint: Constants.layout.mobileBreakpoint,
|
||||
collapsed: { mobile: !mobileMenuOpen, desktop: !props.sidebarVisible },
|
||||
}}
|
||||
padding={{ base: 6, [Constants.layout.mobileBreakpointName]: "md" }}
|
||||
>
|
||||
<AppShell.Header id="header">
|
||||
<OnMobile>
|
||||
{mobileMenuOpen && (
|
||||
<Group justify="space-between" p="md">
|
||||
<Box>{burger}</Box>
|
||||
<Box>
|
||||
<LogoAndTitle />
|
||||
</Box>
|
||||
<Box>{addButton}</Box>
|
||||
</Group>
|
||||
)}
|
||||
{!mobileMenuOpen && (
|
||||
<Group p="md">
|
||||
<Box>{burger}</Box>
|
||||
<Box style={{ flexGrow: 1 }}>{props.header}</Box>
|
||||
</Group>
|
||||
)}
|
||||
</OnMobile>
|
||||
<OnDesktop>
|
||||
const header = (
|
||||
<>
|
||||
<OnMobile>
|
||||
{mobileMenuOpen && (
|
||||
<Group justify="space-between" p="md">
|
||||
<Box>{burger}</Box>
|
||||
<Box>
|
||||
<LogoAndTitle />
|
||||
</Box>
|
||||
<Box>{addButton}</Box>
|
||||
</Group>
|
||||
)}
|
||||
{!mobileMenuOpen && (
|
||||
<Group p="md">
|
||||
<Group justify="space-between" style={{ width: sidebarWidth - 16 }}>
|
||||
<Box>
|
||||
<LogoAndTitle />
|
||||
</Box>
|
||||
<Box>{addButton}</Box>
|
||||
</Group>
|
||||
<Box>{burger}</Box>
|
||||
<Box style={{ flexGrow: 1 }}>{props.header}</Box>
|
||||
</Group>
|
||||
</OnDesktop>
|
||||
</AppShell.Header>
|
||||
<AppShell.Navbar id="sidebar" p={sidebarPadding}>
|
||||
<AppShell.Section grow component={ScrollArea} mx="-sm" px="sm">
|
||||
<Box className={classes.sidebarContent}>{props.sidebar}</Box>
|
||||
</AppShell.Section>
|
||||
</AppShell.Navbar>
|
||||
<Draggable
|
||||
axis="x"
|
||||
defaultPosition={{
|
||||
x: sidebarWidth,
|
||||
y: Constants.layout.headerHeight,
|
||||
}}
|
||||
bounds={{
|
||||
left: 120,
|
||||
right: 1000,
|
||||
}}
|
||||
grid={[30, 30]}
|
||||
onDrag={(_e, data) => setSidebarWidth(data.x)}
|
||||
>
|
||||
<Box
|
||||
style={{
|
||||
position: "fixed",
|
||||
height: "100%",
|
||||
width: "10px",
|
||||
cursor: "ew-resize",
|
||||
}}
|
||||
></Box>
|
||||
</Draggable>
|
||||
)}
|
||||
</OnMobile>
|
||||
<OnDesktop>
|
||||
<Group p="md">
|
||||
<Group justify="space-between" style={{ width: sidebarWidth - 16 }}>
|
||||
<Box>
|
||||
<LogoAndTitle />
|
||||
</Box>
|
||||
<Box>{addButton}</Box>
|
||||
</Group>
|
||||
<Box style={{ flexGrow: 1 }}>{props.header}</Box>
|
||||
</Group>
|
||||
</OnDesktop>
|
||||
</>
|
||||
)
|
||||
|
||||
<AppShell.Main id="content">
|
||||
<Suspense fallback={<Loader />}>
|
||||
<AnnouncementDialog />
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</AppShell.Main>
|
||||
</AppShell>
|
||||
const swipeHandlers = useSwipeable({
|
||||
onSwiping: e => {
|
||||
const threshold = document.documentElement.clientWidth / 6
|
||||
if (e.absX > threshold) {
|
||||
dispatch(setMobileMenuOpen(e.dir === "Right"))
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
if (loading) return <LoadingPage />
|
||||
return (
|
||||
<Box {...swipeHandlers}>
|
||||
<AppShell
|
||||
header={{ height: Constants.layout.headerHeight, collapsed: headerInFooter }}
|
||||
footer={{ height: Constants.layout.headerHeight, collapsed: !headerInFooter }}
|
||||
navbar={{
|
||||
width: sidebarWidth,
|
||||
breakpoint: Constants.layout.mobileBreakpoint,
|
||||
collapsed: { mobile: !mobileMenuOpen, desktop: !props.sidebarVisible },
|
||||
}}
|
||||
padding={{ base: 6, [Constants.layout.mobileBreakpointName]: "md" }}
|
||||
>
|
||||
<AppShell.Header id={Constants.dom.headerId}>{!headerInFooter && header}</AppShell.Header>
|
||||
<AppShell.Footer id={Constants.dom.footerId}>{headerInFooter && header}</AppShell.Footer>
|
||||
<AppShell.Navbar id="sidebar" p={sidebarPadding}>
|
||||
<AppShell.Section grow component={ScrollArea} mx="-sm" px="sm">
|
||||
<Box className={classes.sidebarContent}>{props.sidebar}</Box>
|
||||
</AppShell.Section>
|
||||
</AppShell.Navbar>
|
||||
<OnDesktop>
|
||||
<Draggable
|
||||
axis="x"
|
||||
defaultPosition={{
|
||||
x: sidebarWidth,
|
||||
y: 0,
|
||||
}}
|
||||
bounds={{
|
||||
left: 120,
|
||||
right: 1000,
|
||||
}}
|
||||
grid={[30, 30]}
|
||||
onDrag={(_e, data) => setSidebarWidth(data.x)}
|
||||
>
|
||||
<Box
|
||||
style={{
|
||||
position: "fixed",
|
||||
height: "100%",
|
||||
width: "10px",
|
||||
cursor: "ew-resize",
|
||||
}}
|
||||
></Box>
|
||||
</Draggable>
|
||||
</OnDesktop>
|
||||
|
||||
<AppShell.Main id="content">
|
||||
<Suspense fallback={<Loader />}>
|
||||
<AnnouncementDialog />
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</AppShell.Main>
|
||||
</AppShell>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { useMantineTheme } from "@mantine/core"
|
||||
import { useColorScheme } from "hooks/useColorScheme"
|
||||
import { createTss } from "tss-react"
|
||||
|
||||
const useContext = () => {
|
||||
// return anything here that will be accessible in tss.create()
|
||||
// we don't need anything right now
|
||||
return {}
|
||||
|
||||
const theme = useMantineTheme()
|
||||
const colorScheme = useColorScheme()
|
||||
|
||||
return { theme, colorScheme }
|
||||
}
|
||||
|
||||
export const { tss } = createTss({ useContext })
|
||||
|
||||
export const useStyles = tss.create({})
|
||||
|
||||
@@ -6,7 +6,7 @@ import eslint from "vite-plugin-eslint"
|
||||
import tsconfigPaths from "vite-tsconfig-paths"
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
export default defineConfig(env => ({
|
||||
plugins: [
|
||||
react({
|
||||
babel: {
|
||||
@@ -15,7 +15,8 @@ export default defineConfig({
|
||||
},
|
||||
}),
|
||||
lingui(),
|
||||
eslint(),
|
||||
// https://github.com/vitest-dev/vitest/issues/4055#issuecomment-1732994672
|
||||
env.mode !== "test" && eslint(),
|
||||
tsconfigPaths(),
|
||||
visualizer(),
|
||||
],
|
||||
@@ -29,10 +30,11 @@ export default defineConfig({
|
||||
"/openapi.json": "http://localhost:8083",
|
||||
"/custom_css.css": "http://localhost:8083",
|
||||
"/custom_js.js": "http://localhost:8083",
|
||||
"/logout": "http://localhost:8083",
|
||||
},
|
||||
},
|
||||
build: {
|
||||
chunkSizeWarningLimit: 3000,
|
||||
chunkSizeWarningLimit: 3500,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: id => {
|
||||
@@ -44,4 +46,4 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}))
|
||||
|
||||
@@ -67,6 +67,9 @@ app:
|
||||
# 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
|
||||
|
||||
@@ -92,11 +95,11 @@ app:
|
||||
# -------------------
|
||||
# for MariaDB
|
||||
# driverClass is org.mariadb.jdbc.Driver
|
||||
# url is jdbc:mariadb://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true
|
||||
# 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
|
||||
# url is jdbc:mysql://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC
|
||||
#
|
||||
# for PostgreSQL
|
||||
# driverClass is org.postgresql.Driver
|
||||
|
||||
@@ -67,6 +67,9 @@ app:
|
||||
# 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
|
||||
|
||||
@@ -92,11 +95,11 @@ app:
|
||||
# -------------------
|
||||
# for MariaDB
|
||||
# driverClass is org.mariadb.jdbc.Driver
|
||||
# url is jdbc:mariadb://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true
|
||||
# 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
|
||||
# url is jdbc:mysql://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC
|
||||
#
|
||||
# for PostgreSQL
|
||||
# driverClass is org.postgresql.Driver
|
||||
|
||||
@@ -12,8 +12,8 @@ services:
|
||||
postgresql:
|
||||
image: postgres
|
||||
environment:
|
||||
POSTGRES_USER: root
|
||||
POSTGRES_PASSWORD: root
|
||||
POSTGRES_DB: commafeed
|
||||
- POSTGRES_USER=root
|
||||
- POSTGRES_PASSWORD=root
|
||||
- POSTGRES_DB=commafeed
|
||||
ports:
|
||||
- 5432:5432
|
||||
|
||||
@@ -6,14 +6,14 @@
|
||||
<parent>
|
||||
<groupId>com.commafeed</groupId>
|
||||
<artifactId>commafeed</artifactId>
|
||||
<version>4.0.0</version>
|
||||
<version>4.3.3</version>
|
||||
</parent>
|
||||
<artifactId>commafeed-server</artifactId>
|
||||
<name>CommaFeed Server</name>
|
||||
|
||||
<properties>
|
||||
<guice.version>7.0.0</guice.version>
|
||||
<querydsl.version>5.0.0</querydsl.version>
|
||||
<querydsl.version>5.1.0</querydsl.version>
|
||||
<rome.version>2.1.0</rome.version>
|
||||
</properties>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-dependencies</artifactId>
|
||||
<version>4.0.5</version>
|
||||
<version>4.0.6</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
@@ -31,23 +31,17 @@
|
||||
|
||||
<build>
|
||||
<finalName>commafeed</finalName>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>src/main/resources</directory>
|
||||
<filtering>true</filtering>
|
||||
</resource>
|
||||
</resources>
|
||||
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.2.3</version>
|
||||
<version>3.2.5</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-failsafe-plugin</artifactId>
|
||||
<version>3.2.3</version>
|
||||
<version>3.2.5</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
@@ -69,7 +63,8 @@
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<generateGitPropertiesFile>false</generateGitPropertiesFile>
|
||||
<generateGitPropertiesFile>true</generateGitPropertiesFile>
|
||||
<generateGitPropertiesFilename>${project.build.outputDirectory}/git.properties</generateGitPropertiesFilename>
|
||||
<failOnNoGitDirectory>false</failOnNoGitDirectory>
|
||||
<failOnUnableToExtractRepoInfo>false</failOnUnableToExtractRepoInfo>
|
||||
</configuration>
|
||||
@@ -77,7 +72,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.5.1</version>
|
||||
<version>3.5.2</version>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.kordamp.shade</groupId>
|
||||
@@ -126,7 +121,7 @@
|
||||
<plugin>
|
||||
<groupId>io.swagger.core.v3</groupId>
|
||||
<artifactId>swagger-maven-plugin-jakarta</artifactId>
|
||||
<version>2.2.19</version>
|
||||
<version>2.2.20</version>
|
||||
<?m2e ignore?>
|
||||
<configuration>
|
||||
<outputPath>${project.build.directory}/classes/assets</outputPath>
|
||||
@@ -189,7 +184,7 @@
|
||||
<plugin>
|
||||
<groupId>com.diffplug.spotless</groupId>
|
||||
<artifactId>spotless-maven-plugin</artifactId>
|
||||
<version>2.41.1</version>
|
||||
<version>2.43.0</version>
|
||||
<?m2e ignore?>
|
||||
<executions>
|
||||
<execution>
|
||||
@@ -216,7 +211,7 @@
|
||||
<dependency>
|
||||
<groupId>com.commafeed</groupId>
|
||||
<artifactId>commafeed-client</artifactId>
|
||||
<version>4.0.0</version>
|
||||
<version>4.3.3</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -244,6 +239,10 @@
|
||||
<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>
|
||||
@@ -268,21 +267,20 @@
|
||||
<groupId>io.dropwizard.metrics</groupId>
|
||||
<artifactId>metrics-json</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>be.tomcools</groupId>
|
||||
<artifactId>dropwizard-websocket-jsr356-bundle</artifactId>
|
||||
<version>4.0.0</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.19</version>
|
||||
<version>2.2.20</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -338,7 +336,7 @@
|
||||
<dependency>
|
||||
<groupId>redis.clients</groupId>
|
||||
<artifactId>jedis</artifactId>
|
||||
<version>5.1.0</version>
|
||||
<version>5.1.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.sun.mail</groupId>
|
||||
@@ -370,7 +368,7 @@
|
||||
<dependency>
|
||||
<groupId>org.jsoup</groupId>
|
||||
<artifactId>jsoup</artifactId>
|
||||
<version>1.17.1</version>
|
||||
<version>1.17.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.ibm.icu</groupId>
|
||||
@@ -390,7 +388,7 @@
|
||||
<dependency>
|
||||
<groupId>org.gwtproject</groupId>
|
||||
<artifactId>gwt-servlet</artifactId>
|
||||
<version>2.10.0</version>
|
||||
<version>2.11.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents.client5</groupId>
|
||||
@@ -399,35 +397,39 @@
|
||||
<dependency>
|
||||
<groupId>io.github.hakky54</groupId>
|
||||
<artifactId>sslcontext-kickstart-for-apache5</artifactId>
|
||||
<version>8.2.0</version>
|
||||
<version>8.3.2</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.google.apis</groupId>
|
||||
<artifactId>google-api-services-youtube</artifactId>
|
||||
<version>v3-rev20231011-2.0.0</version>
|
||||
<version>v3-rev20240225-2.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<!-- stay on 2.1 because 2.2 file format changed to version '3' -->
|
||||
<version>2.1.214</version><!--$NO-MVN-MAN-VER$ -->
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.manticore-projects.tools</groupId>
|
||||
<artifactId>h2migrationtool</artifactId>
|
||||
<version>1.4</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<version>8.2.0</version>
|
||||
<version>8.3.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mariadb.jdbc</groupId>
|
||||
<artifactId>mariadb-java-client</artifactId>
|
||||
<version>3.3.1</version>
|
||||
<version>3.3.3</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<version>42.7.1</version>
|
||||
<version>42.7.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.sourceforge.jtds</groupId>
|
||||
@@ -488,7 +490,7 @@
|
||||
<dependency>
|
||||
<groupId>com.microsoft.playwright</groupId>
|
||||
<artifactId>playwright</artifactId>
|
||||
<version>1.40.0</version>
|
||||
<version>1.41.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
package com.commafeed;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Date;
|
||||
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;
|
||||
@@ -22,8 +26,9 @@ 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.DatabaseStartupService;
|
||||
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;
|
||||
@@ -42,19 +47,21 @@ 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 be.tomcools.dropwizard.websocket.WebsocketBundle;
|
||||
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;
|
||||
@@ -76,10 +83,9 @@ public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
|
||||
public static final String USERNAME_ADMIN = "admin";
|
||||
public static final String USERNAME_DEMO = "demo";
|
||||
|
||||
public static final Date STARTUP_TIME = new Date();
|
||||
public static final Instant STARTUP_TIME = Instant.now();
|
||||
|
||||
private HibernateBundle<CommaFeedConfiguration> hibernateBundle;
|
||||
private WebsocketBundle<CommaFeedConfiguration> websocketBundle;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
@@ -89,10 +95,32 @@ public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
|
||||
@Override
|
||||
public void initialize(Bootstrap<CommaFeedConfiguration> bootstrap) {
|
||||
configureEnvironmentSubstitutor(bootstrap);
|
||||
configureObjectMapper(bootstrap.getObjectMapper());
|
||||
|
||||
bootstrap.getObjectMapper().registerModule(new MetricsModule(TimeUnit.SECONDS, TimeUnit.SECONDS, false));
|
||||
// run h2 migration as the first bundle because we need to migrate before hibernate is initialized
|
||||
bootstrap.addBundle(new ConfiguredBundle<CommaFeedConfiguration>() {
|
||||
@Override
|
||||
public void run(CommaFeedConfiguration config, Environment environment) throws Exception {
|
||||
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(websocketBundle = new WebsocketBundle<>());
|
||||
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) {
|
||||
@@ -134,6 +162,15 @@ public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
|
||||
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
|
||||
@@ -156,7 +193,9 @@ public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
|
||||
environment.servlets().setSessionHandler(config.getSessionHandlerFactory().build(config.getDataSourceFactory()));
|
||||
|
||||
// support for "@SecurityCheck User user" injection
|
||||
environment.jersey().register(new SecurityCheckFactoryProvider.Binder(injector.getInstance(UserService.class)));
|
||||
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());
|
||||
|
||||
@@ -182,10 +221,13 @@ public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
|
||||
}
|
||||
|
||||
// WebSocket endpoint
|
||||
ServerEndpointConfig serverEndpointConfig = ServerEndpointConfig.Builder.create(WebSocketEndpoint.class, "/ws")
|
||||
.configurator(injector.getInstance(WebSocketConfigurator.class))
|
||||
.build();
|
||||
websocketBundle.addEndpoint(serverEndpointConfig);
|
||||
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<>() {
|
||||
@@ -208,6 +250,12 @@ public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
|
||||
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
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
package com.commafeed;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
import org.apache.commons.lang3.time.DateUtils;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Properties;
|
||||
|
||||
import com.commafeed.backend.cache.RedisPoolFactory;
|
||||
import com.commafeed.frontend.session.SessionHandlerFactory;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import be.tomcools.dropwizard.websocket.WebsocketBundleConfiguration;
|
||||
import be.tomcools.dropwizard.websocket.WebsocketConfiguration;
|
||||
import io.dropwizard.core.Configuration;
|
||||
import io.dropwizard.db.DataSourceFactory;
|
||||
import io.dropwizard.util.Duration;
|
||||
@@ -24,7 +23,7 @@ import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class CommaFeedConfiguration extends Configuration implements WebsocketBundleConfiguration {
|
||||
public class CommaFeedConfiguration extends Configuration {
|
||||
|
||||
public enum CacheType {
|
||||
NOOP, REDIS
|
||||
@@ -54,17 +53,17 @@ public class CommaFeedConfiguration extends Configuration implements WebsocketBu
|
||||
private final String gitCommit;
|
||||
|
||||
public CommaFeedConfiguration() {
|
||||
ResourceBundle bundle = ResourceBundle.getBundle("application");
|
||||
Properties properties = new Properties();
|
||||
try (InputStream stream = getClass().getResourceAsStream("/git.properties")) {
|
||||
if (stream != null) {
|
||||
properties.load(stream);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
this.version = bundle.getString("version");
|
||||
this.gitCommit = bundle.getString("git.commit");
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebsocketConfiguration getWebsocketConfiguration() {
|
||||
WebsocketConfiguration config = new WebsocketConfiguration();
|
||||
config.setMaxSessionIdleTimeout(getApplicationSettings().getWebsocketPingInterval().toMilliseconds() + 10000);
|
||||
return config;
|
||||
this.version = properties.getProperty("git.build.version", "unknown");
|
||||
this.gitCommit = properties.getProperty("git.commit.id.abbrev", "unknown");
|
||||
}
|
||||
|
||||
@Getter
|
||||
@@ -146,6 +145,11 @@ public class CommaFeedConfiguration extends Configuration implements WebsocketBu
|
||||
@Valid
|
||||
private Integer maxFeedCapacity;
|
||||
|
||||
@NotNull
|
||||
@Min(0)
|
||||
@Valid
|
||||
private Integer maxEntriesAgeDays = 0;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
private Integer maxFeedsPerUser = 0;
|
||||
@@ -170,9 +174,8 @@ public class CommaFeedConfiguration extends Configuration implements WebsocketBu
|
||||
|
||||
private Duration treeReloadInterval = Duration.seconds(30);
|
||||
|
||||
public Date getUnreadThreshold() {
|
||||
int keepStatusDays = getKeepStatusDays();
|
||||
return keepStatusDays > 0 ? DateUtils.addDays(new Date(), -1 * keepStatusDays) : null;
|
||||
public Instant getUnreadThreshold() {
|
||||
return getKeepStatusDays() > 0 ? Instant.now().minus(getKeepStatusDays(), ChronoUnit.DAYS) : null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ 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;
|
||||
@@ -66,6 +67,7 @@ public class CommaFeedModule extends AbstractModule {
|
||||
|
||||
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);
|
||||
|
||||
@@ -10,14 +10,14 @@ import java.util.List;
|
||||
*
|
||||
*
|
||||
*/
|
||||
public class FixedSizeSortedSet<E> {
|
||||
public class FixedSizeSortedList<E> {
|
||||
|
||||
private final List<E> inner;
|
||||
|
||||
private final Comparator<? super E> comparator;
|
||||
private final int capacity;
|
||||
|
||||
public FixedSizeSortedSet(int capacity, Comparator<? super E> comparator) {
|
||||
public FixedSizeSortedList(int capacity, Comparator<? super E> comparator) {
|
||||
this.inner = new ArrayList<>(Math.max(0, capacity));
|
||||
this.capacity = capacity < 0 ? Integer.MAX_VALUE : capacity;
|
||||
this.comparator = comparator;
|
||||
@@ -31,7 +31,6 @@ import com.commafeed.CommaFeedConfiguration;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.net.HttpHeaders;
|
||||
|
||||
import io.dropwizard.lifecycle.Managed;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import lombok.Getter;
|
||||
@@ -44,7 +43,7 @@ import nl.altindag.ssl.apache5.util.Apache5SslUtils;
|
||||
*
|
||||
*/
|
||||
@Singleton
|
||||
public class HttpGetter implements Managed {
|
||||
public class HttpGetter {
|
||||
|
||||
private final CloseableHttpClient client;
|
||||
|
||||
@@ -154,11 +153,6 @@ public class HttpGetter implements Managed {
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() throws Exception {
|
||||
client.close();
|
||||
}
|
||||
|
||||
@Getter
|
||||
public static class NotModifiedException extends Exception {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package com.commafeed.backend.cache;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.commons.codec.digest.DigestUtils;
|
||||
|
||||
import com.commafeed.backend.feed.parser.FeedParserResult.Entry;
|
||||
import com.commafeed.backend.model.Feed;
|
||||
import com.commafeed.backend.model.FeedEntry;
|
||||
import com.commafeed.backend.model.FeedSubscription;
|
||||
import com.commafeed.backend.model.User;
|
||||
import com.commafeed.frontend.model.Category;
|
||||
@@ -14,12 +15,12 @@ import com.commafeed.frontend.model.UnreadCount;
|
||||
public abstract class CacheService {
|
||||
|
||||
// feed entries for faster refresh
|
||||
public abstract List<String> getLastEntries(Feed feed);
|
||||
public abstract Set<String> getLastEntries(Feed feed);
|
||||
|
||||
public abstract void setLastEntries(Feed feed, List<String> entries);
|
||||
|
||||
public String buildUniqueEntryKey(Feed feed, FeedEntry entry) {
|
||||
return DigestUtils.sha1Hex(entry.getGuid() + entry.getUrl());
|
||||
public String buildUniqueEntryKey(Entry entry) {
|
||||
return DigestUtils.sha1Hex(entry.guid() + entry.url());
|
||||
}
|
||||
|
||||
// user categories
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.commafeed.backend.cache;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import com.commafeed.backend.model.Feed;
|
||||
import com.commafeed.backend.model.FeedSubscription;
|
||||
@@ -12,8 +13,8 @@ import com.commafeed.frontend.model.UnreadCount;
|
||||
public class NoopCacheService extends CacheService {
|
||||
|
||||
@Override
|
||||
public List<String> getLastEntries(Feed feed) {
|
||||
return Collections.emptyList();
|
||||
public Set<String> getLastEntries(Feed feed) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.commafeed.backend.cache;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -13,6 +12,7 @@ import com.commafeed.frontend.model.Category;
|
||||
import com.commafeed.frontend.model.UnreadCount;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -24,19 +24,16 @@ import redis.clients.jedis.Pipeline;
|
||||
@RequiredArgsConstructor
|
||||
public class RedisCacheService extends CacheService {
|
||||
|
||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||
private static final ObjectMapper MAPPER = new ObjectMapper().registerModule(new JavaTimeModule());
|
||||
|
||||
private final JedisPool pool;
|
||||
|
||||
@Override
|
||||
public List<String> getLastEntries(Feed feed) {
|
||||
List<String> list = new ArrayList<>();
|
||||
public Set<String> getLastEntries(Feed feed) {
|
||||
try (Jedis jedis = pool.getResource()) {
|
||||
String key = buildRedisEntryKey(feed);
|
||||
Set<String> members = jedis.smembers(key);
|
||||
list.addAll(members);
|
||||
return jedis.smembers(key);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
package com.commafeed.backend.dao;
|
||||
|
||||
import java.util.Date;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.codec.digest.DigestUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.hibernate.SessionFactory;
|
||||
|
||||
import com.commafeed.backend.model.Feed;
|
||||
import com.commafeed.backend.model.QFeed;
|
||||
import com.commafeed.backend.model.QFeedSubscription;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.querydsl.jpa.JPAExpressions;
|
||||
import com.querydsl.jpa.impl.JPAQuery;
|
||||
|
||||
@@ -28,8 +26,8 @@ public class FeedDAO extends GenericDAO<Feed> {
|
||||
super(sessionFactory);
|
||||
}
|
||||
|
||||
public List<Feed> findNextUpdatable(int count, Date lastLoginThreshold) {
|
||||
JPAQuery<Feed> query = query().selectFrom(feed).where(feed.disabledUntil.isNull().or(feed.disabledUntil.lt(new Date())));
|
||||
public List<Feed> findNextUpdatable(int count, Instant lastLoginThreshold) {
|
||||
JPAQuery<Feed> query = query().selectFrom(feed).where(feed.disabledUntil.isNull().or(feed.disabledUntil.lt(Instant.now())));
|
||||
if (lastLoginThreshold != null) {
|
||||
query.where(JPAExpressions.selectOne()
|
||||
.from(subscription)
|
||||
@@ -41,17 +39,18 @@ public class FeedDAO extends GenericDAO<Feed> {
|
||||
return query.orderBy(feed.disabledUntil.asc()).limit(count).fetch();
|
||||
}
|
||||
|
||||
public void setDisabledUntil(List<Long> feedIds, Date date) {
|
||||
public void setDisabledUntil(List<Long> feedIds, Instant date) {
|
||||
updateQuery(feed).set(feed.disabledUntil, date).where(feed.id.in(feedIds)).execute();
|
||||
}
|
||||
|
||||
public Feed findByUrl(String normalizedUrl) {
|
||||
List<Feed> feeds = query().selectFrom(feed).where(feed.normalizedUrlHash.eq(DigestUtils.sha1Hex(normalizedUrl))).fetch();
|
||||
Feed feed = Iterables.getFirst(feeds, null);
|
||||
if (feed != null && StringUtils.equals(normalizedUrl, feed.getNormalizedUrl())) {
|
||||
return feed;
|
||||
}
|
||||
return null;
|
||||
public Feed findByUrl(String normalizedUrl, String normalizedUrlHash) {
|
||||
return query().selectFrom(feed)
|
||||
.where(feed.normalizedUrlHash.eq(normalizedUrlHash))
|
||||
.fetch()
|
||||
.stream()
|
||||
.filter(f -> StringUtils.equals(normalizedUrl, f.getNormalizedUrl()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
public List<Feed> findWithoutSubscriptions(int max) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.commafeed.backend.dao;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.codec.digest.DigestUtils;
|
||||
import org.hibernate.SessionFactory;
|
||||
|
||||
import com.commafeed.backend.model.Feed;
|
||||
@@ -26,12 +26,8 @@ public class FeedEntryDAO extends GenericDAO<FeedEntry> {
|
||||
super(sessionFactory);
|
||||
}
|
||||
|
||||
public Long findExisting(String guid, Feed feed) {
|
||||
return query().select(entry.id)
|
||||
.from(entry)
|
||||
.where(entry.guidHash.eq(DigestUtils.sha1Hex(guid)), entry.feed.eq(feed))
|
||||
.limit(1)
|
||||
.fetchOne();
|
||||
public FeedEntry findExisting(String guidHash, Feed feed) {
|
||||
return query().select(entry).from(entry).where(entry.guidHash.eq(guidHash), entry.feed.eq(feed)).limit(1).fetchOne();
|
||||
}
|
||||
|
||||
public List<FeedCapacity> findFeedsExceedingCapacity(long maxCapacity, long max) {
|
||||
@@ -50,6 +46,17 @@ public class FeedEntryDAO extends GenericDAO<FeedEntry> {
|
||||
return delete(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete entries older than a certain date
|
||||
*/
|
||||
public int deleteEntriesOlderThan(Instant olderThan, long max) {
|
||||
List<FeedEntry> list = query().selectFrom(entry).where(entry.updated.lt(olderThan)).orderBy(entry.updated.asc()).limit(max).fetch();
|
||||
return delete(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the oldest entries of a feed
|
||||
*/
|
||||
public int deleteOldEntries(Long feedId, long max) {
|
||||
List<FeedEntry> list = query().selectFrom(entry).where(entry.feed.id.eq(feedId)).orderBy(entry.updated.asc()).limit(max).fetch();
|
||||
return delete(list);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.commafeed.backend.dao;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
@@ -10,7 +10,7 @@ import org.apache.commons.lang3.builder.CompareToBuilder;
|
||||
import org.hibernate.SessionFactory;
|
||||
|
||||
import com.commafeed.CommaFeedConfiguration;
|
||||
import com.commafeed.backend.FixedSizeSortedSet;
|
||||
import com.commafeed.backend.FixedSizeSortedList;
|
||||
import com.commafeed.backend.feed.FeedEntryKeyword;
|
||||
import com.commafeed.backend.feed.FeedEntryKeyword.Mode;
|
||||
import com.commafeed.backend.model.FeedEntry;
|
||||
@@ -73,8 +73,8 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
||||
|
||||
private FeedEntryStatus handleStatus(User user, FeedEntryStatus status, FeedSubscription sub, FeedEntry entry) {
|
||||
if (status == null) {
|
||||
Date unreadThreshold = config.getApplicationSettings().getUnreadThreshold();
|
||||
boolean read = unreadThreshold != null && entry.getUpdated().before(unreadThreshold);
|
||||
Instant unreadThreshold = config.getApplicationSettings().getUnreadThreshold();
|
||||
boolean read = unreadThreshold != null && entry.getUpdated().isBefore(unreadThreshold);
|
||||
status = new FeedEntryStatus(user, sub, entry);
|
||||
status.setRead(read);
|
||||
status.setMarkable(!read);
|
||||
@@ -90,7 +90,8 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
||||
return status;
|
||||
}
|
||||
|
||||
public List<FeedEntryStatus> findStarred(User user, Date newerThan, int offset, int limit, ReadingOrder order, boolean includeContent) {
|
||||
public List<FeedEntryStatus> findStarred(User user, Instant newerThan, int offset, int limit, ReadingOrder order,
|
||||
boolean includeContent) {
|
||||
JPAQuery<FeedEntryStatus> query = query().selectFrom(status).where(status.user.eq(user), status.starred.isTrue());
|
||||
if (newerThan != null) {
|
||||
query.where(status.entryInserted.gt(newerThan));
|
||||
@@ -114,7 +115,8 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
||||
}
|
||||
|
||||
private JPAQuery<FeedEntry> buildQuery(User user, FeedSubscription sub, boolean unreadOnly, List<FeedEntryKeyword> keywords,
|
||||
Date newerThan, int offset, int limit, ReadingOrder order, FeedEntryStatus last, String tag, Long minEntryId, Long maxEntryId) {
|
||||
Instant newerThan, int offset, int limit, ReadingOrder order, FeedEntryStatus last, String tag, Long minEntryId,
|
||||
Long maxEntryId) {
|
||||
|
||||
JPAQuery<FeedEntry> query = query().selectFrom(entry).where(entry.feed.eq(sub.getFeed()));
|
||||
|
||||
@@ -139,7 +141,7 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
||||
or.or(status.read.isFalse());
|
||||
query.where(or);
|
||||
|
||||
Date unreadThreshold = config.getApplicationSettings().getUnreadThreshold();
|
||||
Instant unreadThreshold = config.getApplicationSettings().getUnreadThreshold();
|
||||
if (unreadThreshold != null) {
|
||||
query.where(entry.updated.goe(unreadThreshold));
|
||||
}
|
||||
@@ -193,22 +195,22 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
||||
}
|
||||
|
||||
public List<FeedEntryStatus> findBySubscriptions(User user, List<FeedSubscription> subs, boolean unreadOnly,
|
||||
List<FeedEntryKeyword> keywords, Date newerThan, int offset, int limit, ReadingOrder order, boolean includeContent,
|
||||
List<FeedEntryKeyword> keywords, Instant newerThan, int offset, int limit, ReadingOrder order, boolean includeContent,
|
||||
boolean onlyIds, String tag, Long minEntryId, Long maxEntryId) {
|
||||
int capacity = offset + limit;
|
||||
|
||||
Comparator<FeedEntryStatus> comparator = order == ReadingOrder.desc ? STATUS_COMPARATOR_DESC : STATUS_COMPARATOR_ASC;
|
||||
|
||||
FixedSizeSortedSet<FeedEntryStatus> set = new FixedSizeSortedSet<>(capacity, comparator);
|
||||
FixedSizeSortedList<FeedEntryStatus> fssl = new FixedSizeSortedList<>(capacity, comparator);
|
||||
for (FeedSubscription sub : subs) {
|
||||
FeedEntryStatus last = (order != null && set.isFull()) ? set.last() : null;
|
||||
FeedEntryStatus last = (order != null && fssl.isFull()) ? fssl.last() : null;
|
||||
JPAQuery<FeedEntry> query = buildQuery(user, sub, unreadOnly, keywords, newerThan, -1, capacity, order, last, tag, minEntryId,
|
||||
maxEntryId);
|
||||
List<Tuple> tuples = query.select(entry.id, entry.updated, status.id, entry.content.title).fetch();
|
||||
|
||||
for (Tuple tuple : tuples) {
|
||||
Long id = tuple.get(entry.id);
|
||||
Date updated = tuple.get(entry.updated);
|
||||
Instant updated = tuple.get(entry.updated);
|
||||
Long statusId = tuple.get(status.id);
|
||||
|
||||
FeedEntryContent content = new FeedEntryContent();
|
||||
@@ -225,11 +227,11 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
||||
status.setEntry(entry);
|
||||
status.setSubscription(sub);
|
||||
|
||||
set.add(status);
|
||||
fssl.add(status);
|
||||
}
|
||||
}
|
||||
|
||||
List<FeedEntryStatus> placeholders = set.asList();
|
||||
List<FeedEntryStatus> placeholders = fssl.asList();
|
||||
int size = placeholders.size();
|
||||
if (size < offset) {
|
||||
return new ArrayList<>();
|
||||
@@ -260,7 +262,7 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
||||
List<Tuple> tuples = query.select(entry.count(), entry.updated.max()).fetch();
|
||||
for (Tuple tuple : tuples) {
|
||||
Long count = tuple.get(entry.count());
|
||||
Date updated = tuple.get(entry.updated.max());
|
||||
Instant updated = tuple.get(entry.updated.max());
|
||||
uc = new UnreadCount(subscription.getId(), count == null ? 0 : count, updated);
|
||||
}
|
||||
return uc;
|
||||
@@ -276,7 +278,7 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
||||
return results;
|
||||
}
|
||||
|
||||
public long deleteOldStatuses(Date olderThan, int limit) {
|
||||
public long deleteOldStatuses(Instant olderThan, int limit) {
|
||||
List<Long> ids = query().select(status.id)
|
||||
.from(status)
|
||||
.where(status.entryInserted.lt(olderThan), status.starred.isFalse())
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
package com.commafeed.backend.feed;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.time.Instant;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.commons.codec.binary.StringUtils;
|
||||
@@ -11,16 +10,14 @@ import org.apache.commons.codec.digest.DigestUtils;
|
||||
import com.commafeed.backend.HttpGetter;
|
||||
import com.commafeed.backend.HttpGetter.HttpResult;
|
||||
import com.commafeed.backend.HttpGetter.NotModifiedException;
|
||||
import com.commafeed.backend.feed.FeedParser.FeedParserResult;
|
||||
import com.commafeed.backend.model.Feed;
|
||||
import com.commafeed.backend.model.FeedEntry;
|
||||
import com.commafeed.backend.feed.parser.FeedParser;
|
||||
import com.commafeed.backend.feed.parser.FeedParserResult;
|
||||
import com.commafeed.backend.urlprovider.FeedURLProvider;
|
||||
import com.rometools.rome.io.FeedException;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Value;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
@@ -35,8 +32,8 @@ public class FeedFetcher {
|
||||
private final HttpGetter getter;
|
||||
private final Set<FeedURLProvider> urlProviders;
|
||||
|
||||
public FeedFetcherResult fetch(String feedUrl, boolean extractFeedUrlFromHtml, String lastModified, String eTag, Date lastPublishedDate,
|
||||
String lastContentHash) throws FeedException, IOException, NotModifiedException {
|
||||
public FeedFetcherResult fetch(String feedUrl, boolean extractFeedUrlFromHtml, String lastModified, String eTag,
|
||||
Instant lastPublishedDate, String lastContentHash) throws FeedException, IOException, NotModifiedException {
|
||||
log.debug("Fetching feed {}", feedUrl);
|
||||
|
||||
int timeout = 20000;
|
||||
@@ -79,20 +76,15 @@ public class FeedFetcher {
|
||||
etagHeaderValueChanged ? result.getETag() : null);
|
||||
}
|
||||
|
||||
if (lastPublishedDate != null && parserResult.getFeed().getLastPublishedDate() != null
|
||||
&& lastPublishedDate.getTime() == parserResult.getFeed().getLastPublishedDate().getTime()) {
|
||||
if (lastPublishedDate != null && lastPublishedDate.equals(parserResult.lastPublishedDate())) {
|
||||
log.debug("publishedDate not modified: {}", feedUrl);
|
||||
throw new NotModifiedException("publishedDate not modified",
|
||||
lastModifiedHeaderValueChanged ? result.getLastModifiedSince() : null,
|
||||
etagHeaderValueChanged ? result.getETag() : null);
|
||||
}
|
||||
|
||||
Feed feed = parserResult.getFeed();
|
||||
feed.setLastModifiedHeader(result.getLastModifiedSince());
|
||||
feed.setEtagHeader(FeedUtils.truncate(result.getETag(), 255));
|
||||
feed.setLastContentHash(hash);
|
||||
return new FeedFetcherResult(parserResult.getFeed(), parserResult.getEntries(), parserResult.getTitle(),
|
||||
result.getUrlAfterRedirect(), result.getDuration());
|
||||
return new FeedFetcherResult(parserResult, result.getUrlAfterRedirect(), result.getLastModifiedSince(), result.getETag(), hash,
|
||||
result.getDuration());
|
||||
}
|
||||
|
||||
private static String extractFeedUrl(Set<FeedURLProvider> urlProviders, String url, String urlContent) {
|
||||
@@ -106,13 +98,8 @@ public class FeedFetcher {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Value
|
||||
public static class FeedFetcherResult {
|
||||
Feed feed;
|
||||
List<FeedEntry> entries;
|
||||
String title;
|
||||
String urlAfterRedirect;
|
||||
long fetchDuration;
|
||||
public record FeedFetcherResult(FeedParserResult feed, String urlAfterRedirect, String lastModifiedHeader, String lastETagHeader,
|
||||
String contentHash, long fetchDuration) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,263 +0,0 @@
|
||||
package com.commafeed.backend.feed;
|
||||
|
||||
import java.io.StringReader;
|
||||
import java.nio.charset.Charset;
|
||||
import java.text.DateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jdom2.Element;
|
||||
import org.jdom2.Namespace;
|
||||
import org.xml.sax.InputSource;
|
||||
|
||||
import com.commafeed.backend.model.Feed;
|
||||
import com.commafeed.backend.model.FeedEntry;
|
||||
import com.commafeed.backend.model.FeedEntryContent;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.rometools.modules.mediarss.MediaEntryModule;
|
||||
import com.rometools.modules.mediarss.MediaModule;
|
||||
import com.rometools.modules.mediarss.types.MediaGroup;
|
||||
import com.rometools.modules.mediarss.types.Metadata;
|
||||
import com.rometools.modules.mediarss.types.Thumbnail;
|
||||
import com.rometools.rome.feed.synd.SyndCategory;
|
||||
import com.rometools.rome.feed.synd.SyndContent;
|
||||
import com.rometools.rome.feed.synd.SyndEnclosure;
|
||||
import com.rometools.rome.feed.synd.SyndEntry;
|
||||
import com.rometools.rome.feed.synd.SyndFeed;
|
||||
import com.rometools.rome.feed.synd.SyndLink;
|
||||
import com.rometools.rome.feed.synd.SyndLinkImpl;
|
||||
import com.rometools.rome.io.FeedException;
|
||||
import com.rometools.rome.io.SyndFeedInput;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Value;
|
||||
|
||||
/**
|
||||
* Parses raw xml as a Feed object
|
||||
*/
|
||||
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||
@Singleton
|
||||
public class FeedParser {
|
||||
|
||||
private static final String ATOM_10_URI = "http://www.w3.org/2005/Atom";
|
||||
private static final Namespace ATOM_10_NS = Namespace.getNamespace(ATOM_10_URI);
|
||||
|
||||
private static final Date START = new Date(86400000);
|
||||
private static final Date END = new Date(1000L * Integer.MAX_VALUE - 86400000);
|
||||
|
||||
public FeedParserResult parse(String feedUrl, byte[] xml) throws FeedException {
|
||||
|
||||
try {
|
||||
Charset encoding = FeedUtils.guessEncoding(xml);
|
||||
String xmlString = FeedUtils.trimInvalidXmlCharacters(new String(xml, encoding));
|
||||
if (xmlString == null) {
|
||||
throw new FeedException("Input string is null for url " + feedUrl);
|
||||
}
|
||||
xmlString = FeedUtils.replaceHtmlEntitiesWithNumericEntities(xmlString);
|
||||
InputSource source = new InputSource(new StringReader(xmlString));
|
||||
|
||||
SyndFeed rss = new SyndFeedInput().build(source);
|
||||
handleForeignMarkup(rss);
|
||||
|
||||
String title = rss.getTitle();
|
||||
Feed feed = new Feed();
|
||||
feed.setUrl(feedUrl);
|
||||
feed.setLink(rss.getLink());
|
||||
|
||||
List<FeedEntry> entries = new ArrayList<>();
|
||||
for (SyndEntry item : rss.getEntries()) {
|
||||
FeedEntry entry = new FeedEntry();
|
||||
|
||||
String guid = item.getUri();
|
||||
if (StringUtils.isBlank(guid)) {
|
||||
guid = item.getLink();
|
||||
}
|
||||
if (StringUtils.isBlank(guid)) {
|
||||
// no guid and no link, skip entry
|
||||
continue;
|
||||
}
|
||||
entry.setGuid(FeedUtils.truncate(guid, 2048));
|
||||
entry.setUpdated(validateDate(getEntryUpdateDate(item), true));
|
||||
entry.setUrl(FeedUtils.truncate(FeedUtils.toAbsoluteUrl(item.getLink(), feed.getLink(), feedUrl), 2048));
|
||||
|
||||
// if link is empty but guid is used as url
|
||||
if (StringUtils.isBlank(entry.getUrl()) && StringUtils.startsWith(entry.getGuid(), "http")) {
|
||||
entry.setUrl(entry.getGuid());
|
||||
}
|
||||
|
||||
FeedEntryContent content = new FeedEntryContent();
|
||||
content.setContent(getContent(item));
|
||||
content.setCategories(FeedUtils
|
||||
.truncate(item.getCategories().stream().map(SyndCategory::getName).collect(Collectors.joining(", ")), 4096));
|
||||
content.setTitle(getTitle(item));
|
||||
content.setAuthor(StringUtils.trimToNull(item.getAuthor()));
|
||||
|
||||
SyndEnclosure enclosure = Iterables.getFirst(item.getEnclosures(), null);
|
||||
if (enclosure != null) {
|
||||
content.setEnclosureUrl(FeedUtils.truncate(enclosure.getUrl(), 2048));
|
||||
content.setEnclosureType(enclosure.getType());
|
||||
}
|
||||
|
||||
MediaEntryModule module = (MediaEntryModule) item.getModule(MediaModule.URI);
|
||||
if (module != null) {
|
||||
Media media = getMedia(module);
|
||||
if (media != null) {
|
||||
content.setMediaDescription(media.getDescription());
|
||||
content.setMediaThumbnailUrl(FeedUtils.truncate(media.getThumbnailUrl(), 2048));
|
||||
content.setMediaThumbnailWidth(media.getThumbnailWidth());
|
||||
content.setMediaThumbnailHeight(media.getThumbnailHeight());
|
||||
}
|
||||
}
|
||||
|
||||
entry.setContent(content);
|
||||
|
||||
entries.add(entry);
|
||||
}
|
||||
|
||||
Date lastEntryDate = null;
|
||||
Date publishedDate = validateDate(rss.getPublishedDate(), false);
|
||||
if (!entries.isEmpty()) {
|
||||
List<Long> sortedTimestamps = FeedUtils.getSortedTimestamps(entries);
|
||||
Long timestamp = sortedTimestamps.get(0);
|
||||
lastEntryDate = new Date(timestamp);
|
||||
publishedDate = (publishedDate == null || publishedDate.before(lastEntryDate)) ? lastEntryDate : publishedDate;
|
||||
}
|
||||
feed.setLastPublishedDate(publishedDate);
|
||||
feed.setAverageEntryInterval(FeedUtils.averageTimeBetweenEntries(entries));
|
||||
feed.setLastEntryDate(lastEntryDate);
|
||||
|
||||
return new FeedParserResult(feed, entries, title);
|
||||
} catch (Exception e) {
|
||||
throw new FeedException(String.format("Could not parse feed from %s : %s", feedUrl, e.getMessage()), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds atom links for rss feeds
|
||||
*/
|
||||
private void handleForeignMarkup(SyndFeed feed) {
|
||||
List<Element> foreignMarkup = feed.getForeignMarkup();
|
||||
if (foreignMarkup == null) {
|
||||
return;
|
||||
}
|
||||
for (Element element : foreignMarkup) {
|
||||
if ("link".equals(element.getName()) && ATOM_10_NS.equals(element.getNamespace())) {
|
||||
SyndLink link = new SyndLinkImpl();
|
||||
link.setRel(element.getAttributeValue("rel"));
|
||||
link.setHref(element.getAttributeValue("href"));
|
||||
feed.getLinks().add(link);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Date getEntryUpdateDate(SyndEntry item) {
|
||||
Date date = item.getUpdatedDate();
|
||||
if (date == null) {
|
||||
date = item.getPublishedDate();
|
||||
}
|
||||
if (date == null) {
|
||||
date = new Date();
|
||||
}
|
||||
return date;
|
||||
}
|
||||
|
||||
private Date validateDate(Date date, boolean nullToNow) {
|
||||
Date now = new Date();
|
||||
if (date == null) {
|
||||
return nullToNow ? now : null;
|
||||
}
|
||||
if (date.before(START) || date.after(END)) {
|
||||
return now;
|
||||
}
|
||||
|
||||
if (date.after(now)) {
|
||||
return now;
|
||||
}
|
||||
return date;
|
||||
}
|
||||
|
||||
private String getContent(SyndEntry item) {
|
||||
String content;
|
||||
if (item.getContents().isEmpty()) {
|
||||
content = item.getDescription() == null ? null : item.getDescription().getValue();
|
||||
} else {
|
||||
content = item.getContents().stream().map(SyndContent::getValue).collect(Collectors.joining(System.lineSeparator()));
|
||||
}
|
||||
return StringUtils.trimToNull(content);
|
||||
}
|
||||
|
||||
private String getTitle(SyndEntry item) {
|
||||
String title = item.getTitle();
|
||||
if (StringUtils.isBlank(title)) {
|
||||
Date date = item.getPublishedDate();
|
||||
if (date != null) {
|
||||
title = DateFormat.getInstance().format(date);
|
||||
} else {
|
||||
title = "(no title)";
|
||||
}
|
||||
}
|
||||
return StringUtils.trimToNull(title);
|
||||
}
|
||||
|
||||
private Media getMedia(MediaEntryModule module) {
|
||||
Media media = getMedia(module.getMetadata());
|
||||
if (media == null && ArrayUtils.isNotEmpty(module.getMediaGroups())) {
|
||||
MediaGroup group = module.getMediaGroups()[0];
|
||||
media = getMedia(group.getMetadata());
|
||||
}
|
||||
|
||||
return media;
|
||||
}
|
||||
|
||||
private Media getMedia(Metadata metadata) {
|
||||
if (metadata == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Media media = new Media();
|
||||
media.setDescription(metadata.getDescription());
|
||||
|
||||
if (ArrayUtils.isNotEmpty(metadata.getThumbnail())) {
|
||||
Thumbnail thumbnail = metadata.getThumbnail()[0];
|
||||
media.setThumbnailWidth(thumbnail.getWidth());
|
||||
media.setThumbnailHeight(thumbnail.getHeight());
|
||||
|
||||
if (thumbnail.getUrl() != null) {
|
||||
media.setThumbnailUrl(thumbnail.getUrl().toString());
|
||||
}
|
||||
}
|
||||
|
||||
if (media.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return media;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class Media {
|
||||
private String description;
|
||||
private String thumbnailUrl;
|
||||
private Integer thumbnailWidth;
|
||||
private Integer thumbnailHeight;
|
||||
|
||||
public boolean isEmpty() {
|
||||
return description == null && thumbnailUrl == null;
|
||||
}
|
||||
}
|
||||
|
||||
@Value
|
||||
public static class FeedParserResult {
|
||||
Feed feed;
|
||||
List<FeedEntry> entries;
|
||||
String title;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.commafeed.backend.feed;
|
||||
|
||||
import java.util.Date;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.BlockingDeque;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
@@ -11,8 +12,6 @@ import java.util.concurrent.SynchronousQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.apache.commons.lang3.time.DateUtils;
|
||||
|
||||
import com.codahale.metrics.Gauge;
|
||||
import com.codahale.metrics.Meter;
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
@@ -156,7 +155,7 @@ public class FeedRefreshEngine implements Managed {
|
||||
|
||||
private void processFeedAsync(Feed feed) {
|
||||
CompletableFuture.supplyAsync(() -> worker.update(feed), workerExecutor)
|
||||
.thenApplyAsync(r -> updater.update(r.getFeed(), r.getEntries()), databaseUpdaterExecutor)
|
||||
.thenApplyAsync(r -> updater.update(r.feed(), r.entries()), databaseUpdaterExecutor)
|
||||
.whenComplete((data, ex) -> {
|
||||
if (ex != null) {
|
||||
log.error("error while processing feed {}", feed.getUrl(), ex);
|
||||
@@ -166,12 +165,12 @@ public class FeedRefreshEngine implements Managed {
|
||||
|
||||
private List<Feed> getNextUpdatableFeeds(int max) {
|
||||
return unitOfWork.call(() -> {
|
||||
Date lastLoginThreshold = Boolean.TRUE.equals(config.getApplicationSettings().getHeavyLoad())
|
||||
? DateUtils.addDays(new Date(), -30)
|
||||
Instant lastLoginThreshold = Boolean.TRUE.equals(config.getApplicationSettings().getHeavyLoad())
|
||||
? Instant.now().minus(Duration.ofDays(30))
|
||||
: null;
|
||||
List<Feed> feeds = feedDAO.findNextUpdatable(max, lastLoginThreshold);
|
||||
// update disabledUntil to prevent feeds from being returned again by feedDAO.findNextUpdatable()
|
||||
Date nextUpdateDate = DateUtils.addMinutes(new Date(), config.getApplicationSettings().getRefreshIntervalMinutes());
|
||||
Instant nextUpdateDate = Instant.now().plus(Duration.ofMinutes(config.getApplicationSettings().getRefreshIntervalMinutes()));
|
||||
feedDAO.setDisabledUntil(feeds.stream().map(AbstractModel::getId).toList(), nextUpdateDate);
|
||||
return feeds;
|
||||
});
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
package com.commafeed.backend.feed;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import org.apache.commons.lang3.time.DateUtils;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
import com.commafeed.CommaFeedConfiguration;
|
||||
import com.commafeed.backend.model.Feed;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
@@ -22,62 +21,59 @@ public class FeedRefreshIntervalCalculator {
|
||||
this.refreshIntervalMinutes = config.getApplicationSettings().getRefreshIntervalMinutes();
|
||||
}
|
||||
|
||||
public Date onFetchSuccess(Feed feed) {
|
||||
Date defaultRefreshInterval = getDefaultRefreshInterval();
|
||||
return heavyLoad ? computeRefreshIntervalForHeavyLoad(feed, defaultRefreshInterval) : defaultRefreshInterval;
|
||||
public Instant onFetchSuccess(Instant publishedDate, Long averageEntryInterval) {
|
||||
Instant defaultRefreshInterval = getDefaultRefreshInterval();
|
||||
return heavyLoad ? computeRefreshIntervalForHeavyLoad(publishedDate, averageEntryInterval, defaultRefreshInterval)
|
||||
: defaultRefreshInterval;
|
||||
}
|
||||
|
||||
public Date onFeedNotModified(Feed feed) {
|
||||
Date defaultRefreshInterval = getDefaultRefreshInterval();
|
||||
return heavyLoad ? computeRefreshIntervalForHeavyLoad(feed, defaultRefreshInterval) : defaultRefreshInterval;
|
||||
public Instant onFeedNotModified(Instant publishedDate, Long averageEntryInterval) {
|
||||
return onFetchSuccess(publishedDate, averageEntryInterval);
|
||||
}
|
||||
|
||||
public Date onFetchError(Feed feed) {
|
||||
int errorCount = feed.getErrorCount();
|
||||
public Instant onFetchError(int errorCount) {
|
||||
int retriesBeforeDisable = 3;
|
||||
if (errorCount < retriesBeforeDisable || !heavyLoad) {
|
||||
return getDefaultRefreshInterval();
|
||||
}
|
||||
|
||||
int disabledHours = Math.min(24 * 7, errorCount - retriesBeforeDisable + 1);
|
||||
return DateUtils.addHours(new Date(), disabledHours);
|
||||
return Instant.now().plus(Duration.ofHours(disabledHours));
|
||||
}
|
||||
|
||||
private Date getDefaultRefreshInterval() {
|
||||
return DateUtils.addMinutes(new Date(), refreshIntervalMinutes);
|
||||
private Instant getDefaultRefreshInterval() {
|
||||
return Instant.now().plus(Duration.ofMinutes(refreshIntervalMinutes));
|
||||
}
|
||||
|
||||
private Date computeRefreshIntervalForHeavyLoad(Feed feed, Date defaultRefreshInterval) {
|
||||
Date now = new Date();
|
||||
Date publishedDate = feed.getLastEntryDate();
|
||||
Long averageEntryInterval = feed.getAverageEntryInterval();
|
||||
private Instant computeRefreshIntervalForHeavyLoad(Instant publishedDate, Long averageEntryInterval, Instant defaultRefreshInterval) {
|
||||
Instant now = Instant.now();
|
||||
|
||||
if (publishedDate == null) {
|
||||
// feed with no entries, recheck in 24 hours
|
||||
return DateUtils.addHours(now, 24);
|
||||
} else if (publishedDate.before(DateUtils.addMonths(now, -1))) {
|
||||
return now.plus(Duration.ofHours(24));
|
||||
} else if (ChronoUnit.DAYS.between(publishedDate, now) >= 30) {
|
||||
// older than a month, recheck in 24 hours
|
||||
return DateUtils.addHours(now, 24);
|
||||
} else if (publishedDate.before(DateUtils.addDays(now, -14))) {
|
||||
return now.plus(Duration.ofHours(24));
|
||||
} else if (ChronoUnit.DAYS.between(publishedDate, now) >= 14) {
|
||||
// older than two weeks, recheck in 12 hours
|
||||
return DateUtils.addHours(now, 12);
|
||||
} else if (publishedDate.before(DateUtils.addDays(now, -7))) {
|
||||
return now.plus(Duration.ofHours(12));
|
||||
} else if (ChronoUnit.DAYS.between(publishedDate, now) >= 7) {
|
||||
// older than a week, recheck in 6 hours
|
||||
return DateUtils.addHours(now, 6);
|
||||
return now.plus(Duration.ofHours(6));
|
||||
} else if (averageEntryInterval != null) {
|
||||
// use average time between entries to decide when to refresh next, divided by factor
|
||||
int factor = 2;
|
||||
|
||||
// not more than 6 hours
|
||||
long date = Math.min(DateUtils.addHours(now, 6).getTime(), now.getTime() + averageEntryInterval / factor);
|
||||
long date = Math.min(now.plus(Duration.ofHours(6)).toEpochMilli(), now.toEpochMilli() + averageEntryInterval / factor);
|
||||
|
||||
// not less than default refresh interval
|
||||
date = Math.max(defaultRefreshInterval.getTime(), date);
|
||||
date = Math.max(defaultRefreshInterval.toEpochMilli(), date);
|
||||
|
||||
return new Date(date);
|
||||
return Instant.ofEpochMilli(date);
|
||||
} else {
|
||||
// unknown case, recheck in 24 hours
|
||||
return DateUtils.addHours(now, 24);
|
||||
return now.plus(Duration.ofHours(24));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
package com.commafeed.backend.feed;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
|
||||
@@ -16,10 +20,12 @@ import com.codahale.metrics.MetricRegistry;
|
||||
import com.commafeed.backend.cache.CacheService;
|
||||
import com.commafeed.backend.dao.FeedSubscriptionDAO;
|
||||
import com.commafeed.backend.dao.UnitOfWork;
|
||||
import com.commafeed.backend.feed.parser.FeedParserResult.Content;
|
||||
import com.commafeed.backend.feed.parser.FeedParserResult.Entry;
|
||||
import com.commafeed.backend.model.Feed;
|
||||
import com.commafeed.backend.model.FeedEntry;
|
||||
import com.commafeed.backend.model.FeedEntryContent;
|
||||
import com.commafeed.backend.model.FeedSubscription;
|
||||
import com.commafeed.backend.model.Models;
|
||||
import com.commafeed.backend.model.User;
|
||||
import com.commafeed.backend.service.FeedEntryService;
|
||||
import com.commafeed.backend.service.FeedService;
|
||||
@@ -27,7 +33,6 @@ import com.commafeed.frontend.ws.WebSocketMessageBuilder;
|
||||
import com.commafeed.frontend.ws.WebSocketSessions;
|
||||
import com.google.common.util.concurrent.Striped;
|
||||
|
||||
import io.dropwizard.lifecycle.Managed;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import lombok.AllArgsConstructor;
|
||||
@@ -38,7 +43,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
*/
|
||||
@Slf4j
|
||||
@Singleton
|
||||
public class FeedRefreshUpdater implements Managed {
|
||||
public class FeedRefreshUpdater {
|
||||
|
||||
private final UnitOfWork unitOfWork;
|
||||
private final FeedService feedService;
|
||||
@@ -72,9 +77,10 @@ public class FeedRefreshUpdater implements Managed {
|
||||
entryInserted = metrics.meter(MetricRegistry.name(getClass(), "entryInserted"));
|
||||
}
|
||||
|
||||
private AddEntryResult addEntry(final Feed feed, final FeedEntry entry, final List<FeedSubscription> subscriptions) {
|
||||
private AddEntryResult addEntry(final Feed feed, final Entry entry, final List<FeedSubscription> subscriptions) {
|
||||
boolean processed = false;
|
||||
boolean inserted = false;
|
||||
Set<FeedSubscription> subscriptionsForWhichEntryIsUnread = new HashSet<>();
|
||||
|
||||
// lock on feed, make sure we are not updating the same feed twice at
|
||||
// the same time
|
||||
@@ -82,8 +88,8 @@ public class FeedRefreshUpdater implements Managed {
|
||||
|
||||
// lock on content, make sure we are not updating the same entry
|
||||
// twice at the same time
|
||||
FeedEntryContent content = entry.getContent();
|
||||
String key2 = DigestUtils.sha1Hex(StringUtils.trimToEmpty(content.getContent() + content.getTitle()));
|
||||
Content content = entry.content();
|
||||
String key2 = DigestUtils.sha1Hex(StringUtils.trimToEmpty(content.content() + content.title()));
|
||||
|
||||
Iterator<Lock> iterator = locks.bulkGet(Arrays.asList(key1, key2)).iterator();
|
||||
Lock lock1 = iterator.next();
|
||||
@@ -96,10 +102,21 @@ public class FeedRefreshUpdater implements Managed {
|
||||
locked2 = lock2.tryLock(1, TimeUnit.MINUTES);
|
||||
if (locked1 && locked2) {
|
||||
processed = true;
|
||||
inserted = unitOfWork.call(() -> feedEntryService.addEntry(feed, entry, subscriptions));
|
||||
if (inserted) {
|
||||
entryInserted.mark();
|
||||
}
|
||||
inserted = unitOfWork.call(() -> {
|
||||
Instant now = Instant.now();
|
||||
FeedEntry feedEntry = feedEntryService.findOrCreate(feed, entry);
|
||||
boolean newEntry = !feedEntry.getInserted().isBefore(now);
|
||||
if (newEntry) {
|
||||
entryInserted.mark();
|
||||
for (FeedSubscription sub : subscriptions) {
|
||||
boolean unread = feedEntryService.applyFilter(sub, feedEntry);
|
||||
if (unread) {
|
||||
subscriptionsForWhichEntryIsUnread.add(sub);
|
||||
}
|
||||
}
|
||||
}
|
||||
return newEntry;
|
||||
});
|
||||
} else {
|
||||
log.error("lock timeout for " + feed.getUrl() + " - " + key1);
|
||||
}
|
||||
@@ -113,32 +130,34 @@ public class FeedRefreshUpdater implements Managed {
|
||||
lock2.unlock();
|
||||
}
|
||||
}
|
||||
return new AddEntryResult(processed, inserted);
|
||||
return new AddEntryResult(processed, inserted, subscriptionsForWhichEntryIsUnread);
|
||||
}
|
||||
|
||||
public boolean update(Feed feed, List<FeedEntry> entries) {
|
||||
public boolean update(Feed feed, List<Entry> entries) {
|
||||
boolean processed = true;
|
||||
boolean insertedAtLeastOneEntry = false;
|
||||
long inserted = 0;
|
||||
Map<FeedSubscription, Long> unreadCountBySubscription = new HashMap<>();
|
||||
|
||||
if (!entries.isEmpty()) {
|
||||
List<String> lastEntries = cache.getLastEntries(feed);
|
||||
Set<String> lastEntries = cache.getLastEntries(feed);
|
||||
List<String> currentEntries = new ArrayList<>();
|
||||
|
||||
List<FeedSubscription> subscriptions = null;
|
||||
for (FeedEntry entry : entries) {
|
||||
String cacheKey = cache.buildUniqueEntryKey(feed, entry);
|
||||
for (Entry entry : entries) {
|
||||
String cacheKey = cache.buildUniqueEntryKey(entry);
|
||||
if (!lastEntries.contains(cacheKey)) {
|
||||
log.debug("cache miss for {}", entry.getUrl());
|
||||
log.debug("cache miss for {}", entry.url());
|
||||
if (subscriptions == null) {
|
||||
subscriptions = unitOfWork.call(() -> feedSubscriptionDAO.findByFeed(feed));
|
||||
}
|
||||
AddEntryResult addEntryResult = addEntry(feed, entry, subscriptions);
|
||||
processed &= addEntryResult.processed;
|
||||
insertedAtLeastOneEntry |= addEntryResult.inserted;
|
||||
inserted += addEntryResult.inserted ? 1 : 0;
|
||||
addEntryResult.subscriptionsForWhichEntryIsUnread.forEach(sub -> unreadCountBySubscription.merge(sub, 1L, Long::sum));
|
||||
|
||||
entryCacheMiss.mark();
|
||||
} else {
|
||||
log.debug("cache hit for {}", entry.getUrl());
|
||||
log.debug("cache hit for {}", entry.url());
|
||||
entryCacheHit.mark();
|
||||
}
|
||||
|
||||
@@ -148,22 +167,21 @@ public class FeedRefreshUpdater implements Managed {
|
||||
|
||||
if (subscriptions == null) {
|
||||
feed.setMessage("No new entries found");
|
||||
} else if (insertedAtLeastOneEntry) {
|
||||
} else if (inserted > 0) {
|
||||
List<User> users = subscriptions.stream().map(FeedSubscription::getUser).toList();
|
||||
cache.invalidateUnreadCount(subscriptions.toArray(new FeedSubscription[0]));
|
||||
cache.invalidateUserRootCategory(users.toArray(new User[0]));
|
||||
|
||||
// notify over websocket
|
||||
subscriptions.forEach(sub -> webSocketSessions.sendMessage(sub.getUser(), WebSocketMessageBuilder.newFeedEntries(sub)));
|
||||
notifyOverWebsocket(unreadCountBySubscription);
|
||||
}
|
||||
}
|
||||
|
||||
if (!processed) {
|
||||
// requeue asap
|
||||
feed.setDisabledUntil(new Date(0));
|
||||
feed.setDisabledUntil(Models.MINIMUM_INSTANT);
|
||||
}
|
||||
|
||||
if (insertedAtLeastOneEntry) {
|
||||
if (inserted > 0) {
|
||||
feedUpdated.mark();
|
||||
}
|
||||
|
||||
@@ -172,10 +190,16 @@ public class FeedRefreshUpdater implements Managed {
|
||||
return processed;
|
||||
}
|
||||
|
||||
private void notifyOverWebsocket(Map<FeedSubscription, Long> unreadCountBySubscription) {
|
||||
unreadCountBySubscription.forEach((sub, unreadCount) -> webSocketSessions.sendMessage(sub.getUser(),
|
||||
WebSocketMessageBuilder.newFeedEntries(sub, unreadCount)));
|
||||
}
|
||||
|
||||
@AllArgsConstructor
|
||||
private static class AddEntryResult {
|
||||
private final boolean processed;
|
||||
private final boolean inserted;
|
||||
private final Set<FeedSubscription> subscriptionsForWhichEntryIsUnread;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.commafeed.backend.feed;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
@@ -11,16 +13,15 @@ import com.codahale.metrics.MetricRegistry;
|
||||
import com.commafeed.CommaFeedConfiguration;
|
||||
import com.commafeed.backend.HttpGetter.NotModifiedException;
|
||||
import com.commafeed.backend.feed.FeedFetcher.FeedFetcherResult;
|
||||
import com.commafeed.backend.feed.parser.FeedParserResult.Entry;
|
||||
import com.commafeed.backend.model.Feed;
|
||||
import com.commafeed.backend.model.FeedEntry;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import lombok.Value;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Calls {@link FeedFetcher} and updates the Feed object, but does not update the database ({@link FeedRefreshUpdater} does that)
|
||||
* Calls {@link FeedFetcher} and updates the Feed object, but does not update the database, ({@link FeedRefreshUpdater} does that)
|
||||
*/
|
||||
@Slf4j
|
||||
@Singleton
|
||||
@@ -44,32 +45,41 @@ public class FeedRefreshWorker {
|
||||
public FeedRefreshWorkerResult update(Feed feed) {
|
||||
try {
|
||||
String url = Optional.ofNullable(feed.getUrlAfterRedirect()).orElse(feed.getUrl());
|
||||
FeedFetcherResult feedFetcherResult = fetcher.fetch(url, false, feed.getLastModifiedHeader(), feed.getEtagHeader(),
|
||||
FeedFetcherResult result = fetcher.fetch(url, false, feed.getLastModifiedHeader(), feed.getEtagHeader(),
|
||||
feed.getLastPublishedDate(), feed.getLastContentHash());
|
||||
// stops here if NotModifiedException or any other exception is thrown
|
||||
List<FeedEntry> entries = feedFetcherResult.getEntries();
|
||||
|
||||
List<Entry> entries = result.feed().entries();
|
||||
|
||||
Integer maxFeedCapacity = config.getApplicationSettings().getMaxFeedCapacity();
|
||||
if (maxFeedCapacity > 0) {
|
||||
entries = entries.stream().limit(maxFeedCapacity).toList();
|
||||
}
|
||||
|
||||
String urlAfterRedirect = feedFetcherResult.getUrlAfterRedirect();
|
||||
Integer maxEntriesAgeDays = config.getApplicationSettings().getMaxEntriesAgeDays();
|
||||
if (maxEntriesAgeDays > 0) {
|
||||
Instant threshold = Instant.now().minus(Duration.ofDays(maxEntriesAgeDays));
|
||||
entries = entries.stream().filter(entry -> entry.updated().isAfter(threshold)).toList();
|
||||
}
|
||||
|
||||
String urlAfterRedirect = result.urlAfterRedirect();
|
||||
if (StringUtils.equals(url, urlAfterRedirect)) {
|
||||
urlAfterRedirect = null;
|
||||
}
|
||||
|
||||
feed.setUrlAfterRedirect(urlAfterRedirect);
|
||||
feed.setLink(feedFetcherResult.getFeed().getLink());
|
||||
feed.setLastModifiedHeader(feedFetcherResult.getFeed().getLastModifiedHeader());
|
||||
feed.setEtagHeader(feedFetcherResult.getFeed().getEtagHeader());
|
||||
feed.setLastContentHash(feedFetcherResult.getFeed().getLastContentHash());
|
||||
feed.setLastPublishedDate(feedFetcherResult.getFeed().getLastPublishedDate());
|
||||
feed.setAverageEntryInterval(feedFetcherResult.getFeed().getAverageEntryInterval());
|
||||
feed.setLastEntryDate(feedFetcherResult.getFeed().getLastEntryDate());
|
||||
feed.setLink(result.feed().link());
|
||||
feed.setLastModifiedHeader(result.lastModifiedHeader());
|
||||
feed.setEtagHeader(result.lastETagHeader());
|
||||
feed.setLastContentHash(result.contentHash());
|
||||
feed.setLastPublishedDate(result.feed().lastPublishedDate());
|
||||
feed.setAverageEntryInterval(result.feed().averageEntryInterval());
|
||||
feed.setLastEntryDate(result.feed().lastEntryDate());
|
||||
|
||||
feed.setErrorCount(0);
|
||||
feed.setMessage(null);
|
||||
feed.setDisabledUntil(refreshIntervalCalculator.onFetchSuccess(feedFetcherResult.getFeed()));
|
||||
feed.setDisabledUntil(
|
||||
refreshIntervalCalculator.onFetchSuccess(result.feed().lastPublishedDate(), result.feed().averageEntryInterval()));
|
||||
|
||||
return new FeedRefreshWorkerResult(feed, entries);
|
||||
} catch (NotModifiedException e) {
|
||||
@@ -77,7 +87,7 @@ public class FeedRefreshWorker {
|
||||
|
||||
feed.setErrorCount(0);
|
||||
feed.setMessage(e.getMessage());
|
||||
feed.setDisabledUntil(refreshIntervalCalculator.onFeedNotModified(feed));
|
||||
feed.setDisabledUntil(refreshIntervalCalculator.onFeedNotModified(feed.getLastPublishedDate(), feed.getAverageEntryInterval()));
|
||||
|
||||
if (e.getNewLastModifiedHeader() != null) {
|
||||
feed.setLastModifiedHeader(e.getNewLastModifiedHeader());
|
||||
@@ -93,7 +103,7 @@ public class FeedRefreshWorker {
|
||||
|
||||
feed.setErrorCount(feed.getErrorCount() + 1);
|
||||
feed.setMessage("Unable to refresh feed : " + e.getMessage());
|
||||
feed.setDisabledUntil(refreshIntervalCalculator.onFetchError(feed));
|
||||
feed.setDisabledUntil(refreshIntervalCalculator.onFetchError(feed.getErrorCount()));
|
||||
|
||||
return new FeedRefreshWorkerResult(feed, Collections.emptyList());
|
||||
} finally {
|
||||
@@ -101,10 +111,7 @@ public class FeedRefreshWorker {
|
||||
}
|
||||
}
|
||||
|
||||
@Value
|
||||
public static class FeedRefreshWorkerResult {
|
||||
Feed feed;
|
||||
List<FeedEntry> entries;
|
||||
public record FeedRefreshWorkerResult(Feed feed, List<Entry> entries) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,20 +2,12 @@ package com.commafeed.backend.feed;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.ahocorasick.trie.Emit;
|
||||
import org.ahocorasick.trie.Trie;
|
||||
import org.ahocorasick.trie.Trie.TrieBuilder;
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.math3.stat.descriptive.SummaryStatistics;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.nodes.Element;
|
||||
@@ -29,8 +21,6 @@ import com.commafeed.backend.model.FeedSubscription;
|
||||
import com.commafeed.frontend.model.Entry;
|
||||
import com.google.gwt.i18n.client.HasDirection.Direction;
|
||||
import com.google.gwt.i18n.shared.BidiUtils;
|
||||
import com.ibm.icu.text.CharsetDetector;
|
||||
import com.ibm.icu.text.CharsetMatch;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@@ -50,70 +40,6 @@ public class FeedUtils {
|
||||
return string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect feed encoding by using the declared encoding in the xml processing instruction and by detecting the characters used in the
|
||||
* feed
|
||||
*
|
||||
*/
|
||||
public static Charset guessEncoding(byte[] bytes) {
|
||||
String extracted = extractDeclaredEncoding(bytes);
|
||||
if (StringUtils.startsWithIgnoreCase(extracted, "iso-8859-")) {
|
||||
if (!StringUtils.endsWith(extracted, "1")) {
|
||||
return Charset.forName(extracted);
|
||||
}
|
||||
} else if (StringUtils.startsWithIgnoreCase(extracted, "windows-")) {
|
||||
return Charset.forName(extracted);
|
||||
}
|
||||
return detectEncoding(bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect encoding by analyzing characters in the array
|
||||
*/
|
||||
public static Charset detectEncoding(byte[] bytes) {
|
||||
String encoding = "UTF-8";
|
||||
|
||||
CharsetDetector detector = new CharsetDetector();
|
||||
detector.setText(bytes);
|
||||
CharsetMatch match = detector.detect();
|
||||
if (match != null) {
|
||||
encoding = match.getName();
|
||||
}
|
||||
if (encoding.equalsIgnoreCase("ISO-8859-1")) {
|
||||
encoding = "windows-1252";
|
||||
}
|
||||
return Charset.forName(encoding);
|
||||
}
|
||||
|
||||
public static String replaceHtmlEntitiesWithNumericEntities(String source) {
|
||||
// Create a buffer sufficiently large that re-allocations are minimized.
|
||||
StringBuilder sb = new StringBuilder(source.length() << 1);
|
||||
|
||||
TrieBuilder builder = Trie.builder();
|
||||
builder.ignoreOverlaps();
|
||||
|
||||
for (String key : HtmlEntities.HTML_ENTITIES) {
|
||||
builder.addKeyword(key);
|
||||
}
|
||||
|
||||
Trie trie = builder.build();
|
||||
Collection<Emit> emits = trie.parseText(source);
|
||||
|
||||
int prevIndex = 0;
|
||||
for (Emit emit : emits) {
|
||||
int matchIndex = emit.getStart();
|
||||
|
||||
sb.append(source, prevIndex, matchIndex);
|
||||
sb.append(HtmlEntities.HTML_TO_NUMERIC_MAP.get(emit.getKeyword()));
|
||||
prevIndex = emit.getEnd() + 1;
|
||||
}
|
||||
|
||||
// Add the remainder of the string (contains no more matches).
|
||||
sb.append(source.substring(prevIndex));
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static boolean isHttp(String url) {
|
||||
return url.startsWith("http://");
|
||||
}
|
||||
@@ -122,6 +48,10 @@ public class FeedUtils {
|
||||
return url.startsWith("https://");
|
||||
}
|
||||
|
||||
public static boolean isAbsoluteUrl(String url) {
|
||||
return isHttp(url) || isHttps(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize the url. The resulting url is not meant to be fetched but rather used as a mean to identify a feed and avoid duplicates
|
||||
*/
|
||||
@@ -163,25 +93,6 @@ public class FeedUtils {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the declared encoding from the xml
|
||||
*/
|
||||
public static String extractDeclaredEncoding(byte[] bytes) {
|
||||
int index = ArrayUtils.indexOf(bytes, (byte) '>');
|
||||
if (index == -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String pi = new String(ArrayUtils.subarray(bytes, 0, index + 1)).replace('\'', '"');
|
||||
index = StringUtils.indexOf(pi, "encoding=\"");
|
||||
if (index == -1) {
|
||||
return null;
|
||||
}
|
||||
String encoding = pi.substring(index + 10);
|
||||
encoding = encoding.substring(0, encoding.indexOf('"'));
|
||||
return encoding;
|
||||
}
|
||||
|
||||
public static boolean isRTL(FeedEntry entry) {
|
||||
String text = entry.getContent().getContent();
|
||||
|
||||
@@ -202,52 +113,6 @@ public class FeedUtils {
|
||||
return direction == Direction.RTL;
|
||||
}
|
||||
|
||||
public static String trimInvalidXmlCharacters(String xml) {
|
||||
if (StringUtils.isBlank(xml)) {
|
||||
return null;
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
boolean firstTagFound = false;
|
||||
for (int i = 0; i < xml.length(); i++) {
|
||||
char c = xml.charAt(i);
|
||||
|
||||
if (!firstTagFound) {
|
||||
if (c == '<') {
|
||||
firstTagFound = true;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (c >= 32 || c == 9 || c == 10 || c == 13) {
|
||||
if (!Character.isHighSurrogate(c) && !Character.isLowSurrogate(c)) {
|
||||
sb.append(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static Long averageTimeBetweenEntries(List<FeedEntry> entries) {
|
||||
if (entries.isEmpty() || entries.size() == 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<Long> timestamps = getSortedTimestamps(entries);
|
||||
|
||||
SummaryStatistics stats = new SummaryStatistics();
|
||||
for (int i = 0; i < timestamps.size() - 1; i++) {
|
||||
long diff = Math.abs(timestamps.get(i) - timestamps.get(i + 1));
|
||||
stats.addValue(diff);
|
||||
}
|
||||
return (long) stats.getMean();
|
||||
}
|
||||
|
||||
public static List<Long> getSortedTimestamps(List<FeedEntry> entries) {
|
||||
return entries.stream().map(t -> t.getUpdated().getTime()).sorted(Collections.reverseOrder()).toList();
|
||||
}
|
||||
|
||||
public static String removeTrailingSlash(String url) {
|
||||
if (url.endsWith("/")) {
|
||||
url = url.substring(0, url.length() - 1);
|
||||
@@ -256,8 +121,8 @@ public class FeedUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param url
|
||||
*
|
||||
* @param relativeUrl
|
||||
* the url of the entry
|
||||
* @param feedLink
|
||||
* the url of the feed as described in the feed
|
||||
@@ -265,32 +130,18 @@ public class FeedUtils {
|
||||
* the url of the feed that we used to fetch the feed
|
||||
* @return an absolute url pointing to the entry
|
||||
*/
|
||||
public static String toAbsoluteUrl(String url, String feedLink, String feedUrl) {
|
||||
url = StringUtils.trimToNull(StringUtils.normalizeSpace(url));
|
||||
if (url == null || url.startsWith("http")) {
|
||||
return url;
|
||||
}
|
||||
|
||||
String baseUrl = (feedLink == null || isRelative(feedLink)) ? feedUrl : feedLink;
|
||||
|
||||
public static String toAbsoluteUrl(String relativeUrl, String feedLink, String feedUrl) {
|
||||
String baseUrl = (feedLink != null && isAbsoluteUrl(feedLink)) ? feedLink : feedUrl;
|
||||
if (baseUrl == null) {
|
||||
return url;
|
||||
return null;
|
||||
}
|
||||
|
||||
String result;
|
||||
try {
|
||||
result = new URL(new URL(baseUrl), url).toString();
|
||||
return new URL(new URL(baseUrl), relativeUrl).toString();
|
||||
} catch (MalformedURLException e) {
|
||||
log.debug("could not parse url : " + e.getMessage(), e);
|
||||
result = url;
|
||||
return null;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static boolean isRelative(final String url) {
|
||||
// the regex means "start with 'scheme://'"
|
||||
return url.startsWith("/") || url.startsWith("#") || !url.matches("^\\w+\\:\\/\\/.*");
|
||||
}
|
||||
|
||||
public static String getFaviconUrl(FeedSubscription subscription) {
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.commafeed.backend.feed.parser;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.ibm.icu.text.CharsetDetector;
|
||||
import com.ibm.icu.text.CharsetMatch;
|
||||
|
||||
import jakarta.inject.Singleton;
|
||||
|
||||
@Singleton
|
||||
class EncodingDetector {
|
||||
|
||||
/**
|
||||
* Detect feed encoding by using the declared encoding in the xml processing instruction and by detecting the characters used in the
|
||||
* feed
|
||||
*
|
||||
*/
|
||||
public Charset getEncoding(byte[] bytes) {
|
||||
String extracted = extractDeclaredEncoding(bytes);
|
||||
if (StringUtils.startsWithIgnoreCase(extracted, "iso-8859-")) {
|
||||
if (!StringUtils.endsWith(extracted, "1")) {
|
||||
return Charset.forName(extracted);
|
||||
}
|
||||
} else if (StringUtils.startsWithIgnoreCase(extracted, "windows-")) {
|
||||
return Charset.forName(extracted);
|
||||
}
|
||||
return detectEncoding(bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the declared encoding from the xml
|
||||
*/
|
||||
public String extractDeclaredEncoding(byte[] bytes) {
|
||||
int index = ArrayUtils.indexOf(bytes, (byte) '>');
|
||||
if (index == -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String pi = new String(ArrayUtils.subarray(bytes, 0, index + 1)).replace('\'', '"');
|
||||
index = StringUtils.indexOf(pi, "encoding=\"");
|
||||
if (index == -1) {
|
||||
return null;
|
||||
}
|
||||
String encoding = pi.substring(index + 10);
|
||||
encoding = encoding.substring(0, encoding.indexOf('"'));
|
||||
return encoding;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect encoding by analyzing characters in the array
|
||||
*/
|
||||
private Charset detectEncoding(byte[] bytes) {
|
||||
String encoding = "UTF-8";
|
||||
|
||||
CharsetDetector detector = new CharsetDetector();
|
||||
detector.setText(bytes);
|
||||
CharsetMatch match = detector.detect();
|
||||
if (match != null) {
|
||||
encoding = match.getName();
|
||||
}
|
||||
if (encoding.equalsIgnoreCase("ISO-8859-1")) {
|
||||
encoding = "windows-1252";
|
||||
}
|
||||
return Charset.forName(encoding);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.commafeed.backend.feed.parser;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import org.ahocorasick.trie.Emit;
|
||||
import org.ahocorasick.trie.Trie;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import jakarta.inject.Singleton;
|
||||
|
||||
@Singleton
|
||||
class FeedCleaner {
|
||||
|
||||
public String trimInvalidXmlCharacters(String xml) {
|
||||
if (StringUtils.isBlank(xml)) {
|
||||
return null;
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
boolean firstTagFound = false;
|
||||
for (int i = 0; i < xml.length(); i++) {
|
||||
char c = xml.charAt(i);
|
||||
|
||||
if (!firstTagFound) {
|
||||
if (c == '<') {
|
||||
firstTagFound = true;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (c >= 32 || c == 9 || c == 10 || c == 13) {
|
||||
if (!Character.isHighSurrogate(c) && !Character.isLowSurrogate(c)) {
|
||||
sb.append(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/40836618/1885506
|
||||
public String replaceHtmlEntitiesWithNumericEntities(String source) {
|
||||
// Create a buffer sufficiently large that re-allocations are minimized.
|
||||
StringBuilder sb = new StringBuilder(source.length() << 1);
|
||||
|
||||
Collection<Emit> emits = Trie.builder().ignoreOverlaps().addKeywords(HtmlEntities.HTML_ENTITIES).build().parseText(source);
|
||||
|
||||
int prevIndex = 0;
|
||||
for (Emit emit : emits) {
|
||||
int matchIndex = emit.getStart();
|
||||
|
||||
sb.append(source, prevIndex, matchIndex);
|
||||
sb.append(HtmlEntities.HTML_TO_NUMERIC_MAP.get(emit.getKeyword()));
|
||||
prevIndex = emit.getEnd() + 1;
|
||||
}
|
||||
|
||||
// Add the remainder of the string (contains no more matches).
|
||||
sb.append(source.substring(prevIndex));
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user