Compare commits

...

130 Commits
3.8.1 ... 4.0.0

Author SHA1 Message Date
Athou
8e0a53fc49 release 4.0.0 2024-01-02 10:56:52 +01:00
Athou
4ea2bad083 test all redirect codes 2024-01-02 08:08:19 +01:00
Athou
46065d938d extract new i18n labels 2024-01-01 19:42:36 +01:00
Athou
16389824f7 fix wrong labels 2024-01-01 19:39:55 +01:00
Athou
92b624ca8a add option to follow system dark/light mode (#1083) 2024-01-01 19:37:52 +01:00
Athou
1ae5111f76 use slightly less dark gray for selected tree node background to improve unread count readability 2024-01-01 18:24:39 +01:00
Athou
d9a9a01a60 add missing pathname to websocket url (#1167) 2024-01-01 18:15:04 +01:00
Athou
bbbb9c10a6 align buttons to the right to match other dialogs 2024-01-01 10:47:39 +01:00
Athou
50cf9718a3 fix wrong clear button style 2024-01-01 10:43:54 +01:00
Athou
99a7ede82d restore bold font for unread items 2024-01-01 10:10:05 +01:00
Athou
7b1218ef1e correctly trim long feed names when sidebar is too narrow 2024-01-01 08:34:00 +01:00
Athou
8dab16090f display links and image placeholders in entries in the same color as the text so that they are not mistaken for commafeed actions 2023-12-31 18:36:55 +01:00
Athou
6e5f362a8e load custom js when the app is done loading to ease custom code usage (#1093) 2023-12-31 09:27:49 +01:00
Athou
96212afd27 save sidebar width in local storage (#1093) 2023-12-30 22:13:35 +01:00
Athou
7e02380858 update to mantine 7 2023-12-30 22:13:35 +01:00
Athou
2742b7fff6 remove usage of createStyles from mantine that is removed in v7 2023-12-29 22:27:54 +01:00
Athou
dade873420 file not needed anymore 2023-12-29 20:15:34 +01:00
Athou
e7925e6330 add tests for the new insertedBefore mechanic 2023-12-29 15:32:06 +01:00
Athou
f845f225cf add a "insertedBefore" field to mark as read requests to make sure the user does not mark entries that were fetched but never seen before (fixes a regression from #1007) 2023-12-29 13:40:30 +01:00
Athou
39ba4a1c97 disable redoc url sync because it causes issues with hashrouter 2023-12-29 12:11:33 +01:00
Athou
a491b95a02 generate a nicer url in documentation (/rest instead of /openapi/../rest) 2023-12-29 11:21:28 +01:00
Athou
e0c05c8e5d redux update 2023-12-29 11:08:33 +01:00
Athou
2f1aa12e30 use redoc instead of swagger ui to be able to update redux 2023-12-29 11:01:57 +01:00
Athou
4c532cf028 fix wrong endpoint name in documentation 2023-12-29 10:53:38 +01:00
Athou
dc95044fbc group swagger api definitions by endpoint 2023-12-29 09:11:47 +01:00
Athou
418cb4797d use latest node and npm now that everything is up to date 2023-12-29 07:27:53 +01:00
Athou
c646503501 update other dependencies 2023-12-28 22:37:12 +01:00
Athou
0ea0db48db split thunks from slices to avoid circular dependencies 2023-12-28 22:11:03 +01:00
Athou
bb4bb0c7d7 createAppAsyncThunk needs to be in its own file (https://stackoverflow.com/a/77136003/1885506) 2023-12-28 21:49:18 +01:00
Athou
97781d5551 eslint update 2023-12-28 21:49:18 +01:00
Athou
f4e48383cc use typed createAsyncThunk 2023-12-28 19:49:38 +01:00
Athou
aa009c366d prettier update 2023-12-28 15:26:07 +01:00
Athou
1289dbae84 add test for websocket ping/pong 2023-12-28 10:25:44 +01:00
Athou
8c69dd355c fix warnings 2023-12-27 11:19:34 +01:00
Athou
fdf4fdcc87 use latest dropwizard release 2023-12-27 09:32:24 +01:00
Athou
9cd1cde571 apply intellij fixes 2023-12-27 09:22:55 +01:00
Athou
1b4b3ca52c fix wrong JPA mapping 2023-12-27 08:48:51 +01:00
Athou
6a76c8b8c3 reduce svg size by removing unused inkscape tags 2023-12-27 08:47:41 +01:00
Athou
b49d35f181 remove all remaining references to httpclient4 2023-12-26 08:21:35 +01:00
Athou
5ba248eaba update to httpclient5 2023-12-25 20:00:47 +01:00
Athou
11aff68052 java http client is unfortunately sometimes not honoring timeouts (https://bugs.openjdk.org/browse/JDK-8258397), use httpclient again 2023-12-25 17:15:33 +01:00
Athou
07dd10848f return default content type if invalid instead of crashing 2023-12-25 10:30:48 +01:00
Athou
b2bd386e9c reset database completely after each test so that tests cannot impact each other 2023-12-24 10:52:09 +01:00
Athou
d09cabb8c6 avoid modifying the admin user because it impacts the test in UserIT 2023-12-24 09:55:02 +01:00
Athou
818d847607 CookieManager parses the cookie header even if we ask to ignore them, use our own cookie handler that does nothing 2023-12-22 22:27:42 +01:00
Athou
1db53e48c6 reduce connection keepalive timeout to 30s, default is 20 minutes 2023-12-22 20:22:00 +01:00
Athou
5601d150c3 restore the connect timeout feature 2023-12-22 16:10:34 +01:00
Athou
a35f55cde6 compact h2 database on exit 2023-12-21 22:27:50 +01:00
Athou
3714bfaccc add test for password recovery 2023-12-21 22:15:39 +01:00
Athou
5541cc9fbe websocket can now be disabled, the websocket ping interval and the tree reload interval can now be configured (#1132) 2023-12-21 21:20:26 +01:00
Athou
bdabd9db0d ran npm audit fix 2023-12-18 18:03:38 +01:00
Athou
2762c535d6 cleanup 2023-12-18 18:03:38 +01:00
Athou
241c465eba add tests for PasswordEncryptionService 2023-12-18 16:06:54 +01:00
Athou
6c3895e60a make sure we ignore cookies 2023-12-18 15:39:11 +01:00
Athou
a30bf18102 add support for youtube playlist favicons 2023-12-18 13:45:25 +01:00
Athou
d9ccdf1caf use java standard http client because apache http clients should be reused because they support pooling but we don't need that 2023-12-18 11:53:14 +01:00
Athou
155e7ba1aa add tests for HttpGetter 2023-12-18 10:24:40 +01:00
Athou
00faf44c94 remove wonky pubsub support 2023-12-18 10:15:43 +01:00
Athou
c45f832131 increase websocket idle timeout above ping interval 2023-12-17 17:40:28 +01:00
Athou
6f781216cd keep using h2 2.1 because 2.2 uses a different file format 2023-12-17 17:40:28 +01:00
Athou
fd0e5426e5 upgrade to dropwizard 4.x 2023-12-17 15:10:57 +01:00
Athou
b5d99b9661 migrate from swagger to openapi3 2023-12-17 13:51:12 +01:00
Athou
50fcdece86 update various dependencies 2023-12-17 13:51:12 +01:00
Athou
d882553644 java 17 is now the new baseline 2023-12-17 13:51:12 +01:00
Athou
bf71e825a4 update code formatter version 2023-12-17 08:49:44 +01:00
Athou
351701d674 add tests for the security layer 2023-12-16 21:20:14 +01:00
Athou
cb4a8df0d2 add more tests 2023-12-16 18:16:52 +01:00
Athou
7ef865506f use non-existing urls 2023-12-15 18:05:48 +01:00
Athou
e4863e8881 add a GET method to the fever api (#1176) 2023-12-15 17:53:47 +01:00
Athou
c86a060170 remove unused AnalyticsServlet, it's handled directly by the client since 3.0 2023-12-15 17:45:15 +01:00
Athou
6ed5637e57 add more IT tests to ease transition to dropwizard 4 2023-12-15 17:35:51 +01:00
Athou
929df60f09 no need to expose admin connector in production 2023-12-13 07:21:27 +01:00
Athou
2b51de8e5b release 3.10.1 2023-12-08 17:19:31 +01:00
Athou
0ba70d29bd readme tweaks 2023-11-24 08:37:41 +01:00
Athou
197b3b258b also build with jdk 21 now that it's been released 2023-11-17 08:52:30 +01:00
Athou
850f66999c use less memory by returning unused memory to the OS (https://openjdk.org/jeps/346) 2023-11-16 08:41:29 +01:00
Athou
d7d3574e36 swap next and previous buttons (#1159) 2023-11-15 07:59:17 +01:00
Jérémie Panzer
435d612cbf Merge pull request #1164 from canoine/master
Update fr/messages.po
2023-11-02 12:53:05 +01:00
canoine
3d3a7c6496 Merge pull request #1 from canoine/canoine-patch-1
Update fr/messages.po
2023-10-18 09:10:41 +02:00
canoine
fba57fe0a7 Update fr/messages.po
Translation of the new fields.
2023-10-18 09:09:15 +02:00
Athou
ce7933f320 add mention of PikaPods 2023-10-02 19:38:06 +02:00
Athou
8ac452afc9 shorten count starting at 10k and add a tooltip with the exact count(#1150) 2023-09-23 16:44:25 +02:00
Jérémie Panzer
a11cb3ac7a Merge pull request #1154 from joerg376/patch-1
Update messages.po
2023-09-23 16:44:11 +02:00
joerg376
39808bbafc Update messages.po 2023-09-23 11:48:10 +02:00
Athou
aee56e3dbe no need to reload everything when websocket connection status changes 2023-09-19 12:31:19 +02:00
Athou
40f451c762 increase websocket ping interval to just under a minute instead of the default 15s 2023-09-12 20:22:34 +02:00
Athou
d633803ab5 only poll tree if websocket connection is unavailable 2023-09-12 20:22:03 +02:00
Athou
d7a3b75687 indicate that the feedLink property is not always filled (#1146) 2023-09-08 07:10:44 +02:00
Athou
df8c4056b6 indicate that the method returns the id of the newly created feed (#1147) 2023-09-08 07:07:29 +02:00
Athou
06319c1eb0 release 3.10.0 2023-09-06 09:04:21 +02:00
Athou
b7ede8eba2 add instructions for the Fever API 2023-09-05 11:04:52 +02:00
Athou
1a4517d6a3 add support for FeedMe 2023-09-05 11:04:52 +02:00
Athou
a402c5d7d8 add support for FocusReader 2023-09-05 11:04:52 +02:00
Athou
408809787e add support for Raven Reader 2023-09-05 11:04:52 +02:00
Athou
d7b0d572c1 add fever-compatible api 2023-09-05 11:04:52 +02:00
Athou
b356be3e6f show the whole title in the detailed view (#1097 #1144) 2023-09-05 09:10:26 +02:00
Athou
998385334b add metric for deleted entries 2023-09-03 12:16:43 +02:00
Athou
c6d613d81a add "s" keyboard shortcut to star/unstar entries (#1142) 2023-08-27 11:43:30 +02:00
Athou
9981d8763d don't set default values for env variables (#1141) 2023-08-24 07:51:10 +02:00
Athou
b37680333c clean database after each test 2023-08-23 20:36:57 +02:00
Athou
66d1eb3f1f store sessions in database 2023-08-23 20:34:29 +02:00
Athou
6fe1c2a3c0 release 3.9.0 2023-08-17 10:01:17 +02:00
Athou
c2e453027c fix desktop entry header shortly rendered as mobile, causing a small visual glitch 2023-08-16 07:36:05 +02:00
Athou
f16bac9b59 remove "required" for more nulalble fields 2023-08-11 19:12:48 +02:00
Athou
8cca826e70 specify in documentation that we support basic auth 2023-08-11 15:12:00 +02:00
Athou
b0165bb26a "created" field is not always filled, remove "required" 2023-08-11 13:34:05 +02:00
Athou
366294ab46 show loader only while loading (#1131) 2023-08-11 12:53:27 +02:00
Jérémie Panzer
2988938440 Merge pull request #1135 from dcelasun/update-tr-locale
Update Turkish translation
2023-08-10 11:21:07 +02:00
D. Can Celasun
e865769e30 Update Turkish translation 2023-08-10 09:53:38 +01:00
Jérémie Panzer
f87be2fc03 Merge pull request #1129 from canoine/master
Update fr/messages.po
2023-08-04 13:31:58 +02:00
Athou
466846d268 add option to disable custom context menu (#1128) 2023-08-04 08:53:34 +02:00
canoine
61b6be4090 Update fr/messages.po
New entries translated
2023-08-04 08:47:34 +02:00
Athou
cb779ec494 add setting to disable mark as read confirmation (#1110) 2023-08-04 07:32:13 +02:00
Athou
da6f2050f9 log as debug because default log level is info and we don't want to see this 2023-08-02 14:39:03 +02:00
Athou
4304f84a55 restore the announcement feature 2023-08-01 16:30:42 +02:00
Athou
8a175d8221 add css parser error handler that just prints info message because it is non-blocking 2023-08-01 15:30:50 +02:00
Athou
f1896d34e2 disable context menu on shift + right click (#1052) 2023-07-21 09:58:08 +02:00
Athou
45d0e0ec98 Merge branch 'dcelasun-configurable-batch-size' 2023-07-12 15:27:18 +02:00
Athou
38c5beec2f remove unused ApplicationSettings type in the client 2023-07-12 15:26:56 +02:00
Athou
c4715dc3f7 rename field to better represent what it does 2023-07-12 15:26:56 +02:00
D. Can Celasun
6ce6b5ef0e Make database cleaning batch size configurable 2023-07-12 15:26:56 +02:00
Athou
1af3dd452c Merge branch 'dcelasun-mariadb-driver-fix' 2023-07-12 15:04:04 +02:00
Athou
1f4ec41222 change other yml files 2023-07-12 15:03:30 +02:00
D. Can Celasun
512c4cc507 Support MariaDB JDBC driver
This fixes #1113
2023-07-11 08:54:17 +01:00
Athou
d391c8f1c9 Merge branch 'ScuttleSE-master' 2023-07-09 14:37:53 +02:00
Athou
46d3e67aec update other yml files 2023-07-09 14:31:46 +02:00
Athou
d9505c4d87 Merge branch 'master' of https://github.com/ScuttleSE/commafeed into ScuttleSE-master 2023-07-09 14:31:13 +02:00
Athou
42491f5778 no need to repeat feed url in message stored in database (#1112) 2023-07-09 14:09:30 +02:00
Gustav Almstrom
9c897c9fb2 Updated MySQL driver 2023-07-09 11:10:27 +02:00
Athou
21b500a96e don't autoclose bugs 2023-07-08 15:56:02 +02:00
289 changed files with 10571 additions and 10873 deletions

1
.github/stale.yml vendored
View File

@@ -7,6 +7,7 @@ exemptLabels:
- pinned - pinned
- security - security
- enhancement - enhancement
- bug
# Label to use when marking an issue as stale # Label to use when marking an issue as stale
staleLabel: wontfix staleLabel: wontfix
# Comment to post when marking an issue as stale. Set to `false` to disable # Comment to post when marking an issue as stale. Set to `false` to disable

View File

@@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
java: [ "8", "11", "17" ] java: [ "17", "21" ]
steps: steps:
- name: Checkout - name: Checkout
@@ -35,7 +35,7 @@ jobs:
- name: Upload JAR - name: Upload JAR
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
if: ${{ matrix.java == '8' }} if: ${{ matrix.java == '17' }}
with: with:
name: commafeed.jar name: commafeed.jar
path: commafeed-server/target/commafeed.jar path: commafeed-server/target/commafeed.jar
@@ -43,14 +43,14 @@ jobs:
# Docker # Docker
- name: Login to Container Registry - name: Login to Container Registry
uses: docker/login-action@v2 uses: docker/login-action@v2
if: ${{ matrix.java == '8' }} if: ${{ matrix.java == '17' }}
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Docker build and push tag - name: Docker build and push tag
uses: docker/build-push-action@v4 uses: docker/build-push-action@v4
if: ${{ matrix.java == '8' && github.ref_type == 'tag' }} if: ${{ matrix.java == '17' && github.ref_type == 'tag' }}
with: with:
context: . context: .
push: true push: true
@@ -61,7 +61,7 @@ jobs:
- name: Docker build and push master - name: Docker build and push master
uses: docker/build-push-action@v4 uses: docker/build-push-action@v4
if: ${{ matrix.java == '8' && github.ref_name == 'master' }} if: ${{ matrix.java == '17' && github.ref_name == 'master' }}
with: with:
context: . context: .
push: true push: true
@@ -71,14 +71,14 @@ jobs:
# Create GitHub release after Docker image has been published # Create GitHub release after Docker image has been published
- name: Extract Changelog Entry - name: Extract Changelog Entry
uses: mindsers/changelog-reader-action@v2 uses: mindsers/changelog-reader-action@v2
if: ${{ matrix.java == '8' && github.ref_type == 'tag' }} if: ${{ matrix.java == '17' && github.ref_type == 'tag' }}
id: changelog_reader id: changelog_reader
with: with:
version: ${{ github.ref_name }} version: ${{ github.ref_name }}
- name: Create GitHub release - name: Create GitHub release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
if: ${{ matrix.java == '8' && github.ref_type == 'tag' }} if: ${{ matrix.java == '17' && github.ref_type == 'tag' }}
with: with:
name: CommaFeed ${{ github.ref_name }} name: CommaFeed ${{ github.ref_name }}
body: ${{ steps.changelog_reader.outputs.changes }} body: ${{ steps.changelog_reader.outputs.changes }}

View File

@@ -1,5 +1,61 @@
# Changelog # Changelog
## [4.0.0]
- migrated from dropwizard 2 to dropwizard 4, Java 17+ is now required
- entries that were fetched and inserted in the database but not yet shown in the UI are no longer marked as read when
marking all entries as read
- your custom sidebar width is now persisted in the local storage of your browser
- there is now a third color scheme option in addition to light and dark: system (follows the system color scheme)
- added support for youtube playlist favicons
- 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
- 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
- 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 "/"
- unstable pubsubhubbub support was removed
## [3.10.1]
- swap next and previous buttons (#1159)
- unread count for subscriptions will now be shortened starting at 10k instead of 1k
- increased websocket ping interval to just under a minute to reduce data and battery usage on mobile
- only refresh subscription tree on a timer if websocket connection is unavailable
- the Docker image now uses less memory by returning unused memory to the OS
- add support for Java 21
## [3.10.0]
- added a Fever-compatible API that is usable with mobile clients that support the Fever API (see instructions in
Settings -> Profile)
- long entry titles are no longer shortened in the detailed view
- added the "s" keyboard shortcut to star/unstar entries
- http sessions are now stored in the database (they were stored on disk before)
- fixed an issue that made it impossible to override the database url in a config.yml mounted in the Docker image
## [3.9.0]
- improve performance by disabling the loader when nothing is loading (most noticeable on mobile)
- added a setting to disable the 'mark all as read' confirmation
- added a setting to disable the custom context menu
- if the custom context is enabled, it can still be disabled by pressing the shift key
- the announcement feature is now working again and supports html ('announcement' configuration element in config.yml)
- add support for MariaDB 11+
- fix entry header shortly rendered as mobile on desktop, causing a small visual glitch
- fix an issue that could cause a feed to not refresh correctly if the url was very long
- database cleanup batch size is now configurable
- css parsing errors are no longer logged to the standard output
- fix small errors in the api documentation
## [3.8.1] ## [3.8.1]
- in expanded mode, don't scroll when clicking on the body of the current entry - in expanded mode, don't scroll when clicking on the body of the current entry
@@ -95,10 +151,10 @@
## [3.0.1] ## [3.0.1]
- allow env variable substitution in config.yml - allow env variable substitution in config.yml
- e.g. having a custom config.yml file with `app.session.path=${SOME_ENV_VAR}` will substitute `SOME_ENV_VAR` with - e.g. having a custom config.yml file with `app.session.path=${SOME_ENV_VAR}` will substitute `SOME_ENV_VAR` with its
its value value
- allow env variable prefixed with `CF_` to override config.yml properties - allow env variable prefixed with `CF_` to override config.yml properties
- e.g. setting `CF_APP_ALLOWREGISTRATIONS=true` will set `app.allowRegistrations` to `true` - e.g. setting `CF_APP_ALLOWREGISTRATIONS=true` will set `app.allowRegistrations` to `true`
## [3.0.0] ## [3.0.0]

View File

@@ -4,10 +4,9 @@ EXPOSE 8082
RUN mkdir -p /commafeed/data RUN mkdir -p /commafeed/data
VOLUME /commafeed/data VOLUME /commafeed/data
ENV CF_SESSION_PATH=/commafeed/data/sessions
ENV CF_DATABASE_URL=jdbc:h2:/commafeed/data/db
COPY commafeed-server/config.yml.example config.yml COPY commafeed-server/config.yml.example config.yml
COPY commafeed-server/target/commafeed.jar . COPY commafeed-server/target/commafeed.jar .
CMD ["java", "-Djava.net.preferIPv4Stack=true", "-jar", "commafeed.jar", "server", "config.yml"] ENV JAVA_TOOL_OPTIONS -Djava.net.preferIPv4Stack=true -Xms20m -XX:+UseG1GC -XX:-ShrinkHeapInSteps -XX:G1PeriodicGCInterval=10000 -XX:-G1PeriodicGCInvokesConcurrent -XX:MinHeapFreeRatio=5 -XX:MaxHeapFreeRatio=10
CMD ["java", "-jar", "commafeed.jar", "server", "config.yml"]

View File

@@ -7,22 +7,32 @@ Google Reader inspired self-hosted RSS reader, based on Dropwizard and React/Typ
## Features ## Features
- 4 different layouts - 4 different layouts
- Dark theme - Light/Dark theme
- Fully responsive - Fully responsive
- Keyboard shortcuts for almost everything - Keyboard shortcuts for almost everything
- Support for right-to-left feeds - Support for right-to-left feeds
- Translated in 25+ languages - Translated in 25+ languages
- Supports thousands of users and millions of feeds - Supports thousands of users and millions of feeds
- OPML import/export - OPML import/export
- REST API - REST API and a Fever-compatible API for native mobile apps
- [Browser extension](https://github.com/Athou/commafeed-browser-extension) - [Browser extension](https://github.com/Athou/commafeed-browser-extension)
## Deployment on your own server ## Deployment
### Docker ### Docker
Docker is the easiest way to get started with CommaFeed.
Docker images are built automatically and are available at https://hub.docker.com/r/athou/commafeed Docker images are built automatically and are available at https://hub.docker.com/r/athou/commafeed
### Cloud hosting
[PikaPods](https://www.pikapods.com) offers 1-click cloud hosting solutions starting at $1/month with a free $5
welcome credit and officially supports CommaFeed.
PikaPods shares 20% of the revenue back to CommaFeed.
[![PikaPods](https://www.pikapods.com/static/run-button.svg)](https://www.pikapods.com/pods?run=commafeed)
### Download precompiled package ### Download precompiled package
mkdir commafeed && cd commafeed mkdir commafeed && cd commafeed

View File

@@ -1,85 +0,0 @@
{
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
"react-app",
"airbnb",
"airbnb-typescript",
"prettier"
],
"plugins": ["@typescript-eslint", "prettier", "hooks"],
"parserOptions": {
"project": "./tsconfig.json"
},
"rules": {
// make eslint check prettier rules
"prettier/prettier": "error",
// enforce consistent curly braces usage
"curly": ["error", "multi-line", "consistent"],
// set "props" to false because it cases false positives with immer
"no-param-reassign": ["error", { "props": false }],
"prefer-destructuring": [
"error",
{
"array": false,
"object": true
},
{
"enforceForRenamedProperties": false
}
],
// causes issues in thunks when we want to dispatch an action that is defined in the reducer
"@typescript-eslint/no-use-before-define": "off",
// make sure the key prop is filled when required
"react/jsx-key": ["error", { "checkFragmentShorthand": true }],
// configure additional hooks
"react-hooks/exhaustive-deps": [
"warn",
{
"additionalHooks": "(^useAsync$|useDidUpdate)"
}
],
// trigger even if props is used only in createStyles()
"react/no-unused-prop-types": "off",
// no longer required with modern react versions
"react/react-in-jsx-scope": "off",
// not required with typescript
"react/prop-types": "off",
"react/require-default-props": "off",
// matter of taste
"react/destructuring-assignment": "off",
"react/jsx-props-no-spreading": "off",
"react/no-unescaped-entities": "off",
"import/prefer-default-export": "off",
// enforce hook call order
"hooks/sort": [
2,
{
"groups": [
"useLocation",
"useParams",
"useState",
"useAppSelector",
"useAppDispatch",
"useAsync",
"useForm",
"useAsyncCallback",
"useCallback",
"useEffect"
]
}
]
}
}

View File

@@ -0,0 +1,41 @@
module.exports = {
env: {
browser: true,
es2021: true
},
extends: ["standard-with-typescript", "plugin:react/recommended", "plugin:react-hooks/recommended", "plugin:prettier/recommended"],
settings: {
react: {
version: "detect"
}
},
overrides: [
{
env: {
node: true
},
files: [".eslintrc.{js,cjs}"],
parserOptions: {
sourceType: "script"
}
}
],
parserOptions: {
ecmaVersion: "latest",
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"
}
}

View File

@@ -3,5 +3,6 @@
"semi": false, "semi": false,
"tabWidth": 4, "tabWidth": 4,
"arrowParens": "avoid", "arrowParens": "avoid",
"endOfLine": "auto" "endOfLine": "auto",
"trailingComma": "es5"
} }

File diff suppressed because it is too large Load Diff

View File

@@ -14,72 +14,72 @@
"i18n:extract": "lingui extract --clean" "i18n:extract": "lingui extract --clean"
}, },
"dependencies": { "dependencies": {
"@emotion/react": "^11.11.0", "@emotion/react": "^11.11.3",
"@fontsource/open-sans": "^5.0.1", "@fontsource/open-sans": "^5.0.20",
"@lingui/core": "^4.1.2", "@lingui/core": "^4.6.0",
"@lingui/macro": "^4.1.2", "@lingui/macro": "^4.6.0",
"@lingui/react": "^4.1.2", "@lingui/react": "^4.6.0",
"@mantine/core": "^6.0.11", "@mantine/core": "^7.3.2",
"@mantine/form": "^6.0.11", "@mantine/form": "^7.3.2",
"@mantine/hooks": "^6.0.11", "@mantine/hooks": "^7.3.2",
"@mantine/modals": "^6.0.11", "@mantine/modals": "^7.3.2",
"@mantine/notifications": "^6.0.11", "@mantine/notifications": "^7.3.2",
"@mantine/spotlight": "^6.0.11", "@mantine/spotlight": "^7.3.2",
"@mantine/styles": "^6.0.11", "@monaco-editor/react": "^4.6.0",
"@monaco-editor/react": "^4.5.1", "@reduxjs/toolkit": "^2.0.1",
"@reduxjs/toolkit": "^1.9.5", "axios": "^1.6.3",
"axios": "^1.4.0", "dayjs": "^1.11.10",
"dayjs": "^1.11.7",
"escape-string-regexp": "^5.0.0", "escape-string-regexp": "^5.0.0",
"interweave": "^13.1.0", "interweave": "^13.1.0",
"monaco-editor": "^0.38.0", "monaco-editor": "^0.45.0",
"mousetrap": "^1.6.5", "mousetrap": "^1.6.5",
"re-resizable": "^6.9.9",
"react": "^18.2.0", "react": "^18.2.0",
"react-async-hook": "^4.0.0", "react-async-hook": "^4.0.0",
"react-contexify": "^6.0.0", "react-contexify": "^6.0.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-draggable": "^4.4.6",
"react-ga4": "^2.1.0", "react-ga4": "^2.1.0",
"react-icons": "^4.8.0", "react-icons": "^4.12.0",
"react-infinite-scroller": "^1.2.6", "react-infinite-scroller": "^1.2.6",
"react-redux": "^8.0.5", "react-redux": "^9.0.4",
"react-router-dom": "^6.11.2", "react-router-dom": "^6.21.1",
"react-swipeable": "^7.0.0", "react-swipeable": "^7.0.1",
"swagger-ui-react": "^4.18.3", "redoc": "^2.1.3",
"throttle-debounce": "^5.0.0", "throttle-debounce": "^5.0.0",
"tinycon": "^0.6.8", "tinycon": "^0.6.8",
"tss-react": "^4.9.3",
"use-local-storage": "^3.0.0", "use-local-storage": "^3.0.0",
"websocket-heartbeat-js": "^1.1.2" "websocket-heartbeat-js": "^1.1.3"
}, },
"devDependencies": { "devDependencies": {
"@lingui/cli": "^4.1.2", "@lingui/cli": "^4.6.0",
"@lingui/vite-plugin": "^4.1.2", "@lingui/vite-plugin": "^4.6.0",
"@types/eslint": "^8.40.0", "@types/mousetrap": "^1.6.15",
"@types/mousetrap": "^1.6.11", "@types/react": "^18.2.46",
"@types/react": "^18.2.6", "@types/react-dom": "^18.2.18",
"@types/react-dom": "^18.2.4", "@types/react-infinite-scroller": "^1.2.5",
"@types/react-infinite-scroller": "^1.2.3", "@types/swagger-ui-react": "^4.18.3",
"@types/swagger-ui-react": "^4.18.0", "@types/throttle-debounce": "^5.0.2",
"@types/throttle-debounce": "^5.0.0", "@types/tinycon": "^0.6.5",
"@types/tinycon": "^0.6.3", "@typescript-eslint/eslint-plugin": "^6.16.0",
"@typescript-eslint/eslint-plugin": "^5.59.7", "@vitejs/plugin-react": "^4.2.1",
"@typescript-eslint/parser": "^5.59.7",
"@vitejs/plugin-react": "^4.0.0",
"babel-plugin-macros": "^3.1.0", "babel-plugin-macros": "^3.1.0",
"eslint": "^8.41.0", "eslint": "^8.56.0",
"eslint-config-airbnb": "^19.0.4", "eslint-config-prettier": "^9.1.0",
"eslint-config-airbnb-typescript": "^17.0.0", "eslint-config-standard-with-typescript": "^43.0.0",
"eslint-config-prettier": "^8.8.0", "eslint-plugin-import": "^2.29.1",
"eslint-config-react-app": "^7.0.1", "eslint-plugin-n": "^16.5.0",
"eslint-plugin-hooks": "^0.4.3", "eslint-plugin-prettier": "^5.1.2",
"eslint-plugin-prettier": "^4.2.1", "eslint-plugin-promise": "^6.1.1",
"prettier": "^2.8.8", "eslint-plugin-react": "^7.33.2",
"rollup-plugin-visualizer": "^5.9.0", "eslint-plugin-react-hooks": "^4.6.0",
"typescript": "^5.0.4", "prettier": "^3.1.1",
"vite": "^4.3.9", "rollup-plugin-visualizer": "^5.12.0",
"typescript": "^5.3.3",
"vite": "^4.5.1",
"vite-plugin-eslint": "^1.8.1", "vite-plugin-eslint": "^1.8.1",
"vite-tsconfig-paths": "^4.2.0", "vite-tsconfig-paths": "^4.2.3",
"vitest": "^0.31.1", "vitest": "^0.34.6",
"vitest-mock-extended": "^1.1.3" "vitest-mock-extended": "^1.3.1"
} }
} }

View File

@@ -5,7 +5,7 @@
<parent> <parent>
<groupId>com.commafeed</groupId> <groupId>com.commafeed</groupId>
<artifactId>commafeed</artifactId> <artifactId>commafeed</artifactId>
<version>3.8.1</version> <version>4.0.0</version>
</parent> </parent>
<artifactId>commafeed-client</artifactId> <artifactId>commafeed-client</artifactId>
<name>CommaFeed Client</name> <name>CommaFeed Client</name>
@@ -15,7 +15,7 @@
<plugin> <plugin>
<groupId>com.github.eirslett</groupId> <groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId> <artifactId>frontend-maven-plugin</artifactId>
<version>1.12.1</version> <version>1.15.0</version>
<?m2e ignore?> <?m2e ignore?>
<executions> <executions>
<execution> <execution>
@@ -25,8 +25,8 @@
</goals> </goals>
<phase>compile</phase> <phase>compile</phase>
<configuration> <configuration>
<nodeVersion>v16.16.0</nodeVersion> <nodeVersion>v20.10.0</nodeVersion>
<npmVersion>8.15.0</npmVersion> <npmVersion>10.2.5</npmVersion>
</configuration> </configuration>
</execution> </execution>
<execution> <execution>
@@ -73,7 +73,7 @@
</plugin> </plugin>
<plugin> <plugin>
<artifactId>maven-resources-plugin</artifactId> <artifactId>maven-resources-plugin</artifactId>
<version>3.3.0</version> <version>3.3.1</version>
<executions> <executions>
<execution> <execution>
<id>copy web interface to resources</id> <id>copy web interface to resources</id>

View File

@@ -1,12 +1,12 @@
import { i18n } from "@lingui/core" import { i18n } from "@lingui/core"
import { I18nProvider } from "@lingui/react" import { I18nProvider } from "@lingui/react"
import { ColorScheme, ColorSchemeProvider, MantineProvider } from "@mantine/core" import { MantineProvider } from "@mantine/core"
import { useColorScheme } from "@mantine/hooks" import { useDidUpdate } from "@mantine/hooks"
import { ModalsProvider } from "@mantine/modals" import { ModalsProvider } from "@mantine/modals"
import { Notifications } from "@mantine/notifications" import { Notifications } from "@mantine/notifications"
import { Constants } from "app/constants" import { Constants } from "app/constants"
import { redirectTo } from "app/slices/redirect" import { redirectTo } from "app/redirect/slice"
import { reloadServerInfos } from "app/slices/server" import { reloadServerInfos } from "app/server/thunks"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { categoryUnreadCount } from "app/utils" import { categoryUnreadCount } from "app/utils"
import { ErrorBoundary } from "components/ErrorBoundary" import { ErrorBoundary } from "components/ErrorBoundary"
@@ -29,44 +29,50 @@ import { LoginPage } from "pages/auth/LoginPage"
import { PasswordRecoveryPage } from "pages/auth/PasswordRecoveryPage" import { PasswordRecoveryPage } from "pages/auth/PasswordRecoveryPage"
import { RegistrationPage } from "pages/auth/RegistrationPage" import { RegistrationPage } from "pages/auth/RegistrationPage"
import { WelcomePage } from "pages/WelcomePage" import { WelcomePage } from "pages/WelcomePage"
import React, { useEffect } from "react" import React, { useEffect, useRef } from "react"
import ReactGA from "react-ga4" import ReactGA from "react-ga4"
import { HashRouter, Navigate, Route, Routes, useLocation, useNavigate } from "react-router-dom" import { HashRouter, Navigate, Route, Routes, useLocation, useNavigate } from "react-router-dom"
import Tinycon from "tinycon" import Tinycon from "tinycon"
import useLocalStorage from "use-local-storage"
function Providers(props: { children: React.ReactNode }) { function Providers(props: { children: React.ReactNode }) {
const preferredColorScheme = useColorScheme()
const [colorScheme, setColorScheme] = useLocalStorage<ColorScheme>("color-scheme", preferredColorScheme)
const toggleColorScheme = (value?: ColorScheme) => setColorScheme(value ?? (colorScheme === "dark" ? "light" : "dark"))
return ( return (
<I18nProvider i18n={i18n}> <I18nProvider i18n={i18n}>
<ColorSchemeProvider colorScheme={colorScheme} toggleColorScheme={toggleColorScheme}> <MantineProvider
<MantineProvider defaultColorScheme="auto"
withGlobalStyles theme={{
withNormalizeCSS primaryColor: "orange",
theme={{ fontFamily: "Open Sans",
primaryColor: "orange", colors: {
colorScheme, // keep using dark colors from mantine v6
fontFamily: "Open Sans", // https://v6.mantine.dev/theming/colors/#default-colors
}} dark: [
> "#C1C2C5",
<ModalsProvider> "#A6A7AB",
<Notifications position="bottom-right" zIndex={9999} /> "#909296",
<ErrorBoundary>{props.children}</ErrorBoundary> "#5c5f66",
</ModalsProvider> "#373A40",
</MantineProvider> "#2C2E33",
</ColorSchemeProvider> "#25262b",
"#1A1B1E",
"#141517",
"#101113",
],
},
}}
>
<ModalsProvider>
<Notifications position="bottom-right" zIndex={9999} />
<ErrorBoundary>{props.children}</ErrorBoundary>
</ModalsProvider>
</MantineProvider>
</I18nProvider> </I18nProvider>
) )
} }
// swagger-ui is very large, load only on-demand // swagger-ui is very large, load only on-demand
const ApiDocumentationPage = React.lazy(() => import("pages/app/ApiDocumentationPage")) const ApiDocumentationPage = React.lazy(async () => await import("pages/app/ApiDocumentationPage"))
function AppRoutes() { function AppRoutes() {
const sidebarWidth = useAppSelector(state => state.tree.sidebarWidth)
const sidebarVisible = useAppSelector(state => state.tree.sidebarVisible) const sidebarVisible = useAppSelector(state => state.tree.sidebarVisible)
return ( return (
@@ -77,7 +83,7 @@ function AppRoutes() {
<Route path="register" element={<RegistrationPage />} /> <Route path="register" element={<RegistrationPage />} />
<Route path="passwordRecovery" element={<PasswordRecoveryPage />} /> <Route path="passwordRecovery" element={<PasswordRecoveryPage />} />
<Route path="api" element={<ApiDocumentationPage />} /> <Route path="api" element={<ApiDocumentationPage />} />
<Route path="app" element={<Layout header={<Header />} sidebar={<Tree />} sidebarWidth={sidebarVisible ? sidebarWidth : 0} />}> <Route path="app" element={<Layout header={<Header />} sidebar={<Tree />} sidebarVisible={sidebarVisible} />}>
<Route path="category"> <Route path="category">
<Route path=":id" element={<FeedEntriesPage sourceType="category" />} /> <Route path=":id" element={<FeedEntriesPage sourceType="category" />} />
<Route path=":id/details" element={<CategoryDetailsPage />} /> <Route path=":id/details" element={<CategoryDetailsPage />} />
@@ -160,6 +166,40 @@ function BrowserExtensionBadgeUnreadCountHandler() {
return null return null
} }
function CustomJs() {
const scriptLoaded = useRef(false)
// useDidUpdate is used instead of useEffect because we want to skip the first render
// the first render is the render of react-router, the routes are actually loaded in a second render
// we want the script to be executed when the first route is done loading
useDidUpdate(() => {
if (scriptLoaded.current) {
return
}
const script = document.createElement("script")
script.src = "custom_js.js"
script.async = true
document.body.appendChild(script)
scriptLoaded.current = true
})
return null
}
function CustomCss() {
useEffect(() => {
const link = document.createElement("link")
link.rel = "stylesheet"
link.type = "text/css"
link.href = "custom_css.css"
document.head.appendChild(link)
}, [])
return null
}
export function App() { export function App() {
useI18n() useI18n()
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
@@ -177,6 +217,8 @@ export function App() {
<GoogleAnalyticsHandler /> <GoogleAnalyticsHandler />
<RedirectHandler /> <RedirectHandler />
<AppRoutes /> <AppRoutes />
<CustomJs />
<CustomCss />
</HashRouter> </HashRouter>
</> </>
</Providers> </Providers>

View File

@@ -0,0 +1,7 @@
import { createAsyncThunk } from "@reduxjs/toolkit"
import { type AppDispatch, type RootState } from "app/store"
export const createAppAsyncThunk = createAsyncThunk.withTypes<{
state: RootState
dispatch: AppDispatch
}>()

View File

@@ -1,29 +1,30 @@
import axios from "axios" import axios from "axios"
import { import {
AddCategoryRequest, type AddCategoryRequest,
Category, type AdminSaveUserRequest,
CategoryModificationRequest, type Category,
CollapseRequest, type CategoryModificationRequest,
Entries, type CollapseRequest,
FeedInfo, type Entries,
FeedInfoRequest, type FeedInfo,
FeedModificationRequest, type FeedInfoRequest,
GetEntriesPaginatedRequest, type FeedModificationRequest,
IDRequest, type GetEntriesPaginatedRequest,
LoginRequest, type IDRequest,
MarkRequest, type LoginRequest,
Metrics, type MarkRequest,
MultipleMarkRequest, type Metrics,
PasswordResetRequest, type MultipleMarkRequest,
ProfileModificationRequest, type PasswordResetRequest,
RegistrationRequest, type ProfileModificationRequest,
ServerInfo, type RegistrationRequest,
Settings, type ServerInfo,
StarRequest, type Settings,
SubscribeRequest, type StarRequest,
Subscription, type SubscribeRequest,
TagRequest, type Subscription,
UserModel, type TagRequest,
type UserModel,
} from "./types" } from "./types"
const axiosInstance = axios.create({ baseURL: "./rest", withCredentials: true }) const axiosInstance = axios.create({ baseURL: "./rest", withCredentials: true })
@@ -42,34 +43,34 @@ axiosInstance.interceptors.response.use(
export const client = { export const client = {
category: { category: {
getRoot: () => axiosInstance.get<Category>("category/get"), getRoot: async () => await axiosInstance.get<Category>("category/get"),
modify: (req: CategoryModificationRequest) => axiosInstance.post("category/modify", req), modify: async (req: CategoryModificationRequest) => await axiosInstance.post("category/modify", req),
collapse: (req: CollapseRequest) => axiosInstance.post("category/collapse", req), collapse: async (req: CollapseRequest) => await axiosInstance.post("category/collapse", req),
getEntries: (req: GetEntriesPaginatedRequest) => axiosInstance.get<Entries>("category/entries", { params: req }), getEntries: async (req: GetEntriesPaginatedRequest) => await axiosInstance.get<Entries>("category/entries", { params: req }),
markEntries: (req: MarkRequest) => axiosInstance.post("category/mark", req), markEntries: async (req: MarkRequest) => await axiosInstance.post("category/mark", req),
add: (req: AddCategoryRequest) => axiosInstance.post("category/add", req), add: async (req: AddCategoryRequest) => await axiosInstance.post("category/add", req),
delete: (req: IDRequest) => axiosInstance.post("category/delete", req), delete: async (req: IDRequest) => await axiosInstance.post("category/delete", req),
}, },
entry: { entry: {
mark: (req: MarkRequest) => axiosInstance.post("entry/mark", req), mark: async (req: MarkRequest) => await axiosInstance.post("entry/mark", req),
markMultiple: (req: MultipleMarkRequest) => axiosInstance.post("entry/markMultiple", req), markMultiple: async (req: MultipleMarkRequest) => await axiosInstance.post("entry/markMultiple", req),
star: (req: StarRequest) => axiosInstance.post("entry/star", req), star: async (req: StarRequest) => await axiosInstance.post("entry/star", req),
getTags: () => axiosInstance.get<string[]>("entry/tags"), getTags: async () => await axiosInstance.get<string[]>("entry/tags"),
tag: (req: TagRequest) => axiosInstance.post("entry/tag", req), tag: async (req: TagRequest) => await axiosInstance.post("entry/tag", req),
}, },
feed: { feed: {
get: (id: string) => axiosInstance.get<Subscription>(`feed/get/${id}`), get: async (id: string) => await axiosInstance.get<Subscription>(`feed/get/${id}`),
modify: (req: FeedModificationRequest) => axiosInstance.post("feed/modify", req), modify: async (req: FeedModificationRequest) => await axiosInstance.post("feed/modify", req),
getEntries: (req: GetEntriesPaginatedRequest) => axiosInstance.get<Entries>("feed/entries", { params: req }), getEntries: async (req: GetEntriesPaginatedRequest) => await axiosInstance.get<Entries>("feed/entries", { params: req }),
markEntries: (req: MarkRequest) => axiosInstance.post("feed/mark", req), markEntries: async (req: MarkRequest) => await axiosInstance.post("feed/mark", req),
fetchFeed: (req: FeedInfoRequest) => axiosInstance.post<FeedInfo>("feed/fetch", req), fetchFeed: async (req: FeedInfoRequest) => await axiosInstance.post<FeedInfo>("feed/fetch", req),
refreshAll: () => axiosInstance.get("feed/refreshAll"), refreshAll: async () => await axiosInstance.get("feed/refreshAll"),
subscribe: (req: SubscribeRequest) => axiosInstance.post<number>("feed/subscribe", req), subscribe: async (req: SubscribeRequest) => await axiosInstance.post<number>("feed/subscribe", req),
unsubscribe: (req: IDRequest) => axiosInstance.post("feed/unsubscribe", req), unsubscribe: async (req: IDRequest) => await axiosInstance.post("feed/unsubscribe", req),
importOpml: (req: File) => { importOpml: async (req: File) => {
const formData = new FormData() const formData = new FormData()
formData.append("file", req) formData.append("file", req)
return axiosInstance.post("feed/import", formData, { return await axiosInstance.post("feed/import", formData, {
headers: { headers: {
"Content-Type": "multipart/form-data", "Content-Type": "multipart/form-data",
}, },
@@ -77,23 +78,23 @@ export const client = {
}, },
}, },
user: { user: {
login: (req: LoginRequest) => axiosInstance.post("user/login", req), login: async (req: LoginRequest) => await axiosInstance.post("user/login", req),
register: (req: RegistrationRequest) => axiosInstance.post("user/register", req), register: async (req: RegistrationRequest) => await axiosInstance.post("user/register", req),
passwordReset: (req: PasswordResetRequest) => axiosInstance.post("user/passwordReset", req), passwordReset: async (req: PasswordResetRequest) => await axiosInstance.post("user/passwordReset", req),
getSettings: () => axiosInstance.get<Settings>("user/settings"), getSettings: async () => await axiosInstance.get<Settings>("user/settings"),
saveSettings: (settings: Settings) => axiosInstance.post("user/settings", settings), saveSettings: async (settings: Settings) => await axiosInstance.post("user/settings", settings),
getProfile: () => axiosInstance.get<UserModel>("user/profile"), getProfile: async () => await axiosInstance.get<UserModel>("user/profile"),
saveProfile: (req: ProfileModificationRequest) => axiosInstance.post("user/profile", req), saveProfile: async (req: ProfileModificationRequest) => await axiosInstance.post("user/profile", req),
deleteProfile: () => axiosInstance.post("user/profile/deleteAccount"), deleteProfile: async () => await axiosInstance.post("user/profile/deleteAccount"),
}, },
server: { server: {
getServerInfos: () => axiosInstance.get<ServerInfo>("server/get"), getServerInfos: async () => await axiosInstance.get<ServerInfo>("server/get"),
}, },
admin: { admin: {
getAllUsers: () => axiosInstance.get<UserModel[]>("admin/user/getAll"), getAllUsers: async () => await axiosInstance.get<UserModel[]>("admin/user/getAll"),
saveUser: (req: UserModel) => axiosInstance.post("admin/user/save", req), saveUser: async (req: AdminSaveUserRequest) => await axiosInstance.post("admin/user/save", req),
deleteUser: (req: IDRequest) => axiosInstance.post("admin/user/delete", req), deleteUser: async (req: IDRequest) => await axiosInstance.post("admin/user/delete", req),
getMetrics: () => axiosInstance.get<Metrics>("admin/metrics"), getMetrics: async () => await axiosInstance.get<Metrics>("admin/metrics"),
}, },
} }
@@ -109,7 +110,7 @@ export const errorToStrings = (err: unknown) => {
if (err.response) { if (err.response) {
const { data } = err.response const { data } = err.response
if (typeof data === "string") strings.push(data) if (typeof data === "string") strings.push(data)
if (typeof data === "object" && data.message) strings.push(data.message) if (typeof data === "object" && data.message) strings.push(data.message as string)
if (typeof data === "object" && data.errors) strings = [...strings, ...data.errors] if (typeof data === "object" && data.errors) strings = [...strings, ...data.errors]
} }
} }

View File

@@ -1,11 +1,10 @@
import { t } from "@lingui/macro" import { t } from "@lingui/macro"
import { DEFAULT_THEME } from "@mantine/core" import { type IconType } from "react-icons"
import { IconType } from "react-icons"
import { FaAt } from "react-icons/fa" import { FaAt } from "react-icons/fa"
import { SiBuffer, SiFacebook, SiGmail, SiInstapaper, SiPocket, SiTumblr, SiTwitter } from "react-icons/si" import { SiBuffer, SiFacebook, SiGmail, SiInstapaper, SiPocket, SiTumblr, SiTwitter } from "react-icons/si"
import { Category, Entry, SharingSettings } from "./types" import { type Category, type Entry, type SharingSettings } from "./types"
const categories: { [key: string]: Category } = { const categories: Record<string, Category> = {
all: { all: {
id: "all", id: "all",
name: t`All`, name: t`All`,
@@ -86,7 +85,8 @@ export const Constants = {
categories, categories,
sharing, sharing,
layout: { layout: {
mobileBreakpoint: DEFAULT_THEME.breakpoints.md, mobileBreakpoint: 992,
mobileBreakpointName: "md",
headerHeight: 60, headerHeight: 60,
entryMaxWidth: 650, entryMaxWidth: 650,
isTopVisible: (div: HTMLElement) => div.getBoundingClientRect().top >= Constants.layout.headerHeight, isTopVisible: (div: HTMLElement) => div.getBoundingClientRect().top >= Constants.layout.headerHeight,

View File

@@ -1,12 +1,12 @@
/* eslint-disable import/first */ /* eslint-disable import/first */
import { configureStore } from "@reduxjs/toolkit" import { configureStore } from "@reduxjs/toolkit"
import { client } from "app/client" import { type client } from "app/client"
import { reducers } from "app/store" import { loadEntries, loadMoreEntries, markAllEntries, markEntry } from "app/entries/thunks"
import { Entries, Entry } from "app/types" import { reducers, type RootState } from "app/store"
import { AxiosResponse } from "axios" import { type Entries, type Entry } from "app/types"
import { type AxiosResponse } from "axios"
import { beforeEach, describe, expect, it, vi } from "vitest" import { beforeEach, describe, expect, it, vi } from "vitest"
import { mockReset } from "vitest-mock-extended" import { mockReset } from "vitest-mock-extended"
import { loadEntries, loadMoreEntries, markAllEntries, markEntry } from "./entries"
const mockClient = await vi.hoisted(async () => { const mockClient = await vi.hoisted(async () => {
const mockModule = await import("vitest-mock-extended") const mockModule = await import("vitest-mock-extended")
@@ -78,9 +78,10 @@ describe("entries", () => {
sourceWebsiteUrl: "", sourceWebsiteUrl: "",
entries: [{ id: "3" } as Entry], entries: [{ id: "3" } as Entry],
hasMore: true, hasMore: true,
loading: false,
scrollingToEntry: false, scrollingToEntry: false,
}, },
}, } as RootState,
}) })
const promise = store.dispatch(loadMoreEntries()) const promise = store.dispatch(loadMoreEntries())
@@ -102,9 +103,10 @@ describe("entries", () => {
sourceWebsiteUrl: "", sourceWebsiteUrl: "",
entries: [{ id: "3", read: false } as Entry, { id: "4", read: false } as Entry], entries: [{ id: "3", read: false } as Entry, { id: "4", read: false } as Entry],
hasMore: true, hasMore: true,
loading: false,
scrollingToEntry: false, scrollingToEntry: false,
}, },
}, } as RootState,
}) })
store.dispatch(markEntry({ entry: { id: "3" } as Entry, read: true })) store.dispatch(markEntry({ entry: { id: "3" } as Entry, read: true }))
@@ -128,9 +130,10 @@ describe("entries", () => {
sourceWebsiteUrl: "", sourceWebsiteUrl: "",
entries: [{ id: "3", read: false } as Entry, { id: "4", read: false } as Entry], entries: [{ id: "3", read: false } as Entry, { id: "4", read: false } as Entry],
hasMore: true, hasMore: true,
loading: false,
scrollingToEntry: false, scrollingToEntry: false,
}, },
}, } as RootState,
}) })
store.dispatch(markAllEntries({ sourceType: "category", req: { id: "all", read: true } })) store.dispatch(markAllEntries({ sourceType: "category", req: { id: "all", read: true } }))

View File

@@ -0,0 +1,134 @@
import { createSlice, type PayloadAction } from "@reduxjs/toolkit"
import { Constants } from "app/constants"
import { loadEntries, loadMoreEntries, markAllEntries, markEntry, markMultipleEntries, starEntry, tagEntry } from "app/entries/thunks"
import { type Entry } from "app/types"
export type EntrySourceType = "category" | "feed" | "tag"
export interface EntrySource {
type: EntrySourceType
id: string
}
export type ExpendableEntry = Entry & { expanded?: boolean }
interface EntriesState {
/** selected source */
source: EntrySource
sourceLabel: string
sourceWebsiteUrl: string
entries: ExpendableEntry[]
/** stores when the first batch of entries were retrieved
*
* this is used when marking all entries of a feed/category to only mark entries up to that timestamp as newer entries were potentially never shown
*/
timestamp?: number
selectedEntryId?: string
hasMore: boolean
loading: boolean
search?: string
scrollingToEntry: boolean
}
const initialState: EntriesState = {
source: {
type: "category",
id: Constants.categories.all.id,
},
sourceLabel: "",
sourceWebsiteUrl: "",
entries: [],
hasMore: true,
loading: false,
scrollingToEntry: false,
}
export const entriesSlice = createSlice({
name: "entries",
initialState,
reducers: {
setSelectedEntry: (state, action: PayloadAction<Entry>) => {
state.selectedEntryId = action.payload.id
},
setEntryExpanded: (state, action: PayloadAction<{ entry: Entry; expanded: boolean }>) => {
state.entries
.filter(e => e.id === action.payload.entry.id)
.forEach(e => {
e.expanded = action.payload.expanded
})
},
setScrollingToEntry: (state, action: PayloadAction<boolean>) => {
state.scrollingToEntry = action.payload
},
setSearch: (state, action: PayloadAction<string>) => {
state.search = action.payload
},
},
extraReducers: builder => {
builder.addCase(markEntry.pending, (state, action) => {
state.entries
.filter(e => e.id === action.meta.arg.entry.id)
.forEach(e => {
e.read = action.meta.arg.read
})
})
builder.addCase(markMultipleEntries.pending, (state, action) => {
state.entries
.filter(e => action.meta.arg.entries.some(e2 => e2.id === e.id))
.forEach(e => {
e.read = action.meta.arg.read
})
})
builder.addCase(markAllEntries.pending, (state, action) => {
state.entries
.filter(e => (action.meta.arg.req.olderThan ? e.date < action.meta.arg.req.olderThan : true))
.forEach(e => {
e.read = true
})
})
builder.addCase(starEntry.pending, (state, action) => {
state.entries
.filter(e => action.meta.arg.entry.id === e.id && action.meta.arg.entry.feedId === e.feedId)
.forEach(e => {
e.starred = action.meta.arg.starred
})
})
builder.addCase(loadEntries.pending, (state, action) => {
state.source = action.meta.arg.source
state.entries = []
state.timestamp = undefined
state.sourceLabel = ""
state.sourceWebsiteUrl = ""
state.hasMore = true
state.selectedEntryId = undefined
state.loading = true
})
builder.addCase(loadMoreEntries.pending, state => {
state.loading = true
})
builder.addCase(loadEntries.fulfilled, (state, action) => {
state.entries = action.payload.entries
state.timestamp = action.payload.timestamp
state.sourceLabel = action.payload.name
state.sourceWebsiteUrl = action.payload.feedLink
state.hasMore = action.payload.hasMore
state.loading = false
})
builder.addCase(loadMoreEntries.fulfilled, (state, action) => {
// remove already existing entries
const entriesToAdd = action.payload.entries.filter(e => !state.entries.some(e2 => e.id === e2.id))
state.entries = [...state.entries, ...entriesToAdd]
state.hasMore = action.payload.hasMore
state.loading = false
})
builder.addCase(tagEntry.pending, (state, action) => {
state.entries
.filter(e => +e.id === action.meta.arg.entryId)
.forEach(e => {
e.tags = action.meta.arg.tags
})
})
},
})
export const { setSearch } = entriesSlice.actions

View File

@@ -0,0 +1,240 @@
import { createAppAsyncThunk } from "app/async-thunk"
import { client } from "app/client"
import { Constants } from "app/constants"
import { entriesSlice, type EntrySource, type EntrySourceType, setSearch } from "app/entries/slice"
import type { RootState } from "app/store"
import { reloadTree } from "app/tree/thunks"
import type { Entry, MarkRequest, TagRequest } from "app/types"
import { reloadTags } from "app/user/thunks"
import { scrollToWithCallback } from "app/utils"
import { flushSync } from "react-dom"
const getEndpoint = (sourceType: EntrySourceType) =>
sourceType === "category" || sourceType === "tag" ? client.category.getEntries : client.feed.getEntries
export const loadEntries = createAppAsyncThunk(
"entries/load",
async (
arg: {
source: EntrySource
clearSearch: boolean
},
thunkApi
) => {
if (arg.clearSearch) thunkApi.dispatch(setSearch(""))
const state = thunkApi.getState()
const endpoint = getEndpoint(arg.source.type)
const result = await endpoint(buildGetEntriesPaginatedRequest(state, arg.source, 0))
return result.data
}
)
export const loadMoreEntries = createAppAsyncThunk("entries/loadMore", async (_, thunkApi) => {
const state = thunkApi.getState()
const { source } = state.entries
const offset =
state.user.settings?.readingMode === "all" ? state.entries.entries.length : state.entries.entries.filter(e => !e.read).length
const endpoint = getEndpoint(state.entries.source.type)
const result = await endpoint(buildGetEntriesPaginatedRequest(state, source, offset))
return result.data
})
const buildGetEntriesPaginatedRequest = (state: RootState, source: EntrySource, offset: number) => ({
id: source.type === "tag" ? Constants.categories.all.id : source.id,
order: state.user.settings?.readingOrder,
readType: state.user.settings?.readingMode,
offset,
limit: 50,
tag: source.type === "tag" ? source.id : undefined,
keywords: state.entries.search,
})
export const reloadEntries = createAppAsyncThunk("entries/reload", async (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) => {
const state = thunkApi.getState()
thunkApi.dispatch(setSearch(arg))
thunkApi.dispatch(loadEntries({ source: state.entries.source, clearSearch: false }))
})
export const markEntry = createAppAsyncThunk(
"entries/entry/mark",
(arg: { entry: Entry; read: boolean }) => {
client.entry.mark({
id: arg.entry.id,
read: arg.read,
})
},
{
condition: arg => arg.entry.read !== arg.read,
}
)
export const markMultipleEntries = createAppAsyncThunk(
"entries/entry/markMultiple",
async (
arg: {
entries: Entry[]
read: boolean
},
thunkApi
) => {
const requests: MarkRequest[] = arg.entries.map(e => ({
id: e.id,
read: arg.read,
}))
await client.entry.markMultiple({ requests })
thunkApi.dispatch(reloadTree())
}
)
export const markEntriesUpToEntry = createAppAsyncThunk("entries/entry/upToEntry", async (arg: Entry, thunkApi) => {
const state = thunkApi.getState()
const { entries } = state.entries
const index = entries.findIndex(e => e.id === arg.id)
if (index === -1) return
thunkApi.dispatch(
markMultipleEntries({
entries: entries.slice(0, index + 1),
read: true,
})
)
})
export const markAllEntries = createAppAsyncThunk(
"entries/entry/markAll",
async (
arg: {
sourceType: EntrySourceType
req: MarkRequest
},
thunkApi
) => {
const endpoint = arg.sourceType === "category" ? client.category.markEntries : client.feed.markEntries
await endpoint(arg.req)
thunkApi.dispatch(reloadEntries())
thunkApi.dispatch(reloadTree())
}
)
export const starEntry = createAppAsyncThunk("entries/entry/star", (arg: { entry: Entry; starred: boolean }) => {
client.entry.star({
id: arg.entry.id,
feedId: +arg.entry.feedId,
starred: arg.starred,
})
})
export const selectEntry = createAppAsyncThunk(
"entries/entry/select",
(
arg: {
entry: Entry
expand: boolean
markAsRead: boolean
scrollToEntry: boolean
},
thunkApi
) => {
const state = thunkApi.getState()
const entry = state.entries.entries.find(e => e.id === arg.entry.id)
if (!entry) return
// flushSync is required because we need the newly selected entry to be expanded
// and the previously selected entry to be collapsed to be able to scroll to the right position
flushSync(() => {
// mark as read if requested
if (arg.markAsRead) {
thunkApi.dispatch(markEntry({ entry, read: true }))
}
// set entry as selected
thunkApi.dispatch(entriesSlice.actions.setSelectedEntry(entry))
// expand if requested
const previouslySelectedEntry = state.entries.entries.find(e => e.id === state.entries.selectedEntryId)
if (previouslySelectedEntry) {
thunkApi.dispatch(
entriesSlice.actions.setEntryExpanded({
entry: previouslySelectedEntry,
expanded: false,
})
)
}
thunkApi.dispatch(entriesSlice.actions.setEntryExpanded({ entry, expanded: arg.expand }))
})
if (arg.scrollToEntry) {
const entryElement = document.getElementById(Constants.dom.entryId(entry))
if (entryElement) {
const alwaysScrollToEntry = state.user.settings?.alwaysScrollToEntry
const entryEntirelyVisible = Constants.layout.isTopVisible(entryElement) && Constants.layout.isBottomVisible(entryElement)
if (alwaysScrollToEntry || !entryEntirelyVisible) {
const scrollSpeed = state.user.settings?.scrollSpeed
thunkApi.dispatch(entriesSlice.actions.setScrollingToEntry(true))
scrollToEntry(entryElement, scrollSpeed, () => thunkApi.dispatch(entriesSlice.actions.setScrollingToEntry(false)))
}
}
}
}
)
const scrollToEntry = (entryElement: HTMLElement, scrollSpeed: number | undefined, onScrollEnded: () => void) => {
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,
behavior: scrollSpeed && scrollSpeed > 0 ? "smooth" : "auto",
},
onScrollEnded,
})
}
export const selectPreviousEntry = createAppAsyncThunk(
"entries/entry/selectPrevious",
(
arg: {
expand: boolean
markAsRead: boolean
scrollToEntry: boolean
},
thunkApi
) => {
const state = thunkApi.getState()
const { entries } = state.entries
const previousIndex = entries.findIndex(e => e.id === state.entries.selectedEntryId) - 1
if (previousIndex >= 0) {
thunkApi.dispatch(
selectEntry({
entry: entries[previousIndex],
expand: arg.expand,
markAsRead: arg.markAsRead,
scrollToEntry: arg.scrollToEntry,
})
)
}
}
)
export const selectNextEntry = createAppAsyncThunk(
"entries/entry/selectNext",
(
arg: {
expand: boolean
markAsRead: boolean
scrollToEntry: boolean
},
thunkApi
) => {
const state = thunkApi.getState()
const { entries } = state.entries
const nextIndex = entries.findIndex(e => e.id === state.entries.selectedEntryId) + 1
if (nextIndex < entries.length) {
thunkApi.dispatch(
selectEntry({
entry: entries[nextIndex],
expand: arg.expand,
markAsRead: arg.markAsRead,
scrollToEntry: arg.scrollToEntry,
})
)
}
}
)
export const tagEntry = createAppAsyncThunk("entries/entry/tag", async (arg: TagRequest, thunkApi) => {
await client.entry.tag(arg)
thunkApi.dispatch(reloadTags())
})

View File

@@ -1,6 +1,6 @@
import { redirectToCategory } from "app/redirect/thunks"
import { store } from "app/store" import { store } from "app/store"
import { describe, expect, it } from "vitest" import { describe, expect, it } from "vitest"
import { redirectToCategory } from "./redirect"
describe("redirects", () => { describe("redirects", () => {
it("redirects to category", async () => { it("redirects to category", async () => {

View File

@@ -0,0 +1,19 @@
import { createSlice, type PayloadAction } from "@reduxjs/toolkit"
interface RedirectState {
to?: string
}
const initialState: RedirectState = {}
export const redirectSlice = createSlice({
name: "redirect",
initialState,
reducers: {
redirectTo: (state, action: PayloadAction<string | undefined>) => {
state.to = action.payload
},
},
})
export const { redirectTo } = redirectSlice.actions

View File

@@ -0,0 +1,45 @@
import { createAppAsyncThunk } from "app/async-thunk"
import { Constants } from "app/constants"
import { redirectTo } from "app/redirect/slice"
export const redirectToLogin = createAppAsyncThunk("redirect/login", (_, thunkApi) => thunkApi.dispatch(redirectTo("/login")))
export const redirectToRegistration = createAppAsyncThunk("redirect/register", (_, thunkApi) => thunkApi.dispatch(redirectTo("/register")))
export const redirectToPasswordRecovery = createAppAsyncThunk("redirect/passwordRecovery", (_, thunkApi) =>
thunkApi.dispatch(redirectTo("/passwordRecovery"))
)
export const redirectToApiDocumentation = createAppAsyncThunk("redirect/api", (_, thunkApi) => thunkApi.dispatch(redirectTo("/api")))
export const redirectToSelectedSource = createAppAsyncThunk("redirect/selectedSource", (_, thunkApi) => {
const { source } = thunkApi.getState().entries
thunkApi.dispatch(redirectTo(`/app/${source.type}/${source.id}`))
})
export const redirectToCategory = createAppAsyncThunk("redirect/category", (id: string, thunkApi) =>
thunkApi.dispatch(redirectTo(`/app/category/${id}`))
)
export const redirectToRootCategory = createAppAsyncThunk(
"redirect/category/root",
async (_, thunkApi) => await thunkApi.dispatch(redirectToCategory(Constants.categories.all.id))
)
export const redirectToCategoryDetails = createAppAsyncThunk("redirect/category/details", (id: string, thunkApi) =>
thunkApi.dispatch(redirectTo(`/app/category/${id}/details`))
)
export const redirectToFeed = createAppAsyncThunk("redirect/feed", (id: string | number, thunkApi) =>
thunkApi.dispatch(redirectTo(`/app/feed/${id}`))
)
export const redirectToFeedDetails = createAppAsyncThunk("redirect/feed/details", (id: string, thunkApi) =>
thunkApi.dispatch(redirectTo(`/app/feed/${id}/details`))
)
export const redirectToTag = createAppAsyncThunk("redirect/tag", (id: string, thunkApi) => thunkApi.dispatch(redirectTo(`/app/tag/${id}`)))
export const redirectToTagDetails = createAppAsyncThunk("redirect/tag/details", (id: string, thunkApi) =>
thunkApi.dispatch(redirectTo(`/app/tag/${id}/details`))
)
export const redirectToAdd = createAppAsyncThunk("redirect/add", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/add")))
export const redirectToSettings = createAppAsyncThunk("redirect/settings", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/settings")))
export const redirectToAdminUsers = createAppAsyncThunk("redirect/admin/users", (_, thunkApi) =>
thunkApi.dispatch(redirectTo("/app/admin/users"))
)
export const redirectToMetrics = createAppAsyncThunk("redirect/admin/metrics", (_, thunkApi) =>
thunkApi.dispatch(redirectTo("/app/admin/metrics"))
)
export const redirectToDonate = createAppAsyncThunk("redirect/donate", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/donate")))
export const redirectToAbout = createAppAsyncThunk("redirect/about", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/about")))

View File

@@ -0,0 +1,29 @@
import { createSlice, type PayloadAction } from "@reduxjs/toolkit"
import { reloadServerInfos } from "app/server/thunks"
import { type ServerInfo } from "app/types"
interface ServerState {
serverInfos?: ServerInfo
webSocketConnected: boolean
}
const initialState: ServerState = {
webSocketConnected: false,
}
export const serverSlice = createSlice({
name: "server",
initialState,
reducers: {
setWebSocketConnected: (state, action: PayloadAction<boolean>) => {
state.webSocketConnected = action.payload
},
},
extraReducers: builder => {
builder.addCase(reloadServerInfos.fulfilled, (state, action) => {
state.serverInfos = action.payload
})
},
})
export const { setWebSocketConnected } = serverSlice.actions

View File

@@ -0,0 +1,4 @@
import { createAppAsyncThunk } from "app/async-thunk"
import { client } from "app/client"
export const reloadServerInfos = createAppAsyncThunk("server/infos", async () => await client.server.getServerInfos().then(r => r.data))

View File

@@ -1,357 +0,0 @@
import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit"
import { client } from "app/client"
import { Constants } from "app/constants"
import { RootState } from "app/store"
import { Entries, Entry, MarkRequest, TagRequest } from "app/types"
import { scrollToWithCallback } from "app/utils"
import { flushSync } from "react-dom"
// eslint-disable-next-line import/no-cycle
import { reloadTree } from "./tree"
// eslint-disable-next-line import/no-cycle
import { reloadTags } from "./user"
export type EntrySourceType = "category" | "feed" | "tag"
export type EntrySource = { type: EntrySourceType; id: string }
export type ExpendableEntry = Entry & { expanded?: boolean }
interface EntriesState {
/** selected source */
source: EntrySource
sourceLabel: string
sourceWebsiteUrl: string
entries: ExpendableEntry[]
/** stores when the first batch of entries were retrieved
*
* this is used when marking all entries of a feed/category to only mark entries up to that timestamp as newer entries were potentially never shown
*/
timestamp?: number
selectedEntryId?: string
hasMore: boolean
search?: string
scrollingToEntry: boolean
}
const initialState: EntriesState = {
source: {
type: "category",
id: Constants.categories.all.id,
},
sourceLabel: "",
sourceWebsiteUrl: "",
entries: [],
hasMore: true,
scrollingToEntry: false,
}
const getEndpoint = (sourceType: EntrySourceType) =>
sourceType === "category" || sourceType === "tag" ? client.category.getEntries : client.feed.getEntries
export const loadEntries = createAsyncThunk<
Entries,
{ source: EntrySource; clearSearch: boolean },
{
state: RootState
}
>("entries/load", async (arg, thunkApi) => {
if (arg.clearSearch) thunkApi.dispatch(setSearch(""))
const state = thunkApi.getState()
const endpoint = getEndpoint(arg.source.type)
const result = await endpoint(buildGetEntriesPaginatedRequest(state, arg.source, 0))
return result.data
})
export const loadMoreEntries = createAsyncThunk<
Entries,
void,
{
state: RootState
}
>("entries/loadMore", async (_, thunkApi) => {
const state = thunkApi.getState()
const { source } = state.entries
const offset =
state.user.settings?.readingMode === "all" ? state.entries.entries.length : state.entries.entries.filter(e => !e.read).length
const endpoint = getEndpoint(state.entries.source.type)
const result = await endpoint(buildGetEntriesPaginatedRequest(state, source, offset))
return result.data
})
const buildGetEntriesPaginatedRequest = (state: RootState, source: EntrySource, offset: number) => ({
id: source.type === "tag" ? Constants.categories.all.id : source.id,
order: state.user.settings?.readingOrder,
readType: state.user.settings?.readingMode,
offset,
limit: 50,
tag: source.type === "tag" ? source.id : undefined,
keywords: state.entries.search,
})
export const reloadEntries = createAsyncThunk<
void,
void,
{
state: RootState
}
>("entries/reload", async (arg, thunkApi) => {
const state = thunkApi.getState()
thunkApi.dispatch(loadEntries({ source: state.entries.source, clearSearch: false }))
})
export const search = createAsyncThunk<void, string, { state: RootState }>("entries/search", async (arg, thunkApi) => {
const state = thunkApi.getState()
thunkApi.dispatch(setSearch(arg))
thunkApi.dispatch(loadEntries({ source: state.entries.source, clearSearch: false }))
})
export const markEntry = createAsyncThunk(
"entries/entry/mark",
(arg: { entry: Entry; read: boolean }) => {
client.entry.mark({
id: arg.entry.id,
read: arg.read,
})
},
{
condition: arg => arg.entry.read !== arg.read,
}
)
export const markMultipleEntries = createAsyncThunk(
"entries/entry/markMultiple",
async (arg: { entries: Entry[]; read: boolean }, thunkApi) => {
const requests: MarkRequest[] = arg.entries.map(e => ({
id: e.id,
read: arg.read,
}))
await client.entry.markMultiple({ requests })
thunkApi.dispatch(reloadTree())
}
)
export const markEntriesUpToEntry = createAsyncThunk<void, Entry, { state: RootState }>(
"entries/entry/upToEntry",
async (arg, thunkApi) => {
const state = thunkApi.getState()
const { entries } = state.entries
const index = entries.findIndex(e => e.id === arg.id)
if (index === -1) return
thunkApi.dispatch(
markMultipleEntries({
entries: entries.slice(0, index + 1),
read: true,
})
)
}
)
export const markAllEntries = createAsyncThunk<
void,
{ sourceType: EntrySourceType; req: MarkRequest },
{
state: RootState
}
>("entries/entry/markAll", async (arg, thunkApi) => {
const endpoint = arg.sourceType === "category" ? client.category.markEntries : client.feed.markEntries
await endpoint(arg.req)
thunkApi.dispatch(reloadEntries())
thunkApi.dispatch(reloadTree())
})
export const starEntry = createAsyncThunk("entries/entry/star", (arg: { entry: Entry; starred: boolean }) => {
client.entry.star({
id: arg.entry.id,
feedId: +arg.entry.feedId,
starred: arg.starred,
})
})
export const selectEntry = createAsyncThunk<
void,
{
entry: Entry
expand: boolean
markAsRead: boolean
scrollToEntry: boolean
},
{ state: RootState }
>("entries/entry/select", (arg, thunkApi) => {
const state = thunkApi.getState()
const entry = state.entries.entries.find(e => e.id === arg.entry.id)
if (!entry) return
// flushSync is required because we need the newly selected entry to be expanded
// and the previously selected entry to be collapsed to be able to scroll to the right position
flushSync(() => {
// mark as read if requested
if (arg.markAsRead) {
thunkApi.dispatch(markEntry({ entry, read: true }))
}
// set entry as selected
thunkApi.dispatch(entriesSlice.actions.setSelectedEntry(entry))
// expand if requested
const previouslySelectedEntry = state.entries.entries.find(e => e.id === state.entries.selectedEntryId)
if (previouslySelectedEntry) {
thunkApi.dispatch(entriesSlice.actions.setEntryExpanded({ entry: previouslySelectedEntry, expanded: false }))
}
thunkApi.dispatch(entriesSlice.actions.setEntryExpanded({ entry, expanded: arg.expand }))
})
if (arg.scrollToEntry) {
const entryElement = document.getElementById(Constants.dom.entryId(entry))
if (entryElement) {
const alwaysScrollToEntry = state.user.settings?.alwaysScrollToEntry
const entryEntirelyVisible = Constants.layout.isTopVisible(entryElement) && Constants.layout.isBottomVisible(entryElement)
if (alwaysScrollToEntry || !entryEntirelyVisible) {
const scrollSpeed = state.user.settings?.scrollSpeed
thunkApi.dispatch(entriesSlice.actions.setScrollingToEntry(true))
scrollToEntry(entryElement, scrollSpeed, () => thunkApi.dispatch(entriesSlice.actions.setScrollingToEntry(false)))
}
}
}
})
const scrollToEntry = (entryElement: HTMLElement, scrollSpeed: number | undefined, onScrollEnded: () => void) => {
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,
behavior: scrollSpeed && scrollSpeed > 0 ? "smooth" : "auto",
},
onScrollEnded,
})
}
export const selectPreviousEntry = createAsyncThunk<
void,
{
expand: boolean
markAsRead: boolean
scrollToEntry: boolean
},
{ state: RootState }
>("entries/entry/selectPrevious", (arg, thunkApi) => {
const state = thunkApi.getState()
const { entries } = state.entries
const previousIndex = entries.findIndex(e => e.id === state.entries.selectedEntryId) - 1
if (previousIndex >= 0) {
thunkApi.dispatch(
selectEntry({
entry: entries[previousIndex],
expand: arg.expand,
markAsRead: arg.markAsRead,
scrollToEntry: arg.scrollToEntry,
})
)
}
})
export const selectNextEntry = createAsyncThunk<
void,
{
expand: boolean
markAsRead: boolean
scrollToEntry: boolean
},
{ state: RootState }
>("entries/entry/selectNext", (arg, thunkApi) => {
const state = thunkApi.getState()
const { entries } = state.entries
const nextIndex = entries.findIndex(e => e.id === state.entries.selectedEntryId) + 1
if (nextIndex < entries.length) {
thunkApi.dispatch(
selectEntry({
entry: entries[nextIndex],
expand: arg.expand,
markAsRead: arg.markAsRead,
scrollToEntry: arg.scrollToEntry,
})
)
}
})
export const tagEntry = createAsyncThunk<
void,
TagRequest,
{
state: RootState
}
>("entries/entry/tag", async (arg, thunkApi) => {
await client.entry.tag(arg)
thunkApi.dispatch(reloadTags())
})
export const entriesSlice = createSlice({
name: "entries",
initialState,
reducers: {
setSelectedEntry: (state, action: PayloadAction<Entry>) => {
state.selectedEntryId = action.payload.id
},
setEntryExpanded: (state, action: PayloadAction<{ entry: Entry; expanded: boolean }>) => {
state.entries
.filter(e => e.id === action.payload.entry.id)
.forEach(e => {
e.expanded = action.payload.expanded
})
},
setScrollingToEntry: (state, action: PayloadAction<boolean>) => {
state.scrollingToEntry = action.payload
},
setSearch: (state, action: PayloadAction<string>) => {
state.search = action.payload
},
},
extraReducers: builder => {
builder.addCase(markEntry.pending, (state, action) => {
state.entries
.filter(e => e.id === action.meta.arg.entry.id)
.forEach(e => {
e.read = action.meta.arg.read
})
})
builder.addCase(markMultipleEntries.pending, (state, action) => {
state.entries
.filter(e => action.meta.arg.entries.some(e2 => e2.id === e.id))
.forEach(e => {
e.read = action.meta.arg.read
})
})
builder.addCase(markAllEntries.pending, (state, action) => {
state.entries
.filter(e => (action.meta.arg.req.olderThan ? e.date < action.meta.arg.req.olderThan : true))
.forEach(e => {
e.read = true
})
})
builder.addCase(starEntry.pending, (state, action) => {
state.entries
.filter(e => action.meta.arg.entry.id === e.id && action.meta.arg.entry.feedId === e.feedId)
.forEach(e => {
e.starred = action.meta.arg.starred
})
})
builder.addCase(loadEntries.pending, (state, action) => {
state.source = action.meta.arg.source
state.entries = []
state.timestamp = undefined
state.sourceLabel = ""
state.sourceWebsiteUrl = ""
state.hasMore = true
state.selectedEntryId = undefined
})
builder.addCase(loadEntries.fulfilled, (state, action) => {
state.entries = action.payload.entries
state.timestamp = action.payload.timestamp
state.sourceLabel = action.payload.name
state.sourceWebsiteUrl = action.payload.feedLink
state.hasMore = action.payload.hasMore
})
builder.addCase(loadMoreEntries.fulfilled, (state, action) => {
// remove already existing entries
const entriesToAdd = action.payload.entries.filter(e => !state.entries.some(e2 => e.id === e2.id))
state.entries = [...state.entries, ...entriesToAdd]
state.hasMore = action.payload.hasMore
})
builder.addCase(tagEntry.pending, (state, action) => {
state.entries
.filter(e => +e.id === action.meta.arg.entryId)
.forEach(e => {
e.tags = action.meta.arg.tags
})
})
},
})
export const { setSearch } = entriesSlice.actions
export default entriesSlice.reducer

View File

@@ -1,69 +0,0 @@
import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit"
import { Constants } from "app/constants"
import { RootState } from "app/store"
interface RedirectState {
to?: string
}
const initialState: RedirectState = {}
export const redirectToLogin = createAsyncThunk("redirect/login", (_, thunkApi) => thunkApi.dispatch(redirectTo("/login")))
export const redirectToRegistration = createAsyncThunk("redirect/register", (_, thunkApi) => thunkApi.dispatch(redirectTo("/register")))
export const redirectToPasswordRecovery = createAsyncThunk("redirect/passwordRecovery", (_, thunkApi) =>
thunkApi.dispatch(redirectTo("/passwordRecovery"))
)
export const redirectToApiDocumentation = createAsyncThunk("redirect/api", (_, thunkApi) => thunkApi.dispatch(redirectTo("/api")))
export const redirectToSelectedSource = createAsyncThunk<
void,
void,
{
state: RootState
}
>("redirect/selectedSource", (_, thunkApi) => {
const { source } = thunkApi.getState().entries
thunkApi.dispatch(redirectTo(`/app/${source.type}/${source.id}`))
})
export const redirectToCategory = createAsyncThunk("redirect/category", (id: string, thunkApi) =>
thunkApi.dispatch(redirectTo(`/app/category/${id}`))
)
export const redirectToRootCategory = createAsyncThunk("redirect/category/root", (_, thunkApi) =>
thunkApi.dispatch(redirectToCategory(Constants.categories.all.id))
)
export const redirectToCategoryDetails = createAsyncThunk("redirect/category/details", (id: string, thunkApi) =>
thunkApi.dispatch(redirectTo(`/app/category/${id}/details`))
)
export const redirectToFeed = createAsyncThunk("redirect/feed", (id: string | number, thunkApi) =>
thunkApi.dispatch(redirectTo(`/app/feed/${id}`))
)
export const redirectToFeedDetails = createAsyncThunk("redirect/feed/details", (id: string, thunkApi) =>
thunkApi.dispatch(redirectTo(`/app/feed/${id}/details`))
)
export const redirectToTag = createAsyncThunk("redirect/tag", (id: string, thunkApi) => thunkApi.dispatch(redirectTo(`/app/tag/${id}`)))
export const redirectToTagDetails = createAsyncThunk("redirect/tag/details", (id: string, thunkApi) =>
thunkApi.dispatch(redirectTo(`/app/tag/${id}/details`))
)
export const redirectToAdd = createAsyncThunk("redirect/add", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/add")))
export const redirectToSettings = createAsyncThunk("redirect/settings", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/settings")))
export const redirectToAdminUsers = createAsyncThunk("redirect/admin/users", (_, thunkApi) =>
thunkApi.dispatch(redirectTo("/app/admin/users"))
)
export const redirectToMetrics = createAsyncThunk("redirect/admin/metrics", (_, thunkApi) =>
thunkApi.dispatch(redirectTo("/app/admin/metrics"))
)
export const redirectToDonate = createAsyncThunk("redirect/donate", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/donate")))
export const redirectToAbout = createAsyncThunk("redirect/about", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/about")))
export const redirectSlice = createSlice({
name: "redirect",
initialState,
reducers: {
redirectTo: (state, action: PayloadAction<string | undefined>) => {
state.to = action.payload
},
},
})
export const { redirectTo } = redirectSlice.actions
export default redirectSlice.reducer

View File

@@ -1,23 +0,0 @@
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"
import { client } from "app/client"
import { ServerInfo } from "app/types"
interface ServerState {
serverInfos?: ServerInfo
}
const initialState: ServerState = {}
export const reloadServerInfos = createAsyncThunk("server/infos", () => client.server.getServerInfos().then(r => r.data))
export const serverSlice = createSlice({
name: "server",
initialState,
reducers: {},
extraReducers: builder => {
builder.addCase(reloadServerInfos.fulfilled, (state, action) => {
state.serverInfos = action.payload
})
},
})
export default serverSlice.reducer

View File

@@ -1,177 +0,0 @@
import { t } from "@lingui/macro"
import { showNotification } from "@mantine/notifications"
import { createAsyncThunk, createSlice, isAnyOf } from "@reduxjs/toolkit"
import { client } from "app/client"
import { RootState } from "app/store"
import { ReadingMode, ReadingOrder, Settings, SharingSettings, UserModel } from "app/types"
// eslint-disable-next-line import/no-cycle
import { reloadEntries } from "./entries"
interface UserState {
settings?: Settings
profile?: UserModel
tags?: string[]
}
const initialState: UserState = {}
export const reloadSettings = createAsyncThunk("settings/reload", () => client.user.getSettings().then(r => r.data))
export const reloadProfile = createAsyncThunk("profile/reload", () => client.user.getProfile().then(r => r.data))
export const reloadTags = createAsyncThunk("entries/tags", () => client.entry.getTags().then(r => r.data))
export const changeReadingMode = createAsyncThunk<void, ReadingMode, { state: RootState }>(
"settings/readingMode",
(readingMode, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, readingMode })
thunkApi.dispatch(reloadEntries())
}
)
export const changeReadingOrder = createAsyncThunk<void, ReadingOrder, { state: RootState }>(
"settings/readingOrder",
(readingOrder, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, readingOrder })
thunkApi.dispatch(reloadEntries())
}
)
export const changeLanguage = createAsyncThunk<
void,
string,
{
state: RootState
}
>("settings/language", (language, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, language })
})
export const changeScrollSpeed = createAsyncThunk<
void,
boolean,
{
state: RootState
}
>("settings/scrollSpeed", (speed, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, scrollSpeed: speed ? 400 : 0 })
})
export const changeShowRead = createAsyncThunk<
void,
boolean,
{
state: RootState
}
>("settings/showRead", (showRead, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, showRead })
})
export const changeScrollMarks = createAsyncThunk<
void,
boolean,
{
state: RootState
}
>("settings/scrollMarks", (scrollMarks, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, scrollMarks })
})
export const changeAlwaysScrollToEntry = createAsyncThunk<
void,
boolean,
{
state: RootState
}
>("settings/alwaysScrollToEntry", (alwaysScrollToEntry, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, alwaysScrollToEntry })
})
export const changeSharingSetting = createAsyncThunk<
void,
{ site: keyof SharingSettings; value: boolean },
{
state: RootState
}
>("settings/sharingSetting", (sharingSetting, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({
...settings,
sharingSettings: {
...settings.sharingSettings,
[sharingSetting.site]: sharingSetting.value,
},
})
})
export const userSlice = createSlice({
name: "user",
initialState,
reducers: {},
extraReducers: builder => {
builder.addCase(reloadSettings.fulfilled, (state, action) => {
state.settings = action.payload
})
builder.addCase(reloadProfile.fulfilled, (state, action) => {
state.profile = action.payload
})
builder.addCase(reloadTags.fulfilled, (state, action) => {
state.tags = action.payload
})
builder.addCase(changeReadingMode.pending, (state, action) => {
if (!state.settings) return
state.settings.readingMode = action.meta.arg
})
builder.addCase(changeReadingOrder.pending, (state, action) => {
if (!state.settings) return
state.settings.readingOrder = action.meta.arg
})
builder.addCase(changeLanguage.pending, (state, action) => {
if (!state.settings) return
state.settings.language = action.meta.arg
})
builder.addCase(changeScrollSpeed.pending, (state, action) => {
if (!state.settings) return
state.settings.scrollSpeed = action.meta.arg ? 400 : 0
})
builder.addCase(changeShowRead.pending, (state, action) => {
if (!state.settings) return
state.settings.showRead = action.meta.arg
})
builder.addCase(changeScrollMarks.pending, (state, action) => {
if (!state.settings) return
state.settings.scrollMarks = action.meta.arg
})
builder.addCase(changeAlwaysScrollToEntry.pending, (state, action) => {
if (!state.settings) return
state.settings.alwaysScrollToEntry = action.meta.arg
})
builder.addCase(changeSharingSetting.pending, (state, action) => {
if (!state.settings) return
state.settings.sharingSettings[action.meta.arg.site] = action.meta.arg.value
})
builder.addMatcher(
isAnyOf(
changeLanguage.fulfilled,
changeScrollSpeed.fulfilled,
changeShowRead.fulfilled,
changeScrollMarks.fulfilled,
changeAlwaysScrollToEntry.fulfilled,
changeSharingSetting.fulfilled
),
() => {
showNotification({
message: t`Settings saved.`,
color: "green",
})
}
)
},
})
export default userSlice.reducer

View File

@@ -1,18 +1,18 @@
import { configureStore } from "@reduxjs/toolkit" import { configureStore } from "@reduxjs/toolkit"
import { setupListeners } from "@reduxjs/toolkit/query" import { setupListeners } from "@reduxjs/toolkit/query"
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux" import { entriesSlice } from "app/entries/slice"
import entriesReducer from "./slices/entries" import { redirectSlice } from "app/redirect/slice"
import redirectReducer from "./slices/redirect" import { serverSlice } from "app/server/slice"
import serverReducer from "./slices/server" import { treeSlice } from "app/tree/slice"
import treeReducer from "./slices/tree" import { userSlice } from "app/user/slice"
import userReducer from "./slices/user" import { type TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"
export const reducers = { export const reducers = {
entries: entriesReducer, entries: entriesSlice.reducer,
redirect: redirectReducer, redirect: redirectSlice.reducer,
tree: treeReducer, tree: treeSlice.reducer,
server: serverReducer, server: serverSlice.reducer,
user: userReducer, user: userSlice.reducer,
} }
export const store = configureStore({ reducer: reducers }) export const store = configureStore({ reducer: reducers })

View File

@@ -1,29 +1,21 @@
import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit" import { createSlice, type PayloadAction } from "@reduxjs/toolkit"
import { client } from "app/client" import { markEntry } from "app/entries/thunks"
import { Category, CollapseRequest } from "app/types" import { redirectTo } from "app/redirect/slice"
import { collapseTreeCategory, reloadTree } from "app/tree/thunks"
import { type Category } from "app/types"
import { visitCategoryTree } from "app/utils" import { visitCategoryTree } from "app/utils"
// eslint-disable-next-line import/no-cycle
import { markEntry } from "./entries"
import { redirectTo } from "./redirect"
interface TreeState { interface TreeState {
rootCategory?: Category rootCategory?: Category
mobileMenuOpen: boolean mobileMenuOpen: boolean
sidebarWidth: number
sidebarVisible: boolean sidebarVisible: boolean
} }
const initialState: TreeState = { const initialState: TreeState = {
mobileMenuOpen: false, mobileMenuOpen: false,
sidebarWidth: 350,
sidebarVisible: true, sidebarVisible: true,
} }
export const reloadTree = createAsyncThunk("tree/reload", () => client.category.getRoot().then(r => r.data))
export const collapseTreeCategory = createAsyncThunk("tree/category/collapse", async (req: CollapseRequest) =>
client.category.collapse(req)
)
export const treeSlice = createSlice({ export const treeSlice = createSlice({
name: "tree", name: "tree",
initialState, initialState,
@@ -31,9 +23,6 @@ export const treeSlice = createSlice({
setMobileMenuOpen: (state, action: PayloadAction<boolean>) => { setMobileMenuOpen: (state, action: PayloadAction<boolean>) => {
state.mobileMenuOpen = action.payload state.mobileMenuOpen = action.payload
}, },
setSidebarWidth: (state, action: PayloadAction<number>) => {
state.sidebarWidth = action.payload
},
toggleSidebar: state => { toggleSidebar: state => {
state.sidebarVisible = !state.sidebarVisible state.sidebarVisible = !state.sidebarVisible
}, },
@@ -64,5 +53,4 @@ export const treeSlice = createSlice({
}, },
}) })
export const { setMobileMenuOpen, setSidebarWidth, toggleSidebar } = treeSlice.actions export const { setMobileMenuOpen, toggleSidebar } = treeSlice.actions
export default treeSlice.reducer

View File

@@ -0,0 +1,9 @@
import { createAppAsyncThunk } from "app/async-thunk"
import { client } from "app/client"
import type { CollapseRequest } from "app/types"
export const reloadTree = createAppAsyncThunk("tree/reload", async () => await client.category.getRoot().then(r => r.data))
export const collapseTreeCategory = createAppAsyncThunk(
"tree/category/collapse",
async (req: CollapseRequest) => await client.category.collapse(req)
)

View File

@@ -3,38 +3,6 @@ export interface AddCategoryRequest {
parentId?: string parentId?: string
} }
export interface ApplicationSettings {
publicUrl: string
allowRegistrations: boolean
createDemoAccount: boolean
googleAnalyticsTrackingCode?: string
googleAuthKey?: string
backgroundThreads: number
databaseUpdateThreads: number
smtpHost?: string
smtpPort?: number
smtpTls?: boolean
smtpUserName?: string
smtpPassword?: string
smtpFromAddress?: string
graphiteEnabled?: boolean
graphitePrefix?: string
graphiteHost?: string
graphitePort?: number
graphiteInterval?: number
heavyLoad: boolean
pubsubhubbub: boolean
imageProxyEnabled: boolean
queryTimeout: number
keepStatusDays: number
maxFeedCapacity: number
refreshIntervalMinutes: number
cache: ApplicationSettingsCache
announcement?: string
userAgent?: string
unreadThreshold?: Date
}
export interface Category { export interface Category {
id: string id: string
parentId?: string parentId?: string
@@ -145,6 +113,7 @@ export interface MarkRequest {
id: string id: string
read: boolean read: boolean
olderThan?: number olderThan?: number
insertedBefore?: number
keywords?: string keywords?: string
excludedSubscriptions?: number[] excludedSubscriptions?: number[]
} }
@@ -166,7 +135,7 @@ export interface MetricMeter {
units: string units: string
} }
export type MetricTimer = { export interface MetricTimer {
count: number count: number
max: number max: number
mean: number mean: number
@@ -187,10 +156,10 @@ export type MetricTimer = {
} }
export interface Metrics { export interface Metrics {
counters: { [key: string]: MetricCounter } counters: Record<string, MetricCounter>
gauges: { [key: string]: MetricGauge } gauges: Record<string, MetricGauge>
meters: { [key: string]: MetricMeter } meters: Record<string, MetricMeter>
timers: { [key: string]: MetricTimer } timers: Record<string, MetricTimer>
} }
export interface MultipleMarkRequest { export interface MultipleMarkRequest {
@@ -222,6 +191,9 @@ export interface ServerInfo {
googleAnalyticsCode?: string googleAnalyticsCode?: string
smtpEnabled: boolean smtpEnabled: boolean
demoAccountEnabled: boolean demoAccountEnabled: boolean
websocketEnabled: boolean
websocketPingInterval: number
treeReloadInterval: number
} }
export interface Settings { export interface Settings {
@@ -234,6 +206,8 @@ export interface Settings {
customJs?: string customJs?: string
scrollSpeed: number scrollSpeed: number
alwaysScrollToEntry: boolean alwaysScrollToEntry: boolean
markAllAsReadConfirmation: boolean
customContextMenu: boolean
sharingSettings: SharingSettings sharingSettings: SharingSettings
} }
@@ -300,7 +274,14 @@ export interface UserModel {
admin: boolean admin: boolean
} }
export type ApplicationSettingsCache = "NOOP" | "REDIS" export interface AdminSaveUserRequest {
id?: number
name: string
email?: string
password?: string
enabled: boolean
admin: boolean
}
export type ReadingMode = "all" | "unread" export type ReadingMode = "all" | "unread"

View File

@@ -0,0 +1,102 @@
import { t } from "@lingui/macro"
import { showNotification } from "@mantine/notifications"
import { createSlice, isAnyOf } from "@reduxjs/toolkit"
import { type Settings, type UserModel } from "app/types"
import {
changeAlwaysScrollToEntry,
changeCustomContextMenu,
changeLanguage,
changeMarkAllAsReadConfirmation,
changeReadingMode,
changeReadingOrder,
changeScrollMarks,
changeScrollSpeed,
changeSharingSetting,
changeShowRead,
reloadProfile,
reloadSettings,
reloadTags,
} from "./thunks"
interface UserState {
settings?: Settings
profile?: UserModel
tags?: string[]
}
const initialState: UserState = {}
export const userSlice = createSlice({
name: "user",
initialState,
reducers: {},
extraReducers: builder => {
builder.addCase(reloadSettings.fulfilled, (state, action) => {
state.settings = action.payload
})
builder.addCase(reloadProfile.fulfilled, (state, action) => {
state.profile = action.payload
})
builder.addCase(reloadTags.fulfilled, (state, action) => {
state.tags = action.payload
})
builder.addCase(changeReadingMode.pending, (state, action) => {
if (!state.settings) return
state.settings.readingMode = action.meta.arg
})
builder.addCase(changeReadingOrder.pending, (state, action) => {
if (!state.settings) return
state.settings.readingOrder = action.meta.arg
})
builder.addCase(changeLanguage.pending, (state, action) => {
if (!state.settings) return
state.settings.language = action.meta.arg
})
builder.addCase(changeScrollSpeed.pending, (state, action) => {
if (!state.settings) return
state.settings.scrollSpeed = action.meta.arg ? 400 : 0
})
builder.addCase(changeShowRead.pending, (state, action) => {
if (!state.settings) return
state.settings.showRead = action.meta.arg
})
builder.addCase(changeScrollMarks.pending, (state, action) => {
if (!state.settings) return
state.settings.scrollMarks = action.meta.arg
})
builder.addCase(changeAlwaysScrollToEntry.pending, (state, action) => {
if (!state.settings) return
state.settings.alwaysScrollToEntry = action.meta.arg
})
builder.addCase(changeMarkAllAsReadConfirmation.pending, (state, action) => {
if (!state.settings) return
state.settings.markAllAsReadConfirmation = action.meta.arg
})
builder.addCase(changeCustomContextMenu.pending, (state, action) => {
if (!state.settings) return
state.settings.customContextMenu = action.meta.arg
})
builder.addCase(changeSharingSetting.pending, (state, action) => {
if (!state.settings) return
state.settings.sharingSettings[action.meta.arg.site] = action.meta.arg.value
})
builder.addMatcher(
isAnyOf(
changeLanguage.fulfilled,
changeScrollSpeed.fulfilled,
changeShowRead.fulfilled,
changeScrollMarks.fulfilled,
changeAlwaysScrollToEntry.fulfilled,
changeMarkAllAsReadConfirmation.fulfilled,
changeCustomContextMenu.fulfilled,
changeSharingSetting.fulfilled
),
() => {
showNotification({
message: t`Settings saved.`,
color: "green",
})
}
)
},
})

View File

@@ -0,0 +1,78 @@
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"
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))
export const reloadTags = createAppAsyncThunk("entries/tags", async () => await client.entry.getTags().then(r => r.data))
export const changeReadingMode = createAppAsyncThunk("settings/readingMode", (readingMode: ReadingMode, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, readingMode })
thunkApi.dispatch(reloadEntries())
})
export const changeReadingOrder = createAppAsyncThunk("settings/readingOrder", (readingOrder: ReadingOrder, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, readingOrder })
thunkApi.dispatch(reloadEntries())
})
export const changeLanguage = createAppAsyncThunk("settings/language", (language: string, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, language })
})
export const changeScrollSpeed = createAppAsyncThunk("settings/scrollSpeed", (speed: boolean, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, scrollSpeed: speed ? 400 : 0 })
})
export const changeShowRead = createAppAsyncThunk("settings/showRead", (showRead: boolean, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, showRead })
})
export const changeScrollMarks = createAppAsyncThunk("settings/scrollMarks", (scrollMarks: boolean, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, scrollMarks })
})
export const changeAlwaysScrollToEntry = createAppAsyncThunk("settings/alwaysScrollToEntry", (alwaysScrollToEntry: boolean, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, alwaysScrollToEntry })
})
export const changeMarkAllAsReadConfirmation = createAppAsyncThunk(
"settings/markAllAsReadConfirmation",
(markAllAsReadConfirmation: boolean, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, markAllAsReadConfirmation })
}
)
export const changeCustomContextMenu = createAppAsyncThunk("settings/customContextMenu", (customContextMenu: boolean, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, customContextMenu })
})
export const changeSharingSetting = createAppAsyncThunk(
"settings/sharingSetting",
(
sharingSetting: {
site: keyof SharingSettings
value: boolean
},
thunkApi
) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({
...settings,
sharingSettings: {
...settings.sharingSettings,
[sharingSetting.site]: sharingSetting.value,
},
})
}
)

View File

@@ -1,5 +1,5 @@
import { throttle } from "throttle-debounce" import { throttle } from "throttle-debounce"
import { Category } from "./types" import { type Category } from "./types"
export function visitCategoryTree(category: Category, visitor: (category: Category) => void): void { export function visitCategoryTree(category: Category, visitor: (category: Category) => void): void {
visitor(category) visitor(category)

View File

@@ -1,20 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg height="512" width="512" viewBox="0 0 6.5625 6.5625" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"> <svg height="512" width="512" viewBox="0 0 6.5625 6.5625" xmlns="http://www.w3.org/2000/svg">
<sodipodi:namedview guidetolerance="10"> <rect fill="#f88a14" rx="0.7" ry="0.7" height="6.5625" width="6.5625" />
<sodipodi:guide position="154.17325,117.15254" orientation="0,1"/> <path d="m1.9761,1.5289c2.9002,0,2.9002,2.9101,2.9002,2.9101" fill="none" stroke="#FFF" stroke-linecap="round"
<sodipodi:guide position="154.17325,166.44575" orientation="0,1"/> stroke-width="0.78125" />
<sodipodi:guide position="154.17325,288.40369" orientation="0,1"/> <path d="m1.9688,2.875c1.5705-0.00908,1.5705,1.5639,1.5705,1.5639" fill="none" stroke="#FFF" stroke-linecap="round"
<sodipodi:guide position="380.44742,392.71992" orientation="0,1"/> stroke-width="0.78125" />
<sodipodi:guide position="101.9661,166.44575" orientation="1,0"/> <path d="m2.6503,4.4062c0,0.23366-0.10712,0.47418-0.24663,0.6537-0.1814,0.2333-0.5705,0.5618-0.6913,0.5653,0.0402-0.0662,0.263-0.5654,0.2563-0.5654-0.36423,0-0.6595-0.29265-0.6595-0.65365s0.29527-0.65365,0.6595-0.65365,0.68159,0.29265,0.68159,0.65365z"
<sodipodi:guide position="276.13119,288.40369" orientation="1,0"/> fill="#FFF" />
<sodipodi:guide position="380.44742,165.67871" orientation="1,0"/>
<sodipodi:guide position="154.17325,288.40369" orientation="1,0"/>
<sodipodi:guide position="154.17325,166.44575" orientation="-0.70710678,0.70710678"/>
<sodipodi:guide position="123.21968,135.49218" orientation="1,0"/>
<sodipodi:guide position="123.21968,135.49218" orientation="0,1"/>
</sodipodi:namedview>
<rect fill="#f88a14" rx="0.7" ry="0.7" height="6.5625" width="6.5625"/>
<path d="m1.9761,1.5289c2.9002,0,2.9002,2.9101,2.9002,2.9101" fill="none" stroke="#FFF" stroke-linecap="round" stroke-width="0.78125"/>
<path d="m1.9688,2.875c1.5705-0.00908,1.5705,1.5639,1.5705,1.5639" fill="none" stroke="#FFF" stroke-linecap="round" stroke-width="0.78125"/>
<path d="m2.6503,4.4062c0,0.23366-0.10712,0.47418-0.24663,0.6537-0.1814,0.2333-0.5705,0.5618-0.6913,0.5653,0.0402-0.0662,0.263-0.5654,0.2563-0.5654-0.36423,0-0.6595-0.29265-0.6595-0.65365s0.29527-0.65365,0.6595-0.65365,0.68159,0.29265,0.68159,0.65365z" fill="#FFF"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 791 B

View File

@@ -1,15 +1,14 @@
import { ActionIcon, Button, Tooltip, useMantineTheme } from "@mantine/core" import { ActionIcon, Button, type ButtonVariant, Tooltip, useMantineTheme } from "@mantine/core"
import { ActionIconProps } from "@mantine/core/lib/ActionIcon/ActionIcon" import { type ActionIconVariant } from "@mantine/core/lib/components/ActionIcon/ActionIcon"
import { ButtonProps } from "@mantine/core/lib/Button/Button"
import { useActionButton } from "hooks/useActionButton" import { useActionButton } from "hooks/useActionButton"
import { forwardRef, MouseEventHandler, ReactNode } from "react" import { forwardRef, type MouseEventHandler, type ReactNode } from "react"
interface ActionButtonProps { interface ActionButtonProps {
className?: string className?: string
icon?: ReactNode icon?: ReactNode
label: ReactNode label: ReactNode
onClick?: MouseEventHandler onClick?: MouseEventHandler
variant?: ActionIconProps["variant"] & ButtonProps["variant"] variant?: ActionIconVariant & ButtonVariant
hideLabelOnDesktop?: boolean hideLabelOnDesktop?: boolean
showLabelOnMobile?: boolean showLabelOnMobile?: boolean
} }
@@ -29,7 +28,7 @@ export const ActionButton = forwardRef<HTMLButtonElement, ActionButtonProps>((pr
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
) : ( ) : (
<Button ref={ref} variant={variant} size="xs" className={props.className} leftIcon={props.icon} onClick={props.onClick}> <Button ref={ref} variant={variant} size="xs" className={props.className} leftSection={props.icon} onClick={props.onClick}>
{props.label} {props.label}
</Button> </Button>
) )

View File

@@ -1,9 +1,10 @@
import { Trans } from "@lingui/macro" import { Trans } from "@lingui/macro"
import { Box, Alert as MantineAlert } from "@mantine/core" import { Alert as MantineAlert, Box } from "@mantine/core"
import { Fragment } from "react" import { Fragment } from "react"
import { TbAlertCircle, TbAlertTriangle, TbCircleCheck } from "react-icons/tb" import { TbAlertCircle, TbAlertTriangle, TbCircleCheck } from "react-icons/tb"
type Level = "error" | "warning" | "success" type Level = "error" | "warning" | "success"
export interface ErrorsAlertProps { export interface ErrorsAlertProps {
level?: Level level?: Level
messages: string[] messages: string[]
@@ -31,8 +32,6 @@ export function Alert(props: ErrorsAlertProps) {
color = "green" color = "green"
icon = <TbCircleCheck /> icon = <TbCircleCheck />
break break
default:
throw Error(`unsupported level: ${level}`)
} }
return ( return (

View File

@@ -0,0 +1,36 @@
import { Trans } from "@lingui/macro"
import { Box, Dialog, Text } from "@mantine/core"
import { useAppSelector } from "app/store"
import { Content } from "components/content/Content"
import { useAsync } from "react-async-hook"
import useLocalStorage from "use-local-storage"
const sha256Hex = async (input: string | undefined) => {
const data = new TextEncoder().encode(input)
const buffer = await crypto.subtle.digest("SHA-256", data)
const array = Array.from(new Uint8Array(buffer))
return array.map(b => b.toString(16).padStart(2, "0")).join("")
}
export function AnnouncementDialog() {
const announcement = useAppSelector(state => state.server.serverInfos?.announcement)
const announcementHash = useAsync(sha256Hex, [announcement]).result
const [localStorageHash, setLocalStorageHash] = useLocalStorage("announcement-hash", "no-hash")
const opened = !!announcementHash && announcementHash !== localStorageHash
const onClosed = () => setLocalStorageHash(announcementHash)
if (!announcement) return null
return (
<Dialog opened={opened} withCloseButton onClose={onClosed} size="xl" radius="md">
<Box>
<Text fw="bold">
<Trans>Announcement</Trans>
</Text>
</Box>
<Box>
<Content content={announcement} />
</Box>
</Dialog>
)
}

View File

@@ -1,5 +1,5 @@
import { ErrorPage } from "pages/ErrorPage" import { ErrorPage } from "pages/ErrorPage"
import React, { ReactNode } from "react" import React, { type ReactNode } from "react"
interface ErrorBoundaryProps { interface ErrorBoundaryProps {
children?: ReactNode children?: ReactNode

View File

@@ -1,6 +1,8 @@
import { Box, Center, createStyles } from "@mantine/core" import { Box, Center, type MantineTheme, useMantineTheme } from "@mantine/core"
import { useColorScheme } from "hooks/useColorScheme"
import { useState } from "react" import { useState } from "react"
import { TbPhoto } from "react-icons/tb" import { TbPhoto } from "react-icons/tb"
import { tss } from "tss"
interface ImageWithPlaceholderWhileLoadingProps { interface ImageWithPlaceholderWhileLoadingProps {
src: string src: string
@@ -12,21 +14,47 @@ interface ImageWithPlaceholderWhileLoadingProps {
placeholderHeight?: number placeholderHeight?: number
placeholderBackgroundColor?: string placeholderBackgroundColor?: string
placeholderIconSize?: number placeholderIconSize?: number
placeholderIconColor?: string
} }
const useStyles = createStyles((theme, props: ImageWithPlaceholderWhileLoadingProps) => ({ const useStyles = tss
placeholder: { .withParams<{
width: props.placeholderWidth ?? 400, theme: MantineTheme
height: props.placeholderHeight ?? 600, colorScheme: "light" | "dark"
maxWidth: "100%", placeholderWidth?: number
color: props.placeholderIconColor ?? theme.fn.variant({ color: theme.primaryColor, variant: "subtle" }).color, placeholderHeight?: number
backgroundColor: props.placeholderBackgroundColor ?? (theme.colorScheme === "dark" ? theme.colors.dark[5] : theme.colors.gray[1]), placeholderBackgroundColor?: string
}, }>()
})) .create(props => ({
placeholder: {
width: props.placeholderWidth ?? 400,
height: props.placeholderHeight ?? 600,
maxWidth: "100%",
backgroundColor:
props.placeholderBackgroundColor ??
(props.colorScheme === "dark" ? props.theme.colors.dark[5] : props.theme.colors.gray[1]),
},
}))
export function ImageWithPlaceholderWhileLoading(props: ImageWithPlaceholderWhileLoadingProps) { export function ImageWithPlaceholderWhileLoading({
const { classes } = useStyles(props) alt,
height,
placeholderBackgroundColor,
placeholderHeight,
placeholderIconSize,
placeholderWidth,
src,
title,
width,
}: ImageWithPlaceholderWhileLoadingProps) {
const theme = useMantineTheme()
const colorScheme = useColorScheme()
const { classes } = useStyles({
theme,
colorScheme,
placeholderWidth,
placeholderHeight,
placeholderBackgroundColor,
})
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
return ( return (
@@ -35,17 +63,17 @@ export function ImageWithPlaceholderWhileLoading(props: ImageWithPlaceholderWhil
<Box> <Box>
<Center className={classes.placeholder}> <Center className={classes.placeholder}>
<div> <div>
<TbPhoto size={props.placeholderIconSize ?? 48} /> <TbPhoto size={placeholderIconSize ?? 48} />
</div> </div>
</Center> </Center>
</Box> </Box>
)} )}
<img <img
src={props.src} src={src}
alt={props.alt} alt={alt}
title={props.title} title={title}
width={props.width} width={width}
height={props.height} height={height}
onLoad={() => setLoading(false)} onLoad={() => setLoading(false)}
style={{ display: loading ? "none" : "block" }} style={{ display: loading ? "none" : "block" }}
/> />

View File

@@ -4,64 +4,64 @@ import { Constants } from "app/constants"
export function KeyboardShortcutsHelp() { export function KeyboardShortcutsHelp() {
return ( return (
<Stack spacing="xs"> <Stack gap="xs">
<Table striped highlightOnHover> <Table striped highlightOnHover>
<tbody> <Table.Tbody>
<tr> <Table.Tr>
<td> <Table.Td>
<Trans>Refresh</Trans> <Trans>Refresh</Trans>
</td> </Table.Td>
<td> <Table.Td>
<Kbd>R</Kbd> <Kbd>R</Kbd>
</td> </Table.Td>
</tr> </Table.Tr>
<tr> <Table.Tr>
<td> <Table.Td>
<Trans>Open next entry</Trans> <Trans>Open next entry</Trans>
</td> </Table.Td>
<td> <Table.Td>
<Kbd>J</Kbd> <Kbd>J</Kbd>
</td> </Table.Td>
</tr> </Table.Tr>
<tr> <Table.Tr>
<td> <Table.Td>
<Trans>Open previous entry</Trans> <Trans>Open previous entry</Trans>
</td> </Table.Td>
<td> <Table.Td>
<Kbd>K</Kbd> <Kbd>K</Kbd>
</td> </Table.Td>
</tr> </Table.Tr>
<tr> <Table.Tr>
<td> <Table.Td>
<Trans>Set focus on next entry without opening it</Trans> <Trans>Set focus on next entry without opening it</Trans>
</td> </Table.Td>
<td> <Table.Td>
<Kbd>N</Kbd> <Kbd>N</Kbd>
</td> </Table.Td>
</tr> </Table.Tr>
<tr> <Table.Tr>
<td> <Table.Td>
<Trans>Set focus on previous entry without opening it</Trans> <Trans>Set focus on previous entry without opening it</Trans>
</td> </Table.Td>
<td> <Table.Td>
<Kbd>P</Kbd> <Kbd>P</Kbd>
</td> </Table.Td>
</tr> </Table.Tr>
<tr> <Table.Tr>
<td> <Table.Td>
<Trans>Move the page down</Trans> <Trans>Move the page down</Trans>
</td> </Table.Td>
<td> <Table.Td>
<Kbd> <Kbd>
<Trans>Space</Trans> <Trans>Space</Trans>
</Kbd> </Kbd>
</td> </Table.Td>
</tr> </Table.Tr>
<tr> <Table.Tr>
<td> <Table.Td>
<Trans>Move the page up</Trans> <Trans>Move the page up</Trans>
</td> </Table.Td>
<td> <Table.Td>
<Kbd> <Kbd>
<Trans>Shift</Trans> <Trans>Shift</Trans>
</Kbd> </Kbd>
@@ -69,77 +69,85 @@ export function KeyboardShortcutsHelp() {
<Kbd> <Kbd>
<Trans>Space</Trans> <Trans>Space</Trans>
</Kbd> </Kbd>
</td> </Table.Td>
</tr> </Table.Tr>
<tr> <Table.Tr>
<td> <Table.Td>
<Trans>Open/close current entry</Trans> <Trans>Open/close current entry</Trans>
</td> </Table.Td>
<td> <Table.Td>
<Kbd>O</Kbd> <Kbd>O</Kbd>
<span>, </span> <span>, </span>
<Kbd> <Kbd>
<Trans>Enter</Trans> <Trans>Enter</Trans>
</Kbd> </Kbd>
</td> </Table.Td>
</tr> </Table.Tr>
<tr> <Table.Tr>
<td> <Table.Td>
<Trans>Open current entry in a new tab</Trans> <Trans>Open current entry in a new tab</Trans>
</td> </Table.Td>
<td> <Table.Td>
<Kbd>V</Kbd> <Kbd>V</Kbd>
</td> </Table.Td>
</tr> </Table.Tr>
<tr> <Table.Tr>
<td> <Table.Td>
<Trans>Open current entry in a new tab in the background</Trans> <Trans>Open current entry in a new tab in the background</Trans>
</td> </Table.Td>
<td> <Table.Td>
<Kbd>B</Kbd> <Kbd>B</Kbd>
<span>*, </span> <span>*, </span>
<Kbd> <Kbd>
<Trans>Middle click</Trans> <Trans>Middle click</Trans>
</Kbd> </Kbd>
</td> </Table.Td>
</tr> </Table.Tr>
<tr> <Table.Tr>
<td> <Table.Td>
<Trans>Toggle read status of current entry</Trans> <Trans>Toggle read status of current entry</Trans>
</td> </Table.Td>
<td> <Table.Td>
<Kbd>M</Kbd> <Kbd>M</Kbd>
<span>, </span> <span>, </span>
<Trans>Swipe header to the right</Trans> <Trans>Swipe header to the right</Trans>
</td> </Table.Td>
</tr> </Table.Tr>
<tr> <Table.Tr>
<td> <Table.Td>
<Trans>Toggle starred status of current entry</Trans>
</Table.Td>
<Table.Td>
<Kbd>S</Kbd>
</Table.Td>
</Table.Tr>
<Table.Tr>
<Table.Td>
<Trans>Mark all entries as read</Trans> <Trans>Mark all entries as read</Trans>
</td> </Table.Td>
<td> <Table.Td>
<Kbd> <Kbd>
<Trans>Shift</Trans> <Trans>Shift</Trans>
</Kbd> </Kbd>
<span> + </span> <span> + </span>
<Kbd>A</Kbd> <Kbd>A</Kbd>
</td> </Table.Td>
</tr> </Table.Tr>
<tr> <Table.Tr>
<td> <Table.Td>
<Trans>Go to the All view</Trans> <Trans>Go to the All view</Trans>
</td> </Table.Td>
<td> <Table.Td>
<Kbd>G</Kbd> <Kbd>G</Kbd>
<span> </span> <span> </span>
<Kbd>A</Kbd> <Kbd>A</Kbd>
</td> </Table.Td>
</tr> </Table.Tr>
<tr> <Table.Tr>
<td> <Table.Td>
<Trans>Navigate to a subscription by entering its name</Trans> <Trans>Navigate to a subscription by entering its name</Trans>
</td> </Table.Td>
<td> <Table.Td>
<Kbd> <Kbd>
<Trans>Ctrl</Trans> <Trans>Ctrl</Trans>
</Kbd> </Kbd>
@@ -149,45 +157,59 @@ export function KeyboardShortcutsHelp() {
<Kbd>G</Kbd> <Kbd>G</Kbd>
<span> </span> <span> </span>
<Kbd>U</Kbd> <Kbd>U</Kbd>
</td> </Table.Td>
</tr> </Table.Tr>
<tr> <Table.Tr>
<td> <Table.Td>
<Trans>Show entry menu (desktop)</Trans> <Trans>Show entry menu (desktop)</Trans>
</td> </Table.Td>
<td> <Table.Td>
<Kbd> <Kbd>
<Trans>Right click</Trans> <Trans>Right click</Trans>
</Kbd> </Kbd>
</td> </Table.Td>
</tr> </Table.Tr>
<tr> <Table.Tr>
<td> <Table.Td>
<Trans>Show native menu (desktop)</Trans>
</Table.Td>
<Table.Td>
<Kbd>
<Trans>Shift</Trans>
</Kbd>
<span> + </span>
<Kbd>
<Trans>Right click</Trans>
</Kbd>
</Table.Td>
</Table.Tr>
<Table.Tr>
<Table.Td>
<Trans>Show entry menu (mobile)</Trans> <Trans>Show entry menu (mobile)</Trans>
</td> </Table.Td>
<td> <Table.Td>
<Kbd> <Kbd>
<Trans>Long press</Trans> <Trans>Long press</Trans>
</Kbd> </Kbd>
</td> </Table.Td>
</tr> </Table.Tr>
<tr> <Table.Tr>
<td> <Table.Td>
<Trans>Toggle sidebar</Trans> <Trans>Toggle sidebar</Trans>
</td> </Table.Td>
<td> <Table.Td>
<Kbd>F</Kbd> <Kbd>F</Kbd>
</td> </Table.Td>
</tr> </Table.Tr>
<tr> <Table.Tr>
<td> <Table.Td>
<Trans>Show keyboard shortcut help</Trans> <Trans>Show keyboard shortcut help</Trans>
</td> </Table.Td>
<td> <Table.Td>
<Kbd>?</Kbd> <Kbd>?</Kbd>
</td> </Table.Td>
</tr> </Table.Tr>
</tbody> </Table.Tbody>
</Table> </Table>
<Box> <Box>
<span>* </span> <span>* </span>

View File

@@ -3,7 +3,7 @@ import { Center, Loader as MantineLoader } from "@mantine/core"
export function Loader() { export function Loader() {
return ( return (
<Center> <Center>
<MantineLoader size="xl" variant="bars" /> <MantineLoader size="lg" type="bars" />
</Center> </Center>
) )
} }

View File

@@ -6,5 +6,5 @@ export interface LogoProps {
} }
export function Logo(props: LogoProps) { export function Logo(props: LogoProps) {
return <Image src={logo} width={props.size} /> return <Image src={logo} w={props.size} />
} }

View File

@@ -2,7 +2,7 @@ import { Trans } from "@lingui/macro"
import { Box, Button, Checkbox, Group, PasswordInput, Stack, TextInput } from "@mantine/core" import { Box, Button, Checkbox, Group, PasswordInput, Stack, TextInput } from "@mantine/core"
import { useForm } from "@mantine/form" import { useForm } from "@mantine/form"
import { client, errorToStrings } from "app/client" import { client, errorToStrings } from "app/client"
import { UserModel } from "app/types" import { type AdminSaveUserRequest, type UserModel } from "app/types"
import { Alert } from "components/Alert" import { Alert } from "components/Alert"
import { useAsyncCallback } from "react-async-hook" import { useAsyncCallback } from "react-async-hook"
import { TbDeviceFloppy } from "react-icons/tb" import { TbDeviceFloppy } from "react-icons/tb"
@@ -14,8 +14,12 @@ interface UserEditProps {
} }
export function UserEdit(props: UserEditProps) { export function UserEdit(props: UserEditProps) {
const form = useForm<UserModel>({ const form = useForm<AdminSaveUserRequest>({
initialValues: props.user ?? ({ enabled: true } as UserModel), initialValues: props.user ?? {
name: "",
enabled: true,
admin: false,
},
}) })
const saveUser = useAsyncCallback(client.admin.saveUser, { onSuccess: props.onSave }) const saveUser = useAsyncCallback(client.admin.saveUser, { onSuccess: props.onSave })
@@ -35,11 +39,11 @@ export function UserEdit(props: UserEditProps) {
<Checkbox label={<Trans>Admin</Trans>} {...form.getInputProps("admin", { type: "checkbox" })} /> <Checkbox label={<Trans>Admin</Trans>} {...form.getInputProps("admin", { type: "checkbox" })} />
<Checkbox label={<Trans>Enabled</Trans>} {...form.getInputProps("enabled", { type: "checkbox" })} /> <Checkbox label={<Trans>Enabled</Trans>} {...form.getInputProps("enabled", { type: "checkbox" })} />
<Group> <Group justify="right">
<Button variant="default" onClick={props.onCancel}> <Button variant="default" onClick={props.onCancel}>
<Trans>Cancel</Trans> <Trans>Cancel</Trans>
</Button> </Button>
<Button type="submit" leftIcon={<TbDeviceFloppy size={16} />} loading={saveUser.loading}> <Button type="submit" leftSection={<TbDeviceFloppy size={16} />} loading={saveUser.loading}>
<Trans>Save</Trans> <Trans>Save</Trans>
</Button> </Button>
</Group> </Group>

View File

@@ -1,7 +1,7 @@
import { Input, Textarea } from "@mantine/core" import { Input, Textarea } from "@mantine/core"
import RichCodeEditor from "components/code/RichCodeEditor" import RichCodeEditor from "components/code/RichCodeEditor"
import { useMobile } from "hooks/useMobile" import { useMobile } from "hooks/useMobile"
import { ReactNode } from "react" import { type ReactNode } from "react"
interface CodeEditorProps { interface CodeEditorProps {
description?: ReactNode description?: ReactNode

View File

@@ -1,5 +1,5 @@
import { useMantineTheme } from "@mantine/core"
import { Loader } from "components/Loader" import { Loader } from "components/Loader"
import { useColorScheme } from "hooks/useColorScheme"
import { useAsync } from "react-async-hook" import { useAsync } from "react-async-hook"
const init = async () => { const init = async () => {
@@ -32,8 +32,8 @@ interface RichCodeEditorProps {
} }
function RichCodeEditor(props: RichCodeEditorProps) { function RichCodeEditor(props: RichCodeEditorProps) {
const theme = useMantineTheme() const colorScheme = useColorScheme()
const editorTheme = theme.colorScheme === "dark" ? "vs-dark" : "light" const editorTheme = colorScheme === "dark" ? "vs-dark" : "light"
const { result: Editor } = useAsync(init, []) const { result: Editor } = useAsync(init, [])
if (!Editor) return <Loader /> if (!Editor) return <Loader />

View File

@@ -0,0 +1,11 @@
import { TypographyStylesProvider } from "@mantine/core"
import { type ReactNode } from "react"
/**
* This component is used to provide basic styles to html typography elements.
*
* see https://mantine.dev/core/typography-styles-provider/
*/
export const BasicHtmlStyles = (props: { children: ReactNode }) => {
return <TypographyStylesProvider pl={0}>{props.children}</TypographyStylesProvider>
}

View File

@@ -1,22 +1,25 @@
import { Box, createStyles, Mark, TypographyStylesProvider } from "@mantine/core" import { Box, Mark } from "@mantine/core"
import { Constants } from "app/constants" import { Constants } from "app/constants"
import { calculatePlaceholderSize } from "app/utils" import { calculatePlaceholderSize } from "app/utils"
import { BasicHtmlStyles } from "components/content/BasicHtmlStyles"
import { ImageWithPlaceholderWhileLoading } from "components/ImageWithPlaceholderWhileLoading" import { ImageWithPlaceholderWhileLoading } from "components/ImageWithPlaceholderWhileLoading"
import escapeStringRegexp from "escape-string-regexp" import escapeStringRegexp from "escape-string-regexp"
import { ChildrenNode, Interweave, Matcher, MatchResponse, Node, TransformCallback } from "interweave" import { type ChildrenNode, Interweave, Matcher, type MatchResponse, type Node, type TransformCallback } from "interweave"
import React from "react" import React from "react"
import { tss } from "tss"
export interface ContentProps { export interface ContentProps {
content: string content: string
highlight?: string highlight?: string
} }
const useStyles = createStyles(theme => ({ const useStyles = tss.create(() => ({
content: { content: {
// break long links or long words // break long links or long words
overflowWrap: "anywhere", overflowWrap: "anywhere",
"& a": { "& a": {
color: theme.fn.variant({ color: theme.primaryColor, variant: "subtle" }).color, color: "inherit",
textDecoration: "underline",
}, },
"& iframe": { "& iframe": {
maxWidth: "100%", maxWidth: "100%",
@@ -61,7 +64,7 @@ const transform: TransformCallback = node => {
} }
class HighlightMatcher extends Matcher { class HighlightMatcher extends Matcher {
private search: string private readonly search: string
constructor(search: string) { constructor(search: string) {
super("highlight") super("highlight")
@@ -73,12 +76,10 @@ class HighlightMatcher extends Matcher {
return this.doMatch(string, new RegExp(pattern, "i"), () => ({})) return this.doMatch(string, new RegExp(pattern, "i"), () => ({}))
} }
// eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-unused-vars
replaceWith(children: ChildrenNode, props: unknown): Node { replaceWith(children: ChildrenNode, props: unknown): Node {
return <Mark>{children}</Mark> return <Mark>{children}</Mark>
} }
// eslint-disable-next-line class-methods-use-this
asTag(): string { asTag(): string {
return "span" return "span"
} }
@@ -90,12 +91,13 @@ const Content = React.memo((props: ContentProps) => {
const matchers = props.highlight ? [new HighlightMatcher(props.highlight)] : [] const matchers = props.highlight ? [new HighlightMatcher(props.highlight)] : []
return ( return (
<TypographyStylesProvider> <BasicHtmlStyles>
<Box className={classes.content}> <Box className={classes.content}>
<Interweave content={props.content} transform={transform} matchers={matchers} /> <Interweave content={props.content} transform={transform} matchers={matchers} />
</Box> </Box>
</TypographyStylesProvider> </BasicHtmlStyles>
) )
}) })
Content.displayName = "Content"
export { Content } export { Content }

View File

@@ -1,26 +1,24 @@
import { TypographyStylesProvider } from "@mantine/core" import { BasicHtmlStyles } from "components/content/BasicHtmlStyles"
import { ImageWithPlaceholderWhileLoading } from "components/ImageWithPlaceholderWhileLoading" import { ImageWithPlaceholderWhileLoading } from "components/ImageWithPlaceholderWhileLoading"
export function Enclosure(props: { enclosureType: string; enclosureUrl: string }) { export function Enclosure(props: { enclosureType: string; enclosureUrl: string }) {
const hasVideo = props.enclosureType && props.enclosureType.indexOf("video") === 0 const hasVideo = props.enclosureType?.startsWith("video")
const hasAudio = props.enclosureType && props.enclosureType.indexOf("audio") === 0 const hasAudio = props.enclosureType?.startsWith("audio")
const hasImage = props.enclosureType && props.enclosureType.indexOf("image") === 0 const hasImage = props.enclosureType?.startsWith("image")
return ( return (
<TypographyStylesProvider> <BasicHtmlStyles>
{hasVideo && ( {hasVideo && (
// eslint-disable-next-line jsx-a11y/media-has-caption
<video controls> <video controls>
<source src={props.enclosureUrl} type={props.enclosureType} /> <source src={props.enclosureUrl} type={props.enclosureType} />
</video> </video>
)} )}
{hasAudio && ( {hasAudio && (
// eslint-disable-next-line jsx-a11y/media-has-caption
<audio controls> <audio controls>
<source src={props.enclosureUrl} type={props.enclosureType} /> <source src={props.enclosureUrl} type={props.enclosureType} />
</audio> </audio>
)} )}
{hasImage && <ImageWithPlaceholderWhileLoading src={props.enclosureUrl} alt="enclosure" />} {hasImage && <ImageWithPlaceholderWhileLoading src={props.enclosureUrl} alt="enclosure" />}
</TypographyStylesProvider> </BasicHtmlStyles>
) )
} }

View File

@@ -1,8 +1,9 @@
import { Trans } from "@lingui/macro" import { Trans } from "@lingui/macro"
import { Box } from "@mantine/core"
import { openModal } from "@mantine/modals" import { openModal } from "@mantine/modals"
import { Constants } from "app/constants" import { Constants } from "app/constants"
import { type ExpendableEntry } from "app/entries/slice"
import { import {
ExpendableEntry,
loadMoreEntries, loadMoreEntries,
markAllEntries, markAllEntries,
markEntry, markEntry,
@@ -10,10 +11,11 @@ import {
selectEntry, selectEntry,
selectNextEntry, selectNextEntry,
selectPreviousEntry, selectPreviousEntry,
} from "app/slices/entries" starEntry,
import { redirectToRootCategory } from "app/slices/redirect" } from "app/entries/thunks"
import { toggleSidebar } from "app/slices/tree" import { redirectToRootCategory } from "app/redirect/thunks"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { toggleSidebar } from "app/tree/slice"
import { KeyboardShortcutsHelp } from "components/KeyboardShortcutsHelp" import { KeyboardShortcutsHelp } from "components/KeyboardShortcutsHelp"
import { Loader } from "components/Loader" import { Loader } from "components/Loader"
import { useBrowserExtension } from "hooks/useBrowserExtension" import { useBrowserExtension } from "hooks/useBrowserExtension"
@@ -31,9 +33,11 @@ export function FeedEntries() {
const entriesTimestamp = useAppSelector(state => state.entries.timestamp) const entriesTimestamp = useAppSelector(state => state.entries.timestamp)
const selectedEntryId = useAppSelector(state => state.entries.selectedEntryId) const selectedEntryId = useAppSelector(state => state.entries.selectedEntryId)
const hasMore = useAppSelector(state => state.entries.hasMore) const hasMore = useAppSelector(state => state.entries.hasMore)
const loading = useAppSelector(state => state.entries.loading)
const scrollMarks = useAppSelector(state => state.user.settings?.scrollMarks) const scrollMarks = useAppSelector(state => state.user.settings?.scrollMarks)
const scrollingToEntry = useAppSelector(state => state.entries.scrollingToEntry) const scrollingToEntry = useAppSelector(state => state.entries.scrollingToEntry)
const sidebarVisible = useAppSelector(state => state.tree.sidebarVisible) const sidebarVisible = useAppSelector(state => state.tree.sidebarVisible)
const customContextMenu = useAppSelector(state => state.user.settings?.customContextMenu)
const { viewMode } = useViewMode() const { viewMode } = useViewMode()
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const { openLinkInBackgroundTab } = useBrowserExtension() const { openLinkInBackgroundTab } = useBrowserExtension()
@@ -62,6 +66,8 @@ export function FeedEntries() {
const contextMenu = useContextMenu() const contextMenu = useContextMenu()
const headerRightClicked = (entry: ExpendableEntry, event: React.MouseEvent) => { const headerRightClicked = (entry: ExpendableEntry, event: React.MouseEvent) => {
if (event.shiftKey || !customContextMenu) return
event.preventDefault() event.preventDefault()
contextMenu.show({ contextMenu.show({
id: Constants.dom.entryContextMenuId(entry), id: Constants.dom.entryContextMenuId(entry),
@@ -85,7 +91,7 @@ export function FeedEntries() {
) )
} }
const swipedRight = (entry: ExpendableEntry) => dispatch(markEntry({ entry, read: !entry.read })) const swipedRight = async (entry: ExpendableEntry) => await dispatch(markEntry({ entry, read: !entry.read }))
// close context menu on scroll // close context menu on scroll
useEffect(() => { useEffect(() => {
@@ -122,42 +128,50 @@ export function FeedEntries() {
return () => window.removeEventListener("scroll", listener) return () => window.removeEventListener("scroll", listener)
}, [dispatch, contextMenu, entries, viewMode, scrollMarks, scrollingToEntry]) }, [dispatch, contextMenu, entries, viewMode, scrollMarks, scrollingToEntry])
useMousetrap("r", () => dispatch(reloadEntries())) useMousetrap("r", async () => await dispatch(reloadEntries()))
useMousetrap("j", () => useMousetrap(
dispatch( "j",
selectNextEntry({ async () =>
expand: true, await dispatch(
markAsRead: true, selectNextEntry({
scrollToEntry: true, expand: true,
}) markAsRead: true,
) scrollToEntry: true,
})
)
) )
useMousetrap("n", () => useMousetrap(
dispatch( "n",
selectNextEntry({ async () =>
expand: false, await dispatch(
markAsRead: false, selectNextEntry({
scrollToEntry: true, expand: false,
}) markAsRead: false,
) scrollToEntry: true,
})
)
) )
useMousetrap("k", () => useMousetrap(
dispatch( "k",
selectPreviousEntry({ async () =>
expand: true, await dispatch(
markAsRead: true, selectPreviousEntry({
scrollToEntry: true, expand: true,
}) markAsRead: true,
) scrollToEntry: true,
})
)
) )
useMousetrap("p", () => useMousetrap(
dispatch( "p",
selectPreviousEntry({ async () =>
expand: false, await dispatch(
markAsRead: false, selectPreviousEntry({
scrollToEntry: true, expand: false,
}) markAsRead: false,
) scrollToEntry: true,
})
)
) )
useMousetrap("space", () => { useMousetrap("space", () => {
if (selectedEntry) { if (selectedEntry) {
@@ -252,6 +266,11 @@ export function FeedEntries() {
if (!selectedEntry) return if (!selectedEntry) return
dispatch(markEntry({ entry: selectedEntry, read: !selectedEntry.read })) dispatch(markEntry({ entry: selectedEntry, read: !selectedEntry.read }))
}) })
useMousetrap("s", () => {
// toggle starred status
if (!selectedEntry) return
dispatch(starEntry({ entry: selectedEntry, starred: !selectedEntry.starred }))
})
useMousetrap("shift+a", () => { useMousetrap("shift+a", () => {
// mark all entries as read // mark all entries as read
dispatch( dispatch(
@@ -260,12 +279,13 @@ export function FeedEntries() {
req: { req: {
id: source.id, id: source.id,
read: true, read: true,
olderThan: entriesTimestamp, olderThan: Date.now(),
insertedBefore: entriesTimestamp,
}, },
}) })
) )
}) })
useMousetrap("g a", () => dispatch(redirectToRootCategory())) useMousetrap("g a", async () => await dispatch(redirectToRootCategory()))
useMousetrap("f", () => dispatch(toggleSidebar())) useMousetrap("f", () => dispatch(toggleSidebar()))
useMousetrap("?", () => useMousetrap("?", () =>
openModal({ openModal({
@@ -280,9 +300,9 @@ export function FeedEntries() {
<InfiniteScroll <InfiniteScroll
id="entries" id="entries"
initialLoad={false} initialLoad={false}
loadMore={() => dispatch(loadMoreEntries())} loadMore={async () => await (!loading && dispatch(loadMoreEntries()))}
hasMore={hasMore} hasMore={hasMore}
loader={<Loader key={0} />} loader={<Box key={0}>{loading && <Loader />}</Box>}
> >
{entries.map(entry => ( {entries.map(entry => (
<div <div
@@ -300,7 +320,7 @@ export function FeedEntries() {
onHeaderClick={event => headerClicked(entry, event)} onHeaderClick={event => headerClicked(entry, event)}
onHeaderRightClick={event => headerRightClicked(entry, event)} onHeaderRightClick={event => headerRightClicked(entry, event)}
onBodyClick={() => bodyClicked(entry)} onBodyClick={() => bodyClicked(entry)}
onSwipedRight={() => swipedRight(entry)} onSwipedRight={async () => await swipedRight(entry)}
/> />
</div> </div>
))} ))}

View File

@@ -1,10 +1,11 @@
import { Box, createStyles, Divider, Paper } from "@mantine/core" import { Box, Divider, type MantineRadius, type MantineSpacing, type MantineTheme, Paper, useMantineTheme } from "@mantine/core"
import { MantineNumberSize } from "@mantine/styles"
import { Constants } from "app/constants" import { Constants } from "app/constants"
import { Entry, ViewMode } from "app/types" import { type Entry, type ViewMode } from "app/types"
import { useColorScheme } from "hooks/useColorScheme"
import { useViewMode } from "hooks/useViewMode" import { useViewMode } from "hooks/useViewMode"
import React from "react" import React from "react"
import { useSwipeable } from "react-swipeable" import { useSwipeable } from "react-swipeable"
import { tss } from "tss"
import { FeedEntryBody } from "./FeedEntryBody" import { FeedEntryBody } from "./FeedEntryBody"
import { FeedEntryCompactHeader } from "./FeedEntryCompactHeader" import { FeedEntryCompactHeader } from "./FeedEntryCompactHeader"
import { FeedEntryContextMenu } from "./FeedEntryContextMenu" import { FeedEntryContextMenu } from "./FeedEntryContextMenu"
@@ -23,85 +24,107 @@ interface FeedEntryProps {
onSwipedRight: () => void onSwipedRight: () => void
} }
const useStyles = createStyles((theme, props: FeedEntryProps & { viewMode?: ViewMode }) => { const useStyles = tss
let backgroundColor .withParams<{
if (theme.colorScheme === "dark") { theme: MantineTheme
backgroundColor = props.entry.read ? "inherit" : theme.colors.dark[5] colorScheme: "light" | "dark"
} else { read: boolean
backgroundColor = props.entry.read && !props.expanded ? theme.colors.gray[0] : "inherit" expanded: boolean
} viewMode: ViewMode
rtl: boolean
showSelectionIndicator: boolean
maxWidth?: number
}>()
.create(({ theme, colorScheme, read, expanded, viewMode, rtl, showSelectionIndicator, maxWidth }) => {
let backgroundColor
if (colorScheme === "dark") {
backgroundColor = read ? "inherit" : theme.colors.dark[5]
} else {
backgroundColor = read && !expanded ? theme.colors.gray[0] : "inherit"
}
let marginY = 10 let marginY = 10
if (props.viewMode === "title") { if (viewMode === "title") {
marginY = 2 marginY = 2
} else if (props.viewMode === "cozy") { } else if (viewMode === "cozy") {
marginY = 6 marginY = 6
} }
let mobileMarginY = 6 let mobileMarginY = 6
if (props.viewMode === "title") { if (viewMode === "title") {
mobileMarginY = 2 mobileMarginY = 2
} else if (props.viewMode === "cozy") { } else if (viewMode === "cozy") {
mobileMarginY = 4 mobileMarginY = 4
} }
let backgroundHoverColor = backgroundColor let backgroundHoverColor = backgroundColor
if (!props.expanded && !props.entry.read) { if (!expanded && !read) {
backgroundHoverColor = theme.colorScheme === "dark" ? theme.colors.dark[6] : theme.colors.gray[1] backgroundHoverColor = colorScheme === "dark" ? theme.colors.dark[6] : theme.colors.gray[1]
} }
let paperBorderLeftColor let paperBorderLeftColor
if (props.showSelectionIndicator) { if (showSelectionIndicator) {
const borderLeftColor = theme.colorScheme === "dark" ? theme.colors.orange[4] : theme.colors.orange[6] const borderLeftColor = colorScheme === "dark" ? theme.colors[theme.primaryColor][4] : theme.colors[theme.primaryColor][6]
paperBorderLeftColor = `${borderLeftColor} !important` paperBorderLeftColor = `${borderLeftColor} !important`
} }
return { return {
paper: { paper: {
backgroundColor, backgroundColor,
borderLeftColor: paperBorderLeftColor, borderLeftColor: paperBorderLeftColor,
marginTop: marginY, marginTop: marginY,
marginBottom: marginY, marginBottom: marginY,
[theme.fn.smallerThan(Constants.layout.mobileBreakpoint)]: { [`@media (max-width: ${Constants.layout.mobileBreakpoint}px)`]: {
marginTop: mobileMarginY, marginTop: mobileMarginY,
marginBottom: mobileMarginY, marginBottom: mobileMarginY,
}, },
"@media (hover: hover)": { "@media (hover: hover)": {
"&:hover": { "&:hover": {
backgroundColor: backgroundHoverColor, backgroundColor: backgroundHoverColor,
},
}, },
}, },
}, headerLink: {
headerLink: { color: "inherit",
color: "inherit", textDecoration: "none",
textDecoration: "none", },
}, body: {
body: { direction: rtl ? "rtl" : "ltr",
direction: props.entry.rtl ? "rtl" : "ltr", maxWidth: maxWidth ?? "100%",
maxWidth: props.maxWidth ?? "100%", },
}, }
} })
})
export function FeedEntry(props: FeedEntryProps) { export function FeedEntry(props: FeedEntryProps) {
const theme = useMantineTheme()
const colorScheme = useColorScheme()
const { viewMode } = useViewMode() const { viewMode } = useViewMode()
const { classes, cx } = useStyles({ ...props, viewMode }) const { classes, cx } = useStyles({
theme,
colorScheme,
read: props.entry.read,
expanded: props.expanded,
viewMode,
rtl: props.entry.rtl,
showSelectionIndicator: props.showSelectionIndicator,
maxWidth: props.maxWidth,
})
const swipeHandlers = useSwipeable({ const swipeHandlers = useSwipeable({
onSwipedRight: props.onSwipedRight, onSwipedRight: props.onSwipedRight,
}) })
let paddingX: MantineNumberSize = "xs" let paddingX: MantineSpacing = "xs"
if (viewMode === "title" || viewMode === "cozy") paddingX = 6 if (viewMode === "title" || viewMode === "cozy") paddingX = 6
let paddingY: MantineNumberSize = "xs" let paddingY: MantineSpacing = "xs"
if (viewMode === "title") { if (viewMode === "title") {
paddingY = 4 paddingY = 4
} else if (viewMode === "cozy") { } else if (viewMode === "cozy") {
paddingY = 8 paddingY = 8
} }
let borderRadius: MantineNumberSize = "sm" let borderRadius: MantineRadius = "sm"
if (viewMode === "title") { if (viewMode === "title") {
borderRadius = 0 borderRadius = 0
} else if (viewMode === "cozy") { } else if (viewMode === "cozy") {

View File

@@ -1,6 +1,6 @@
import { Box } from "@mantine/core" import { Box } from "@mantine/core"
import { useAppSelector } from "app/store" import { useAppSelector } from "app/store"
import { Entry } from "app/types" import { type Entry } from "app/types"
import { Content } from "./Content" import { Content } from "./Content"
import { Enclosure } from "./Enclosure" import { Enclosure } from "./Enclosure"
import { Media } from "./Media" import { Media } from "./Media"

View File

@@ -1,7 +1,9 @@
import { Box, createStyles, Text } from "@mantine/core" import { Box, Text } from "@mantine/core"
import { Entry } from "app/types" import { type Entry } from "app/types"
import { RelativeDate } from "components/RelativeDate" import { RelativeDate } from "components/RelativeDate"
import { OnDesktop } from "components/responsive/OnDesktop" import { OnDesktop } from "components/responsive/OnDesktop"
import { useColorScheme } from "hooks/useColorScheme"
import { tss } from "tss"
import { FeedEntryTitle } from "./FeedEntryTitle" import { FeedEntryTitle } from "./FeedEntryTitle"
import { FeedFavicon } from "./FeedFavicon" import { FeedFavicon } from "./FeedFavicon"
@@ -9,39 +11,49 @@ export interface FeedEntryHeaderProps {
entry: Entry entry: Entry
} }
const useStyles = createStyles((theme, props: FeedEntryHeaderProps) => ({ const useStyles = tss
wrapper: { .withParams<{
display: "flex", colorScheme: "light" | "dark"
alignItems: "center", read: boolean
columnGap: "10px", }>()
}, .create(({ colorScheme, read }) => ({
title: { wrapper: {
flexGrow: 1, display: "flex",
fontWeight: theme.colorScheme === "light" && !props.entry.read ? "bold" : "inherit", alignItems: "center",
whiteSpace: "nowrap", columnGap: "10px",
overflow: "hidden", },
textOverflow: "ellipsis", title: {
}, flexGrow: 1,
feedName: { fontWeight: colorScheme === "light" && !read ? "bold" : "inherit",
width: "145px", whiteSpace: "nowrap",
minWidth: "145px", overflow: "hidden",
whiteSpace: "nowrap", textOverflow: "ellipsis",
overflow: "hidden", },
textOverflow: "ellipsis", feedName: {
}, width: "145px",
date: { minWidth: "145px",
whiteSpace: "nowrap", whiteSpace: "nowrap",
}, overflow: "hidden",
})) textOverflow: "ellipsis",
},
date: {
whiteSpace: "nowrap",
},
}))
export function FeedEntryCompactHeader(props: FeedEntryHeaderProps) { export function FeedEntryCompactHeader(props: FeedEntryHeaderProps) {
const { classes } = useStyles(props) const colorScheme = useColorScheme()
const { classes } = useStyles({
colorScheme,
read: props.entry.read,
})
return ( return (
<Box className={classes.wrapper}> <Box className={classes.wrapper}>
<Box> <Box>
<FeedFavicon url={props.entry.iconUrl} /> <FeedFavicon url={props.entry.iconUrl} />
</Box> </Box>
<OnDesktop> <OnDesktop>
<Text color="dimmed" className={classes.feedName}> <Text c="dimmed" className={classes.feedName}>
{props.entry.feedName} {props.entry.feedName}
</Text> </Text>
</OnDesktop> </OnDesktop>
@@ -49,7 +61,7 @@ export function FeedEntryCompactHeader(props: FeedEntryHeaderProps) {
<FeedEntryTitle entry={props.entry} /> <FeedEntryTitle entry={props.entry} />
</Box> </Box>
<OnDesktop> <OnDesktop>
<Text color="dimmed" className={classes.date}> <Text c="dimmed" className={classes.date}>
<RelativeDate date={props.entry.date} /> <RelativeDate date={props.entry.date} />
</Text> </Text>
</OnDesktop> </OnDesktop>

View File

@@ -1,40 +1,50 @@
import { Trans } from "@lingui/macro" import { Trans } from "@lingui/macro"
import { createStyles, Group } from "@mantine/core" import { Group, type MantineTheme, useMantineTheme } from "@mantine/core"
import { Constants } from "app/constants" import { Constants } from "app/constants"
import { markEntriesUpToEntry, markEntry, starEntry } from "app/slices/entries" import { markEntriesUpToEntry, markEntry, starEntry } from "app/entries/thunks"
import { redirectToFeed } from "app/slices/redirect" import { redirectToFeed } from "app/redirect/thunks"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { Entry } from "app/types" import { type Entry } from "app/types"
import { truncate } from "app/utils" import { truncate } from "app/utils"
import { useBrowserExtension } from "hooks/useBrowserExtension" import { useBrowserExtension } from "hooks/useBrowserExtension"
import { useColorScheme } from "hooks/useColorScheme"
import { Item, Menu, Separator } from "react-contexify" import { Item, Menu, Separator } from "react-contexify"
import { TbArrowBarToDown, TbExternalLink, TbEyeCheck, TbEyeOff, TbRss, TbStar, TbStarOff } from "react-icons/tb" import { TbArrowBarToDown, TbExternalLink, TbEyeCheck, TbEyeOff, TbRss, TbStar, TbStarOff } from "react-icons/tb"
import { tss } from "tss"
interface FeedEntryContextMenuProps { interface FeedEntryContextMenuProps {
entry: Entry entry: Entry
} }
const iconSize = 16 const iconSize = 16
const useStyles = createStyles(theme => ({ const useStyles = tss
menu: { .withParams<{
// apply mantine theme from MenuItem.styles.ts theme: MantineTheme
fontSize: theme.fontSizes.sm, colorScheme: "light" | "dark"
"--contexify-item-color": `${theme.colorScheme === "dark" ? theme.colors.dark[0] : theme.black} !important`, }>()
"--contexify-activeItem-color": `${theme.colorScheme === "dark" ? theme.colors.dark[0] : theme.black} !important`, .create(({ theme, colorScheme }) => ({
"--contexify-activeItem-bgColor": `${ menu: {
theme.colorScheme === "dark" ? theme.fn.rgba(theme.colors.dark[3], 0.35) : theme.colors.gray[1] // apply mantine theme from MenuItem.styles.ts
} !important`, 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) { export function FeedEntryContextMenu(props: FeedEntryContextMenuProps) {
const { classes, theme } = useStyles() const theme = useMantineTheme()
const colorScheme = useColorScheme()
const { classes } = useStyles({
theme,
colorScheme,
})
const sourceType = useAppSelector(state => state.entries.source.type) const sourceType = useAppSelector(state => state.entries.source.type)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const { openLinkInBackgroundTab } = useBrowserExtension() const { openLinkInBackgroundTab } = useBrowserExtension()
return ( return (
<Menu id={Constants.dom.entryContextMenuId(props.entry)} theme={theme.colorScheme} animation={false} className={classes.menu}> <Menu id={Constants.dom.entryContextMenuId(props.entry)} theme={colorScheme} animation={false} className={classes.menu}>
<Item <Item
onClick={() => { onClick={() => {
window.open(props.entry.url, "_blank", "noreferrer") window.open(props.entry.url, "_blank", "noreferrer")
@@ -60,19 +70,19 @@ export function FeedEntryContextMenu(props: FeedEntryContextMenuProps) {
<Separator /> <Separator />
<Item onClick={() => dispatch(starEntry({ entry: props.entry, starred: !props.entry.starred }))}> <Item onClick={async () => await dispatch(starEntry({ entry: props.entry, starred: !props.entry.starred }))}>
<Group> <Group>
{props.entry.starred ? <TbStarOff size={iconSize} /> : <TbStar size={iconSize} />} {props.entry.starred ? <TbStarOff size={iconSize} /> : <TbStar size={iconSize} />}
{props.entry.starred ? <Trans>Unstar</Trans> : <Trans>Star</Trans>} {props.entry.starred ? <Trans>Unstar</Trans> : <Trans>Star</Trans>}
</Group> </Group>
</Item> </Item>
<Item onClick={() => dispatch(markEntry({ entry: props.entry, read: !props.entry.read }))}> <Item onClick={async () => await dispatch(markEntry({ entry: props.entry, read: !props.entry.read }))}>
<Group> <Group>
{props.entry.read ? <TbEyeOff size={iconSize} /> : <TbEyeCheck size={iconSize} />} {props.entry.read ? <TbEyeOff size={iconSize} /> : <TbEyeCheck size={iconSize} />}
{props.entry.read ? <Trans>Keep unread</Trans> : <Trans>Mark as read</Trans>} {props.entry.read ? <Trans>Keep unread</Trans> : <Trans>Mark as read</Trans>}
</Group> </Group>
</Item> </Item>
<Item onClick={() => dispatch(markEntriesUpToEntry(props.entry))}> <Item onClick={async () => await dispatch(markEntriesUpToEntry(props.entry))}>
<Group> <Group>
<TbArrowBarToDown size={iconSize} /> <TbArrowBarToDown size={iconSize} />
<Trans>Mark as read up to here</Trans> <Trans>Mark as read up to here</Trans>

View File

@@ -1,8 +1,8 @@
import { t, Trans } from "@lingui/macro" import { t, Trans } from "@lingui/macro"
import { Group, Indicator, MultiSelect, Popover } from "@mantine/core" import { Group, Indicator, Popover, TagsInput } from "@mantine/core"
import { markEntriesUpToEntry, markEntry, starEntry, tagEntry } from "app/slices/entries" import { markEntriesUpToEntry, markEntry, starEntry, tagEntry } from "app/entries/thunks"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { Entry } from "app/types" import { type Entry } from "app/types"
import { ActionButton } from "components/ActionButton" import { ActionButton } from "components/ActionButton"
import { useActionButton } from "hooks/useActionButton" import { useActionButton } from "hooks/useActionButton"
import { useMobile } from "hooks/useMobile" import { useMobile } from "hooks/useMobile"
@@ -22,9 +22,15 @@ export function FeedEntryFooter(props: FeedEntryFooterProps) {
const showSharingButtons = sharingSettings && Object.values(sharingSettings).some(v => v) const showSharingButtons = sharingSettings && Object.values(sharingSettings).some(v => v)
const readStatusButtonClicked = () => dispatch(markEntry({ entry: props.entry, read: !props.entry.read })) const readStatusButtonClicked = async () =>
const onTagsChange = (values: string[]) => await dispatch(
dispatch( markEntry({
entry: props.entry,
read: !props.entry.read,
})
)
const onTagsChange = async (values: string[]) =>
await dispatch(
tagEntry({ tagEntry({
entryId: +props.entry.id, entryId: +props.entry.id,
tags: values, tags: values,
@@ -32,8 +38,8 @@ export function FeedEntryFooter(props: FeedEntryFooterProps) {
) )
return ( return (
<Group position="apart"> <Group justify="space-between">
<Group spacing={spacing}> <Group gap={spacing}>
{props.entry.markable && ( {props.entry.markable && (
<ActionButton <ActionButton
icon={props.entry.read ? <TbEyeOff size={18} /> : <TbEyeCheck size={18} />} icon={props.entry.read ? <TbEyeOff size={18} /> : <TbEyeCheck size={18} />}
@@ -44,7 +50,14 @@ export function FeedEntryFooter(props: FeedEntryFooterProps) {
<ActionButton <ActionButton
icon={props.entry.starred ? <TbStarOff size={18} /> : <TbStar size={18} />} icon={props.entry.starred ? <TbStarOff size={18} /> : <TbStar size={18} />}
label={props.entry.starred ? <Trans>Unstar</Trans> : <Trans>Star</Trans>} label={props.entry.starred ? <Trans>Unstar</Trans> : <Trans>Star</Trans>}
onClick={() => dispatch(starEntry({ entry: props.entry, starred: !props.entry.starred }))} onClick={async () =>
await dispatch(
starEntry({
entry: props.entry,
starred: !props.entry.starred,
})
)
}
/> />
{showSharingButtons && ( {showSharingButtons && (
@@ -59,22 +72,21 @@ export function FeedEntryFooter(props: FeedEntryFooterProps) {
)} )}
{tags && ( {tags && (
<Popover withArrow withinPortal shadow="md" closeOnClickOutside={!mobile}> <Popover withArrow shadow="md" closeOnClickOutside={!mobile}>
<Popover.Target> <Popover.Target>
<Indicator label={props.entry.tags.length} disabled={props.entry.tags.length === 0} inline size={16}> <Indicator label={props.entry.tags.length} disabled={props.entry.tags.length === 0} inline size={16}>
<ActionButton icon={<TbTag size={18} />} label={<Trans>Tags</Trans>} /> <ActionButton icon={<TbTag size={18} />} label={<Trans>Tags</Trans>} />
</Indicator> </Indicator>
</Popover.Target> </Popover.Target>
<Popover.Dropdown> <Popover.Dropdown>
<MultiSelect <TagsInput
placeholder={t`Tags`}
data={tags} data={tags}
placeholder="Tags"
searchable
creatable
autoFocus
getCreateLabel={query => t`Create tag: ${query}`}
value={props.entry.tags} value={props.entry.tags}
onChange={onTagsChange} onChange={onTagsChange}
comboboxProps={{
withinPortal: false,
}}
/> />
</Popover.Dropdown> </Popover.Dropdown>
</Popover> </Popover>
@@ -88,7 +100,7 @@ export function FeedEntryFooter(props: FeedEntryFooterProps) {
<ActionButton <ActionButton
icon={<TbArrowBarToDown size={18} />} icon={<TbArrowBarToDown size={18} />}
label={<Trans>Mark as read up to here</Trans>} label={<Trans>Mark as read up to here</Trans>}
onClick={() => dispatch(markEntriesUpToEntry(props.entry))} onClick={async () => await dispatch(markEntriesUpToEntry(props.entry))}
/> />
</Group> </Group>
) )

View File

@@ -1,6 +1,8 @@
import { Box, createStyles, Text } from "@mantine/core" import { Box, Space, Text } from "@mantine/core"
import { Entry } from "app/types" import { type Entry } from "app/types"
import { RelativeDate } from "components/RelativeDate" import { RelativeDate } from "components/RelativeDate"
import { useColorScheme } from "hooks/useColorScheme"
import { tss } from "tss"
import { FeedEntryTitle } from "./FeedEntryTitle" import { FeedEntryTitle } from "./FeedEntryTitle"
import { FeedFavicon } from "./FeedFavicon" import { FeedFavicon } from "./FeedFavicon"
@@ -9,46 +11,45 @@ export interface FeedEntryHeaderProps {
expanded: boolean expanded: boolean
} }
const useStyles = createStyles((theme, props: FeedEntryHeaderProps) => ({ const useStyles = tss
headerText: { .withParams<{
fontWeight: theme.colorScheme === "light" && !props.entry.read ? "bold" : "inherit", colorScheme: "light" | "dark"
whiteSpace: props.expanded ? "inherit" : "nowrap", read: boolean
overflow: "hidden", }>()
textOverflow: "ellipsis", .create(({ colorScheme, read }) => ({
}, headerText: {
headerSubtext: { fontWeight: colorScheme === "light" && !read ? "bold" : "inherit",
display: "flex", },
alignItems: "center", headerSubtext: {
fontSize: "90%", display: "flex",
whiteSpace: props.expanded ? "inherit" : "nowrap", alignItems: "center",
overflow: "hidden", fontSize: "90%",
textOverflow: "ellipsis", },
}, }))
}))
export function FeedEntryHeader(props: FeedEntryHeaderProps) { export function FeedEntryHeader(props: FeedEntryHeaderProps) {
const { classes } = useStyles(props) const colorScheme = useColorScheme()
const { classes } = useStyles({
colorScheme,
read: props.entry.read,
})
return ( return (
<Box> <Box>
<Box className={classes.headerText}> <Box className={classes.headerText}>
<FeedEntryTitle entry={props.entry} /> <FeedEntryTitle entry={props.entry} />
</Box> </Box>
<Box className={classes.headerSubtext}> <Box className={classes.headerSubtext}>
<Box mr={6}> <FeedFavicon url={props.entry.iconUrl} />
<FeedFavicon url={props.entry.iconUrl} /> <Space w={6} />
</Box> <Text c="dimmed">
<Box> {props.entry.feedName}
<Text color="dimmed">{props.entry.feedName}</Text> <span> · </span>
</Box> <RelativeDate date={props.entry.date} />
<Box> </Text>
<Text color="dimmed">
<span>&nbsp;·&nbsp;</span>
<RelativeDate date={props.entry.date} />
</Text>
</Box>
</Box> </Box>
{props.expanded && ( {props.expanded && (
<Box className={classes.headerSubtext}> <Box className={classes.headerSubtext}>
<Text color="dimmed"> <Text c="dimmed">
{props.entry.author && <span>by {props.entry.author}</span>} {props.entry.author && <span>by {props.entry.author}</span>}
{props.entry.author && props.entry.categories && <span>&nbsp;·&nbsp;</span>} {props.entry.author && props.entry.categories && <span>&nbsp;·&nbsp;</span>}
{props.entry.categories && <span>{props.entry.categories}</span>} {props.entry.categories && <span>{props.entry.categories}</span>}

View File

@@ -1,6 +1,6 @@
import { Highlight } from "@mantine/core" import { Highlight } from "@mantine/core"
import { useAppSelector } from "app/store" import { useAppSelector } from "app/store"
import { Entry } from "app/types" import { type Entry } from "app/types"
export interface FeedEntryTitleProps { export interface FeedEntryTitleProps {
entry: Entry entry: Entry
@@ -11,6 +11,7 @@ export function FeedEntryTitle(props: FeedEntryTitleProps) {
const keywords = search?.split(" ") const keywords = search?.split(" ")
return ( return (
<Highlight <Highlight
inherit
highlight={keywords ?? ""} highlight={keywords ?? ""}
// make sure ellipsis is shown when title is too long // make sure ellipsis is shown when title is too long
span span

View File

@@ -16,7 +16,6 @@ export function FeedFavicon({ url, size = 18 }: FeedFaviconProps) {
placeholderHeight={size} placeholderHeight={size}
placeholderBackgroundColor="inherit" placeholderBackgroundColor="inherit"
placeholderIconSize={size} placeholderIconSize={size}
placeholderIconColor="inherit"
/> />
) )
} }

View File

@@ -1,6 +1,7 @@
import { Box, TypographyStylesProvider } from "@mantine/core" import { Box } from "@mantine/core"
import { Constants } from "app/constants" import { Constants } from "app/constants"
import { calculatePlaceholderSize } from "app/utils" import { calculatePlaceholderSize } from "app/utils"
import { BasicHtmlStyles } from "components/content/BasicHtmlStyles"
import { ImageWithPlaceholderWhileLoading } from "components/ImageWithPlaceholderWhileLoading" import { ImageWithPlaceholderWhileLoading } from "components/ImageWithPlaceholderWhileLoading"
import { Content } from "./Content" import { Content } from "./Content"
@@ -20,7 +21,7 @@ export function Media(props: MediaProps) {
maxWidth: Constants.layout.entryMaxWidth, maxWidth: Constants.layout.entryMaxWidth,
}) })
return ( return (
<TypographyStylesProvider> <BasicHtmlStyles>
<ImageWithPlaceholderWhileLoading <ImageWithPlaceholderWhileLoading
src={props.thumbnailUrl} src={props.thumbnailUrl}
alt="media thumbnail" alt="media thumbnail"
@@ -34,6 +35,6 @@ export function Media(props: MediaProps) {
<Content content={props.description} /> <Content content={props.description} />
</Box> </Box>
)} )}
</TypographyStylesProvider> </BasicHtmlStyles>
) )
} }

View File

@@ -1,21 +1,35 @@
import { ActionIcon, Box, createStyles, SimpleGrid } from "@mantine/core" import { ActionIcon, Box, type MantineTheme, SimpleGrid, useMantineTheme } from "@mantine/core"
import { Constants } from "app/constants" import { Constants } from "app/constants"
import { useAppSelector } from "app/store" import { useAppSelector } from "app/store"
import { SharingSettings } from "app/types" import { type SharingSettings } from "app/types"
import { IconType } from "react-icons" import { useColorScheme } from "hooks/useColorScheme"
import { type IconType } from "react-icons"
import { tss } from "tss"
type Color = `#${string}` type Color = `#${string}`
const useStyles = createStyles((theme, props: { color: Color }) => ({ const useStyles = tss
socialIcon: { .withParams<{
color: props.color, theme: MantineTheme
backgroundColor: theme.colorScheme === "dark" ? theme.colors.gray[2] : "white", colorScheme: "light" | "dark"
borderRadius: "50%", color: Color
}, }>()
})) .create(({ theme, colorScheme, color }) => ({
socialIcon: {
color,
backgroundColor: colorScheme === "dark" ? theme.colors.gray[2] : "white",
borderRadius: "50%",
},
}))
function ShareButton({ url, icon, color }: { url: string; icon: IconType; color: Color }) { function ShareButton({ url, icon, color }: { url: string; icon: IconType; color: Color }) {
const { classes } = useStyles({ color }) const theme = useMantineTheme()
const colorScheme = useColorScheme()
const { classes } = useStyles({
theme,
colorScheme,
color,
})
const onClick = (e: React.MouseEvent) => { const onClick = (e: React.MouseEvent) => {
e.preventDefault() e.preventDefault()
@@ -23,7 +37,7 @@ function ShareButton({ url, icon, color }: { url: string; icon: IconType; color:
} }
return ( return (
<ActionIcon> <ActionIcon variant="transparent">
<a href={url} target="_blank" rel="noreferrer" onClick={onClick}> <a href={url} target="_blank" rel="noreferrer" onClick={onClick}>
<Box p={6} className={classes.socialIcon}> <Box p={6} className={classes.socialIcon}>
{icon({ size: 18 })} {icon({ size: 18 })}
@@ -41,7 +55,7 @@ export function ShareButtons(props: { url: string; description: string }) {
return ( return (
<SimpleGrid cols={4}> <SimpleGrid cols={4}>
{(Object.keys(Constants.sharing) as Array<keyof SharingSettings>) {(Object.keys(Constants.sharing) as Array<keyof SharingSettings>)
.filter(site => sharingSettings && sharingSettings[site]) .filter(site => sharingSettings?.[site])
.map(site => ( .map(site => (
<ShareButton <ShareButton
key={site} key={site}

View File

@@ -2,10 +2,10 @@ import { t, Trans } from "@lingui/macro"
import { Box, Button, Group, Stack, TextInput } from "@mantine/core" import { Box, Button, Group, Stack, TextInput } from "@mantine/core"
import { useForm } from "@mantine/form" import { useForm } from "@mantine/form"
import { client, errorToStrings } from "app/client" import { client, errorToStrings } from "app/client"
import { redirectToSelectedSource } from "app/slices/redirect" import { redirectToSelectedSource } from "app/redirect/thunks"
import { reloadTree } from "app/slices/tree"
import { useAppDispatch } from "app/store" import { useAppDispatch } from "app/store"
import { AddCategoryRequest } from "app/types" import { reloadTree } from "app/tree/thunks"
import { type AddCategoryRequest } from "app/types"
import { Alert } from "components/Alert" import { Alert } from "components/Alert"
import { useAsyncCallback } from "react-async-hook" import { useAsyncCallback } from "react-async-hook"
import { TbFolderPlus } from "react-icons/tb" import { TbFolderPlus } from "react-icons/tb"
@@ -35,11 +35,11 @@ export function AddCategory() {
<Stack> <Stack>
<TextInput label={<Trans>Category</Trans>} placeholder={t`Category`} {...form.getInputProps("name")} required /> <TextInput label={<Trans>Category</Trans>} placeholder={t`Category`} {...form.getInputProps("name")} required />
<CategorySelect label={<Trans>Parent</Trans>} {...form.getInputProps("parentId")} clearable /> <CategorySelect label={<Trans>Parent</Trans>} {...form.getInputProps("parentId")} clearable />
<Group position="center"> <Group justify="center">
<Button variant="default" onClick={() => dispatch(redirectToSelectedSource())}> <Button variant="default" onClick={async () => await dispatch(redirectToSelectedSource())}>
<Trans>Cancel</Trans> <Trans>Cancel</Trans>
</Button> </Button>
<Button type="submit" leftIcon={<TbFolderPlus size={16} />} loading={addCategory.loading}> <Button type="submit" leftSection={<TbFolderPlus size={16} />} loading={addCategory.loading}>
<Trans>Add</Trans> <Trans>Add</Trans>
</Button> </Button>
</Group> </Group>

View File

@@ -1,5 +1,6 @@
import { t } from "@lingui/macro" import { t } from "@lingui/macro"
import { Select, SelectItem, SelectProps } from "@mantine/core" import { Select, type SelectProps } from "@mantine/core"
import { type ComboboxItem } from "@mantine/core/lib/components/Combobox/Combobox.types"
import { Constants } from "app/constants" import { Constants } from "app/constants"
import { useAppSelector } from "app/store" import { useAppSelector } from "app/store"
import { flattenCategoryTree } from "app/utils" import { flattenCategoryTree } from "app/utils"
@@ -12,9 +13,9 @@ type CategorySelectProps = Partial<SelectProps> & {
export function CategorySelect(props: CategorySelectProps) { export function CategorySelect(props: CategorySelectProps) {
const rootCategory = useAppSelector(state => state.tree.rootCategory) const rootCategory = useAppSelector(state => state.tree.rootCategory)
const categories = rootCategory && flattenCategoryTree(rootCategory) const categories = rootCategory && flattenCategoryTree(rootCategory)
const selectData: SelectItem[] | undefined = categories const selectData: ComboboxItem[] | undefined = categories
?.filter(c => c.id !== Constants.categories.all.id) ?.filter(c => c.id !== Constants.categories.all.id)
.filter(c => !props.withoutCategoryIds || !props.withoutCategoryIds.includes(c.id)) .filter(c => !props.withoutCategoryIds?.includes(c.id))
.sort((c1, c2) => c1.name.localeCompare(c2.name)) .sort((c1, c2) => c1.name.localeCompare(c2.name))
.map(c => ({ .map(c => ({
label: c.parentName ? t`${c.name} (in ${c.parentName})` : c.name, label: c.parentName ? t`${c.name} (in ${c.parentName})` : c.name,

View File

@@ -2,9 +2,9 @@ import { t, Trans } from "@lingui/macro"
import { Box, Button, FileInput, Group, Stack } from "@mantine/core" import { Box, Button, FileInput, Group, Stack } from "@mantine/core"
import { useForm } from "@mantine/form" import { useForm } from "@mantine/form"
import { client, errorToStrings } from "app/client" import { client, errorToStrings } from "app/client"
import { redirectToSelectedSource } from "app/slices/redirect" import { redirectToSelectedSource } from "app/redirect/thunks"
import { reloadTree } from "app/slices/tree"
import { useAppDispatch } from "app/store" import { useAppDispatch } from "app/store"
import { reloadTree } from "app/tree/thunks"
import { Alert } from "components/Alert" import { Alert } from "components/Alert"
import { useAsyncCallback } from "react-async-hook" import { useAsyncCallback } from "react-async-hook"
import { TbFileImport } from "react-icons/tb" import { TbFileImport } from "react-icons/tb"
@@ -33,11 +33,13 @@ export function ImportOpml() {
</Box> </Box>
)} )}
<form onSubmit={form.onSubmit(v => importOpml.execute(v.file))}> <form onSubmit={form.onSubmit(async v => await importOpml.execute(v.file))}>
<Stack> <Stack>
<FileInput <FileInput
label={<Trans>OPML file</Trans>} label={<Trans>OPML file</Trans>}
placeholder={t`OPML file`} leftSection={<TbFileImport />}
// https://github.com/mantinedev/mantine/issues/5401
{...{ placeholder: t`OPML file` }}
description={ description={
<Trans> <Trans>
An opml file is an XML file containing feed URLs and categories. You can get an OPML file by exporting your An opml file is an XML file containing feed URLs and categories. You can get an OPML file by exporting your
@@ -48,11 +50,11 @@ export function ImportOpml() {
required required
accept="application/xml" accept="application/xml"
/> />
<Group position="center"> <Group justify="center">
<Button variant="default" onClick={() => dispatch(redirectToSelectedSource())}> <Button variant="default" onClick={async () => await dispatch(redirectToSelectedSource())}>
<Trans>Cancel</Trans> <Trans>Cancel</Trans>
</Button> </Button>
<Button type="submit" leftIcon={<TbFileImport size={16} />} loading={importOpml.loading}> <Button type="submit" leftSection={<TbFileImport size={16} />} loading={importOpml.loading}>
<Trans>Import</Trans> <Trans>Import</Trans>
</Button> </Button>
</Group> </Group>

View File

@@ -3,10 +3,10 @@ import { Box, Button, Group, Stack, Stepper, TextInput } from "@mantine/core"
import { useForm } from "@mantine/form" import { useForm } from "@mantine/form"
import { client, errorToStrings } from "app/client" import { client, errorToStrings } from "app/client"
import { Constants } from "app/constants" import { Constants } from "app/constants"
import { redirectToFeed, redirectToSelectedSource } from "app/slices/redirect" import { redirectToFeed, redirectToSelectedSource } from "app/redirect/thunks"
import { reloadTree } from "app/slices/tree"
import { useAppDispatch } from "app/store" import { useAppDispatch } from "app/store"
import { FeedInfoRequest, SubscribeRequest } from "app/types" import { reloadTree } from "app/tree/thunks"
import { type FeedInfoRequest, type SubscribeRequest } from "app/types"
import { Alert } from "components/Alert" import { Alert } from "components/Alert"
import { useState } from "react" import { useState } from "react"
import { useAsyncCallback } from "react-async-hook" import { useAsyncCallback } from "react-async-hook"
@@ -46,8 +46,11 @@ export function Subscribe() {
}) })
const previousStep = () => { const previousStep = () => {
if (activeStep === 0) dispatch(redirectToSelectedSource()) if (activeStep === 0) {
else setActiveStep(activeStep - 1) dispatch(redirectToSelectedSource())
} else {
setActiveStep(activeStep - 1)
}
} }
const nextStep = (e: React.FormEvent<HTMLFormElement>) => { const nextStep = (e: React.FormEvent<HTMLFormElement>) => {
if (activeStep === 0) { if (activeStep === 0) {
@@ -80,7 +83,7 @@ export function Subscribe() {
> >
<TextInput <TextInput
label={<Trans>Feed URL</Trans>} label={<Trans>Feed URL</Trans>}
placeholder="http://www.mysite.com/rss" placeholder="https://www.mysite.com/rss"
description={ description={
<Trans> <Trans>
The URL for the feed you want to subscribe to. You can also use the website's url directly and CommaFeed The URL for the feed you want to subscribe to. You can also use the website's url directly and CommaFeed
@@ -105,7 +108,7 @@ export function Subscribe() {
</Stepper.Step> </Stepper.Step>
</Stepper> </Stepper>
<Group position="center" mt="xl"> <Group justify="center" mt="xl">
<Button variant="default" onClick={previousStep}> <Button variant="default" onClick={previousStep}>
<Trans>Back</Trans> <Trans>Back</Trans>
</Button> </Button>
@@ -115,7 +118,7 @@ export function Subscribe() {
</Button> </Button>
)} )}
{activeStep === 1 && ( {activeStep === 1 && (
<Button type="submit" leftIcon={<TbRss size={16} />} loading={fetchFeed.loading || subscribe.loading}> <Button type="submit" leftSection={<TbRss size={16} />} loading={fetchFeed.loading || subscribe.loading}>
<Trans>Subscribe</Trans> <Trans>Subscribe</Trans>
</Button> </Button>
)} )}

View File

@@ -1,9 +1,9 @@
import { t, Trans } from "@lingui/macro" import { t, Trans } from "@lingui/macro"
import { ActionIcon, Box, Center, Divider, Group, Indicator, Popover, TextInput } from "@mantine/core" import { Box, Center, CloseButton, Divider, Group, Indicator, Popover, TextInput } from "@mantine/core"
import { useForm } from "@mantine/form" import { useForm } from "@mantine/form"
import { reloadEntries, search, selectNextEntry, selectPreviousEntry } from "app/slices/entries" import { reloadEntries, search, selectNextEntry, selectPreviousEntry } from "app/entries/thunks"
import { changeReadingMode, changeReadingOrder } from "app/slices/user"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { changeReadingMode, changeReadingOrder } from "app/user/thunks"
import { ActionButton } from "components/ActionButton" import { ActionButton } from "components/ActionButton"
import { Loader } from "components/Loader" import { Loader } from "components/Loader"
import { useActionButton } from "hooks/useActionButton" import { useActionButton } from "hooks/useActionButton"
@@ -22,7 +22,6 @@ import {
TbSortAscending, TbSortAscending,
TbSortDescending, TbSortDescending,
TbUser, TbUser,
TbX,
} from "react-icons/tb" } from "react-icons/tb"
import { MarkAllAsReadButton } from "./MarkAllAsReadButton" import { MarkAllAsReadButton } from "./MarkAllAsReadButton"
import { ProfileMenu } from "./ProfileMenu" import { ProfileMenu } from "./ProfileMenu"
@@ -37,7 +36,7 @@ function HeaderToolbar(props: { children: React.ReactNode }) {
return mobile ? ( return mobile ? (
// on mobile use all available width // on mobile use all available width
<Box <Box
sx={{ style={{
width: "100%", width: "100%",
display: "flex", display: "flex",
justifyContent: "space-between", justifyContent: "space-between",
@@ -46,7 +45,7 @@ function HeaderToolbar(props: { children: React.ReactNode }) {
{props.children} {props.children}
</Box> </Box>
) : ( ) : (
<Group spacing={spacing}>{props.children}</Group> <Group gap={spacing}>{props.children}</Group>
) )
} }
@@ -77,11 +76,11 @@ export function Header() {
<Center> <Center>
<HeaderToolbar> <HeaderToolbar>
<ActionButton <ActionButton
icon={<TbArrowDown size={iconSize} />} icon={<TbArrowUp size={iconSize} />}
label={<Trans>Next</Trans>} label={<Trans>Previous</Trans>}
onClick={() => onClick={async () =>
dispatch( await dispatch(
selectNextEntry({ selectPreviousEntry({
expand: true, expand: true,
markAsRead: true, markAsRead: true,
scrollToEntry: true, scrollToEntry: true,
@@ -90,11 +89,11 @@ export function Header() {
} }
/> />
<ActionButton <ActionButton
icon={<TbArrowUp size={iconSize} />} icon={<TbArrowDown size={iconSize} />}
label={<Trans>Previous</Trans>} label={<Trans>Next</Trans>}
onClick={() => onClick={async () =>
dispatch( await dispatch(
selectPreviousEntry({ selectNextEntry({
expand: true, expand: true,
markAsRead: true, markAsRead: true,
scrollToEntry: true, scrollToEntry: true,
@@ -108,7 +107,7 @@ export function Header() {
<ActionButton <ActionButton
icon={<TbRefresh size={iconSize} />} icon={<TbRefresh size={iconSize} />}
label={<Trans>Refresh</Trans>} label={<Trans>Refresh</Trans>}
onClick={() => dispatch(reloadEntries())} onClick={async () => await dispatch(reloadEntries())}
/> />
<MarkAllAsReadButton iconSize={iconSize} /> <MarkAllAsReadButton iconSize={iconSize} />
@@ -117,12 +116,12 @@ export function Header() {
<ActionButton <ActionButton
icon={settings.readingMode === "all" ? <TbEye size={iconSize} /> : <TbEyeOff size={iconSize} />} icon={settings.readingMode === "all" ? <TbEye size={iconSize} /> : <TbEyeOff size={iconSize} />}
label={settings.readingMode === "all" ? <Trans>All</Trans> : <Trans>Unread</Trans>} label={settings.readingMode === "all" ? <Trans>All</Trans> : <Trans>Unread</Trans>}
onClick={() => dispatch(changeReadingMode(settings.readingMode === "all" ? "unread" : "all"))} onClick={async () => await dispatch(changeReadingMode(settings.readingMode === "all" ? "unread" : "all"))}
/> />
<ActionButton <ActionButton
icon={settings.readingOrder === "asc" ? <TbSortAscending size={iconSize} /> : <TbSortDescending size={iconSize} />} icon={settings.readingOrder === "asc" ? <TbSortAscending size={iconSize} /> : <TbSortDescending size={iconSize} />}
label={settings.readingOrder === "asc" ? <Trans>Asc</Trans> : <Trans>Desc</Trans>} label={settings.readingOrder === "asc" ? <Trans>Asc</Trans> : <Trans>Desc</Trans>}
onClick={() => dispatch(changeReadingOrder(settings.readingOrder === "asc" ? "desc" : "asc"))} onClick={async () => await dispatch(changeReadingOrder(settings.readingOrder === "asc" ? "desc" : "asc"))}
/> />
<Popover> <Popover>
@@ -132,16 +131,12 @@ export function Header() {
</Indicator> </Indicator>
</Popover.Target> </Popover.Target>
<Popover.Dropdown> <Popover.Dropdown>
<form onSubmit={searchForm.onSubmit(values => dispatch(search(values.search)))}> <form onSubmit={searchForm.onSubmit(async values => await dispatch(search(values.search)))}>
<TextInput <TextInput
placeholder={t`Search`} placeholder={t`Search`}
{...searchForm.getInputProps("search")} {...searchForm.getInputProps("search")}
icon={<TbSearch size={iconSize} />} leftSection={<TbSearch size={iconSize} />}
rightSection={ rightSection={<CloseButton onClick={async () => await (searchFromStore && dispatch(search("")))} />}
<ActionIcon onClick={() => searchFromStore && dispatch(search(""))}>
<TbX />
</ActionIcon>
}
autoFocus autoFocus
/> />
</form> </form>

View File

@@ -1,7 +1,7 @@
import { Trans } from "@lingui/macro" import { Trans } from "@lingui/macro"
import { Button, Code, Group, Modal, Slider, Stack, Text } from "@mantine/core" import { Button, Code, Group, Modal, Slider, Stack, Text } from "@mantine/core"
import { markAllEntries } from "app/slices/entries" import { markAllEntries } from "app/entries/thunks"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { ActionButton } from "components/ActionButton" import { ActionButton } from "components/ActionButton"
import { useState } from "react" import { useState } from "react"
@@ -13,8 +13,28 @@ export function MarkAllAsReadButton(props: { iconSize: number }) {
const source = useAppSelector(state => state.entries.source) const source = useAppSelector(state => state.entries.source)
const sourceLabel = useAppSelector(state => state.entries.sourceLabel) const sourceLabel = useAppSelector(state => state.entries.sourceLabel)
const entriesTimestamp = useAppSelector(state => state.entries.timestamp) ?? Date.now() const entriesTimestamp = useAppSelector(state => state.entries.timestamp) ?? Date.now()
const markAllAsReadConfirmation = useAppSelector(state => state.user.settings?.markAllAsReadConfirmation)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const buttonClicked = () => {
if (markAllAsReadConfirmation) {
setThreshold(0)
setOpened(true)
} else {
dispatch(
markAllEntries({
sourceType: source.type,
req: {
id: source.id,
read: true,
olderThan: Date.now(),
insertedBefore: entriesTimestamp,
},
})
)
}
}
return ( return (
<> <>
<Modal opened={opened} onClose={() => setOpened(false)} title={<Trans>Mark all entries as read</Trans>}> <Modal opened={opened} onClose={() => setOpened(false)} title={<Trans>Mark all entries as read</Trans>}>
@@ -45,7 +65,7 @@ export function MarkAllAsReadButton(props: { iconSize: number }) {
value={threshold} value={threshold}
onChange={setThreshold} onChange={setThreshold}
/> />
<Group position="right"> <Group justify="flex-end">
<Button variant="default" onClick={() => setOpened(false)}> <Button variant="default" onClick={() => setOpened(false)}>
<Trans>Cancel</Trans> <Trans>Cancel</Trans>
</Button> </Button>
@@ -59,7 +79,8 @@ export function MarkAllAsReadButton(props: { iconSize: number }) {
req: { req: {
id: source.id, id: source.id,
read: true, read: true,
olderThan: entriesTimestamp - threshold * 24 * 60 * 60 * 1000, olderThan: Date.now() - threshold * 24 * 60 * 60 * 1000,
insertedBefore: entriesTimestamp,
}, },
}) })
) )
@@ -70,14 +91,7 @@ export function MarkAllAsReadButton(props: { iconSize: number }) {
</Group> </Group>
</Stack> </Stack>
</Modal> </Modal>
<ActionButton <ActionButton icon={<TbChecks size={props.iconSize} />} label={<Trans>Mark all as read</Trans>} onClick={buttonClicked} />
icon={<TbChecks size={props.iconSize} />}
label={<Trans>Mark all as read</Trans>}
onClick={() => {
setThreshold(0)
setOpened(true)
}}
/>
</> </>
) )
} }

View File

@@ -1,12 +1,21 @@
import { Trans } from "@lingui/macro" import { Trans } from "@lingui/macro"
import { Box, Divider, Group, Menu, SegmentedControl, SegmentedControlItem, useMantineColorScheme } from "@mantine/core" import {
Box,
Divider,
Group,
type MantineColorScheme,
Menu,
SegmentedControl,
type SegmentedControlItem,
useMantineColorScheme,
} from "@mantine/core"
import { showNotification } from "@mantine/notifications" import { showNotification } from "@mantine/notifications"
import { client } from "app/client" import { client } from "app/client"
import { redirectToAbout, redirectToAdminUsers, redirectToDonate, redirectToMetrics, redirectToSettings } from "app/slices/redirect" import { redirectToAbout, redirectToAdminUsers, redirectToDonate, redirectToMetrics, redirectToSettings } from "app/redirect/thunks"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { ViewMode } from "app/types" import { type ViewMode } from "app/types"
import { useViewMode } from "hooks/useViewMode" import { useViewMode } from "hooks/useViewMode"
import { useState } from "react" import { type ReactNode, useState } from "react"
import { import {
TbChartLine, TbChartLine,
TbHeartFilled, TbHeartFilled,
@@ -19,6 +28,7 @@ import {
TbPower, TbPower,
TbSettings, TbSettings,
TbSun, TbSun,
TbSunMoon,
TbUsers, TbUsers,
TbWorldDownload, TbWorldDownload,
} from "react-icons/tb" } from "react-icons/tb"
@@ -27,56 +37,56 @@ interface ProfileMenuProps {
control: React.ReactElement control: React.ReactElement
} }
interface ViewModeControlItem extends SegmentedControlItem { const ProfileMenuControlItem = ({ icon, label }: { icon: ReactNode; label: ReactNode }) => {
value: ViewMode return (
<Group>
{icon}
<Box ml={6}>{label}</Box>
</Group>
)
} }
const iconSize = 16 const iconSize = 16
interface ColorSchemeControlItem extends SegmentedControlItem {
value: MantineColorScheme
}
const colorSchemeData: ColorSchemeControlItem[] = [
{
value: "light",
label: <ProfileMenuControlItem icon={<TbSun size={iconSize} />} label={<Trans>Light</Trans>} />,
},
{
value: "dark",
label: <ProfileMenuControlItem icon={<TbMoon size={iconSize} />} label={<Trans>Dark</Trans>} />,
},
{
value: "auto",
label: <ProfileMenuControlItem icon={<TbSunMoon size={iconSize} />} label={<Trans>System</Trans>} />,
},
]
interface ViewModeControlItem extends SegmentedControlItem {
value: ViewMode
}
const viewModeData: ViewModeControlItem[] = [ const viewModeData: ViewModeControlItem[] = [
{ {
value: "title", value: "title",
label: ( label: <ProfileMenuControlItem icon={<TbList size={iconSize} />} label={<Trans>Compact</Trans>} />,
<Group>
<TbList size={iconSize} />
<Box ml={6}>
<Trans>Compact</Trans>
</Box>
</Group>
),
}, },
{ {
value: "cozy", value: "cozy",
label: ( label: <ProfileMenuControlItem icon={<TbLayoutList size={iconSize} />} label={<Trans>Cozy</Trans>} />,
<Group>
<TbLayoutList size={iconSize} />
<Box ml={6}>
<Trans>Cozy</Trans>
</Box>
</Group>
),
}, },
{ {
value: "detailed", value: "detailed",
label: ( label: <ProfileMenuControlItem icon={<TbListDetails size={iconSize} />} label={<Trans>Detailed</Trans>} />,
<Group>
<TbListDetails size={iconSize} />
<Box ml={6}>
<Trans>Detailed</Trans>
</Box>
</Group>
),
}, },
{ {
value: "expanded", value: "expanded",
label: ( label: <ProfileMenuControlItem icon={<TbNotes size={iconSize} />} label={<Trans>Expanded</Trans>} />,
<Group>
<TbNotes size={iconSize} />
<Box ml={6}>
<Trans>Expanded</Trans>
</Box>
</Group>
),
}, },
] ]
@@ -86,8 +96,7 @@ export function ProfileMenu(props: ProfileMenuProps) {
const profile = useAppSelector(state => state.user.profile) const profile = useAppSelector(state => state.user.profile)
const admin = useAppSelector(state => state.user.profile?.admin) const admin = useAppSelector(state => state.user.profile?.admin)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const { colorScheme, toggleColorScheme } = useMantineColorScheme() const { colorScheme, setColorScheme } = useMantineColorScheme()
const dark = colorScheme === "dark"
const logout = () => { const logout = () => {
window.location.href = "logout" window.location.href = "logout"
@@ -99,7 +108,7 @@ export function ProfileMenu(props: ProfileMenuProps) {
<Menu.Dropdown> <Menu.Dropdown>
{profile && <Menu.Label>{profile.name}</Menu.Label>} {profile && <Menu.Label>{profile.name}</Menu.Label>}
<Menu.Item <Menu.Item
icon={<TbSettings size={iconSize} />} leftSection={<TbSettings size={iconSize} />}
onClick={() => { onClick={() => {
dispatch(redirectToSettings()) dispatch(redirectToSettings())
setOpened(false) setOpened(false)
@@ -108,9 +117,9 @@ export function ProfileMenu(props: ProfileMenuProps) {
<Trans>Settings</Trans> <Trans>Settings</Trans>
</Menu.Item> </Menu.Item>
<Menu.Item <Menu.Item
icon={<TbWorldDownload size={iconSize} />} leftSection={<TbWorldDownload size={iconSize} />}
onClick={() => onClick={async () =>
client.feed.refreshAll().then(() => { await client.feed.refreshAll().then(() => {
showNotification({ showNotification({
message: <Trans>Your feeds have been queued for refresh.</Trans>, message: <Trans>Your feeds have been queued for refresh.</Trans>,
color: "green", color: "green",
@@ -128,9 +137,14 @@ export function ProfileMenu(props: ProfileMenuProps) {
<Menu.Label> <Menu.Label>
<Trans>Theme</Trans> <Trans>Theme</Trans>
</Menu.Label> </Menu.Label>
<Menu.Item icon={dark ? <TbSun size={iconSize} /> : <TbMoon size={iconSize} />} onClick={() => toggleColorScheme()}> <SegmentedControl
{dark ? <Trans>Switch to light theme</Trans> : <Trans>Switch to dark theme</Trans>} fullWidth
</Menu.Item> orientation="vertical"
data={colorSchemeData}
value={colorScheme}
onChange={e => setColorScheme(e as MantineColorScheme)}
mb="xs"
/>
<Divider /> <Divider />
@@ -153,7 +167,7 @@ export function ProfileMenu(props: ProfileMenuProps) {
<Trans>Admin</Trans> <Trans>Admin</Trans>
</Menu.Label> </Menu.Label>
<Menu.Item <Menu.Item
icon={<TbUsers size={iconSize} />} leftSection={<TbUsers size={iconSize} />}
onClick={() => { onClick={() => {
dispatch(redirectToAdminUsers()) dispatch(redirectToAdminUsers())
setOpened(false) setOpened(false)
@@ -162,7 +176,7 @@ export function ProfileMenu(props: ProfileMenuProps) {
<Trans>Manage users</Trans> <Trans>Manage users</Trans>
</Menu.Item> </Menu.Item>
<Menu.Item <Menu.Item
icon={<TbChartLine size={iconSize} />} leftSection={<TbChartLine size={iconSize} />}
onClick={() => { onClick={() => {
dispatch(redirectToMetrics()) dispatch(redirectToMetrics())
setOpened(false) setOpened(false)
@@ -176,7 +190,7 @@ export function ProfileMenu(props: ProfileMenuProps) {
<Divider /> <Divider />
<Menu.Item <Menu.Item
icon={<TbHeartFilled size={iconSize} color="red" />} leftSection={<TbHeartFilled size={iconSize} color="red" />}
onClick={() => { onClick={() => {
dispatch(redirectToDonate()) dispatch(redirectToDonate())
setOpened(false) setOpened(false)
@@ -186,7 +200,7 @@ export function ProfileMenu(props: ProfileMenuProps) {
</Menu.Item> </Menu.Item>
<Menu.Item <Menu.Item
icon={<TbHelp size={iconSize} />} leftSection={<TbHelp size={iconSize} />}
onClick={() => { onClick={() => {
dispatch(redirectToAbout()) dispatch(redirectToAbout())
setOpened(false) setOpened(false)
@@ -194,7 +208,7 @@ export function ProfileMenu(props: ProfileMenuProps) {
> >
<Trans>About</Trans> <Trans>About</Trans>
</Menu.Item> </Menu.Item>
<Menu.Item icon={<TbPower size={iconSize} />} onClick={logout}> <Menu.Item leftSection={<TbPower size={iconSize} />} onClick={logout}>
<Trans>Logout</Trans> <Trans>Logout</Trans>
</Menu.Item> </Menu.Item>
</Menu.Dropdown> </Menu.Dropdown>

View File

@@ -1,4 +1,4 @@
import { MetricGauge } from "app/types" import { type MetricGauge } from "app/types"
interface MeterProps { interface MeterProps {
gauge: MetricGauge gauge: MetricGauge

View File

@@ -1,5 +1,5 @@
import { Box } from "@mantine/core" import { Box } from "@mantine/core"
import { MetricMeter } from "app/types" import { type MetricMeter } from "app/types"
interface MeterProps { interface MeterProps {
meter: MetricMeter meter: MetricMeter

View File

@@ -11,7 +11,7 @@ export function MetricAccordionItem({ metricKey, name, headerValue, children }:
return ( return (
<Accordion.Item value={metricKey} key={metricKey}> <Accordion.Item value={metricKey} key={metricKey}>
<Accordion.Control> <Accordion.Control>
<Group position="apart"> <Group justify="space-between">
<Box>{name}</Box> <Box>{name}</Box>
<Box>{headerValue}</Box> <Box>{headerValue}</Box>
</Group> </Group>

View File

@@ -1,5 +1,5 @@
import { Box } from "@mantine/core" import { Box } from "@mantine/core"
import { MetricTimer } from "app/types" import { type MetricTimer } from "app/types"
interface MetricTimerProps { interface MetricTimerProps {
timer: MetricTimer timer: MetricTimer

View File

@@ -2,7 +2,7 @@ import { Trans } from "@lingui/macro"
import { Box, Button, Group, Stack } from "@mantine/core" import { Box, Button, Group, Stack } from "@mantine/core"
import { useForm } from "@mantine/form" import { useForm } from "@mantine/form"
import { client, errorToStrings } from "app/client" import { client, errorToStrings } from "app/client"
import { redirectToSelectedSource } from "app/slices/redirect" import { redirectToSelectedSource } from "app/redirect/thunks"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { Alert } from "components/Alert" import { Alert } from "components/Alert"
import { CodeEditor } from "components/code/CodeEditor" import { CodeEditor } from "components/code/CodeEditor"
@@ -69,10 +69,10 @@ export function CustomCodeSettings() {
/> />
<Group> <Group>
<Button variant="default" onClick={() => dispatch(redirectToSelectedSource())}> <Button variant="default" onClick={async () => await dispatch(redirectToSelectedSource())}>
<Trans>Cancel</Trans> <Trans>Cancel</Trans>
</Button> </Button>
<Button type="submit" leftIcon={<TbDeviceFloppy size={16} />} loading={saveCustomCode.loading}> <Button type="submit" leftSection={<TbDeviceFloppy size={16} />} loading={saveCustomCode.loading}>
<Trans>Save</Trans> <Trans>Save</Trans>
</Button> </Button>
</Group> </Group>

View File

@@ -1,16 +1,18 @@
import { Trans } from "@lingui/macro" import { Trans } from "@lingui/macro"
import { Divider, Select, SimpleGrid, Stack, Switch } from "@mantine/core" import { Divider, Select, SimpleGrid, Stack, Switch } from "@mantine/core"
import { Constants } from "app/constants" import { Constants } from "app/constants"
import { useAppDispatch, useAppSelector } from "app/store"
import { type SharingSettings } from "app/types"
import { import {
changeAlwaysScrollToEntry, changeAlwaysScrollToEntry,
changeCustomContextMenu,
changeLanguage, changeLanguage,
changeMarkAllAsReadConfirmation,
changeScrollMarks, changeScrollMarks,
changeScrollSpeed, changeScrollSpeed,
changeSharingSetting, changeSharingSetting,
changeShowRead, changeShowRead,
} from "app/slices/user" } from "app/user/thunks"
import { useAppDispatch, useAppSelector } from "app/store"
import { SharingSettings } from "app/types"
import { locales } from "i18n" import { locales } from "i18n"
export function DisplaySettings() { export function DisplaySettings() {
@@ -19,6 +21,8 @@ export function DisplaySettings() {
const showRead = useAppSelector(state => state.user.settings?.showRead) const showRead = useAppSelector(state => state.user.settings?.showRead)
const scrollMarks = useAppSelector(state => state.user.settings?.scrollMarks) const scrollMarks = useAppSelector(state => state.user.settings?.scrollMarks)
const alwaysScrollToEntry = useAppSelector(state => state.user.settings?.alwaysScrollToEntry) const alwaysScrollToEntry = useAppSelector(state => state.user.settings?.alwaysScrollToEntry)
const markAllAsReadConfirmation = useAppSelector(state => state.user.settings?.markAllAsReadConfirmation)
const customContextMenu = useAppSelector(state => state.user.settings?.customContextMenu)
const sharingSettings = useAppSelector(state => state.user.settings?.sharingSettings) const sharingSettings = useAppSelector(state => state.user.settings?.sharingSettings)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
@@ -31,31 +35,43 @@ export function DisplaySettings() {
value: l.key, value: l.key,
label: l.label, label: l.label,
}))} }))}
onChange={s => s && dispatch(changeLanguage(s))} onChange={async s => await (s && dispatch(changeLanguage(s)))}
/> />
<Switch <Switch
label={<Trans>Scroll smoothly when navigating between entries</Trans>} label={<Trans>Scroll smoothly when navigating between entries</Trans>}
checked={scrollSpeed ? scrollSpeed > 0 : false} checked={scrollSpeed ? scrollSpeed > 0 : false}
onChange={e => dispatch(changeScrollSpeed(e.currentTarget.checked))} onChange={async e => await dispatch(changeScrollSpeed(e.currentTarget.checked))}
/> />
<Switch <Switch
label={<Trans>Always scroll selected entry to the top of the page, even if it fits entirely on screen</Trans>} label={<Trans>Always scroll selected entry to the top of the page, even if it fits entirely on screen</Trans>}
checked={alwaysScrollToEntry} checked={alwaysScrollToEntry}
onChange={e => dispatch(changeAlwaysScrollToEntry(e.currentTarget.checked))} onChange={async e => await dispatch(changeAlwaysScrollToEntry(e.currentTarget.checked))}
/> />
<Switch <Switch
label={<Trans>Show feeds and categories with no unread entries</Trans>} label={<Trans>Show feeds and categories with no unread entries</Trans>}
checked={showRead} checked={showRead}
onChange={e => dispatch(changeShowRead(e.currentTarget.checked))} onChange={async e => await dispatch(changeShowRead(e.currentTarget.checked))}
/> />
<Switch <Switch
label={<Trans>In expanded view, scrolling through entries mark them as read</Trans>} label={<Trans>In expanded view, scrolling through entries mark them as read</Trans>}
checked={scrollMarks} checked={scrollMarks}
onChange={e => dispatch(changeScrollMarks(e.currentTarget.checked))} onChange={async e => await dispatch(changeScrollMarks(e.currentTarget.checked))}
/>
<Switch
label={<Trans>Show confirmation when marking all entries as read</Trans>}
checked={markAllAsReadConfirmation}
onChange={async e => await dispatch(changeMarkAllAsReadConfirmation(e.currentTarget.checked))}
/>
<Switch
label={<Trans>Show CommaFeed's own context menu on right click</Trans>}
checked={customContextMenu}
onChange={async e => await dispatch(changeCustomContextMenu(e.currentTarget.checked))}
/> />
<Divider label={<Trans>Sharing sites</Trans>} labelPosition="center" /> <Divider label={<Trans>Sharing sites</Trans>} labelPosition="center" />
@@ -65,8 +81,15 @@ export function DisplaySettings() {
<Switch <Switch
key={site} key={site}
label={Constants.sharing[site].label} label={Constants.sharing[site].label}
checked={sharingSettings && sharingSettings[site]} checked={sharingSettings?.[site]}
onChange={e => dispatch(changeSharingSetting({ site, value: e.currentTarget.checked }))} onChange={async e =>
await dispatch(
changeSharingSetting({
site,
value: e.currentTarget.checked,
})
)
}
/> />
))} ))}
</SimpleGrid> </SimpleGrid>

View File

@@ -3,10 +3,10 @@ import { Anchor, Box, Button, Checkbox, Divider, Group, Input, PasswordInput, St
import { useForm } from "@mantine/form" import { useForm } from "@mantine/form"
import { openConfirmModal } from "@mantine/modals" import { openConfirmModal } from "@mantine/modals"
import { client, errorToStrings } from "app/client" import { client, errorToStrings } from "app/client"
import { redirectToLogin, redirectToSelectedSource } from "app/slices/redirect" import { redirectToLogin, redirectToSelectedSource } from "app/redirect/thunks"
import { reloadProfile } from "app/slices/user"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { ProfileModificationRequest } from "app/types" import { type ProfileModificationRequest } from "app/types"
import { reloadProfile } from "app/user/thunks"
import { Alert } from "components/Alert" import { Alert } from "components/Alert"
import { useEffect } from "react" import { useEffect } from "react"
import { useAsyncCallback } from "react-async-hook" import { useAsyncCallback } from "react-async-hook"
@@ -49,7 +49,7 @@ export function ProfileSettings() {
), ),
labels: { confirm: <Trans>Confirm</Trans>, cancel: <Trans>Cancel</Trans> }, labels: { confirm: <Trans>Confirm</Trans>, cancel: <Trans>Cancel</Trans> },
confirmProps: { color: "red" }, confirmProps: { color: "red" },
onConfirm: () => deleteProfile.execute(), onConfirm: async () => await deleteProfile.execute(),
}) })
useEffect(() => { useEffect(() => {
@@ -77,10 +77,7 @@ export function ProfileSettings() {
<form onSubmit={form.onSubmit(saveProfile.execute)}> <form onSubmit={form.onSubmit(saveProfile.execute)}>
<Stack> <Stack>
<Input.Wrapper label={<Trans>User name</Trans>}> <TextInput label={<Trans>User name</Trans>} readOnly value={profile?.name} />
<Box>{profile?.name}</Box>
</Input.Wrapper>
<TextInput label={<Trans>API key</Trans>} readOnly value={profile?.apiKey} /> <TextInput label={<Trans>API key</Trans>} readOnly value={profile?.apiKey} />
<Input.Wrapper <Input.Wrapper
@@ -98,6 +95,22 @@ export function ProfileSettings() {
</Box> </Box>
</Input.Wrapper> </Input.Wrapper>
<Input.Wrapper
label={<Trans>Fever API</Trans>}
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.
</Trans>
}
>
<Box>
<Anchor href={`rest/fever/user/${profile?.id}`} target="_blank">
<Trans>Fever API URL</Trans>
</Anchor>
</Box>
</Input.Wrapper>
<Divider /> <Divider />
<PasswordInput <PasswordInput
@@ -116,16 +129,16 @@ export function ProfileSettings() {
<Checkbox label={<Trans>Generate new API key</Trans>} {...form.getInputProps("newApiKey", { type: "checkbox" })} /> <Checkbox label={<Trans>Generate new API key</Trans>} {...form.getInputProps("newApiKey", { type: "checkbox" })} />
<Group> <Group>
<Button variant="default" onClick={() => dispatch(redirectToSelectedSource())}> <Button variant="default" onClick={async () => await dispatch(redirectToSelectedSource())}>
<Trans>Cancel</Trans> <Trans>Cancel</Trans>
</Button> </Button>
<Button type="submit" leftIcon={<TbDeviceFloppy size={16} />} loading={saveProfile.loading}> <Button type="submit" leftSection={<TbDeviceFloppy size={16} />} loading={saveProfile.loading}>
<Trans>Save</Trans> <Trans>Save</Trans>
</Button> </Button>
<Divider orientation="vertical" /> <Divider orientation="vertical" />
<Button <Button
color="red" color="red"
leftIcon={<TbTrash size={16} />} leftSection={<TbTrash size={16} />}
onClick={() => openDeleteProfileModal()} onClick={() => openDeleteProfileModal()}
loading={deleteProfile.loading} loading={deleteProfile.loading}
> >

View File

@@ -8,10 +8,10 @@ import {
redirectToFeedDetails, redirectToFeedDetails,
redirectToTag, redirectToTag,
redirectToTagDetails, redirectToTagDetails,
} from "app/slices/redirect" } from "app/redirect/thunks"
import { collapseTreeCategory } from "app/slices/tree"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { Category, Subscription } from "app/types" import { collapseTreeCategory } from "app/tree/thunks"
import { type Category, type Subscription } from "app/types"
import { categoryUnreadCount, flattenCategoryTree } from "app/utils" import { categoryUnreadCount, flattenCategoryTree } from "app/utils"
import { Loader } from "components/Loader" import { Loader } from "components/Loader"
import { OnDesktop } from "components/responsive/OnDesktop" import { OnDesktop } from "components/responsive/OnDesktop"
@@ -36,8 +36,11 @@ export function Tree() {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const feedClicked = (e: React.MouseEvent, id: string) => { const feedClicked = (e: React.MouseEvent, id: string) => {
if (e.detail === 2) dispatch(redirectToFeedDetails(id)) if (e.detail === 2) {
else dispatch(redirectToFeed(id)) dispatch(redirectToFeedDetails(id))
} else {
dispatch(redirectToFeed(id))
}
} }
const categoryClicked = (e: React.MouseEvent, id: string) => { const categoryClicked = (e: React.MouseEvent, id: string) => {
if (e.detail === 2) { if (e.detail === 2) {
@@ -57,8 +60,11 @@ export function Tree() {
) )
} }
const tagClicked = (e: React.MouseEvent, id: string) => { const tagClicked = (e: React.MouseEvent, id: string) => {
if (e.detail === 2) dispatch(redirectToTagDetails(id)) if (e.detail === 2) {
else dispatch(redirectToTag(id)) dispatch(redirectToTagDetails(id))
} else {
dispatch(redirectToTag(id))
}
} }
const allCategoryNode = () => ( const allCategoryNode = () => (

View File

@@ -1,6 +1,8 @@
import { Box, Center, createStyles } from "@mantine/core" import { Box, Center, type MantineTheme, useMantineTheme } from "@mantine/core"
import { FeedFavicon } from "components/content/FeedFavicon" import { FeedFavicon } from "components/content/FeedFavicon"
import React, { ReactNode } from "react" import { useColorScheme } from "hooks/useColorScheme"
import React, { type ReactNode } from "react"
import { tss } from "tss"
import { UnreadCount } from "./UnreadCount" import { UnreadCount } from "./UnreadCount"
interface TreeNodeProps { interface TreeNodeProps {
@@ -16,40 +18,60 @@ interface TreeNodeProps {
onIconClick?: (e: React.MouseEvent, id: string) => void onIconClick?: (e: React.MouseEvent, id: string) => void
} }
const useStyles = createStyles((theme, props: TreeNodeProps) => { const useStyles = tss
let backgroundColor = "inherit" .withParams<{
if (props.selected) backgroundColor = theme.colorScheme === "dark" ? theme.colors.dark[4] : theme.colors.gray[3] theme: MantineTheme
colorScheme: "dark" | "light"
selected: boolean
hasError: boolean
hasUnread: boolean
}>()
.create(({ theme, colorScheme, selected, hasError, hasUnread }) => {
let backgroundColor = "inherit"
if (selected) backgroundColor = colorScheme === "dark" ? theme.colors.dark[4] : theme.colors.gray[1]
let color let color
if (props.hasError) color = theme.colors.red[6] if (hasError) {
else if (theme.colorScheme === "dark") color = props.unread > 0 ? theme.colors.dark[0] : theme.colors.dark[3] color = theme.colors.red[6]
else color = props.unread > 0 ? theme.black : theme.colors.gray[6] } else if (colorScheme === "dark") {
color = hasUnread ? theme.colors.dark[0] : theme.colors.dark[3]
} else {
color = hasUnread ? theme.black : theme.colors.gray[6]
}
return { return {
node: { node: {
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
cursor: "pointer", cursor: "pointer",
color, color,
backgroundColor, backgroundColor,
"&:hover": { "&:hover": {
backgroundColor: theme.colorScheme === "dark" ? theme.colors.dark[6] : theme.colors.gray[0], backgroundColor: colorScheme === "dark" ? theme.colors.dark[6] : theme.colors.gray[0],
},
}, },
}, nodeText: {
nodeText: { flexGrow: 1,
flexGrow: 1, whiteSpace: "nowrap",
whiteSpace: "nowrap", overflow: "hidden",
overflow: "hidden", textOverflow: "ellipsis",
textOverflow: "ellipsis", },
}, }
} })
})
export function TreeNode(props: TreeNodeProps) { export function TreeNode(props: TreeNodeProps) {
const { classes } = useStyles(props) const theme = useMantineTheme()
const colorScheme = useColorScheme()
const { classes } = useStyles({
theme,
colorScheme,
selected: props.selected,
hasError: props.hasError,
hasUnread: props.unread > 0,
})
return ( return (
<Box py={1} pl={props.level * 20} className={classes.node} onClick={(e: React.MouseEvent) => props.onClick(e, props.id)}> <Box py={1} pl={props.level * 20} className={classes.node} onClick={(e: React.MouseEvent) => props.onClick(e, props.id)}>
<Box mr={6} onClick={(e: React.MouseEvent) => props.onIconClick && props.onIconClick(e, props.id)}> <Box mr={6} onClick={(e: React.MouseEvent) => props.onIconClick?.(e, props.id)}>
<Center>{typeof props.icon === "string" ? <FeedFavicon url={props.icon} /> : props.icon}</Center> <Center>{typeof props.icon === "string" ? <FeedFavicon url={props.icon} /> : props.icon}</Center>
</Box> </Box>
<Box className={classes.nodeText}>{props.name}</Box> <Box className={classes.nodeText}>{props.name}</Box>

View File

@@ -1,9 +1,9 @@
import { t, Trans } from "@lingui/macro" import { t, Trans } from "@lingui/macro"
import { Box, Center, Kbd, TextInput } from "@mantine/core" import { Box, Center, Kbd, TextInput } from "@mantine/core"
import { openSpotlight, SpotlightAction, SpotlightProvider } from "@mantine/spotlight" import { Spotlight, spotlight, type SpotlightActionData } from "@mantine/spotlight"
import { redirectToFeed } from "app/slices/redirect" import { redirectToFeed } from "app/redirect/thunks"
import { useAppDispatch } from "app/store" import { useAppDispatch } from "app/store"
import { Subscription } from "app/types" import { type Subscription } from "app/types"
import { FeedFavicon } from "components/content/FeedFavicon" import { FeedFavicon } from "components/content/FeedFavicon"
import { useMousetrap } from "hooks/useMousetrap" import { useMousetrap } from "hooks/useMousetrap"
import { TbSearch } from "react-icons/tb" import { TbSearch } from "react-icons/tb"
@@ -15,17 +15,18 @@ export interface TreeSearchProps {
export function TreeSearch(props: TreeSearchProps) { export function TreeSearch(props: TreeSearchProps) {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const actions: SpotlightAction[] = props.feeds const actions: SpotlightActionData[] = props.feeds
.sort((f1, f2) => f1.name.localeCompare(f2.name)) .toSorted((f1, f2) => f1.name.localeCompare(f2.name))
.map(f => ({ .map(f => ({
title: f.name, id: `${f.id}`,
icon: <FeedFavicon url={f.iconUrl} />, label: f.name,
onTrigger: () => dispatch(redirectToFeed(f.id)), leftSection: <FeedFavicon url={f.iconUrl} />,
onClick: async () => await dispatch(redirectToFeed(f.id)),
})) }))
const searchIcon = <TbSearch size={18} /> const searchIcon = <TbSearch size={18} />
const rightSection = ( const rightSection = (
<Center> <Center style={{ cursor: "pointer" }} onClick={() => spotlight.open()}>
<Kbd>Ctrl</Kbd> <Kbd>Ctrl</Kbd>
<Box mx={5}>+</Box> <Box mx={5}>+</Box>
<Kbd>K</Kbd> <Kbd>K</Kbd>
@@ -33,30 +34,35 @@ export function TreeSearch(props: TreeSearchProps) {
) )
// additional keyboard shortcut used by commafeed v1 // additional keyboard shortcut used by commafeed v1
useMousetrap("g u", () => openSpotlight()) useMousetrap("g u", () => spotlight.open())
return ( return (
<SpotlightProvider <>
actions={actions}
searchIcon={searchIcon}
searchPlaceholder={t`Search`}
shortcut="ctrl+k"
nothingFoundMessage={<Trans>Nothing found</Trans>}
>
<TextInput <TextInput
placeholder={t`Search`} placeholder={t`Search`}
icon={searchIcon} leftSection={searchIcon}
rightSectionWidth={100} rightSectionWidth={100}
rightSection={rightSection} rightSection={rightSection}
styles={{ styles={{
input: { cursor: "pointer" }, input: {
rightSection: { pointerEvents: "none" }, cursor: "pointer",
},
}} }}
onClick={() => openSpotlight()} onClick={() => spotlight.open()}
// prevent focus // prevent focus
onFocus={e => e.target.blur()} onFocus={e => e.target.blur()}
readOnly readOnly
/> />
</SpotlightProvider> <Spotlight
actions={actions}
limit={10}
shortcut="ctrl+k"
searchProps={{
leftSection: searchIcon,
placeholder: t`Search`,
}}
nothingFound={<Trans>Nothing found</Trans>}
></Spotlight>
</>
) )
} }

View File

@@ -1,6 +1,7 @@
import { Badge, createStyles } from "@mantine/core" import { Badge, Tooltip } from "@mantine/core"
import { tss } from "tss"
const useStyles = createStyles(() => ({ const useStyles = tss.create(() => ({
badge: { badge: {
width: "3.2rem", width: "3.2rem",
// for some reason, mantine Badge has "cursor: 'default'" // for some reason, mantine Badge has "cursor: 'default'"
@@ -13,6 +14,12 @@ export function UnreadCount(props: { unreadCount: number }) {
if (props.unreadCount <= 0) return null if (props.unreadCount <= 0) return null
const count = props.unreadCount >= 1000 ? "999+" : props.unreadCount const count = props.unreadCount >= 10000 ? "10k+" : props.unreadCount
return <Badge className={classes.badge}>{count}</Badge> return (
<Tooltip label={props.unreadCount} disabled={props.unreadCount === count}>
<Badge className={classes.badge} variant="light">
{count}
</Badge>
</Tooltip>
)
} }

View File

@@ -0,0 +1,4 @@
import { useComputedColorScheme } from "@mantine/core"
// the color scheme to use to render components
export const useColorScheme = () => useComputedColorScheme("light")

View File

@@ -1,4 +1,9 @@
import { useMediaQuery } from "@mantine/hooks" import { useMediaQuery } from "@mantine/hooks"
import { Constants } from "app/constants" import { Constants } from "app/constants"
export const useMobile = (breakpoint: string = Constants.layout.mobileBreakpoint) => !useMediaQuery(`(min-width: ${breakpoint})`) export const useMobile = (breakpoint: string | number = Constants.layout.mobileBreakpoint) => {
const bp = typeof breakpoint === "number" ? `${breakpoint}px` : breakpoint
return !useMediaQuery(`(min-width: ${bp})`, undefined, {
getInitialValueInEffect: false,
})
}

View File

@@ -1,4 +1,4 @@
import mousetrap, { ExtendedKeyboardEvent } from "mousetrap" import mousetrap, { type ExtendedKeyboardEvent } from "mousetrap"
import { useEffect, useRef } from "react" import { useEffect, useRef } from "react"
type Callback = (e: ExtendedKeyboardEvent, combo: string) => void type Callback = (e: ExtendedKeyboardEvent, combo: string) => void

View File

@@ -1,4 +1,4 @@
import { ViewMode } from "app/types" import { type ViewMode } from "app/types"
import useLocalStorage from "use-local-storage" import useLocalStorage from "use-local-storage"
export function useViewMode() { export function useViewMode() {

View File

@@ -1,24 +1,37 @@
import { reloadTree } from "app/slices/tree" import { setWebSocketConnected } from "app/server/slice"
import { useAppDispatch } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { reloadTree } from "app/tree/thunks"
import { useEffect } from "react" import { useEffect } from "react"
import WebsocketHeartbeatJs from "websocket-heartbeat-js" import WebsocketHeartbeatJs from "websocket-heartbeat-js"
export const useWebSocket = () => { export const useWebSocket = () => {
const websocketEnabled = useAppSelector(state => state.server.serverInfos?.websocketEnabled)
const websocketPingInterval = useAppSelector(state => state.server.serverInfos?.websocketPingInterval)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
useEffect(() => { useEffect(() => {
const currentUrl = new URL(window.location.href) let ws: WebsocketHeartbeatJs | undefined
const wsProtocol = currentUrl.protocol === "http:" ? "ws" : "wss"
const wsUrl = `${wsProtocol}://${currentUrl.hostname}:${currentUrl.port}/ws`
const ws = new WebsocketHeartbeatJs({ url: wsUrl, pingMsg: "ping" }) if (websocketEnabled && websocketPingInterval) {
ws.onmessage = event => { const currentUrl = new URL(window.location.href)
const { data } = event const wsProtocol = currentUrl.protocol === "http:" ? "ws" : "wss"
if (typeof data === "string") { const wsUrl = `${wsProtocol}://${currentUrl.hostname}:${currentUrl.port}${currentUrl.pathname}ws`
if (data.startsWith("new-feed-entries:")) dispatch(reloadTree())
ws = new WebsocketHeartbeatJs({
url: wsUrl,
pingMsg: "ping",
pingTimeout: websocketPingInterval,
})
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())
}
} }
} }
return () => ws.close() return () => ws?.close()
}, [dispatch]) }, [dispatch, websocketEnabled, websocketPingInterval])
} }

View File

@@ -1,4 +1,4 @@
import { i18n } from "@lingui/core" import { i18n, type Messages } from "@lingui/core"
import { useAppSelector } from "app/store" import { useAppSelector } from "app/store"
import dayjs from "dayjs" import dayjs from "dayjs"
import { useEffect } from "react" import { useEffect } from "react"
@@ -12,40 +12,40 @@ interface Locale {
// add an object to the array to add a new locale // add an object to the array to add a new locale
// don't forget to also add it to the 'locales' array in .linguirc // don't forget to also add it to the 'locales' array in .linguirc
export const locales: Locale[] = [ export const locales: Locale[] = [
{ key: "ar", label: "العربية", daysjsImportFn: () => import("dayjs/locale/ar") }, { key: "ar", label: "العربية", daysjsImportFn: async () => await import("dayjs/locale/ar") },
{ key: "ca", label: "Català", daysjsImportFn: () => import("dayjs/locale/ca") }, { key: "ca", label: "Català", daysjsImportFn: async () => await import("dayjs/locale/ca") },
{ key: "cs", label: "Čeština", daysjsImportFn: () => import("dayjs/locale/cs") }, { key: "cs", label: "Čeština", daysjsImportFn: async () => await import("dayjs/locale/cs") },
{ key: "cy", label: "Cymraeg", daysjsImportFn: () => import("dayjs/locale/cy") }, { key: "cy", label: "Cymraeg", daysjsImportFn: async () => await import("dayjs/locale/cy") },
{ key: "da", label: "Danish", daysjsImportFn: () => import("dayjs/locale/da") }, { key: "da", label: "Danish", daysjsImportFn: async () => await import("dayjs/locale/da") },
{ key: "de", label: "Deutsch", daysjsImportFn: () => import("dayjs/locale/de") }, { key: "de", label: "Deutsch", daysjsImportFn: async () => await import("dayjs/locale/de") },
{ key: "en", label: "English", daysjsImportFn: () => import("dayjs/locale/en") }, { key: "en", label: "English", daysjsImportFn: async () => await import("dayjs/locale/en") },
{ key: "es", label: "Español", daysjsImportFn: () => import("dayjs/locale/es") }, { key: "es", label: "Español", daysjsImportFn: async () => await import("dayjs/locale/es") },
{ key: "fa", label: "فارسی", daysjsImportFn: () => import("dayjs/locale/fa") }, { key: "fa", label: "فارسی", daysjsImportFn: async () => await import("dayjs/locale/fa") },
{ key: "fi", label: "Suomi", daysjsImportFn: () => import("dayjs/locale/fi") }, { key: "fi", label: "Suomi", daysjsImportFn: async () => await import("dayjs/locale/fi") },
{ key: "fr", label: "Français", daysjsImportFn: () => import("dayjs/locale/fr") }, { key: "fr", label: "Français", daysjsImportFn: async () => await import("dayjs/locale/fr") },
{ key: "gl", label: "Galician", daysjsImportFn: () => import("dayjs/locale/gl") }, { key: "gl", label: "Galician", daysjsImportFn: async () => await import("dayjs/locale/gl") },
{ key: "hu", label: "Magyar", daysjsImportFn: () => import("dayjs/locale/hu") }, { key: "hu", label: "Magyar", daysjsImportFn: async () => await import("dayjs/locale/hu") },
{ key: "id", label: "Indonesian", daysjsImportFn: () => import("dayjs/locale/id") }, { key: "id", label: "Indonesian", daysjsImportFn: async () => await import("dayjs/locale/id") },
{ key: "it", label: "Italiano", daysjsImportFn: () => import("dayjs/locale/it") }, { key: "it", label: "Italiano", daysjsImportFn: async () => await import("dayjs/locale/it") },
{ key: "ja", label: "日本語", daysjsImportFn: () => import("dayjs/locale/ja") }, { key: "ja", label: "日本語", daysjsImportFn: async () => await import("dayjs/locale/ja") },
{ key: "ko", label: "한국어", daysjsImportFn: () => import("dayjs/locale/ko") }, { key: "ko", label: "한국어", daysjsImportFn: async () => await import("dayjs/locale/ko") },
{ key: "ms", label: "Bahasa Malaysian", daysjsImportFn: () => import("dayjs/locale/ms") }, { key: "ms", label: "Bahasa Malaysian", daysjsImportFn: async () => await import("dayjs/locale/ms") },
{ key: "nb", label: "Norsk (bokmål)", daysjsImportFn: () => import("dayjs/locale/nb") }, { key: "nb", label: "Norsk (bokmål)", daysjsImportFn: async () => await import("dayjs/locale/nb") },
{ key: "nl", label: "Nederlands", daysjsImportFn: () => import("dayjs/locale/nl") }, { key: "nl", label: "Nederlands", daysjsImportFn: async () => await import("dayjs/locale/nl") },
{ key: "nn", label: "Norsk (nynorsk)", daysjsImportFn: () => import("dayjs/locale/nn") }, { key: "nn", label: "Norsk (nynorsk)", daysjsImportFn: async () => await import("dayjs/locale/nn") },
{ key: "pl", label: "Polski", daysjsImportFn: () => import("dayjs/locale/pl") }, { key: "pl", label: "Polski", daysjsImportFn: async () => await import("dayjs/locale/pl") },
{ key: "pt", label: "Português", daysjsImportFn: () => import("dayjs/locale/pt") }, { key: "pt", label: "Português", daysjsImportFn: async () => await import("dayjs/locale/pt") },
{ key: "ru", label: "Русский", daysjsImportFn: () => import("dayjs/locale/ru") }, { key: "ru", label: "Русский", daysjsImportFn: async () => await import("dayjs/locale/ru") },
{ key: "sk", label: "Slovenčina", daysjsImportFn: () => import("dayjs/locale/sk") }, { key: "sk", label: "Slovenčina", daysjsImportFn: async () => await import("dayjs/locale/sk") },
{ key: "sv", label: "Svenska", daysjsImportFn: () => import("dayjs/locale/sv") }, { key: "sv", label: "Svenska", daysjsImportFn: async () => await import("dayjs/locale/sv") },
{ key: "tr", label: "Türkçe", daysjsImportFn: () => import("dayjs/locale/tr") }, { key: "tr", label: "Türkçe", daysjsImportFn: async () => await import("dayjs/locale/tr") },
{ key: "zh", label: "简体中文", daysjsImportFn: () => import("dayjs/locale/zh") }, { key: "zh", label: "简体中文", daysjsImportFn: async () => await import("dayjs/locale/zh") },
] ]
function activateLocale(locale: string) { function activateLocale(locale: string) {
// lingui // lingui
import(`./locales/${locale}/messages.po`).then(data => { import(`./locales/${locale}/messages.po`).then(data => {
i18n.load(locale, data.messages) i18n.load(locale, data.messages as Messages)
i18n.activate(locale) i18n.activate(locale)
}) })

View File

@@ -87,6 +87,10 @@ msgstr "ملف opml هو ملف XML يحتوي على عناوين URL للتغ
msgid "Analyze feed" msgid "Analyze feed"
msgstr "تحليل التغذية" msgstr "تحليل التغذية"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "API key" msgid "API key"
msgstr "مفتاح API" msgstr "مفتاح API"
@@ -171,10 +175,18 @@ msgstr "سيؤدي تغيير كلمة المرور إلى إنشاء مفتاح
msgid "Check that the feed is working" msgid "Check that the feed is working"
msgstr "تأكد من عمل الخلاصة" msgstr "تأكد من عمل الخلاصة"
#: src/pages/app/Layout.tsx
msgid "Close menu"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}." msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "" 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 ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed التالي العنصر غير المقروء" msgstr "CommaFeed التالي العنصر غير المقروء"
@@ -203,10 +215,6 @@ msgstr "تأكيد كلمة المرور"
msgid "Cozy" msgid "Cozy"
msgstr "دافئ" msgstr "دافئ"
#: src/components/content/FeedEntryFooter.tsx
msgid "Create tag: {query}"
msgstr "إنشاء علامة: {استعلام}"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Ctrl" msgid "Ctrl"
msgstr "السيطرة" msgstr "السيطرة"
@@ -227,6 +235,10 @@ msgstr ""
msgid "Custom JS code that will be executed on page load" msgid "Custom JS code that will be executed on page load"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Dark"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
msgid "Date created" msgid "Date created"
msgstr "تاريخ الإنشاء" msgstr "تاريخ الإنشاء"
@@ -339,6 +351,14 @@ msgstr "موجز URL"
msgid "Fetch all my feeds now" msgid "Fetch all my feeds now"
msgstr "" msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx #: src/components/content/add/ImportOpml.tsx
msgid "file is required" msgid "file is required"
msgstr "الملف مطلوب" msgstr "الملف مطلوب"
@@ -429,6 +449,10 @@ msgstr "التحديث الأخير"
msgid "Last refresh message" msgid "Last refresh message"
msgstr "آخر رسالة تحديث" msgstr "آخر رسالة تحديث"
#: src/components/header/ProfileMenu.tsx
msgid "Light"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
#: src/pages/app/TagDetailsPage.tsx #: src/pages/app/TagDetailsPage.tsx
@@ -582,6 +606,10 @@ msgstr ""
msgid "Open link in new tab" msgid "Open link in new tab"
msgstr "" msgstr ""
#: src/pages/app/Layout.tsx
msgid "Open menu"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry" msgid "Open next entry"
msgstr "فتح الإدخال التالي" msgstr "فتح الإدخال التالي"
@@ -665,6 +693,7 @@ msgstr "تم إغلاق التسجيلات في مثيل CommaFeed هذا"
msgid "REST API" msgid "REST API"
msgstr "" msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click" msgid "Right click"
msgstr "" msgstr ""
@@ -704,7 +733,7 @@ msgstr "ضع التركيز على الإدخال السابق دون فتحه"
msgid "Settings" msgid "Settings"
msgstr "إعدادات" msgstr "إعدادات"
#: src/app/slices/user.ts #: src/app/user/slice.ts
msgid "Settings saved." msgid "Settings saved."
msgstr "تم حفظ الإعدادات." msgstr "تم حفظ الإعدادات."
@@ -716,11 +745,20 @@ msgstr "مشاركة"
msgid "Sharing sites" msgid "Sharing sites"
msgstr "مشاركة المواقع" msgstr "مشاركة المواقع"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift" msgid "Shift"
msgstr "الحلقة" msgstr "الحلقة"
#: src/components/settings/DisplaySettings.tsx
msgid "Show CommaFeed's own context menu on right click"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show confirmation when marking all entries as read"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)" msgid "Show entry menu (desktop)"
msgstr "" msgstr ""
@@ -737,6 +775,10 @@ msgstr "إظهار موجز ويب والفئات التي لا تحتوي عل
msgid "Show keyboard shortcut help" msgid "Show keyboard shortcut help"
msgstr "إظهار تعليمات اختصار لوحة المفاتيح" msgstr "إظهار تعليمات اختصار لوحة المفاتيح"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show native menu (desktop)"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
@@ -784,16 +826,19 @@ msgstr "النجاح"
msgid "Swipe header to the right" msgid "Swipe header to the right"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
msgid "Switch to dark theme" msgid "Switch to dark theme"
msgstr "التبديل إلى النسق الداكن" msgstr "التبديل إلى النسق الداكن"
#: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
msgid "Switch to light theme" msgid "Switch to light theme"
msgstr "قم بالتبديل إلى النسق الفاتح" msgstr "قم بالتبديل إلى النسق الفاتح"
#: src/components/header/ProfileMenu.tsx
msgid "System"
msgstr ""
#: src/components/content/FeedEntryFooter.tsx
#: src/components/content/FeedEntryFooter.tsx #: src/components/content/FeedEntryFooter.tsx
msgid "Tags" msgid "Tags"
msgstr "الكلمات" msgstr "الكلمات"
@@ -814,6 +859,10 @@ msgstr "تبديل قراءة حالة الإدخال الحالي"
msgid "Toggle sidebar" msgid "Toggle sidebar"
msgstr "" msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "جرب CommaFeed باستخدام الحساب التجريبي: تجريبي / تجريبي" msgstr "جرب CommaFeed باستخدام الحساب التجريبي: تجريبي / تجريبي"

View File

@@ -87,6 +87,10 @@ msgstr "Un fitxer opml és un fitxer XML que conté URL i categories de canals.
msgid "Analyze feed" msgid "Analyze feed"
msgstr "Analitzar el feed" msgstr "Analitzar el feed"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "API key" msgid "API key"
msgstr "clau API" msgstr "clau API"
@@ -171,10 +175,18 @@ msgstr "Canviar la contrasenya generarà una nova clau d'API"
msgid "Check that the feed is working" msgid "Check that the feed is working"
msgstr "Comproveu que el canal funciona" msgstr "Comproveu que el canal funciona"
#: src/pages/app/Layout.tsx
msgid "Close menu"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}." msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "" 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 ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed següent element no llegit" msgstr "CommaFeed següent element no llegit"
@@ -203,10 +215,6 @@ msgstr "Confirmeu la contrasenya"
msgid "Cozy" msgid "Cozy"
msgstr "Acollidor" msgstr "Acollidor"
#: src/components/content/FeedEntryFooter.tsx
msgid "Create tag: {query}"
msgstr "Crea una etiqueta: {consulta}"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Ctrl" msgid "Ctrl"
msgstr "" msgstr ""
@@ -227,6 +235,10 @@ msgstr ""
msgid "Custom JS code that will be executed on page load" msgid "Custom JS code that will be executed on page load"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Dark"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
msgid "Date created" msgid "Date created"
msgstr "Data de creació" msgstr "Data de creació"
@@ -339,6 +351,14 @@ msgstr "URL del canal"
msgid "Fetch all my feeds now" msgid "Fetch all my feeds now"
msgstr "" msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx #: src/components/content/add/ImportOpml.tsx
msgid "file is required" msgid "file is required"
msgstr "el fitxer és necessari" msgstr "el fitxer és necessari"
@@ -429,6 +449,10 @@ msgstr "Última actualització"
msgid "Last refresh message" msgid "Last refresh message"
msgstr "últim missatge d'actualització" msgstr "últim missatge d'actualització"
#: src/components/header/ProfileMenu.tsx
msgid "Light"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
#: src/pages/app/TagDetailsPage.tsx #: src/pages/app/TagDetailsPage.tsx
@@ -582,6 +606,10 @@ msgstr ""
msgid "Open link in new tab" msgid "Open link in new tab"
msgstr "" msgstr ""
#: src/pages/app/Layout.tsx
msgid "Open menu"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry" msgid "Open next entry"
msgstr "Obre la següent entrada" msgstr "Obre la següent entrada"
@@ -665,6 +693,7 @@ msgstr "Els registres estan tancats en aquesta instància de CommaFeed"
msgid "REST API" msgid "REST API"
msgstr "API REST" msgstr "API REST"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click" msgid "Right click"
msgstr "" msgstr ""
@@ -704,7 +733,7 @@ msgstr "Estableix el focus en l'entrada anterior sense obrir-la"
msgid "Settings" msgid "Settings"
msgstr "Configuració" msgstr "Configuració"
#: src/app/slices/user.ts #: src/app/user/slice.ts
msgid "Settings saved." msgid "Settings saved."
msgstr "Configuració desada." msgstr "Configuració desada."
@@ -716,11 +745,20 @@ msgstr "Comparteix"
msgid "Sharing sites" msgid "Sharing sites"
msgstr "Compartir llocs" msgstr "Compartir llocs"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift" msgid "Shift"
msgstr "canvi" msgstr "canvi"
#: src/components/settings/DisplaySettings.tsx
msgid "Show CommaFeed's own context menu on right click"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show confirmation when marking all entries as read"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)" msgid "Show entry menu (desktop)"
msgstr "" msgstr ""
@@ -737,6 +775,10 @@ msgstr "Mostra feeds i categories sense entrades no llegides"
msgid "Show keyboard shortcut help" msgid "Show keyboard shortcut help"
msgstr "Mostra l'ajuda de la drecera del teclat" msgstr "Mostra l'ajuda de la drecera del teclat"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show native menu (desktop)"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
@@ -784,16 +826,19 @@ msgstr "Éxit"
msgid "Swipe header to the right" msgid "Swipe header to the right"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
msgid "Switch to dark theme" msgid "Switch to dark theme"
msgstr "Canvia al tema fosc" msgstr "Canvia al tema fosc"
#: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
msgid "Switch to light theme" msgid "Switch to light theme"
msgstr "Canvia al tema clar" msgstr "Canvia al tema clar"
#: src/components/header/ProfileMenu.tsx
msgid "System"
msgstr ""
#: src/components/content/FeedEntryFooter.tsx
#: src/components/content/FeedEntryFooter.tsx #: src/components/content/FeedEntryFooter.tsx
msgid "Tags" msgid "Tags"
msgstr "Etiquetes" msgstr "Etiquetes"
@@ -814,6 +859,10 @@ msgstr "Canvia l'estat de lectura de l'entrada actual"
msgid "Toggle sidebar" msgid "Toggle sidebar"
msgstr "" msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Proveu CommaFeed amb el compte de demostració: demo/demo" msgstr "Proveu CommaFeed amb el compte de demostració: demo/demo"

View File

@@ -87,6 +87,10 @@ msgstr "Soubor opml je soubor XML obsahující adresy URL a kategorie zdrojů. "
msgid "Analyze feed" msgid "Analyze feed"
msgstr "Analyzujte krmivo" msgstr "Analyzujte krmivo"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "API key" msgid "API key"
msgstr "Klíč API" msgstr "Klíč API"
@@ -171,10 +175,18 @@ msgstr "Změna hesla vygeneruje nový klíč API"
msgid "Check that the feed is working" msgid "Check that the feed is working"
msgstr "Zkontrolujte, zda zdroj funguje" msgstr "Zkontrolujte, zda zdroj funguje"
#: src/pages/app/Layout.tsx
msgid "Close menu"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}." msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "" 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 ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed další nepřečtená položka" msgstr "CommaFeed další nepřečtená položka"
@@ -203,10 +215,6 @@ msgstr "Potvrďte heslo"
msgid "Cozy" msgid "Cozy"
msgstr "Útulný" msgstr "Útulný"
#: src/components/content/FeedEntryFooter.tsx
msgid "Create tag: {query}"
msgstr "Vytvořit značku: {query}"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Ctrl" msgid "Ctrl"
msgstr "" msgstr ""
@@ -227,6 +235,10 @@ msgstr ""
msgid "Custom JS code that will be executed on page load" msgid "Custom JS code that will be executed on page load"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Dark"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
msgid "Date created" msgid "Date created"
msgstr "Datum vytvoření" msgstr "Datum vytvoření"
@@ -339,6 +351,14 @@ msgstr "URL zdroje"
msgid "Fetch all my feeds now" msgid "Fetch all my feeds now"
msgstr "" msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx #: src/components/content/add/ImportOpml.tsx
msgid "file is required" msgid "file is required"
msgstr "" msgstr ""
@@ -429,6 +449,10 @@ msgstr "Poslední aktualizace"
msgid "Last refresh message" msgid "Last refresh message"
msgstr "Poslední obnovovací zpráva" msgstr "Poslední obnovovací zpráva"
#: src/components/header/ProfileMenu.tsx
msgid "Light"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
#: src/pages/app/TagDetailsPage.tsx #: src/pages/app/TagDetailsPage.tsx
@@ -582,6 +606,10 @@ msgstr ""
msgid "Open link in new tab" msgid "Open link in new tab"
msgstr "" msgstr ""
#: src/pages/app/Layout.tsx
msgid "Open menu"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry" msgid "Open next entry"
msgstr "Otevřete další položku" msgstr "Otevřete další položku"
@@ -665,6 +693,7 @@ msgstr "V této instanci CommaFeed jsou registrace uzavřeny"
msgid "REST API" msgid "REST API"
msgstr "" msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click" msgid "Right click"
msgstr "" msgstr ""
@@ -704,7 +733,7 @@ msgstr "Nastavit fokus na předchozí záznam bez jeho otevření"
msgid "Settings" msgid "Settings"
msgstr "Nastavení" msgstr "Nastavení"
#: src/app/slices/user.ts #: src/app/user/slice.ts
msgid "Settings saved." msgid "Settings saved."
msgstr "Nastavení uloženo." msgstr "Nastavení uloženo."
@@ -716,11 +745,20 @@ msgstr "Sdílejte"
msgid "Sharing sites" msgid "Sharing sites"
msgstr "Stránky pro sdílení" msgstr "Stránky pro sdílení"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift" msgid "Shift"
msgstr "Směna" msgstr "Směna"
#: src/components/settings/DisplaySettings.tsx
msgid "Show CommaFeed's own context menu on right click"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show confirmation when marking all entries as read"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)" msgid "Show entry menu (desktop)"
msgstr "" msgstr ""
@@ -737,6 +775,10 @@ msgstr "Zobrazit kanály a kategorie bez nepřečtených položek"
msgid "Show keyboard shortcut help" msgid "Show keyboard shortcut help"
msgstr "Zobrazit nápovědu ke klávesovým zkratkám" msgstr "Zobrazit nápovědu ke klávesovým zkratkám"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show native menu (desktop)"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
@@ -784,16 +826,19 @@ msgstr "Úspěch"
msgid "Swipe header to the right" msgid "Swipe header to the right"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
msgid "Switch to dark theme" msgid "Switch to dark theme"
msgstr "Přepněte na tmavý motiv" msgstr "Přepněte na tmavý motiv"
#: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
msgid "Switch to light theme" msgid "Switch to light theme"
msgstr "Přepněte na světlé téma" msgstr "Přepněte na světlé téma"
#: src/components/header/ProfileMenu.tsx
msgid "System"
msgstr ""
#: src/components/content/FeedEntryFooter.tsx
#: src/components/content/FeedEntryFooter.tsx #: src/components/content/FeedEntryFooter.tsx
msgid "Tags" msgid "Tags"
msgstr "Značky" msgstr "Značky"
@@ -814,6 +859,10 @@ msgstr "Přepne stav čtení aktuálního záznamu"
msgid "Toggle sidebar" msgid "Toggle sidebar"
msgstr "" msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Vyzkoušejte CommaFeed s demo účtem: demo/demo" msgstr "Vyzkoušejte CommaFeed s demo účtem: demo/demo"

View File

@@ -87,6 +87,10 @@ msgstr "Mae ffeil opml yn ffeil XML sy'n cynnwys URLs porthiant a chategorïau.
msgid "Analyze feed" msgid "Analyze feed"
msgstr "Dadansoddi porthiant" msgstr "Dadansoddi porthiant"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "API key" msgid "API key"
msgstr "Allwedd API" msgstr "Allwedd API"
@@ -171,10 +175,18 @@ msgstr "Bydd newid cyfrinair yn cynhyrchu allwedd API newydd"
msgid "Check that the feed is working" msgid "Check that the feed is working"
msgstr "Gwiriwch fod y porthiant yn gweithio" msgstr "Gwiriwch fod y porthiant yn gweithio"
#: src/pages/app/Layout.tsx
msgid "Close menu"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}." msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "" 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 ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed eitem nesaf heb ei darllen" msgstr "CommaFeed eitem nesaf heb ei darllen"
@@ -203,10 +215,6 @@ msgstr "Cadarnhau'r cyfrinair"
msgid "Cozy" msgid "Cozy"
msgstr "clyd" msgstr "clyd"
#: src/components/content/FeedEntryFooter.tsx
msgid "Create tag: {query}"
msgstr "Creu tag: {query}"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Ctrl" msgid "Ctrl"
msgstr "" msgstr ""
@@ -227,6 +235,10 @@ msgstr ""
msgid "Custom JS code that will be executed on page load" msgid "Custom JS code that will be executed on page load"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Dark"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
msgid "Date created" msgid "Date created"
msgstr "Dyddiad creu" msgstr "Dyddiad creu"
@@ -339,6 +351,14 @@ msgstr "URL porthiant"
msgid "Fetch all my feeds now" msgid "Fetch all my feeds now"
msgstr "" msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx #: src/components/content/add/ImportOpml.tsx
msgid "file is required" msgid "file is required"
msgstr "mae angen y ffeil" msgstr "mae angen y ffeil"
@@ -429,6 +449,10 @@ msgstr "adnewyddu diwethaf"
msgid "Last refresh message" msgid "Last refresh message"
msgstr "Neges adnewyddu ddiwethaf" msgstr "Neges adnewyddu ddiwethaf"
#: src/components/header/ProfileMenu.tsx
msgid "Light"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
#: src/pages/app/TagDetailsPage.tsx #: src/pages/app/TagDetailsPage.tsx
@@ -582,6 +606,10 @@ msgstr ""
msgid "Open link in new tab" msgid "Open link in new tab"
msgstr "" msgstr ""
#: src/pages/app/Layout.tsx
msgid "Open menu"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry" msgid "Open next entry"
msgstr "Agor y cofnod nesaf" msgstr "Agor y cofnod nesaf"
@@ -665,6 +693,7 @@ msgstr "Mae cofrestriadau ar gau ar yr achos CommaFeed hwn"
msgid "REST API" msgid "REST API"
msgstr "" msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click" msgid "Right click"
msgstr "" msgstr ""
@@ -704,7 +733,7 @@ msgstr "Gosod ffocws ar gofnod blaenorol heb ei agor"
msgid "Settings" msgid "Settings"
msgstr "Gosodiadau" msgstr "Gosodiadau"
#: src/app/slices/user.ts #: src/app/user/slice.ts
msgid "Settings saved." msgid "Settings saved."
msgstr "Gosodiadau wedi'u cadw." msgstr "Gosodiadau wedi'u cadw."
@@ -716,11 +745,20 @@ msgstr "Rhannu"
msgid "Sharing sites" msgid "Sharing sites"
msgstr "Rhannu gwefannau" msgstr "Rhannu gwefannau"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift" msgid "Shift"
msgstr "shifft" msgstr "shifft"
#: src/components/settings/DisplaySettings.tsx
msgid "Show CommaFeed's own context menu on right click"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show confirmation when marking all entries as read"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)" msgid "Show entry menu (desktop)"
msgstr "" msgstr ""
@@ -737,6 +775,10 @@ msgstr "Dangos ffrydiau a chategorïau heb unrhyw gofnodion heb eu darllen"
msgid "Show keyboard shortcut help" msgid "Show keyboard shortcut help"
msgstr "Dangos cymorth llwybr byr bysellfwrdd" msgstr "Dangos cymorth llwybr byr bysellfwrdd"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show native menu (desktop)"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
@@ -784,16 +826,19 @@ msgstr "Llwyddiant"
msgid "Swipe header to the right" msgid "Swipe header to the right"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
msgid "Switch to dark theme" msgid "Switch to dark theme"
msgstr "Newid i thema dywyll" msgstr "Newid i thema dywyll"
#: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
msgid "Switch to light theme" msgid "Switch to light theme"
msgstr "Newid i thema golau" msgstr "Newid i thema golau"
#: src/components/header/ProfileMenu.tsx
msgid "System"
msgstr ""
#: src/components/content/FeedEntryFooter.tsx
#: src/components/content/FeedEntryFooter.tsx #: src/components/content/FeedEntryFooter.tsx
msgid "Tags" msgid "Tags"
msgstr "Tagiau" msgstr "Tagiau"
@@ -814,6 +859,10 @@ msgstr "Toglo statws darllen y cofnod cyfredol"
msgid "Toggle sidebar" msgid "Toggle sidebar"
msgstr "" msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Rhowch gynnig ar CommaFeed gyda'r cyfrif demo: demo / demo" msgstr "Rhowch gynnig ar CommaFeed gyda'r cyfrif demo: demo / demo"

View File

@@ -87,6 +87,10 @@ msgstr "En opml-fil er en XML-fil, der indeholder feed-URL'er og kategorier. "
msgid "Analyze feed" msgid "Analyze feed"
msgstr "Analyser foder" msgstr "Analyser foder"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "API key" msgid "API key"
msgstr "API-nøgle" msgstr "API-nøgle"
@@ -171,10 +175,18 @@ msgstr "Ændring af adgangskode vil generere en ny API-nøgle"
msgid "Check that the feed is working" msgid "Check that the feed is working"
msgstr "Tjek, at foderet virker" msgstr "Tjek, at foderet virker"
#: src/pages/app/Layout.tsx
msgid "Close menu"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}." msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "" 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 ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed næste ulæste element" msgstr "CommaFeed næste ulæste element"
@@ -203,10 +215,6 @@ msgstr "Bekræft adgangskode"
msgid "Cozy" msgid "Cozy"
msgstr "Hyggeligt" msgstr "Hyggeligt"
#: src/components/content/FeedEntryFooter.tsx
msgid "Create tag: {query}"
msgstr "Opret tag: {query}"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Ctrl" msgid "Ctrl"
msgstr "" msgstr ""
@@ -227,6 +235,10 @@ msgstr ""
msgid "Custom JS code that will be executed on page load" msgid "Custom JS code that will be executed on page load"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Dark"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
msgid "Date created" msgid "Date created"
msgstr "Dato oprettet" msgstr "Dato oprettet"
@@ -339,6 +351,14 @@ msgstr ""
msgid "Fetch all my feeds now" msgid "Fetch all my feeds now"
msgstr "" msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx #: src/components/content/add/ImportOpml.tsx
msgid "file is required" msgid "file is required"
msgstr "fil er påkrævet" msgstr "fil er påkrævet"
@@ -429,6 +449,10 @@ msgstr "Sidste opdatering"
msgid "Last refresh message" msgid "Last refresh message"
msgstr "Sidste opdateringsmeddelelse" msgstr "Sidste opdateringsmeddelelse"
#: src/components/header/ProfileMenu.tsx
msgid "Light"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
#: src/pages/app/TagDetailsPage.tsx #: src/pages/app/TagDetailsPage.tsx
@@ -582,6 +606,10 @@ msgstr ""
msgid "Open link in new tab" msgid "Open link in new tab"
msgstr "" msgstr ""
#: src/pages/app/Layout.tsx
msgid "Open menu"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry" msgid "Open next entry"
msgstr "Åbn næste post" msgstr "Åbn næste post"
@@ -665,6 +693,7 @@ msgstr "Registreringer er lukket på denne CommaFeed-instans"
msgid "REST API" msgid "REST API"
msgstr "" msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click" msgid "Right click"
msgstr "" msgstr ""
@@ -704,7 +733,7 @@ msgstr "Sæt fokus på forrige indtastning uden at åbne den"
msgid "Settings" msgid "Settings"
msgstr "Indstillinger" msgstr "Indstillinger"
#: src/app/slices/user.ts #: src/app/user/slice.ts
msgid "Settings saved." msgid "Settings saved."
msgstr "Indstillinger gemt." msgstr "Indstillinger gemt."
@@ -716,11 +745,20 @@ msgstr "Del"
msgid "Sharing sites" msgid "Sharing sites"
msgstr "Delingssider" msgstr "Delingssider"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift" msgid "Shift"
msgstr "Skift" msgstr "Skift"
#: src/components/settings/DisplaySettings.tsx
msgid "Show CommaFeed's own context menu on right click"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show confirmation when marking all entries as read"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)" msgid "Show entry menu (desktop)"
msgstr "" msgstr ""
@@ -737,6 +775,10 @@ msgstr "Vis feeds og kategorier uden ulæste poster"
msgid "Show keyboard shortcut help" msgid "Show keyboard shortcut help"
msgstr "Vis hjælp til tastaturgenveje" msgstr "Vis hjælp til tastaturgenveje"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show native menu (desktop)"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
@@ -784,16 +826,19 @@ msgstr "Succes"
msgid "Swipe header to the right" msgid "Swipe header to the right"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
msgid "Switch to dark theme" msgid "Switch to dark theme"
msgstr "Skift til mørkt tema" msgstr "Skift til mørkt tema"
#: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
msgid "Switch to light theme" msgid "Switch to light theme"
msgstr "Skift til lystema" msgstr "Skift til lystema"
#: src/components/header/ProfileMenu.tsx
msgid "System"
msgstr ""
#: src/components/content/FeedEntryFooter.tsx
#: src/components/content/FeedEntryFooter.tsx #: src/components/content/FeedEntryFooter.tsx
msgid "Tags" msgid "Tags"
msgstr "" msgstr ""
@@ -814,6 +859,10 @@ msgstr "Skift læsestatus for den aktuelle post"
msgid "Toggle sidebar" msgid "Toggle sidebar"
msgstr "" msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Prøv CommaFeed med demokontoen: demo/demo" msgstr "Prøv CommaFeed med demokontoen: demo/demo"

View File

@@ -19,11 +19,11 @@ msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>." msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
msgstr "" msgstr "CommaFeed ist ein Open Source Projekt. Der Quellcode wird auf auf </0><1>GitHub</1> gehostet."
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "<0>Complete syntax is available </0><1>here</1>." msgid "<0>Complete syntax is available </0><1>here</1>."
msgstr "" msgstr "Vollständiger Syntax ist </0><1>hier</1> verfügbar."
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
msgid "<0>Have an account?</0><1>Log in!</1>" msgid "<0>Have an account?</0><1>Log in!</1>"
@@ -35,12 +35,12 @@ msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "<0>Need an account?</0><1>Sign up!</1>" msgid "<0>Need an account?</0><1>Sign up!</1>"
msgstr "<0>Benötigen Sie ein Konto?</0><1>Melden Sie sich an!</1>" msgstr "<0>Benötigen Sie ein Konto?</0><1>Hier geht's zur Registrierung!</1>"
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "About" msgid "About"
msgstr "Ungefähr" msgstr "Über"
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
msgid "Actions" msgid "Actions"
@@ -77,16 +77,20 @@ msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "Eine E-Mail wurde gesendet, wenn diese Adresse registriert wurde. " msgstr "Eine E-Mail wurde gesendet, wenn diese Adresse registriert wurde. Bitte den Posteingang prüfen."
#: src/components/content/add/ImportOpml.tsx #: src/components/content/add/ImportOpml.tsx
msgid "An opml file is an XML file containing feed URLs and categories. You can get an OPML file by exporting your data from other feed reading services." msgid "An opml file is an XML file containing feed URLs and categories. You can get an OPML file by exporting your data from other feed reading services."
msgstr "Eine opml-Datei ist eine XML-Datei, die Feed-URLs und Kategorien enthält. " msgstr "Eine opml-Datei ist eine XML-Datei, die Feed-URLs und Kategorien enthält."
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
msgid "Analyze feed" msgid "Analyze feed"
msgstr "Feed analysieren" msgstr "Feed analysieren"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr "Ankündigung"
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "API key" msgid "API key"
msgstr "API-Schlüssel" msgstr "API-Schlüssel"
@@ -101,7 +105,7 @@ msgstr "Sind Sie sicher, dass Sie Benutzer <0>{userName}</0> löschen möchten?"
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "Are you sure you want to delete your account? There's no turning back!" msgid "Are you sure you want to delete your account? There's no turning back!"
msgstr "Sind Sie sicher, dass Sie Ihr Konto löschen möchten? " msgstr "Sind Sie sicher, dass Sie Ihr Konto löschen möchten?"
#: src/components/header/MarkAllAsReadButton.tsx #: src/components/header/MarkAllAsReadButton.tsx
msgid "Are you sure you want to mark all entries of <0>{sourceLabel}</0> as read?" msgid "Are you sure you want to mark all entries of <0>{sourceLabel}</0> as read?"
@@ -117,7 +121,7 @@ msgstr "Sind Sie sicher, dass Sie <0>{feedName}</0> abbestellen möchten?"
#: src/components/header/Header.tsx #: src/components/header/Header.tsx
msgid "Asc" msgid "Asc"
msgstr "Asz" msgstr "Aufsteigend"
#: src/pages/app/FeedDetailsPage.tsx #: 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." msgid "Available variables are 'title', 'content', 'url' 'author' and 'categories' and their content is converted to lower case to ease string comparison."
@@ -129,15 +133,15 @@ msgstr "Zurück"
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Back to log in" msgid "Back to log in"
msgstr "Zurück zum Anmelden" msgstr "Zurück zum Login"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome" msgid "Browser extension required for Chrome"
msgstr "" msgstr "Browser-Erweiterung für Chrome benötigt"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extention" msgid "Browser extention"
msgstr "" msgstr "Browser-Erweiterung"
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -171,10 +175,18 @@ msgstr "Das Ändern des Passworts generiert einen neuen API-Schlüssel"
msgid "Check that the feed is working" msgid "Check that the feed is working"
msgstr "Überprüfen Sie, ob der Feed funktioniert" msgstr "Überprüfen Sie, ob der Feed funktioniert"
#: src/pages/app/Layout.tsx
msgid "Close menu"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}." msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "" 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."
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed nächstes ungelesenes Element" msgstr "CommaFeed nächstes ungelesenes Element"
@@ -203,10 +215,6 @@ msgstr "Passwort bestätigen"
msgid "Cozy" msgid "Cozy"
msgstr "Gemütlich" msgstr "Gemütlich"
#: src/components/content/FeedEntryFooter.tsx
msgid "Create tag: {query}"
msgstr "Tag erstellen: {query}"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Ctrl" msgid "Ctrl"
msgstr "Strg" msgstr "Strg"
@@ -227,6 +235,10 @@ msgstr ""
msgid "Custom JS code that will be executed on page load" msgid "Custom JS code that will be executed on page load"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Dark"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
msgid "Date created" msgid "Date created"
msgstr "Erstellungsdatum" msgstr "Erstellungsdatum"
@@ -264,7 +276,7 @@ msgstr "Anzeige"
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/app/DonatePage.tsx #: src/pages/app/DonatePage.tsx
msgid "Donate" msgid "Donate"
msgstr "" msgstr "Spenden"
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "Download" msgid "Download"
@@ -323,7 +335,7 @@ msgstr "Exportieren Sie Ihre Abonnements und Kategorien als OPML-Datei, die in a
#: src/components/header/Header.tsx #: src/components/header/Header.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
msgid "Extension options" msgid "Extension options"
msgstr "" msgstr "Erweiterungsoptionen"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
msgid "Feed name" msgid "Feed name"
@@ -337,6 +349,14 @@ msgstr "Feed-URL"
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Fetch all my feeds now" msgid "Fetch all my feeds now"
msgstr "Alle Feeds jetzt abrufen"
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr "" msgstr ""
#: src/components/content/add/ImportOpml.tsx #: src/components/content/add/ImportOpml.tsx
@@ -369,7 +389,7 @@ msgstr "Generierte Feed-URL"
#: src/components/content/FeedEntryContextMenu.tsx #: src/components/content/FeedEntryContextMenu.tsx
msgid "Go to {0}" msgid "Go to {0}"
msgstr "" msgstr "Gehe zu {0}"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Go to the All view" msgid "Go to the All view"
@@ -381,7 +401,7 @@ msgstr "Gehen Sie zur API-Dokumentation."
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Goodies" msgid "Goodies"
msgstr "Gutes" msgstr "Goodies"
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
msgid "Id" msgid "Id"
@@ -411,7 +431,7 @@ msgstr "Ungelesen lassen"
#: src/components/content/FeedEntries.tsx #: src/components/content/FeedEntries.tsx
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Keyboard shortcuts" msgid "Keyboard shortcuts"
msgstr "Tastenkürzel" msgstr "Tastaturkürzel"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Language" msgid "Language"
@@ -429,6 +449,10 @@ msgstr "Letzte Aktualisierung"
msgid "Last refresh message" msgid "Last refresh message"
msgstr "Letzte Aktualisierungsmeldung" msgstr "Letzte Aktualisierungsmeldung"
#: src/components/header/ProfileMenu.tsx
msgid "Light"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
#: src/pages/app/TagDetailsPage.tsx #: src/pages/app/TagDetailsPage.tsx
@@ -463,7 +487,7 @@ msgstr "Abmelden"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Long press" msgid "Long press"
msgstr "" msgstr "Langer Tastendruck"
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
@@ -495,7 +519,7 @@ msgstr "Metriken"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Middle click" msgid "Middle click"
msgstr "" msgstr "Mittelklick"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Move the page down" msgid "Move the page down"
@@ -508,7 +532,7 @@ msgstr "Bewege die Seite nach oben"
#: src/components/RelativeDate.tsx #: src/components/RelativeDate.tsx
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "N/A" msgid "N/A"
msgstr "n. z" msgstr "n.v."
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
@@ -576,10 +600,14 @@ msgstr "Link öffnen"
#: src/components/content/FeedEntryContextMenu.tsx #: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new background tab" msgid "Open link in new background tab"
msgstr "" msgstr "Link in neuem Tab im Hintergrund öffnen"
#: src/components/content/FeedEntryContextMenu.tsx #: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new tab" msgid "Open link in new tab"
msgstr "Link in neuem Tab öffnen"
#: src/pages/app/Layout.tsx
msgid "Open menu"
msgstr "" msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
@@ -613,11 +641,11 @@ msgstr "Bestellung"
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
msgid "Parent" msgid "Parent"
msgstr "Elternteil" msgstr "Übergeordnet"
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
msgid "Parent Category" msgid "Parent Category"
msgstr "Elternkategorie" msgstr "Übergeordnete Kategorie"
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
@@ -638,11 +666,11 @@ msgstr "Passwörter stimmen nicht überein"
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "Position" msgid "Position"
msgstr "Stellung" msgstr "Position"
#: src/components/header/Header.tsx #: src/components/header/Header.tsx
msgid "Previous" msgid "Previous"
msgstr "" msgstr "Vorheriges"
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
@@ -665,9 +693,10 @@ msgstr "Registrierungen sind für diese CommaFeed-Instanz geschlossen"
msgid "REST API" msgid "REST API"
msgstr "REST-API" msgstr "REST-API"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click" msgid "Right click"
msgstr "" msgstr "Rechtsklick"
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
@@ -679,7 +708,7 @@ msgstr "Speichern"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Scroll smoothly when navigating between entries" msgid "Scroll smoothly when navigating between entries"
msgstr "Geschwindes Scrollen beim Navigieren zwischen Einträgen" msgstr "Schnelles Scrollen beim Navigieren zwischen Einträgen"
#: src/components/header/Header.tsx #: src/components/header/Header.tsx
#: src/components/header/Header.tsx #: src/components/header/Header.tsx
@@ -704,7 +733,7 @@ msgstr "Fokus auf vorherigen Eintrag setzen, ohne ihn zu öffnen"
msgid "Settings" msgid "Settings"
msgstr "Einstellungen" msgstr "Einstellungen"
#: src/app/slices/user.ts #: src/app/user/slice.ts
msgid "Settings saved." msgid "Settings saved."
msgstr "Einstellungen gespeichert." msgstr "Einstellungen gespeichert."
@@ -716,11 +745,20 @@ msgstr "Teilen"
msgid "Sharing sites" msgid "Sharing sites"
msgstr "Seiten teilen" msgstr "Seiten teilen"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift" msgid "Shift"
msgstr "Verschiebung" msgstr "Verschiebung"
#: src/components/settings/DisplaySettings.tsx
msgid "Show CommaFeed's own context menu on right click"
msgstr "CommaFeed-Kontextmenü anzeigen bei Rechtsklick"
#: src/components/settings/DisplaySettings.tsx
msgid "Show confirmation when marking all entries as read"
msgstr "Bestätigung beim Markieren von allen Einträgen als gelesen"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)" msgid "Show entry menu (desktop)"
msgstr "" msgstr ""
@@ -737,6 +775,10 @@ msgstr "Feeds und Kategorien ohne ungelesene Einträge anzeigen"
msgid "Show keyboard shortcut help" msgid "Show keyboard shortcut help"
msgstr "Tastenkürzel-Hilfe anzeigen" msgstr "Tastenkürzel-Hilfe anzeigen"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show native menu (desktop)"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
@@ -784,16 +826,19 @@ msgstr "Erfolg"
msgid "Swipe header to the right" msgid "Swipe header to the right"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
msgid "Switch to dark theme" msgid "Switch to dark theme"
msgstr "Zum dunklen Design wechseln" msgstr "Zum Darkmode wechseln"
#: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
msgid "Switch to light theme" msgid "Switch to light theme"
msgstr "Wechseln Sie zum Lichtdesign" msgstr "Zum Lightmode wechseln"
#: src/components/header/ProfileMenu.tsx
msgid "System"
msgstr ""
#: src/components/content/FeedEntryFooter.tsx
#: src/components/content/FeedEntryFooter.tsx #: src/components/content/FeedEntryFooter.tsx
msgid "Tags" msgid "Tags"
msgstr "" msgstr ""
@@ -812,7 +857,11 @@ msgstr "Lesestatus des aktuellen Eintrags umschalten"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar" msgid "Toggle sidebar"
msgstr "" msgstr "Sidebar an- und ausschalten"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr "Markierungsstatus des aktuellen Eintrags ändern"
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
@@ -820,7 +869,7 @@ msgstr "Testen Sie CommaFeed mit dem Demokonto: demo/demo"
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
msgid "Try the demo!" msgid "Try the demo!"
msgstr "" msgstr "Testen Sie die Demo!"
#: src/components/header/Header.tsx #: src/components/header/Header.tsx
msgid "Unread" msgid "Unread"
@@ -859,4 +908,4 @@ msgstr "Sie haben noch keine Abonnements. "
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Your feeds have been queued for refresh." msgid "Your feeds have been queued for refresh."
msgstr "" msgstr "Ihr Feed wurde für die Aktualisierung eingereiht."

View File

@@ -87,6 +87,10 @@ msgstr "An opml file is an XML file containing feed URLs and categories. You can
msgid "Analyze feed" msgid "Analyze feed"
msgstr "Analyze feed" msgstr "Analyze feed"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr "Announcement"
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "API key" msgid "API key"
msgstr "API key" msgstr "API key"
@@ -171,10 +175,18 @@ msgstr "Changing password will generate a new API key"
msgid "Check that the feed is working" msgid "Check that the feed is working"
msgstr "Check that the feed is working" msgstr "Check that the feed is working"
#: src/pages/app/Layout.tsx
msgid "Close menu"
msgstr "Close menu"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}." msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "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."
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed next unread item" msgstr "CommaFeed next unread item"
@@ -203,10 +215,6 @@ msgstr "Confirm password"
msgid "Cozy" msgid "Cozy"
msgstr "Cozy" msgstr "Cozy"
#: src/components/content/FeedEntryFooter.tsx
msgid "Create tag: {query}"
msgstr "Create tag: {query}"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Ctrl" msgid "Ctrl"
msgstr "Ctrl" msgstr "Ctrl"
@@ -227,6 +235,10 @@ msgstr "Custom CSS rules that will be applied"
msgid "Custom JS code that will be executed on page load" msgid "Custom JS code that will be executed on page load"
msgstr "Custom JS code that will be executed on page load" msgstr "Custom JS code that will be executed on page load"
#: src/components/header/ProfileMenu.tsx
msgid "Dark"
msgstr "Dark"
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
msgid "Date created" msgid "Date created"
msgstr "Date created" msgstr "Date created"
@@ -339,6 +351,14 @@ msgstr "Feed URL"
msgid "Fetch all my feeds now" msgid "Fetch all my feeds now"
msgstr "Fetch all my feeds now" msgstr "Fetch all my feeds now"
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr "Fever API"
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr "Fever API URL"
#: src/components/content/add/ImportOpml.tsx #: src/components/content/add/ImportOpml.tsx
msgid "file is required" msgid "file is required"
msgstr "file is required" msgstr "file is required"
@@ -429,6 +449,10 @@ msgstr "Last refresh"
msgid "Last refresh message" msgid "Last refresh message"
msgstr "Last refresh message" msgstr "Last refresh message"
#: src/components/header/ProfileMenu.tsx
msgid "Light"
msgstr "Light"
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
#: src/pages/app/TagDetailsPage.tsx #: src/pages/app/TagDetailsPage.tsx
@@ -582,6 +606,10 @@ msgstr "Open link in new background tab"
msgid "Open link in new tab" msgid "Open link in new tab"
msgstr "Open link in new tab" msgstr "Open link in new tab"
#: src/pages/app/Layout.tsx
msgid "Open menu"
msgstr "Open menu"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry" msgid "Open next entry"
msgstr "Open next entry" msgstr "Open next entry"
@@ -665,6 +693,7 @@ msgstr "Registrations are closed on this CommaFeed instance"
msgid "REST API" msgid "REST API"
msgstr "REST API" msgstr "REST API"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click" msgid "Right click"
msgstr "Right click" msgstr "Right click"
@@ -704,7 +733,7 @@ msgstr "Set focus on previous entry without opening it"
msgid "Settings" msgid "Settings"
msgstr "Settings" msgstr "Settings"
#: src/app/slices/user.ts #: src/app/user/slice.ts
msgid "Settings saved." msgid "Settings saved."
msgstr "Settings saved." msgstr "Settings saved."
@@ -716,11 +745,20 @@ msgstr "Share"
msgid "Sharing sites" msgid "Sharing sites"
msgstr "Sharing sites" msgstr "Sharing sites"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift" msgid "Shift"
msgstr "Shift" msgstr "Shift"
#: src/components/settings/DisplaySettings.tsx
msgid "Show CommaFeed's own context menu on right click"
msgstr "Show CommaFeed's own context menu on right click"
#: src/components/settings/DisplaySettings.tsx
msgid "Show confirmation when marking all entries as read"
msgstr "Show confirmation when marking all entries as read"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)" msgid "Show entry menu (desktop)"
msgstr "Show entry menu (desktop)" msgstr "Show entry menu (desktop)"
@@ -737,6 +775,10 @@ msgstr "Show feeds and categories with no unread entries"
msgid "Show keyboard shortcut help" msgid "Show keyboard shortcut help"
msgstr "Show keyboard shortcut help" msgstr "Show keyboard shortcut help"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show native menu (desktop)"
msgstr "Show native menu (desktop)"
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
@@ -784,16 +826,19 @@ msgstr "Success"
msgid "Swipe header to the right" msgid "Swipe header to the right"
msgstr "Swipe header to the right" msgstr "Swipe header to the right"
#: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
msgid "Switch to dark theme" msgid "Switch to dark theme"
msgstr "Switch to dark theme" msgstr "Switch to dark theme"
#: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
msgid "Switch to light theme" msgid "Switch to light theme"
msgstr "Switch to light theme" msgstr "Switch to light theme"
#: src/components/header/ProfileMenu.tsx
msgid "System"
msgstr "System"
#: src/components/content/FeedEntryFooter.tsx
#: src/components/content/FeedEntryFooter.tsx #: src/components/content/FeedEntryFooter.tsx
msgid "Tags" msgid "Tags"
msgstr "Tags" msgstr "Tags"
@@ -814,6 +859,10 @@ msgstr "Toggle read status of current entry"
msgid "Toggle sidebar" msgid "Toggle sidebar"
msgstr "Toggle sidebar" msgstr "Toggle sidebar"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr "Toggle starred status of current entry"
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Try out CommaFeed with the demo account: demo/demo" msgstr "Try out CommaFeed with the demo account: demo/demo"

View File

@@ -87,6 +87,10 @@ msgstr "Un archivo opml es un archivo XML que contiene categorías y direcciones
msgid "Analyze feed" msgid "Analyze feed"
msgstr "Analizar alimentación" msgstr "Analizar alimentación"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "API key" msgid "API key"
msgstr "clave API" msgstr "clave API"
@@ -171,10 +175,18 @@ msgstr "Cambiar la contraseña generará una nueva clave API"
msgid "Check that the feed is working" msgid "Check that the feed is working"
msgstr "Compruebe que el feed funciona" msgstr "Compruebe que el feed funciona"
#: src/pages/app/Layout.tsx
msgid "Close menu"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}." msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "" 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 ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed siguiente elemento no leído" msgstr "CommaFeed siguiente elemento no leído"
@@ -203,10 +215,6 @@ msgstr "Confirmar contraseña"
msgid "Cozy" msgid "Cozy"
msgstr "Acogedor" msgstr "Acogedor"
#: src/components/content/FeedEntryFooter.tsx
msgid "Create tag: {query}"
msgstr "Crear etiqueta: {consulta}"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Ctrl" msgid "Ctrl"
msgstr "" msgstr ""
@@ -227,6 +235,10 @@ msgstr ""
msgid "Custom JS code that will be executed on page load" msgid "Custom JS code that will be executed on page load"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Dark"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
msgid "Date created" msgid "Date created"
msgstr "Fecha de creación" msgstr "Fecha de creación"
@@ -339,6 +351,14 @@ msgstr "URL de fuente"
msgid "Fetch all my feeds now" msgid "Fetch all my feeds now"
msgstr "" msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx #: src/components/content/add/ImportOpml.tsx
msgid "file is required" msgid "file is required"
msgstr "archivo requerido" msgstr "archivo requerido"
@@ -429,6 +449,10 @@ msgstr "Última actualización"
msgid "Last refresh message" msgid "Last refresh message"
msgstr "Último mensaje de actualización" msgstr "Último mensaje de actualización"
#: src/components/header/ProfileMenu.tsx
msgid "Light"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
#: src/pages/app/TagDetailsPage.tsx #: src/pages/app/TagDetailsPage.tsx
@@ -582,6 +606,10 @@ msgstr ""
msgid "Open link in new tab" msgid "Open link in new tab"
msgstr "" msgstr ""
#: src/pages/app/Layout.tsx
msgid "Open menu"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry" msgid "Open next entry"
msgstr "Abrir siguiente entrada" msgstr "Abrir siguiente entrada"
@@ -665,6 +693,7 @@ msgstr "Los registros están cerrados en esta instancia de CommaFeed"
msgid "REST API" msgid "REST API"
msgstr "API REST" msgstr "API REST"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click" msgid "Right click"
msgstr "" msgstr ""
@@ -704,7 +733,7 @@ msgstr "Poner el foco en la entrada anterior sin abrirla"
msgid "Settings" msgid "Settings"
msgstr "Configuraciones" msgstr "Configuraciones"
#: src/app/slices/user.ts #: src/app/user/slice.ts
msgid "Settings saved." msgid "Settings saved."
msgstr "Ajustes guardados." msgstr "Ajustes guardados."
@@ -716,11 +745,20 @@ msgstr "Compartir"
msgid "Sharing sites" msgid "Sharing sites"
msgstr "Compartir sitios" msgstr "Compartir sitios"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift" msgid "Shift"
msgstr "Cambio" msgstr "Cambio"
#: src/components/settings/DisplaySettings.tsx
msgid "Show CommaFeed's own context menu on right click"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show confirmation when marking all entries as read"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)" msgid "Show entry menu (desktop)"
msgstr "" msgstr ""
@@ -737,6 +775,10 @@ msgstr "Mostrar feeds y categorías sin entradas no leídas"
msgid "Show keyboard shortcut help" msgid "Show keyboard shortcut help"
msgstr "Mostrar ayuda de atajo de teclado" msgstr "Mostrar ayuda de atajo de teclado"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show native menu (desktop)"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
@@ -784,16 +826,19 @@ msgstr "Éxito"
msgid "Swipe header to the right" msgid "Swipe header to the right"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
msgid "Switch to dark theme" msgid "Switch to dark theme"
msgstr "Cambiar a tema oscuro" msgstr "Cambiar a tema oscuro"
#: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
msgid "Switch to light theme" msgid "Switch to light theme"
msgstr "Cambiar a tema claro" msgstr "Cambiar a tema claro"
#: src/components/header/ProfileMenu.tsx
msgid "System"
msgstr ""
#: src/components/content/FeedEntryFooter.tsx
#: src/components/content/FeedEntryFooter.tsx #: src/components/content/FeedEntryFooter.tsx
msgid "Tags" msgid "Tags"
msgstr "Etiquetas" msgstr "Etiquetas"
@@ -814,6 +859,10 @@ msgstr "Alternar estado de lectura de la entrada actual"
msgid "Toggle sidebar" msgid "Toggle sidebar"
msgstr "" msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Pruebe CommaFeed con la cuenta demo: demo/demo" msgstr "Pruebe CommaFeed con la cuenta demo: demo/demo"

View File

@@ -87,6 +87,10 @@ msgstr "یک فایل opml یک فایل XML است که حاوی آدرس‌ه
msgid "Analyze feed" msgid "Analyze feed"
msgstr "خوراک را تجزیه و تحلیل کنید" msgstr "خوراک را تجزیه و تحلیل کنید"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "API key" msgid "API key"
msgstr "کلید API" msgstr "کلید API"
@@ -171,10 +175,18 @@ msgstr "تغییر رمز عبور یک کلید API جدید ایجاد می ک
msgid "Check that the feed is working" msgid "Check that the feed is working"
msgstr "بررسی کنید که خوراک کار می کند" msgstr "بررسی کنید که خوراک کار می کند"
#: src/pages/app/Layout.tsx
msgid "Close menu"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}." msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "" 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 ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "مورد خوانده نشده بعدی CommaFeed" msgstr "مورد خوانده نشده بعدی CommaFeed"
@@ -203,10 +215,6 @@ msgstr "رمز عبور را تأیید کنید"
msgid "Cozy" msgid "Cozy"
msgstr "دنج" msgstr "دنج"
#: src/components/content/FeedEntryFooter.tsx
msgid "Create tag: {query}"
msgstr "ایجاد برچسب: {query}"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Ctrl" msgid "Ctrl"
msgstr "" msgstr ""
@@ -227,6 +235,10 @@ msgstr ""
msgid "Custom JS code that will be executed on page load" msgid "Custom JS code that will be executed on page load"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Dark"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
msgid "Date created" msgid "Date created"
msgstr "تاریخ ایجاد" msgstr "تاریخ ایجاد"
@@ -339,6 +351,14 @@ msgstr "URL فید"
msgid "Fetch all my feeds now" msgid "Fetch all my feeds now"
msgstr "" msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx #: src/components/content/add/ImportOpml.tsx
msgid "file is required" msgid "file is required"
msgstr "فایل مورد نیاز است" msgstr "فایل مورد نیاز است"
@@ -429,6 +449,10 @@ msgstr "آخرین به روز رسانی"
msgid "Last refresh message" msgid "Last refresh message"
msgstr "آخرین پیام تازه کردن" msgstr "آخرین پیام تازه کردن"
#: src/components/header/ProfileMenu.tsx
msgid "Light"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
#: src/pages/app/TagDetailsPage.tsx #: src/pages/app/TagDetailsPage.tsx
@@ -582,6 +606,10 @@ msgstr ""
msgid "Open link in new tab" msgid "Open link in new tab"
msgstr "" msgstr ""
#: src/pages/app/Layout.tsx
msgid "Open menu"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry" msgid "Open next entry"
msgstr "ورودی بعدی را باز کنید" msgstr "ورودی بعدی را باز کنید"
@@ -665,6 +693,7 @@ msgstr "ثبت نام در این نمونه CommaFeed بسته شده است"
msgid "REST API" msgid "REST API"
msgstr "" msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click" msgid "Right click"
msgstr "" msgstr ""
@@ -704,7 +733,7 @@ msgstr "فوکوس را روی ورودی قبلی بدون باز کردن آن
msgid "Settings" msgid "Settings"
msgstr "تنظیمات" msgstr "تنظیمات"
#: src/app/slices/user.ts #: src/app/user/slice.ts
msgid "Settings saved." msgid "Settings saved."
msgstr "تنظیمات ذخیره شد." msgstr "تنظیمات ذخیره شد."
@@ -716,11 +745,20 @@ msgstr "به اشتراک بگذارید"
msgid "Sharing sites" msgid "Sharing sites"
msgstr "اشتراک گذاری سایت ها" msgstr "اشتراک گذاری سایت ها"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift" msgid "Shift"
msgstr "شیفت" msgstr "شیفت"
#: src/components/settings/DisplaySettings.tsx
msgid "Show CommaFeed's own context menu on right click"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show confirmation when marking all entries as read"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)" msgid "Show entry menu (desktop)"
msgstr "" msgstr ""
@@ -737,6 +775,10 @@ msgstr "فیدها و دسته ها را بدون ورودی خوانده نشد
msgid "Show keyboard shortcut help" msgid "Show keyboard shortcut help"
msgstr "نمایش راهنمایی میانبر صفحه کلید" msgstr "نمایش راهنمایی میانبر صفحه کلید"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show native menu (desktop)"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
@@ -784,16 +826,19 @@ msgstr "موفقیت"
msgid "Swipe header to the right" msgid "Swipe header to the right"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
msgid "Switch to dark theme" msgid "Switch to dark theme"
msgstr "تغییر به تم تیره" msgstr "تغییر به تم تیره"
#: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
msgid "Switch to light theme" msgid "Switch to light theme"
msgstr "روی زمینه روشن" msgstr "روی زمینه روشن"
#: src/components/header/ProfileMenu.tsx
msgid "System"
msgstr ""
#: src/components/content/FeedEntryFooter.tsx
#: src/components/content/FeedEntryFooter.tsx #: src/components/content/FeedEntryFooter.tsx
msgid "Tags" msgid "Tags"
msgstr "برچسب ها" msgstr "برچسب ها"
@@ -814,6 +859,10 @@ msgstr "وضعیت خواندن ورودی فعلی را تغییر دهید"
msgid "Toggle sidebar" msgid "Toggle sidebar"
msgstr "" msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "CommaFeed را با حساب آزمایشی امتحان کنید: دمو/دمو" msgstr "CommaFeed را با حساب آزمایشی امتحان کنید: دمو/دمو"

View File

@@ -87,6 +87,10 @@ msgstr "Opml-tiedosto on XML-tiedosto, joka sisältää syötteen URL-osoitteet
msgid "Analyze feed" msgid "Analyze feed"
msgstr "Analysoi syöte" msgstr "Analysoi syöte"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "API key" msgid "API key"
msgstr "API-avain" msgstr "API-avain"
@@ -171,10 +175,18 @@ msgstr "Salasanan vaihtaminen luo uuden API-avaimen"
msgid "Check that the feed is working" msgid "Check that the feed is working"
msgstr "Tarkista, että syöttö toimii" msgstr "Tarkista, että syöttö toimii"
#: src/pages/app/Layout.tsx
msgid "Close menu"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}." msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "" 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 ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed seuraava lukematon kohde" msgstr "CommaFeed seuraava lukematon kohde"
@@ -203,10 +215,6 @@ msgstr "Vahvista salasana"
msgid "Cozy" msgid "Cozy"
msgstr "Viihtyisä" msgstr "Viihtyisä"
#: src/components/content/FeedEntryFooter.tsx
msgid "Create tag: {query}"
msgstr "Luo tunniste: {query}"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Ctrl" msgid "Ctrl"
msgstr "" msgstr ""
@@ -227,6 +235,10 @@ msgstr ""
msgid "Custom JS code that will be executed on page load" msgid "Custom JS code that will be executed on page load"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Dark"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
msgid "Date created" msgid "Date created"
msgstr "Luontipäivämäärä" msgstr "Luontipäivämäärä"
@@ -339,6 +351,14 @@ msgstr "Syötteen URL-osoite"
msgid "Fetch all my feeds now" msgid "Fetch all my feeds now"
msgstr "" msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx #: src/components/content/add/ImportOpml.tsx
msgid "file is required" msgid "file is required"
msgstr "tiedosto vaaditaan" msgstr "tiedosto vaaditaan"
@@ -429,6 +449,10 @@ msgstr "Viimeinen päivitys"
msgid "Last refresh message" msgid "Last refresh message"
msgstr "Viimeinen päivitysviesti" msgstr "Viimeinen päivitysviesti"
#: src/components/header/ProfileMenu.tsx
msgid "Light"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
#: src/pages/app/TagDetailsPage.tsx #: src/pages/app/TagDetailsPage.tsx
@@ -582,6 +606,10 @@ msgstr ""
msgid "Open link in new tab" msgid "Open link in new tab"
msgstr "" msgstr ""
#: src/pages/app/Layout.tsx
msgid "Open menu"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry" msgid "Open next entry"
msgstr "Avaa seuraava merkintä" msgstr "Avaa seuraava merkintä"
@@ -665,6 +693,7 @@ msgstr "Tämän CommaFeed-esiintymän rekisteröinnit on suljettu"
msgid "REST API" msgid "REST API"
msgstr "" msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click" msgid "Right click"
msgstr "" msgstr ""
@@ -704,7 +733,7 @@ msgstr "Aseta kohdistus edelliseen merkintään avaamatta sitä"
msgid "Settings" msgid "Settings"
msgstr "Asetukset" msgstr "Asetukset"
#: src/app/slices/user.ts #: src/app/user/slice.ts
msgid "Settings saved." msgid "Settings saved."
msgstr "Asetukset tallennettu." msgstr "Asetukset tallennettu."
@@ -716,11 +745,20 @@ msgstr "Jaa"
msgid "Sharing sites" msgid "Sharing sites"
msgstr "Sivustojen jakaminen" msgstr "Sivustojen jakaminen"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift" msgid "Shift"
msgstr "Vaihto" msgstr "Vaihto"
#: src/components/settings/DisplaySettings.tsx
msgid "Show CommaFeed's own context menu on right click"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show confirmation when marking all entries as read"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)" msgid "Show entry menu (desktop)"
msgstr "" msgstr ""
@@ -737,6 +775,10 @@ msgstr "Näytä syötteet ja luokat ilman lukemattomia merkintöjä"
msgid "Show keyboard shortcut help" msgid "Show keyboard shortcut help"
msgstr "Näytä pikanäppäimen ohje" msgstr "Näytä pikanäppäimen ohje"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show native menu (desktop)"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
@@ -784,16 +826,19 @@ msgstr "Onnistui"
msgid "Swipe header to the right" msgid "Swipe header to the right"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
msgid "Switch to dark theme" msgid "Switch to dark theme"
msgstr "Vaihda tummaan teemaan" msgstr "Vaihda tummaan teemaan"
#: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
msgid "Switch to light theme" msgid "Switch to light theme"
msgstr "Vaihda vaaleaan teemaan" msgstr "Vaihda vaaleaan teemaan"
#: src/components/header/ProfileMenu.tsx
msgid "System"
msgstr ""
#: src/components/content/FeedEntryFooter.tsx
#: src/components/content/FeedEntryFooter.tsx #: src/components/content/FeedEntryFooter.tsx
msgid "Tags" msgid "Tags"
msgstr "" msgstr ""
@@ -814,6 +859,10 @@ msgstr "Vaihda nykyisen merkinnän lukutila"
msgid "Toggle sidebar" msgid "Toggle sidebar"
msgstr "" msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Kokeile CommaFeediä demotilillä: demo/demo" msgstr "Kokeile CommaFeediä demotilillä: demo/demo"

View File

@@ -87,6 +87,10 @@ msgstr "Un fichier OPML est un fichier XML contenant des URL de flux et des cat
msgid "Analyze feed" msgid "Analyze feed"
msgstr "Analyser le flux" msgstr "Analyser le flux"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr "Annonces"
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "API key" msgid "API key"
msgstr "Clé API" msgstr "Clé API"
@@ -171,10 +175,18 @@ msgstr "Changer de mot de passe générera une nouvelle clé API"
msgid "Check that the feed is working" msgid "Check that the feed is working"
msgstr "Vérifie que le flux fonctionne" msgstr "Vérifie que le flux fonctionne"
#: src/pages/app/Layout.tsx
msgid "Close menu"
msgstr "Fermer le menu"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}." msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "Extension CommaFeed pour navigateur 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."
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed prochain article non lu" msgstr "CommaFeed prochain article non lu"
@@ -203,10 +215,6 @@ msgstr "Confirmer le mot de passe"
msgid "Cozy" msgid "Cozy"
msgstr "Cozy" msgstr "Cozy"
#: src/components/content/FeedEntryFooter.tsx
msgid "Create tag: {query}"
msgstr "Créer le marqueur : {query}"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Ctrl" msgid "Ctrl"
msgstr "Ctrl" msgstr "Ctrl"
@@ -227,6 +235,10 @@ msgstr "Code CSS personnalisé qui sera appliqué"
msgid "Custom JS code that will be executed on page load" msgid "Custom JS code that will be executed on page load"
msgstr "Code JS personnalisé qui sera appliqué au chargement des pages" msgstr "Code JS personnalisé qui sera appliqué au chargement des pages"
#: src/components/header/ProfileMenu.tsx
msgid "Dark"
msgstr "Foncé"
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
msgid "Date created" msgid "Date created"
msgstr "Date de création" msgstr "Date de création"
@@ -264,7 +276,7 @@ msgstr "Affichage"
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/app/DonatePage.tsx #: src/pages/app/DonatePage.tsx
msgid "Donate" msgid "Donate"
msgstr "Faites un don" msgstr "Faire un don"
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "Download" msgid "Download"
@@ -339,6 +351,14 @@ msgstr "URL du flux"
msgid "Fetch all my feeds now" msgid "Fetch all my feeds now"
msgstr "Rafraîchir tous mes flux" msgstr "Rafraîchir tous mes flux"
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr "API Fever"
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr "URL API Fever"
#: src/components/content/add/ImportOpml.tsx #: src/components/content/add/ImportOpml.tsx
msgid "file is required" msgid "file is required"
msgstr "fichier requis" msgstr "fichier requis"
@@ -429,6 +449,10 @@ msgstr "Dernière mise à jour"
msgid "Last refresh message" msgid "Last refresh message"
msgstr "Dernier message de mise à jour" msgstr "Dernier message de mise à jour"
#: src/components/header/ProfileMenu.tsx
msgid "Light"
msgstr "Clair"
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
#: src/pages/app/TagDetailsPage.tsx #: src/pages/app/TagDetailsPage.tsx
@@ -582,6 +606,10 @@ msgstr "Ouvrir le lien dans un nouvel onglet en arrière-plan"
msgid "Open link in new tab" msgid "Open link in new tab"
msgstr "Ouvrir le lien dans un nouvel onglet" msgstr "Ouvrir le lien dans un nouvel onglet"
#: src/pages/app/Layout.tsx
msgid "Open menu"
msgstr "Ouvrir le menu"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry" msgid "Open next entry"
msgstr "Ouvrir l'entrée suivante" msgstr "Ouvrir l'entrée suivante"
@@ -665,6 +693,7 @@ msgstr "Les inscriptions sont fermées sur cette instance de CommaFeed"
msgid "REST API" msgid "REST API"
msgstr "API REST" msgstr "API REST"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click" msgid "Right click"
msgstr "Clic droit" msgstr "Clic droit"
@@ -704,7 +733,7 @@ msgstr "Sélectionner l'article précédent sans l'ouvrir"
msgid "Settings" msgid "Settings"
msgstr "Réglages" msgstr "Réglages"
#: src/app/slices/user.ts #: src/app/user/slice.ts
msgid "Settings saved." msgid "Settings saved."
msgstr "Réglages enregistrés." msgstr "Réglages enregistrés."
@@ -716,11 +745,20 @@ msgstr "Partager"
msgid "Sharing sites" msgid "Sharing sites"
msgstr "Sites de partage" msgstr "Sites de partage"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift" msgid "Shift"
msgstr "Maj" msgstr "Maj"
#: src/components/settings/DisplaySettings.tsx
msgid "Show CommaFeed's own context menu on right click"
msgstr "Afficher le menu contextuel de Commafeed lors d'un clic droit"
#: src/components/settings/DisplaySettings.tsx
msgid "Show confirmation when marking all entries as read"
msgstr "Demander une confirmation avant de tout marquer comme lu"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)" msgid "Show entry menu (desktop)"
msgstr "Afficher les options de l'entrée (ordinateur)" msgstr "Afficher les options de l'entrée (ordinateur)"
@@ -737,6 +775,10 @@ msgstr "Afficher les flux et les catégories pour lesquels tout est déjà lu"
msgid "Show keyboard shortcut help" msgid "Show keyboard shortcut help"
msgstr "Montrer les raccourcis clavier" msgstr "Montrer les raccourcis clavier"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show native menu (desktop)"
msgstr "Afficher les options du navigateur (ordinateur)"
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
@@ -784,16 +826,19 @@ msgstr "Succès"
msgid "Swipe header to the right" msgid "Swipe header to the right"
msgstr "Faire glisser le titre vers la droite" msgstr "Faire glisser le titre vers la droite"
#: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
msgid "Switch to dark theme" msgid "Switch to dark theme"
msgstr "Activer le mode sombre" msgstr "Activer le mode sombre"
#: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
msgid "Switch to light theme" msgid "Switch to light theme"
msgstr "Activer le mode clair" msgstr "Activer le mode clair"
#: src/components/header/ProfileMenu.tsx
msgid "System"
msgstr "Système"
#: src/components/content/FeedEntryFooter.tsx
#: src/components/content/FeedEntryFooter.tsx #: src/components/content/FeedEntryFooter.tsx
msgid "Tags" msgid "Tags"
msgstr "Marqueurs" msgstr "Marqueurs"
@@ -814,6 +859,10 @@ msgstr "Marquer l'entrée actuelle comme lue/non lue"
msgid "Toggle sidebar" msgid "Toggle sidebar"
msgstr "Montrer/cacher la barre latérale" msgstr "Montrer/cacher la barre latérale"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr "Montrer/cacher le statut favori de l'entrée"
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Essayez CommaFeed avec le compte de démonstration : demo/demo" msgstr "Essayez CommaFeed avec le compte de démonstration : demo/demo"

View File

@@ -87,6 +87,10 @@ msgstr "Un ficheiro opml é un ficheiro XML que contén URL e categorías de fon
msgid "Analyze feed" msgid "Analyze feed"
msgstr "Analizar feed" msgstr "Analizar feed"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "API key" msgid "API key"
msgstr "chave API" msgstr "chave API"
@@ -171,10 +175,18 @@ msgstr "O cambio de contrasinal xerará unha nova clave de API"
msgid "Check that the feed is working" msgid "Check that the feed is working"
msgstr "Comproba que a fonte funciona" msgstr "Comproba que a fonte funciona"
#: src/pages/app/Layout.tsx
msgid "Close menu"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}." msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "" 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 ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed seguinte elemento non lido" msgstr "CommaFeed seguinte elemento non lido"
@@ -203,10 +215,6 @@ msgstr "Confirmar contrasinal"
msgid "Cozy" msgid "Cozy"
msgstr "acolledor" msgstr "acolledor"
#: src/components/content/FeedEntryFooter.tsx
msgid "Create tag: {query}"
msgstr "Crear etiqueta: {consulta}"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Ctrl" msgid "Ctrl"
msgstr "" msgstr ""
@@ -227,6 +235,10 @@ msgstr ""
msgid "Custom JS code that will be executed on page load" msgid "Custom JS code that will be executed on page load"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Dark"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
msgid "Date created" msgid "Date created"
msgstr "Data de creación" msgstr "Data de creación"
@@ -339,6 +351,14 @@ msgstr "URL da fonte"
msgid "Fetch all my feeds now" msgid "Fetch all my feeds now"
msgstr "" msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx #: src/components/content/add/ImportOpml.tsx
msgid "file is required" msgid "file is required"
msgstr "é necesario o ficheiro" msgstr "é necesario o ficheiro"
@@ -429,6 +449,10 @@ msgstr "Última actualización"
msgid "Last refresh message" msgid "Last refresh message"
msgstr "Última mensaxe de actualización" msgstr "Última mensaxe de actualización"
#: src/components/header/ProfileMenu.tsx
msgid "Light"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
#: src/pages/app/TagDetailsPage.tsx #: src/pages/app/TagDetailsPage.tsx
@@ -582,6 +606,10 @@ msgstr ""
msgid "Open link in new tab" msgid "Open link in new tab"
msgstr "" msgstr ""
#: src/pages/app/Layout.tsx
msgid "Open menu"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry" msgid "Open next entry"
msgstr "Abrir a seguinte entrada" msgstr "Abrir a seguinte entrada"
@@ -665,6 +693,7 @@ msgstr "Os rexistros están pechados nesta instancia de CommaFeed"
msgid "REST API" msgid "REST API"
msgstr "API REST" msgstr "API REST"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click" msgid "Right click"
msgstr "" msgstr ""
@@ -704,7 +733,7 @@ msgstr "Establecer o foco na entrada anterior sen abrila"
msgid "Settings" msgid "Settings"
msgstr "Configuración" msgstr "Configuración"
#: src/app/slices/user.ts #: src/app/user/slice.ts
msgid "Settings saved." msgid "Settings saved."
msgstr "axustes gardados." msgstr "axustes gardados."
@@ -716,11 +745,20 @@ msgstr "Compartir"
msgid "Sharing sites" msgid "Sharing sites"
msgstr "Compartir sitios" msgstr "Compartir sitios"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift" msgid "Shift"
msgstr "quendas" msgstr "quendas"
#: src/components/settings/DisplaySettings.tsx
msgid "Show CommaFeed's own context menu on right click"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show confirmation when marking all entries as read"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)" msgid "Show entry menu (desktop)"
msgstr "" msgstr ""
@@ -737,6 +775,10 @@ msgstr "Mostrar fontes e categorías sen entradas sen ler"
msgid "Show keyboard shortcut help" msgid "Show keyboard shortcut help"
msgstr "Mostrar axuda do atallo do teclado" msgstr "Mostrar axuda do atallo do teclado"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show native menu (desktop)"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
@@ -784,16 +826,19 @@ msgstr "Éxito"
msgid "Swipe header to the right" msgid "Swipe header to the right"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
msgid "Switch to dark theme" msgid "Switch to dark theme"
msgstr "Cambiar ao tema escuro" msgstr "Cambiar ao tema escuro"
#: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
msgid "Switch to light theme" msgid "Switch to light theme"
msgstr "Cambiar ao tema claro" msgstr "Cambiar ao tema claro"
#: src/components/header/ProfileMenu.tsx
msgid "System"
msgstr ""
#: src/components/content/FeedEntryFooter.tsx
#: src/components/content/FeedEntryFooter.tsx #: src/components/content/FeedEntryFooter.tsx
msgid "Tags" msgid "Tags"
msgstr "Etiquetas" msgstr "Etiquetas"
@@ -814,6 +859,10 @@ msgstr "alternar o estado de lectura da entrada actual"
msgid "Toggle sidebar" msgid "Toggle sidebar"
msgstr "" msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Proba CommaFeed coa conta de demostración: demo/demo" msgstr "Proba CommaFeed coa conta de demostración: demo/demo"

View File

@@ -87,6 +87,10 @@ msgstr "Az opml-fájl olyan XML-fájl, amely feed URL-címeket és kategóriáka
msgid "Analyze feed" msgid "Analyze feed"
msgstr "Hírcsatorna elemzése" msgstr "Hírcsatorna elemzése"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "API key" msgid "API key"
msgstr "API kulcs" msgstr "API kulcs"
@@ -171,10 +175,18 @@ msgstr "A jelszó megváltoztatása új API-kulcsot generál"
msgid "Check that the feed is working" msgid "Check that the feed is working"
msgstr "Ellenőrizze, hogy a feed működik-e" msgstr "Ellenőrizze, hogy a feed működik-e"
#: src/pages/app/Layout.tsx
msgid "Close menu"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}." msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "" 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 ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed következő olvasatlan elem" msgstr "CommaFeed következő olvasatlan elem"
@@ -203,10 +215,6 @@ msgstr "Erősítse meg a jelszót"
msgid "Cozy" msgid "Cozy"
msgstr "Hangulatos" msgstr "Hangulatos"
#: src/components/content/FeedEntryFooter.tsx
msgid "Create tag: {query}"
msgstr "Címke létrehozása: {query}"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Ctrl" msgid "Ctrl"
msgstr "" msgstr ""
@@ -227,6 +235,10 @@ msgstr ""
msgid "Custom JS code that will be executed on page load" msgid "Custom JS code that will be executed on page load"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Dark"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
msgid "Date created" msgid "Date created"
msgstr "Létrehozás dátuma" msgstr "Létrehozás dátuma"
@@ -339,6 +351,14 @@ msgstr ""
msgid "Fetch all my feeds now" msgid "Fetch all my feeds now"
msgstr "" msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx #: src/components/content/add/ImportOpml.tsx
msgid "file is required" msgid "file is required"
msgstr "fájl szükséges" msgstr "fájl szükséges"
@@ -429,6 +449,10 @@ msgstr "Utolsó frissítés"
msgid "Last refresh message" msgid "Last refresh message"
msgstr "Utolsó frissítési üzenet" msgstr "Utolsó frissítési üzenet"
#: src/components/header/ProfileMenu.tsx
msgid "Light"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
#: src/pages/app/TagDetailsPage.tsx #: src/pages/app/TagDetailsPage.tsx
@@ -582,6 +606,10 @@ msgstr ""
msgid "Open link in new tab" msgid "Open link in new tab"
msgstr "" msgstr ""
#: src/pages/app/Layout.tsx
msgid "Open menu"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry" msgid "Open next entry"
msgstr "Következő bejegyzés megnyitása" msgstr "Következő bejegyzés megnyitása"
@@ -665,6 +693,7 @@ msgstr "A regisztrációk le vannak zárva ezen a CommaFeed példányon"
msgid "REST API" msgid "REST API"
msgstr "" msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click" msgid "Right click"
msgstr "" msgstr ""
@@ -704,7 +733,7 @@ msgstr "Állítsa a fókuszt az előző bejegyzésre anélkül, hogy megnyitná
msgid "Settings" msgid "Settings"
msgstr "Beállítások" msgstr "Beállítások"
#: src/app/slices/user.ts #: src/app/user/slice.ts
msgid "Settings saved." msgid "Settings saved."
msgstr "Beállítások mentve." msgstr "Beállítások mentve."
@@ -716,11 +745,20 @@ msgstr "Oszd meg"
msgid "Sharing sites" msgid "Sharing sites"
msgstr "Webhelyek megosztása" msgstr "Webhelyek megosztása"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift" msgid "Shift"
msgstr "" msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show CommaFeed's own context menu on right click"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show confirmation when marking all entries as read"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)" msgid "Show entry menu (desktop)"
msgstr "" msgstr ""
@@ -737,6 +775,10 @@ msgstr "Hírcsatornák és kategóriák megjelenítése olvasatlan bejegyzések
msgid "Show keyboard shortcut help" msgid "Show keyboard shortcut help"
msgstr "A billentyűparancsok súgójának megjelenítése" msgstr "A billentyűparancsok súgójának megjelenítése"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show native menu (desktop)"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
@@ -784,16 +826,19 @@ msgstr "Siker"
msgid "Swipe header to the right" msgid "Swipe header to the right"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
msgid "Switch to dark theme" msgid "Switch to dark theme"
msgstr "Váltás sötét témára" msgstr "Váltás sötét témára"
#: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
msgid "Switch to light theme" msgid "Switch to light theme"
msgstr "Váltás világos témára" msgstr "Váltás világos témára"
#: src/components/header/ProfileMenu.tsx
msgid "System"
msgstr ""
#: src/components/content/FeedEntryFooter.tsx
#: src/components/content/FeedEntryFooter.tsx #: src/components/content/FeedEntryFooter.tsx
msgid "Tags" msgid "Tags"
msgstr "Címkék" msgstr "Címkék"
@@ -814,6 +859,10 @@ msgstr "Az aktuális bejegyzés olvasási állapotának váltása"
msgid "Toggle sidebar" msgid "Toggle sidebar"
msgstr "" msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Próbálja ki a CommaFeed-et a demo fiókkal: demo/demo" msgstr "Próbálja ki a CommaFeed-et a demo fiókkal: demo/demo"

View File

@@ -87,6 +87,10 @@ msgstr "File opml adalah file XML yang berisi URL dan kategori feed. "
msgid "Analyze feed" msgid "Analyze feed"
msgstr "Analisis umpan" msgstr "Analisis umpan"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "API key" msgid "API key"
msgstr "kunci API" msgstr "kunci API"
@@ -171,10 +175,18 @@ msgstr "Mengubah kata sandi akan menghasilkan kunci API baru"
msgid "Check that the feed is working" msgid "Check that the feed is working"
msgstr "Periksa apakah umpannya berfungsi" msgstr "Periksa apakah umpannya berfungsi"
#: src/pages/app/Layout.tsx
msgid "Close menu"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}." msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "" 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 ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed item yang belum dibaca berikutnya" msgstr "CommaFeed item yang belum dibaca berikutnya"
@@ -203,10 +215,6 @@ msgstr "Konfirmasi kata sandi"
msgid "Cozy" msgid "Cozy"
msgstr "Nyaman" msgstr "Nyaman"
#: src/components/content/FeedEntryFooter.tsx
msgid "Create tag: {query}"
msgstr "Buat tag: {query}"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Ctrl" msgid "Ctrl"
msgstr "" msgstr ""
@@ -227,6 +235,10 @@ msgstr ""
msgid "Custom JS code that will be executed on page load" msgid "Custom JS code that will be executed on page load"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Dark"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
msgid "Date created" msgid "Date created"
msgstr "Tanggal dibuat" msgstr "Tanggal dibuat"
@@ -339,6 +351,14 @@ msgstr "URL Umpan"
msgid "Fetch all my feeds now" msgid "Fetch all my feeds now"
msgstr "" msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx #: src/components/content/add/ImportOpml.tsx
msgid "file is required" msgid "file is required"
msgstr "file diperlukan" msgstr "file diperlukan"
@@ -429,6 +449,10 @@ msgstr "Penyegaran terakhir"
msgid "Last refresh message" msgid "Last refresh message"
msgstr "Pesan penyegaran terakhir" msgstr "Pesan penyegaran terakhir"
#: src/components/header/ProfileMenu.tsx
msgid "Light"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
#: src/pages/app/TagDetailsPage.tsx #: src/pages/app/TagDetailsPage.tsx
@@ -582,6 +606,10 @@ msgstr ""
msgid "Open link in new tab" msgid "Open link in new tab"
msgstr "" msgstr ""
#: src/pages/app/Layout.tsx
msgid "Open menu"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry" msgid "Open next entry"
msgstr "Buka entri berikutnya" msgstr "Buka entri berikutnya"
@@ -665,6 +693,7 @@ msgstr "Pendaftaran ditutup pada instans CommaFeed ini"
msgid "REST API" msgid "REST API"
msgstr "" msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click" msgid "Right click"
msgstr "" msgstr ""
@@ -704,7 +733,7 @@ msgstr "Tetapkan fokus pada entri sebelumnya tanpa membukanya"
msgid "Settings" msgid "Settings"
msgstr "Pengaturan" msgstr "Pengaturan"
#: src/app/slices/user.ts #: src/app/user/slice.ts
msgid "Settings saved." msgid "Settings saved."
msgstr "Pengaturan disimpan." msgstr "Pengaturan disimpan."
@@ -716,11 +745,20 @@ msgstr "Bagikan"
msgid "Sharing sites" msgid "Sharing sites"
msgstr "Berbagi situs" msgstr "Berbagi situs"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift" msgid "Shift"
msgstr "Pergeseran" msgstr "Pergeseran"
#: src/components/settings/DisplaySettings.tsx
msgid "Show CommaFeed's own context menu on right click"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show confirmation when marking all entries as read"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)" msgid "Show entry menu (desktop)"
msgstr "" msgstr ""
@@ -737,6 +775,10 @@ msgstr "Tampilkan umpan dan kategori tanpa entri yang belum dibaca"
msgid "Show keyboard shortcut help" msgid "Show keyboard shortcut help"
msgstr "Tampilkan bantuan pintasan keyboard" msgstr "Tampilkan bantuan pintasan keyboard"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show native menu (desktop)"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
@@ -784,16 +826,19 @@ msgstr "Sukses"
msgid "Swipe header to the right" msgid "Swipe header to the right"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
msgid "Switch to dark theme" msgid "Switch to dark theme"
msgstr "Beralih ke tema gelap" msgstr "Beralih ke tema gelap"
#: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
msgid "Switch to light theme" msgid "Switch to light theme"
msgstr "Beralih ke tema terang" msgstr "Beralih ke tema terang"
#: src/components/header/ProfileMenu.tsx
msgid "System"
msgstr ""
#: src/components/content/FeedEntryFooter.tsx
#: src/components/content/FeedEntryFooter.tsx #: src/components/content/FeedEntryFooter.tsx
msgid "Tags" msgid "Tags"
msgstr "Tag" msgstr "Tag"
@@ -814,6 +859,10 @@ msgstr "Beralih status baca entri saat ini"
msgid "Toggle sidebar" msgid "Toggle sidebar"
msgstr "" msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Cobalah CommaFeed dengan akun demo: demo/demo" msgstr "Cobalah CommaFeed dengan akun demo: demo/demo"

View File

@@ -87,6 +87,10 @@ msgstr "Un file opml è un file XML contenente URL e categorie di feed. "
msgid "Analyze feed" msgid "Analyze feed"
msgstr "Analizza feed" msgstr "Analizza feed"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "API key" msgid "API key"
msgstr "Chiave API" msgstr "Chiave API"
@@ -171,10 +175,18 @@ msgstr "La modifica della password genererà una nuova chiave API"
msgid "Check that the feed is working" msgid "Check that the feed is working"
msgstr "Verifica che il feed funzioni" msgstr "Verifica che il feed funzioni"
#: src/pages/app/Layout.tsx
msgid "Close menu"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}." msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "" 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 ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed successivo elemento non letto" msgstr "CommaFeed successivo elemento non letto"
@@ -203,10 +215,6 @@ msgstr "Conferma password"
msgid "Cozy" msgid "Cozy"
msgstr "Accogliente" msgstr "Accogliente"
#: src/components/content/FeedEntryFooter.tsx
msgid "Create tag: {query}"
msgstr "Crea tag: {query}"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Ctrl" msgid "Ctrl"
msgstr "ctrl" msgstr "ctrl"
@@ -227,6 +235,10 @@ msgstr ""
msgid "Custom JS code that will be executed on page load" msgid "Custom JS code that will be executed on page load"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Dark"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
msgid "Date created" msgid "Date created"
msgstr "Data di creazione" msgstr "Data di creazione"
@@ -339,6 +351,14 @@ msgstr "URL feed"
msgid "Fetch all my feeds now" msgid "Fetch all my feeds now"
msgstr "" msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx #: src/components/content/add/ImportOpml.tsx
msgid "file is required" msgid "file is required"
msgstr "è richiesto il file" msgstr "è richiesto il file"
@@ -429,6 +449,10 @@ msgstr "Ultimo aggiornamento"
msgid "Last refresh message" msgid "Last refresh message"
msgstr "Ultimo messaggio di aggiornamento" msgstr "Ultimo messaggio di aggiornamento"
#: src/components/header/ProfileMenu.tsx
msgid "Light"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
#: src/pages/app/TagDetailsPage.tsx #: src/pages/app/TagDetailsPage.tsx
@@ -582,6 +606,10 @@ msgstr ""
msgid "Open link in new tab" msgid "Open link in new tab"
msgstr "" msgstr ""
#: src/pages/app/Layout.tsx
msgid "Open menu"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry" msgid "Open next entry"
msgstr "Apri voce successiva" msgstr "Apri voce successiva"
@@ -665,6 +693,7 @@ msgstr "Le registrazioni sono chiuse su questa istanza CommaFeed"
msgid "REST API" msgid "REST API"
msgstr "API REST" msgstr "API REST"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click" msgid "Right click"
msgstr "" msgstr ""
@@ -704,7 +733,7 @@ msgstr "Imposta il focus sulla voce precedente senza aprirla"
msgid "Settings" msgid "Settings"
msgstr "Impostazioni" msgstr "Impostazioni"
#: src/app/slices/user.ts #: src/app/user/slice.ts
msgid "Settings saved." msgid "Settings saved."
msgstr "Impostazioni salvate." msgstr "Impostazioni salvate."
@@ -716,11 +745,20 @@ msgstr "Condividi"
msgid "Sharing sites" msgid "Sharing sites"
msgstr "Condivisione di siti" msgstr "Condivisione di siti"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift" msgid "Shift"
msgstr "Cambio" msgstr "Cambio"
#: src/components/settings/DisplaySettings.tsx
msgid "Show CommaFeed's own context menu on right click"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show confirmation when marking all entries as read"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)" msgid "Show entry menu (desktop)"
msgstr "" msgstr ""
@@ -737,6 +775,10 @@ msgstr "Mostra feed e categorie senza voci non lette"
msgid "Show keyboard shortcut help" msgid "Show keyboard shortcut help"
msgstr "Mostra la guida alle scorciatoie da tastiera" msgstr "Mostra la guida alle scorciatoie da tastiera"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show native menu (desktop)"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
@@ -784,16 +826,19 @@ msgstr "Successo"
msgid "Swipe header to the right" msgid "Swipe header to the right"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
msgid "Switch to dark theme" msgid "Switch to dark theme"
msgstr "Passa al tema scuro" msgstr "Passa al tema scuro"
#: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
msgid "Switch to light theme" msgid "Switch to light theme"
msgstr "Passa al tema della luce" msgstr "Passa al tema della luce"
#: src/components/header/ProfileMenu.tsx
msgid "System"
msgstr ""
#: src/components/content/FeedEntryFooter.tsx
#: src/components/content/FeedEntryFooter.tsx #: src/components/content/FeedEntryFooter.tsx
msgid "Tags" msgid "Tags"
msgstr "Tag" msgstr "Tag"
@@ -814,6 +859,10 @@ msgstr "Commuta lo stato di lettura della voce corrente"
msgid "Toggle sidebar" msgid "Toggle sidebar"
msgstr "" msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Prova CommaFeed con il conto demo: demo/demo" msgstr "Prova CommaFeed con il conto demo: demo/demo"

View File

@@ -87,6 +87,10 @@ msgstr "opml ファイルは、フィードの URL とカテゴリを含む XML
msgid "Analyze feed" msgid "Analyze feed"
msgstr "フィードを分析する" msgstr "フィードを分析する"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "API key" msgid "API key"
msgstr "APIキー" msgstr "APIキー"
@@ -171,10 +175,18 @@ msgstr "パスワードを変更すると、新しい API キーが生成され
msgid "Check that the feed is working" msgid "Check that the feed is working"
msgstr "フィードが動作していることを確認してください" msgstr "フィードが動作していることを確認してください"
#: src/pages/app/Layout.tsx
msgid "Close menu"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}." msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "" 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 ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "次の未読アイテムをカンマフィード" msgstr "次の未読アイテムをカンマフィード"
@@ -203,10 +215,6 @@ msgstr "パスワード確認"
msgid "Cozy" msgid "Cozy"
msgstr "コージー" msgstr "コージー"
#: src/components/content/FeedEntryFooter.tsx
msgid "Create tag: {query}"
msgstr "タグを作成: {query}"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Ctrl" msgid "Ctrl"
msgstr "コントロール" msgstr "コントロール"
@@ -227,6 +235,10 @@ msgstr ""
msgid "Custom JS code that will be executed on page load" msgid "Custom JS code that will be executed on page load"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Dark"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
msgid "Date created" msgid "Date created"
msgstr "作成日" msgstr "作成日"
@@ -339,6 +351,14 @@ msgstr "フィード URL"
msgid "Fetch all my feeds now" msgid "Fetch all my feeds now"
msgstr "" msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx #: src/components/content/add/ImportOpml.tsx
msgid "file is required" msgid "file is required"
msgstr "ファイルが必要です" msgstr "ファイルが必要です"
@@ -429,6 +449,10 @@ msgstr "最終更新"
msgid "Last refresh message" msgid "Last refresh message"
msgstr "最終更新メッセージ" msgstr "最終更新メッセージ"
#: src/components/header/ProfileMenu.tsx
msgid "Light"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
#: src/pages/app/TagDetailsPage.tsx #: src/pages/app/TagDetailsPage.tsx
@@ -582,6 +606,10 @@ msgstr ""
msgid "Open link in new tab" msgid "Open link in new tab"
msgstr "" msgstr ""
#: src/pages/app/Layout.tsx
msgid "Open menu"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry" msgid "Open next entry"
msgstr "次のエントリを開く" msgstr "次のエントリを開く"
@@ -665,6 +693,7 @@ msgstr "この CommaFeed インスタンスの登録は終了しています"
msgid "REST API" msgid "REST API"
msgstr "" msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click" msgid "Right click"
msgstr "" msgstr ""
@@ -704,7 +733,7 @@ msgstr "前のエントリを開かずにフォーカスを設定する"
msgid "Settings" msgid "Settings"
msgstr "設定" msgstr "設定"
#: src/app/slices/user.ts #: src/app/user/slice.ts
msgid "Settings saved." msgid "Settings saved."
msgstr "設定が保存されました。" msgstr "設定が保存されました。"
@@ -716,11 +745,20 @@ msgstr "シェア"
msgid "Sharing sites" msgid "Sharing sites"
msgstr "共有サイト" msgstr "共有サイト"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Shift" msgid "Shift"
msgstr "シフト" msgstr "シフト"
#: src/components/settings/DisplaySettings.tsx
msgid "Show CommaFeed's own context menu on right click"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Show confirmation when marking all entries as read"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)" msgid "Show entry menu (desktop)"
msgstr "" msgstr ""
@@ -737,6 +775,10 @@ msgstr "未読エントリのないフィードとカテゴリを表示する"
msgid "Show keyboard shortcut help" msgid "Show keyboard shortcut help"
msgstr "キーボード ショートカットのヘルプを表示" msgstr "キーボード ショートカットのヘルプを表示"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show native menu (desktop)"
msgstr ""
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx #: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
@@ -784,16 +826,19 @@ msgstr "成功"
msgid "Swipe header to the right" msgid "Swipe header to the right"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
msgid "Switch to dark theme" msgid "Switch to dark theme"
msgstr "ダークテーマに切り替え" msgstr "ダークテーマに切り替え"
#: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
msgid "Switch to light theme" msgid "Switch to light theme"
msgstr "ライトテーマに切り替え" msgstr "ライトテーマに切り替え"
#: src/components/header/ProfileMenu.tsx
msgid "System"
msgstr ""
#: src/components/content/FeedEntryFooter.tsx
#: src/components/content/FeedEntryFooter.tsx #: src/components/content/FeedEntryFooter.tsx
msgid "Tags" msgid "Tags"
msgstr "タグ" msgstr "タグ"
@@ -814,6 +859,10 @@ msgstr "現在のエントリの読み取りステータスを切り替えます
msgid "Toggle sidebar" msgid "Toggle sidebar"
msgstr "" msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "デモアカウントで CommaFeed を試す: demo/demo" msgstr "デモアカウントで CommaFeed を試す: demo/demo"

Some files were not shown because too many files have changed in this diff Show More