forked from Archives/Athou_commafeed
Compare commits
115 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b51de8e5b | ||
|
|
0ba70d29bd | ||
|
|
197b3b258b | ||
|
|
850f66999c | ||
|
|
d7d3574e36 | ||
|
|
435d612cbf | ||
|
|
3d3a7c6496 | ||
|
|
fba57fe0a7 | ||
|
|
ce7933f320 | ||
|
|
8ac452afc9 | ||
|
|
a11cb3ac7a | ||
|
|
39808bbafc | ||
|
|
aee56e3dbe | ||
|
|
40f451c762 | ||
|
|
d633803ab5 | ||
|
|
d7a3b75687 | ||
|
|
df8c4056b6 | ||
|
|
06319c1eb0 | ||
|
|
b7ede8eba2 | ||
|
|
1a4517d6a3 | ||
|
|
a402c5d7d8 | ||
|
|
408809787e | ||
|
|
d7b0d572c1 | ||
|
|
b356be3e6f | ||
|
|
998385334b | ||
|
|
c6d613d81a | ||
|
|
9981d8763d | ||
|
|
b37680333c | ||
|
|
66d1eb3f1f | ||
|
|
6fe1c2a3c0 | ||
|
|
c2e453027c | ||
|
|
f16bac9b59 | ||
|
|
8cca826e70 | ||
|
|
b0165bb26a | ||
|
|
366294ab46 | ||
|
|
2988938440 | ||
|
|
e865769e30 | ||
|
|
f87be2fc03 | ||
|
|
466846d268 | ||
|
|
61b6be4090 | ||
|
|
cb779ec494 | ||
|
|
da6f2050f9 | ||
|
|
4304f84a55 | ||
|
|
8a175d8221 | ||
|
|
f1896d34e2 | ||
|
|
45d0e0ec98 | ||
|
|
38c5beec2f | ||
|
|
c4715dc3f7 | ||
|
|
6ce6b5ef0e | ||
|
|
1af3dd452c | ||
|
|
1f4ec41222 | ||
|
|
512c4cc507 | ||
|
|
d391c8f1c9 | ||
|
|
46d3e67aec | ||
|
|
d9505c4d87 | ||
|
|
42491f5778 | ||
|
|
9c897c9fb2 | ||
|
|
21b500a96e | ||
|
|
04c74b5daa | ||
|
|
3edb8a3ee2 | ||
|
|
922346bef6 | ||
|
|
82cf0e154a | ||
|
|
efe32e86c9 | ||
|
|
e208d4ae1e | ||
|
|
adf20327bd | ||
|
|
781c41b452 | ||
|
|
2b597f9b43 | ||
|
|
2e26f34135 | ||
|
|
9e59a472da | ||
|
|
970043467c | ||
|
|
3e903fc6bc | ||
|
|
95f4cffa7c | ||
|
|
6ebe0fa827 | ||
|
|
488a88fe95 | ||
|
|
d5898a0173 | ||
|
|
bdcfbc22bf | ||
|
|
53b06f41f3 | ||
|
|
872247d80f | ||
|
|
7c226f41db | ||
|
|
bb55a91a14 | ||
|
|
f140650b4e | ||
|
|
8a64a9db31 | ||
|
|
c1520652f2 | ||
|
|
90e3044249 | ||
|
|
f7786d9962 | ||
|
|
aeaaeaee0e | ||
|
|
4d0a8fd133 | ||
|
|
b1938c234c | ||
|
|
6a5052787d | ||
|
|
877fc33180 | ||
|
|
8b0b9b1a66 | ||
|
|
689c329430 | ||
|
|
52f911f303 | ||
|
|
91d0988177 | ||
|
|
4f644ba9f5 | ||
|
|
4f699d9675 | ||
|
|
a5aba6f7ae | ||
|
|
78c8711a79 | ||
|
|
8325236d0e | ||
|
|
437401e73f | ||
|
|
fa06d321d5 | ||
|
|
d1ddcb6ace | ||
|
|
6944d4dc0b | ||
|
|
c835d805b1 | ||
|
|
4a90e1f69d | ||
|
|
fcfeaa462e | ||
|
|
b16978d8fe | ||
|
|
68c62b4528 | ||
|
|
18f68aab31 | ||
|
|
8abb2770ec | ||
|
|
9156b8b6d0 | ||
|
|
2c32fa1e13 | ||
|
|
7e48afe36c | ||
|
|
cd94a3b56f | ||
|
|
22e0f1f382 |
1
.github/stale.yml
vendored
1
.github/stale.yml
vendored
@@ -7,6 +7,7 @@ exemptLabels:
|
||||
- pinned
|
||||
- security
|
||||
- enhancement
|
||||
- bug
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: wontfix
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
|
||||
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -7,7 +7,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
java: [ "8", "11", "17" ]
|
||||
java: [ "8", "11", "17", "21" ]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
66
CHANGELOG.md
66
CHANGELOG.md
@@ -1,5 +1,63 @@
|
||||
# Changelog
|
||||
|
||||
## [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]
|
||||
|
||||
- in expanded mode, don't scroll when clicking on the body of the current entry
|
||||
- improve content cleanup task performance for instances with a very large number of feeds
|
||||
|
||||
## [3.8.0]
|
||||
|
||||
- add previous and next buttons in the toolbar
|
||||
- add a setting to always scroll selected entry to the top of the page, even if it fits entirely on screen
|
||||
- clicking on the body of an entry in expanded mode selects it and marks it as read
|
||||
- add rich text editor with autocomplete for custom css and js code in settings (desktop only)
|
||||
- dramatically improve performance while scrolling
|
||||
- fix broken welcome page mobile layout
|
||||
- format dates in user locale instead of GMT in relative date popups
|
||||
|
||||
## [3.7.0]
|
||||
|
||||
- the sidebar is now resizable
|
||||
- added the "f" keyboard shortcut to hide the sidebar
|
||||
- added tooltips to relative dates with the exact date
|
||||
- add a setting to hide commafeed from search engines (exposes a robots.txt file, enabled by default)
|
||||
- the browser extension unread count now updates when articles are marked as read/unread in the app
|
||||
- The "b" keyboard shortcut now works as expected on Chrome but requires the browser extension to be installed
|
||||
- dark mode has been disabled on the api documentation page as it was unreadable
|
||||
- improvement to the feed refresh queuing logic when "heavy load" mode is enabled
|
||||
- fix a bug that could prevent feeds and categories from being edited
|
||||
|
||||
## [3.6.0]
|
||||
|
||||
- add a button to open CommaFeed in a new tab and a button to open options when using the browser extension
|
||||
@@ -26,7 +84,8 @@
|
||||
- add divider to visually separate read-only information from form on the profile settings page
|
||||
- reduce javascript bundle size by 30% by loading only the necessary translations
|
||||
- add a standalone donate page with all ways to support CommaFeed
|
||||
- fix an issue introduced in 3.1.0 that could make CommaFeed not refresh feeds as fast as before on instances with lots of feeds
|
||||
- fix an issue introduced in 3.1.0 that could make CommaFeed not refresh feeds as fast as before on instances with lots
|
||||
of feeds
|
||||
- fix alignment of icon with text for category tree nodes
|
||||
- fix alignment of burger button with the rest of the header on mobile
|
||||
|
||||
@@ -67,10 +126,9 @@
|
||||
## [3.0.1]
|
||||
|
||||
- allow env variable substitution in config.yml
|
||||
- e.g. having a custom config.yml file with `app.session.path=${SOME_ENV_VAR}` will substitute `SOME_ENV_VAR` with
|
||||
its value
|
||||
- e.g. having a custom config.yml file with `app.session.path=${SOME_ENV_VAR}` will substitute `SOME_ENV_VAR` with its value
|
||||
- allow env variable prefixed with `CF_` to override config.yml properties
|
||||
- e.g. setting `CF_APP_ALLOWREGISTRATIONS=true` will set `app.allowRegistrations` to `true`
|
||||
- e.g. setting `CF_APP_ALLOWREGISTRATIONS=true` will set `app.allowRegistrations` to `true`
|
||||
|
||||
## [3.0.0]
|
||||
|
||||
|
||||
@@ -4,10 +4,9 @@ EXPOSE 8082
|
||||
|
||||
RUN mkdir -p /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/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"]
|
||||
|
||||
16
README.md
16
README.md
@@ -7,22 +7,32 @@ Google Reader inspired self-hosted RSS reader, based on Dropwizard and React/Typ
|
||||
## Features
|
||||
|
||||
- 4 different layouts
|
||||
- Dark theme
|
||||
- Light/Dark theme
|
||||
- Fully responsive
|
||||
- Keyboard shortcuts for almost everything
|
||||
- Support for right-to-left feeds
|
||||
- Translated in 25+ languages
|
||||
- Supports thousands of users and millions of feeds
|
||||
- OPML import/export
|
||||
- REST API
|
||||
- REST API and a Fever-compatible API for native mobile apps
|
||||
- [Browser extension](https://github.com/Athou/commafeed-browser-extension)
|
||||
|
||||
## Deployment on your own server
|
||||
## Deployment
|
||||
|
||||
### 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
|
||||
|
||||
### 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.
|
||||
|
||||
[](https://www.pikapods.com/pods?run=commafeed)
|
||||
|
||||
### Download precompiled package
|
||||
|
||||
mkdir commafeed && cd commafeed
|
||||
|
||||
@@ -4,13 +4,11 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||
<link rel="manifest" href="manifest.json" />
|
||||
<link rel="stylesheet" href="custom_css.css" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<title>CommaFeed</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
<script src="custom_js.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
78
commafeed-client/package-lock.json
generated
78
commafeed-client/package-lock.json
generated
@@ -20,11 +20,15 @@
|
||||
"@mantine/notifications": "^6.0.11",
|
||||
"@mantine/spotlight": "^6.0.11",
|
||||
"@mantine/styles": "^6.0.11",
|
||||
"@monaco-editor/react": "^4.5.1",
|
||||
"@reduxjs/toolkit": "^1.9.5",
|
||||
"axios": "^1.4.0",
|
||||
"dayjs": "^1.11.7",
|
||||
"escape-string-regexp": "^5.0.0",
|
||||
"interweave": "^13.1.0",
|
||||
"monaco-editor": "^0.38.0",
|
||||
"mousetrap": "^1.6.5",
|
||||
"re-resizable": "^6.9.9",
|
||||
"react": "^18.2.0",
|
||||
"react-async-hook": "^4.0.0",
|
||||
"react-contexify": "^6.0.0",
|
||||
@@ -2054,6 +2058,17 @@
|
||||
"stylis": "4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emotion/babel-plugin/node_modules/escape-string-regexp": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@emotion/cache": {
|
||||
"version": "11.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz",
|
||||
@@ -3048,6 +3063,30 @@
|
||||
"moo": "^0.5.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@monaco-editor/loader": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.3.3.tgz",
|
||||
"integrity": "sha512-6KKF4CTzcJiS8BJwtxtfyYt9shBiEv32ateQ9T4UVogwn4HM/uPo9iJd2Dmbkpz8CM6Y0PDUpjnZzCwC+eYo2Q==",
|
||||
"dependencies": {
|
||||
"state-local": "^1.0.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"monaco-editor": ">= 0.21.0 < 1"
|
||||
}
|
||||
},
|
||||
"node_modules/@monaco-editor/react": {
|
||||
"version": "4.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.5.1.tgz",
|
||||
"integrity": "sha512-NNDFdP+2HojtNhCkRfE6/D6ro6pBNihaOzMbGK84lNWzRu+CfBjwzGt4jmnqimLuqp5yE5viHS2vi+QOAnD5FQ==",
|
||||
"dependencies": {
|
||||
"@monaco-editor/loader": "^1.3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"monaco-editor": ">= 0.25.0 < 1",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nicolo-ribaudo/eslint-scope-5-internals": {
|
||||
"version": "5.1.1-v1",
|
||||
"resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz",
|
||||
@@ -6666,11 +6705,11 @@
|
||||
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
|
||||
},
|
||||
"node_modules/escape-string-regexp": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
|
||||
"integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
@@ -7148,6 +7187,18 @@
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/escape-string-regexp": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/eslint-scope": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz",
|
||||
@@ -8998,6 +9049,11 @@
|
||||
"ufo": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/monaco-editor": {
|
||||
"version": "0.38.0",
|
||||
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.38.0.tgz",
|
||||
"integrity": "sha512-11Fkh6yzEmwx7O0YoLxeae0qEGFwmyPRlVxpg7oF9czOOCB/iCjdJrG5I67da5WiXK3YJCxoz9TJFE8Tfq/v9A=="
|
||||
},
|
||||
"node_modules/moo": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz",
|
||||
@@ -10035,6 +10091,15 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/re-resizable": {
|
||||
"version": "6.9.9",
|
||||
"resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-6.9.9.tgz",
|
||||
"integrity": "sha512-l+MBlKZffv/SicxDySKEEh42hR6m5bAHfNu3Tvxks2c4Ah+ldnWjfnVRwxo/nxF27SsUsxDS0raAzFuJNKABXA==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.13.1 || ^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "18.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
|
||||
@@ -11000,6 +11065,11 @@
|
||||
"resolved": "https://registry.npmjs.org/stampit/-/stampit-4.3.2.tgz",
|
||||
"integrity": "sha512-pE2org1+ZWQBnIxRPrBM2gVupkuDD0TTNIo1H6GdT/vO82NXli2z8lRE8cu/nBIHrcOCXFBAHpb9ZldrB2/qOA=="
|
||||
},
|
||||
"node_modules/state-local": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz",
|
||||
"integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w=="
|
||||
},
|
||||
"node_modules/std-env": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.3.3.tgz",
|
||||
|
||||
@@ -26,11 +26,15 @@
|
||||
"@mantine/notifications": "^6.0.11",
|
||||
"@mantine/spotlight": "^6.0.11",
|
||||
"@mantine/styles": "^6.0.11",
|
||||
"@monaco-editor/react": "^4.5.1",
|
||||
"@reduxjs/toolkit": "^1.9.5",
|
||||
"axios": "^1.4.0",
|
||||
"dayjs": "^1.11.7",
|
||||
"escape-string-regexp": "^5.0.0",
|
||||
"interweave": "^13.1.0",
|
||||
"monaco-editor": "^0.38.0",
|
||||
"mousetrap": "^1.6.5",
|
||||
"re-resizable": "^6.9.9",
|
||||
"react": "^18.2.0",
|
||||
"react-async-hook": "^4.0.0",
|
||||
"react-contexify": "^6.0.0",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<groupId>com.commafeed</groupId>
|
||||
<artifactId>commafeed</artifactId>
|
||||
<version>3.6.0</version>
|
||||
<version>3.10.1</version>
|
||||
</parent>
|
||||
<artifactId>commafeed-client</artifactId>
|
||||
<name>CommaFeed Client</name>
|
||||
|
||||
@@ -12,6 +12,7 @@ import { categoryUnreadCount } from "app/utils"
|
||||
import { ErrorBoundary } from "components/ErrorBoundary"
|
||||
import { Header } from "components/header/Header"
|
||||
import { Tree } from "components/sidebar/Tree"
|
||||
import { useBrowserExtension } from "hooks/useBrowserExtension"
|
||||
import { useI18n } from "i18n"
|
||||
import { AdminUsersPage } from "pages/admin/AdminUsersPage"
|
||||
import { MetricsPage } from "pages/admin/MetricsPage"
|
||||
@@ -37,7 +38,7 @@ import useLocalStorage from "use-local-storage"
|
||||
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"))
|
||||
const toggleColorScheme = (value?: ColorScheme) => setColorScheme(value ?? (colorScheme === "dark" ? "light" : "dark"))
|
||||
|
||||
return (
|
||||
<I18nProvider i18n={i18n}>
|
||||
@@ -65,6 +66,9 @@ function Providers(props: { children: React.ReactNode }) {
|
||||
const ApiDocumentationPage = React.lazy(() => import("pages/app/ApiDocumentationPage"))
|
||||
|
||||
function AppRoutes() {
|
||||
const sidebarWidth = useAppSelector(state => state.tree.sidebarWidth)
|
||||
const sidebarVisible = useAppSelector(state => state.tree.sidebarVisible)
|
||||
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/" element={<Navigate to={`/app/category/${Constants.categories.all.id}`} replace />} />
|
||||
@@ -73,7 +77,7 @@ function AppRoutes() {
|
||||
<Route path="register" element={<RegistrationPage />} />
|
||||
<Route path="passwordRecovery" element={<PasswordRecoveryPage />} />
|
||||
<Route path="api" element={<ApiDocumentationPage />} />
|
||||
<Route path="app" element={<Layout header={<Header />} sidebar={<Tree />} />}>
|
||||
<Route path="app" element={<Layout header={<Header />} sidebar={<Tree />} sidebarWidth={sidebarVisible ? sidebarWidth : 0} />}>
|
||||
<Route path="category">
|
||||
<Route path=":id" element={<FeedEntriesPage sourceType="category" />} />
|
||||
<Route path=":id/details" element={<CategoryDetailsPage />} />
|
||||
@@ -134,13 +138,28 @@ function FaviconHandler() {
|
||||
const root = useAppSelector(state => state.tree.rootCategory)
|
||||
useEffect(() => {
|
||||
const unreadCount = categoryUnreadCount(root)
|
||||
if (unreadCount === 0) Tinycon.reset()
|
||||
else Tinycon.setBubble(unreadCount)
|
||||
if (unreadCount === 0) {
|
||||
Tinycon.reset()
|
||||
} else {
|
||||
Tinycon.setBubble(unreadCount)
|
||||
}
|
||||
}, [root])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function BrowserExtensionBadgeUnreadCountHandler() {
|
||||
const root = useAppSelector(state => state.tree.rootCategory)
|
||||
const { setBadgeUnreadCount } = useBrowserExtension()
|
||||
useEffect(() => {
|
||||
if (!root) return
|
||||
const unreadCount = categoryUnreadCount(root)
|
||||
setBadgeUnreadCount(unreadCount)
|
||||
}, [root, setBadgeUnreadCount])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function App() {
|
||||
useI18n()
|
||||
const dispatch = useAppDispatch()
|
||||
@@ -153,6 +172,7 @@ export function App() {
|
||||
<Providers>
|
||||
<>
|
||||
<FaviconHandler />
|
||||
<BrowserExtensionBadgeUnreadCountHandler />
|
||||
<HashRouter>
|
||||
<GoogleAnalyticsHandler />
|
||||
<RedirectHandler />
|
||||
|
||||
@@ -88,14 +88,14 @@ export const Constants = {
|
||||
layout: {
|
||||
mobileBreakpoint: DEFAULT_THEME.breakpoints.md,
|
||||
headerHeight: 60,
|
||||
sidebarWidth: 350,
|
||||
entryMaxWidth: 650,
|
||||
isTopVisible: (div: HTMLElement) => div.getBoundingClientRect().top >= Constants.layout.headerHeight,
|
||||
isBottomVisible: (div: HTMLElement) => div.getBoundingClientRect().bottom <= window.innerHeight,
|
||||
},
|
||||
dom: {
|
||||
mainScrollAreaId: "main-scroll-area-id",
|
||||
entryId: (entry: Entry) => `entry-id-${entry.id}`,
|
||||
entryContextMenuId: (entry: Entry) => entry.id,
|
||||
},
|
||||
browserExtensionUrl: "https://github.com/Athou/commafeed-browser-extension",
|
||||
bitcoinWalletAddress: "1dymfUxqCWpyD7a6rQSqNy4rLVDBsAr5e",
|
||||
}
|
||||
|
||||
@@ -78,6 +78,7 @@ describe("entries", () => {
|
||||
sourceWebsiteUrl: "",
|
||||
entries: [{ id: "3" } as Entry],
|
||||
hasMore: true,
|
||||
loading: false,
|
||||
scrollingToEntry: false,
|
||||
},
|
||||
},
|
||||
@@ -102,6 +103,7 @@ describe("entries", () => {
|
||||
sourceWebsiteUrl: "",
|
||||
entries: [{ id: "3", read: false } as Entry, { id: "4", read: false } as Entry],
|
||||
hasMore: true,
|
||||
loading: false,
|
||||
scrollingToEntry: false,
|
||||
},
|
||||
},
|
||||
@@ -128,6 +130,7 @@ describe("entries", () => {
|
||||
sourceWebsiteUrl: "",
|
||||
entries: [{ id: "3", read: false } as Entry, { id: "4", read: false } as Entry],
|
||||
hasMore: true,
|
||||
loading: false,
|
||||
scrollingToEntry: false,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -27,6 +27,7 @@ interface EntriesState {
|
||||
timestamp?: number
|
||||
selectedEntryId?: string
|
||||
hasMore: boolean
|
||||
loading: boolean
|
||||
search?: string
|
||||
scrollingToEntry: boolean
|
||||
}
|
||||
@@ -40,23 +41,33 @@ const initialState: EntriesState = {
|
||||
sourceWebsiteUrl: "",
|
||||
entries: [],
|
||||
hasMore: true,
|
||||
loading: false,
|
||||
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 loadEntries = createAsyncThunk<
|
||||
Entries,
|
||||
{ source: EntrySource; clearSearch: boolean },
|
||||
{
|
||||
state: RootState
|
||||
}
|
||||
)
|
||||
export const loadMoreEntries = createAsyncThunk<Entries, void, { state: RootState }>("entries/loadMore", async (_, thunkApi) => {
|
||||
>("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 =
|
||||
@@ -74,7 +85,13 @@ const buildGetEntriesPaginatedRequest = (state: RootState, source: EntrySource,
|
||||
tag: source.type === "tag" ? source.id : undefined,
|
||||
keywords: state.entries.search,
|
||||
})
|
||||
export const reloadEntries = createAsyncThunk<void, void, { state: RootState }>("entries/reload", async (arg, thunkApi) => {
|
||||
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 }))
|
||||
})
|
||||
@@ -123,15 +140,18 @@ export const markEntriesUpToEntry = createAsyncThunk<void, Entry, { state: RootS
|
||||
)
|
||||
}
|
||||
)
|
||||
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 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,
|
||||
@@ -175,31 +195,25 @@ export const selectEntry = createAsyncThunk<
|
||||
if (arg.scrollToEntry) {
|
||||
const entryElement = document.getElementById(Constants.dom.entryId(entry))
|
||||
if (entryElement) {
|
||||
const scrollSpeed = state.user.settings?.scrollSpeed
|
||||
thunkApi.dispatch(entriesSlice.actions.setScrollingToEntry(true))
|
||||
scrollToEntry(entryElement, scrollSpeed, () => thunkApi.dispatch(entriesSlice.actions.setScrollingToEntry(false)))
|
||||
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) => {
|
||||
// the entry is entirely visible, no need to scroll
|
||||
if (Constants.layout.isTopVisible(entryElement) && Constants.layout.isBottomVisible(entryElement)) {
|
||||
onScrollEnded()
|
||||
return
|
||||
}
|
||||
|
||||
const scrollArea = document.getElementById(Constants.dom.mainScrollAreaId)
|
||||
if (scrollArea) {
|
||||
scrollToWithCallback({
|
||||
element: scrollArea,
|
||||
options: {
|
||||
// add a small gap between the top of the content and the top of the page
|
||||
top: entryElement.offsetTop - 3,
|
||||
behavior: scrollSpeed && scrollSpeed > 0 ? "smooth" : "auto",
|
||||
},
|
||||
onScrollEnded,
|
||||
})
|
||||
}
|
||||
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<
|
||||
@@ -248,7 +262,13 @@ export const selectNextEntry = createAsyncThunk<
|
||||
)
|
||||
}
|
||||
})
|
||||
export const tagEntry = createAsyncThunk<void, TagRequest, { state: RootState }>("entries/entry/tag", async (arg, thunkApi) => {
|
||||
export const tagEntry = createAsyncThunk<
|
||||
void,
|
||||
TagRequest,
|
||||
{
|
||||
state: RootState
|
||||
}
|
||||
>("entries/entry/tag", async (arg, thunkApi) => {
|
||||
await client.entry.tag(arg)
|
||||
thunkApi.dispatch(reloadTags())
|
||||
})
|
||||
@@ -311,6 +331,10 @@ export const entriesSlice = createSlice({
|
||||
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
|
||||
@@ -318,12 +342,14 @@ export const entriesSlice = createSlice({
|
||||
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
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"
|
||||
import { PayloadAction, createAsyncThunk, createSlice } from "@reduxjs/toolkit"
|
||||
import { client } from "app/client"
|
||||
import { ServerInfo } from "app/types"
|
||||
|
||||
interface ServerState {
|
||||
serverInfos?: ServerInfo
|
||||
webSocketConnected: boolean
|
||||
}
|
||||
|
||||
const initialState: ServerState = {}
|
||||
const initialState: ServerState = {
|
||||
webSocketConnected: false,
|
||||
}
|
||||
|
||||
export const reloadServerInfos = createAsyncThunk("server/infos", () => client.server.getServerInfos().then(r => r.data))
|
||||
export const serverSlice = createSlice({
|
||||
name: "server",
|
||||
initialState,
|
||||
reducers: {},
|
||||
reducers: {
|
||||
setWebSocketConnected: (state, action: PayloadAction<boolean>) => {
|
||||
state.webSocketConnected = action.payload
|
||||
},
|
||||
},
|
||||
extraReducers: builder => {
|
||||
builder.addCase(reloadServerInfos.fulfilled, (state, action) => {
|
||||
state.serverInfos = action.payload
|
||||
@@ -20,4 +27,5 @@ export const serverSlice = createSlice({
|
||||
},
|
||||
})
|
||||
|
||||
export const { setWebSocketConnected } = serverSlice.actions
|
||||
export default serverSlice.reducer
|
||||
|
||||
@@ -9,10 +9,14 @@ import { redirectTo } from "./redirect"
|
||||
interface TreeState {
|
||||
rootCategory?: Category
|
||||
mobileMenuOpen: boolean
|
||||
sidebarWidth: number
|
||||
sidebarVisible: boolean
|
||||
}
|
||||
|
||||
const initialState: TreeState = {
|
||||
mobileMenuOpen: false,
|
||||
sidebarWidth: 350,
|
||||
sidebarVisible: true,
|
||||
}
|
||||
|
||||
export const reloadTree = createAsyncThunk("tree/reload", () => client.category.getRoot().then(r => r.data))
|
||||
@@ -27,6 +31,12 @@ export const treeSlice = createSlice({
|
||||
setMobileMenuOpen: (state, action: PayloadAction<boolean>) => {
|
||||
state.mobileMenuOpen = action.payload
|
||||
},
|
||||
setSidebarWidth: (state, action: PayloadAction<number>) => {
|
||||
state.sidebarWidth = action.payload
|
||||
},
|
||||
toggleSidebar: state => {
|
||||
state.sidebarVisible = !state.sidebarVisible
|
||||
},
|
||||
},
|
||||
extraReducers: builder => {
|
||||
builder.addCase(reloadTree.fulfilled, (state, action) => {
|
||||
@@ -54,5 +64,5 @@ export const treeSlice = createSlice({
|
||||
},
|
||||
})
|
||||
|
||||
export const { setMobileMenuOpen } = treeSlice.actions
|
||||
export const { setMobileMenuOpen, setSidebarWidth, toggleSidebar } = treeSlice.actions
|
||||
export default treeSlice.reducer
|
||||
|
||||
@@ -80,6 +80,39 @@ export const changeScrollMarks = createAsyncThunk<
|
||||
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 changeMarkAllAsReadConfirmation = createAsyncThunk<
|
||||
void,
|
||||
boolean,
|
||||
{
|
||||
state: RootState
|
||||
}
|
||||
>("settings/markAllAsReadConfirmation", (markAllAsReadConfirmation, thunkApi) => {
|
||||
const { settings } = thunkApi.getState().user
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, markAllAsReadConfirmation })
|
||||
})
|
||||
export const changeCustomContextMenu = createAsyncThunk<
|
||||
void,
|
||||
boolean,
|
||||
{
|
||||
state: RootState
|
||||
}
|
||||
>("settings/customContextMenu", (customContextMenu, thunkApi) => {
|
||||
const { settings } = thunkApi.getState().user
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, customContextMenu })
|
||||
})
|
||||
export const changeSharingSetting = createAsyncThunk<
|
||||
void,
|
||||
{ site: keyof SharingSettings; value: boolean },
|
||||
@@ -136,6 +169,18 @@ export const userSlice = createSlice({
|
||||
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
|
||||
@@ -146,6 +191,9 @@ export const userSlice = createSlice({
|
||||
changeScrollSpeed.fulfilled,
|
||||
changeShowRead.fulfilled,
|
||||
changeScrollMarks.fulfilled,
|
||||
changeAlwaysScrollToEntry.fulfilled,
|
||||
changeMarkAllAsReadConfirmation.fulfilled,
|
||||
changeCustomContextMenu.fulfilled,
|
||||
changeSharingSetting.fulfilled
|
||||
),
|
||||
() => {
|
||||
|
||||
@@ -3,38 +3,6 @@ export interface AddCategoryRequest {
|
||||
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 {
|
||||
id: string
|
||||
parentId?: string
|
||||
@@ -233,6 +201,9 @@ export interface Settings {
|
||||
customCss?: string
|
||||
customJs?: string
|
||||
scrollSpeed: number
|
||||
alwaysScrollToEntry: boolean
|
||||
markAllAsReadConfirmation: boolean
|
||||
customContextMenu: boolean
|
||||
sharingSettings: SharingSettings
|
||||
}
|
||||
|
||||
@@ -271,7 +242,7 @@ export interface Subscription {
|
||||
iconUrl: string
|
||||
unread: number
|
||||
categoryId?: string
|
||||
position?: number
|
||||
position: number
|
||||
newestItemTime?: number
|
||||
filter?: string
|
||||
}
|
||||
@@ -299,8 +270,6 @@ export interface UserModel {
|
||||
admin: boolean
|
||||
}
|
||||
|
||||
export type ApplicationSettingsCache = "NOOP" | "REDIS"
|
||||
|
||||
export type ReadingMode = "all" | "unread"
|
||||
|
||||
export type ReadingOrder = "asc" | "desc"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { throttle } from "throttle-debounce"
|
||||
import { Category } from "./types"
|
||||
|
||||
export function visitCategoryTree(category: Category, visitor: (category: Category) => void): void {
|
||||
@@ -26,43 +27,21 @@ export const calculatePlaceholderSize = ({ width, height, maxWidth }: { width?:
|
||||
return { width: placeholderWidth, height: placeholderHeight }
|
||||
}
|
||||
|
||||
export const scrollToWithCallback = ({
|
||||
element,
|
||||
options,
|
||||
onScrollEnded,
|
||||
}: {
|
||||
element: HTMLElement
|
||||
options: ScrollToOptions
|
||||
onScrollEnded: () => void
|
||||
}) => {
|
||||
export const scrollToWithCallback = ({ options, onScrollEnded }: { options: ScrollToOptions; onScrollEnded: () => void }) => {
|
||||
const offset = (options.top ?? 0).toFixed()
|
||||
|
||||
const onScroll = () => {
|
||||
if (element.offsetTop.toFixed() === offset) {
|
||||
element.removeEventListener("scroll", onScroll)
|
||||
const onScroll = throttle(100, () => {
|
||||
if (window.scrollY.toFixed() === offset) {
|
||||
window.removeEventListener("scroll", onScroll)
|
||||
onScrollEnded()
|
||||
}
|
||||
}
|
||||
|
||||
element.addEventListener("scroll", onScroll)
|
||||
})
|
||||
window.addEventListener("scroll", onScroll)
|
||||
|
||||
// scrollTo does not trigger if there's nothing to do, trigger it manually
|
||||
onScroll()
|
||||
|
||||
element.scrollTo(options)
|
||||
}
|
||||
|
||||
export const openLinkInBackgroundTab = (url: string) => {
|
||||
// simulate ctrl+click to open tab in background
|
||||
const a = document.createElement("a")
|
||||
a.href = url
|
||||
a.rel = "noreferrer"
|
||||
a.dispatchEvent(
|
||||
new MouseEvent("click", {
|
||||
ctrlKey: true,
|
||||
metaKey: true,
|
||||
})
|
||||
)
|
||||
window.scrollTo(options)
|
||||
}
|
||||
|
||||
export const truncate = (str: string, n: number) => (str.length > n ? `${str.slice(0, n - 1)}\u2026` : str)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ActionIcon, Button, Tooltip, useMantineTheme } from "@mantine/core"
|
||||
import { ActionIconProps } from "@mantine/core/lib/ActionIcon/ActionIcon"
|
||||
import { ButtonProps } from "@mantine/core/lib/Button/Button"
|
||||
import { useMediaQuery } from "@mantine/hooks"
|
||||
import { useActionButton } from "hooks/useActionButton"
|
||||
import { forwardRef, MouseEventHandler, ReactNode } from "react"
|
||||
|
||||
interface ActionButtonProps {
|
||||
@@ -18,9 +18,9 @@ interface ActionButtonProps {
|
||||
* Switches between Button with label (desktop) and ActionIcon (mobile)
|
||||
*/
|
||||
export const ActionButton = forwardRef<HTMLButtonElement, ActionButtonProps>((props: ActionButtonProps, ref) => {
|
||||
const { mobile } = useActionButton()
|
||||
const theme = useMantineTheme()
|
||||
const variant = props.variant ?? "subtle"
|
||||
const mobile = !useMediaQuery(`(min-width: ${theme.breakpoints.lg})`)
|
||||
const iconOnly = (mobile && !props.showLabelOnMobile) || (!mobile && props.hideLabelOnDesktop)
|
||||
return iconOnly ? (
|
||||
<Tooltip label={props.label} openDelay={500}>
|
||||
36
commafeed-client/src/components/AnnouncementDialog.tsx
Normal file
36
commafeed-client/src/components/AnnouncementDialog.tsx
Normal 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 weight="bold">
|
||||
<Trans>Announcement</Trans>
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Content content={announcement} />
|
||||
</Box>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { Group } from "@mantine/core"
|
||||
|
||||
export function ButtonToolbar(props: { children: React.ReactNode }) {
|
||||
return <Group spacing={14}>{props.children}</Group>
|
||||
}
|
||||
@@ -1,183 +1,222 @@
|
||||
import { Trans } from "@lingui/macro"
|
||||
import { Kbd, Table } from "@mantine/core"
|
||||
import { Anchor, Box, Kbd, Stack, Table } from "@mantine/core"
|
||||
import { Constants } from "app/constants"
|
||||
|
||||
export function KeyboardShortcutsHelp() {
|
||||
return (
|
||||
<Table striped highlightOnHover>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Refresh</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>R</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Open next entry</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>J</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Open previous entry</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>K</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Set focus on next entry without opening it</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>N</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Set focus on previous entry without opening it</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>P</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Move the page down</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>
|
||||
<Trans>Space</Trans>
|
||||
</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Move the page up</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>
|
||||
<Trans>Shift</Trans>
|
||||
</Kbd>
|
||||
<span> + </span>
|
||||
<Kbd>
|
||||
<Trans>Space</Trans>
|
||||
</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Open/close current entry</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>O</Kbd>
|
||||
<span>, </span>
|
||||
<Kbd>
|
||||
<Trans>Enter</Trans>
|
||||
</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Open current entry in a new tab</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>V</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Open current entry in a new tab in the background</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>B</Kbd>
|
||||
<span>, </span>
|
||||
<Kbd>
|
||||
<Trans>Middle click</Trans>
|
||||
</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Toggle read status of current entry</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>M</Kbd>
|
||||
<span>, </span>
|
||||
<Trans>Swipe header to the right</Trans>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Mark all entries as read</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>
|
||||
<Trans>Shift</Trans>
|
||||
</Kbd>
|
||||
<span> + </span>
|
||||
<Kbd>A</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Go to the All view</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>G</Kbd>
|
||||
<span> </span>
|
||||
<Kbd>A</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Navigate to a subscription by entering its name</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>
|
||||
<Trans>Ctrl</Trans>
|
||||
</Kbd>
|
||||
<span> + </span>
|
||||
<Kbd>K</Kbd>
|
||||
<span>, </span>
|
||||
<Kbd>G</Kbd>
|
||||
<span> </span>
|
||||
<Kbd>U</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Show entry menu (desktop)</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>
|
||||
<Trans>Right click</Trans>
|
||||
</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Show entry menu (mobile)</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>
|
||||
<Trans>Long press</Trans>
|
||||
</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Show keyboard shortcut help</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>?</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</Table>
|
||||
<Stack spacing="xs">
|
||||
<Table striped highlightOnHover>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Refresh</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>R</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Open next entry</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>J</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Open previous entry</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>K</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Set focus on next entry without opening it</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>N</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Set focus on previous entry without opening it</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>P</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Move the page down</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>
|
||||
<Trans>Space</Trans>
|
||||
</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Move the page up</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>
|
||||
<Trans>Shift</Trans>
|
||||
</Kbd>
|
||||
<span> + </span>
|
||||
<Kbd>
|
||||
<Trans>Space</Trans>
|
||||
</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Open/close current entry</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>O</Kbd>
|
||||
<span>, </span>
|
||||
<Kbd>
|
||||
<Trans>Enter</Trans>
|
||||
</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Open current entry in a new tab</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>V</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Open current entry in a new tab in the background</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>B</Kbd>
|
||||
<span>*, </span>
|
||||
<Kbd>
|
||||
<Trans>Middle click</Trans>
|
||||
</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Toggle read status of current entry</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>M</Kbd>
|
||||
<span>, </span>
|
||||
<Trans>Swipe header to the right</Trans>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Toggle starred status of current entry</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>S</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Mark all entries as read</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>
|
||||
<Trans>Shift</Trans>
|
||||
</Kbd>
|
||||
<span> + </span>
|
||||
<Kbd>A</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Go to the All view</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>G</Kbd>
|
||||
<span> </span>
|
||||
<Kbd>A</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Navigate to a subscription by entering its name</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>
|
||||
<Trans>Ctrl</Trans>
|
||||
</Kbd>
|
||||
<span> + </span>
|
||||
<Kbd>K</Kbd>
|
||||
<span>, </span>
|
||||
<Kbd>G</Kbd>
|
||||
<span> </span>
|
||||
<Kbd>U</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Show entry menu (desktop)</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>
|
||||
<Trans>Right click</Trans>
|
||||
</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Show native menu (desktop)</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>
|
||||
<Trans>Shift</Trans>
|
||||
</Kbd>
|
||||
<span> + </span>
|
||||
<Kbd>
|
||||
<Trans>Right click</Trans>
|
||||
</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Show entry menu (mobile)</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>
|
||||
<Trans>Long press</Trans>
|
||||
</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Toggle sidebar</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>F</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Show keyboard shortcut help</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>?</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</Table>
|
||||
<Box>
|
||||
<span>* </span>
|
||||
<Anchor href={Constants.browserExtensionUrl} target="_blank" rel="noreferrer">
|
||||
<Trans>Browser extension required for Chrome</Trans>
|
||||
</Anchor>
|
||||
</Box>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Trans } from "@lingui/macro"
|
||||
import { Tooltip } from "@mantine/core"
|
||||
import dayjs from "dayjs"
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
@@ -10,5 +11,10 @@ export function RelativeDate(props: { date: Date | number | undefined }) {
|
||||
}, [])
|
||||
|
||||
if (!props.date) return <Trans>N/A</Trans>
|
||||
return <>{dayjs(props.date).from(dayjs(now))}</>
|
||||
const date = dayjs(props.date)
|
||||
return (
|
||||
<Tooltip label={date.toDate().toLocaleString()} openDelay={500}>
|
||||
<span>{date.from(dayjs(now))}</span>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
36
commafeed-client/src/components/code/CodeEditor.tsx
Normal file
36
commafeed-client/src/components/code/CodeEditor.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Input, Textarea } from "@mantine/core"
|
||||
import RichCodeEditor from "components/code/RichCodeEditor"
|
||||
import { useMobile } from "hooks/useMobile"
|
||||
import { ReactNode } from "react"
|
||||
|
||||
interface CodeEditorProps {
|
||||
description?: ReactNode
|
||||
language: "css" | "javascript"
|
||||
value: string
|
||||
onChange: (value: string | undefined) => void
|
||||
}
|
||||
|
||||
export function CodeEditor(props: CodeEditorProps) {
|
||||
const mobile = useMobile()
|
||||
|
||||
return mobile ? (
|
||||
// monaco mobile support is poor, fallback to textarea
|
||||
<Textarea
|
||||
autosize
|
||||
minRows={4}
|
||||
maxRows={15}
|
||||
description={props.description}
|
||||
styles={{
|
||||
input: {
|
||||
fontFamily: "monospace",
|
||||
},
|
||||
}}
|
||||
value={props.value}
|
||||
onChange={e => props.onChange(e.currentTarget.value)}
|
||||
/>
|
||||
) : (
|
||||
<Input.Wrapper description={props.description}>
|
||||
<RichCodeEditor height="30vh" language={props.language} value={props.value} onChange={props.onChange} />
|
||||
</Input.Wrapper>
|
||||
)
|
||||
}
|
||||
52
commafeed-client/src/components/code/RichCodeEditor.tsx
Normal file
52
commafeed-client/src/components/code/RichCodeEditor.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { useMantineTheme } from "@mantine/core"
|
||||
import { Loader } from "components/Loader"
|
||||
import { useAsync } from "react-async-hook"
|
||||
|
||||
const init = async () => {
|
||||
window.MonacoEnvironment = {
|
||||
async getWorker(_, label) {
|
||||
let worker
|
||||
if (label === "css") {
|
||||
worker = await import("monaco-editor/esm/vs/language/css/css.worker?worker")
|
||||
} else if (label === "javascript") {
|
||||
worker = await import("monaco-editor/esm/vs/language/typescript/ts.worker?worker")
|
||||
} else {
|
||||
worker = await import("monaco-editor/esm/vs/editor/editor.worker?worker")
|
||||
}
|
||||
// eslint-disable-next-line new-cap
|
||||
return new worker.default()
|
||||
},
|
||||
}
|
||||
|
||||
const monacoReact = await import("@monaco-editor/react")
|
||||
const monaco = await import("monaco-editor")
|
||||
monacoReact.loader.config({ monaco })
|
||||
return monacoReact.Editor
|
||||
}
|
||||
|
||||
interface RichCodeEditorProps {
|
||||
height: number | string
|
||||
language: "css" | "javascript"
|
||||
value: string
|
||||
onChange: (value: string | undefined) => void
|
||||
}
|
||||
|
||||
function RichCodeEditor(props: RichCodeEditorProps) {
|
||||
const theme = useMantineTheme()
|
||||
const editorTheme = theme.colorScheme === "dark" ? "vs-dark" : "light"
|
||||
|
||||
const { result: Editor } = useAsync(init, [])
|
||||
if (!Editor) return <Loader />
|
||||
return (
|
||||
<Editor
|
||||
height={props.height}
|
||||
defaultLanguage={props.language}
|
||||
theme={editorTheme}
|
||||
options={{ minimap: { enabled: false } }}
|
||||
value={props.value}
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default RichCodeEditor
|
||||
@@ -1,12 +1,14 @@
|
||||
import { Box, createStyles, Mark, TypographyStylesProvider } from "@mantine/core"
|
||||
import { Constants } from "app/constants"
|
||||
import { useAppSelector } from "app/store"
|
||||
import { calculatePlaceholderSize } from "app/utils"
|
||||
import { ImageWithPlaceholderWhileLoading } from "components/ImageWithPlaceholderWhileLoading"
|
||||
import escapeStringRegexp from "escape-string-regexp"
|
||||
import { ChildrenNode, Interweave, Matcher, MatchResponse, Node, TransformCallback } from "interweave"
|
||||
import React from "react"
|
||||
|
||||
export interface ContentProps {
|
||||
content: string
|
||||
highlight?: string
|
||||
}
|
||||
|
||||
const useStyles = createStyles(theme => ({
|
||||
@@ -63,7 +65,7 @@ class HighlightMatcher extends Matcher {
|
||||
|
||||
constructor(search: string) {
|
||||
super("highlight")
|
||||
this.search = search
|
||||
this.search = escapeStringRegexp(search)
|
||||
}
|
||||
|
||||
match(string: string): MatchResponse<unknown> | null {
|
||||
@@ -82,10 +84,10 @@ class HighlightMatcher extends Matcher {
|
||||
}
|
||||
}
|
||||
|
||||
export function Content(props: ContentProps) {
|
||||
// memoize component because Interweave is costly
|
||||
const Content = React.memo((props: ContentProps) => {
|
||||
const { classes } = useStyles()
|
||||
const search = useAppSelector(state => state.entries.search)
|
||||
const matchers = search ? [new HighlightMatcher(search)] : []
|
||||
const matchers = props.highlight ? [new HighlightMatcher(props.highlight)] : []
|
||||
|
||||
return (
|
||||
<TypographyStylesProvider>
|
||||
@@ -94,4 +96,6 @@ export function Content(props: ContentProps) {
|
||||
</Box>
|
||||
</TypographyStylesProvider>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
export { Content }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Trans } from "@lingui/macro"
|
||||
import { Box } from "@mantine/core"
|
||||
import { openModal } from "@mantine/modals"
|
||||
import { Constants } from "app/constants"
|
||||
import {
|
||||
@@ -10,15 +11,18 @@ import {
|
||||
selectEntry,
|
||||
selectNextEntry,
|
||||
selectPreviousEntry,
|
||||
starEntry,
|
||||
} from "app/slices/entries"
|
||||
import { redirectToRootCategory } from "app/slices/redirect"
|
||||
import { toggleSidebar } from "app/slices/tree"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import { openLinkInBackgroundTab } from "app/utils"
|
||||
import { KeyboardShortcutsHelp } from "components/KeyboardShortcutsHelp"
|
||||
import { Loader } from "components/Loader"
|
||||
import { useBrowserExtension } from "hooks/useBrowserExtension"
|
||||
import { useMousetrap } from "hooks/useMousetrap"
|
||||
import { useViewMode } from "hooks/useViewMode"
|
||||
import { useEffect } from "react"
|
||||
import { useContextMenu } from "react-contexify"
|
||||
import InfiniteScroll from "react-infinite-scroller"
|
||||
import { throttle } from "throttle-debounce"
|
||||
import { FeedEntry } from "./FeedEntry"
|
||||
@@ -29,10 +33,14 @@ export function FeedEntries() {
|
||||
const entriesTimestamp = useAppSelector(state => state.entries.timestamp)
|
||||
const selectedEntryId = useAppSelector(state => state.entries.selectedEntryId)
|
||||
const hasMore = useAppSelector(state => state.entries.hasMore)
|
||||
const { viewMode } = useViewMode()
|
||||
const loading = useAppSelector(state => state.entries.loading)
|
||||
const scrollMarks = useAppSelector(state => state.user.settings?.scrollMarks)
|
||||
const scrollingToEntry = useAppSelector(state => state.entries.scrollingToEntry)
|
||||
const sidebarVisible = useAppSelector(state => state.tree.sidebarVisible)
|
||||
const customContextMenu = useAppSelector(state => state.user.settings?.customContextMenu)
|
||||
const { viewMode } = useViewMode()
|
||||
const dispatch = useAppDispatch()
|
||||
const { openLinkInBackgroundTab } = useBrowserExtension()
|
||||
|
||||
const selectedEntry = entries.find(e => e.id === selectedEntryId)
|
||||
|
||||
@@ -56,10 +64,44 @@ export function FeedEntries() {
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const scrollArea = document.getElementById(Constants.dom.mainScrollAreaId)
|
||||
const contextMenu = useContextMenu()
|
||||
const headerRightClicked = (entry: ExpendableEntry, event: React.MouseEvent) => {
|
||||
if (event.shiftKey || !customContextMenu) return
|
||||
|
||||
const listener = () => {
|
||||
event.preventDefault()
|
||||
contextMenu.show({
|
||||
id: Constants.dom.entryContextMenuId(entry),
|
||||
event,
|
||||
})
|
||||
}
|
||||
|
||||
const bodyClicked = (entry: ExpendableEntry) => {
|
||||
if (viewMode !== "expanded") return
|
||||
|
||||
// entry is already selected
|
||||
if (entry.id === selectedEntryId) return
|
||||
|
||||
dispatch(
|
||||
selectEntry({
|
||||
entry,
|
||||
expand: true,
|
||||
markAsRead: true,
|
||||
scrollToEntry: true,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const swipedRight = (entry: ExpendableEntry) => dispatch(markEntry({ entry, read: !entry.read }))
|
||||
|
||||
// close context menu on scroll
|
||||
useEffect(() => {
|
||||
const listener = throttle(100, () => contextMenu.hideAll())
|
||||
window.addEventListener("scroll", listener)
|
||||
return () => window.removeEventListener("scroll", listener)
|
||||
}, [contextMenu])
|
||||
|
||||
useEffect(() => {
|
||||
const listener = throttle(100, () => {
|
||||
if (viewMode !== "expanded") return
|
||||
if (scrollingToEntry) return
|
||||
|
||||
@@ -81,11 +123,10 @@ export function FeedEntries() {
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
const throttledListener = throttle(100, listener)
|
||||
scrollArea?.addEventListener("scroll", throttledListener)
|
||||
return () => scrollArea?.removeEventListener("scroll", throttledListener)
|
||||
}, [dispatch, entries, viewMode, scrollMarks, scrollingToEntry])
|
||||
})
|
||||
window.addEventListener("scroll", listener)
|
||||
return () => window.removeEventListener("scroll", listener)
|
||||
}, [dispatch, contextMenu, entries, viewMode, scrollMarks, scrollingToEntry])
|
||||
|
||||
useMousetrap("r", () => dispatch(reloadEntries()))
|
||||
useMousetrap("j", () =>
|
||||
@@ -137,9 +178,8 @@ export function FeedEntries() {
|
||||
})
|
||||
)
|
||||
} else {
|
||||
const scrollArea = document.getElementById(Constants.dom.mainScrollAreaId)
|
||||
scrollArea?.scrollTo({
|
||||
top: scrollArea.scrollTop + scrollArea.clientHeight * 0.8,
|
||||
window.scrollTo({
|
||||
top: window.scrollY + document.documentElement.clientHeight * 0.8,
|
||||
behavior: "smooth",
|
||||
})
|
||||
}
|
||||
@@ -176,9 +216,8 @@ export function FeedEntries() {
|
||||
})
|
||||
)
|
||||
} else {
|
||||
const scrollArea = document.getElementById(Constants.dom.mainScrollAreaId)
|
||||
scrollArea?.scrollTo({
|
||||
top: scrollArea.scrollTop - scrollArea.clientHeight * 0.8,
|
||||
window.scrollTo({
|
||||
top: window.scrollY - document.documentElement.clientHeight * 0.8,
|
||||
behavior: "smooth",
|
||||
})
|
||||
}
|
||||
@@ -211,7 +250,6 @@ export function FeedEntries() {
|
||||
window.open(selectedEntry.url, "_blank", "noreferrer")
|
||||
})
|
||||
useMousetrap("b", () => {
|
||||
// simulate ctrl+click to open tab in background
|
||||
if (!selectedEntry) return
|
||||
openLinkInBackgroundTab(selectedEntry.url)
|
||||
})
|
||||
@@ -220,6 +258,11 @@ export function FeedEntries() {
|
||||
if (!selectedEntry) return
|
||||
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", () => {
|
||||
// mark all entries as read
|
||||
dispatch(
|
||||
@@ -234,6 +277,7 @@ export function FeedEntries() {
|
||||
)
|
||||
})
|
||||
useMousetrap("g a", () => dispatch(redirectToRootCategory()))
|
||||
useMousetrap("f", () => dispatch(toggleSidebar()))
|
||||
useMousetrap("?", () =>
|
||||
openModal({
|
||||
title: <Trans>Keyboard shortcuts</Trans>,
|
||||
@@ -247,11 +291,9 @@ export function FeedEntries() {
|
||||
<InfiniteScroll
|
||||
id="entries"
|
||||
initialLoad={false}
|
||||
loadMore={() => dispatch(loadMoreEntries())}
|
||||
loadMore={() => !loading && dispatch(loadMoreEntries())}
|
||||
hasMore={hasMore}
|
||||
loader={<Loader key={0} />}
|
||||
useWindow={false}
|
||||
getScrollParent={() => document.getElementById(Constants.dom.mainScrollAreaId)}
|
||||
loader={<Box key={0}>{loading && <Loader />}</Box>}
|
||||
>
|
||||
{entries.map(entry => (
|
||||
<div
|
||||
@@ -265,7 +307,11 @@ export function FeedEntries() {
|
||||
expanded={!!entry.expanded || viewMode === "expanded"}
|
||||
selected={entry.id === selectedEntryId}
|
||||
showSelectionIndicator={entry.id === selectedEntryId && (!entry.expanded || viewMode === "expanded")}
|
||||
maxWidth={sidebarVisible ? Constants.layout.entryMaxWidth : undefined}
|
||||
onHeaderClick={event => headerClicked(entry, event)}
|
||||
onHeaderRightClick={event => headerRightClicked(entry, event)}
|
||||
onBodyClick={() => bodyClicked(entry)}
|
||||
onSwipedRight={() => swipedRight(entry)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { Box, createStyles, Divider, Paper } from "@mantine/core"
|
||||
import { MantineNumberSize } from "@mantine/styles"
|
||||
import { Constants } from "app/constants"
|
||||
import { markEntry } from "app/slices/entries"
|
||||
import { useAppDispatch } from "app/store"
|
||||
import { Entry, ViewMode } from "app/types"
|
||||
import { useViewMode } from "hooks/useViewMode"
|
||||
import React from "react"
|
||||
import { useSwipeable } from "react-swipeable"
|
||||
import { FeedEntryBody } from "./FeedEntryBody"
|
||||
import { FeedEntryCompactHeader } from "./FeedEntryCompactHeader"
|
||||
import { FeedEntryContextMenu, useFeedEntryContextMenu } from "./FeedEntryContextMenu"
|
||||
import { FeedEntryContextMenu } from "./FeedEntryContextMenu"
|
||||
import { FeedEntryFooter } from "./FeedEntryFooter"
|
||||
import { FeedEntryHeader } from "./FeedEntryHeader"
|
||||
|
||||
@@ -18,30 +16,50 @@ interface FeedEntryProps {
|
||||
expanded: boolean
|
||||
selected: boolean
|
||||
showSelectionIndicator: boolean
|
||||
maxWidth?: number
|
||||
onHeaderClick: (e: React.MouseEvent) => void
|
||||
onHeaderRightClick: (e: React.MouseEvent) => void
|
||||
onBodyClick: (e: React.MouseEvent) => void
|
||||
onSwipedRight: () => void
|
||||
}
|
||||
|
||||
const useStyles = createStyles((theme, props: FeedEntryProps & { viewMode?: ViewMode }) => {
|
||||
let backgroundColor
|
||||
if (theme.colorScheme === "dark") backgroundColor = props.entry.read ? "inherit" : theme.colors.dark[5]
|
||||
else backgroundColor = props.entry.read && !props.expanded ? theme.colors.gray[0] : "inherit"
|
||||
if (theme.colorScheme === "dark") {
|
||||
backgroundColor = props.entry.read ? "inherit" : theme.colors.dark[5]
|
||||
} else {
|
||||
backgroundColor = props.entry.read && !props.expanded ? theme.colors.gray[0] : "inherit"
|
||||
}
|
||||
|
||||
let marginY = 10
|
||||
if (props.viewMode === "title") marginY = 2
|
||||
else if (props.viewMode === "cozy") marginY = 6
|
||||
if (props.viewMode === "title") {
|
||||
marginY = 2
|
||||
} else if (props.viewMode === "cozy") {
|
||||
marginY = 6
|
||||
}
|
||||
|
||||
let mobileMarginY = 6
|
||||
if (props.viewMode === "title") mobileMarginY = 2
|
||||
else if (props.viewMode === "cozy") mobileMarginY = 4
|
||||
if (props.viewMode === "title") {
|
||||
mobileMarginY = 2
|
||||
} else if (props.viewMode === "cozy") {
|
||||
mobileMarginY = 4
|
||||
}
|
||||
|
||||
let backgroundHoverColor = backgroundColor
|
||||
if (!props.expanded && !props.entry.read) {
|
||||
backgroundHoverColor = theme.colorScheme === "dark" ? theme.colors.dark[6] : theme.colors.gray[1]
|
||||
}
|
||||
|
||||
const styles = {
|
||||
let paperBorderLeftColor
|
||||
if (props.showSelectionIndicator) {
|
||||
const borderLeftColor = theme.colorScheme === "dark" ? theme.colors.orange[4] : theme.colors.orange[6]
|
||||
paperBorderLeftColor = `${borderLeftColor} !important`
|
||||
}
|
||||
|
||||
return {
|
||||
paper: {
|
||||
backgroundColor,
|
||||
borderLeftColor: paperBorderLeftColor,
|
||||
marginTop: marginY,
|
||||
marginBottom: marginY,
|
||||
[theme.fn.smallerThan(Constants.layout.mobileBreakpoint)]: {
|
||||
@@ -59,40 +77,36 @@ const useStyles = createStyles((theme, props: FeedEntryProps & { viewMode?: View
|
||||
textDecoration: "none",
|
||||
},
|
||||
body: {
|
||||
maxWidth: Constants.layout.entryMaxWidth,
|
||||
direction: props.entry.rtl ? "rtl" : "ltr",
|
||||
maxWidth: props.maxWidth ?? "100%",
|
||||
},
|
||||
}
|
||||
|
||||
if (props.showSelectionIndicator) {
|
||||
const borderLeftColor = theme.colorScheme === "dark" ? theme.colors.orange[4] : theme.colors.orange[6]
|
||||
styles.paper.borderLeftColor = `${borderLeftColor} !important`
|
||||
}
|
||||
|
||||
return styles
|
||||
})
|
||||
|
||||
export function FeedEntry(props: FeedEntryProps) {
|
||||
const { viewMode } = useViewMode()
|
||||
const { classes, cx } = useStyles({ ...props, viewMode })
|
||||
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const swipeHandlers = useSwipeable({
|
||||
onSwipedRight: () => dispatch(markEntry({ entry: props.entry, read: !props.entry.read })),
|
||||
onSwipedRight: props.onSwipedRight,
|
||||
})
|
||||
|
||||
const { onContextMenu } = useFeedEntryContextMenu(props.entry)
|
||||
|
||||
let paddingX: MantineNumberSize = "xs"
|
||||
if (viewMode === "title" || viewMode === "cozy") paddingX = 6
|
||||
|
||||
let paddingY: MantineNumberSize = "xs"
|
||||
if (viewMode === "title") paddingY = 4
|
||||
else if (viewMode === "cozy") paddingY = 8
|
||||
if (viewMode === "title") {
|
||||
paddingY = 4
|
||||
} else if (viewMode === "cozy") {
|
||||
paddingY = 8
|
||||
}
|
||||
|
||||
let borderRadius: MantineNumberSize = "sm"
|
||||
if (viewMode === "title") borderRadius = 0
|
||||
else if (viewMode === "cozy") borderRadius = "xs"
|
||||
if (viewMode === "title") {
|
||||
borderRadius = 0
|
||||
} else if (viewMode === "cozy") {
|
||||
borderRadius = "xs"
|
||||
}
|
||||
|
||||
const compactHeader = !props.expanded && (viewMode === "title" || viewMode === "cozy")
|
||||
return (
|
||||
@@ -114,7 +128,7 @@ export function FeedEntry(props: FeedEntryProps) {
|
||||
rel="noreferrer"
|
||||
onClick={props.onHeaderClick}
|
||||
onAuxClick={props.onHeaderClick}
|
||||
onContextMenu={onContextMenu}
|
||||
onContextMenu={props.onHeaderRightClick}
|
||||
>
|
||||
<Box px={paddingX} py={paddingY} {...swipeHandlers}>
|
||||
{compactHeader && <FeedEntryCompactHeader entry={props.entry} />}
|
||||
@@ -122,8 +136,8 @@ export function FeedEntry(props: FeedEntryProps) {
|
||||
</Box>
|
||||
</a>
|
||||
{props.expanded && (
|
||||
<Box px={paddingX} pb={paddingY}>
|
||||
<Box className={classes.body} sx={{ direction: props.entry.rtl ? "rtl" : "ltr" }}>
|
||||
<Box px={paddingX} pb={paddingY} onClick={props.onBodyClick}>
|
||||
<Box className={classes.body}>
|
||||
<FeedEntryBody entry={props.entry} />
|
||||
</Box>
|
||||
<Divider variant="dashed" my={paddingY} />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Box } from "@mantine/core"
|
||||
import { useAppSelector } from "app/store"
|
||||
import { Entry } from "app/types"
|
||||
import { Content } from "./Content"
|
||||
import { Enclosure } from "./Enclosure"
|
||||
@@ -9,10 +10,11 @@ export interface FeedEntryBodyProps {
|
||||
}
|
||||
|
||||
export function FeedEntryBody(props: FeedEntryBodyProps) {
|
||||
const search = useAppSelector(state => state.entries.search)
|
||||
return (
|
||||
<Box>
|
||||
<Box>
|
||||
<Content content={props.entry.content} />
|
||||
<Content content={props.entry.content} highlight={search} />
|
||||
</Box>
|
||||
{props.entry.enclosureType && props.entry.enclosureUrl && (
|
||||
<Box pt="md">
|
||||
|
||||
@@ -5,11 +5,10 @@ import { markEntriesUpToEntry, markEntry, starEntry } from "app/slices/entries"
|
||||
import { redirectToFeed } from "app/slices/redirect"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import { Entry } from "app/types"
|
||||
import { openLinkInBackgroundTab, truncate } from "app/utils"
|
||||
import { useEffect } from "react"
|
||||
import { Item, Menu, Separator, useContextMenu } from "react-contexify"
|
||||
import { truncate } from "app/utils"
|
||||
import { useBrowserExtension } from "hooks/useBrowserExtension"
|
||||
import { Item, Menu, Separator } from "react-contexify"
|
||||
import { TbArrowBarToDown, TbExternalLink, TbEyeCheck, TbEyeOff, TbRss, TbStar, TbStarOff } from "react-icons/tb"
|
||||
import { throttle } from "throttle-debounce"
|
||||
|
||||
interface FeedEntryContextMenuProps {
|
||||
entry: Entry
|
||||
@@ -28,15 +27,14 @@ const useStyles = createStyles(theme => ({
|
||||
},
|
||||
}))
|
||||
|
||||
const menuId = (entry: Entry) => entry.id
|
||||
|
||||
export function FeedEntryContextMenu(props: FeedEntryContextMenuProps) {
|
||||
const { classes, theme } = useStyles()
|
||||
const sourceType = useAppSelector(state => state.entries.source.type)
|
||||
const dispatch = useAppDispatch()
|
||||
const { openLinkInBackgroundTab } = useBrowserExtension()
|
||||
|
||||
return (
|
||||
<Menu id={menuId(props.entry)} theme={theme.colorScheme} animation={false} className={classes.menu}>
|
||||
<Menu id={Constants.dom.entryContextMenuId(props.entry)} theme={theme.colorScheme} animation={false} className={classes.menu}>
|
||||
<Item
|
||||
onClick={() => {
|
||||
window.open(props.entry.url, "_blank", "noreferrer")
|
||||
@@ -100,29 +98,3 @@ export function FeedEntryContextMenu(props: FeedEntryContextMenuProps) {
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
|
||||
export function useFeedEntryContextMenu(entry: Entry) {
|
||||
const contextMenu = useContextMenu({
|
||||
id: menuId(entry),
|
||||
})
|
||||
|
||||
const onContextMenu = (event: React.MouseEvent) => {
|
||||
event.preventDefault()
|
||||
contextMenu.show({
|
||||
event,
|
||||
})
|
||||
}
|
||||
|
||||
// close context menu on scroll
|
||||
useEffect(() => {
|
||||
const scrollArea = document.getElementById(Constants.dom.mainScrollAreaId)
|
||||
|
||||
const listener = () => contextMenu.hideAll()
|
||||
const throttledListener = throttle(100, listener)
|
||||
|
||||
scrollArea?.addEventListener("scroll", throttledListener)
|
||||
return () => scrollArea?.removeEventListener("scroll", throttledListener)
|
||||
}, [contextMenu])
|
||||
|
||||
return { onContextMenu }
|
||||
}
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import { t, Trans } from "@lingui/macro"
|
||||
import { Group, Indicator, MultiSelect, Popover } from "@mantine/core"
|
||||
import { useMediaQuery } from "@mantine/hooks"
|
||||
import { Constants } from "app/constants"
|
||||
import { markEntriesUpToEntry, markEntry, starEntry, tagEntry } from "app/slices/entries"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import { Entry } from "app/types"
|
||||
import { ActionButton } from "components/ActionButtton"
|
||||
import { ButtonToolbar } from "components/ButtonToolbar"
|
||||
import { useEffect, useState } from "react"
|
||||
import { ActionButton } from "components/ActionButton"
|
||||
import { useActionButton } from "hooks/useActionButton"
|
||||
import { useMobile } from "hooks/useMobile"
|
||||
import { TbArrowBarToDown, TbExternalLink, TbEyeCheck, TbEyeOff, TbShare, TbStar, TbStarOff, TbTag } from "react-icons/tb"
|
||||
import { throttle } from "throttle-debounce"
|
||||
import { ShareButtons } from "./ShareButtons"
|
||||
|
||||
interface FeedEntryFooterProps {
|
||||
@@ -17,10 +14,10 @@ interface FeedEntryFooterProps {
|
||||
}
|
||||
|
||||
export function FeedEntryFooter(props: FeedEntryFooterProps) {
|
||||
const [scrollPosition, setScrollPosition] = useState(0)
|
||||
const sharingSettings = useAppSelector(state => state.user.settings?.sharingSettings)
|
||||
const tags = useAppSelector(state => state.user.tags)
|
||||
const mobile = !useMediaQuery(`(min-width: ${Constants.layout.mobileBreakpoint})`)
|
||||
const mobile = useMobile()
|
||||
const { spacing } = useActionButton()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const showSharingButtons = sharingSettings && Object.values(sharingSettings).some(v => v)
|
||||
@@ -34,19 +31,9 @@ export function FeedEntryFooter(props: FeedEntryFooterProps) {
|
||||
})
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const scrollArea = document.getElementById(Constants.dom.mainScrollAreaId)
|
||||
|
||||
const listener = () => setScrollPosition(scrollArea ? scrollArea.scrollTop : 0)
|
||||
const throttledListener = throttle(100, listener)
|
||||
|
||||
scrollArea?.addEventListener("scroll", throttledListener)
|
||||
return () => scrollArea?.removeEventListener("scroll", throttledListener)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Group position="apart">
|
||||
<ButtonToolbar>
|
||||
<Group spacing={spacing}>
|
||||
{props.entry.markable && (
|
||||
<ActionButton
|
||||
icon={props.entry.read ? <TbEyeOff size={18} /> : <TbEyeCheck size={18} />}
|
||||
@@ -61,7 +48,7 @@ export function FeedEntryFooter(props: FeedEntryFooterProps) {
|
||||
/>
|
||||
|
||||
{showSharingButtons && (
|
||||
<Popover withArrow withinPortal shadow="md" positionDependencies={[scrollPosition]} closeOnClickOutside={!mobile}>
|
||||
<Popover withArrow withinPortal shadow="md" closeOnClickOutside={!mobile}>
|
||||
<Popover.Target>
|
||||
<ActionButton icon={<TbShare size={18} />} label={<Trans>Share</Trans>} />
|
||||
</Popover.Target>
|
||||
@@ -72,7 +59,7 @@ export function FeedEntryFooter(props: FeedEntryFooterProps) {
|
||||
)}
|
||||
|
||||
{tags && (
|
||||
<Popover withArrow withinPortal shadow="md" positionDependencies={[scrollPosition]} closeOnClickOutside={!mobile}>
|
||||
<Popover withArrow withinPortal shadow="md" closeOnClickOutside={!mobile}>
|
||||
<Popover.Target>
|
||||
<Indicator label={props.entry.tags.length} disabled={props.entry.tags.length === 0} inline size={16}>
|
||||
<ActionButton icon={<TbTag size={18} />} label={<Trans>Tags</Trans>} />
|
||||
@@ -96,7 +83,7 @@ export function FeedEntryFooter(props: FeedEntryFooterProps) {
|
||||
<a href={props.entry.url} target="_blank" rel="noreferrer">
|
||||
<ActionButton icon={<TbExternalLink size={18} />} label={<Trans>Open link</Trans>} />
|
||||
</a>
|
||||
</ButtonToolbar>
|
||||
</Group>
|
||||
|
||||
<ActionButton
|
||||
icon={<TbArrowBarToDown size={18} />}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Box, createStyles, Text } from "@mantine/core"
|
||||
import { Box, createStyles, Space, Text } from "@mantine/core"
|
||||
import { Entry } from "app/types"
|
||||
import { RelativeDate } from "components/RelativeDate"
|
||||
import { FeedEntryTitle } from "./FeedEntryTitle"
|
||||
@@ -12,17 +12,11 @@ export interface FeedEntryHeaderProps {
|
||||
const useStyles = createStyles((theme, props: FeedEntryHeaderProps) => ({
|
||||
headerText: {
|
||||
fontWeight: theme.colorScheme === "light" && !props.entry.read ? "bold" : "inherit",
|
||||
whiteSpace: props.expanded ? "inherit" : "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
},
|
||||
headerSubtext: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
fontSize: "90%",
|
||||
whiteSpace: props.expanded ? "inherit" : "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
},
|
||||
}))
|
||||
export function FeedEntryHeader(props: FeedEntryHeaderProps) {
|
||||
@@ -33,18 +27,13 @@ export function FeedEntryHeader(props: FeedEntryHeaderProps) {
|
||||
<FeedEntryTitle entry={props.entry} />
|
||||
</Box>
|
||||
<Box className={classes.headerSubtext}>
|
||||
<Box mr={6}>
|
||||
<FeedFavicon url={props.entry.iconUrl} />
|
||||
</Box>
|
||||
<Box>
|
||||
<Text color="dimmed">{props.entry.feedName}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text color="dimmed">
|
||||
<span> · </span>
|
||||
<RelativeDate date={props.entry.date} />
|
||||
</Text>
|
||||
</Box>
|
||||
<FeedFavicon url={props.entry.iconUrl} />
|
||||
<Space w={6} />
|
||||
<Text color="dimmed">
|
||||
{props.entry.feedName}
|
||||
<span> · </span>
|
||||
<RelativeDate date={props.entry.date} />
|
||||
</Text>
|
||||
</Box>
|
||||
{props.expanded && (
|
||||
<Box className={classes.headerSubtext}>
|
||||
|
||||
@@ -1,15 +1,29 @@
|
||||
import { t, Trans } from "@lingui/macro"
|
||||
import { ActionIcon, Center, Divider, Indicator, Popover, TextInput } from "@mantine/core"
|
||||
import { ActionIcon, Box, Center, Divider, Group, Indicator, Popover, TextInput } from "@mantine/core"
|
||||
import { useForm } from "@mantine/form"
|
||||
import { reloadEntries, search } from "app/slices/entries"
|
||||
import { reloadEntries, search, selectNextEntry, selectPreviousEntry } from "app/slices/entries"
|
||||
import { changeReadingMode, changeReadingOrder } from "app/slices/user"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import { ActionButton } from "components/ActionButtton"
|
||||
import { ButtonToolbar } from "components/ButtonToolbar"
|
||||
import { ActionButton } from "components/ActionButton"
|
||||
import { Loader } from "components/Loader"
|
||||
import { useActionButton } from "hooks/useActionButton"
|
||||
import { useBrowserExtension } from "hooks/useBrowserExtension"
|
||||
import { useMobile } from "hooks/useMobile"
|
||||
import { useEffect } from "react"
|
||||
import { TbArrowDown, TbArrowUp, TbExternalLink, TbEye, TbEyeOff, TbRefresh, TbSearch, TbSettings, TbUser, TbX } from "react-icons/tb"
|
||||
import {
|
||||
TbArrowDown,
|
||||
TbArrowUp,
|
||||
TbExternalLink,
|
||||
TbEye,
|
||||
TbEyeOff,
|
||||
TbRefresh,
|
||||
TbSearch,
|
||||
TbSettings,
|
||||
TbSortAscending,
|
||||
TbSortDescending,
|
||||
TbUser,
|
||||
TbX,
|
||||
} from "react-icons/tb"
|
||||
import { MarkAllAsReadButton } from "./MarkAllAsReadButton"
|
||||
import { ProfileMenu } from "./ProfileMenu"
|
||||
|
||||
@@ -17,13 +31,32 @@ function HeaderDivider() {
|
||||
return <Divider orientation="vertical" />
|
||||
}
|
||||
|
||||
function HeaderToolbar(props: { children: React.ReactNode }) {
|
||||
const { spacing } = useActionButton()
|
||||
const mobile = useMobile("480px")
|
||||
return mobile ? (
|
||||
// on mobile use all available width
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</Box>
|
||||
) : (
|
||||
<Group spacing={spacing}>{props.children}</Group>
|
||||
)
|
||||
}
|
||||
|
||||
const iconSize = 18
|
||||
|
||||
export function Header() {
|
||||
const settings = useAppSelector(state => state.user.settings)
|
||||
const profile = useAppSelector(state => state.user.profile)
|
||||
const searchFromStore = useAppSelector(state => state.entries.search)
|
||||
const { isBrowserExtension, openSettingsPage, openAppInNewTab } = useBrowserExtension()
|
||||
const { isBrowserExtensionPopup, openSettingsPage, openAppInNewTab } = useBrowserExtension()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const searchForm = useForm<{ search: string }>({
|
||||
@@ -42,7 +75,36 @@ export function Header() {
|
||||
if (!settings) return <Loader />
|
||||
return (
|
||||
<Center>
|
||||
<ButtonToolbar>
|
||||
<HeaderToolbar>
|
||||
<ActionButton
|
||||
icon={<TbArrowUp size={iconSize} />}
|
||||
label={<Trans>Previous</Trans>}
|
||||
onClick={() =>
|
||||
dispatch(
|
||||
selectPreviousEntry({
|
||||
expand: true,
|
||||
markAsRead: true,
|
||||
scrollToEntry: true,
|
||||
})
|
||||
)
|
||||
}
|
||||
/>
|
||||
<ActionButton
|
||||
icon={<TbArrowDown size={iconSize} />}
|
||||
label={<Trans>Next</Trans>}
|
||||
onClick={() =>
|
||||
dispatch(
|
||||
selectNextEntry({
|
||||
expand: true,
|
||||
markAsRead: true,
|
||||
scrollToEntry: true,
|
||||
})
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
<HeaderDivider />
|
||||
|
||||
<ActionButton
|
||||
icon={<TbRefresh size={iconSize} />}
|
||||
label={<Trans>Refresh</Trans>}
|
||||
@@ -58,7 +120,7 @@ export function Header() {
|
||||
onClick={() => dispatch(changeReadingMode(settings.readingMode === "all" ? "unread" : "all"))}
|
||||
/>
|
||||
<ActionButton
|
||||
icon={settings.readingOrder === "asc" ? <TbArrowUp size={iconSize} /> : <TbArrowDown size={iconSize} />}
|
||||
icon={settings.readingOrder === "asc" ? <TbSortAscending size={iconSize} /> : <TbSortDescending size={iconSize} />}
|
||||
label={settings.readingOrder === "asc" ? <Trans>Asc</Trans> : <Trans>Desc</Trans>}
|
||||
onClick={() => dispatch(changeReadingOrder(settings.readingOrder === "asc" ? "desc" : "asc"))}
|
||||
/>
|
||||
@@ -90,7 +152,7 @@ export function Header() {
|
||||
|
||||
<ProfileMenu control={<ActionButton icon={<TbUser size={iconSize} />} label={profile?.name} />} />
|
||||
|
||||
{isBrowserExtension && (
|
||||
{isBrowserExtensionPopup && (
|
||||
<>
|
||||
<HeaderDivider />
|
||||
|
||||
@@ -106,7 +168,7 @@ export function Header() {
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</ButtonToolbar>
|
||||
</HeaderToolbar>
|
||||
</Center>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Trans } from "@lingui/macro"
|
||||
import { Button, Code, Group, Modal, Slider, Stack, Text } from "@mantine/core"
|
||||
import { markAllEntries } from "app/slices/entries"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import { ActionButton } from "components/ActionButtton"
|
||||
import { ActionButton } from "components/ActionButton"
|
||||
import { useState } from "react"
|
||||
import { TbChecks } from "react-icons/tb"
|
||||
|
||||
@@ -13,8 +13,27 @@ export function MarkAllAsReadButton(props: { iconSize: number }) {
|
||||
const source = useAppSelector(state => state.entries.source)
|
||||
const sourceLabel = useAppSelector(state => state.entries.sourceLabel)
|
||||
const entriesTimestamp = useAppSelector(state => state.entries.timestamp) ?? Date.now()
|
||||
const markAllAsReadConfirmation = useAppSelector(state => state.user.settings?.markAllAsReadConfirmation)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const buttonClicked = () => {
|
||||
if (markAllAsReadConfirmation) {
|
||||
setThreshold(0)
|
||||
setOpened(true)
|
||||
} else {
|
||||
dispatch(
|
||||
markAllEntries({
|
||||
sourceType: source.type,
|
||||
req: {
|
||||
id: source.id,
|
||||
read: true,
|
||||
olderThan: entriesTimestamp,
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal opened={opened} onClose={() => setOpened(false)} title={<Trans>Mark all entries as read</Trans>}>
|
||||
@@ -70,14 +89,7 @@ export function MarkAllAsReadButton(props: { iconSize: number }) {
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
<ActionButton
|
||||
icon={<TbChecks size={props.iconSize} />}
|
||||
label={<Trans>Mark all as read</Trans>}
|
||||
onClick={() => {
|
||||
setThreshold(0)
|
||||
setOpened(true)
|
||||
}}
|
||||
/>
|
||||
<ActionButton icon={<TbChecks size={props.iconSize} />} label={<Trans>Mark all as read</Trans>} onClick={buttonClicked} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { Box, MediaQuery } from "@mantine/core"
|
||||
import { Constants } from "app/constants"
|
||||
import { Box } from "@mantine/core"
|
||||
import { useMobile } from "hooks/useMobile"
|
||||
import React from "react"
|
||||
|
||||
export function OnDesktop(props: { children: React.ReactNode }) {
|
||||
return (
|
||||
<MediaQuery smallerThan={Constants.layout.mobileBreakpoint} styles={{ display: "none" }}>
|
||||
<Box>{props.children}</Box>
|
||||
</MediaQuery>
|
||||
)
|
||||
const mobile = useMobile()
|
||||
return <Box>{!mobile && props.children}</Box>
|
||||
}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { Box, MediaQuery } from "@mantine/core"
|
||||
import { Constants } from "app/constants"
|
||||
import { Box } from "@mantine/core"
|
||||
import { useMobile } from "hooks/useMobile"
|
||||
import React from "react"
|
||||
|
||||
export function OnMobile(props: { children: React.ReactNode }) {
|
||||
return (
|
||||
<MediaQuery largerThan={Constants.layout.mobileBreakpoint} styles={{ display: "none" }}>
|
||||
<Box>{props.children}</Box>
|
||||
</MediaQuery>
|
||||
)
|
||||
const mobile = useMobile()
|
||||
return <Box>{mobile && props.children}</Box>
|
||||
}
|
||||
|
||||
@@ -1,96 +1,83 @@
|
||||
import { Trans } from "@lingui/macro"
|
||||
import { Box, Button, Group, Stack, Textarea } from "@mantine/core"
|
||||
import { useForm } from "@mantine/form"
|
||||
import { client, errorToStrings } from "app/client"
|
||||
import { redirectToSelectedSource } from "app/slices/redirect"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import { Alert } from "components/Alert"
|
||||
import { useEffect } from "react"
|
||||
import { useAsyncCallback } from "react-async-hook"
|
||||
import { TbDeviceFloppy } from "react-icons/tb"
|
||||
|
||||
interface FormData {
|
||||
customCss: string
|
||||
customJs: string
|
||||
}
|
||||
|
||||
export function CustomCodeSettings() {
|
||||
const settings = useAppSelector(state => state.user.settings)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const form = useForm<FormData>()
|
||||
const { setValues } = form
|
||||
|
||||
const saveCustomCode = useAsyncCallback(
|
||||
async (d: FormData) => {
|
||||
if (!settings) return
|
||||
await client.user.saveSettings({
|
||||
...settings,
|
||||
customCss: d.customCss,
|
||||
customJs: d.customJs,
|
||||
})
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
window.location.reload()
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!settings) return
|
||||
setValues({
|
||||
customCss: settings.customCss,
|
||||
customJs: settings.customJs,
|
||||
})
|
||||
}, [setValues, settings])
|
||||
|
||||
return (
|
||||
<>
|
||||
{saveCustomCode.error && (
|
||||
<Box mb="md">
|
||||
<Alert messages={errorToStrings(saveCustomCode.error)} />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<form onSubmit={form.onSubmit(saveCustomCode.execute)}>
|
||||
<Stack>
|
||||
<Textarea
|
||||
autosize
|
||||
minRows={4}
|
||||
maxRows={15}
|
||||
{...form.getInputProps("customCss")}
|
||||
description={<Trans>Custom CSS rules that will be applied</Trans>}
|
||||
styles={{
|
||||
input: {
|
||||
fontFamily: "monospace",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
autosize
|
||||
minRows={4}
|
||||
maxRows={15}
|
||||
{...form.getInputProps("customJs")}
|
||||
description={<Trans>Custom JS code that will be executed on page load</Trans>}
|
||||
styles={{
|
||||
input: {
|
||||
fontFamily: "monospace",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Group>
|
||||
<Button variant="default" onClick={() => dispatch(redirectToSelectedSource())}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
<Button type="submit" leftIcon={<TbDeviceFloppy size={16} />} loading={saveCustomCode.loading}>
|
||||
<Trans>Save</Trans>
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
</>
|
||||
)
|
||||
}
|
||||
import { Trans } from "@lingui/macro"
|
||||
import { Box, Button, Group, Stack } from "@mantine/core"
|
||||
import { useForm } from "@mantine/form"
|
||||
import { client, errorToStrings } from "app/client"
|
||||
import { redirectToSelectedSource } from "app/slices/redirect"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import { Alert } from "components/Alert"
|
||||
import { CodeEditor } from "components/code/CodeEditor"
|
||||
import { useEffect } from "react"
|
||||
import { useAsyncCallback } from "react-async-hook"
|
||||
import { TbDeviceFloppy } from "react-icons/tb"
|
||||
|
||||
interface FormData {
|
||||
customCss: string
|
||||
customJs: string
|
||||
}
|
||||
|
||||
export function CustomCodeSettings() {
|
||||
const settings = useAppSelector(state => state.user.settings)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const form = useForm<FormData>()
|
||||
const { setValues } = form
|
||||
|
||||
const saveCustomCode = useAsyncCallback(
|
||||
async (d: FormData) => {
|
||||
if (!settings) return
|
||||
await client.user.saveSettings({
|
||||
...settings,
|
||||
customCss: d.customCss,
|
||||
customJs: d.customJs,
|
||||
})
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
window.location.reload()
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!settings) return
|
||||
setValues({
|
||||
customCss: settings.customCss,
|
||||
customJs: settings.customJs,
|
||||
})
|
||||
}, [setValues, settings])
|
||||
|
||||
return (
|
||||
<>
|
||||
{saveCustomCode.error && (
|
||||
<Box mb="md">
|
||||
<Alert messages={errorToStrings(saveCustomCode.error)} />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<form onSubmit={form.onSubmit(saveCustomCode.execute)}>
|
||||
<Stack>
|
||||
<CodeEditor
|
||||
description={<Trans>Custom CSS rules that will be applied</Trans>}
|
||||
language="css"
|
||||
{...form.getInputProps("customCss")}
|
||||
/>
|
||||
|
||||
<CodeEditor
|
||||
description={<Trans>Custom JS code that will be executed on page load</Trans>}
|
||||
language="javascript"
|
||||
{...form.getInputProps("customJs")}
|
||||
/>
|
||||
|
||||
<Group>
|
||||
<Button variant="default" onClick={() => dispatch(redirectToSelectedSource())}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
<Button type="submit" leftIcon={<TbDeviceFloppy size={16} />} loading={saveCustomCode.loading}>
|
||||
<Trans>Save</Trans>
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
import { Trans } from "@lingui/macro"
|
||||
import { Divider, Select, SimpleGrid, Stack, Switch } from "@mantine/core"
|
||||
import { Constants } from "app/constants"
|
||||
import { changeLanguage, changeScrollMarks, changeScrollSpeed, changeSharingSetting, changeShowRead } from "app/slices/user"
|
||||
import {
|
||||
changeAlwaysScrollToEntry,
|
||||
changeCustomContextMenu,
|
||||
changeLanguage,
|
||||
changeMarkAllAsReadConfirmation,
|
||||
changeScrollMarks,
|
||||
changeScrollSpeed,
|
||||
changeSharingSetting,
|
||||
changeShowRead,
|
||||
} from "app/slices/user"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import { SharingSettings } from "app/types"
|
||||
import { locales } from "i18n"
|
||||
@@ -11,6 +20,9 @@ export function DisplaySettings() {
|
||||
const scrollSpeed = useAppSelector(state => state.user.settings?.scrollSpeed)
|
||||
const showRead = useAppSelector(state => state.user.settings?.showRead)
|
||||
const scrollMarks = useAppSelector(state => state.user.settings?.scrollMarks)
|
||||
const alwaysScrollToEntry = useAppSelector(state => state.user.settings?.alwaysScrollToEntry)
|
||||
const markAllAsReadConfirmation = useAppSelector(state => state.user.settings?.markAllAsReadConfirmation)
|
||||
const customContextMenu = useAppSelector(state => state.user.settings?.customContextMenu)
|
||||
const sharingSettings = useAppSelector(state => state.user.settings?.sharingSettings)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
@@ -32,6 +44,12 @@ export function DisplaySettings() {
|
||||
onChange={e => dispatch(changeScrollSpeed(e.currentTarget.checked))}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label={<Trans>Always scroll selected entry to the top of the page, even if it fits entirely on screen</Trans>}
|
||||
checked={alwaysScrollToEntry}
|
||||
onChange={e => dispatch(changeAlwaysScrollToEntry(e.currentTarget.checked))}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label={<Trans>Show feeds and categories with no unread entries</Trans>}
|
||||
checked={showRead}
|
||||
@@ -44,6 +62,18 @@ export function DisplaySettings() {
|
||||
onChange={e => dispatch(changeScrollMarks(e.currentTarget.checked))}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label={<Trans>Show confirmation when marking all entries as read</Trans>}
|
||||
checked={markAllAsReadConfirmation}
|
||||
onChange={e => dispatch(changeMarkAllAsReadConfirmation(e.currentTarget.checked))}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label={<Trans>Show CommaFeed's own context menu on right click</Trans>}
|
||||
checked={customContextMenu}
|
||||
onChange={e => dispatch(changeCustomContextMenu(e.currentTarget.checked))}
|
||||
/>
|
||||
|
||||
<Divider label={<Trans>Sharing sites</Trans>} labelPosition="center" />
|
||||
|
||||
<SimpleGrid cols={2}>
|
||||
|
||||
@@ -77,10 +77,7 @@ export function ProfileSettings() {
|
||||
|
||||
<form onSubmit={form.onSubmit(saveProfile.execute)}>
|
||||
<Stack>
|
||||
<Input.Wrapper label={<Trans>User name</Trans>}>
|
||||
<Box>{profile?.name}</Box>
|
||||
</Input.Wrapper>
|
||||
|
||||
<TextInput label={<Trans>User name</Trans>} readOnly value={profile?.name} />
|
||||
<TextInput label={<Trans>API key</Trans>} readOnly value={profile?.apiKey} />
|
||||
|
||||
<Input.Wrapper
|
||||
@@ -98,6 +95,22 @@ export function ProfileSettings() {
|
||||
</Box>
|
||||
</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 />
|
||||
|
||||
<PasswordInput
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Badge, createStyles } from "@mantine/core"
|
||||
import { Badge, createStyles, Tooltip } from "@mantine/core"
|
||||
|
||||
const useStyles = createStyles(() => ({
|
||||
badge: {
|
||||
@@ -13,6 +13,10 @@ export function UnreadCount(props: { unreadCount: number }) {
|
||||
|
||||
if (props.unreadCount <= 0) return null
|
||||
|
||||
const count = props.unreadCount >= 1000 ? "999+" : props.unreadCount
|
||||
return <Badge className={classes.badge}>{count}</Badge>
|
||||
const count = props.unreadCount >= 10000 ? "10k+" : props.unreadCount
|
||||
return (
|
||||
<Tooltip label={props.unreadCount} disabled={props.unreadCount === count}>
|
||||
<Badge className={classes.badge}>{count}</Badge>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
9
commafeed-client/src/hooks/useActionButton.ts
Normal file
9
commafeed-client/src/hooks/useActionButton.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { useMantineTheme } from "@mantine/core"
|
||||
import { useMobile } from "hooks/useMobile"
|
||||
|
||||
export const useActionButton = () => {
|
||||
const theme = useMantineTheme()
|
||||
const mobile = useMobile(theme.breakpoints.xl)
|
||||
const spacing = mobile ? 14 : 0
|
||||
return { mobile, spacing }
|
||||
}
|
||||
@@ -1,9 +1,64 @@
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
export const useBrowserExtension = () => {
|
||||
// the extension will set the "browser-extension-installed" attribute on the root element
|
||||
const [browserExtensionVersion, setBrowserExtensionVersion] = useState(
|
||||
document.documentElement.getAttribute("browser-extension-installed")
|
||||
)
|
||||
|
||||
// monitor the attribute on the root element as it may change after the page was loaded
|
||||
useEffect(() => {
|
||||
const observer = new MutationObserver(mutations => {
|
||||
mutations.forEach(mutation => {
|
||||
if (mutation.type === "attributes") {
|
||||
const element = mutation.target as Element
|
||||
const version = element.getAttribute("browser-extension-installed")
|
||||
if (version) setBrowserExtensionVersion(version)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
})
|
||||
|
||||
return () => observer.disconnect()
|
||||
}, [])
|
||||
|
||||
// when not in an iframe, window.parent is a reference to window
|
||||
const isBrowserExtension = window.parent !== window
|
||||
const isBrowserExtensionPopup = window.parent !== window
|
||||
const isBrowserExtensionInstalled = isBrowserExtensionPopup || !!browserExtensionVersion
|
||||
const isBrowserExtensionInstallable = !isBrowserExtensionPopup
|
||||
|
||||
const openSettingsPage = () => window.parent.postMessage("open-settings-page", "*")
|
||||
const openAppInNewTab = () => window.parent.postMessage("open-app-in-new-tab", "*")
|
||||
const w = isBrowserExtensionPopup ? window.parent : window
|
||||
const openSettingsPage = () => w.postMessage("open-settings-page", "*")
|
||||
const openAppInNewTab = () => w.postMessage("open-app-in-new-tab", "*")
|
||||
const openLinkInBackgroundTab = (url: string) => {
|
||||
if (isBrowserExtensionInstalled) {
|
||||
w.postMessage(`open-link-in-background-tab:${url}`, "*")
|
||||
} else {
|
||||
// fallback to ctrl+click simulation
|
||||
const a = document.createElement("a")
|
||||
a.href = url
|
||||
a.rel = "noreferrer"
|
||||
a.dispatchEvent(
|
||||
new MouseEvent("click", {
|
||||
ctrlKey: true,
|
||||
metaKey: true,
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
const setBadgeUnreadCount = (count: number) => w.postMessage(`set-badge-unread-count:${count}`, "*")
|
||||
|
||||
return { isBrowserExtension, openSettingsPage, openAppInNewTab }
|
||||
return {
|
||||
browserExtensionVersion,
|
||||
isBrowserExtensionInstallable,
|
||||
isBrowserExtensionInstalled,
|
||||
isBrowserExtensionPopup,
|
||||
openSettingsPage,
|
||||
openAppInNewTab,
|
||||
openLinkInBackgroundTab,
|
||||
setBadgeUnreadCount,
|
||||
}
|
||||
}
|
||||
|
||||
7
commafeed-client/src/hooks/useMobile.ts
Normal file
7
commafeed-client/src/hooks/useMobile.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { useMediaQuery } from "@mantine/hooks"
|
||||
import { Constants } from "app/constants"
|
||||
|
||||
export const useMobile = (breakpoint: string = Constants.layout.mobileBreakpoint) =>
|
||||
!useMediaQuery(`(min-width: ${breakpoint})`, undefined, {
|
||||
getInitialValueInEffect: false,
|
||||
})
|
||||
@@ -1,3 +1,4 @@
|
||||
import { setWebSocketConnected } from "app/slices/server"
|
||||
import { reloadTree } from "app/slices/tree"
|
||||
import { useAppDispatch } from "app/store"
|
||||
import { useEffect } from "react"
|
||||
@@ -11,7 +12,14 @@ export const useWebSocket = () => {
|
||||
const wsProtocol = currentUrl.protocol === "http:" ? "ws" : "wss"
|
||||
const wsUrl = `${wsProtocol}://${currentUrl.hostname}:${currentUrl.port}/ws`
|
||||
|
||||
const ws = new WebsocketHeartbeatJs({ url: wsUrl, pingMsg: "ping" })
|
||||
const ws = new WebsocketHeartbeatJs({
|
||||
url: wsUrl,
|
||||
pingMsg: "ping",
|
||||
// ping interval, just under a minute to prevent firewalls from closing idle connections
|
||||
pingTimeout: 55000,
|
||||
})
|
||||
ws.onopen = () => dispatch(setWebSocketConnected(true))
|
||||
ws.onclose = () => dispatch(setWebSocketConnected(false))
|
||||
ws.onmessage = event => {
|
||||
const { data } = event
|
||||
if (typeof data === "string") {
|
||||
|
||||
@@ -71,6 +71,10 @@ msgstr "إداري"
|
||||
msgid "All"
|
||||
msgstr "الكل"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "An email has been sent if this address was registered. Check your inbox."
|
||||
msgstr "تم إرسال بريد إلكتروني إذا تم تسجيل هذا العنوان. "
|
||||
@@ -83,6 +87,10 @@ msgstr "ملف opml هو ملف XML يحتوي على عناوين URL للتغ
|
||||
msgid "Analyze feed"
|
||||
msgstr "تحليل التغذية"
|
||||
|
||||
#: src/components/AnnouncementDialog.tsx
|
||||
msgid "Announcement"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "API key"
|
||||
msgstr "مفتاح API"
|
||||
@@ -127,9 +135,13 @@ msgstr "العودة"
|
||||
msgid "Back to log in"
|
||||
msgstr "العودة لتسجيل الدخول"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Browser extension required for Chrome"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Browser extentions"
|
||||
msgstr "ملحقات المستعرض"
|
||||
msgid "Browser extention"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
@@ -163,13 +175,21 @@ msgstr "سيؤدي تغيير كلمة المرور إلى إنشاء مفتاح
|
||||
msgid "Check that the feed is working"
|
||||
msgstr "تأكد من عمل الخلاصة"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed next unread item"
|
||||
msgstr "CommaFeed التالي العنصر غير المقروء"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed version {version} ({revision})"
|
||||
msgstr "إصدار CommaFeed {الإصدار} ({مراجعة})"
|
||||
msgid "CommaFeed version {version} ({revision})."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Compact"
|
||||
@@ -327,6 +347,14 @@ msgstr "موجز URL"
|
||||
msgid "Fetch all my feeds now"
|
||||
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
|
||||
msgid "file is required"
|
||||
msgstr "الملف مطلوب"
|
||||
@@ -518,6 +546,7 @@ msgid "Newest first"
|
||||
msgstr "الأحدث أولاً"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Next"
|
||||
msgstr "التالي"
|
||||
|
||||
@@ -627,6 +656,10 @@ msgstr "كلمات المرور غير متطابقة"
|
||||
msgid "Position"
|
||||
msgstr "المنـصب"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Previous"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Profile"
|
||||
msgstr "الملف الشخصي"
|
||||
@@ -648,6 +681,7 @@ msgstr "تم إغلاق التسجيلات في مثيل CommaFeed هذا"
|
||||
msgid "REST API"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Right click"
|
||||
msgstr ""
|
||||
@@ -699,11 +733,20 @@ msgstr "مشاركة"
|
||||
msgid "Sharing sites"
|
||||
msgstr "مشاركة المواقع"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Shift"
|
||||
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
|
||||
msgid "Show entry menu (desktop)"
|
||||
msgstr ""
|
||||
@@ -720,6 +763,10 @@ msgstr "إظهار موجز ويب والفئات التي لا تحتوي عل
|
||||
msgid "Show keyboard shortcut help"
|
||||
msgstr "إظهار تعليمات اختصار لوحة المفاتيح"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show native menu (desktop)"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
@@ -793,6 +840,14 @@ msgstr "الموضوع"
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "تبديل قراءة حالة الإدخال الحالي"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle sidebar"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle starred status of current entry"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||
msgstr "جرب CommaFeed باستخدام الحساب التجريبي: تجريبي / تجريبي"
|
||||
|
||||
@@ -71,6 +71,10 @@ msgstr "Administrador"
|
||||
msgid "All"
|
||||
msgstr "Tot"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "An email has been sent if this address was registered. Check your inbox."
|
||||
msgstr "S'ha enviat un correu electrònic si aquesta adreça estava registrada. "
|
||||
@@ -83,6 +87,10 @@ msgstr "Un fitxer opml és un fitxer XML que conté URL i categories de canals.
|
||||
msgid "Analyze feed"
|
||||
msgstr "Analitzar el feed"
|
||||
|
||||
#: src/components/AnnouncementDialog.tsx
|
||||
msgid "Announcement"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "API key"
|
||||
msgstr "clau API"
|
||||
@@ -127,9 +135,13 @@ msgstr "Enrere"
|
||||
msgid "Back to log in"
|
||||
msgstr "Tornar a iniciar sessió"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Browser extension required for Chrome"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Browser extentions"
|
||||
msgstr "Extensions del navegador"
|
||||
msgid "Browser extention"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
@@ -163,13 +175,21 @@ msgstr "Canviar la contrasenya generarà una nova clau d'API"
|
||||
msgid "Check that the feed is working"
|
||||
msgstr "Comproveu que el canal funciona"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed next unread item"
|
||||
msgstr "CommaFeed següent element no llegit"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed version {version} ({revision})"
|
||||
msgstr "Versió CommaFeed {versió} ({revisió})"
|
||||
msgid "CommaFeed version {version} ({revision})."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Compact"
|
||||
@@ -327,6 +347,14 @@ msgstr "URL del canal"
|
||||
msgid "Fetch all my feeds now"
|
||||
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
|
||||
msgid "file is required"
|
||||
msgstr "el fitxer és necessari"
|
||||
@@ -518,6 +546,7 @@ msgid "Newest first"
|
||||
msgstr "El més nou primer"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Next"
|
||||
msgstr "Següent"
|
||||
|
||||
@@ -627,6 +656,10 @@ msgstr "Les contrasenyes no coincideixen"
|
||||
msgid "Position"
|
||||
msgstr "Posició"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Previous"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Profile"
|
||||
msgstr "Perfil"
|
||||
@@ -648,6 +681,7 @@ msgstr "Els registres estan tancats en aquesta instància de CommaFeed"
|
||||
msgid "REST API"
|
||||
msgstr "API REST"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Right click"
|
||||
msgstr ""
|
||||
@@ -699,11 +733,20 @@ msgstr "Comparteix"
|
||||
msgid "Sharing sites"
|
||||
msgstr "Compartir llocs"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Shift"
|
||||
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
|
||||
msgid "Show entry menu (desktop)"
|
||||
msgstr ""
|
||||
@@ -720,6 +763,10 @@ msgstr "Mostra feeds i categories sense entrades no llegides"
|
||||
msgid "Show keyboard shortcut help"
|
||||
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/WelcomePage.tsx
|
||||
@@ -793,6 +840,14 @@ msgstr "Tema"
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "Canvia l'estat de lectura de l'entrada actual"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle sidebar"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle starred status of current entry"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||
msgstr "Proveu CommaFeed amb el compte de demostració: demo/demo"
|
||||
|
||||
@@ -71,6 +71,10 @@ msgstr "Správce"
|
||||
msgid "All"
|
||||
msgstr "Všechny"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "An email has been sent if this address was registered. Check your inbox."
|
||||
msgstr "Pokud byla tato adresa zaregistrována, byl odeslán e-mail. "
|
||||
@@ -83,6 +87,10 @@ msgstr "Soubor opml je soubor XML obsahující adresy URL a kategorie zdrojů. "
|
||||
msgid "Analyze feed"
|
||||
msgstr "Analyzujte krmivo"
|
||||
|
||||
#: src/components/AnnouncementDialog.tsx
|
||||
msgid "Announcement"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "API key"
|
||||
msgstr "Klíč API"
|
||||
@@ -127,9 +135,13 @@ msgstr "Zpět"
|
||||
msgid "Back to log in"
|
||||
msgstr "Zpět k přihlášení"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Browser extension required for Chrome"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Browser extentions"
|
||||
msgstr "Rozšíření prohlížeče"
|
||||
msgid "Browser extention"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
@@ -163,13 +175,21 @@ msgstr "Změna hesla vygeneruje nový klíč API"
|
||||
msgid "Check that the feed is working"
|
||||
msgstr "Zkontrolujte, zda zdroj funguje"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed next unread item"
|
||||
msgstr "CommaFeed další nepřečtená položka"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed version {version} ({revision})"
|
||||
msgstr "CommaFeed verze {version} ({revision})"
|
||||
msgid "CommaFeed version {version} ({revision})."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Compact"
|
||||
@@ -327,6 +347,14 @@ msgstr "URL zdroje"
|
||||
msgid "Fetch all my feeds now"
|
||||
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
|
||||
msgid "file is required"
|
||||
msgstr ""
|
||||
@@ -518,6 +546,7 @@ msgid "Newest first"
|
||||
msgstr "Nejnovější jako první"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Next"
|
||||
msgstr "Další"
|
||||
|
||||
@@ -627,6 +656,10 @@ msgstr "Hesla se neshodují"
|
||||
msgid "Position"
|
||||
msgstr "Pozice"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Previous"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Profile"
|
||||
msgstr "Profil"
|
||||
@@ -648,6 +681,7 @@ msgstr "V této instanci CommaFeed jsou registrace uzavřeny"
|
||||
msgid "REST API"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Right click"
|
||||
msgstr ""
|
||||
@@ -699,11 +733,20 @@ msgstr "Sdílejte"
|
||||
msgid "Sharing sites"
|
||||
msgstr "Stránky pro sdílení"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Shift"
|
||||
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
|
||||
msgid "Show entry menu (desktop)"
|
||||
msgstr ""
|
||||
@@ -720,6 +763,10 @@ msgstr "Zobrazit kanály a kategorie bez nepřečtených položek"
|
||||
msgid "Show keyboard shortcut help"
|
||||
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/WelcomePage.tsx
|
||||
@@ -793,6 +840,14 @@ msgstr "Téma"
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "Přepne stav čtení aktuálního záznamu"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle sidebar"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle starred status of current entry"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||
msgstr "Vyzkoušejte CommaFeed s demo účtem: demo/demo"
|
||||
|
||||
@@ -71,6 +71,10 @@ msgstr "Gweinyddol"
|
||||
msgid "All"
|
||||
msgstr "Pawb"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "An email has been sent if this address was registered. Check your inbox."
|
||||
msgstr "Mae e-bost wedi'i anfon os oedd y cyfeiriad hwn wedi'i gofrestru. "
|
||||
@@ -83,6 +87,10 @@ msgstr "Mae ffeil opml yn ffeil XML sy'n cynnwys URLs porthiant a chategorïau.
|
||||
msgid "Analyze feed"
|
||||
msgstr "Dadansoddi porthiant"
|
||||
|
||||
#: src/components/AnnouncementDialog.tsx
|
||||
msgid "Announcement"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "API key"
|
||||
msgstr "Allwedd API"
|
||||
@@ -127,9 +135,13 @@ msgstr "Yn ôl"
|
||||
msgid "Back to log in"
|
||||
msgstr "Yn ôl i fewngofnodi"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Browser extension required for Chrome"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Browser extentions"
|
||||
msgstr "Estyniadau porwr"
|
||||
msgid "Browser extention"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
@@ -163,13 +175,21 @@ msgstr "Bydd newid cyfrinair yn cynhyrchu allwedd API newydd"
|
||||
msgid "Check that the feed is working"
|
||||
msgstr "Gwiriwch fod y porthiant yn gweithio"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed next unread item"
|
||||
msgstr "CommaFeed eitem nesaf heb ei darllen"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed version {version} ({revision})"
|
||||
msgstr "Fersiwn ComaFeed {fersiwn} ({ adolygu})"
|
||||
msgid "CommaFeed version {version} ({revision})."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Compact"
|
||||
@@ -327,6 +347,14 @@ msgstr "URL porthiant"
|
||||
msgid "Fetch all my feeds now"
|
||||
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
|
||||
msgid "file is required"
|
||||
msgstr "mae angen y ffeil"
|
||||
@@ -518,6 +546,7 @@ msgid "Newest first"
|
||||
msgstr "Y diweddaraf yn gyntaf"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Next"
|
||||
msgstr "Nesaf"
|
||||
|
||||
@@ -627,6 +656,10 @@ msgstr "Nid yw cyfrineiriau yn cyfateb"
|
||||
msgid "Position"
|
||||
msgstr "Swydd"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Previous"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Profile"
|
||||
msgstr "Proffil"
|
||||
@@ -648,6 +681,7 @@ msgstr "Mae cofrestriadau ar gau ar yr achos CommaFeed hwn"
|
||||
msgid "REST API"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Right click"
|
||||
msgstr ""
|
||||
@@ -699,11 +733,20 @@ msgstr "Rhannu"
|
||||
msgid "Sharing sites"
|
||||
msgstr "Rhannu gwefannau"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Shift"
|
||||
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
|
||||
msgid "Show entry menu (desktop)"
|
||||
msgstr ""
|
||||
@@ -720,6 +763,10 @@ msgstr "Dangos ffrydiau a chategorïau heb unrhyw gofnodion heb eu darllen"
|
||||
msgid "Show keyboard shortcut help"
|
||||
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/WelcomePage.tsx
|
||||
@@ -793,6 +840,14 @@ msgstr "Thema"
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "Toglo statws darllen y cofnod cyfredol"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle sidebar"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle starred status of current entry"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||
msgstr "Rhowch gynnig ar CommaFeed gyda'r cyfrif demo: demo / demo"
|
||||
|
||||
@@ -71,6 +71,10 @@ msgstr ""
|
||||
msgid "All"
|
||||
msgstr "Alle"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "An email has been sent if this address was registered. Check your inbox."
|
||||
msgstr "Der er sendt en e-mail, hvis denne adresse var registreret. "
|
||||
@@ -83,6 +87,10 @@ msgstr "En opml-fil er en XML-fil, der indeholder feed-URL'er og kategorier. "
|
||||
msgid "Analyze feed"
|
||||
msgstr "Analyser foder"
|
||||
|
||||
#: src/components/AnnouncementDialog.tsx
|
||||
msgid "Announcement"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "API key"
|
||||
msgstr "API-nøgle"
|
||||
@@ -127,9 +135,13 @@ msgstr "Tilbage"
|
||||
msgid "Back to log in"
|
||||
msgstr "Tilbage for at logge ind"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Browser extension required for Chrome"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Browser extentions"
|
||||
msgstr "Browserudvidelser"
|
||||
msgid "Browser extention"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
@@ -163,12 +175,20 @@ msgstr "Ændring af adgangskode vil generere en ny API-nøgle"
|
||||
msgid "Check that the feed is working"
|
||||
msgstr "Tjek, at foderet virker"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed next unread item"
|
||||
msgstr "CommaFeed næste ulæste element"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed version {version} ({revision})"
|
||||
msgid "CommaFeed version {version} ({revision})."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
@@ -327,6 +347,14 @@ msgstr ""
|
||||
msgid "Fetch all my feeds now"
|
||||
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
|
||||
msgid "file is required"
|
||||
msgstr "fil er påkrævet"
|
||||
@@ -518,6 +546,7 @@ msgid "Newest first"
|
||||
msgstr "Nyeste først"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Next"
|
||||
msgstr "Næste"
|
||||
|
||||
@@ -627,6 +656,10 @@ msgstr "Adgangskoder stemmer ikke overens"
|
||||
msgid "Position"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Previous"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Profile"
|
||||
msgstr "Profil"
|
||||
@@ -648,6 +681,7 @@ msgstr "Registreringer er lukket på denne CommaFeed-instans"
|
||||
msgid "REST API"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Right click"
|
||||
msgstr ""
|
||||
@@ -699,11 +733,20 @@ msgstr "Del"
|
||||
msgid "Sharing sites"
|
||||
msgstr "Delingssider"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Shift"
|
||||
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
|
||||
msgid "Show entry menu (desktop)"
|
||||
msgstr ""
|
||||
@@ -720,6 +763,10 @@ msgstr "Vis feeds og kategorier uden ulæste poster"
|
||||
msgid "Show keyboard shortcut help"
|
||||
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/WelcomePage.tsx
|
||||
@@ -793,6 +840,14 @@ msgstr "Tema"
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "Skift læsestatus for den aktuelle post"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle sidebar"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle starred status of current entry"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||
msgstr "Prøv CommaFeed med demokontoen: demo/demo"
|
||||
|
||||
@@ -19,11 +19,11 @@ msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
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
|
||||
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
|
||||
msgid "<0>Have an account?</0><1>Log in!</1>"
|
||||
@@ -35,12 +35,12 @@ msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
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/pages/app/AboutPage.tsx
|
||||
msgid "About"
|
||||
msgstr "Ungefähr"
|
||||
msgstr "Über"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Actions"
|
||||
@@ -71,18 +71,26 @@ msgstr "Verwaltung"
|
||||
msgid "All"
|
||||
msgstr "Alle"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
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
|
||||
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
|
||||
msgid "Analyze feed"
|
||||
msgstr "Feed analysieren"
|
||||
|
||||
#: src/components/AnnouncementDialog.tsx
|
||||
msgid "Announcement"
|
||||
msgstr "Ankündigung"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "API key"
|
||||
msgstr "API-Schlüssel"
|
||||
@@ -97,7 +105,7 @@ msgstr "Sind Sie sicher, dass Sie Benutzer <0>{userName}</0> löschen möchten?"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
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
|
||||
msgid "Are you sure you want to mark all entries of <0>{sourceLabel}</0> as read?"
|
||||
@@ -113,7 +121,7 @@ msgstr "Sind Sie sicher, dass Sie <0>{feedName}</0> abbestellen möchten?"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Asc"
|
||||
msgstr "Asz"
|
||||
msgstr "Aufsteigend"
|
||||
|
||||
#: 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."
|
||||
@@ -125,11 +133,15 @@ msgstr "Zurück"
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "Back to log in"
|
||||
msgstr "Zurück zum Anmelden"
|
||||
msgstr "Zurück zum Login"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Browser extension required for Chrome"
|
||||
msgstr "Browser-Erweiterung für Chrome benötigt"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Browser extentions"
|
||||
msgstr "Browsererweiterungen"
|
||||
msgid "Browser extention"
|
||||
msgstr "Browser-Erweiterung"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
@@ -163,13 +175,21 @@ msgstr "Das Ändern des Passworts generiert einen neuen API-Schlüssel"
|
||||
msgid "Check that the feed is working"
|
||||
msgstr "Überprüfen Sie, ob der Feed funktioniert"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
msgstr "CommaFeed ist kompatibel zur Fever API. Benutzen Sie folgende URL in Ihrem Fever-kompatiblen Mobilclient. Der Benutzername ist Ihr User Name, das Passwort ist der API-Schlüssel."
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed next unread item"
|
||||
msgstr "CommaFeed nächstes ungelesenes Element"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed version {version} ({revision})"
|
||||
msgstr "CommaFeed-Version {Version} ({Revision})"
|
||||
msgid "CommaFeed version {version} ({revision})."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Compact"
|
||||
@@ -252,7 +272,7 @@ msgstr "Anzeige"
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/app/DonatePage.tsx
|
||||
msgid "Donate"
|
||||
msgstr ""
|
||||
msgstr "Spenden"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Download"
|
||||
@@ -311,7 +331,7 @@ msgstr "Exportieren Sie Ihre Abonnements und Kategorien als OPML-Datei, die in a
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
msgid "Extension options"
|
||||
msgstr ""
|
||||
msgstr "Erweiterungsoptionen"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Feed name"
|
||||
@@ -325,6 +345,14 @@ msgstr "Feed-URL"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
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 ""
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
@@ -357,7 +385,7 @@ msgstr "Generierte Feed-URL"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Go to {0}"
|
||||
msgstr ""
|
||||
msgstr "Gehe zu {0}"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Go to the All view"
|
||||
@@ -369,7 +397,7 @@ msgstr "Gehen Sie zur API-Dokumentation."
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Goodies"
|
||||
msgstr "Gutes"
|
||||
msgstr "Goodies"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Id"
|
||||
@@ -399,7 +427,7 @@ msgstr "Ungelesen lassen"
|
||||
#: src/components/content/FeedEntries.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Keyboard shortcuts"
|
||||
msgstr "Tastenkürzel"
|
||||
msgstr "Tastaturkürzel"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Language"
|
||||
@@ -451,7 +479,7 @@ msgstr "Abmelden"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Long press"
|
||||
msgstr ""
|
||||
msgstr "Langer Tastendruck"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
@@ -483,7 +511,7 @@ msgstr "Metriken"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Middle click"
|
||||
msgstr ""
|
||||
msgstr "Mittelklick"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Move the page down"
|
||||
@@ -496,7 +524,7 @@ msgstr "Bewege die Seite nach oben"
|
||||
#: src/components/RelativeDate.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "N/A"
|
||||
msgstr "n. z"
|
||||
msgstr "n.v."
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
@@ -518,6 +546,7 @@ msgid "Newest first"
|
||||
msgstr "Neueste zuerst"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Next"
|
||||
msgstr "Weiter"
|
||||
|
||||
@@ -563,11 +592,11 @@ msgstr "Link öffnen"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Open link in new background tab"
|
||||
msgstr ""
|
||||
msgstr "Link in neuem Tab im Hintergrund öffnen"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Open link in new tab"
|
||||
msgstr ""
|
||||
msgstr "Link in neuem Tab öffnen"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open next entry"
|
||||
@@ -600,11 +629,11 @@ msgstr "Bestellung"
|
||||
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
msgid "Parent"
|
||||
msgstr "Elternteil"
|
||||
msgstr "Übergeordnet"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Parent Category"
|
||||
msgstr "Elternkategorie"
|
||||
msgstr "Übergeordnete Kategorie"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
@@ -625,7 +654,11 @@ msgstr "Passwörter stimmen nicht überein"
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Position"
|
||||
msgstr "Stellung"
|
||||
msgstr "Position"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Previous"
|
||||
msgstr "Vorheriges"
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Profile"
|
||||
@@ -648,9 +681,10 @@ msgstr "Registrierungen sind für diese CommaFeed-Instanz geschlossen"
|
||||
msgid "REST API"
|
||||
msgstr "REST-API"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Right click"
|
||||
msgstr ""
|
||||
msgstr "Rechtsklick"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
@@ -662,7 +696,7 @@ msgstr "Speichern"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
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
|
||||
@@ -699,11 +733,20 @@ msgstr "Teilen"
|
||||
msgid "Sharing sites"
|
||||
msgstr "Seiten teilen"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Shift"
|
||||
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
|
||||
msgid "Show entry menu (desktop)"
|
||||
msgstr ""
|
||||
@@ -720,6 +763,10 @@ msgstr "Feeds und Kategorien ohne ungelesene Einträge anzeigen"
|
||||
msgid "Show keyboard shortcut help"
|
||||
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/WelcomePage.tsx
|
||||
@@ -770,12 +817,12 @@ msgstr ""
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
msgid "Switch to dark theme"
|
||||
msgstr "Zum dunklen Design wechseln"
|
||||
msgstr "Zum Darkmode wechseln"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
msgid "Switch to light theme"
|
||||
msgstr "Wechseln Sie zum Lichtdesign"
|
||||
msgstr "Zum Lightmode wechseln"
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Tags"
|
||||
@@ -793,13 +840,21 @@ msgstr "Thema"
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "Lesestatus des aktuellen Eintrags umschalten"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle sidebar"
|
||||
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
|
||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||
msgstr "Testen Sie CommaFeed mit dem Demokonto: demo/demo"
|
||||
|
||||
#: src/pages/WelcomePage.tsx
|
||||
msgid "Try the demo!"
|
||||
msgstr ""
|
||||
msgstr "Testen Sie die Demo!"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Unread"
|
||||
@@ -838,4 +893,4 @@ msgstr "Sie haben noch keine Abonnements. "
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Your feeds have been queued for refresh."
|
||||
msgstr ""
|
||||
msgstr "Ihr Feed wurde für die Aktualisierung eingereiht."
|
||||
|
||||
@@ -71,6 +71,10 @@ msgstr "Admin"
|
||||
msgid "All"
|
||||
msgstr "All"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgstr "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "An email has been sent if this address was registered. Check your inbox."
|
||||
msgstr "An email has been sent if this address was registered. Check your inbox."
|
||||
@@ -83,6 +87,10 @@ msgstr "An opml file is an XML file containing feed URLs and categories. You can
|
||||
msgid "Analyze feed"
|
||||
msgstr "Analyze feed"
|
||||
|
||||
#: src/components/AnnouncementDialog.tsx
|
||||
msgid "Announcement"
|
||||
msgstr "Announcement"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "API key"
|
||||
msgstr "API key"
|
||||
@@ -127,9 +135,13 @@ msgstr "Back"
|
||||
msgid "Back to log in"
|
||||
msgstr "Back to log in"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Browser extension required for Chrome"
|
||||
msgstr "Browser extension required for Chrome"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Browser extentions"
|
||||
msgstr "Browser extentions"
|
||||
msgid "Browser extention"
|
||||
msgstr "Browser extention"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
@@ -163,13 +175,21 @@ msgstr "Changing password will generate a new API key"
|
||||
msgid "Check that the feed is working"
|
||||
msgstr "Check that the feed is working"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
msgstr "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed next unread item"
|
||||
msgstr "CommaFeed next unread item"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed version {version} ({revision})"
|
||||
msgstr "CommaFeed version {version} ({revision})"
|
||||
msgid "CommaFeed version {version} ({revision})."
|
||||
msgstr "CommaFeed version {version} ({revision})."
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Compact"
|
||||
@@ -327,6 +347,14 @@ msgstr "Feed URL"
|
||||
msgid "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
|
||||
msgid "file is required"
|
||||
msgstr "file is required"
|
||||
@@ -518,6 +546,7 @@ msgid "Newest first"
|
||||
msgstr "Newest first"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Next"
|
||||
msgstr "Next"
|
||||
|
||||
@@ -627,6 +656,10 @@ msgstr "Passwords do not match"
|
||||
msgid "Position"
|
||||
msgstr "Position"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Previous"
|
||||
msgstr "Previous"
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Profile"
|
||||
msgstr "Profile"
|
||||
@@ -648,6 +681,7 @@ msgstr "Registrations are closed on this CommaFeed instance"
|
||||
msgid "REST API"
|
||||
msgstr "REST API"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Right click"
|
||||
msgstr "Right click"
|
||||
@@ -699,11 +733,20 @@ msgstr "Share"
|
||||
msgid "Sharing sites"
|
||||
msgstr "Sharing sites"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "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
|
||||
msgid "Show entry menu (desktop)"
|
||||
msgstr "Show entry menu (desktop)"
|
||||
@@ -720,6 +763,10 @@ msgstr "Show feeds and categories with no unread entries"
|
||||
msgid "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/WelcomePage.tsx
|
||||
@@ -793,6 +840,14 @@ msgstr "Theme"
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "Toggle read status of current entry"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "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
|
||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||
msgstr "Try out CommaFeed with the demo account: demo/demo"
|
||||
|
||||
@@ -71,6 +71,10 @@ msgstr "Administrador"
|
||||
msgid "All"
|
||||
msgstr "Todo"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "An email has been sent if this address was registered. Check your inbox."
|
||||
msgstr "Se ha enviado un correo electrónico si se registró esta dirección. "
|
||||
@@ -83,6 +87,10 @@ msgstr "Un archivo opml es un archivo XML que contiene categorías y direcciones
|
||||
msgid "Analyze feed"
|
||||
msgstr "Analizar alimentación"
|
||||
|
||||
#: src/components/AnnouncementDialog.tsx
|
||||
msgid "Announcement"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "API key"
|
||||
msgstr "clave API"
|
||||
@@ -127,9 +135,13 @@ msgstr "Atrás"
|
||||
msgid "Back to log in"
|
||||
msgstr "Volver a iniciar sesión"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Browser extension required for Chrome"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Browser extentions"
|
||||
msgstr "Extensiones del navegador"
|
||||
msgid "Browser extention"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
@@ -163,13 +175,21 @@ msgstr "Cambiar la contraseña generará una nueva clave API"
|
||||
msgid "Check that the feed is working"
|
||||
msgstr "Compruebe que el feed funciona"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed next unread item"
|
||||
msgstr "CommaFeed siguiente elemento no leído"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed version {version} ({revision})"
|
||||
msgstr "versión de CommaFeed {versión} ({revisión})"
|
||||
msgid "CommaFeed version {version} ({revision})."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Compact"
|
||||
@@ -327,6 +347,14 @@ msgstr "URL de fuente"
|
||||
msgid "Fetch all my feeds now"
|
||||
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
|
||||
msgid "file is required"
|
||||
msgstr "archivo requerido"
|
||||
@@ -518,6 +546,7 @@ msgid "Newest first"
|
||||
msgstr "más reciente primero"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Next"
|
||||
msgstr "Siguiente"
|
||||
|
||||
@@ -627,6 +656,10 @@ msgstr "Las contraseñas no coinciden"
|
||||
msgid "Position"
|
||||
msgstr "Posición"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Previous"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Profile"
|
||||
msgstr "Perfil"
|
||||
@@ -648,6 +681,7 @@ msgstr "Los registros están cerrados en esta instancia de CommaFeed"
|
||||
msgid "REST API"
|
||||
msgstr "API REST"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Right click"
|
||||
msgstr ""
|
||||
@@ -699,11 +733,20 @@ msgstr "Compartir"
|
||||
msgid "Sharing sites"
|
||||
msgstr "Compartir sitios"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Shift"
|
||||
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
|
||||
msgid "Show entry menu (desktop)"
|
||||
msgstr ""
|
||||
@@ -720,6 +763,10 @@ msgstr "Mostrar feeds y categorías sin entradas no leídas"
|
||||
msgid "Show keyboard shortcut help"
|
||||
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/WelcomePage.tsx
|
||||
@@ -793,6 +840,14 @@ msgstr "Tema"
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "Alternar estado de lectura de la entrada actual"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle sidebar"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle starred status of current entry"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||
msgstr "Pruebe CommaFeed con la cuenta demo: demo/demo"
|
||||
|
||||
@@ -71,6 +71,10 @@ msgstr "مدیر"
|
||||
msgid "All"
|
||||
msgstr "همه"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "An email has been sent if this address was registered. Check your inbox."
|
||||
msgstr "اگر این آدرس ثبت شده باشد ایمیل ارسال شده است. "
|
||||
@@ -83,6 +87,10 @@ msgstr "یک فایل opml یک فایل XML است که حاوی آدرسه
|
||||
msgid "Analyze feed"
|
||||
msgstr "خوراک را تجزیه و تحلیل کنید"
|
||||
|
||||
#: src/components/AnnouncementDialog.tsx
|
||||
msgid "Announcement"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "API key"
|
||||
msgstr "کلید API"
|
||||
@@ -127,9 +135,13 @@ msgstr "برگشت"
|
||||
msgid "Back to log in"
|
||||
msgstr "بازگشت برای ورود به سیستم"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Browser extension required for Chrome"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Browser extentions"
|
||||
msgstr "گسترش مرورگر"
|
||||
msgid "Browser extention"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
@@ -163,13 +175,21 @@ msgstr "تغییر رمز عبور یک کلید API جدید ایجاد می ک
|
||||
msgid "Check that the feed is working"
|
||||
msgstr "بررسی کنید که خوراک کار می کند"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed next unread item"
|
||||
msgstr "مورد خوانده نشده بعدی CommaFeed"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed version {version} ({revision})"
|
||||
msgstr "نسخه {نسخه} CommaFeed ({نسخه})"
|
||||
msgid "CommaFeed version {version} ({revision})."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Compact"
|
||||
@@ -327,6 +347,14 @@ msgstr "URL فید"
|
||||
msgid "Fetch all my feeds now"
|
||||
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
|
||||
msgid "file is required"
|
||||
msgstr "فایل مورد نیاز است"
|
||||
@@ -518,6 +546,7 @@ msgid "Newest first"
|
||||
msgstr "ابتدا جدیدترین"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Next"
|
||||
msgstr "بعد"
|
||||
|
||||
@@ -627,6 +656,10 @@ msgstr "گذرواژه ها مطابقت ندارند"
|
||||
msgid "Position"
|
||||
msgstr "موقعیت"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Previous"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Profile"
|
||||
msgstr "نمایه"
|
||||
@@ -648,6 +681,7 @@ msgstr "ثبت نام در این نمونه CommaFeed بسته شده است"
|
||||
msgid "REST API"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Right click"
|
||||
msgstr ""
|
||||
@@ -699,11 +733,20 @@ msgstr "به اشتراک بگذارید"
|
||||
msgid "Sharing sites"
|
||||
msgstr "اشتراک گذاری سایت ها"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Shift"
|
||||
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
|
||||
msgid "Show entry menu (desktop)"
|
||||
msgstr ""
|
||||
@@ -720,6 +763,10 @@ msgstr "فیدها و دسته ها را بدون ورودی خوانده نشد
|
||||
msgid "Show keyboard shortcut help"
|
||||
msgstr "نمایش راهنمایی میانبر صفحه کلید"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show native menu (desktop)"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
@@ -793,6 +840,14 @@ msgstr "تم"
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "وضعیت خواندن ورودی فعلی را تغییر دهید"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle sidebar"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle starred status of current entry"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||
msgstr "CommaFeed را با حساب آزمایشی امتحان کنید: دمو/دمو"
|
||||
|
||||
@@ -71,6 +71,10 @@ msgstr "Järjestelmänvalvoja"
|
||||
msgid "All"
|
||||
msgstr "Kaikki"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "An email has been sent if this address was registered. Check your inbox."
|
||||
msgstr "Sähköposti on lähetetty, jos tämä osoite on rekisteröity. "
|
||||
@@ -83,6 +87,10 @@ msgstr "Opml-tiedosto on XML-tiedosto, joka sisältää syötteen URL-osoitteet
|
||||
msgid "Analyze feed"
|
||||
msgstr "Analysoi syöte"
|
||||
|
||||
#: src/components/AnnouncementDialog.tsx
|
||||
msgid "Announcement"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "API key"
|
||||
msgstr "API-avain"
|
||||
@@ -127,9 +135,13 @@ msgstr "Takaisin"
|
||||
msgid "Back to log in"
|
||||
msgstr "Takaisin sisäänkirjautumiseen"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Browser extension required for Chrome"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Browser extentions"
|
||||
msgstr "Selaimen laajennukset"
|
||||
msgid "Browser extention"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
@@ -163,13 +175,21 @@ msgstr "Salasanan vaihtaminen luo uuden API-avaimen"
|
||||
msgid "Check that the feed is working"
|
||||
msgstr "Tarkista, että syöttö toimii"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed next unread item"
|
||||
msgstr "CommaFeed seuraava lukematon kohde"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed version {version} ({revision})"
|
||||
msgstr "CommaFeed-versio {version} ({versio})"
|
||||
msgid "CommaFeed version {version} ({revision})."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Compact"
|
||||
@@ -327,6 +347,14 @@ msgstr "Syötteen URL-osoite"
|
||||
msgid "Fetch all my feeds now"
|
||||
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
|
||||
msgid "file is required"
|
||||
msgstr "tiedosto vaaditaan"
|
||||
@@ -518,6 +546,7 @@ msgid "Newest first"
|
||||
msgstr "Uusin ensin"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Next"
|
||||
msgstr "Seuraava"
|
||||
|
||||
@@ -627,6 +656,10 @@ msgstr "Salasanat eivät täsmää"
|
||||
msgid "Position"
|
||||
msgstr "Sijainti"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Previous"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Profile"
|
||||
msgstr "Profiili"
|
||||
@@ -648,6 +681,7 @@ msgstr "Tämän CommaFeed-esiintymän rekisteröinnit on suljettu"
|
||||
msgid "REST API"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Right click"
|
||||
msgstr ""
|
||||
@@ -699,11 +733,20 @@ msgstr "Jaa"
|
||||
msgid "Sharing sites"
|
||||
msgstr "Sivustojen jakaminen"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Shift"
|
||||
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
|
||||
msgid "Show entry menu (desktop)"
|
||||
msgstr ""
|
||||
@@ -720,6 +763,10 @@ msgstr "Näytä syötteet ja luokat ilman lukemattomia merkintöjä"
|
||||
msgid "Show keyboard shortcut help"
|
||||
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/WelcomePage.tsx
|
||||
@@ -793,6 +840,14 @@ msgstr "Teema"
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "Vaihda nykyisen merkinnän lukutila"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle sidebar"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle starred status of current entry"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||
msgstr "Kokeile CommaFeediä demotilillä: demo/demo"
|
||||
|
||||
@@ -71,6 +71,10 @@ msgstr "Administrateur"
|
||||
msgid "All"
|
||||
msgstr "Tout"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgstr "Toujours remonter l'entrée sélectionnée en haut de la page, même si elle s'affiche complètement à l'écran"
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "An email has been sent if this address was registered. Check your inbox."
|
||||
msgstr "Un e-mail a été envoyé si cette adresse est enregistrée. Vérifiez votre boîte de réception."
|
||||
@@ -83,6 +87,10 @@ msgstr "Un fichier OPML est un fichier XML contenant des URL de flux et des cat
|
||||
msgid "Analyze feed"
|
||||
msgstr "Analyser le flux"
|
||||
|
||||
#: src/components/AnnouncementDialog.tsx
|
||||
msgid "Announcement"
|
||||
msgstr "Annonces"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "API key"
|
||||
msgstr "Clé API"
|
||||
@@ -127,9 +135,13 @@ msgstr "Retour"
|
||||
msgid "Back to log in"
|
||||
msgstr "Retour à la connexion"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Browser extension required for Chrome"
|
||||
msgstr "L'extension navigateur est nécessaire sur Chrome"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Browser extentions"
|
||||
msgstr "Extensions pour navigateurs"
|
||||
msgid "Browser extention"
|
||||
msgstr "Extension navigateur"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
@@ -163,13 +175,21 @@ msgstr "Changer de mot de passe générera une nouvelle clé API"
|
||||
msgid "Check that the feed is working"
|
||||
msgstr "Vérifie que le flux fonctionne"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr "Extension CommaFeed pour navigateur version {browserExtensionVersion}."
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
msgstr "Commafeed est compatible avec l'API Fever, en inscrivant l'URL suivante dans votre client mobile compatible. Entrez votre nom d'utilisateur habituel, et votre clef API comme mot de passe."
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed next unread item"
|
||||
msgstr "CommaFeed prochain article non lu"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed version {version} ({revision})"
|
||||
msgstr "CommaFeed version {version} ({revision})"
|
||||
msgid "CommaFeed version {version} ({revision})."
|
||||
msgstr "CommaFeed version {version} ({revision})."
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Compact"
|
||||
@@ -252,7 +272,7 @@ msgstr "Affichage"
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/app/DonatePage.tsx
|
||||
msgid "Donate"
|
||||
msgstr "Faites un don"
|
||||
msgstr "Faire un don"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Download"
|
||||
@@ -311,7 +331,7 @@ msgstr "Exporter vos abonnements et catégories en tant que fichier OPML qui peu
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
msgid "Extension options"
|
||||
msgstr ""
|
||||
msgstr "Options de l'extension"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Feed name"
|
||||
@@ -327,6 +347,14 @@ msgstr "URL du flux"
|
||||
msgid "Fetch all my feeds now"
|
||||
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
|
||||
msgid "file is required"
|
||||
msgstr "fichier requis"
|
||||
@@ -518,6 +546,7 @@ msgid "Newest first"
|
||||
msgstr "Plus récent en premier"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Next"
|
||||
msgstr "Suivant"
|
||||
|
||||
@@ -547,7 +576,7 @@ msgstr "Oups !"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Open CommaFeed"
|
||||
msgstr ""
|
||||
msgstr "Ouvrir CommaFeed"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open current entry in a new tab"
|
||||
@@ -627,6 +656,10 @@ msgstr "Les mots de passe ne correspondent pas"
|
||||
msgid "Position"
|
||||
msgstr "Position"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Previous"
|
||||
msgstr "Précédent"
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Profile"
|
||||
msgstr "Profil"
|
||||
@@ -648,6 +681,7 @@ msgstr "Les inscriptions sont fermées sur cette instance de CommaFeed"
|
||||
msgid "REST API"
|
||||
msgstr "API REST"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Right click"
|
||||
msgstr "Clic droit"
|
||||
@@ -699,11 +733,20 @@ msgstr "Partager"
|
||||
msgid "Sharing sites"
|
||||
msgstr "Sites de partage"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Shift"
|
||||
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
|
||||
msgid "Show entry menu (desktop)"
|
||||
msgstr "Afficher les options de l'entrée (ordinateur)"
|
||||
@@ -720,6 +763,10 @@ msgstr "Afficher les flux et les catégories pour lesquels tout est déjà lu"
|
||||
msgid "Show keyboard shortcut help"
|
||||
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/WelcomePage.tsx
|
||||
@@ -793,6 +840,14 @@ msgstr "Thème"
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "Marquer l'entrée actuelle comme lue/non lue"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle sidebar"
|
||||
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
|
||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||
msgstr "Essayez CommaFeed avec le compte de démonstration : demo/demo"
|
||||
|
||||
@@ -71,6 +71,10 @@ msgstr "Administración"
|
||||
msgid "All"
|
||||
msgstr "Todos"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "An email has been sent if this address was registered. Check your inbox."
|
||||
msgstr "Enviouse un correo electrónico se este enderezo estaba rexistrado. "
|
||||
@@ -83,6 +87,10 @@ msgstr "Un ficheiro opml é un ficheiro XML que contén URL e categorías de fon
|
||||
msgid "Analyze feed"
|
||||
msgstr "Analizar feed"
|
||||
|
||||
#: src/components/AnnouncementDialog.tsx
|
||||
msgid "Announcement"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "API key"
|
||||
msgstr "chave API"
|
||||
@@ -127,9 +135,13 @@ msgstr "Atrás"
|
||||
msgid "Back to log in"
|
||||
msgstr "Volver para iniciar sesión"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Browser extension required for Chrome"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Browser extentions"
|
||||
msgstr "Extensións do navegador"
|
||||
msgid "Browser extention"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
@@ -163,13 +175,21 @@ msgstr "O cambio de contrasinal xerará unha nova clave de API"
|
||||
msgid "Check that the feed is working"
|
||||
msgstr "Comproba que a fonte funciona"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed next unread item"
|
||||
msgstr "CommaFeed seguinte elemento non lido"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed version {version} ({revision})"
|
||||
msgstr "Versión de CommaFeed {versión} ({revisión})"
|
||||
msgid "CommaFeed version {version} ({revision})."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Compact"
|
||||
@@ -327,6 +347,14 @@ msgstr "URL da fonte"
|
||||
msgid "Fetch all my feeds now"
|
||||
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
|
||||
msgid "file is required"
|
||||
msgstr "é necesario o ficheiro"
|
||||
@@ -518,6 +546,7 @@ msgid "Newest first"
|
||||
msgstr "o máis novo primeiro"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Next"
|
||||
msgstr "Seguinte"
|
||||
|
||||
@@ -627,6 +656,10 @@ msgstr "Os contrasinais non coinciden"
|
||||
msgid "Position"
|
||||
msgstr "Posición"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Previous"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Profile"
|
||||
msgstr "Perfil"
|
||||
@@ -648,6 +681,7 @@ msgstr "Os rexistros están pechados nesta instancia de CommaFeed"
|
||||
msgid "REST API"
|
||||
msgstr "API REST"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Right click"
|
||||
msgstr ""
|
||||
@@ -699,11 +733,20 @@ msgstr "Compartir"
|
||||
msgid "Sharing sites"
|
||||
msgstr "Compartir sitios"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Shift"
|
||||
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
|
||||
msgid "Show entry menu (desktop)"
|
||||
msgstr ""
|
||||
@@ -720,6 +763,10 @@ msgstr "Mostrar fontes e categorías sen entradas sen ler"
|
||||
msgid "Show keyboard shortcut help"
|
||||
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/WelcomePage.tsx
|
||||
@@ -793,6 +840,14 @@ msgstr "Tema"
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "alternar o estado de lectura da entrada actual"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle sidebar"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle starred status of current entry"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||
msgstr "Proba CommaFeed coa conta de demostración: demo/demo"
|
||||
|
||||
@@ -71,6 +71,10 @@ msgstr ""
|
||||
msgid "All"
|
||||
msgstr "Mind"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "An email has been sent if this address was registered. Check your inbox."
|
||||
msgstr "E-mailt küldtünk, ha ez a cím regisztrálva volt. "
|
||||
@@ -83,6 +87,10 @@ msgstr "Az opml-fájl olyan XML-fájl, amely feed URL-címeket és kategóriáka
|
||||
msgid "Analyze feed"
|
||||
msgstr "Hírcsatorna elemzése"
|
||||
|
||||
#: src/components/AnnouncementDialog.tsx
|
||||
msgid "Announcement"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "API key"
|
||||
msgstr "API kulcs"
|
||||
@@ -127,9 +135,13 @@ msgstr "Vissza"
|
||||
msgid "Back to log in"
|
||||
msgstr "Vissza a bejelentkezéshez"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Browser extension required for Chrome"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Browser extentions"
|
||||
msgstr "Böngészőbővítések"
|
||||
msgid "Browser extention"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
@@ -163,13 +175,21 @@ msgstr "A jelszó megváltoztatása új API-kulcsot generál"
|
||||
msgid "Check that the feed is working"
|
||||
msgstr "Ellenőrizze, hogy a feed működik-e"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed next unread item"
|
||||
msgstr "CommaFeed következő olvasatlan elem"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed version {version} ({revision})"
|
||||
msgstr "CommaFeed verzió {version} ({revision})"
|
||||
msgid "CommaFeed version {version} ({revision})."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Compact"
|
||||
@@ -327,6 +347,14 @@ msgstr ""
|
||||
msgid "Fetch all my feeds now"
|
||||
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
|
||||
msgid "file is required"
|
||||
msgstr "fájl szükséges"
|
||||
@@ -518,6 +546,7 @@ msgid "Newest first"
|
||||
msgstr "A legújabbak először"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Next"
|
||||
msgstr "Következő"
|
||||
|
||||
@@ -627,6 +656,10 @@ msgstr "A jelszavak nem egyeznek"
|
||||
msgid "Position"
|
||||
msgstr "Pozíció"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Previous"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Profile"
|
||||
msgstr "Profil"
|
||||
@@ -648,6 +681,7 @@ msgstr "A regisztrációk le vannak zárva ezen a CommaFeed példányon"
|
||||
msgid "REST API"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Right click"
|
||||
msgstr ""
|
||||
@@ -699,11 +733,20 @@ msgstr "Oszd meg"
|
||||
msgid "Sharing sites"
|
||||
msgstr "Webhelyek megosztása"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Shift"
|
||||
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
|
||||
msgid "Show entry menu (desktop)"
|
||||
msgstr ""
|
||||
@@ -720,6 +763,10 @@ msgstr "Hírcsatornák és kategóriák megjelenítése olvasatlan bejegyzések
|
||||
msgid "Show keyboard shortcut help"
|
||||
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/WelcomePage.tsx
|
||||
@@ -793,6 +840,14 @@ msgstr "Téma"
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "Az aktuális bejegyzés olvasási állapotának váltása"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle sidebar"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle starred status of current entry"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||
msgstr "Próbálja ki a CommaFeed-et a demo fiókkal: demo/demo"
|
||||
|
||||
@@ -71,6 +71,10 @@ msgstr ""
|
||||
msgid "All"
|
||||
msgstr "Semua"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "An email has been sent if this address was registered. Check your inbox."
|
||||
msgstr "Email telah dikirim jika alamat ini terdaftar. "
|
||||
@@ -83,6 +87,10 @@ msgstr "File opml adalah file XML yang berisi URL dan kategori feed. "
|
||||
msgid "Analyze feed"
|
||||
msgstr "Analisis umpan"
|
||||
|
||||
#: src/components/AnnouncementDialog.tsx
|
||||
msgid "Announcement"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "API key"
|
||||
msgstr "kunci API"
|
||||
@@ -127,9 +135,13 @@ msgstr "Kembali"
|
||||
msgid "Back to log in"
|
||||
msgstr "Kembali untuk masuk"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Browser extension required for Chrome"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Browser extentions"
|
||||
msgstr "Ekstensi peramban"
|
||||
msgid "Browser extention"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
@@ -163,13 +175,21 @@ msgstr "Mengubah kata sandi akan menghasilkan kunci API baru"
|
||||
msgid "Check that the feed is working"
|
||||
msgstr "Periksa apakah umpannya berfungsi"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed next unread item"
|
||||
msgstr "CommaFeed item yang belum dibaca berikutnya"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed version {version} ({revision})"
|
||||
msgstr "CommaFeed versi {versi} ({revisi})"
|
||||
msgid "CommaFeed version {version} ({revision})."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Compact"
|
||||
@@ -327,6 +347,14 @@ msgstr "URL Umpan"
|
||||
msgid "Fetch all my feeds now"
|
||||
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
|
||||
msgid "file is required"
|
||||
msgstr "file diperlukan"
|
||||
@@ -518,6 +546,7 @@ msgid "Newest first"
|
||||
msgstr "Terbaru dulu"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Next"
|
||||
msgstr "Selanjutnya"
|
||||
|
||||
@@ -627,6 +656,10 @@ msgstr "Kata sandi tidak cocok"
|
||||
msgid "Position"
|
||||
msgstr "Posisi"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Previous"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Profile"
|
||||
msgstr "Profil"
|
||||
@@ -648,6 +681,7 @@ msgstr "Pendaftaran ditutup pada instans CommaFeed ini"
|
||||
msgid "REST API"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Right click"
|
||||
msgstr ""
|
||||
@@ -699,11 +733,20 @@ msgstr "Bagikan"
|
||||
msgid "Sharing sites"
|
||||
msgstr "Berbagi situs"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Shift"
|
||||
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
|
||||
msgid "Show entry menu (desktop)"
|
||||
msgstr ""
|
||||
@@ -720,6 +763,10 @@ msgstr "Tampilkan umpan dan kategori tanpa entri yang belum dibaca"
|
||||
msgid "Show keyboard shortcut help"
|
||||
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/WelcomePage.tsx
|
||||
@@ -793,6 +840,14 @@ msgstr "Tema"
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "Beralih status baca entri saat ini"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle sidebar"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle starred status of current entry"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||
msgstr "Cobalah CommaFeed dengan akun demo: demo/demo"
|
||||
|
||||
@@ -71,6 +71,10 @@ msgstr "Ammin"
|
||||
msgid "All"
|
||||
msgstr "Tutto"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "An email has been sent if this address was registered. Check your inbox."
|
||||
msgstr "È stata inviata un'e-mail se questo indirizzo è stato registrato. "
|
||||
@@ -83,6 +87,10 @@ msgstr "Un file opml è un file XML contenente URL e categorie di feed. "
|
||||
msgid "Analyze feed"
|
||||
msgstr "Analizza feed"
|
||||
|
||||
#: src/components/AnnouncementDialog.tsx
|
||||
msgid "Announcement"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "API key"
|
||||
msgstr "Chiave API"
|
||||
@@ -127,9 +135,13 @@ msgstr "Indietro"
|
||||
msgid "Back to log in"
|
||||
msgstr "Torna per accedere"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Browser extension required for Chrome"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Browser extentions"
|
||||
msgstr "Estensioni del browser"
|
||||
msgid "Browser extention"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
@@ -163,13 +175,21 @@ msgstr "La modifica della password genererà una nuova chiave API"
|
||||
msgid "Check that the feed is working"
|
||||
msgstr "Verifica che il feed funzioni"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed next unread item"
|
||||
msgstr "CommaFeed successivo elemento non letto"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed version {version} ({revision})"
|
||||
msgstr "Versione CommaFeed {versione} ({revisione})"
|
||||
msgid "CommaFeed version {version} ({revision})."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Compact"
|
||||
@@ -327,6 +347,14 @@ msgstr "URL feed"
|
||||
msgid "Fetch all my feeds now"
|
||||
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
|
||||
msgid "file is required"
|
||||
msgstr "è richiesto il file"
|
||||
@@ -518,6 +546,7 @@ msgid "Newest first"
|
||||
msgstr "Il più recente prima"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Next"
|
||||
msgstr "Avanti"
|
||||
|
||||
@@ -627,6 +656,10 @@ msgstr "Le password non corrispondono"
|
||||
msgid "Position"
|
||||
msgstr "Posizione"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Previous"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Profile"
|
||||
msgstr "Profilo"
|
||||
@@ -648,6 +681,7 @@ msgstr "Le registrazioni sono chiuse su questa istanza CommaFeed"
|
||||
msgid "REST API"
|
||||
msgstr "API REST"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Right click"
|
||||
msgstr ""
|
||||
@@ -699,11 +733,20 @@ msgstr "Condividi"
|
||||
msgid "Sharing sites"
|
||||
msgstr "Condivisione di siti"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Shift"
|
||||
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
|
||||
msgid "Show entry menu (desktop)"
|
||||
msgstr ""
|
||||
@@ -720,6 +763,10 @@ msgstr "Mostra feed e categorie senza voci non lette"
|
||||
msgid "Show keyboard shortcut help"
|
||||
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/WelcomePage.tsx
|
||||
@@ -793,6 +840,14 @@ msgstr "Tema"
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "Commuta lo stato di lettura della voce corrente"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle sidebar"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle starred status of current entry"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||
msgstr "Prova CommaFeed con il conto demo: demo/demo"
|
||||
|
||||
@@ -71,6 +71,10 @@ msgstr "管理人"
|
||||
msgid "All"
|
||||
msgstr "全員"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "An email has been sent if this address was registered. Check your inbox."
|
||||
msgstr "このアドレスが登録されていれば、メールが送信されました。"
|
||||
@@ -83,6 +87,10 @@ msgstr "opml ファイルは、フィードの URL とカテゴリを含む XML
|
||||
msgid "Analyze feed"
|
||||
msgstr "フィードを分析する"
|
||||
|
||||
#: src/components/AnnouncementDialog.tsx
|
||||
msgid "Announcement"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "API key"
|
||||
msgstr "APIキー"
|
||||
@@ -127,9 +135,13 @@ msgstr "裏"
|
||||
msgid "Back to log in"
|
||||
msgstr "ログインに戻る"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Browser extension required for Chrome"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Browser extentions"
|
||||
msgstr "ブラウザ拡張機能"
|
||||
msgid "Browser extention"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
@@ -163,13 +175,21 @@ msgstr "パスワードを変更すると、新しい API キーが生成され
|
||||
msgid "Check that the feed is working"
|
||||
msgstr "フィードが動作していることを確認してください"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed next unread item"
|
||||
msgstr "次の未読アイテムをカンマフィード"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed version {version} ({revision})"
|
||||
msgstr "コンマフィードのバージョン {version} ({revision})"
|
||||
msgid "CommaFeed version {version} ({revision})."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Compact"
|
||||
@@ -327,6 +347,14 @@ msgstr "フィード URL"
|
||||
msgid "Fetch all my feeds now"
|
||||
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
|
||||
msgid "file is required"
|
||||
msgstr "ファイルが必要です"
|
||||
@@ -518,6 +546,7 @@ msgid "Newest first"
|
||||
msgstr "最新順"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Next"
|
||||
msgstr "次へ"
|
||||
|
||||
@@ -627,6 +656,10 @@ msgstr "パスワードが一致しません"
|
||||
msgid "Position"
|
||||
msgstr "位置"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Previous"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Profile"
|
||||
msgstr "プロフィール"
|
||||
@@ -648,6 +681,7 @@ msgstr "この CommaFeed インスタンスの登録は終了しています"
|
||||
msgid "REST API"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Right click"
|
||||
msgstr ""
|
||||
@@ -699,11 +733,20 @@ msgstr "シェア"
|
||||
msgid "Sharing sites"
|
||||
msgstr "共有サイト"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Shift"
|
||||
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
|
||||
msgid "Show entry menu (desktop)"
|
||||
msgstr ""
|
||||
@@ -720,6 +763,10 @@ msgstr "未読エントリのないフィードとカテゴリを表示する"
|
||||
msgid "Show keyboard shortcut help"
|
||||
msgstr "キーボード ショートカットのヘルプを表示"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show native menu (desktop)"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
@@ -793,6 +840,14 @@ msgstr "テーマ"
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "現在のエントリの読み取りステータスを切り替えます"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle sidebar"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle starred status of current entry"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||
msgstr "デモアカウントで CommaFeed を試す: demo/demo"
|
||||
|
||||
@@ -71,6 +71,10 @@ msgstr "관리자"
|
||||
msgid "All"
|
||||
msgstr "전체"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "An email has been sent if this address was registered. Check your inbox."
|
||||
msgstr "이 주소가 등록된 경우 이메일이 전송되었습니다. "
|
||||
@@ -83,6 +87,10 @@ msgstr "opml 파일은 피드 URL과 카테고리를 포함하는 XML 파일입
|
||||
msgid "Analyze feed"
|
||||
msgstr "피드 분석"
|
||||
|
||||
#: src/components/AnnouncementDialog.tsx
|
||||
msgid "Announcement"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "API key"
|
||||
msgstr "API 키"
|
||||
@@ -127,9 +135,13 @@ msgstr "뒤로"
|
||||
msgid "Back to log in"
|
||||
msgstr "로그인으로 돌아가기"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Browser extension required for Chrome"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Browser extentions"
|
||||
msgstr "브라우저 확장"
|
||||
msgid "Browser extention"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
@@ -163,13 +175,21 @@ msgstr "비밀번호를 변경하면 새 API 키가 생성됩니다."
|
||||
msgid "Check that the feed is working"
|
||||
msgstr "피드가 작동하는지 확인"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed next unread item"
|
||||
msgstr "다음 읽지 않은 항목을 쉼표로 피드"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed version {version} ({revision})"
|
||||
msgstr "쉼표 피드 버전 {버전}({개정})"
|
||||
msgid "CommaFeed version {version} ({revision})."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Compact"
|
||||
@@ -327,6 +347,14 @@ msgstr "피드 URL"
|
||||
msgid "Fetch all my feeds now"
|
||||
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
|
||||
msgid "file is required"
|
||||
msgstr "파일이 필요합니다"
|
||||
@@ -518,6 +546,7 @@ msgid "Newest first"
|
||||
msgstr "최신순"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Next"
|
||||
msgstr "다음"
|
||||
|
||||
@@ -627,6 +656,10 @@ msgstr "비밀번호가 일치하지 않습니다"
|
||||
msgid "Position"
|
||||
msgstr "위치"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Previous"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Profile"
|
||||
msgstr "프로필"
|
||||
@@ -648,6 +681,7 @@ msgstr "이 CommaFeed 인스턴스에 대한 등록이 마감되었습니다."
|
||||
msgid "REST API"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Right click"
|
||||
msgstr ""
|
||||
@@ -699,11 +733,20 @@ msgstr "공유"
|
||||
msgid "Sharing sites"
|
||||
msgstr "사이트 공유"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Shift"
|
||||
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
|
||||
msgid "Show entry menu (desktop)"
|
||||
msgstr ""
|
||||
@@ -720,6 +763,10 @@ msgstr "읽지 않은 항목이 없는 피드 및 카테고리 표시"
|
||||
msgid "Show keyboard shortcut help"
|
||||
msgstr "키보드 단축키 도움말 표시"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show native menu (desktop)"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
@@ -793,6 +840,14 @@ msgstr "테마"
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "현재 항목의 읽기 상태 전환"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle sidebar"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle starred status of current entry"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||
msgstr "데모 계정으로 CommaFeed를 사용해 보세요: demo/demo"
|
||||
|
||||
@@ -71,6 +71,10 @@ msgstr "Pentadbir"
|
||||
msgid "All"
|
||||
msgstr "Semua"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "An email has been sent if this address was registered. Check your inbox."
|
||||
msgstr "E-mel telah dihantar jika alamat ini didaftarkan. "
|
||||
@@ -83,6 +87,10 @@ msgstr "Fail opml ialah fail XML yang mengandungi URL suapan dan kategori. "
|
||||
msgid "Analyze feed"
|
||||
msgstr "Menganalisis suapan"
|
||||
|
||||
#: src/components/AnnouncementDialog.tsx
|
||||
msgid "Announcement"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "API key"
|
||||
msgstr "Kunci API"
|
||||
@@ -127,9 +135,13 @@ msgstr "Kembali"
|
||||
msgid "Back to log in"
|
||||
msgstr "Kembali untuk log masuk"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Browser extension required for Chrome"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Browser extentions"
|
||||
msgstr "Peluasan penyemak imbas"
|
||||
msgid "Browser extention"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
@@ -163,13 +175,21 @@ msgstr "Menukar kata laluan akan menjana kunci API baharu"
|
||||
msgid "Check that the feed is working"
|
||||
msgstr "Semak sama ada suapan berfungsi"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed next unread item"
|
||||
msgstr "CommaFeed item belum dibaca seterusnya"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed version {version} ({revision})"
|
||||
msgstr "Versi CommaFeed {versi} ({semakan})"
|
||||
msgid "CommaFeed version {version} ({revision})."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Compact"
|
||||
@@ -327,6 +347,14 @@ msgstr "URL Suapan"
|
||||
msgid "Fetch all my feeds now"
|
||||
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
|
||||
msgid "file is required"
|
||||
msgstr "fail diperlukan"
|
||||
@@ -518,6 +546,7 @@ msgid "Newest first"
|
||||
msgstr "Terbaharu dahulu"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Next"
|
||||
msgstr "Seterusnya"
|
||||
|
||||
@@ -627,6 +656,10 @@ msgstr "Kata laluan tidak sepadan"
|
||||
msgid "Position"
|
||||
msgstr "Kedudukan"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Previous"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Profile"
|
||||
msgstr "Profil"
|
||||
@@ -648,6 +681,7 @@ msgstr "Pendaftaran ditutup pada contoh CommaFeed ini"
|
||||
msgid "REST API"
|
||||
msgstr "REHAT API"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Right click"
|
||||
msgstr ""
|
||||
@@ -699,11 +733,20 @@ msgstr "Kongsi"
|
||||
msgid "Sharing sites"
|
||||
msgstr "Berkongsi tapak"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Shift"
|
||||
msgstr "Anjakan"
|
||||
|
||||
#: 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
|
||||
msgid "Show entry menu (desktop)"
|
||||
msgstr ""
|
||||
@@ -720,6 +763,10 @@ msgstr "Tunjukkan suapan dan kategori tanpa entri yang belum dibaca"
|
||||
msgid "Show keyboard shortcut help"
|
||||
msgstr "Tunjukkan bantuan pintasan papan kekunci"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show native menu (desktop)"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
@@ -793,6 +840,14 @@ msgstr "Tema"
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "Togol status bacaan entri semasa"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle sidebar"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle starred status of current entry"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||
msgstr "Cuba CommaFeed dengan akaun demo: demo/demo"
|
||||
|
||||
@@ -71,6 +71,10 @@ msgstr ""
|
||||
msgid "All"
|
||||
msgstr "Alle"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "An email has been sent if this address was registered. Check your inbox."
|
||||
msgstr "En e-post er sendt hvis denne adressen var registrert. "
|
||||
@@ -83,6 +87,10 @@ msgstr "En opml-fil er en XML-fil som inneholder feed-URLer og kategorier. "
|
||||
msgid "Analyze feed"
|
||||
msgstr "Analyser feed"
|
||||
|
||||
#: src/components/AnnouncementDialog.tsx
|
||||
msgid "Announcement"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "API key"
|
||||
msgstr "API-nøkkel"
|
||||
@@ -127,9 +135,13 @@ msgstr "Tilbake"
|
||||
msgid "Back to log in"
|
||||
msgstr "Tilbake for å logge inn"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Browser extension required for Chrome"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Browser extentions"
|
||||
msgstr "Nettleserutvidelser"
|
||||
msgid "Browser extention"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
@@ -163,13 +175,21 @@ msgstr "Endring av passord vil generere en ny API-nøkkel"
|
||||
msgid "Check that the feed is working"
|
||||
msgstr "Sjekk at feeden fungerer"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed next unread item"
|
||||
msgstr "CommaFeed neste uleste element"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed version {version} ({revision})"
|
||||
msgstr "CommaFeed versjon {versjon} ({revisjon})"
|
||||
msgid "CommaFeed version {version} ({revision})."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Compact"
|
||||
@@ -327,6 +347,14 @@ msgstr "Feed-URL"
|
||||
msgid "Fetch all my feeds now"
|
||||
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
|
||||
msgid "file is required"
|
||||
msgstr "fil kreves"
|
||||
@@ -518,6 +546,7 @@ msgid "Newest first"
|
||||
msgstr "Nyeste først"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Next"
|
||||
msgstr "Neste"
|
||||
|
||||
@@ -627,6 +656,10 @@ msgstr "Passordene samsvarer ikke"
|
||||
msgid "Position"
|
||||
msgstr "Posisjon"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Previous"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Profile"
|
||||
msgstr "Profil"
|
||||
@@ -648,6 +681,7 @@ msgstr "Registreringer er stengt på denne CommaFeed-forekomsten"
|
||||
msgid "REST API"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Right click"
|
||||
msgstr ""
|
||||
@@ -699,11 +733,20 @@ msgstr "Del"
|
||||
msgid "Sharing sites"
|
||||
msgstr "Delingssider"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Shift"
|
||||
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
|
||||
msgid "Show entry menu (desktop)"
|
||||
msgstr ""
|
||||
@@ -720,6 +763,10 @@ msgstr "Vis feeder og kategorier uten uleste oppføringer"
|
||||
msgid "Show keyboard shortcut help"
|
||||
msgstr "Vis hurtigtasthjelp"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show native menu (desktop)"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
@@ -793,6 +840,14 @@ msgstr "Tema"
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "Veksle lesestatus for gjeldende oppføring"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle sidebar"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle starred status of current entry"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||
msgstr "Prøv CommaFeed med demokontoen: demo/demo"
|
||||
|
||||
@@ -71,6 +71,10 @@ msgstr "Beheerder"
|
||||
msgid "All"
|
||||
msgstr "Alles"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "An email has been sent if this address was registered. Check your inbox."
|
||||
msgstr "Er is een e-mail verzonden als dit adres is geregistreerd. "
|
||||
@@ -83,6 +87,10 @@ msgstr "Een opml-bestand is een XML-bestand met feed-URL's en categorieën. "
|
||||
msgid "Analyze feed"
|
||||
msgstr "Analyseer feed"
|
||||
|
||||
#: src/components/AnnouncementDialog.tsx
|
||||
msgid "Announcement"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "API key"
|
||||
msgstr "API-sleutel"
|
||||
@@ -127,9 +135,13 @@ msgstr "Terug"
|
||||
msgid "Back to log in"
|
||||
msgstr "Terug naar inloggen"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Browser extension required for Chrome"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Browser extentions"
|
||||
msgstr "Browserextensies"
|
||||
msgid "Browser extention"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
@@ -163,13 +175,21 @@ msgstr "Het wijzigen van het wachtwoord genereert een nieuwe API-sleutel"
|
||||
msgid "Check that the feed is working"
|
||||
msgstr "Controleer of de feed werkt"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed next unread item"
|
||||
msgstr "CommaFeed volgende ongelezen item"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed version {version} ({revision})"
|
||||
msgstr "CommaFeed-versie {versie} ({revisie})"
|
||||
msgid "CommaFeed version {version} ({revision})."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Compact"
|
||||
@@ -327,6 +347,14 @@ msgstr "Feed-URL"
|
||||
msgid "Fetch all my feeds now"
|
||||
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
|
||||
msgid "file is required"
|
||||
msgstr "bestand is vereist"
|
||||
@@ -518,6 +546,7 @@ msgid "Newest first"
|
||||
msgstr "Nieuwste eerst"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Next"
|
||||
msgstr "Volgende"
|
||||
|
||||
@@ -627,6 +656,10 @@ msgstr "Wachtwoorden komen niet overeen"
|
||||
msgid "Position"
|
||||
msgstr "Positie"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Previous"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Profile"
|
||||
msgstr "Profiel"
|
||||
@@ -648,6 +681,7 @@ msgstr "Registraties zijn gesloten op deze CommaFeed-instantie"
|
||||
msgid "REST API"
|
||||
msgstr "REST-API"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Right click"
|
||||
msgstr ""
|
||||
@@ -699,11 +733,20 @@ msgstr "Delen"
|
||||
msgid "Sharing sites"
|
||||
msgstr "Sites delen"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Shift"
|
||||
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
|
||||
msgid "Show entry menu (desktop)"
|
||||
msgstr ""
|
||||
@@ -720,6 +763,10 @@ msgstr "Toon feeds en categorieën zonder ongelezen items"
|
||||
msgid "Show keyboard shortcut help"
|
||||
msgstr "Toon hulp bij sneltoetsen"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show native menu (desktop)"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
@@ -793,6 +840,14 @@ msgstr "Thema"
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "Toggle leesstatus van huidige invoer"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle sidebar"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle starred status of current entry"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||
msgstr "Probeer CommaFeed uit met het demo-account: demo/demo"
|
||||
|
||||
@@ -71,6 +71,10 @@ msgstr ""
|
||||
msgid "All"
|
||||
msgstr "Alle"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "An email has been sent if this address was registered. Check your inbox."
|
||||
msgstr "En e-post er sendt hvis denne adressen var registrert. "
|
||||
@@ -83,6 +87,10 @@ msgstr "En opml-fil er en XML-fil som inneholder feed-URLer og kategorier. "
|
||||
msgid "Analyze feed"
|
||||
msgstr "Analyser feed"
|
||||
|
||||
#: src/components/AnnouncementDialog.tsx
|
||||
msgid "Announcement"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "API key"
|
||||
msgstr "API-nøkkel"
|
||||
@@ -127,9 +135,13 @@ msgstr "Tilbake"
|
||||
msgid "Back to log in"
|
||||
msgstr "Tilbake for å logge inn"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Browser extension required for Chrome"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Browser extentions"
|
||||
msgstr "Nettleserutvidelser"
|
||||
msgid "Browser extention"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
@@ -163,13 +175,21 @@ msgstr "Endring av passord vil generere en ny API-nøkkel"
|
||||
msgid "Check that the feed is working"
|
||||
msgstr "Sjekk at feeden fungerer"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed next unread item"
|
||||
msgstr "CommaFeed neste uleste element"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed version {version} ({revision})"
|
||||
msgstr "CommaFeed versjon {versjon} ({revisjon})"
|
||||
msgid "CommaFeed version {version} ({revision})."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Compact"
|
||||
@@ -327,6 +347,14 @@ msgstr "Feed-URL"
|
||||
msgid "Fetch all my feeds now"
|
||||
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
|
||||
msgid "file is required"
|
||||
msgstr "fil kreves"
|
||||
@@ -518,6 +546,7 @@ msgid "Newest first"
|
||||
msgstr "Nyeste først"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Next"
|
||||
msgstr "Neste"
|
||||
|
||||
@@ -627,6 +656,10 @@ msgstr "Passordene samsvarer ikke"
|
||||
msgid "Position"
|
||||
msgstr "Posisjon"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Previous"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Profile"
|
||||
msgstr "Profil"
|
||||
@@ -648,6 +681,7 @@ msgstr "Registreringer er stengt på denne CommaFeed-forekomsten"
|
||||
msgid "REST API"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Right click"
|
||||
msgstr ""
|
||||
@@ -699,11 +733,20 @@ msgstr "Del"
|
||||
msgid "Sharing sites"
|
||||
msgstr "Delingssider"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Shift"
|
||||
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
|
||||
msgid "Show entry menu (desktop)"
|
||||
msgstr ""
|
||||
@@ -720,6 +763,10 @@ msgstr "Vis feeder og kategorier uten uleste oppføringer"
|
||||
msgid "Show keyboard shortcut help"
|
||||
msgstr "Vis hurtigtasthjelp"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show native menu (desktop)"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
@@ -793,6 +840,14 @@ msgstr "Tema"
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "Veksle lesestatus for gjeldende oppføring"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle sidebar"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle starred status of current entry"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||
msgstr "Prøv CommaFeed med demokontoen: demo/demo"
|
||||
|
||||
@@ -71,6 +71,10 @@ msgstr "Administracja"
|
||||
msgid "All"
|
||||
msgstr "Wszystkie"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "An email has been sent if this address was registered. Check your inbox."
|
||||
msgstr "E-mail został wysłany, jeśli ten adres został zarejestrowany. "
|
||||
@@ -83,6 +87,10 @@ msgstr "Plik opml to plik XML zawierający adresy URL i kategorie kanałów. "
|
||||
msgid "Analyze feed"
|
||||
msgstr "Analizuj kanał"
|
||||
|
||||
#: src/components/AnnouncementDialog.tsx
|
||||
msgid "Announcement"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "API key"
|
||||
msgstr "klucz API"
|
||||
@@ -127,9 +135,13 @@ msgstr "Powrót"
|
||||
msgid "Back to log in"
|
||||
msgstr "Powrót do logowania"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Browser extension required for Chrome"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Browser extentions"
|
||||
msgstr "Rozszerzenia przeglądarki"
|
||||
msgid "Browser extention"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
@@ -163,13 +175,21 @@ msgstr "Zmiana hasła spowoduje wygenerowanie nowego klucza API"
|
||||
msgid "Check that the feed is working"
|
||||
msgstr "Sprawdź, czy kanał działa"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed next unread item"
|
||||
msgstr "Przecinek następny nieprzeczytany element"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed version {version} ({revision})"
|
||||
msgstr "Wersja CommaFeed {wersja} ({wersja})"
|
||||
msgid "CommaFeed version {version} ({revision})."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Compact"
|
||||
@@ -327,6 +347,14 @@ msgstr "URL kanału"
|
||||
msgid "Fetch all my feeds now"
|
||||
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
|
||||
msgid "file is required"
|
||||
msgstr "plik jest wymagany"
|
||||
@@ -518,6 +546,7 @@ msgid "Newest first"
|
||||
msgstr "Najnowsze jako pierwsze"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Next"
|
||||
msgstr "Dalej"
|
||||
|
||||
@@ -627,6 +656,10 @@ msgstr "Hasła nie pasują"
|
||||
msgid "Position"
|
||||
msgstr "Pozycja"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Previous"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Profile"
|
||||
msgstr "Profil"
|
||||
@@ -648,6 +681,7 @@ msgstr "Rejestracje są zamknięte w tej instancji CommaFeed"
|
||||
msgid "REST API"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Right click"
|
||||
msgstr ""
|
||||
@@ -699,11 +733,20 @@ msgstr "Udostępnij"
|
||||
msgid "Sharing sites"
|
||||
msgstr "Udostępnianie witryn"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Shift"
|
||||
msgstr "zmiana"
|
||||
|
||||
#: 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
|
||||
msgid "Show entry menu (desktop)"
|
||||
msgstr ""
|
||||
@@ -720,6 +763,10 @@ msgstr "Pokaż kanały i kategorie bez nieprzeczytanych wpisów"
|
||||
msgid "Show keyboard shortcut help"
|
||||
msgstr "Pokaż pomoc dotyczącą skrótów klawiaturowych"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show native menu (desktop)"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
@@ -793,6 +840,14 @@ msgstr "Motyw"
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "Przełącz stan odczytu bieżącego wpisu"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle sidebar"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle starred status of current entry"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||
msgstr "Wypróbuj CommaFeed z kontem demo: demo/demo"
|
||||
|
||||
@@ -71,6 +71,10 @@ msgstr "Administrador"
|
||||
msgid "All"
|
||||
msgstr "Todos"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "An email has been sent if this address was registered. Check your inbox."
|
||||
msgstr "Um email foi enviado se este endereço foi registrado. "
|
||||
@@ -83,6 +87,10 @@ msgstr "Um arquivo opml é um arquivo XML contendo URLs e categorias de feed. "
|
||||
msgid "Analyze feed"
|
||||
msgstr "Analisar feed"
|
||||
|
||||
#: src/components/AnnouncementDialog.tsx
|
||||
msgid "Announcement"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "API key"
|
||||
msgstr "chave de API"
|
||||
@@ -127,9 +135,13 @@ msgstr "Voltar"
|
||||
msgid "Back to log in"
|
||||
msgstr "Voltar para logar"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Browser extension required for Chrome"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Browser extentions"
|
||||
msgstr "Extensões do navegador"
|
||||
msgid "Browser extention"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
@@ -163,13 +175,21 @@ msgstr "A alteração da senha gerará uma nova chave de API"
|
||||
msgid "Check that the feed is working"
|
||||
msgstr "Verifique se o feed está funcionando"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed next unread item"
|
||||
msgstr "CommaFeed próximo item não lido"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed version {version} ({revision})"
|
||||
msgstr "Versão do CommaFeed {versão} ({revisão})"
|
||||
msgid "CommaFeed version {version} ({revision})."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Compact"
|
||||
@@ -327,6 +347,14 @@ msgstr "URL do feed"
|
||||
msgid "Fetch all my feeds now"
|
||||
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
|
||||
msgid "file is required"
|
||||
msgstr "o arquivo é obrigatório"
|
||||
@@ -518,6 +546,7 @@ msgid "Newest first"
|
||||
msgstr "Mais novo primeiro"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Next"
|
||||
msgstr "Próximo"
|
||||
|
||||
@@ -627,6 +656,10 @@ msgstr "Senhas não coincidem"
|
||||
msgid "Position"
|
||||
msgstr "Posição"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Previous"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Profile"
|
||||
msgstr "Perfil"
|
||||
@@ -648,6 +681,7 @@ msgstr "Os registros estão fechados nesta instância do CommaFeed"
|
||||
msgid "REST API"
|
||||
msgstr "API REST"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Right click"
|
||||
msgstr ""
|
||||
@@ -699,11 +733,20 @@ msgstr "Compartilhar"
|
||||
msgid "Sharing sites"
|
||||
msgstr "Compartilhando sites"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Shift"
|
||||
msgstr "Mudar"
|
||||
|
||||
#: 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
|
||||
msgid "Show entry menu (desktop)"
|
||||
msgstr ""
|
||||
@@ -720,6 +763,10 @@ msgstr "Mostrar feeds e categorias sem entradas não lidas"
|
||||
msgid "Show keyboard shortcut help"
|
||||
msgstr "Mostrar ajuda de atalho de teclado"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show native menu (desktop)"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
@@ -793,6 +840,14 @@ msgstr "Tema"
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "Alternar o status de leitura da entrada atual"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle sidebar"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle starred status of current entry"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||
msgstr "Experimente o CommaFeed com a conta demo: demo/demo"
|
||||
|
||||
@@ -71,6 +71,10 @@ msgstr "Админ"
|
||||
msgid "All"
|
||||
msgstr "Все"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "An email has been sent if this address was registered. Check your inbox."
|
||||
msgstr "Электронное письмо было отправлено, если этот адрес был зарегистрирован. "
|
||||
@@ -83,6 +87,10 @@ msgstr "OPML-файл — это XML-файл, содержащий URL-адре
|
||||
msgid "Analyze feed"
|
||||
msgstr "Анализ канала"
|
||||
|
||||
#: src/components/AnnouncementDialog.tsx
|
||||
msgid "Announcement"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "API key"
|
||||
msgstr "ключ API"
|
||||
@@ -127,9 +135,13 @@ msgstr "Назад"
|
||||
msgid "Back to log in"
|
||||
msgstr "Вернуться к входу"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Browser extension required for Chrome"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Browser extentions"
|
||||
msgstr "Расширения браузера"
|
||||
msgid "Browser extention"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
@@ -163,13 +175,21 @@ msgstr "При изменении пароля будет сгенерирова
|
||||
msgid "Check that the feed is working"
|
||||
msgstr "Проверьте, работает ли лента."
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed next unread item"
|
||||
msgstr "CommaFeed следующий непрочитанный элемент"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed version {version} ({revision})"
|
||||
msgstr "CommaFeed версия {версия} ({редакция})"
|
||||
msgid "CommaFeed version {version} ({revision})."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Compact"
|
||||
@@ -327,6 +347,14 @@ msgstr "URL-адрес фида"
|
||||
msgid "Fetch all my feeds now"
|
||||
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
|
||||
msgid "file is required"
|
||||
msgstr "требуется файл"
|
||||
@@ -518,6 +546,7 @@ msgid "Newest first"
|
||||
msgstr "Сначала новые"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Next"
|
||||
msgstr "Далее"
|
||||
|
||||
@@ -627,6 +656,10 @@ msgstr "Пароли не совпадают"
|
||||
msgid "Position"
|
||||
msgstr "Позиция"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Previous"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Profile"
|
||||
msgstr "Профиль"
|
||||
@@ -648,6 +681,7 @@ msgstr "Регистрация закрыта для этого экземпля
|
||||
msgid "REST API"
|
||||
msgstr "ОТДЫХА API"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Right click"
|
||||
msgstr ""
|
||||
@@ -699,11 +733,20 @@ msgstr "Поделиться"
|
||||
msgid "Sharing sites"
|
||||
msgstr "Обмен сайтами"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Shift"
|
||||
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
|
||||
msgid "Show entry menu (desktop)"
|
||||
msgstr ""
|
||||
@@ -720,6 +763,10 @@ msgstr "Показать каналы и категории без непроч
|
||||
msgid "Show keyboard shortcut help"
|
||||
msgstr "Показать справку по сочетаниям клавиш."
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show native menu (desktop)"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
@@ -793,6 +840,14 @@ msgstr "Тема"
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "Переключить статус чтения текущей записи"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle sidebar"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle starred status of current entry"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||
msgstr "Попробуйте CommaFeed на демо-счете: demo/demo"
|
||||
|
||||
@@ -71,6 +71,10 @@ msgstr "Správca"
|
||||
msgid "All"
|
||||
msgstr "Všetky"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "An email has been sent if this address was registered. Check your inbox."
|
||||
msgstr "E-mail bol odoslaný, ak bola táto adresa zaregistrovaná. "
|
||||
@@ -83,6 +87,10 @@ msgstr "Súbor opml je súbor XML obsahujúci adresy URL kanálov a kategórie.
|
||||
msgid "Analyze feed"
|
||||
msgstr "Analyzujte krmivo"
|
||||
|
||||
#: src/components/AnnouncementDialog.tsx
|
||||
msgid "Announcement"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "API key"
|
||||
msgstr "Kľúč API"
|
||||
@@ -127,9 +135,13 @@ msgstr "Späť"
|
||||
msgid "Back to log in"
|
||||
msgstr "Späť na prihlásenie"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Browser extension required for Chrome"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Browser extentions"
|
||||
msgstr "Rozšírenia prehliadača"
|
||||
msgid "Browser extention"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
@@ -163,13 +175,21 @@ msgstr "Zmena hesla vygeneruje nový kľúč API"
|
||||
msgid "Check that the feed is working"
|
||||
msgstr "Skontrolujte, či feed funguje"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed next unread item"
|
||||
msgstr "CommaFeed ďalšia neprečítaná položka"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed version {version} ({revision})"
|
||||
msgstr "CommaFeed verzia {version} ({revision})"
|
||||
msgid "CommaFeed version {version} ({revision})."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Compact"
|
||||
@@ -327,6 +347,14 @@ msgstr "URL informačného kanála"
|
||||
msgid "Fetch all my feeds now"
|
||||
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
|
||||
msgid "file is required"
|
||||
msgstr ""
|
||||
@@ -518,6 +546,7 @@ msgid "Newest first"
|
||||
msgstr "Najnovšie ako prvé"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Next"
|
||||
msgstr "Ďalej"
|
||||
|
||||
@@ -627,6 +656,10 @@ msgstr "Heslá sa nezhodujú"
|
||||
msgid "Position"
|
||||
msgstr "Pozícia"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Previous"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Profile"
|
||||
msgstr "Profil"
|
||||
@@ -648,6 +681,7 @@ msgstr "V tejto inštancii CommaFeed sú registrácie uzavreté"
|
||||
msgid "REST API"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Right click"
|
||||
msgstr ""
|
||||
@@ -699,11 +733,20 @@ msgstr "Zdieľať"
|
||||
msgid "Sharing sites"
|
||||
msgstr "Zdieľanie stránok"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Shift"
|
||||
msgstr "Smena"
|
||||
|
||||
#: 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
|
||||
msgid "Show entry menu (desktop)"
|
||||
msgstr ""
|
||||
@@ -720,6 +763,10 @@ msgstr "Zobraziť kanály a kategórie bez neprečítaných záznamov"
|
||||
msgid "Show keyboard shortcut help"
|
||||
msgstr "Zobraziť pomoc s klávesovými skratkami"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show native menu (desktop)"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
@@ -793,6 +840,14 @@ msgstr "Téma"
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "Prepne stav čítania aktuálneho záznamu"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle sidebar"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle starred status of current entry"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||
msgstr "Vyskúšajte CommaFeed s demo účtom: demo/demo"
|
||||
|
||||
@@ -71,6 +71,10 @@ msgstr ""
|
||||
msgid "All"
|
||||
msgstr "Alla"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "An email has been sent if this address was registered. Check your inbox."
|
||||
msgstr "Ett e-postmeddelande har skickats om denna adress var registrerad. "
|
||||
@@ -83,6 +87,10 @@ msgstr "En opml-fil är en XML-fil som innehåller feed-URL:er och kategorier. "
|
||||
msgid "Analyze feed"
|
||||
msgstr "Analysera foder"
|
||||
|
||||
#: src/components/AnnouncementDialog.tsx
|
||||
msgid "Announcement"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "API key"
|
||||
msgstr "API-nyckel"
|
||||
@@ -127,9 +135,13 @@ msgstr "Tillbaka"
|
||||
msgid "Back to log in"
|
||||
msgstr "Tillbaka för att logga in"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Browser extension required for Chrome"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Browser extentions"
|
||||
msgstr "Webbläsartillägg"
|
||||
msgid "Browser extention"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
@@ -163,12 +175,20 @@ msgstr "Ändra lösenord kommer att generera en ny API-nyckel"
|
||||
msgid "Check that the feed is working"
|
||||
msgstr "Kontrollera att matningen fungerar"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed next unread item"
|
||||
msgstr "CommaFeed nästa olästa objekt"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed version {version} ({revision})"
|
||||
msgid "CommaFeed version {version} ({revision})."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
@@ -327,6 +347,14 @@ msgstr "Flödes-URL"
|
||||
msgid "Fetch all my feeds now"
|
||||
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
|
||||
msgid "file is required"
|
||||
msgstr "fil krävs"
|
||||
@@ -518,6 +546,7 @@ msgid "Newest first"
|
||||
msgstr "Nyast först"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Next"
|
||||
msgstr "Nästa"
|
||||
|
||||
@@ -627,6 +656,10 @@ msgstr "Lösenorden matchar inte"
|
||||
msgid "Position"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Previous"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Profile"
|
||||
msgstr "Profil"
|
||||
@@ -648,6 +681,7 @@ msgstr "Registreringar är stängda på denna CommaFeed-instans"
|
||||
msgid "REST API"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Right click"
|
||||
msgstr ""
|
||||
@@ -699,11 +733,20 @@ msgstr "Dela"
|
||||
msgid "Sharing sites"
|
||||
msgstr "Delningssajter"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Shift"
|
||||
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
|
||||
msgid "Show entry menu (desktop)"
|
||||
msgstr ""
|
||||
@@ -720,6 +763,10 @@ msgstr "Visa flöden och kategorier utan olästa poster"
|
||||
msgid "Show keyboard shortcut help"
|
||||
msgstr "Visa kortkommandohjälp"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show native menu (desktop)"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
@@ -793,6 +840,14 @@ msgstr "Tema"
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "Växla lässtatus för aktuell post"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle sidebar"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle starred status of current entry"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||
msgstr "Prova CommaFeed med demokontot: demo/demo"
|
||||
|
||||
@@ -15,15 +15,15 @@ msgstr ""
|
||||
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
msgid "{0} (in {1})"
|
||||
msgstr ""
|
||||
msgstr "{0} ({1} içinde)"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr ""
|
||||
msgstr "<0>CommaFeed açık kaynak kodlu bir proje. Kaynak kodları </0><1>GitHub</1>'da."
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1>."
|
||||
msgstr ""
|
||||
msgstr "<0>Tüm sözdizimi </0><1>burada</1>."
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "<0>Have an account?</0><1>Log in!</1>"
|
||||
@@ -31,7 +31,7 @@ msgstr "<0>Hesabınız var mı?</0><1>Giriş yapın!</1>"
|
||||
|
||||
#: src/pages/app/DonatePage.tsx
|
||||
msgid "<0>Hey,</0><1>I'm Jérémie from Belgium and I've been working on CommaFeed in my free time for over 10 years now. Thanks for taking an interest in helping me continue supporting CommaFeed.</1>"
|
||||
msgstr ""
|
||||
msgstr "<0>Merhaba,</0><1>Ben Belçika'dan Jérémie ve 10 yıldır boş zamanlarımda CommaFeed üzerinde çalışıyorum. CommaFeed'i desteklememe ilgi gösterdiğiniz için teşekkürler.</1>"
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
@@ -71,6 +71,10 @@ msgstr "Yönetici"
|
||||
msgid "All"
|
||||
msgstr "Tümü"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgstr "Seçilen girişi her zaman sayfanın üstüne kaydır, ekrana tamamen sığsa bile"
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "An email has been sent if this address was registered. Check your inbox."
|
||||
msgstr "Bu adres kayıtlıysa bir e-posta gönderildi. "
|
||||
@@ -83,6 +87,10 @@ msgstr "Bir opml dosyası, besleme URL'lerini ve kategorilerini içeren bir XML
|
||||
msgid "Analyze feed"
|
||||
msgstr "Feed'i analiz et"
|
||||
|
||||
#: src/components/AnnouncementDialog.tsx
|
||||
msgid "Announcement"
|
||||
msgstr "Duyuru"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "API key"
|
||||
msgstr "API anahtarı"
|
||||
@@ -127,9 +135,13 @@ msgstr "Geri"
|
||||
msgid "Back to log in"
|
||||
msgstr "Giriş yapmak için geri dön"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Browser extension required for Chrome"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Browser extentions"
|
||||
msgstr "Tarayıcı uzantıları"
|
||||
msgid "Browser extention"
|
||||
msgstr "Tarayıcı eklentisi"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
@@ -163,13 +175,21 @@ msgstr "Şifreyi değiştirmek yeni bir API anahtarı oluşturacak"
|
||||
msgid "Check that the feed is working"
|
||||
msgstr "Feed'in çalışıp çalışmadığını kontrol edin"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr "CommaFeed tarayıcı eklentisi sürüm {browserExtensionVersion}."
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed next unread item"
|
||||
msgstr "CommaFeed sonraki okunmamış öğe"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed version {version} ({revision})"
|
||||
msgstr "CommaFeed sürümü {sürüm} ({revizyon})"
|
||||
msgid "CommaFeed version {version} ({revision})."
|
||||
msgstr "CommaFeed sürüm {version} ({revision})."
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Compact"
|
||||
@@ -197,7 +217,7 @@ msgstr "Etiket oluştur: {query}"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Ctrl"
|
||||
msgstr ""
|
||||
msgstr "Ctrl"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Current password"
|
||||
@@ -205,15 +225,15 @@ msgstr "Geçerli şifre"
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Custom code"
|
||||
msgstr ""
|
||||
msgstr "Özel kod"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "Custom CSS rules that will be applied"
|
||||
msgstr ""
|
||||
msgstr "Uygulanacak özel CSS kuralları"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "Custom JS code that will be executed on page load"
|
||||
msgstr ""
|
||||
msgstr "Sayfa yüklendiğinde çalıştırılacak özel JS kodu"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Date created"
|
||||
@@ -311,7 +331,7 @@ msgstr "Aboneliklerinizi ve kategorilerinizi diğer besleme okuma hizmetlerinde
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
msgid "Extension options"
|
||||
msgstr ""
|
||||
msgstr "Eklenti ayarları"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Feed name"
|
||||
@@ -325,6 +345,14 @@ msgstr "Feed URL'si"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Fetch all my feeds now"
|
||||
msgstr "Tüm feed'lerimi şimdi çek"
|
||||
|
||||
#: 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
|
||||
@@ -357,7 +385,7 @@ msgstr "Oluşturulan besleme url'si"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Go to {0}"
|
||||
msgstr ""
|
||||
msgstr "{0}'a git"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Go to the All view"
|
||||
@@ -451,7 +479,7 @@ msgstr "Çıkış"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Long press"
|
||||
msgstr ""
|
||||
msgstr "Uzun bas"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
@@ -483,7 +511,7 @@ msgstr "Metrikler"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Middle click"
|
||||
msgstr ""
|
||||
msgstr "Orta tuş ile tıkla"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Move the page down"
|
||||
@@ -518,6 +546,7 @@ msgid "Newest first"
|
||||
msgstr "Önce en yenisi"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Next"
|
||||
msgstr "Sonraki"
|
||||
|
||||
@@ -547,7 +576,7 @@ msgstr "Hata!"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Open CommaFeed"
|
||||
msgstr ""
|
||||
msgstr "CommaFeed'i aç"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open current entry in a new tab"
|
||||
@@ -563,11 +592,11 @@ msgstr "Bağlantıyı aç"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Open link in new background tab"
|
||||
msgstr ""
|
||||
msgstr "Bağlantıyı arkaplanda yeni sekmede aç"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Open link in new tab"
|
||||
msgstr ""
|
||||
msgstr "Bağlantıyı yeni sekmede aç"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open next entry"
|
||||
@@ -583,7 +612,7 @@ msgstr "Geçerli girişi aç/kapat"
|
||||
|
||||
#: src/pages/app/AddPage.tsx
|
||||
msgid "OPML"
|
||||
msgstr ""
|
||||
msgstr "OPML"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "OPML export"
|
||||
@@ -627,6 +656,10 @@ msgstr "Parolalar eşleşmiyor"
|
||||
msgid "Position"
|
||||
msgstr "Konum"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Previous"
|
||||
msgstr "Önceki"
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Profile"
|
||||
msgstr "Profil"
|
||||
@@ -646,11 +679,12 @@ msgstr "Bu CommaFeed örneğinde kayıtlar kapalı"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "REST API"
|
||||
msgstr ""
|
||||
msgstr "REST API"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Right click"
|
||||
msgstr ""
|
||||
msgstr "Sağ tık"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
@@ -699,18 +733,27 @@ msgstr "Paylaş"
|
||||
msgid "Sharing sites"
|
||||
msgstr "Siteleri paylaşma"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Shift"
|
||||
msgstr "Vardiya"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show CommaFeed's own context menu on right click"
|
||||
msgstr "Sağ tıkta CommaFeed'in kendi menüsünü göster"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show confirmation when marking all entries as read"
|
||||
msgstr "Tüm girişleri okundu işaretlerken onay iste"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show entry menu (desktop)"
|
||||
msgstr ""
|
||||
msgstr "Giriş menüsünü göster (masaüstü)"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show entry menu (mobile)"
|
||||
msgstr ""
|
||||
msgstr "Giriş menüsünü göster (mobil)"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show feeds and categories with no unread entries"
|
||||
@@ -720,6 +763,10 @@ msgstr "Okunmamış girişi olmayan beslemeleri ve kategorileri göster"
|
||||
msgid "Show keyboard shortcut help"
|
||||
msgstr "Klavye kısayolu yardımını göster"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show native menu (desktop)"
|
||||
msgstr "Orijinal tarayıcı menüsünü göster (masaüstü)"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
@@ -765,7 +812,7 @@ msgstr "Başarı"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Swipe header to the right"
|
||||
msgstr ""
|
||||
msgstr "Başlığı sağa kaydır"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
@@ -775,7 +822,7 @@ msgstr "Karanlık temaya geç"
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
msgid "Switch to light theme"
|
||||
msgstr "Açık temaya geç"
|
||||
msgstr "Aydınlık temaya geç"
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Tags"
|
||||
@@ -793,17 +840,25 @@ msgstr "Tema"
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "Geçerli girişin okuma durumunu değiştir"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle sidebar"
|
||||
msgstr "Kenar çubuğunu göster/gizle"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle starred status of current entry"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||
msgstr "CommaFeed'i demo hesabıyla deneyin: demo/demo"
|
||||
|
||||
#: src/pages/WelcomePage.tsx
|
||||
msgid "Try the demo!"
|
||||
msgstr ""
|
||||
msgstr "Demo'yu deneyin!"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Unread"
|
||||
msgstr "Okunmadı"
|
||||
msgstr "Okunmamış"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
@@ -834,8 +889,8 @@ msgstr "Web sitesi"
|
||||
|
||||
#: src/pages/app/FeedEntriesPage.tsx
|
||||
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
||||
msgstr "Henüz aboneliğiniz yok. "
|
||||
msgstr "Henüz aboneliğiniz yok. Sayfanın üstündeki + işaretiyle feed ekleyebilirsiniz."
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Your feeds have been queued for refresh."
|
||||
msgstr ""
|
||||
msgstr "Feed'leriniz yenileme için sıraya alındı."
|
||||
|
||||
@@ -71,6 +71,10 @@ msgstr "管理员"
|
||||
msgid "All"
|
||||
msgstr "全部"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "An email has been sent if this address was registered. Check your inbox."
|
||||
msgstr "如果此地址已注册,则已发送电子邮件。"
|
||||
@@ -83,6 +87,10 @@ msgstr "opml 文件是包含提要 URL 和类别的 XML 文件。"
|
||||
msgid "Analyze feed"
|
||||
msgstr "分析饲料"
|
||||
|
||||
#: src/components/AnnouncementDialog.tsx
|
||||
msgid "Announcement"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "API key"
|
||||
msgstr "API 密钥"
|
||||
@@ -127,9 +135,13 @@ msgstr "返回"
|
||||
msgid "Back to log in"
|
||||
msgstr "返回登录"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Browser extension required for Chrome"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Browser extentions"
|
||||
msgstr "浏览器扩展"
|
||||
msgid "Browser extention"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
@@ -163,13 +175,21 @@ msgstr "更改密码将生成新的 API 密钥"
|
||||
msgid "Check that the feed is working"
|
||||
msgstr "检查提要是否正常工作"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. The username is your user name and the password is your API key."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed next unread item"
|
||||
msgstr "CommaFeed 下一个未读项目"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed version {version} ({revision})"
|
||||
msgstr "CommaFeed 版本 {version} ({revision})"
|
||||
msgid "CommaFeed version {version} ({revision})."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Compact"
|
||||
@@ -327,6 +347,14 @@ msgstr "供稿网址"
|
||||
msgid "Fetch all my feeds now"
|
||||
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
|
||||
msgid "file is required"
|
||||
msgstr "文件是必需的"
|
||||
@@ -518,6 +546,7 @@ msgid "Newest first"
|
||||
msgstr "最新优先"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Next"
|
||||
msgstr "下一个"
|
||||
|
||||
@@ -627,6 +656,10 @@ msgstr "密码不匹配"
|
||||
msgid "Position"
|
||||
msgstr "位置"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Previous"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Profile"
|
||||
msgstr "配置文件"
|
||||
@@ -648,6 +681,7 @@ msgstr "此 CommaFeed 实例上的注册已关闭"
|
||||
msgid "REST API"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Right click"
|
||||
msgstr ""
|
||||
@@ -699,11 +733,20 @@ msgstr "分享"
|
||||
msgid "Sharing sites"
|
||||
msgstr "共享站点"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Shift"
|
||||
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
|
||||
msgid "Show entry menu (desktop)"
|
||||
msgstr ""
|
||||
@@ -720,6 +763,10 @@ msgstr "显示没有未读条目的提要和类别"
|
||||
msgid "Show keyboard shortcut help"
|
||||
msgstr "显示键盘快捷键帮助"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show native menu (desktop)"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
@@ -793,6 +840,14 @@ msgstr "主题"
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "切换当前条目的读取状态"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle sidebar"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle starred status of current entry"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||
msgstr "使用演示帐户试用 CommaFeed:demo/demo"
|
||||
|
||||
@@ -1,23 +1,32 @@
|
||||
import { Trans } from "@lingui/macro"
|
||||
import { Anchor, Box, Center, Container, Divider, Group, Image, Title, useMantineColorScheme } from "@mantine/core"
|
||||
import { useMediaQuery } from "@mantine/hooks"
|
||||
import { client } from "app/client"
|
||||
import { Constants } from "app/constants"
|
||||
import { redirectToApiDocumentation, redirectToLogin, redirectToRegistration, redirectToRootCategory } from "app/slices/redirect"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import welcome_page_dark from "assets/welcome_page_dark.png"
|
||||
import welcome_page_light from "assets/welcome_page_light.png"
|
||||
import { ActionButton } from "components/ActionButtton"
|
||||
import { ButtonToolbar } from "components/ButtonToolbar"
|
||||
import { ActionButton } from "components/ActionButton"
|
||||
import { useBrowserExtension } from "hooks/useBrowserExtension"
|
||||
import { useMobile } from "hooks/useMobile"
|
||||
import { useAsyncCallback } from "react-async-hook"
|
||||
import { SiGithub, SiTwitter } from "react-icons/si"
|
||||
import { TbClock, TbKey, TbMoon, TbSettings, TbSun, TbUserPlus } from "react-icons/tb"
|
||||
import { PageTitle } from "./PageTitle"
|
||||
|
||||
const iconSize = 18
|
||||
|
||||
export function WelcomePage() {
|
||||
const serverInfos = useAppSelector(state => state.server.serverInfos)
|
||||
const { colorScheme } = useMantineColorScheme()
|
||||
const dispatch = useAppDispatch()
|
||||
const image = colorScheme === "light" ? welcome_page_light : welcome_page_dark
|
||||
|
||||
const login = useAsyncCallback(client.user.login, {
|
||||
onSuccess: () => {
|
||||
dispatch(redirectToRootCategory())
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Header />
|
||||
@@ -26,6 +35,18 @@ export function WelcomePage() {
|
||||
<Title order={3}>Bloat-free feed reader</Title>
|
||||
</Center>
|
||||
|
||||
{serverInfos?.demoAccountEnabled && (
|
||||
<Center>
|
||||
<ActionButton
|
||||
label={<Trans>Try the demo!</Trans>}
|
||||
icon={<TbClock size={iconSize} />}
|
||||
variant="outline"
|
||||
onClick={() => login.execute({ name: "demo", password: "demo" })}
|
||||
showLabelOnMobile
|
||||
/>
|
||||
</Center>
|
||||
)}
|
||||
|
||||
<Divider my="xl" />
|
||||
|
||||
<Image src={image} />
|
||||
@@ -38,7 +59,7 @@ export function WelcomePage() {
|
||||
}
|
||||
|
||||
function Header() {
|
||||
const mobile = !useMediaQuery(`(min-width: ${Constants.layout.mobileBreakpoint})`)
|
||||
const mobile = useMobile()
|
||||
|
||||
if (mobile) {
|
||||
return (
|
||||
@@ -60,30 +81,14 @@ function Header() {
|
||||
}
|
||||
|
||||
function Buttons() {
|
||||
const iconSize = 18
|
||||
const serverInfos = useAppSelector(state => state.server.serverInfos)
|
||||
const { colorScheme, toggleColorScheme } = useMantineColorScheme()
|
||||
const { isBrowserExtension, openSettingsPage } = useBrowserExtension()
|
||||
const { isBrowserExtensionPopup, openSettingsPage } = useBrowserExtension()
|
||||
const dispatch = useAppDispatch()
|
||||
const dark = colorScheme === "dark"
|
||||
|
||||
const login = useAsyncCallback(client.user.login, {
|
||||
onSuccess: () => {
|
||||
dispatch(redirectToRootCategory())
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<ButtonToolbar>
|
||||
{serverInfos?.demoAccountEnabled && (
|
||||
<ActionButton
|
||||
label={<Trans>Try the demo!</Trans>}
|
||||
icon={<TbClock size={iconSize} />}
|
||||
variant="outline"
|
||||
onClick={() => login.execute({ name: "demo", password: "demo" })}
|
||||
showLabelOnMobile
|
||||
/>
|
||||
)}
|
||||
<Group spacing={14}>
|
||||
<ActionButton
|
||||
label={<Trans>Log in</Trans>}
|
||||
icon={<TbKey size={iconSize} />}
|
||||
@@ -108,7 +113,7 @@ function Buttons() {
|
||||
hideLabelOnDesktop
|
||||
/>
|
||||
|
||||
{isBrowserExtension && (
|
||||
{isBrowserExtensionPopup && (
|
||||
<ActionButton
|
||||
label={<Trans>Extension options</Trans>}
|
||||
icon={<TbSettings size={iconSize} />}
|
||||
@@ -116,7 +121,7 @@ function Buttons() {
|
||||
hideLabelOnDesktop
|
||||
/>
|
||||
)}
|
||||
</ButtonToolbar>
|
||||
</Group>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,12 +14,15 @@ const shownMeters: { [key: string]: string } = {
|
||||
"com.commafeed.backend.feed.FeedRefreshUpdater.feedUpdated": "Feed update rate",
|
||||
"com.commafeed.backend.feed.FeedRefreshUpdater.entryCacheHit": "Entry cache hit rate",
|
||||
"com.commafeed.backend.feed.FeedRefreshUpdater.entryCacheMiss": "Entry cache miss rate",
|
||||
"com.commafeed.backend.service.DatabaseCleaningService.entriesDeleted": "Entries deleted",
|
||||
}
|
||||
|
||||
const shownGauges: { [key: string]: string } = {
|
||||
"com.commafeed.backend.feed.FeedRefreshEngine.queue.size": "Queue size",
|
||||
"com.commafeed.backend.feed.FeedRefreshEngine.worker.active": "Feed Worker active",
|
||||
"com.commafeed.backend.feed.FeedRefreshEngine.updater.active": "Feed Updater active",
|
||||
"com.commafeed.frontend.ws.WebSocketSessions.users": "WebSocket users",
|
||||
"com.commafeed.frontend.ws.WebSocketSessions.sessions": "WebSocket sessions",
|
||||
}
|
||||
|
||||
export function MetricsPage() {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { redirectToApiDocumentation } from "app/slices/redirect"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import { CategorySelect } from "components/content/add/CategorySelect"
|
||||
import { KeyboardShortcutsHelp } from "components/KeyboardShortcutsHelp"
|
||||
import { useBrowserExtension } from "hooks/useBrowserExtension"
|
||||
import React, { useState } from "react"
|
||||
import { TbHelp, TbKeyboard, TbPuzzle, TbRocket } from "react-icons/tb"
|
||||
|
||||
@@ -60,16 +61,23 @@ function NextUnreadBookmarklet() {
|
||||
export function AboutPage() {
|
||||
const version = useAppSelector(state => state.server.serverInfos?.version)
|
||||
const revision = useAppSelector(state => state.server.serverInfos?.gitCommit)
|
||||
const { isBrowserExtensionInstalled, browserExtensionVersion, isBrowserExtensionInstallable } = useBrowserExtension()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
return (
|
||||
<Container size="xl">
|
||||
<SimpleGrid cols={2} breakpoints={[{ maxWidth: Constants.layout.mobileBreakpoint, cols: 1 }]}>
|
||||
<Section title={<Trans>About</Trans>} icon={<TbHelp size={24} />}>
|
||||
<Box>
|
||||
<Trans>
|
||||
CommaFeed version {version} ({revision})
|
||||
CommaFeed version {version} ({revision}).
|
||||
</Trans>
|
||||
</Box>
|
||||
{isBrowserExtensionInstallable && isBrowserExtensionInstalled && (
|
||||
<Box>
|
||||
<Trans>CommaFeed browser extension version {browserExtensionVersion}.</Trans>
|
||||
</Box>
|
||||
)}
|
||||
<Box mt="md">
|
||||
<Trans>
|
||||
<span>CommaFeed is an open-source project. Sources are hosted on </span>
|
||||
@@ -86,8 +94,8 @@ export function AboutPage() {
|
||||
<Section title={<Trans>Goodies</Trans>} icon={<TbPuzzle size={24} />}>
|
||||
<List>
|
||||
<List.Item>
|
||||
<Anchor href="https://github.com/Athou/commafeed-browser-extension" target="_blank" rel="noreferrer">
|
||||
<Trans>Browser extentions</Trans>
|
||||
<Anchor href={Constants.browserExtensionUrl} target="_blank" rel="noreferrer">
|
||||
<Trans>Browser extention</Trans>
|
||||
</Anchor>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { Box } from "@mantine/core"
|
||||
import SwaggerUI from "swagger-ui-react"
|
||||
import "swagger-ui-react/swagger-ui.css"
|
||||
|
||||
function ApiDocumentationPage() {
|
||||
return <SwaggerUI url="swagger/swagger.json" />
|
||||
return (
|
||||
// force white background because swagger is unreadable with dark theme
|
||||
<Box style={{ backgroundColor: "#fff" }}>
|
||||
<SwaggerUI url="swagger/swagger.json" />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default ApiDocumentationPage
|
||||
|
||||
@@ -13,41 +13,53 @@ import {
|
||||
Title,
|
||||
useMantineTheme,
|
||||
} from "@mantine/core"
|
||||
import { useViewportSize } from "@mantine/hooks"
|
||||
import { Constants } from "app/constants"
|
||||
import { redirectToAdd, redirectToRootCategory } from "app/slices/redirect"
|
||||
import { reloadTree, setMobileMenuOpen } from "app/slices/tree"
|
||||
import { reloadTree, setMobileMenuOpen, setSidebarWidth } from "app/slices/tree"
|
||||
import { reloadProfile, reloadSettings, reloadTags } from "app/slices/user"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import { AnnouncementDialog } from "components/AnnouncementDialog"
|
||||
import { Loader } from "components/Loader"
|
||||
import { Logo } from "components/Logo"
|
||||
import { OnDesktop } from "components/responsive/OnDesktop"
|
||||
import { OnMobile } from "components/responsive/OnMobile"
|
||||
import { useAppLoading } from "hooks/useAppLoading"
|
||||
import { useMobile } from "hooks/useMobile"
|
||||
import { useWebSocket } from "hooks/useWebSocket"
|
||||
import { LoadingPage } from "pages/LoadingPage"
|
||||
import { Resizable } from "re-resizable"
|
||||
import { ReactNode, Suspense, useEffect } from "react"
|
||||
import { TbPlus } from "react-icons/tb"
|
||||
import { Outlet } from "react-router-dom"
|
||||
|
||||
interface LayoutProps {
|
||||
sidebar: ReactNode
|
||||
sidebarWidth: number
|
||||
header: ReactNode
|
||||
}
|
||||
|
||||
const sidebarPadding = DEFAULT_THEME.spacing.xs
|
||||
const sidebarRightBorderWidth = "1px"
|
||||
|
||||
const useStyles = createStyles(theme => ({
|
||||
const useStyles = createStyles((theme, props: LayoutProps) => ({
|
||||
sidebar: {
|
||||
"& .mantine-ScrollArea-scrollbar[data-orientation='horizontal']": {
|
||||
display: "none",
|
||||
},
|
||||
},
|
||||
sidebarContentResizeWrapper: {
|
||||
padding: sidebarPadding,
|
||||
minHeight: `calc(100vh - ${Constants.layout.headerHeight}px)`,
|
||||
},
|
||||
sidebarContent: {
|
||||
maxWidth: `calc(${Constants.layout.sidebarWidth}px - ${sidebarPadding} * 2 - ${sidebarRightBorderWidth})`,
|
||||
maxWidth: `calc(${props.sidebarWidth}px - ${sidebarPadding} * 2 - ${sidebarRightBorderWidth})`,
|
||||
[theme.fn.smallerThan(Constants.layout.mobileBreakpoint)]: {
|
||||
maxWidth: `calc(100vw - ${sidebarPadding} * 2 - ${sidebarRightBorderWidth})`,
|
||||
},
|
||||
},
|
||||
mainContentWrapper: {
|
||||
paddingTop: Constants.layout.headerHeight,
|
||||
paddingLeft: Constants.layout.sidebarWidth,
|
||||
paddingLeft: props.sidebarWidth,
|
||||
paddingRight: 0,
|
||||
paddingBottom: 0,
|
||||
[theme.fn.smallerThan(Constants.layout.mobileBreakpoint)]: {
|
||||
@@ -55,7 +67,7 @@ const useStyles = createStyles(theme => ({
|
||||
},
|
||||
},
|
||||
mainContent: {
|
||||
maxWidth: `calc(100vw - ${Constants.layout.sidebarWidth}px)`,
|
||||
maxWidth: `calc(100vw - ${props.sidebarWidth}px)`,
|
||||
padding: theme.spacing.md,
|
||||
[theme.fn.smallerThan(Constants.layout.mobileBreakpoint)]: {
|
||||
maxWidth: "100vw",
|
||||
@@ -76,26 +88,35 @@ function LogoAndTitle() {
|
||||
)
|
||||
}
|
||||
|
||||
export default function Layout({ sidebar, header }: LayoutProps) {
|
||||
const { classes } = useStyles()
|
||||
export default function Layout(props: LayoutProps) {
|
||||
const { classes } = useStyles(props)
|
||||
const theme = useMantineTheme()
|
||||
const viewport = useViewportSize()
|
||||
const { loading } = useAppLoading()
|
||||
const mobile = useMobile()
|
||||
const mobileMenuOpen = useAppSelector(state => state.tree.mobileMenuOpen)
|
||||
const webSocketConnected = useAppSelector(state => state.server.webSocketConnected)
|
||||
const sidebarHidden = props.sidebarWidth === 0
|
||||
const dispatch = useAppDispatch()
|
||||
useWebSocket()
|
||||
|
||||
const handleResize = (element: HTMLElement) => dispatch(setSidebarWidth(element.offsetWidth))
|
||||
|
||||
useEffect(() => {
|
||||
// load initial data
|
||||
dispatch(reloadSettings())
|
||||
dispatch(reloadProfile())
|
||||
dispatch(reloadTree())
|
||||
dispatch(reloadTags())
|
||||
|
||||
// reload tree periodically
|
||||
const id = setInterval(() => dispatch(reloadTree()), 30000)
|
||||
return () => clearInterval(id)
|
||||
}, [dispatch])
|
||||
|
||||
useEffect(() => {
|
||||
// reload tree periodically if not receiving websocket events
|
||||
const timer = setInterval(() => {
|
||||
if (!webSocketConnected) dispatch(reloadTree())
|
||||
}, 30000)
|
||||
return () => clearInterval(timer)
|
||||
}, [dispatch, webSocketConnected])
|
||||
|
||||
const burger = (
|
||||
<Center>
|
||||
<Burger
|
||||
@@ -122,13 +143,29 @@ export default function Layout({ sidebar, header }: LayoutProps) {
|
||||
navbar={
|
||||
<Navbar
|
||||
id="sidebar"
|
||||
p={sidebarPadding}
|
||||
hiddenBreakpoint={Constants.layout.mobileBreakpoint}
|
||||
hidden={!mobileMenuOpen}
|
||||
width={{ md: Constants.layout.sidebarWidth }}
|
||||
hiddenBreakpoint={sidebarHidden ? 99999999 : Constants.layout.mobileBreakpoint}
|
||||
hidden={sidebarHidden || !mobileMenuOpen}
|
||||
width={{ md: props.sidebarWidth }}
|
||||
className={classes.sidebar}
|
||||
>
|
||||
<Navbar.Section grow component={ScrollArea} mx="-xs" px="xs">
|
||||
<Box className={classes.sidebarContent}>{sidebar}</Box>
|
||||
<Navbar.Section grow component={ScrollArea} mx={mobile ? 0 : "-sm"} px={mobile ? 0 : "sm"}>
|
||||
<Resizable
|
||||
enable={{
|
||||
top: false,
|
||||
right: !mobile,
|
||||
bottom: false,
|
||||
left: false,
|
||||
topRight: false,
|
||||
bottomRight: false,
|
||||
bottomLeft: false,
|
||||
topLeft: false,
|
||||
}}
|
||||
onResize={(e, dir, el) => handleResize(el)}
|
||||
minWidth={120}
|
||||
className={classes.sidebarContentResizeWrapper}
|
||||
>
|
||||
<Box className={classes.sidebarContent}>{props.sidebar}</Box>
|
||||
</Resizable>
|
||||
</Navbar.Section>
|
||||
</Navbar>
|
||||
}
|
||||
@@ -146,37 +183,31 @@ export default function Layout({ sidebar, header }: LayoutProps) {
|
||||
)}
|
||||
{!mobileMenuOpen && (
|
||||
<Group>
|
||||
<Box mr="sm">{burger}</Box>
|
||||
<Box sx={{ flexGrow: 1 }}>{header}</Box>
|
||||
<Box>{burger}</Box>
|
||||
<Box sx={{ flexGrow: 1 }}>{props.header}</Box>
|
||||
</Group>
|
||||
)}
|
||||
</OnMobile>
|
||||
<OnDesktop>
|
||||
<Group>
|
||||
<Group position="apart" sx={{ width: Constants.layout.sidebarWidth - 16 }}>
|
||||
<Group position="apart" sx={{ width: props.sidebarWidth - 16 }}>
|
||||
<Box>
|
||||
<LogoAndTitle />
|
||||
</Box>
|
||||
<Box>{addButton}</Box>
|
||||
</Group>
|
||||
<Box sx={{ flexGrow: 1 }}>{header}</Box>
|
||||
<Box sx={{ flexGrow: 1 }}>{props.header}</Box>
|
||||
</Group>
|
||||
</OnDesktop>
|
||||
</Header>
|
||||
}
|
||||
>
|
||||
<ScrollArea
|
||||
sx={{ height: viewport.height - Constants.layout.headerHeight }}
|
||||
viewportRef={ref => {
|
||||
if (ref) ref.id = Constants.dom.mainScrollAreaId
|
||||
}}
|
||||
>
|
||||
<Box id="content" className={classes.mainContent}>
|
||||
<Suspense fallback={<Loader />}>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</Box>
|
||||
</ScrollArea>
|
||||
<Box id="content" className={classes.mainContent}>
|
||||
<Suspense fallback={<Loader />}>
|
||||
<AnnouncementDialog />
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</Box>
|
||||
</AppShell>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { TbCode, TbPhoto, TbUser } from "react-icons/tb"
|
||||
export function SettingsPage() {
|
||||
return (
|
||||
<Container size="sm" px={0}>
|
||||
<Tabs defaultValue="display">
|
||||
<Tabs defaultValue="display" keepMounted={false}>
|
||||
<Tabs.List>
|
||||
<Tabs.Tab value="display" icon={<TbPhoto size={16} />}>
|
||||
<Trans>Display</Trans>
|
||||
|
||||
@@ -1,13 +1,41 @@
|
||||
import { lingui } from "@lingui/vite-plugin"
|
||||
import react from "@vitejs/plugin-react"
|
||||
import { visualizer } from "rollup-plugin-visualizer"
|
||||
import { defineConfig } from "vite"
|
||||
import { defineConfig, PluginOption } from "vite"
|
||||
import eslint from "vite-plugin-eslint"
|
||||
import tsconfigPaths from "vite-tsconfig-paths"
|
||||
|
||||
// inject custom js and css links in html
|
||||
const customCodeInjector: PluginOption = {
|
||||
name: "customCodeInjector",
|
||||
transformIndexHtml: html => {
|
||||
return {
|
||||
html,
|
||||
tags: [
|
||||
{
|
||||
tag: "script",
|
||||
attrs: {
|
||||
src: "custom_js.js",
|
||||
},
|
||||
injectTo: "body",
|
||||
},
|
||||
{
|
||||
tag: "link",
|
||||
attrs: {
|
||||
rel: "stylesheet",
|
||||
href: "custom_css.css",
|
||||
},
|
||||
injectTo: "head",
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
customCodeInjector,
|
||||
react({
|
||||
babel: {
|
||||
// babel-macro is needed for lingui
|
||||
@@ -32,7 +60,7 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
build: {
|
||||
chunkSizeWarningLimit: 1000,
|
||||
chunkSizeWarningLimit: 3000,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: id => {
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
app:
|
||||
# url used to access commafeed
|
||||
publicUrl: http://localhost:8082/
|
||||
|
||||
# whether to expose a robots.txt file that disallows web crawlers and search engine indexers
|
||||
hideFromWebCrawlers: true
|
||||
|
||||
# whether to allow user registrations
|
||||
allowRegistrations: true
|
||||
@@ -24,7 +27,10 @@ app:
|
||||
|
||||
# number of database updating threads
|
||||
databaseUpdateThreads: 1
|
||||
|
||||
|
||||
# rows to delete per query while cleaning up old entries
|
||||
databaseCleanupBatchSize: 100
|
||||
|
||||
# settings for sending emails (password recovery)
|
||||
smtpHost: localhost
|
||||
smtpPort: 25
|
||||
@@ -78,8 +84,12 @@ app:
|
||||
|
||||
# Database connection
|
||||
# -------------------
|
||||
# for MariaDB
|
||||
# driverClass is org.mariadb.jdbc.Driver
|
||||
# url is jdbc:mariadb://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true
|
||||
#
|
||||
# for MySQL
|
||||
# driverClass is com.mysql.jdbc.Driver
|
||||
# driverClass is com.mysql.cj.jdbc.Driver
|
||||
# url is jdbc:mysql://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true
|
||||
#
|
||||
# for PostgreSQL
|
||||
@@ -114,7 +124,6 @@ logging:
|
||||
liquibase: INFO
|
||||
org.hibernate.SQL: INFO # or ALL for sql debugging
|
||||
org.hibernate.engine.internal.StatisticalLoggingSessionEventListener: WARN
|
||||
org.hibernate.orm.deprecation: "OFF"
|
||||
appenders:
|
||||
- type: console
|
||||
- type: file
|
||||
|
||||
@@ -3,28 +3,34 @@
|
||||
app:
|
||||
# url used to access commafeed
|
||||
publicUrl: http://localhost:8082/
|
||||
|
||||
|
||||
# whether to expose a robots.txt file that disallows web crawlers and search engine indexers
|
||||
hideFromWebCrawlers: true
|
||||
|
||||
# whether to allow user registrations
|
||||
allowRegistrations: false
|
||||
|
||||
# whether to enable strict password validation (1 uppercase char, 1 lowercase char, 1 digit, 1 special char)
|
||||
strictPasswordPolicy: true
|
||||
|
||||
|
||||
# create a demo account the first time the app starts
|
||||
createDemoAccount: false
|
||||
|
||||
|
||||
# put your google analytics tracking code here
|
||||
googleAnalyticsTrackingCode:
|
||||
|
||||
|
||||
# put your google server key (used for youtube favicon fetching)
|
||||
googleAuthKey:
|
||||
|
||||
|
||||
# number of http threads
|
||||
backgroundThreads: 3
|
||||
|
||||
|
||||
# number of database updating threads
|
||||
databaseUpdateThreads: 1
|
||||
|
||||
|
||||
# rows to delete per query while cleaning up old entries
|
||||
databaseCleanupBatchSize: 100
|
||||
|
||||
# settings for sending emails (password recovery)
|
||||
smtpHost:
|
||||
smtpPort:
|
||||
@@ -40,28 +46,28 @@ app:
|
||||
graphiteHost: "localhost"
|
||||
graphitePort: 2003
|
||||
graphiteInterval: 60
|
||||
|
||||
|
||||
# whether this commafeed instance has a lot of feeds to refresh
|
||||
# leave this to false in almost all cases
|
||||
heavyLoad: false
|
||||
|
||||
|
||||
# minimum amount of time commafeed will wait before refreshing the same feed
|
||||
refreshIntervalMinutes: 5
|
||||
|
||||
|
||||
# whether to enable pubsub
|
||||
# probably not needed if refreshIntervalMinutes is low
|
||||
pubsubhubbub: false
|
||||
|
||||
|
||||
# if enabled, images in feed entries will be proxied through the server instead of accessed directly by the browser
|
||||
# useful if commafeed is usually accessed through a restricting proxy
|
||||
# useful if commafeed is usually accessed through a restricting proxy
|
||||
imageProxyEnabled: false
|
||||
|
||||
|
||||
# database query timeout (in milliseconds), 0 to disable
|
||||
queryTimeout: 0
|
||||
|
||||
|
||||
# time to keep unread statuses (in days), 0 to disable
|
||||
keepStatusDays: 0
|
||||
|
||||
|
||||
# entries to keep per feed, old entries will be deleted, 0 to disable
|
||||
maxFeedCapacity: 500
|
||||
|
||||
@@ -70,17 +76,21 @@ app:
|
||||
|
||||
# cache service to use, possible values are 'noop' and 'redis'
|
||||
cache: noop
|
||||
|
||||
|
||||
# announcement string displayed on the main page
|
||||
announcement:
|
||||
|
||||
|
||||
# user-agent string that will be used by the http client, leave empty for the default one
|
||||
userAgent:
|
||||
|
||||
|
||||
# Database connection
|
||||
# -------------------
|
||||
# -------------------
|
||||
# for MariaDB
|
||||
# driverClass is org.mariadb.jdbc.Driver
|
||||
# url is jdbc:mariadb://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true
|
||||
#
|
||||
# for MySQL
|
||||
# driverClass is com.mysql.jdbc.Driver
|
||||
# driverClass is com.mysql.cj.jdbc.Driver
|
||||
# url is jdbc:mysql://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true
|
||||
#
|
||||
# for PostgreSQL
|
||||
@@ -93,7 +103,7 @@ app:
|
||||
|
||||
database:
|
||||
driverClass: org.h2.Driver
|
||||
url: jdbc:h2:./db/commafeed
|
||||
url: jdbc:h2:/commafeed/data/db
|
||||
user: sa
|
||||
password: sa
|
||||
properties:
|
||||
@@ -102,7 +112,7 @@ database:
|
||||
minSize: 1
|
||||
maxSize: 50
|
||||
maxConnectionAge: 30m
|
||||
|
||||
|
||||
server:
|
||||
applicationConnectors:
|
||||
- type: http
|
||||
@@ -112,14 +122,13 @@ server:
|
||||
port: 8084
|
||||
requestLog:
|
||||
appenders: [ ]
|
||||
|
||||
|
||||
logging:
|
||||
level: ERROR
|
||||
loggers:
|
||||
com.commafeed: INFO
|
||||
liquibase: INFO
|
||||
io.dropwizard.server.ServerFactory: INFO
|
||||
org.hibernate.orm.deprecation: "OFF"
|
||||
appenders:
|
||||
- type: console
|
||||
- type: file
|
||||
@@ -129,7 +138,7 @@ logging:
|
||||
archivedLogFilenamePattern: log/commafeed-%d.log
|
||||
archivedFileCount: 5
|
||||
timeZone: UTC
|
||||
|
||||
|
||||
# Redis pool configuration
|
||||
# (only used if app.cache is 'redis')
|
||||
# -----------------------------------
|
||||
@@ -138,8 +147,7 @@ redis:
|
||||
port: 6379
|
||||
# username is only required when using ACLs
|
||||
username:
|
||||
password:
|
||||
password:
|
||||
timeout: 2000
|
||||
database: 0
|
||||
maxTotal: 500
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>com.commafeed</groupId>
|
||||
<artifactId>commafeed</artifactId>
|
||||
<version>3.6.0</version>
|
||||
<version>3.10.1</version>
|
||||
</parent>
|
||||
<artifactId>commafeed-server</artifactId>
|
||||
<name>CommaFeed Server</name>
|
||||
@@ -142,6 +142,12 @@
|
||||
<title>CommaFeed</title>
|
||||
<version>${project.version}</version>
|
||||
</info>
|
||||
<securityDefinitions>
|
||||
<securityDefinition>
|
||||
<name>basicAuth</name>
|
||||
<type>basic</type>
|
||||
</securityDefinition>
|
||||
</securityDefinitions>
|
||||
<typesToSkip>
|
||||
<typeToSkip>com.commafeed.backend.model.User</typeToSkip>
|
||||
</typesToSkip>
|
||||
@@ -226,13 +232,13 @@
|
||||
<dependency>
|
||||
<groupId>com.commafeed</groupId>
|
||||
<artifactId>commafeed-client</artifactId>
|
||||
<version>3.6.0</version>
|
||||
<version>3.10.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.26</version>
|
||||
<version>1.18.30</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
@@ -444,6 +450,11 @@
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<version>8.0.33</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mariadb.jdbc</groupId>
|
||||
<artifactId>mariadb-java-client</artifactId>
|
||||
<version>3.1.4</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
@@ -494,4 +505,4 @@
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
</project>
|
||||
</project>
|
||||
|
||||
@@ -42,11 +42,13 @@ import com.commafeed.frontend.resource.FeedREST;
|
||||
import com.commafeed.frontend.resource.PubSubHubbubCallbackREST;
|
||||
import com.commafeed.frontend.resource.ServerREST;
|
||||
import com.commafeed.frontend.resource.UserREST;
|
||||
import com.commafeed.frontend.resource.fever.FeverREST;
|
||||
import com.commafeed.frontend.servlet.AnalyticsServlet;
|
||||
import com.commafeed.frontend.servlet.CustomCssServlet;
|
||||
import com.commafeed.frontend.servlet.CustomJsServlet;
|
||||
import com.commafeed.frontend.servlet.LogoutServlet;
|
||||
import com.commafeed.frontend.servlet.NextUnreadServlet;
|
||||
import com.commafeed.frontend.servlet.RobotsTxtDisallowAllServlet;
|
||||
import com.commafeed.frontend.session.SessionHelperFactoryProvider;
|
||||
import com.commafeed.frontend.ws.WebSocketConfigurator;
|
||||
import com.commafeed.frontend.ws.WebSocketEndpoint;
|
||||
@@ -146,7 +148,7 @@ public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
|
||||
Injector injector = Guice.createInjector(new CommaFeedModule(hibernateBundle.getSessionFactory(), config, environment.metrics()));
|
||||
|
||||
// session management
|
||||
environment.servlets().setSessionHandler(config.getSessionHandlerFactory().build());
|
||||
environment.servlets().setSessionHandler(config.getSessionHandlerFactory().build(config.getDataSourceFactory()));
|
||||
|
||||
// support for "@SecurityCheck User user" injection
|
||||
environment.jersey().register(new SecurityCheckFactoryProvider.Binder(injector.getInstance(UserService.class)));
|
||||
@@ -162,6 +164,7 @@ public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
|
||||
environment.jersey().register(injector.getInstance(PubSubHubbubCallbackREST.class));
|
||||
environment.jersey().register(injector.getInstance(ServerREST.class));
|
||||
environment.jersey().register(injector.getInstance(UserREST.class));
|
||||
environment.jersey().register(injector.getInstance(FeverREST.class));
|
||||
|
||||
// Servlets
|
||||
environment.servlets().addServlet("next", injector.getInstance(NextUnreadServlet.class)).addMapping("/next");
|
||||
@@ -169,6 +172,11 @@ public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
|
||||
environment.servlets().addServlet("customCss", injector.getInstance(CustomCssServlet.class)).addMapping("/custom_css.css");
|
||||
environment.servlets().addServlet("customJs", injector.getInstance(CustomJsServlet.class)).addMapping("/custom_js.js");
|
||||
environment.servlets().addServlet("analytics.js", injector.getInstance(AnalyticsServlet.class)).addMapping("/analytics.js");
|
||||
if (Boolean.TRUE.equals(config.getApplicationSettings().getHideFromWebCrawlers())) {
|
||||
environment.servlets()
|
||||
.addServlet("robots.txt", injector.getInstance(RobotsTxtDisallowAllServlet.class))
|
||||
.addMapping("/robots.txt");
|
||||
}
|
||||
|
||||
// WebSocket endpoint
|
||||
ServerEndpointConfig serverEndpointConfig = ServerEndpointConfig.Builder.create(WebSocketEndpoint.class, "/ws")
|
||||
|
||||
@@ -7,6 +7,7 @@ import javax.validation.Valid;
|
||||
import javax.validation.constraints.Min;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Positive;
|
||||
|
||||
import org.apache.commons.lang3.time.DateUtils;
|
||||
|
||||
@@ -65,6 +66,10 @@ public class CommaFeedConfiguration extends Configuration {
|
||||
@Valid
|
||||
private String publicUrl;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
private Boolean hideFromWebCrawlers = true;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
private Boolean allowRegistrations;
|
||||
@@ -91,6 +96,11 @@ public class CommaFeedConfiguration extends Configuration {
|
||||
@Valid
|
||||
private Integer databaseUpdateThreads;
|
||||
|
||||
@NotNull
|
||||
@Positive
|
||||
@Valid
|
||||
private Integer databaseCleanupBatchSize = 100;
|
||||
|
||||
private String smtpHost;
|
||||
private int smtpPort;
|
||||
private boolean smtpTls;
|
||||
|
||||
@@ -15,23 +15,30 @@ import com.commafeed.backend.model.QFeed;
|
||||
import com.commafeed.backend.model.QFeedSubscription;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.querydsl.jpa.JPAExpressions;
|
||||
import com.querydsl.jpa.impl.JPAQuery;
|
||||
|
||||
@Singleton
|
||||
public class FeedDAO extends GenericDAO<Feed> {
|
||||
|
||||
private final QFeed feed = QFeed.feed;
|
||||
private final QFeedSubscription subscription = QFeedSubscription.feedSubscription;
|
||||
|
||||
@Inject
|
||||
public FeedDAO(SessionFactory sessionFactory) {
|
||||
super(sessionFactory);
|
||||
}
|
||||
|
||||
public List<Feed> findNextUpdatable(int count) {
|
||||
return query().selectFrom(feed)
|
||||
.where(feed.disabledUntil.isNull().or(feed.disabledUntil.lt(new Date())))
|
||||
.orderBy(feed.disabledUntil.asc())
|
||||
.limit(count)
|
||||
.fetch();
|
||||
public List<Feed> findNextUpdatable(int count, Date lastLoginThreshold) {
|
||||
JPAQuery<Feed> query = query().selectFrom(feed).where(feed.disabledUntil.isNull().or(feed.disabledUntil.lt(new Date())));
|
||||
if (lastLoginThreshold != null) {
|
||||
query.where(JPAExpressions.selectOne()
|
||||
.from(subscription)
|
||||
.join(subscription.user)
|
||||
.where(subscription.feed.id.eq(feed.id), subscription.user.lastLogin.gt(lastLoginThreshold))
|
||||
.exists());
|
||||
}
|
||||
|
||||
return query.orderBy(feed.disabledUntil.asc()).limit(count).fetch();
|
||||
}
|
||||
|
||||
public void setDisabledUntil(List<Long> feedIds, Date date) {
|
||||
|
||||
@@ -28,13 +28,10 @@ public class FeedEntryContentDAO extends GenericDAO<FeedEntryContent> {
|
||||
return query().select(content).from(content).where(content.contentHash.eq(contentHash), content.titleHash.eq(titleHash)).fetch();
|
||||
}
|
||||
|
||||
public int deleteWithoutEntries(int max) {
|
||||
|
||||
public long deleteWithoutEntries(int max) {
|
||||
JPQLQuery<Integer> subQuery = JPAExpressions.selectOne().from(entry).where(entry.content.id.eq(content.id));
|
||||
List<FeedEntryContent> list = query().selectFrom(content).where(subQuery.notExists()).limit(max).fetch();
|
||||
List<Long> ids = query().select(content.id).from(content).where(subQuery.notExists()).limit(max).fetch();
|
||||
|
||||
int deleted = list.size();
|
||||
delete(list);
|
||||
return deleted;
|
||||
return deleteQuery(content).where(content.id.in(ids)).execute();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,6 @@ public class FeedEntryDAO extends GenericDAO<FeedEntry> {
|
||||
}
|
||||
|
||||
public int delete(Long feedId, long max) {
|
||||
|
||||
List<FeedEntry> list = query().selectFrom(entry).where(entry.feed.id.eq(feedId)).limit(max).fetch();
|
||||
return delete(list);
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
||||
}
|
||||
|
||||
private JPAQuery<FeedEntry> buildQuery(User user, FeedSubscription sub, boolean unreadOnly, List<FeedEntryKeyword> keywords,
|
||||
Date newerThan, int offset, int limit, ReadingOrder order, FeedEntryStatus last, String tag) {
|
||||
Date newerThan, int offset, int limit, ReadingOrder order, FeedEntryStatus last, String tag, Long minEntryId, Long maxEntryId) {
|
||||
|
||||
JPAQuery<FeedEntry> query = query().selectFrom(entry).where(entry.feed.eq(sub.getFeed()));
|
||||
|
||||
@@ -159,6 +159,14 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
||||
query.where(entry.inserted.goe(newerThan));
|
||||
}
|
||||
|
||||
if (minEntryId != null) {
|
||||
query.where(entry.id.gt(minEntryId));
|
||||
}
|
||||
|
||||
if (maxEntryId != null) {
|
||||
query.where(entry.id.lt(maxEntryId));
|
||||
}
|
||||
|
||||
if (last != null) {
|
||||
if (order == ReadingOrder.desc) {
|
||||
query.where(entry.updated.gt(last.getEntryUpdated()));
|
||||
@@ -189,7 +197,7 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
||||
|
||||
public List<FeedEntryStatus> findBySubscriptions(User user, List<FeedSubscription> subs, boolean unreadOnly,
|
||||
List<FeedEntryKeyword> keywords, Date newerThan, int offset, int limit, ReadingOrder order, boolean includeContent,
|
||||
boolean onlyIds, String tag) {
|
||||
boolean onlyIds, String tag, Long minEntryId, Long maxEntryId) {
|
||||
int capacity = offset + limit;
|
||||
|
||||
Comparator<FeedEntryStatus> comparator = order == ReadingOrder.desc ? STATUS_COMPARATOR_DESC : STATUS_COMPARATOR_ASC;
|
||||
@@ -197,7 +205,8 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
||||
FixedSizeSortedSet<FeedEntryStatus> set = new FixedSizeSortedSet<>(capacity, comparator);
|
||||
for (FeedSubscription sub : subs) {
|
||||
FeedEntryStatus last = (order != null && set.isFull()) ? set.last() : null;
|
||||
JPAQuery<FeedEntry> query = buildQuery(user, sub, unreadOnly, keywords, newerThan, -1, capacity, order, last, tag);
|
||||
JPAQuery<FeedEntry> query = buildQuery(user, sub, unreadOnly, keywords, newerThan, -1, capacity, order, last, tag, minEntryId,
|
||||
maxEntryId);
|
||||
List<Tuple> tuples = query.select(entry.id, entry.updated, status.id, entry.content.title).fetch();
|
||||
|
||||
for (Tuple tuple : tuples) {
|
||||
@@ -250,7 +259,7 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
||||
|
||||
public UnreadCount getUnreadCount(User user, FeedSubscription subscription) {
|
||||
UnreadCount uc = null;
|
||||
JPAQuery<FeedEntry> query = buildQuery(user, subscription, true, null, null, -1, -1, null, null, null);
|
||||
JPAQuery<FeedEntry> query = buildQuery(user, subscription, true, null, null, -1, -1, null, null, null, null, null);
|
||||
List<Tuple> tuples = query.select(entry.count(), entry.updated.max()).fetch();
|
||||
for (Tuple tuple : tuples) {
|
||||
Long count = tuple.get(entry.count());
|
||||
@@ -270,8 +279,13 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
||||
return results;
|
||||
}
|
||||
|
||||
public List<FeedEntryStatus> getOldStatuses(Date olderThan, int limit) {
|
||||
return query().selectFrom(status).where(status.entryInserted.lt(olderThan), status.starred.isFalse()).limit(limit).fetch();
|
||||
public long deleteOldStatuses(Date olderThan, int limit) {
|
||||
List<Long> ids = query().select(status.id)
|
||||
.from(status)
|
||||
.where(status.entryInserted.lt(olderThan), status.starred.isFalse())
|
||||
.limit(limit)
|
||||
.fetch();
|
||||
return deleteQuery(status).where(status.id.in(ids)).execute();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import org.hibernate.annotations.QueryHints;
|
||||
|
||||
import com.commafeed.backend.model.AbstractModel;
|
||||
import com.querydsl.core.types.EntityPath;
|
||||
import com.querydsl.jpa.impl.JPADeleteClause;
|
||||
import com.querydsl.jpa.impl.JPAQuery;
|
||||
import com.querydsl.jpa.impl.JPAQueryFactory;
|
||||
import com.querydsl.jpa.impl.JPAUpdateClause;
|
||||
@@ -30,6 +31,10 @@ public abstract class GenericDAO<T extends AbstractModel> extends AbstractDAO<T>
|
||||
return new JPAUpdateClause(currentSession(), entityPath);
|
||||
}
|
||||
|
||||
protected JPADeleteClause deleteQuery(EntityPath<T> entityPath) {
|
||||
return new JPADeleteClause(currentSession(), entityPath);
|
||||
}
|
||||
|
||||
public void saveOrUpdate(T model) {
|
||||
persist(model);
|
||||
}
|
||||
|
||||
@@ -86,10 +86,12 @@ public class FeedRefreshEngine implements Managed {
|
||||
Feed feed = queue.take();
|
||||
|
||||
// send the feed to be processed
|
||||
log.debug("got feed {} from the queue, send it for processing", feed.getId());
|
||||
processFeedAsync(feed);
|
||||
|
||||
// we removed a feed from the queue, try to refill it as it may now be empty
|
||||
if (queue.isEmpty()) {
|
||||
log.debug("took the last feed from the queue, try to refill");
|
||||
refillQueueAsync();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
@@ -108,9 +110,11 @@ public class FeedRefreshEngine implements Managed {
|
||||
while (!refillLoopExecutor.isShutdown()) {
|
||||
try {
|
||||
if (queue.isEmpty()) {
|
||||
log.debug("refilling queue");
|
||||
refillQueueAsync();
|
||||
}
|
||||
|
||||
log.debug("sleeping for 15s");
|
||||
TimeUnit.SECONDS.sleep(15);
|
||||
} catch (InterruptedException e) {
|
||||
log.debug("interrupted while sleeping");
|
||||
@@ -123,6 +127,7 @@ public class FeedRefreshEngine implements Managed {
|
||||
}
|
||||
|
||||
public void refreshImmediately(Feed feed) {
|
||||
log.debug("add feed {} at the start of the queue", feed.getId());
|
||||
// remove the feed from the queue if it was already queued to avoid refreshing it twice
|
||||
queue.removeIf(f -> f.getId().equals(feed.getId()));
|
||||
queue.addFirst(feed);
|
||||
@@ -136,7 +141,9 @@ public class FeedRefreshEngine implements Managed {
|
||||
|
||||
refill.mark();
|
||||
|
||||
for (Feed feed : getNextUpdatableFeeds(getBatchSize())) {
|
||||
List<Feed> nextUpdatableFeeds = getNextUpdatableFeeds(getBatchSize());
|
||||
log.debug("found {} feeds that are up for refresh", nextUpdatableFeeds.size());
|
||||
for (Feed feed : nextUpdatableFeeds) {
|
||||
// add the feed only if it was not already queued
|
||||
if (queue.stream().noneMatch(f -> f.getId().equals(feed.getId()))) {
|
||||
queue.addLast(feed);
|
||||
@@ -161,7 +168,10 @@ public class FeedRefreshEngine implements Managed {
|
||||
|
||||
private List<Feed> getNextUpdatableFeeds(int max) {
|
||||
return unitOfWork.call(() -> {
|
||||
List<Feed> feeds = feedDAO.findNextUpdatable(max);
|
||||
Date lastLoginThreshold = Boolean.TRUE.equals(config.getApplicationSettings().getHeavyLoad())
|
||||
? DateUtils.addDays(new Date(), -30)
|
||||
: null;
|
||||
List<Feed> feeds = feedDAO.findNextUpdatable(max, lastLoginThreshold);
|
||||
// update disabledUntil to prevent feeds from being returned again by feedDAO.findNextUpdatable()
|
||||
Date nextUpdateDate = DateUtils.addMinutes(new Date(), config.getApplicationSettings().getRefreshIntervalMinutes());
|
||||
feedDAO.setDisabledUntil(feeds.stream().map(AbstractModel::getId).collect(Collectors.toList()), nextUpdateDate);
|
||||
|
||||
@@ -94,11 +94,10 @@ public class FeedRefreshWorker {
|
||||
|
||||
return new FeedRefreshWorkerResult(feed, Collections.emptyList());
|
||||
} catch (Exception e) {
|
||||
String message = "Unable to refresh feed " + feed.getUrl() + " : " + e.getMessage();
|
||||
log.debug(e.getClass().getName() + " " + message, e);
|
||||
log.debug("unable to refresh feed {}", feed.getUrl(), e);
|
||||
|
||||
feed.setErrorCount(feed.getErrorCount() + 1);
|
||||
feed.setMessage(message);
|
||||
feed.setMessage("Unable to refresh feed : " + e.getMessage());
|
||||
feed.setDisabledUntil(refreshIntervalCalculator.onFetchError(feed));
|
||||
|
||||
return new FeedRefreshWorkerResult(feed, Collections.emptyList());
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
package com.commafeed.backend.feed;
|
||||
|
||||
import java.io.StringReader;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
@@ -22,16 +19,10 @@ import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.math3.stat.descriptive.SummaryStatistics;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.nodes.Document.OutputSettings;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.jsoup.nodes.Entities.EscapeMode;
|
||||
import org.jsoup.safety.Cleaner;
|
||||
import org.jsoup.safety.Safelist;
|
||||
import org.jsoup.select.Elements;
|
||||
import org.netpreserve.urlcanon.Canonicalizer;
|
||||
import org.netpreserve.urlcanon.ParsedUrl;
|
||||
import org.w3c.css.sac.InputSource;
|
||||
import org.w3c.dom.css.CSSStyleDeclaration;
|
||||
|
||||
import com.commafeed.backend.feed.FeedEntryKeyword.Mode;
|
||||
import com.commafeed.backend.model.FeedEntry;
|
||||
@@ -41,7 +32,6 @@ import com.google.gwt.i18n.client.HasDirection.Direction;
|
||||
import com.google.gwt.i18n.shared.BidiUtils;
|
||||
import com.ibm.icu.text.CharsetDetector;
|
||||
import com.ibm.icu.text.CharsetMatch;
|
||||
import com.steadystate.css.parser.CSSOMParser;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@@ -54,12 +44,6 @@ public class FeedUtils {
|
||||
|
||||
private static final String ESCAPED_QUESTION_MARK = Pattern.quote("?");
|
||||
|
||||
private static final List<String> ALLOWED_IFRAME_CSS_RULES = Arrays.asList("height", "width", "border");
|
||||
private static final List<String> ALLOWED_IMG_CSS_RULES = Arrays.asList("display", "width", "height");
|
||||
private static final char[] FORBIDDEN_CSS_RULE_CHARACTERS = new char[] { '(', ')' };
|
||||
|
||||
private static final Safelist WHITELIST = buildWhiteList();
|
||||
|
||||
public static String truncate(String string, int length) {
|
||||
if (string != null) {
|
||||
string = string.substring(0, Math.min(length, string.length()));
|
||||
@@ -67,40 +51,6 @@ public class FeedUtils {
|
||||
return string;
|
||||
}
|
||||
|
||||
private static synchronized Safelist buildWhiteList() {
|
||||
Safelist whitelist = new Safelist();
|
||||
whitelist.addTags("a", "b", "blockquote", "br", "caption", "cite", "code", "col", "colgroup", "dd", "div", "dl", "dt", "em", "h1",
|
||||
"h2", "h3", "h4", "h5", "h6", "i", "iframe", "img", "li", "ol", "p", "pre", "q", "small", "strike", "strong", "sub", "sup",
|
||||
"table", "tbody", "td", "tfoot", "th", "thead", "tr", "u", "ul");
|
||||
|
||||
whitelist.addAttributes("div", "dir");
|
||||
whitelist.addAttributes("pre", "dir");
|
||||
whitelist.addAttributes("code", "dir");
|
||||
whitelist.addAttributes("table", "dir");
|
||||
whitelist.addAttributes("p", "dir");
|
||||
whitelist.addAttributes("a", "href", "title");
|
||||
whitelist.addAttributes("blockquote", "cite");
|
||||
whitelist.addAttributes("col", "span", "width");
|
||||
whitelist.addAttributes("colgroup", "span", "width");
|
||||
whitelist.addAttributes("iframe", "src", "height", "width", "allowfullscreen", "frameborder", "style");
|
||||
whitelist.addAttributes("img", "align", "alt", "height", "src", "title", "width", "style");
|
||||
whitelist.addAttributes("ol", "start", "type");
|
||||
whitelist.addAttributes("q", "cite");
|
||||
whitelist.addAttributes("table", "border", "bordercolor", "summary", "width");
|
||||
whitelist.addAttributes("td", "border", "bordercolor", "abbr", "axis", "colspan", "rowspan", "width");
|
||||
whitelist.addAttributes("th", "border", "bordercolor", "abbr", "axis", "colspan", "rowspan", "scope", "width");
|
||||
whitelist.addAttributes("ul", "type");
|
||||
|
||||
whitelist.addProtocols("a", "href", "ftp", "http", "https", "magnet", "mailto");
|
||||
whitelist.addProtocols("blockquote", "cite", "http", "https");
|
||||
whitelist.addProtocols("img", "src", "http", "https");
|
||||
whitelist.addProtocols("q", "cite", "http", "https");
|
||||
|
||||
whitelist.addEnforcedAttribute("a", "target", "_blank");
|
||||
whitelist.addEnforcedAttribute("a", "rel", "noreferrer");
|
||||
return whitelist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect feed encoding by using the declared encoding in the xml processing instruction and by detecting the characters used in the
|
||||
* feed
|
||||
@@ -233,87 +183,6 @@ public class FeedUtils {
|
||||
return encoding;
|
||||
}
|
||||
|
||||
public static String handleContent(String content, String baseUri, boolean keepTextOnly) {
|
||||
if (StringUtils.isNotBlank(content)) {
|
||||
baseUri = StringUtils.trimToEmpty(baseUri);
|
||||
|
||||
Document dirty = Jsoup.parseBodyFragment(content, baseUri);
|
||||
Cleaner cleaner = new Cleaner(WHITELIST);
|
||||
Document clean = cleaner.clean(dirty);
|
||||
|
||||
for (Element e : clean.select("iframe[style]")) {
|
||||
String style = e.attr("style");
|
||||
String escaped = escapeIFrameCss(style);
|
||||
e.attr("style", escaped);
|
||||
}
|
||||
|
||||
for (Element e : clean.select("img[style]")) {
|
||||
String style = e.attr("style");
|
||||
String escaped = escapeImgCss(style);
|
||||
e.attr("style", escaped);
|
||||
}
|
||||
|
||||
clean.outputSettings(new OutputSettings().escapeMode(EscapeMode.base).prettyPrint(false));
|
||||
Element body = clean.body();
|
||||
if (keepTextOnly) {
|
||||
content = body.text();
|
||||
} else {
|
||||
content = body.html();
|
||||
}
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
public static String escapeIFrameCss(String orig) {
|
||||
String rule = "";
|
||||
CSSOMParser parser = new CSSOMParser();
|
||||
try {
|
||||
List<String> rules = new ArrayList<>();
|
||||
CSSStyleDeclaration decl = parser.parseStyleDeclaration(new InputSource(new StringReader(orig)));
|
||||
|
||||
for (int i = 0; i < decl.getLength(); i++) {
|
||||
String property = decl.item(i);
|
||||
String value = decl.getPropertyValue(property);
|
||||
if (StringUtils.isBlank(property) || StringUtils.isBlank(value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ALLOWED_IFRAME_CSS_RULES.contains(property) && StringUtils.containsNone(value, FORBIDDEN_CSS_RULE_CHARACTERS)) {
|
||||
rules.add(property + ":" + decl.getPropertyValue(property) + ";");
|
||||
}
|
||||
}
|
||||
rule = StringUtils.join(rules, "");
|
||||
} catch (Exception e) {
|
||||
log.error(e.getMessage(), e);
|
||||
}
|
||||
return rule;
|
||||
}
|
||||
|
||||
public static String escapeImgCss(String orig) {
|
||||
String rule = "";
|
||||
CSSOMParser parser = new CSSOMParser();
|
||||
try {
|
||||
List<String> rules = new ArrayList<>();
|
||||
CSSStyleDeclaration decl = parser.parseStyleDeclaration(new InputSource(new StringReader(orig)));
|
||||
|
||||
for (int i = 0; i < decl.getLength(); i++) {
|
||||
String property = decl.item(i);
|
||||
String value = decl.getPropertyValue(property);
|
||||
if (StringUtils.isBlank(property) || StringUtils.isBlank(value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ALLOWED_IMG_CSS_RULES.contains(property) && StringUtils.containsNone(value, FORBIDDEN_CSS_RULE_CHARACTERS)) {
|
||||
rules.add(property + ":" + decl.getPropertyValue(property) + ";");
|
||||
}
|
||||
}
|
||||
rule = StringUtils.join(rules, "");
|
||||
} catch (Exception e) {
|
||||
log.error(e.getMessage(), e);
|
||||
}
|
||||
return rule;
|
||||
}
|
||||
|
||||
public static boolean isRTL(FeedEntry entry) {
|
||||
String text = entry.getContent().getContent();
|
||||
|
||||
|
||||
@@ -38,6 +38,6 @@ public class FeedCategory extends AbstractModel {
|
||||
|
||||
private boolean collapsed;
|
||||
|
||||
private Integer position;
|
||||
private int position;
|
||||
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ public class FeedSubscription extends AbstractModel {
|
||||
@OneToMany(mappedBy = "subscription", cascade = CascadeType.REMOVE)
|
||||
private Set<FeedEntryStatus> statuses;
|
||||
|
||||
private Integer position;
|
||||
private int position;
|
||||
|
||||
@Column(name = "filtering_expression", length = 4096)
|
||||
private String filter;
|
||||
|
||||
@@ -8,8 +8,6 @@ import javax.persistence.Table;
|
||||
import javax.persistence.Temporal;
|
||||
import javax.persistence.TemporalType;
|
||||
|
||||
import org.apache.commons.lang3.time.DateUtils;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@@ -49,17 +47,4 @@ public class User extends AbstractModel {
|
||||
|
||||
@Temporal(TemporalType.TIMESTAMP)
|
||||
private Date recoverPasswordTokenDate;
|
||||
|
||||
@Column(name = "last_full_refresh")
|
||||
@Temporal(TemporalType.TIMESTAMP)
|
||||
private Date lastFullRefresh;
|
||||
|
||||
public boolean shouldRefreshFeedsAt(Date when) {
|
||||
return lastFullRefresh == null || lastFullRefreshMoreThan30MinutesBefore(when);
|
||||
}
|
||||
|
||||
private boolean lastFullRefreshMoreThan30MinutesBefore(Date when) {
|
||||
return lastFullRefresh.before(DateUtils.addMinutes(when, -30));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -65,6 +65,10 @@ public class UserSettings extends AbstractModel {
|
||||
@Column(name = "scroll_speed")
|
||||
private int scrollSpeed;
|
||||
|
||||
private boolean alwaysScrollToEntry;
|
||||
private boolean markAllAsReadConfirmation;
|
||||
private boolean customContextMenu;
|
||||
|
||||
private boolean email;
|
||||
private boolean gmail;
|
||||
private boolean facebook;
|
||||
|
||||
@@ -6,6 +6,9 @@ import java.util.List;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import com.codahale.metrics.Meter;
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.commafeed.CommaFeedConfiguration;
|
||||
import com.commafeed.backend.dao.FeedDAO;
|
||||
import com.commafeed.backend.dao.FeedEntryContentDAO;
|
||||
import com.commafeed.backend.dao.FeedEntryDAO;
|
||||
@@ -14,7 +17,6 @@ import com.commafeed.backend.dao.FeedEntryStatusDAO;
|
||||
import com.commafeed.backend.dao.UnitOfWork;
|
||||
import com.commafeed.backend.model.Feed;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
@@ -22,29 +24,42 @@ import lombok.extern.slf4j.Slf4j;
|
||||
*
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||
@Singleton
|
||||
public class DatabaseCleaningService {
|
||||
|
||||
private static final int BATCH_SIZE = 100;
|
||||
private final int batchSize;
|
||||
|
||||
private final UnitOfWork unitOfWork;
|
||||
private final FeedDAO feedDAO;
|
||||
private final FeedEntryDAO feedEntryDAO;
|
||||
private final FeedEntryContentDAO feedEntryContentDAO;
|
||||
private final FeedEntryStatusDAO feedEntryStatusDAO;
|
||||
private final Meter entriesDeletedMeter;
|
||||
|
||||
public long cleanFeedsWithoutSubscriptions() {
|
||||
@Inject
|
||||
public DatabaseCleaningService(CommaFeedConfiguration config, UnitOfWork unitOfWork, FeedDAO feedDAO, FeedEntryDAO feedEntryDAO,
|
||||
FeedEntryContentDAO feedEntryContentDAO, FeedEntryStatusDAO feedEntryStatusDAO, MetricRegistry metrics) {
|
||||
this.unitOfWork = unitOfWork;
|
||||
this.feedDAO = feedDAO;
|
||||
this.feedEntryDAO = feedEntryDAO;
|
||||
this.feedEntryContentDAO = feedEntryContentDAO;
|
||||
this.feedEntryStatusDAO = feedEntryStatusDAO;
|
||||
this.batchSize = config.getApplicationSettings().getDatabaseCleanupBatchSize();
|
||||
this.entriesDeletedMeter = metrics.meter(MetricRegistry.name(getClass(), "entriesDeleted"));
|
||||
}
|
||||
|
||||
public void cleanFeedsWithoutSubscriptions() {
|
||||
log.info("cleaning feeds without subscriptions");
|
||||
long total = 0;
|
||||
int deleted = 0;
|
||||
int deleted;
|
||||
long entriesTotal = 0;
|
||||
do {
|
||||
List<Feed> feeds = unitOfWork.call(() -> feedDAO.findWithoutSubscriptions(1));
|
||||
for (Feed feed : feeds) {
|
||||
int entriesDeleted = 0;
|
||||
long entriesDeleted;
|
||||
do {
|
||||
entriesDeleted = unitOfWork.call(() -> feedEntryDAO.delete(feed.getId(), BATCH_SIZE));
|
||||
entriesDeleted = unitOfWork.call(() -> feedEntryDAO.delete(feed.getId(), batchSize));
|
||||
entriesDeletedMeter.mark(entriesDeleted);
|
||||
entriesTotal += entriesDeleted;
|
||||
log.info("removed {} entries for feeds without subscriptions", entriesTotal);
|
||||
} while (entriesDeleted > 0);
|
||||
@@ -54,26 +69,24 @@ public class DatabaseCleaningService {
|
||||
log.info("removed {} feeds without subscriptions", total);
|
||||
} while (deleted != 0);
|
||||
log.info("cleanup done: {} feeds without subscriptions deleted", total);
|
||||
return total;
|
||||
}
|
||||
|
||||
public long cleanContentsWithoutEntries() {
|
||||
public void cleanContentsWithoutEntries() {
|
||||
log.info("cleaning contents without entries");
|
||||
long total = 0;
|
||||
int deleted = 0;
|
||||
long deleted;
|
||||
do {
|
||||
deleted = unitOfWork.call(() -> feedEntryContentDAO.deleteWithoutEntries(BATCH_SIZE));
|
||||
deleted = unitOfWork.call(() -> feedEntryContentDAO.deleteWithoutEntries(batchSize));
|
||||
total += deleted;
|
||||
log.info("removed {} contents without entries", total);
|
||||
} while (deleted != 0);
|
||||
log.info("cleanup done: {} contents without entries deleted", total);
|
||||
return total;
|
||||
}
|
||||
|
||||
public long cleanEntriesForFeedsExceedingCapacity(final int maxFeedCapacity) {
|
||||
public void cleanEntriesForFeedsExceedingCapacity(final int maxFeedCapacity) {
|
||||
long total = 0;
|
||||
while (true) {
|
||||
List<FeedCapacity> feeds = unitOfWork.call(() -> feedEntryDAO.findFeedsExceedingCapacity(maxFeedCapacity, BATCH_SIZE));
|
||||
List<FeedCapacity> feeds = unitOfWork.call(() -> feedEntryDAO.findFeedsExceedingCapacity(maxFeedCapacity, batchSize));
|
||||
if (feeds.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
@@ -82,7 +95,8 @@ public class DatabaseCleaningService {
|
||||
long remaining = feed.getCapacity() - maxFeedCapacity;
|
||||
do {
|
||||
final long rem = remaining;
|
||||
int deleted = unitOfWork.call(() -> feedEntryDAO.deleteOldEntries(feed.getId(), Math.min(BATCH_SIZE, rem)));
|
||||
int deleted = unitOfWork.call(() -> feedEntryDAO.deleteOldEntries(feed.getId(), Math.min(batchSize, rem)));
|
||||
entriesDeletedMeter.mark(deleted);
|
||||
total += deleted;
|
||||
remaining -= deleted;
|
||||
log.info("removed {} entries for feeds exceeding capacity", total);
|
||||
@@ -90,19 +104,17 @@ public class DatabaseCleaningService {
|
||||
}
|
||||
}
|
||||
log.info("cleanup done: {} entries for feeds exceeding capacity deleted", total);
|
||||
return total;
|
||||
}
|
||||
|
||||
public long cleanStatusesOlderThan(final Date olderThan) {
|
||||
public void cleanStatusesOlderThan(final Date olderThan) {
|
||||
log.info("cleaning old read statuses");
|
||||
long total = 0;
|
||||
int deleted = 0;
|
||||
long deleted;
|
||||
do {
|
||||
deleted = unitOfWork.call(() -> feedEntryStatusDAO.delete(feedEntryStatusDAO.getOldStatuses(olderThan, BATCH_SIZE)));
|
||||
deleted = unitOfWork.call(() -> feedEntryStatusDAO.deleteOldStatuses(olderThan, batchSize));
|
||||
total += deleted;
|
||||
log.info("removed {} old read statuses", total);
|
||||
} while (deleted != 0);
|
||||
log.info("cleanup done: {} old read statuses deleted", total);
|
||||
return total;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package com.commafeed.backend.service;
|
||||
|
||||
import java.io.StringReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -8,27 +11,47 @@ import javax.inject.Singleton;
|
||||
|
||||
import org.apache.commons.codec.digest.DigestUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.nodes.Document.OutputSettings;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.jsoup.nodes.Entities.EscapeMode;
|
||||
import org.jsoup.safety.Cleaner;
|
||||
import org.jsoup.safety.Safelist;
|
||||
import org.w3c.css.sac.CSSException;
|
||||
import org.w3c.css.sac.CSSParseException;
|
||||
import org.w3c.css.sac.ErrorHandler;
|
||||
import org.w3c.css.sac.InputSource;
|
||||
import org.w3c.dom.css.CSSStyleDeclaration;
|
||||
|
||||
import com.commafeed.backend.dao.FeedEntryContentDAO;
|
||||
import com.commafeed.backend.feed.FeedUtils;
|
||||
import com.commafeed.backend.model.FeedEntryContent;
|
||||
import com.steadystate.css.parser.CSSOMParser;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||
@Slf4j
|
||||
@Singleton
|
||||
public class FeedEntryContentService {
|
||||
|
||||
private static final Safelist HTML_WHITELIST = buildWhiteList();
|
||||
private static final List<String> ALLOWED_IFRAME_CSS_RULES = Arrays.asList("height", "width", "border");
|
||||
private static final List<String> ALLOWED_IMG_CSS_RULES = Arrays.asList("display", "width", "height");
|
||||
private static final char[] FORBIDDEN_CSS_RULE_CHARACTERS = new char[] { '(', ')' };
|
||||
|
||||
private final FeedEntryContentDAO feedEntryContentDAO;
|
||||
|
||||
/**
|
||||
* this is NOT thread-safe
|
||||
*/
|
||||
public FeedEntryContent findOrCreate(FeedEntryContent content, String baseUrl) {
|
||||
content.setAuthor(FeedUtils.truncate(FeedUtils.handleContent(content.getAuthor(), baseUrl, true), 128));
|
||||
content.setTitle(FeedUtils.truncate(FeedUtils.handleContent(content.getTitle(), baseUrl, true), 2048));
|
||||
content.setContent(FeedUtils.handleContent(content.getContent(), baseUrl, false));
|
||||
content.setMediaDescription(FeedUtils.handleContent(content.getMediaDescription(), baseUrl, false));
|
||||
content.setAuthor(FeedUtils.truncate(handleContent(content.getAuthor(), baseUrl, true), 128));
|
||||
content.setTitle(FeedUtils.truncate(handleContent(content.getTitle(), baseUrl, true), 2048));
|
||||
content.setContent(handleContent(content.getContent(), baseUrl, false));
|
||||
content.setMediaDescription(handleContent(content.getMediaDescription(), baseUrl, false));
|
||||
|
||||
String contentHash = DigestUtils.sha1Hex(StringUtils.trimToEmpty(content.getContent()));
|
||||
content.setContentHash(contentHash);
|
||||
@@ -37,7 +60,7 @@ public class FeedEntryContentService {
|
||||
content.setTitleHash(titleHash);
|
||||
|
||||
List<FeedEntryContent> existing = feedEntryContentDAO.findExisting(contentHash, titleHash);
|
||||
Optional<FeedEntryContent> equivalentContent = existing.stream().filter(c -> content.equivalentTo(c)).findFirst();
|
||||
Optional<FeedEntryContent> equivalentContent = existing.stream().filter(content::equivalentTo).findFirst();
|
||||
if (equivalentContent.isPresent()) {
|
||||
return equivalentContent.get();
|
||||
}
|
||||
@@ -45,4 +68,140 @@ public class FeedEntryContentService {
|
||||
feedEntryContentDAO.saveOrUpdate(content);
|
||||
return content;
|
||||
}
|
||||
|
||||
private static Safelist buildWhiteList() {
|
||||
Safelist whitelist = new Safelist();
|
||||
whitelist.addTags("a", "b", "blockquote", "br", "caption", "cite", "code", "col", "colgroup", "dd", "div", "dl", "dt", "em", "h1",
|
||||
"h2", "h3", "h4", "h5", "h6", "i", "iframe", "img", "li", "ol", "p", "pre", "q", "small", "strike", "strong", "sub", "sup",
|
||||
"table", "tbody", "td", "tfoot", "th", "thead", "tr", "u", "ul");
|
||||
|
||||
whitelist.addAttributes("div", "dir");
|
||||
whitelist.addAttributes("pre", "dir");
|
||||
whitelist.addAttributes("code", "dir");
|
||||
whitelist.addAttributes("table", "dir");
|
||||
whitelist.addAttributes("p", "dir");
|
||||
whitelist.addAttributes("a", "href", "title");
|
||||
whitelist.addAttributes("blockquote", "cite");
|
||||
whitelist.addAttributes("col", "span", "width");
|
||||
whitelist.addAttributes("colgroup", "span", "width");
|
||||
whitelist.addAttributes("iframe", "src", "height", "width", "allowfullscreen", "frameborder", "style");
|
||||
whitelist.addAttributes("img", "align", "alt", "height", "src", "title", "width", "style");
|
||||
whitelist.addAttributes("ol", "start", "type");
|
||||
whitelist.addAttributes("q", "cite");
|
||||
whitelist.addAttributes("table", "border", "bordercolor", "summary", "width");
|
||||
whitelist.addAttributes("td", "border", "bordercolor", "abbr", "axis", "colspan", "rowspan", "width");
|
||||
whitelist.addAttributes("th", "border", "bordercolor", "abbr", "axis", "colspan", "rowspan", "scope", "width");
|
||||
whitelist.addAttributes("ul", "type");
|
||||
|
||||
whitelist.addProtocols("a", "href", "ftp", "http", "https", "magnet", "mailto");
|
||||
whitelist.addProtocols("blockquote", "cite", "http", "https");
|
||||
whitelist.addProtocols("img", "src", "http", "https");
|
||||
whitelist.addProtocols("q", "cite", "http", "https");
|
||||
|
||||
whitelist.addEnforcedAttribute("a", "target", "_blank");
|
||||
whitelist.addEnforcedAttribute("a", "rel", "noreferrer");
|
||||
return whitelist;
|
||||
}
|
||||
|
||||
private String handleContent(String content, String baseUri, boolean keepTextOnly) {
|
||||
if (StringUtils.isNotBlank(content)) {
|
||||
baseUri = StringUtils.trimToEmpty(baseUri);
|
||||
|
||||
Document dirty = Jsoup.parseBodyFragment(content, baseUri);
|
||||
Cleaner cleaner = new Cleaner(HTML_WHITELIST);
|
||||
Document clean = cleaner.clean(dirty);
|
||||
|
||||
for (Element e : clean.select("iframe[style]")) {
|
||||
String style = e.attr("style");
|
||||
String escaped = escapeIFrameCss(style);
|
||||
e.attr("style", escaped);
|
||||
}
|
||||
|
||||
for (Element e : clean.select("img[style]")) {
|
||||
String style = e.attr("style");
|
||||
String escaped = escapeImgCss(style);
|
||||
e.attr("style", escaped);
|
||||
}
|
||||
|
||||
clean.outputSettings(new OutputSettings().escapeMode(EscapeMode.base).prettyPrint(false));
|
||||
Element body = clean.body();
|
||||
if (keepTextOnly) {
|
||||
content = body.text();
|
||||
} else {
|
||||
content = body.html();
|
||||
}
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
private String escapeIFrameCss(String orig) {
|
||||
String rule = "";
|
||||
try {
|
||||
List<String> rules = new ArrayList<>();
|
||||
CSSStyleDeclaration decl = buildCssParser().parseStyleDeclaration(new InputSource(new StringReader(orig)));
|
||||
|
||||
for (int i = 0; i < decl.getLength(); i++) {
|
||||
String property = decl.item(i);
|
||||
String value = decl.getPropertyValue(property);
|
||||
if (StringUtils.isBlank(property) || StringUtils.isBlank(value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ALLOWED_IFRAME_CSS_RULES.contains(property) && StringUtils.containsNone(value, FORBIDDEN_CSS_RULE_CHARACTERS)) {
|
||||
rules.add(property + ":" + decl.getPropertyValue(property) + ";");
|
||||
}
|
||||
}
|
||||
rule = StringUtils.join(rules, "");
|
||||
} catch (Exception e) {
|
||||
log.error(e.getMessage(), e);
|
||||
}
|
||||
return rule;
|
||||
}
|
||||
|
||||
private String escapeImgCss(String orig) {
|
||||
String rule = "";
|
||||
try {
|
||||
List<String> rules = new ArrayList<>();
|
||||
CSSStyleDeclaration decl = buildCssParser().parseStyleDeclaration(new InputSource(new StringReader(orig)));
|
||||
|
||||
for (int i = 0; i < decl.getLength(); i++) {
|
||||
String property = decl.item(i);
|
||||
String value = decl.getPropertyValue(property);
|
||||
if (StringUtils.isBlank(property) || StringUtils.isBlank(value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ALLOWED_IMG_CSS_RULES.contains(property) && StringUtils.containsNone(value, FORBIDDEN_CSS_RULE_CHARACTERS)) {
|
||||
rules.add(property + ":" + decl.getPropertyValue(property) + ";");
|
||||
}
|
||||
}
|
||||
rule = StringUtils.join(rules, "");
|
||||
} catch (Exception e) {
|
||||
log.error(e.getMessage(), e);
|
||||
}
|
||||
return rule;
|
||||
}
|
||||
|
||||
private CSSOMParser buildCssParser() {
|
||||
CSSOMParser parser = new CSSOMParser();
|
||||
|
||||
parser.setErrorHandler(new ErrorHandler() {
|
||||
@Override
|
||||
public void warning(CSSParseException exception) throws CSSException {
|
||||
log.debug("warning while parsing css: {}", exception.getMessage(), exception);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(CSSParseException exception) throws CSSException {
|
||||
log.debug("error while parsing css: {}", exception.getMessage(), exception);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fatalError(CSSParseException exception) throws CSSException {
|
||||
log.debug("fatal error while parsing css: {}", exception.getMessage(), exception);
|
||||
}
|
||||
});
|
||||
|
||||
return parser;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ public class FeedEntryService {
|
||||
|
||||
public void markSubscriptionEntries(User user, List<FeedSubscription> subscriptions, Date olderThan, List<FeedEntryKeyword> keywords) {
|
||||
List<FeedEntryStatus> statuses = feedEntryStatusDAO.findBySubscriptions(user, subscriptions, true, keywords, null, -1, -1, null,
|
||||
false, false, null);
|
||||
false, false, null, null, null);
|
||||
markList(statuses, olderThan);
|
||||
cache.invalidateUnreadCount(subscriptions.toArray(new FeedSubscription[0]));
|
||||
cache.invalidateUserRootCategory(user);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.commafeed.backend.service;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -107,6 +108,17 @@ public class FeedSubscriptionService {
|
||||
}
|
||||
}
|
||||
|
||||
public void refreshAllUpForRefresh(User user) {
|
||||
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user);
|
||||
for (FeedSubscription sub : subs) {
|
||||
Date disabledUntil = sub.getFeed().getDisabledUntil();
|
||||
if (disabledUntil == null || disabledUntil.before(new Date())) {
|
||||
Feed feed = sub.getFeed();
|
||||
feedRefreshEngine.refreshImmediately(feed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Map<Long, UnreadCount> getUnreadCount(User user) {
|
||||
return feedSubscriptionDAO.findAll(user).stream().collect(Collectors.toMap(FeedSubscription::getId, s -> getUnreadCount(user, s)));
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user