Compare commits

...

92 Commits
3.4.0 ... 3.8.1

Author SHA1 Message Date
Athou
04c74b5daa release 3.8.1 2023-07-04 18:40:15 +02:00
Athou
3edb8a3ee2 don't scroll to entry if it's already selected (#1108) 2023-07-04 08:37:56 +02:00
Athou
922346bef6 fetch only ids to improve performance during cleanup 2023-07-01 22:54:28 +02:00
Athou
82cf0e154a release 3.8.0 2023-06-28 20:40:22 +02:00
Athou
efe32e86c9 remove warning about vite not finding custom code at build time 2023-06-27 19:25:44 +02:00
Athou
e208d4ae1e escape input before using it as a regex 2023-06-27 19:11:17 +02:00
Athou
adf20327bd fix broken welcome page mobile layout 2023-06-27 19:05:38 +02:00
Jérémie Panzer
781c41b452 Merge pull request #1107 from canoine/master
Update fr/messages.po
2023-06-27 12:21:17 +02:00
canoine
2b597f9b43 Update fr/messages.po
Translating new entries
2023-06-27 12:19:21 +02:00
Athou
2e26f34135 reduce button spacing on desktop to be able to reduce breakpoint (#1106) 2023-06-27 11:18:56 +02:00
Athou
9e59a472da fix typo 2023-06-27 08:21:30 +02:00
Athou
970043467c make sure there's enough room to show all buttons 2023-06-27 08:21:05 +02:00
Athou
3e903fc6bc use a single call to useContextMenu as recommended in the docs 2023-06-26 20:06:46 +02:00
Athou
95f4cffa7c avoid using sx in feed entry list to improve performance 2023-06-25 21:12:27 +02:00
Athou
6ebe0fa827 memoize feed entry content because Interweave is costly 2023-06-25 20:58:36 +02:00
Athou
488a88fe95 we removed the usage of the deprecated hibernate id generator, we no longer need to ignore warning log messages about it 2023-06-25 07:32:14 +02:00
Athou
d5898a0173 throttle scroll listener 2023-06-24 23:04:52 +02:00
Athou
bdcfbc22bf remove ScrollArea as it causes performance issues on chrome (#1087) 2023-06-24 23:00:25 +02:00
Athou
53b06f41f3 add divider to avoid misclicks 2023-06-24 18:30:26 +02:00
Athou
872247d80f add previous and next buttons (#1096) 2023-06-24 13:30:58 +02:00
Athou
7c226f41db add a setting to always scroll selected entry to the top of the page, even if it fits entirely on screen (#1088) 2023-06-24 09:48:59 +02:00
Athou
bb55a91a14 format absolute dates in popups in user locale instead of GMT 2023-06-22 22:33:53 +02:00
Athou
f140650b4e monaco mobile support is poor, fallback to textarea 2023-06-22 22:19:40 +02:00
Athou
8a64a9db31 host monaco ourselves, don't download it from a CDN 2023-06-22 20:40:28 +02:00
Athou
c1520652f2 move RichCodeEditor to its own component 2023-06-22 18:41:12 +02:00
Athou
90e3044249 wait for tab to be activated to load rich code editor 2023-06-22 16:30:00 +02:00
Athou
f7786d9962 add rich editor for custom code 2023-06-22 14:37:56 +02:00
Athou
aeaaeaee0e fix warning 2023-06-22 10:36:03 +02:00
Athou
4d0a8fd133 fix user count 2023-06-22 07:19:33 +02:00
Athou
b1938c234c use useMobile 2023-06-21 21:58:11 +02:00
Athou
6a5052787d clicking on the body of an entry in expanded mode selects it and marks it as read (#1089) 2023-06-21 20:30:08 +02:00
Athou
877fc33180 move swipe callback next to other callbacks 2023-06-21 20:30:08 +02:00
Jérémie Panzer
8b0b9b1a66 Merge pull request #1092 from canoine/patch-1
Update fr/messages.po
2023-06-21 17:02:10 +02:00
canoine
689c329430 Update fr/messages.po
Traduction des nouveaux messages
2023-06-21 15:45:11 +02:00
Athou
52f911f303 add websocket metrics 2023-06-21 14:20:14 +02:00
Athou
91d0988177 add useMobile 2023-06-21 09:13:20 +02:00
Athou
4f644ba9f5 remove workaround to make popovers follow their target on scroll, it causes lagging issues and was fixed in https://github.com/mantinedev/mantine/issues/3351 (#1087) 2023-06-20 10:46:42 +02:00
Athou
4f699d9675 release 3.7.0 2023-06-20 09:18:18 +02:00
Athou
a5aba6f7ae content is no longer limited to 650px when sidebar is hidden (same as commafeed v2) (#1084) 2023-06-18 12:46:42 +02:00
Athou
78c8711a79 add tooltips to all relative dates with exact time 2023-06-17 22:53:07 +02:00
Athou
8325236d0e hide horizontal scrollbar (#1084) 2023-06-17 22:43:23 +02:00
Athou
437401e73f fix sidebar scrolling (#1084) 2023-06-17 08:37:41 +02:00
Athou
fa06d321d5 restore F shortcut to hide sidebar (#1084) 2023-06-16 21:49:08 +02:00
Athou
d1ddcb6ace resizeable tree (#1084) 2023-06-16 21:24:34 +02:00
Athou
6944d4dc0b fix unreadable api documentation page with dark theme (#1082) 2023-06-16 20:07:36 +02:00
Athou
c835d805b1 restore a version of findNextUpdatable that handles inactive users better than the one we removed a while ago 2023-06-16 15:27:39 +02:00
Athou
4a90e1f69d add some debugging 2023-06-16 13:14:37 +02:00
Athou
fcfeaa462e on user login and in heavy load mode, only force refresh feeds that are up for refresh 2023-06-16 13:14:37 +02:00
Athou
b16978d8fe position is now always set (#1076) 2023-06-15 21:12:10 +02:00
Athou
68c62b4528 no need to push the extension this much 2023-06-14 01:09:31 +02:00
Athou
18f68aab31 fallback to ctrl+click simulation if extension is not installed (#1074 #1075) 2023-06-14 01:03:59 +02:00
Athou
8abb2770ec fix release script, it's the CHANGELOG that needs to be updated 2023-06-13 11:15:29 +02:00
Athou
9156b8b6d0 add a setting to hide commafeed from search engines 2023-06-13 10:51:12 +02:00
Athou
2c32fa1e13 make "b" keyboard shortcut work in extension popup 2023-06-13 10:29:18 +02:00
Athou
7e48afe36c correctly detect the extension if the hook is not used on the initial page 2023-06-12 21:47:54 +02:00
Athou
cd94a3b56f update browser extension badge unread count 2023-06-12 20:54:40 +02:00
Athou
22e0f1f382 use browser extension to open tab in background (#1074) 2023-06-11 17:59:46 +02:00
Athou
e2eeba90ef release 3.6.0 2023-06-08 08:46:20 +02:00
Athou
662c0fc6b9 add buttons that communicate with the browser extension (Athou/commafeed-browser-extension#1) 2023-06-07 15:28:16 +02:00
Athou
fafc0619ad add tooltip to action buttons when label is hidden because viewport width is below mobile breakpoint (Athou/commafeed-browser-extension#1) 2023-06-07 11:32:54 +02:00
Athou
3a5dc5d0ed open link on click in expanded mode since we don't need to collapse entry (#1073) 2023-06-07 08:58:50 +02:00
Jérémie Panzer
23a696e644 Merge pull request #1071 from Athou/dependabot/npm_and_yarn/commafeed-client/vite-4.3.9
Bump vite from 4.3.8 to 4.3.9 in /commafeed-client
2023-06-06 10:12:41 +02:00
dependabot[bot]
72cb71a2fb Bump vite from 4.3.8 to 4.3.9 in /commafeed-client
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.3.8 to 4.3.9.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v4.3.9/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-06 08:06:36 +00:00
Athou
9b757735b8 a 403 may be returned if the user has been deleted but the session still exists, redirect to welcome page 2023-06-05 18:46:32 +02:00
Athou
6b9f8f268f make /next work in development 2023-06-05 10:17:00 +02:00
Athou
e5b0eb426c change default config to store h2 database in the same directory as the jar 2023-06-04 07:45:01 +02:00
Athou
ea6c83ca33 add link to api documentation on welcome page 2023-06-01 16:12:39 +02:00
Athou
763ce1e4fd correctly invalidate unread count cache when using the next unread servlet 2023-06-01 13:11:19 +02:00
Athou
e748499ed8 Merge branch 'canoine-patch-2' 2023-06-01 08:06:05 +02:00
Athou
e430604528 remove nbsp from label 2023-06-01 08:05:52 +02:00
Athou
5e08c81d12 Merge branch 'patch-2' of https://github.com/canoine/commafeed into canoine-patch-2 2023-06-01 07:58:12 +02:00
Athou
84626e1ef2 release 3.5.0 2023-06-01 07:49:42 +02:00
Athou
191ece0bac update browser extensions link 2023-06-01 07:43:17 +02:00
canoine
24eaff61f2 Update fr/messages.po
Adds, updates and fixes.
2023-06-01 06:25:39 +02:00
Athou
aa5e9bfd83 update readme to point to the new browser extension 2023-05-31 17:55:47 +02:00
Athou
a200147926 remove X-Frame-Options: DENY as it blocks the iframe from the future browser extension 2023-05-31 15:24:17 +02:00
Athou
d6205b7da3 fix typo 2023-05-31 07:36:50 +02:00
Athou
5ecf3e0fbf add setting to disable strict password policy (#1059) 2023-05-31 07:31:40 +02:00
Athou
bb25e0ede6 intellij autofixes 2023-05-31 07:27:24 +02:00
Athou
f5c0e2d375 update documentation: alphabetical ordering is no longer available 2023-05-30 21:22:02 +02:00
Athou
12ab5b1e7b add default value to allow app startup even if the setting is missing in config.yml 2023-05-30 10:53:28 +02:00
Athou
3e6451289f add setting to limit feeds per user 2023-05-30 09:10:20 +02:00
Athou
09d21d88a4 remove usage of deprecated id generator that blocks migration to hibernate 6 2023-05-29 20:36:45 +02:00
Athou
2ec6d0a66a api documentation page no longer requires users to be authenticated 2023-05-28 22:38:57 +02:00
Athou
412fc52f1c add css classes to help with custom css rules (#1061) 2023-05-28 12:57:28 +02:00
Athou
b5e5989604 disable pull-to-refresh on mobile as it messes with vertical scrolling 2023-05-27 19:54:02 +02:00
Athou
105ff46c01 UnitOfWork is now injectable 2023-05-27 19:46:49 +02:00
Athou
f100f3f91a run post login activities in a new transaction to avoid database locks 2023-05-27 19:29:44 +02:00
Athou
45eb436b8f reduce chance of deadlocks 2023-05-27 08:38:23 +02:00
Athou
bf3914e748 remove very slow query 2023-05-27 08:03:36 +02:00
Athou
5df7aaf7cd try to fix redis timeouts (#1060) 2023-05-27 07:58:22 +02:00
Athou
f10cfd7ad0 add feed refresh engine metrics 2023-05-26 15:20:17 +02:00
139 changed files with 2717 additions and 1092 deletions

View File

@@ -1,12 +1,60 @@
# Changelog # Changelog
## [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
- clicking on the entry title in expanded mode now opens the link instead of doing nothing
- add tooltips to buttons when the mobile layout is used on desktop
- redirect the user to the welcome page if the user was deleted from the database
- add link to api documentation on welcome page
- the unread count is now correctly updated when using the "/next" bookmarklet while redis cache is enabled
## [3.5.0]
- add compatibility with the new version of the CommaFeed browser extension
- disable pull-to-refresh on mobile as it messes with vertical scrolling
- add css classes to feed entries to help with custom css rules
- api documentation page no longer requires users to be authenticated
- add a setting to limit the number of feeds a user can subscribe to
- add a setting to disable strict password policy
- add feed refresh engine metrics
- fix redis timeouts
## [3.4.0] ## [3.4.0]
- add support for arm64 docker images - add support for arm64 docker images
- add divider to visually separate read-only information from form on the profile settings page - 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 - reduce javascript bundle size by 30% by loading only the necessary translations
- add a standalone donate page with all ways to support CommaFeed - 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 icon with text for category tree nodes
- fix alignment of burger button with the rest of the header on mobile - fix alignment of burger button with the rest of the header on mobile
@@ -47,10 +95,10 @@
## [3.0.1] ## [3.0.1]
- allow env variable substitution in config.yml - allow env variable substitution in config.yml
- e.g. having a custom config.yml file with `app.session.path=${SOME_ENV_VAR}` will substitute `SOME_ENV_VAR` with - e.g. having a custom config.yml file with `app.session.path=${SOME_ENV_VAR}` will substitute `SOME_ENV_VAR` with
its value its value
- allow env variable prefixed with `CF_` to override config.yml properties - allow env variable prefixed with `CF_` to override config.yml properties
- e.g. setting `CF_APP_ALLOWREGISTRATIONS=true` will set `app.allowRegistrations` to `true` - e.g. setting `CF_APP_ALLOWREGISTRATIONS=true` will set `app.allowRegistrations` to `true`
## [3.0.0] ## [3.0.0]

View File

@@ -5,6 +5,7 @@ EXPOSE 8082
RUN mkdir -p /commafeed/data RUN mkdir -p /commafeed/data
VOLUME /commafeed/data VOLUME /commafeed/data
ENV CF_SESSION_PATH=/commafeed/data/sessions ENV CF_SESSION_PATH=/commafeed/data/sessions
ENV CF_DATABASE_URL=jdbc:h2:/commafeed/data/db
COPY commafeed-server/config.yml.example config.yml COPY commafeed-server/config.yml.example config.yml
COPY commafeed-server/target/commafeed.jar . COPY commafeed-server/target/commafeed.jar .

View File

@@ -15,15 +15,7 @@ Google Reader inspired self-hosted RSS reader, based on Dropwizard and React/Typ
- Supports thousands of users and millions of feeds - Supports thousands of users and millions of feeds
- OPML import/export - OPML import/export
- REST API - REST API
- [Browser extension](https://github.com/Athou/commafeed-browser-extension)
## Related open-source projects
Browser extensions:
- [Chrome](https://github.com/Athou/commafeed-chrome)
- [Firefox](https://github.com/Athou/commafeed-firefox)
- [Opera](https://github.com/Athou/commafeed-opera)
- [Safari](https://github.com/Athou/commafeed-safari)
## Deployment on your own server ## Deployment on your own server

View File

@@ -4,13 +4,11 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/x-icon" href="favicon.ico" /> <link rel="icon" type="image/x-icon" href="favicon.ico" />
<link rel="manifest" href="manifest.json" /> <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" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>CommaFeed</title> <title>CommaFeed</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>
<script src="custom_js.js"></script>
</body> </body>
</html> </html>

View File

@@ -20,11 +20,15 @@
"@mantine/notifications": "^6.0.11", "@mantine/notifications": "^6.0.11",
"@mantine/spotlight": "^6.0.11", "@mantine/spotlight": "^6.0.11",
"@mantine/styles": "^6.0.11", "@mantine/styles": "^6.0.11",
"@monaco-editor/react": "^4.5.1",
"@reduxjs/toolkit": "^1.9.5", "@reduxjs/toolkit": "^1.9.5",
"axios": "^1.4.0", "axios": "^1.4.0",
"dayjs": "^1.11.7", "dayjs": "^1.11.7",
"escape-string-regexp": "^5.0.0",
"interweave": "^13.1.0", "interweave": "^13.1.0",
"monaco-editor": "^0.38.0",
"mousetrap": "^1.6.5", "mousetrap": "^1.6.5",
"re-resizable": "^6.9.9",
"react": "^18.2.0", "react": "^18.2.0",
"react-async-hook": "^4.0.0", "react-async-hook": "^4.0.0",
"react-contexify": "^6.0.0", "react-contexify": "^6.0.0",
@@ -66,7 +70,7 @@
"prettier": "^2.8.8", "prettier": "^2.8.8",
"rollup-plugin-visualizer": "^5.9.0", "rollup-plugin-visualizer": "^5.9.0",
"typescript": "^5.0.4", "typescript": "^5.0.4",
"vite": "^4.3.8", "vite": "^4.3.9",
"vite-plugin-eslint": "^1.8.1", "vite-plugin-eslint": "^1.8.1",
"vite-tsconfig-paths": "^4.2.0", "vite-tsconfig-paths": "^4.2.0",
"vitest": "^0.31.1", "vitest": "^0.31.1",
@@ -2054,6 +2058,17 @@
"stylis": "4.2.0" "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": { "node_modules/@emotion/cache": {
"version": "11.11.0", "version": "11.11.0",
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz",
@@ -3048,6 +3063,30 @@
"moo": "^0.5.1" "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": { "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": {
"version": "5.1.1-v1", "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", "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==" "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
}, },
"node_modules/escape-string-regexp": { "node_modules/escape-string-regexp": {
"version": "4.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
"engines": { "engines": {
"node": ">=10" "node": ">=12"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
@@ -7148,6 +7187,18 @@
"url": "https://opencollective.com/eslint" "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": { "node_modules/eslint/node_modules/eslint-scope": {
"version": "7.2.0", "version": "7.2.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz",
@@ -8998,6 +9049,11 @@
"ufo": "^1.1.2" "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": { "node_modules/moo": {
"version": "0.5.2", "version": "0.5.2",
"resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz",
@@ -10035,6 +10091,15 @@
"node": ">=0.10.0" "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": { "node_modules/react": {
"version": "18.2.0", "version": "18.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", "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", "resolved": "https://registry.npmjs.org/stampit/-/stampit-4.3.2.tgz",
"integrity": "sha512-pE2org1+ZWQBnIxRPrBM2gVupkuDD0TTNIo1H6GdT/vO82NXli2z8lRE8cu/nBIHrcOCXFBAHpb9ZldrB2/qOA==" "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": { "node_modules/std-env": {
"version": "3.3.3", "version": "3.3.3",
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.3.3.tgz", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.3.3.tgz",
@@ -11829,9 +11899,9 @@
"devOptional": true "devOptional": true
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "4.3.8", "version": "4.3.9",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.3.8.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.9.tgz",
"integrity": "sha512-uYB8PwN7hbMrf4j1xzGDk/lqjsZvCDbt/JC5dyfxc19Pg8kRm14LinK/uq+HSLNswZEoKmweGdtpbnxRtrAXiQ==", "integrity": "sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"esbuild": "^0.17.5", "esbuild": "^0.17.5",

View File

@@ -26,11 +26,15 @@
"@mantine/notifications": "^6.0.11", "@mantine/notifications": "^6.0.11",
"@mantine/spotlight": "^6.0.11", "@mantine/spotlight": "^6.0.11",
"@mantine/styles": "^6.0.11", "@mantine/styles": "^6.0.11",
"@monaco-editor/react": "^4.5.1",
"@reduxjs/toolkit": "^1.9.5", "@reduxjs/toolkit": "^1.9.5",
"axios": "^1.4.0", "axios": "^1.4.0",
"dayjs": "^1.11.7", "dayjs": "^1.11.7",
"escape-string-regexp": "^5.0.0",
"interweave": "^13.1.0", "interweave": "^13.1.0",
"monaco-editor": "^0.38.0",
"mousetrap": "^1.6.5", "mousetrap": "^1.6.5",
"re-resizable": "^6.9.9",
"react": "^18.2.0", "react": "^18.2.0",
"react-async-hook": "^4.0.0", "react-async-hook": "^4.0.0",
"react-contexify": "^6.0.0", "react-contexify": "^6.0.0",
@@ -72,7 +76,7 @@
"prettier": "^2.8.8", "prettier": "^2.8.8",
"rollup-plugin-visualizer": "^5.9.0", "rollup-plugin-visualizer": "^5.9.0",
"typescript": "^5.0.4", "typescript": "^5.0.4",
"vite": "^4.3.8", "vite": "^4.3.9",
"vite-plugin-eslint": "^1.8.1", "vite-plugin-eslint": "^1.8.1",
"vite-tsconfig-paths": "^4.2.0", "vite-tsconfig-paths": "^4.2.0",
"vitest": "^0.31.1", "vitest": "^0.31.1",

View File

@@ -5,7 +5,7 @@
<parent> <parent>
<groupId>com.commafeed</groupId> <groupId>com.commafeed</groupId>
<artifactId>commafeed</artifactId> <artifactId>commafeed</artifactId>
<version>3.4.0</version> <version>3.8.1</version>
</parent> </parent>
<artifactId>commafeed-client</artifactId> <artifactId>commafeed-client</artifactId>
<name>CommaFeed Client</name> <name>CommaFeed Client</name>

View File

@@ -12,6 +12,7 @@ import { categoryUnreadCount } from "app/utils"
import { ErrorBoundary } from "components/ErrorBoundary" import { ErrorBoundary } from "components/ErrorBoundary"
import { Header } from "components/header/Header" import { Header } from "components/header/Header"
import { Tree } from "components/sidebar/Tree" import { Tree } from "components/sidebar/Tree"
import { useBrowserExtension } from "hooks/useBrowserExtension"
import { useI18n } from "i18n" import { useI18n } from "i18n"
import { AdminUsersPage } from "pages/admin/AdminUsersPage" import { AdminUsersPage } from "pages/admin/AdminUsersPage"
import { MetricsPage } from "pages/admin/MetricsPage" import { MetricsPage } from "pages/admin/MetricsPage"
@@ -37,7 +38,7 @@ import useLocalStorage from "use-local-storage"
function Providers(props: { children: React.ReactNode }) { function Providers(props: { children: React.ReactNode }) {
const preferredColorScheme = useColorScheme() const preferredColorScheme = useColorScheme()
const [colorScheme, setColorScheme] = useLocalStorage<ColorScheme>("color-scheme", preferredColorScheme) 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 ( return (
<I18nProvider i18n={i18n}> <I18nProvider i18n={i18n}>
@@ -65,6 +66,9 @@ function Providers(props: { children: React.ReactNode }) {
const ApiDocumentationPage = React.lazy(() => import("pages/app/ApiDocumentationPage")) const ApiDocumentationPage = React.lazy(() => import("pages/app/ApiDocumentationPage"))
function AppRoutes() { function AppRoutes() {
const sidebarWidth = useAppSelector(state => state.tree.sidebarWidth)
const sidebarVisible = useAppSelector(state => state.tree.sidebarVisible)
return ( return (
<Routes> <Routes>
<Route path="/" element={<Navigate to={`/app/category/${Constants.categories.all.id}`} replace />} /> <Route path="/" element={<Navigate to={`/app/category/${Constants.categories.all.id}`} replace />} />
@@ -72,7 +76,8 @@ function AppRoutes() {
<Route path="login" element={<LoginPage />} /> <Route path="login" element={<LoginPage />} />
<Route path="register" element={<RegistrationPage />} /> <Route path="register" element={<RegistrationPage />} />
<Route path="passwordRecovery" element={<PasswordRecoveryPage />} /> <Route path="passwordRecovery" element={<PasswordRecoveryPage />} />
<Route path="app" element={<Layout header={<Header />} sidebar={<Tree />} />}> <Route path="api" element={<ApiDocumentationPage />} />
<Route path="app" element={<Layout header={<Header />} sidebar={<Tree />} sidebarWidth={sidebarVisible ? sidebarWidth : 0} />}>
<Route path="category"> <Route path="category">
<Route path=":id" element={<FeedEntriesPage sourceType="category" />} /> <Route path=":id" element={<FeedEntriesPage sourceType="category" />} />
<Route path=":id/details" element={<CategoryDetailsPage />} /> <Route path=":id/details" element={<CategoryDetailsPage />} />
@@ -93,7 +98,6 @@ function AppRoutes() {
</Route> </Route>
<Route path="about" element={<AboutPage />} /> <Route path="about" element={<AboutPage />} />
<Route path="donate" element={<DonatePage />} /> <Route path="donate" element={<DonatePage />} />
<Route path="api" element={<ApiDocumentationPage />} />
</Route> </Route>
<Route path="*" element={<Navigate to="/" replace />} /> <Route path="*" element={<Navigate to="/" replace />} />
</Routes> </Routes>
@@ -134,13 +138,28 @@ function FaviconHandler() {
const root = useAppSelector(state => state.tree.rootCategory) const root = useAppSelector(state => state.tree.rootCategory)
useEffect(() => { useEffect(() => {
const unreadCount = categoryUnreadCount(root) const unreadCount = categoryUnreadCount(root)
if (unreadCount === 0) Tinycon.reset() if (unreadCount === 0) {
else Tinycon.setBubble(unreadCount) Tinycon.reset()
} else {
Tinycon.setBubble(unreadCount)
}
}, [root]) }, [root])
return null 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() { export function App() {
useI18n() useI18n()
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
@@ -153,6 +172,7 @@ export function App() {
<Providers> <Providers>
<> <>
<FaviconHandler /> <FaviconHandler />
<BrowserExtensionBadgeUnreadCountHandler />
<HashRouter> <HashRouter>
<GoogleAnalyticsHandler /> <GoogleAnalyticsHandler />
<RedirectHandler /> <RedirectHandler />

View File

@@ -30,7 +30,10 @@ const axiosInstance = axios.create({ baseURL: "./rest", withCredentials: true })
axiosInstance.interceptors.response.use( axiosInstance.interceptors.response.use(
response => response, response => response,
error => { error => {
if (error.response.status === 401 && error.response.data === "Credentials are required to access this resource.") { if (
(error.response.status === 401 && error.response.data === "Credentials are required to access this resource.") ||
(error.response.status === 403 && error.response.data === "You don't have the required role to access this resource.")
) {
window.location.hash = "/welcome" window.location.hash = "/welcome"
} }
throw error throw error

View File

@@ -88,14 +88,14 @@ export const Constants = {
layout: { layout: {
mobileBreakpoint: DEFAULT_THEME.breakpoints.md, mobileBreakpoint: DEFAULT_THEME.breakpoints.md,
headerHeight: 60, headerHeight: 60,
sidebarWidth: 350,
entryMaxWidth: 650, entryMaxWidth: 650,
isTopVisible: (div: HTMLElement) => div.getBoundingClientRect().top >= Constants.layout.headerHeight, isTopVisible: (div: HTMLElement) => div.getBoundingClientRect().top >= Constants.layout.headerHeight,
isBottomVisible: (div: HTMLElement) => div.getBoundingClientRect().bottom <= window.innerHeight, isBottomVisible: (div: HTMLElement) => div.getBoundingClientRect().bottom <= window.innerHeight,
}, },
dom: { dom: {
mainScrollAreaId: "main-scroll-area-id",
entryId: (entry: Entry) => `entry-id-${entry.id}`, entryId: (entry: Entry) => `entry-id-${entry.id}`,
entryContextMenuId: (entry: Entry) => entry.id,
}, },
browserExtensionUrl: "https://github.com/Athou/commafeed-browser-extension",
bitcoinWalletAddress: "1dymfUxqCWpyD7a6rQSqNy4rLVDBsAr5e", bitcoinWalletAddress: "1dymfUxqCWpyD7a6rQSqNy4rLVDBsAr5e",
} }

View File

@@ -45,18 +45,27 @@ const initialState: EntriesState = {
const getEndpoint = (sourceType: EntrySourceType) => const getEndpoint = (sourceType: EntrySourceType) =>
sourceType === "category" || sourceType === "tag" ? client.category.getEntries : client.feed.getEntries sourceType === "category" || sourceType === "tag" ? client.category.getEntries : client.feed.getEntries
export const loadEntries = createAsyncThunk<Entries, { source: EntrySource; clearSearch: boolean }, { state: RootState }>( export const loadEntries = createAsyncThunk<
"entries/load", Entries,
async (arg, thunkApi) => { { source: EntrySource; clearSearch: boolean },
if (arg.clearSearch) thunkApi.dispatch(setSearch("")) {
state: RootState
const state = thunkApi.getState()
const endpoint = getEndpoint(arg.source.type)
const result = await endpoint(buildGetEntriesPaginatedRequest(state, arg.source, 0))
return result.data
} }
) >("entries/load", async (arg, thunkApi) => {
export const loadMoreEntries = createAsyncThunk<Entries, void, { state: RootState }>("entries/loadMore", async (_, 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 state = thunkApi.getState()
const { source } = state.entries const { source } = state.entries
const offset = const offset =
@@ -74,7 +83,13 @@ const buildGetEntriesPaginatedRequest = (state: RootState, source: EntrySource,
tag: source.type === "tag" ? source.id : undefined, tag: source.type === "tag" ? source.id : undefined,
keywords: state.entries.search, 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() const state = thunkApi.getState()
thunkApi.dispatch(loadEntries({ source: state.entries.source, clearSearch: false })) thunkApi.dispatch(loadEntries({ source: state.entries.source, clearSearch: false }))
}) })
@@ -123,15 +138,18 @@ export const markEntriesUpToEntry = createAsyncThunk<void, Entry, { state: RootS
) )
} }
) )
export const markAllEntries = createAsyncThunk<void, { sourceType: EntrySourceType; req: MarkRequest }, { state: RootState }>( export const markAllEntries = createAsyncThunk<
"entries/entry/markAll", void,
async (arg, thunkApi) => { { sourceType: EntrySourceType; req: MarkRequest },
const endpoint = arg.sourceType === "category" ? client.category.markEntries : client.feed.markEntries {
await endpoint(arg.req) state: RootState
thunkApi.dispatch(reloadEntries())
thunkApi.dispatch(reloadTree())
} }
) >("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 }) => { export const starEntry = createAsyncThunk("entries/entry/star", (arg: { entry: Entry; starred: boolean }) => {
client.entry.star({ client.entry.star({
id: arg.entry.id, id: arg.entry.id,
@@ -175,31 +193,25 @@ export const selectEntry = createAsyncThunk<
if (arg.scrollToEntry) { if (arg.scrollToEntry) {
const entryElement = document.getElementById(Constants.dom.entryId(entry)) const entryElement = document.getElementById(Constants.dom.entryId(entry))
if (entryElement) { if (entryElement) {
const scrollSpeed = state.user.settings?.scrollSpeed const alwaysScrollToEntry = state.user.settings?.alwaysScrollToEntry
thunkApi.dispatch(entriesSlice.actions.setScrollingToEntry(true)) const entryEntirelyVisible = Constants.layout.isTopVisible(entryElement) && Constants.layout.isBottomVisible(entryElement)
scrollToEntry(entryElement, scrollSpeed, () => thunkApi.dispatch(entriesSlice.actions.setScrollingToEntry(false))) 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) => { const scrollToEntry = (entryElement: HTMLElement, scrollSpeed: number | undefined, onScrollEnded: () => void) => {
// the entry is entirely visible, no need to scroll scrollToWithCallback({
if (Constants.layout.isTopVisible(entryElement) && Constants.layout.isBottomVisible(entryElement)) { options: {
onScrollEnded() // add a small gap between the top of the content and the top of the page
return top: entryElement.offsetTop - Constants.layout.headerHeight - 3,
} behavior: scrollSpeed && scrollSpeed > 0 ? "smooth" : "auto",
},
const scrollArea = document.getElementById(Constants.dom.mainScrollAreaId) onScrollEnded,
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,
})
}
} }
export const selectPreviousEntry = createAsyncThunk< export const selectPreviousEntry = createAsyncThunk<
@@ -248,7 +260,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) await client.entry.tag(arg)
thunkApi.dispatch(reloadTags()) thunkApi.dispatch(reloadTags())
}) })

View File

@@ -13,6 +13,8 @@ export const redirectToRegistration = createAsyncThunk("redirect/register", (_,
export const redirectToPasswordRecovery = createAsyncThunk("redirect/passwordRecovery", (_, thunkApi) => export const redirectToPasswordRecovery = createAsyncThunk("redirect/passwordRecovery", (_, thunkApi) =>
thunkApi.dispatch(redirectTo("/passwordRecovery")) thunkApi.dispatch(redirectTo("/passwordRecovery"))
) )
export const redirectToApiDocumentation = createAsyncThunk("redirect/api", (_, thunkApi) => thunkApi.dispatch(redirectTo("/api")))
export const redirectToSelectedSource = createAsyncThunk< export const redirectToSelectedSource = createAsyncThunk<
void, void,
void, void,
@@ -52,7 +54,6 @@ export const redirectToMetrics = createAsyncThunk("redirect/admin/metrics", (_,
) )
export const redirectToDonate = createAsyncThunk("redirect/donate", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/donate"))) export const redirectToDonate = createAsyncThunk("redirect/donate", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/donate")))
export const redirectToAbout = createAsyncThunk("redirect/about", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/about"))) export const redirectToAbout = createAsyncThunk("redirect/about", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/about")))
export const redirectToApiDocumentation = createAsyncThunk("redirect/api", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/api")))
export const redirectSlice = createSlice({ export const redirectSlice = createSlice({
name: "redirect", name: "redirect",

View File

@@ -9,10 +9,14 @@ import { redirectTo } from "./redirect"
interface TreeState { interface TreeState {
rootCategory?: Category rootCategory?: Category
mobileMenuOpen: boolean mobileMenuOpen: boolean
sidebarWidth: number
sidebarVisible: boolean
} }
const initialState: TreeState = { const initialState: TreeState = {
mobileMenuOpen: false, mobileMenuOpen: false,
sidebarWidth: 350,
sidebarVisible: true,
} }
export const reloadTree = createAsyncThunk("tree/reload", () => client.category.getRoot().then(r => r.data)) 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>) => { setMobileMenuOpen: (state, action: PayloadAction<boolean>) => {
state.mobileMenuOpen = action.payload state.mobileMenuOpen = action.payload
}, },
setSidebarWidth: (state, action: PayloadAction<number>) => {
state.sidebarWidth = action.payload
},
toggleSidebar: state => {
state.sidebarVisible = !state.sidebarVisible
},
}, },
extraReducers: builder => { extraReducers: builder => {
builder.addCase(reloadTree.fulfilled, (state, action) => { 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 export default treeSlice.reducer

View File

@@ -80,6 +80,17 @@ export const changeScrollMarks = createAsyncThunk<
if (!settings) return if (!settings) return
client.user.saveSettings({ ...settings, scrollMarks }) client.user.saveSettings({ ...settings, scrollMarks })
}) })
export const changeAlwaysScrollToEntry = createAsyncThunk<
void,
boolean,
{
state: RootState
}
>("settings/alwaysScrollToEntry", (alwaysScrollToEntry, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, alwaysScrollToEntry })
})
export const changeSharingSetting = createAsyncThunk< export const changeSharingSetting = createAsyncThunk<
void, void,
{ site: keyof SharingSettings; value: boolean }, { site: keyof SharingSettings; value: boolean },
@@ -136,6 +147,10 @@ export const userSlice = createSlice({
if (!state.settings) return if (!state.settings) return
state.settings.scrollMarks = action.meta.arg state.settings.scrollMarks = action.meta.arg
}) })
builder.addCase(changeAlwaysScrollToEntry.pending, (state, action) => {
if (!state.settings) return
state.settings.alwaysScrollToEntry = action.meta.arg
})
builder.addCase(changeSharingSetting.pending, (state, action) => { builder.addCase(changeSharingSetting.pending, (state, action) => {
if (!state.settings) return if (!state.settings) return
state.settings.sharingSettings[action.meta.arg.site] = action.meta.arg.value state.settings.sharingSettings[action.meta.arg.site] = action.meta.arg.value
@@ -146,6 +161,7 @@ export const userSlice = createSlice({
changeScrollSpeed.fulfilled, changeScrollSpeed.fulfilled,
changeShowRead.fulfilled, changeShowRead.fulfilled,
changeScrollMarks.fulfilled, changeScrollMarks.fulfilled,
changeAlwaysScrollToEntry.fulfilled,
changeSharingSetting.fulfilled changeSharingSetting.fulfilled
), ),
() => { () => {

View File

@@ -233,6 +233,7 @@ export interface Settings {
customCss?: string customCss?: string
customJs?: string customJs?: string
scrollSpeed: number scrollSpeed: number
alwaysScrollToEntry: boolean
sharingSettings: SharingSettings sharingSettings: SharingSettings
} }
@@ -271,7 +272,7 @@ export interface Subscription {
iconUrl: string iconUrl: string
unread: number unread: number
categoryId?: string categoryId?: string
position?: number position: number
newestItemTime?: number newestItemTime?: number
filter?: string filter?: string
} }

View File

@@ -1,3 +1,4 @@
import { throttle } from "throttle-debounce"
import { Category } from "./types" import { Category } from "./types"
export function visitCategoryTree(category: Category, visitor: (category: Category) => void): void { 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 } return { width: placeholderWidth, height: placeholderHeight }
} }
export const scrollToWithCallback = ({ export const scrollToWithCallback = ({ options, onScrollEnded }: { options: ScrollToOptions; onScrollEnded: () => void }) => {
element,
options,
onScrollEnded,
}: {
element: HTMLElement
options: ScrollToOptions
onScrollEnded: () => void
}) => {
const offset = (options.top ?? 0).toFixed() const offset = (options.top ?? 0).toFixed()
const onScroll = () => { const onScroll = throttle(100, () => {
if (element.offsetTop.toFixed() === offset) { if (window.scrollY.toFixed() === offset) {
element.removeEventListener("scroll", onScroll) window.removeEventListener("scroll", onScroll)
onScrollEnded() onScrollEnded()
} }
} })
window.addEventListener("scroll", onScroll)
element.addEventListener("scroll", onScroll)
// scrollTo does not trigger if there's nothing to do, trigger it manually // scrollTo does not trigger if there's nothing to do, trigger it manually
onScroll() onScroll()
element.scrollTo(options) window.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,
})
)
} }
export const truncate = (str: string, n: number) => (str.length > n ? `${str.slice(0, n - 1)}\u2026` : str) export const truncate = (str: string, n: number) => (str.length > n ? `${str.slice(0, n - 1)}\u2026` : str)

View File

@@ -1,15 +1,16 @@
import { ActionIcon, Button, useMantineTheme } from "@mantine/core" import { ActionIcon, Button, Tooltip, useMantineTheme } from "@mantine/core"
import { ActionIconProps } from "@mantine/core/lib/ActionIcon/ActionIcon" import { ActionIconProps } from "@mantine/core/lib/ActionIcon/ActionIcon"
import { ButtonProps } from "@mantine/core/lib/Button/Button" import { ButtonProps } from "@mantine/core/lib/Button/Button"
import { useMediaQuery } from "@mantine/hooks" import { useActionButton } from "hooks/useActionButton"
import { forwardRef, MouseEventHandler, ReactNode } from "react" import { forwardRef, MouseEventHandler, ReactNode } from "react"
interface ActionButtonProps { interface ActionButtonProps {
className?: string className?: string
icon?: ReactNode icon?: ReactNode
label?: ReactNode label: ReactNode
onClick?: MouseEventHandler onClick?: MouseEventHandler
variant?: ActionIconProps["variant"] & ButtonProps["variant"] variant?: ActionIconProps["variant"] & ButtonProps["variant"]
hideLabelOnDesktop?: boolean
showLabelOnMobile?: boolean showLabelOnMobile?: boolean
} }
@@ -17,14 +18,16 @@ interface ActionButtonProps {
* Switches between Button with label (desktop) and ActionIcon (mobile) * Switches between Button with label (desktop) and ActionIcon (mobile)
*/ */
export const ActionButton = forwardRef<HTMLButtonElement, ActionButtonProps>((props: ActionButtonProps, ref) => { export const ActionButton = forwardRef<HTMLButtonElement, ActionButtonProps>((props: ActionButtonProps, ref) => {
const { mobile } = useActionButton()
const theme = useMantineTheme() const theme = useMantineTheme()
const variant = props.variant ?? "subtle" const variant = props.variant ?? "subtle"
const mobile = !useMediaQuery(`(min-width: ${theme.breakpoints.lg})`) const iconOnly = (mobile && !props.showLabelOnMobile) || (!mobile && props.hideLabelOnDesktop)
const iconOnly = !props.showLabelOnMobile && (mobile || !props.label)
return iconOnly ? ( return iconOnly ? (
<ActionIcon ref={ref} color={theme.primaryColor} variant={variant} className={props.className} onClick={props.onClick}> <Tooltip label={props.label} openDelay={500}>
{props.icon} <ActionIcon ref={ref} color={theme.primaryColor} variant={variant} className={props.className} onClick={props.onClick}>
</ActionIcon> {props.icon}
</ActionIcon>
</Tooltip>
) : ( ) : (
<Button ref={ref} variant={variant} size="xs" className={props.className} leftIcon={props.icon} onClick={props.onClick}> <Button ref={ref} variant={variant} size="xs" className={props.className} leftIcon={props.icon} onClick={props.onClick}>
{props.label} {props.label}

View File

@@ -1,5 +0,0 @@
import { Group } from "@mantine/core"
export function ButtonToolbar(props: { children: React.ReactNode }) {
return <Group spacing={14}>{props.children}</Group>
}

View File

@@ -1,183 +1,200 @@
import { Trans } from "@lingui/macro" 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() { export function KeyboardShortcutsHelp() {
return ( return (
<Table striped highlightOnHover> <Stack spacing="xs">
<tbody> <Table striped highlightOnHover>
<tr> <tbody>
<td> <tr>
<Trans>Refresh</Trans> <td>
</td> <Trans>Refresh</Trans>
<td> </td>
<Kbd>R</Kbd> <td>
</td> <Kbd>R</Kbd>
</tr> </td>
<tr> </tr>
<td> <tr>
<Trans>Open next entry</Trans> <td>
</td> <Trans>Open next entry</Trans>
<td> </td>
<Kbd>J</Kbd> <td>
</td> <Kbd>J</Kbd>
</tr> </td>
<tr> </tr>
<td> <tr>
<Trans>Open previous entry</Trans> <td>
</td> <Trans>Open previous entry</Trans>
<td> </td>
<Kbd>K</Kbd> <td>
</td> <Kbd>K</Kbd>
</tr> </td>
<tr> </tr>
<td> <tr>
<Trans>Set focus on next entry without opening it</Trans> <td>
</td> <Trans>Set focus on next entry without opening it</Trans>
<td> </td>
<Kbd>N</Kbd> <td>
</td> <Kbd>N</Kbd>
</tr> </td>
<tr> </tr>
<td> <tr>
<Trans>Set focus on previous entry without opening it</Trans> <td>
</td> <Trans>Set focus on previous entry without opening it</Trans>
<td> </td>
<Kbd>P</Kbd> <td>
</td> <Kbd>P</Kbd>
</tr> </td>
<tr> </tr>
<td> <tr>
<Trans>Move the page down</Trans> <td>
</td> <Trans>Move the page down</Trans>
<td> </td>
<Kbd> <td>
<Trans>Space</Trans> <Kbd>
</Kbd> <Trans>Space</Trans>
</td> </Kbd>
</tr> </td>
<tr> </tr>
<td> <tr>
<Trans>Move the page up</Trans> <td>
</td> <Trans>Move the page up</Trans>
<td> </td>
<Kbd> <td>
<Trans>Shift</Trans> <Kbd>
</Kbd> <Trans>Shift</Trans>
<span> + </span> </Kbd>
<Kbd> <span> + </span>
<Trans>Space</Trans> <Kbd>
</Kbd> <Trans>Space</Trans>
</td> </Kbd>
</tr> </td>
<tr> </tr>
<td> <tr>
<Trans>Open/close current entry</Trans> <td>
</td> <Trans>Open/close current entry</Trans>
<td> </td>
<Kbd>O</Kbd> <td>
<span>, </span> <Kbd>O</Kbd>
<Kbd> <span>, </span>
<Trans>Enter</Trans> <Kbd>
</Kbd> <Trans>Enter</Trans>
</td> </Kbd>
</tr> </td>
<tr> </tr>
<td> <tr>
<Trans>Open current entry in a new tab</Trans> <td>
</td> <Trans>Open current entry in a new tab</Trans>
<td> </td>
<Kbd>V</Kbd> <td>
</td> <Kbd>V</Kbd>
</tr> </td>
<tr> </tr>
<td> <tr>
<Trans>Open current entry in a new tab in the background</Trans> <td>
</td> <Trans>Open current entry in a new tab in the background</Trans>
<td> </td>
<Kbd>B</Kbd> <td>
<span>, </span> <Kbd>B</Kbd>
<Kbd> <span>*, </span>
<Trans>Middle click</Trans> <Kbd>
</Kbd> <Trans>Middle click</Trans>
</td> </Kbd>
</tr> </td>
<tr> </tr>
<td> <tr>
<Trans>Toggle read status of current entry</Trans> <td>
</td> <Trans>Toggle read status of current entry</Trans>
<td> </td>
<Kbd>M</Kbd> <td>
<span>, </span> <Kbd>M</Kbd>
<Trans>Swipe header to the right</Trans> <span>, </span>
</td> <Trans>Swipe header to the right</Trans>
</tr> </td>
<tr> </tr>
<td> <tr>
<Trans>Mark all entries as read</Trans> <td>
</td> <Trans>Mark all entries as read</Trans>
<td> </td>
<Kbd> <td>
<Trans>Shift</Trans> <Kbd>
</Kbd> <Trans>Shift</Trans>
<span> + </span> </Kbd>
<Kbd>A</Kbd> <span> + </span>
</td> <Kbd>A</Kbd>
</tr> </td>
<tr> </tr>
<td> <tr>
<Trans>Go to the All view</Trans> <td>
</td> <Trans>Go to the All view</Trans>
<td> </td>
<Kbd>G</Kbd> <td>
<span> </span> <Kbd>G</Kbd>
<Kbd>A</Kbd> <span> </span>
</td> <Kbd>A</Kbd>
</tr> </td>
<tr> </tr>
<td> <tr>
<Trans>Navigate to a subscription by entering its name</Trans> <td>
</td> <Trans>Navigate to a subscription by entering its name</Trans>
<td> </td>
<Kbd> <td>
<Trans>Ctrl</Trans> <Kbd>
</Kbd> <Trans>Ctrl</Trans>
<span> + </span> </Kbd>
<Kbd>K</Kbd> <span> + </span>
<span>, </span> <Kbd>K</Kbd>
<Kbd>G</Kbd> <span>, </span>
<span> </span> <Kbd>G</Kbd>
<Kbd>U</Kbd> <span> </span>
</td> <Kbd>U</Kbd>
</tr> </td>
<tr> </tr>
<td> <tr>
<Trans>Show entry menu (desktop)</Trans> <td>
</td> <Trans>Show entry menu (desktop)</Trans>
<td> </td>
<Kbd> <td>
<Trans>Right click</Trans> <Kbd>
</Kbd> <Trans>Right click</Trans>
</td> </Kbd>
</tr> </td>
<tr> </tr>
<td> <tr>
<Trans>Show entry menu (mobile)</Trans> <td>
</td> <Trans>Show entry menu (mobile)</Trans>
<td> </td>
<Kbd> <td>
<Trans>Long press</Trans> <Kbd>
</Kbd> <Trans>Long press</Trans>
</td> </Kbd>
</tr> </td>
<tr> </tr>
<td> <tr>
<Trans>Show keyboard shortcut help</Trans> <td>
</td> <Trans>Toggle sidebar</Trans>
<td> </td>
<Kbd>?</Kbd> <td>
</td> <Kbd>F</Kbd>
</tr> </td>
</tbody> </tr>
</Table> <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>
) )
} }

View File

@@ -1,4 +1,5 @@
import { Trans } from "@lingui/macro" import { Trans } from "@lingui/macro"
import { Tooltip } from "@mantine/core"
import dayjs from "dayjs" import dayjs from "dayjs"
import { useEffect, useState } from "react" 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> 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>
)
} }

View 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>
)
}

View 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

View File

@@ -1,12 +1,14 @@
import { Box, createStyles, Mark, TypographyStylesProvider } from "@mantine/core" import { Box, createStyles, Mark, TypographyStylesProvider } from "@mantine/core"
import { Constants } from "app/constants" import { Constants } from "app/constants"
import { useAppSelector } from "app/store"
import { calculatePlaceholderSize } from "app/utils" import { calculatePlaceholderSize } from "app/utils"
import { ImageWithPlaceholderWhileLoading } from "components/ImageWithPlaceholderWhileLoading" import { ImageWithPlaceholderWhileLoading } from "components/ImageWithPlaceholderWhileLoading"
import escapeStringRegexp from "escape-string-regexp"
import { ChildrenNode, Interweave, Matcher, MatchResponse, Node, TransformCallback } from "interweave" import { ChildrenNode, Interweave, Matcher, MatchResponse, Node, TransformCallback } from "interweave"
import React from "react"
export interface ContentProps { export interface ContentProps {
content: string content: string
highlight?: string
} }
const useStyles = createStyles(theme => ({ const useStyles = createStyles(theme => ({
@@ -63,7 +65,7 @@ class HighlightMatcher extends Matcher {
constructor(search: string) { constructor(search: string) {
super("highlight") super("highlight")
this.search = search this.search = escapeStringRegexp(search)
} }
match(string: string): MatchResponse<unknown> | null { 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 { classes } = useStyles()
const search = useAppSelector(state => state.entries.search) const matchers = props.highlight ? [new HighlightMatcher(props.highlight)] : []
const matchers = search ? [new HighlightMatcher(search)] : []
return ( return (
<TypographyStylesProvider> <TypographyStylesProvider>
@@ -94,4 +96,6 @@ export function Content(props: ContentProps) {
</Box> </Box>
</TypographyStylesProvider> </TypographyStylesProvider>
) )
} })
export { Content }

View File

@@ -12,13 +12,15 @@ import {
selectPreviousEntry, selectPreviousEntry,
} from "app/slices/entries" } from "app/slices/entries"
import { redirectToRootCategory } from "app/slices/redirect" import { redirectToRootCategory } from "app/slices/redirect"
import { toggleSidebar } from "app/slices/tree"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { openLinkInBackgroundTab } from "app/utils"
import { KeyboardShortcutsHelp } from "components/KeyboardShortcutsHelp" import { KeyboardShortcutsHelp } from "components/KeyboardShortcutsHelp"
import { Loader } from "components/Loader" import { Loader } from "components/Loader"
import { useBrowserExtension } from "hooks/useBrowserExtension"
import { useMousetrap } from "hooks/useMousetrap" import { useMousetrap } from "hooks/useMousetrap"
import { useViewMode } from "hooks/useViewMode" import { useViewMode } from "hooks/useViewMode"
import { useEffect } from "react" import { useEffect } from "react"
import { useContextMenu } from "react-contexify"
import InfiniteScroll from "react-infinite-scroller" import InfiniteScroll from "react-infinite-scroller"
import { throttle } from "throttle-debounce" import { throttle } from "throttle-debounce"
import { FeedEntry } from "./FeedEntry" import { FeedEntry } from "./FeedEntry"
@@ -29,16 +31,18 @@ export function FeedEntries() {
const entriesTimestamp = useAppSelector(state => state.entries.timestamp) const entriesTimestamp = useAppSelector(state => state.entries.timestamp)
const selectedEntryId = useAppSelector(state => state.entries.selectedEntryId) const selectedEntryId = useAppSelector(state => state.entries.selectedEntryId)
const hasMore = useAppSelector(state => state.entries.hasMore) const hasMore = useAppSelector(state => state.entries.hasMore)
const { viewMode } = useViewMode()
const scrollMarks = useAppSelector(state => state.user.settings?.scrollMarks) const scrollMarks = useAppSelector(state => state.user.settings?.scrollMarks)
const scrollingToEntry = useAppSelector(state => state.entries.scrollingToEntry) const scrollingToEntry = useAppSelector(state => state.entries.scrollingToEntry)
const sidebarVisible = useAppSelector(state => state.tree.sidebarVisible)
const { viewMode } = useViewMode()
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const { openLinkInBackgroundTab } = useBrowserExtension()
const selectedEntry = entries.find(e => e.id === selectedEntryId) const selectedEntry = entries.find(e => e.id === selectedEntryId)
const headerClicked = (entry: ExpendableEntry, event: React.MouseEvent) => { const headerClicked = (entry: ExpendableEntry, event: React.MouseEvent) => {
if (event.button === 1 || event.ctrlKey || event.metaKey) { const middleClick = event.button === 1 || event.ctrlKey || event.metaKey
// middle click if (middleClick || viewMode === "expanded") {
dispatch(markEntry({ entry, read: true })) dispatch(markEntry({ entry, read: true }))
} else if (event.button === 0) { } else if (event.button === 0) {
// main click // main click
@@ -56,10 +60,42 @@ export function FeedEntries() {
} }
} }
useEffect(() => { const contextMenu = useContextMenu()
const scrollArea = document.getElementById(Constants.dom.mainScrollAreaId) const headerRightClicked = (entry: ExpendableEntry, event: React.MouseEvent) => {
event.preventDefault()
contextMenu.show({
id: Constants.dom.entryContextMenuId(entry),
event,
})
}
const listener = () => { 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 (viewMode !== "expanded") return
if (scrollingToEntry) return if (scrollingToEntry) return
@@ -81,11 +117,10 @@ export function FeedEntries() {
}) })
) )
} }
} })
const throttledListener = throttle(100, listener) window.addEventListener("scroll", listener)
scrollArea?.addEventListener("scroll", throttledListener) return () => window.removeEventListener("scroll", listener)
return () => scrollArea?.removeEventListener("scroll", throttledListener) }, [dispatch, contextMenu, entries, viewMode, scrollMarks, scrollingToEntry])
}, [dispatch, entries, viewMode, scrollMarks, scrollingToEntry])
useMousetrap("r", () => dispatch(reloadEntries())) useMousetrap("r", () => dispatch(reloadEntries()))
useMousetrap("j", () => useMousetrap("j", () =>
@@ -137,9 +172,8 @@ export function FeedEntries() {
}) })
) )
} else { } else {
const scrollArea = document.getElementById(Constants.dom.mainScrollAreaId) window.scrollTo({
scrollArea?.scrollTo({ top: window.scrollY + document.documentElement.clientHeight * 0.8,
top: scrollArea.scrollTop + scrollArea.clientHeight * 0.8,
behavior: "smooth", behavior: "smooth",
}) })
} }
@@ -176,9 +210,8 @@ export function FeedEntries() {
}) })
) )
} else { } else {
const scrollArea = document.getElementById(Constants.dom.mainScrollAreaId) window.scrollTo({
scrollArea?.scrollTo({ top: window.scrollY - document.documentElement.clientHeight * 0.8,
top: scrollArea.scrollTop - scrollArea.clientHeight * 0.8,
behavior: "smooth", behavior: "smooth",
}) })
} }
@@ -211,7 +244,6 @@ export function FeedEntries() {
window.open(selectedEntry.url, "_blank", "noreferrer") window.open(selectedEntry.url, "_blank", "noreferrer")
}) })
useMousetrap("b", () => { useMousetrap("b", () => {
// simulate ctrl+click to open tab in background
if (!selectedEntry) return if (!selectedEntry) return
openLinkInBackgroundTab(selectedEntry.url) openLinkInBackgroundTab(selectedEntry.url)
}) })
@@ -234,6 +266,7 @@ export function FeedEntries() {
) )
}) })
useMousetrap("g a", () => dispatch(redirectToRootCategory())) useMousetrap("g a", () => dispatch(redirectToRootCategory()))
useMousetrap("f", () => dispatch(toggleSidebar()))
useMousetrap("?", () => useMousetrap("?", () =>
openModal({ openModal({
title: <Trans>Keyboard shortcuts</Trans>, title: <Trans>Keyboard shortcuts</Trans>,
@@ -250,8 +283,6 @@ export function FeedEntries() {
loadMore={() => dispatch(loadMoreEntries())} loadMore={() => dispatch(loadMoreEntries())}
hasMore={hasMore} hasMore={hasMore}
loader={<Loader key={0} />} loader={<Loader key={0} />}
useWindow={false}
getScrollParent={() => document.getElementById(Constants.dom.mainScrollAreaId)}
> >
{entries.map(entry => ( {entries.map(entry => (
<div <div
@@ -263,8 +294,13 @@ export function FeedEntries() {
<FeedEntry <FeedEntry
entry={entry} entry={entry}
expanded={!!entry.expanded || viewMode === "expanded"} expanded={!!entry.expanded || viewMode === "expanded"}
selected={entry.id === selectedEntryId}
showSelectionIndicator={entry.id === selectedEntryId && (!entry.expanded || viewMode === "expanded")} showSelectionIndicator={entry.id === selectedEntryId && (!entry.expanded || viewMode === "expanded")}
maxWidth={sidebarVisible ? Constants.layout.entryMaxWidth : undefined}
onHeaderClick={event => headerClicked(entry, event)} onHeaderClick={event => headerClicked(entry, event)}
onHeaderRightClick={event => headerRightClicked(entry, event)}
onBodyClick={() => bodyClicked(entry)}
onSwipedRight={() => swipedRight(entry)}
/> />
</div> </div>
))} ))}

View File

@@ -1,46 +1,65 @@
import { Box, createStyles, Divider, Paper } from "@mantine/core" import { Box, createStyles, Divider, Paper } from "@mantine/core"
import { MantineNumberSize } from "@mantine/styles" import { MantineNumberSize } from "@mantine/styles"
import { Constants } from "app/constants" import { Constants } from "app/constants"
import { markEntry } from "app/slices/entries"
import { useAppDispatch } from "app/store"
import { Entry, ViewMode } from "app/types" import { Entry, ViewMode } from "app/types"
import { useViewMode } from "hooks/useViewMode" import { useViewMode } from "hooks/useViewMode"
import React from "react" import React from "react"
import { useSwipeable } from "react-swipeable" import { useSwipeable } from "react-swipeable"
import { FeedEntryBody } from "./FeedEntryBody" import { FeedEntryBody } from "./FeedEntryBody"
import { FeedEntryCompactHeader } from "./FeedEntryCompactHeader" import { FeedEntryCompactHeader } from "./FeedEntryCompactHeader"
import { FeedEntryContextMenu, useFeedEntryContextMenu } from "./FeedEntryContextMenu" import { FeedEntryContextMenu } from "./FeedEntryContextMenu"
import { FeedEntryFooter } from "./FeedEntryFooter" import { FeedEntryFooter } from "./FeedEntryFooter"
import { FeedEntryHeader } from "./FeedEntryHeader" import { FeedEntryHeader } from "./FeedEntryHeader"
interface FeedEntryProps { interface FeedEntryProps {
entry: Entry entry: Entry
expanded: boolean expanded: boolean
selected: boolean
showSelectionIndicator: boolean showSelectionIndicator: boolean
maxWidth?: number
onHeaderClick: (e: React.MouseEvent) => void 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 }) => { const useStyles = createStyles((theme, props: FeedEntryProps & { viewMode?: ViewMode }) => {
let backgroundColor let backgroundColor
if (theme.colorScheme === "dark") backgroundColor = props.entry.read ? "inherit" : theme.colors.dark[5] if (theme.colorScheme === "dark") {
else backgroundColor = props.entry.read && !props.expanded ? theme.colors.gray[0] : "inherit" backgroundColor = props.entry.read ? "inherit" : theme.colors.dark[5]
} else {
backgroundColor = props.entry.read && !props.expanded ? theme.colors.gray[0] : "inherit"
}
let marginY = 10 let marginY = 10
if (props.viewMode === "title") marginY = 2 if (props.viewMode === "title") {
else if (props.viewMode === "cozy") marginY = 6 marginY = 2
} else if (props.viewMode === "cozy") {
marginY = 6
}
let mobileMarginY = 6 let mobileMarginY = 6
if (props.viewMode === "title") mobileMarginY = 2 if (props.viewMode === "title") {
else if (props.viewMode === "cozy") mobileMarginY = 4 mobileMarginY = 2
} else if (props.viewMode === "cozy") {
mobileMarginY = 4
}
let backgroundHoverColor = backgroundColor let backgroundHoverColor = backgroundColor
if (!props.expanded && !props.entry.read) { if (!props.expanded && !props.entry.read) {
backgroundHoverColor = theme.colorScheme === "dark" ? theme.colors.dark[6] : theme.colors.gray[1] 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: { paper: {
backgroundColor, backgroundColor,
borderLeftColor: paperBorderLeftColor,
marginTop: marginY, marginTop: marginY,
marginBottom: marginY, marginBottom: marginY,
[theme.fn.smallerThan(Constants.layout.mobileBreakpoint)]: { [theme.fn.smallerThan(Constants.layout.mobileBreakpoint)]: {
@@ -58,44 +77,50 @@ const useStyles = createStyles((theme, props: FeedEntryProps & { viewMode?: View
textDecoration: "none", textDecoration: "none",
}, },
body: { 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) { export function FeedEntry(props: FeedEntryProps) {
const { viewMode } = useViewMode() const { viewMode } = useViewMode()
const { classes } = useStyles({ ...props, viewMode }) const { classes, cx } = useStyles({ ...props, viewMode })
const dispatch = useAppDispatch()
const swipeHandlers = useSwipeable({ 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" let paddingX: MantineNumberSize = "xs"
if (viewMode === "title" || viewMode === "cozy") paddingX = 6 if (viewMode === "title" || viewMode === "cozy") paddingX = 6
let paddingY: MantineNumberSize = "xs" let paddingY: MantineNumberSize = "xs"
if (viewMode === "title") paddingY = 4 if (viewMode === "title") {
else if (viewMode === "cozy") paddingY = 8 paddingY = 4
} else if (viewMode === "cozy") {
paddingY = 8
}
let borderRadius: MantineNumberSize = "sm" let borderRadius: MantineNumberSize = "sm"
if (viewMode === "title") borderRadius = 0 if (viewMode === "title") {
else if (viewMode === "cozy") borderRadius = "xs" borderRadius = 0
} else if (viewMode === "cozy") {
borderRadius = "xs"
}
const compactHeader = !props.expanded && (viewMode === "title" || viewMode === "cozy") const compactHeader = !props.expanded && (viewMode === "title" || viewMode === "cozy")
return ( return (
<Paper withBorder radius={borderRadius} className={classes.paper}> <Paper
withBorder
radius={borderRadius}
className={cx(classes.paper, {
read: props.entry.read,
unread: !props.entry.read,
expanded: props.expanded,
selected: props.selected,
"show-selection-indicator": props.showSelectionIndicator,
})}
>
<a <a
className={classes.headerLink} className={classes.headerLink}
href={props.entry.url} href={props.entry.url}
@@ -103,7 +128,7 @@ export function FeedEntry(props: FeedEntryProps) {
rel="noreferrer" rel="noreferrer"
onClick={props.onHeaderClick} onClick={props.onHeaderClick}
onAuxClick={props.onHeaderClick} onAuxClick={props.onHeaderClick}
onContextMenu={onContextMenu} onContextMenu={props.onHeaderRightClick}
> >
<Box px={paddingX} py={paddingY} {...swipeHandlers}> <Box px={paddingX} py={paddingY} {...swipeHandlers}>
{compactHeader && <FeedEntryCompactHeader entry={props.entry} />} {compactHeader && <FeedEntryCompactHeader entry={props.entry} />}
@@ -111,8 +136,8 @@ export function FeedEntry(props: FeedEntryProps) {
</Box> </Box>
</a> </a>
{props.expanded && ( {props.expanded && (
<Box px={paddingX} pb={paddingY}> <Box px={paddingX} pb={paddingY} onClick={props.onBodyClick}>
<Box className={classes.body} sx={{ direction: props.entry.rtl ? "rtl" : "ltr" }}> <Box className={classes.body}>
<FeedEntryBody entry={props.entry} /> <FeedEntryBody entry={props.entry} />
</Box> </Box>
<Divider variant="dashed" my={paddingY} /> <Divider variant="dashed" my={paddingY} />

View File

@@ -1,4 +1,5 @@
import { Box } from "@mantine/core" import { Box } from "@mantine/core"
import { useAppSelector } from "app/store"
import { Entry } from "app/types" import { Entry } from "app/types"
import { Content } from "./Content" import { Content } from "./Content"
import { Enclosure } from "./Enclosure" import { Enclosure } from "./Enclosure"
@@ -9,10 +10,11 @@ export interface FeedEntryBodyProps {
} }
export function FeedEntryBody(props: FeedEntryBodyProps) { export function FeedEntryBody(props: FeedEntryBodyProps) {
const search = useAppSelector(state => state.entries.search)
return ( return (
<Box> <Box>
<Box> <Box>
<Content content={props.entry.content} /> <Content content={props.entry.content} highlight={search} />
</Box> </Box>
{props.entry.enclosureType && props.entry.enclosureUrl && ( {props.entry.enclosureType && props.entry.enclosureUrl && (
<Box pt="md"> <Box pt="md">

View File

@@ -5,11 +5,10 @@ import { markEntriesUpToEntry, markEntry, starEntry } from "app/slices/entries"
import { redirectToFeed } from "app/slices/redirect" import { redirectToFeed } from "app/slices/redirect"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { Entry } from "app/types" import { Entry } from "app/types"
import { openLinkInBackgroundTab, truncate } from "app/utils" import { truncate } from "app/utils"
import { useEffect } from "react" import { useBrowserExtension } from "hooks/useBrowserExtension"
import { Item, Menu, Separator, useContextMenu } from "react-contexify" import { Item, Menu, Separator } from "react-contexify"
import { TbArrowBarToDown, TbExternalLink, TbEyeCheck, TbEyeOff, TbRss, TbStar, TbStarOff } from "react-icons/tb" import { TbArrowBarToDown, TbExternalLink, TbEyeCheck, TbEyeOff, TbRss, TbStar, TbStarOff } from "react-icons/tb"
import { throttle } from "throttle-debounce"
interface FeedEntryContextMenuProps { interface FeedEntryContextMenuProps {
entry: Entry entry: Entry
@@ -28,15 +27,14 @@ const useStyles = createStyles(theme => ({
}, },
})) }))
const menuId = (entry: Entry) => entry.id
export function FeedEntryContextMenu(props: FeedEntryContextMenuProps) { export function FeedEntryContextMenu(props: FeedEntryContextMenuProps) {
const { classes, theme } = useStyles() const { classes, theme } = useStyles()
const sourceType = useAppSelector(state => state.entries.source.type) const sourceType = useAppSelector(state => state.entries.source.type)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const { openLinkInBackgroundTab } = useBrowserExtension()
return ( 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 <Item
onClick={() => { onClick={() => {
window.open(props.entry.url, "_blank", "noreferrer") window.open(props.entry.url, "_blank", "noreferrer")
@@ -100,29 +98,3 @@ export function FeedEntryContextMenu(props: FeedEntryContextMenuProps) {
</Menu> </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 }
}

View File

@@ -1,15 +1,12 @@
import { t, Trans } from "@lingui/macro" import { t, Trans } from "@lingui/macro"
import { Group, Indicator, MultiSelect, Popover } from "@mantine/core" 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 { markEntriesUpToEntry, markEntry, starEntry, tagEntry } from "app/slices/entries"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { Entry } from "app/types" import { Entry } from "app/types"
import { ActionButton } from "components/ActionButtton" import { ActionButton } from "components/ActionButton"
import { ButtonToolbar } from "components/ButtonToolbar" import { useActionButton } from "hooks/useActionButton"
import { useEffect, useState } from "react" import { useMobile } from "hooks/useMobile"
import { TbArrowBarToDown, TbExternalLink, TbEyeCheck, TbEyeOff, TbShare, TbStar, TbStarOff, TbTag } from "react-icons/tb" import { TbArrowBarToDown, TbExternalLink, TbEyeCheck, TbEyeOff, TbShare, TbStar, TbStarOff, TbTag } from "react-icons/tb"
import { throttle } from "throttle-debounce"
import { ShareButtons } from "./ShareButtons" import { ShareButtons } from "./ShareButtons"
interface FeedEntryFooterProps { interface FeedEntryFooterProps {
@@ -17,10 +14,10 @@ interface FeedEntryFooterProps {
} }
export function FeedEntryFooter(props: FeedEntryFooterProps) { export function FeedEntryFooter(props: FeedEntryFooterProps) {
const [scrollPosition, setScrollPosition] = useState(0)
const sharingSettings = useAppSelector(state => state.user.settings?.sharingSettings) const sharingSettings = useAppSelector(state => state.user.settings?.sharingSettings)
const tags = useAppSelector(state => state.user.tags) 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 dispatch = useAppDispatch()
const showSharingButtons = sharingSettings && Object.values(sharingSettings).some(v => v) 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 ( return (
<Group position="apart"> <Group position="apart">
<ButtonToolbar> <Group spacing={spacing}>
{props.entry.markable && ( {props.entry.markable && (
<ActionButton <ActionButton
icon={props.entry.read ? <TbEyeOff size={18} /> : <TbEyeCheck size={18} />} icon={props.entry.read ? <TbEyeOff size={18} /> : <TbEyeCheck size={18} />}
@@ -61,7 +48,7 @@ export function FeedEntryFooter(props: FeedEntryFooterProps) {
/> />
{showSharingButtons && ( {showSharingButtons && (
<Popover withArrow withinPortal shadow="md" positionDependencies={[scrollPosition]} closeOnClickOutside={!mobile}> <Popover withArrow withinPortal shadow="md" closeOnClickOutside={!mobile}>
<Popover.Target> <Popover.Target>
<ActionButton icon={<TbShare size={18} />} label={<Trans>Share</Trans>} /> <ActionButton icon={<TbShare size={18} />} label={<Trans>Share</Trans>} />
</Popover.Target> </Popover.Target>
@@ -72,7 +59,7 @@ export function FeedEntryFooter(props: FeedEntryFooterProps) {
)} )}
{tags && ( {tags && (
<Popover withArrow withinPortal shadow="md" positionDependencies={[scrollPosition]} closeOnClickOutside={!mobile}> <Popover withArrow withinPortal shadow="md" closeOnClickOutside={!mobile}>
<Popover.Target> <Popover.Target>
<Indicator label={props.entry.tags.length} disabled={props.entry.tags.length === 0} inline size={16}> <Indicator label={props.entry.tags.length} disabled={props.entry.tags.length === 0} inline size={16}>
<ActionButton icon={<TbTag size={18} />} label={<Trans>Tags</Trans>} /> <ActionButton icon={<TbTag size={18} />} label={<Trans>Tags</Trans>} />
@@ -96,7 +83,7 @@ export function FeedEntryFooter(props: FeedEntryFooterProps) {
<a href={props.entry.url} target="_blank" rel="noreferrer"> <a href={props.entry.url} target="_blank" rel="noreferrer">
<ActionButton icon={<TbExternalLink size={18} />} label={<Trans>Open link</Trans>} /> <ActionButton icon={<TbExternalLink size={18} />} label={<Trans>Open link</Trans>} />
</a> </a>
</ButtonToolbar> </Group>
<ActionButton <ActionButton
icon={<TbArrowBarToDown size={18} />} icon={<TbArrowBarToDown size={18} />}

View File

@@ -1,14 +1,29 @@
import { t, Trans } from "@lingui/macro" 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 { 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 { changeReadingMode, changeReadingOrder } from "app/slices/user"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { ActionButton } from "components/ActionButtton" import { ActionButton } from "components/ActionButton"
import { ButtonToolbar } from "components/ButtonToolbar"
import { Loader } from "components/Loader" 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 { useEffect } from "react"
import { TbArrowDown, TbArrowUp, TbEye, TbEyeOff, TbRefresh, TbSearch, 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 { MarkAllAsReadButton } from "./MarkAllAsReadButton"
import { ProfileMenu } from "./ProfileMenu" import { ProfileMenu } from "./ProfileMenu"
@@ -16,12 +31,32 @@ function HeaderDivider() {
return <Divider orientation="vertical" /> 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 const iconSize = 18
export function Header() { export function Header() {
const settings = useAppSelector(state => state.user.settings) const settings = useAppSelector(state => state.user.settings)
const profile = useAppSelector(state => state.user.profile) const profile = useAppSelector(state => state.user.profile)
const searchFromStore = useAppSelector(state => state.entries.search) const searchFromStore = useAppSelector(state => state.entries.search)
const { isBrowserExtensionPopup, openSettingsPage, openAppInNewTab } = useBrowserExtension()
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const searchForm = useForm<{ search: string }>({ const searchForm = useForm<{ search: string }>({
@@ -40,7 +75,36 @@ export function Header() {
if (!settings) return <Loader /> if (!settings) return <Loader />
return ( return (
<Center> <Center>
<ButtonToolbar> <HeaderToolbar>
<ActionButton
icon={<TbArrowDown size={iconSize} />}
label={<Trans>Next</Trans>}
onClick={() =>
dispatch(
selectNextEntry({
expand: true,
markAsRead: true,
scrollToEntry: true,
})
)
}
/>
<ActionButton
icon={<TbArrowUp size={iconSize} />}
label={<Trans>Previous</Trans>}
onClick={() =>
dispatch(
selectPreviousEntry({
expand: true,
markAsRead: true,
scrollToEntry: true,
})
)
}
/>
<HeaderDivider />
<ActionButton <ActionButton
icon={<TbRefresh size={iconSize} />} icon={<TbRefresh size={iconSize} />}
label={<Trans>Refresh</Trans>} label={<Trans>Refresh</Trans>}
@@ -56,7 +120,7 @@ export function Header() {
onClick={() => dispatch(changeReadingMode(settings.readingMode === "all" ? "unread" : "all"))} onClick={() => dispatch(changeReadingMode(settings.readingMode === "all" ? "unread" : "all"))}
/> />
<ActionButton <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>} label={settings.readingOrder === "asc" ? <Trans>Asc</Trans> : <Trans>Desc</Trans>}
onClick={() => dispatch(changeReadingOrder(settings.readingOrder === "asc" ? "desc" : "asc"))} onClick={() => dispatch(changeReadingOrder(settings.readingOrder === "asc" ? "desc" : "asc"))}
/> />
@@ -87,7 +151,24 @@ export function Header() {
<HeaderDivider /> <HeaderDivider />
<ProfileMenu control={<ActionButton icon={<TbUser size={iconSize} />} label={profile?.name} />} /> <ProfileMenu control={<ActionButton icon={<TbUser size={iconSize} />} label={profile?.name} />} />
</ButtonToolbar>
{isBrowserExtensionPopup && (
<>
<HeaderDivider />
<ActionButton
icon={<TbSettings size={iconSize} />}
label={<Trans>Extension options</Trans>}
onClick={() => openSettingsPage()}
/>
<ActionButton
icon={<TbExternalLink size={iconSize} />}
label={<Trans>Open CommaFeed</Trans>}
onClick={() => openAppInNewTab()}
/>
</>
)}
</HeaderToolbar>
</Center> </Center>
) )
} }

View File

@@ -3,7 +3,7 @@ import { Trans } from "@lingui/macro"
import { Button, Code, Group, Modal, Slider, Stack, Text } from "@mantine/core" import { Button, Code, Group, Modal, Slider, Stack, Text } from "@mantine/core"
import { markAllEntries } from "app/slices/entries" import { markAllEntries } from "app/slices/entries"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { ActionButton } from "components/ActionButtton" import { ActionButton } from "components/ActionButton"
import { useState } from "react" import { useState } from "react"
import { TbChecks } from "react-icons/tb" import { TbChecks } from "react-icons/tb"

View File

@@ -1,11 +1,8 @@
import { Box, MediaQuery } from "@mantine/core" import { Box } from "@mantine/core"
import { Constants } from "app/constants" import { useMobile } from "hooks/useMobile"
import React from "react" import React from "react"
export function OnDesktop(props: { children: React.ReactNode }) { export function OnDesktop(props: { children: React.ReactNode }) {
return ( const mobile = useMobile()
<MediaQuery smallerThan={Constants.layout.mobileBreakpoint} styles={{ display: "none" }}> return <Box>{!mobile && props.children}</Box>
<Box>{props.children}</Box>
</MediaQuery>
)
} }

View File

@@ -1,11 +1,8 @@
import { Box, MediaQuery } from "@mantine/core" import { Box } from "@mantine/core"
import { Constants } from "app/constants" import { useMobile } from "hooks/useMobile"
import React from "react" import React from "react"
export function OnMobile(props: { children: React.ReactNode }) { export function OnMobile(props: { children: React.ReactNode }) {
return ( const mobile = useMobile()
<MediaQuery largerThan={Constants.layout.mobileBreakpoint} styles={{ display: "none" }}> return <Box>{mobile && props.children}</Box>
<Box>{props.children}</Box>
</MediaQuery>
)
} }

View File

@@ -1,96 +1,83 @@
import { Trans } from "@lingui/macro" import { Trans } from "@lingui/macro"
import { Box, Button, Group, Stack, Textarea } from "@mantine/core" import { Box, Button, Group, Stack } from "@mantine/core"
import { useForm } from "@mantine/form" import { useForm } from "@mantine/form"
import { client, errorToStrings } from "app/client" import { client, errorToStrings } from "app/client"
import { redirectToSelectedSource } from "app/slices/redirect" import { redirectToSelectedSource } from "app/slices/redirect"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { Alert } from "components/Alert" import { Alert } from "components/Alert"
import { useEffect } from "react" import { CodeEditor } from "components/code/CodeEditor"
import { useAsyncCallback } from "react-async-hook" import { useEffect } from "react"
import { TbDeviceFloppy } from "react-icons/tb" import { useAsyncCallback } from "react-async-hook"
import { TbDeviceFloppy } from "react-icons/tb"
interface FormData {
customCss: string interface FormData {
customJs: string customCss: string
} customJs: string
}
export function CustomCodeSettings() {
const settings = useAppSelector(state => state.user.settings) export function CustomCodeSettings() {
const dispatch = useAppDispatch() const settings = useAppSelector(state => state.user.settings)
const dispatch = useAppDispatch()
const form = useForm<FormData>()
const { setValues } = form const form = useForm<FormData>()
const { setValues } = form
const saveCustomCode = useAsyncCallback(
async (d: FormData) => { const saveCustomCode = useAsyncCallback(
if (!settings) return async (d: FormData) => {
await client.user.saveSettings({ if (!settings) return
...settings, await client.user.saveSettings({
customCss: d.customCss, ...settings,
customJs: d.customJs, customCss: d.customCss,
}) customJs: d.customJs,
}, })
{ },
onSuccess: () => { {
window.location.reload() onSuccess: () => {
}, window.location.reload()
} },
) }
)
useEffect(() => {
if (!settings) return useEffect(() => {
setValues({ if (!settings) return
customCss: settings.customCss, setValues({
customJs: settings.customJs, customCss: settings.customCss,
}) customJs: settings.customJs,
}, [setValues, settings]) })
}, [setValues, settings])
return (
<> return (
{saveCustomCode.error && ( <>
<Box mb="md"> {saveCustomCode.error && (
<Alert messages={errorToStrings(saveCustomCode.error)} /> <Box mb="md">
</Box> <Alert messages={errorToStrings(saveCustomCode.error)} />
)} </Box>
)}
<form onSubmit={form.onSubmit(saveCustomCode.execute)}>
<Stack> <form onSubmit={form.onSubmit(saveCustomCode.execute)}>
<Textarea <Stack>
autosize <CodeEditor
minRows={4} description={<Trans>Custom CSS rules that will be applied</Trans>}
maxRows={15} language="css"
{...form.getInputProps("customCss")} {...form.getInputProps("customCss")}
description={<Trans>Custom CSS rules that will be applied</Trans>} />
styles={{
input: { <CodeEditor
fontFamily: "monospace", description={<Trans>Custom JS code that will be executed on page load</Trans>}
}, language="javascript"
}} {...form.getInputProps("customJs")}
/> />
<Textarea <Group>
autosize <Button variant="default" onClick={() => dispatch(redirectToSelectedSource())}>
minRows={4} <Trans>Cancel</Trans>
maxRows={15} </Button>
{...form.getInputProps("customJs")} <Button type="submit" leftIcon={<TbDeviceFloppy size={16} />} loading={saveCustomCode.loading}>
description={<Trans>Custom JS code that will be executed on page load</Trans>} <Trans>Save</Trans>
styles={{ </Button>
input: { </Group>
fontFamily: "monospace", </Stack>
}, </form>
}} </>
/> )
}
<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>
</>
)
}

View File

@@ -1,7 +1,14 @@
import { Trans } from "@lingui/macro" import { Trans } from "@lingui/macro"
import { Divider, Select, SimpleGrid, Stack, Switch } from "@mantine/core" import { Divider, Select, SimpleGrid, Stack, Switch } from "@mantine/core"
import { Constants } from "app/constants" import { Constants } from "app/constants"
import { changeLanguage, changeScrollMarks, changeScrollSpeed, changeSharingSetting, changeShowRead } from "app/slices/user" import {
changeAlwaysScrollToEntry,
changeLanguage,
changeScrollMarks,
changeScrollSpeed,
changeSharingSetting,
changeShowRead,
} from "app/slices/user"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { SharingSettings } from "app/types" import { SharingSettings } from "app/types"
import { locales } from "i18n" import { locales } from "i18n"
@@ -11,6 +18,7 @@ export function DisplaySettings() {
const scrollSpeed = useAppSelector(state => state.user.settings?.scrollSpeed) const scrollSpeed = useAppSelector(state => state.user.settings?.scrollSpeed)
const showRead = useAppSelector(state => state.user.settings?.showRead) const showRead = useAppSelector(state => state.user.settings?.showRead)
const scrollMarks = useAppSelector(state => state.user.settings?.scrollMarks) const scrollMarks = useAppSelector(state => state.user.settings?.scrollMarks)
const alwaysScrollToEntry = useAppSelector(state => state.user.settings?.alwaysScrollToEntry)
const sharingSettings = useAppSelector(state => state.user.settings?.sharingSettings) const sharingSettings = useAppSelector(state => state.user.settings?.sharingSettings)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
@@ -32,6 +40,12 @@ export function DisplaySettings() {
onChange={e => dispatch(changeScrollSpeed(e.currentTarget.checked))} 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 <Switch
label={<Trans>Show feeds and categories with no unread entries</Trans>} label={<Trans>Show feeds and categories with no unread entries</Trans>}
checked={showRead} checked={showRead}

View 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 }
}

View File

@@ -0,0 +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 isBrowserExtensionPopup = window.parent !== window
const isBrowserExtensionInstalled = isBrowserExtensionPopup || !!browserExtensionVersion
const isBrowserExtensionInstallable = !isBrowserExtensionPopup
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 {
browserExtensionVersion,
isBrowserExtensionInstallable,
isBrowserExtensionInstalled,
isBrowserExtensionPopup,
openSettingsPage,
openAppInNewTab,
openLinkInBackgroundTab,
setBadgeUnreadCount,
}
}

View File

@@ -0,0 +1,4 @@
import { useMediaQuery } from "@mantine/hooks"
import { Constants } from "app/constants"
export const useMobile = (breakpoint: string = Constants.layout.mobileBreakpoint) => !useMediaQuery(`(min-width: ${breakpoint})`)

View File

@@ -17,6 +17,10 @@ msgstr ""
msgid "{0} (in {1})" msgid "{0} (in {1})"
msgstr "" msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "<0>Complete syntax is available </0><1>here</1>." msgid "<0>Complete syntax is available </0><1>here</1>."
msgstr "" msgstr ""
@@ -67,6 +71,10 @@ msgstr "إداري"
msgid "All" msgid "All"
msgstr "الكل" 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 #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "تم إرسال بريد إلكتروني إذا تم تسجيل هذا العنوان. " msgstr "تم إرسال بريد إلكتروني إذا تم تسجيل هذا العنوان. "
@@ -123,9 +131,13 @@ msgstr "العودة"
msgid "Back to log in" msgid "Back to log in"
msgstr "العودة لتسجيل الدخول" msgstr "العودة لتسجيل الدخول"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "ملحقات المستعرض" msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -160,16 +172,16 @@ msgid "Check that the feed is working"
msgstr "تأكد من عمل الخلاصة" msgstr "تأكد من عمل الخلاصة"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed is an open-source project. Sources are hosted on <0>GitHub</0>." msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "CommaFeed هو مشروع مفتوح المصدر. " msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed التالي العنصر غير المقروء" msgstr "CommaFeed التالي العنصر غير المقروء"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "إصدار CommaFeed {الإصدار} ({مراجعة})" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -308,6 +320,11 @@ msgstr "موسع"
msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services" msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services"
msgstr "قم بتصدير اشتراكاتك وفئاتك كملف OPML يمكن استيراده في خدمات قراءة الأعلاف الأخرى" msgstr "قم بتصدير اشتراكاتك وفئاتك كملف OPML يمكن استيراده في خدمات قراءة الأعلاف الأخرى"
#: src/components/header/Header.tsx
#: src/pages/WelcomePage.tsx
msgid "Extension options"
msgstr ""
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
msgid "Feed name" msgid "Feed name"
msgstr "اسم الخلاصة" msgstr "اسم الخلاصة"
@@ -513,6 +530,7 @@ msgid "Newest first"
msgstr "الأحدث أولاً" msgstr "الأحدث أولاً"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "التالي" msgstr "التالي"
@@ -540,6 +558,10 @@ msgstr "الأقدم أولا"
msgid "Oops!" msgid "Oops!"
msgstr "اوووه!" msgstr "اوووه!"
#: src/components/header/Header.tsx
msgid "Open CommaFeed"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Open current entry in a new tab" msgid "Open current entry in a new tab"
msgstr "فتح الإدخال الحالي في علامة تبويب جديدة" msgstr "فتح الإدخال الحالي في علامة تبويب جديدة"
@@ -618,6 +640,10 @@ msgstr "كلمات المرور غير متطابقة"
msgid "Position" msgid "Position"
msgstr "المنـصب" msgstr "المنـصب"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "الملف الشخصي" msgstr "الملف الشخصي"
@@ -759,10 +785,12 @@ msgid "Swipe header to the right"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to dark theme" msgid "Switch to dark theme"
msgstr "التبديل إلى النسق الداكن" msgstr "التبديل إلى النسق الداكن"
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to light theme" msgid "Switch to light theme"
msgstr "قم بالتبديل إلى النسق الفاتح" msgstr "قم بالتبديل إلى النسق الفاتح"
@@ -782,6 +810,10 @@ msgstr "الموضوع"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "تبديل قراءة حالة الإدخال الحالي" msgstr "تبديل قراءة حالة الإدخال الحالي"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "جرب CommaFeed باستخدام الحساب التجريبي: تجريبي / تجريبي" msgstr "جرب CommaFeed باستخدام الحساب التجريبي: تجريبي / تجريبي"

View File

@@ -17,6 +17,10 @@ msgstr ""
msgid "{0} (in {1})" msgid "{0} (in {1})"
msgstr "" msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "<0>Complete syntax is available </0><1>here</1>." msgid "<0>Complete syntax is available </0><1>here</1>."
msgstr "" msgstr ""
@@ -67,6 +71,10 @@ msgstr "Administrador"
msgid "All" msgid "All"
msgstr "Tot" 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 #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "S'ha enviat un correu electrònic si aquesta adreça estava registrada. " msgstr "S'ha enviat un correu electrònic si aquesta adreça estava registrada. "
@@ -123,9 +131,13 @@ msgstr "Enrere"
msgid "Back to log in" msgid "Back to log in"
msgstr "Tornar a iniciar sessió" msgstr "Tornar a iniciar sessió"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "Extensions del navegador" msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -160,16 +172,16 @@ msgid "Check that the feed is working"
msgstr "Comproveu que el canal funciona" msgstr "Comproveu que el canal funciona"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed is an open-source project. Sources are hosted on <0>GitHub</0>." msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "CommaFeed és un projecte de codi obert. " msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed següent element no llegit" msgstr "CommaFeed següent element no llegit"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "Versió CommaFeed {versió} ({revisió})" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -308,6 +320,11 @@ msgstr "Ampliat"
msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services" msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services"
msgstr "exporteu les vostres subscripcions i categories com a fitxer OPML que es pot importar a altres serveis de lectura de feeds" msgstr "exporteu les vostres subscripcions i categories com a fitxer OPML que es pot importar a altres serveis de lectura de feeds"
#: src/components/header/Header.tsx
#: src/pages/WelcomePage.tsx
msgid "Extension options"
msgstr ""
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
msgid "Feed name" msgid "Feed name"
msgstr "Nom del canal" msgstr "Nom del canal"
@@ -513,6 +530,7 @@ msgid "Newest first"
msgstr "El més nou primer" msgstr "El més nou primer"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "Següent" msgstr "Següent"
@@ -540,6 +558,10 @@ msgstr "el més vell primer"
msgid "Oops!" msgid "Oops!"
msgstr "Vaja!" msgstr "Vaja!"
#: src/components/header/Header.tsx
msgid "Open CommaFeed"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Open current entry in a new tab" msgid "Open current entry in a new tab"
msgstr "Obre l'entrada actual en una pestanya nova" msgstr "Obre l'entrada actual en una pestanya nova"
@@ -618,6 +640,10 @@ msgstr "Les contrasenyes no coincideixen"
msgid "Position" msgid "Position"
msgstr "Posició" msgstr "Posició"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "Perfil" msgstr "Perfil"
@@ -759,10 +785,12 @@ msgid "Swipe header to the right"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to dark theme" msgid "Switch to dark theme"
msgstr "Canvia al tema fosc" msgstr "Canvia al tema fosc"
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to light theme" msgid "Switch to light theme"
msgstr "Canvia al tema clar" msgstr "Canvia al tema clar"
@@ -782,6 +810,10 @@ msgstr "Tema"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "Canvia l'estat de lectura de l'entrada actual" msgstr "Canvia l'estat de lectura de l'entrada actual"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Proveu CommaFeed amb el compte de demostració: demo/demo" msgstr "Proveu CommaFeed amb el compte de demostració: demo/demo"

View File

@@ -17,6 +17,10 @@ msgstr ""
msgid "{0} (in {1})" msgid "{0} (in {1})"
msgstr "" msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "<0>Complete syntax is available </0><1>here</1>." msgid "<0>Complete syntax is available </0><1>here</1>."
msgstr "" msgstr ""
@@ -67,6 +71,10 @@ msgstr "Správce"
msgid "All" msgid "All"
msgstr "Všechny" 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 #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "Pokud byla tato adresa zaregistrována, byl odeslán e-mail. " msgstr "Pokud byla tato adresa zaregistrována, byl odeslán e-mail. "
@@ -123,9 +131,13 @@ msgstr "Zpět"
msgid "Back to log in" msgid "Back to log in"
msgstr "Zpět k přihlášení" msgstr "Zpět k přihlášení"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "Rozšíření prohlížeče" msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -160,16 +172,16 @@ msgid "Check that the feed is working"
msgstr "Zkontrolujte, zda zdroj funguje" msgstr "Zkontrolujte, zda zdroj funguje"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed is an open-source project. Sources are hosted on <0>GitHub</0>." msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "CommaFeed je projekt s otevřeným zdrojovým kódem. " msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed další nepřečtená položka" msgstr "CommaFeed další nepřečtená položka"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "CommaFeed verze {version} ({revision})" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -308,6 +320,11 @@ msgstr "Rozbaleno"
msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services" msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services"
msgstr "Exportujte svá předplatná a kategorie jako soubor OPML, který lze importovat do jiných služeb čtení kanálů" msgstr "Exportujte svá předplatná a kategorie jako soubor OPML, který lze importovat do jiných služeb čtení kanálů"
#: src/components/header/Header.tsx
#: src/pages/WelcomePage.tsx
msgid "Extension options"
msgstr ""
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
msgid "Feed name" msgid "Feed name"
msgstr "Název zdroje" msgstr "Název zdroje"
@@ -513,6 +530,7 @@ msgid "Newest first"
msgstr "Nejnovější jako první" msgstr "Nejnovější jako první"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "Další" msgstr "Další"
@@ -540,6 +558,10 @@ msgstr "Nejdříve nejstarší"
msgid "Oops!" msgid "Oops!"
msgstr "Jejda!" msgstr "Jejda!"
#: src/components/header/Header.tsx
msgid "Open CommaFeed"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Open current entry in a new tab" msgid "Open current entry in a new tab"
msgstr "Otevřete aktuální položku na nové kartě" msgstr "Otevřete aktuální položku na nové kartě"
@@ -618,6 +640,10 @@ msgstr "Hesla se neshodují"
msgid "Position" msgid "Position"
msgstr "Pozice" msgstr "Pozice"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
@@ -759,10 +785,12 @@ msgid "Swipe header to the right"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to dark theme" msgid "Switch to dark theme"
msgstr "Přepněte na tmavý motiv" msgstr "Přepněte na tmavý motiv"
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to light theme" msgid "Switch to light theme"
msgstr "Přepněte na světlé téma" msgstr "Přepněte na světlé téma"
@@ -782,6 +810,10 @@ msgstr "Téma"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "Přepne stav čtení aktuálního záznamu" msgstr "Přepne stav čtení aktuálního záznamu"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Vyzkoušejte CommaFeed s demo účtem: demo/demo" msgstr "Vyzkoušejte CommaFeed s demo účtem: demo/demo"

View File

@@ -17,6 +17,10 @@ msgstr ""
msgid "{0} (in {1})" msgid "{0} (in {1})"
msgstr "" msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "<0>Complete syntax is available </0><1>here</1>." msgid "<0>Complete syntax is available </0><1>here</1>."
msgstr "" msgstr ""
@@ -67,6 +71,10 @@ msgstr "Gweinyddol"
msgid "All" msgid "All"
msgstr "Pawb" 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 #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "Mae e-bost wedi'i anfon os oedd y cyfeiriad hwn wedi'i gofrestru. " msgstr "Mae e-bost wedi'i anfon os oedd y cyfeiriad hwn wedi'i gofrestru. "
@@ -123,9 +131,13 @@ msgstr "Yn ôl"
msgid "Back to log in" msgid "Back to log in"
msgstr "Yn ôl i fewngofnodi" msgstr "Yn ôl i fewngofnodi"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "Estyniadau porwr" msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -160,16 +172,16 @@ msgid "Check that the feed is working"
msgstr "Gwiriwch fod y porthiant yn gweithio" msgstr "Gwiriwch fod y porthiant yn gweithio"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed is an open-source project. Sources are hosted on <0>GitHub</0>." msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "Mae ComaFeed yn brosiect ffynhonnell agored. " msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed eitem nesaf heb ei darllen" msgstr "CommaFeed eitem nesaf heb ei darllen"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "Fersiwn ComaFeed {fersiwn} ({ adolygu})" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -308,6 +320,11 @@ msgstr "Ehangu"
msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services" msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services"
msgstr "Allforio eich tanysgrifiadau a'ch categorïau fel ffeil OPML y gellir ei mewnforio i wasanaethau darllen porthiant eraill" msgstr "Allforio eich tanysgrifiadau a'ch categorïau fel ffeil OPML y gellir ei mewnforio i wasanaethau darllen porthiant eraill"
#: src/components/header/Header.tsx
#: src/pages/WelcomePage.tsx
msgid "Extension options"
msgstr ""
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
msgid "Feed name" msgid "Feed name"
msgstr "Enw porthiant" msgstr "Enw porthiant"
@@ -513,6 +530,7 @@ msgid "Newest first"
msgstr "Y diweddaraf yn gyntaf" msgstr "Y diweddaraf yn gyntaf"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "Nesaf" msgstr "Nesaf"
@@ -540,6 +558,10 @@ msgstr "Hynaf yn gyntaf"
msgid "Oops!" msgid "Oops!"
msgstr "Wps!" msgstr "Wps!"
#: src/components/header/Header.tsx
msgid "Open CommaFeed"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Open current entry in a new tab" msgid "Open current entry in a new tab"
msgstr "Agorwch y cofnod cyfredol mewn tab newydd" msgstr "Agorwch y cofnod cyfredol mewn tab newydd"
@@ -618,6 +640,10 @@ msgstr "Nid yw cyfrineiriau yn cyfateb"
msgid "Position" msgid "Position"
msgstr "Swydd" msgstr "Swydd"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "Proffil" msgstr "Proffil"
@@ -759,10 +785,12 @@ msgid "Swipe header to the right"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to dark theme" msgid "Switch to dark theme"
msgstr "Newid i thema dywyll" msgstr "Newid i thema dywyll"
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to light theme" msgid "Switch to light theme"
msgstr "Newid i thema golau" msgstr "Newid i thema golau"
@@ -782,6 +810,10 @@ msgstr "Thema"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "Toglo statws darllen y cofnod cyfredol" msgstr "Toglo statws darllen y cofnod cyfredol"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Rhowch gynnig ar CommaFeed gyda'r cyfrif demo: demo / demo" msgstr "Rhowch gynnig ar CommaFeed gyda'r cyfrif demo: demo / demo"

View File

@@ -17,6 +17,10 @@ msgstr ""
msgid "{0} (in {1})" msgid "{0} (in {1})"
msgstr "" msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "<0>Complete syntax is available </0><1>here</1>." msgid "<0>Complete syntax is available </0><1>here</1>."
msgstr "" msgstr ""
@@ -67,6 +71,10 @@ msgstr ""
msgid "All" msgid "All"
msgstr "Alle" 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 #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "Der er sendt en e-mail, hvis denne adresse var registreret. " msgstr "Der er sendt en e-mail, hvis denne adresse var registreret. "
@@ -123,9 +131,13 @@ msgstr "Tilbage"
msgid "Back to log in" msgid "Back to log in"
msgstr "Tilbage for at logge ind" msgstr "Tilbage for at logge ind"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "Browserudvidelser" msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -160,15 +172,15 @@ msgid "Check that the feed is working"
msgstr "Tjek, at foderet virker" msgstr "Tjek, at foderet virker"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed is an open-source project. Sources are hosted on <0>GitHub</0>." msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "CommaFeed er et open source-projekt. " msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed næste ulæste element" msgstr "CommaFeed næste ulæste element"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
@@ -308,6 +320,11 @@ msgstr "Udvidet"
msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services" msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services"
msgstr "Eksporter dine abonnementer og kategorier som en OPML-fil, der kan importeres i andre feed-læsningstjenester" msgstr "Eksporter dine abonnementer og kategorier som en OPML-fil, der kan importeres i andre feed-læsningstjenester"
#: src/components/header/Header.tsx
#: src/pages/WelcomePage.tsx
msgid "Extension options"
msgstr ""
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
msgid "Feed name" msgid "Feed name"
msgstr "Feednavn" msgstr "Feednavn"
@@ -513,6 +530,7 @@ msgid "Newest first"
msgstr "Nyeste først" msgstr "Nyeste først"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "Næste" msgstr "Næste"
@@ -540,6 +558,10 @@ msgstr "Ældst først"
msgid "Oops!" msgid "Oops!"
msgstr "Hovsa!" msgstr "Hovsa!"
#: src/components/header/Header.tsx
msgid "Open CommaFeed"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Open current entry in a new tab" msgid "Open current entry in a new tab"
msgstr "Åbn den aktuelle post i en ny fane" msgstr "Åbn den aktuelle post i en ny fane"
@@ -618,6 +640,10 @@ msgstr "Adgangskoder stemmer ikke overens"
msgid "Position" msgid "Position"
msgstr "" msgstr ""
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
@@ -759,10 +785,12 @@ msgid "Swipe header to the right"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to dark theme" msgid "Switch to dark theme"
msgstr "Skift til mørkt tema" msgstr "Skift til mørkt tema"
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to light theme" msgid "Switch to light theme"
msgstr "Skift til lystema" msgstr "Skift til lystema"
@@ -782,6 +810,10 @@ msgstr "Tema"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "Skift læsestatus for den aktuelle post" msgstr "Skift læsestatus for den aktuelle post"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Prøv CommaFeed med demokontoen: demo/demo" msgstr "Prøv CommaFeed med demokontoen: demo/demo"

View File

@@ -17,6 +17,10 @@ msgstr ""
msgid "{0} (in {1})" msgid "{0} (in {1})"
msgstr "" msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "<0>Complete syntax is available </0><1>here</1>." msgid "<0>Complete syntax is available </0><1>here</1>."
msgstr "" msgstr ""
@@ -67,6 +71,10 @@ msgstr "Verwaltung"
msgid "All" msgid "All"
msgstr "Alle" 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 #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "Eine E-Mail wurde gesendet, wenn diese Adresse registriert wurde. " msgstr "Eine E-Mail wurde gesendet, wenn diese Adresse registriert wurde. "
@@ -123,9 +131,13 @@ msgstr "Zurück"
msgid "Back to log in" msgid "Back to log in"
msgstr "Zurück zum Anmelden" msgstr "Zurück zum Anmelden"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "Browsererweiterungen" msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -160,16 +172,16 @@ msgid "Check that the feed is working"
msgstr "Überprüfen Sie, ob der Feed funktioniert" msgstr "Überprüfen Sie, ob der Feed funktioniert"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed is an open-source project. Sources are hosted on <0>GitHub</0>." msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "CommaFeed ist ein Open-Source-Projekt. " msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed nächstes ungelesenes Element" msgstr "CommaFeed nächstes ungelesenes Element"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "CommaFeed-Version {Version} ({Revision})" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -308,6 +320,11 @@ msgstr "Erweitert"
msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services" msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services"
msgstr "Exportieren Sie Ihre Abonnements und Kategorien als OPML-Datei, die in andere Feed-Lesedienste importiert werden kann" msgstr "Exportieren Sie Ihre Abonnements und Kategorien als OPML-Datei, die in andere Feed-Lesedienste importiert werden kann"
#: src/components/header/Header.tsx
#: src/pages/WelcomePage.tsx
msgid "Extension options"
msgstr ""
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
msgid "Feed name" msgid "Feed name"
msgstr "Feedname" msgstr "Feedname"
@@ -513,6 +530,7 @@ msgid "Newest first"
msgstr "Neueste zuerst" msgstr "Neueste zuerst"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "Weiter" msgstr "Weiter"
@@ -540,6 +558,10 @@ msgstr "Älteste zuerst"
msgid "Oops!" msgid "Oops!"
msgstr "Ups!" msgstr "Ups!"
#: src/components/header/Header.tsx
msgid "Open CommaFeed"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Open current entry in a new tab" msgid "Open current entry in a new tab"
msgstr "Aktuellen Eintrag in neuem Tab öffnen" msgstr "Aktuellen Eintrag in neuem Tab öffnen"
@@ -618,6 +640,10 @@ msgstr "Passwörter stimmen nicht überein"
msgid "Position" msgid "Position"
msgstr "Stellung" msgstr "Stellung"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
@@ -759,10 +785,12 @@ msgid "Swipe header to the right"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to dark theme" msgid "Switch to dark theme"
msgstr "Zum dunklen Design wechseln" msgstr "Zum dunklen Design wechseln"
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to light theme" msgid "Switch to light theme"
msgstr "Wechseln Sie zum Lichtdesign" msgstr "Wechseln Sie zum Lichtdesign"
@@ -782,6 +810,10 @@ msgstr "Thema"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "Lesestatus des aktuellen Eintrags umschalten" msgstr "Lesestatus des aktuellen Eintrags umschalten"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Testen Sie CommaFeed mit dem Demokonto: demo/demo" msgstr "Testen Sie CommaFeed mit dem Demokonto: demo/demo"

View File

@@ -17,6 +17,10 @@ msgstr ""
msgid "{0} (in {1})" msgid "{0} (in {1})"
msgstr "{0} (in {1})" msgstr "{0} (in {1})"
#: src/pages/app/AboutPage.tsx
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
msgstr "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "<0>Complete syntax is available </0><1>here</1>." msgid "<0>Complete syntax is available </0><1>here</1>."
msgstr "<0>Complete syntax is available </0><1>here</1>." msgstr "<0>Complete syntax is available </0><1>here</1>."
@@ -67,6 +71,10 @@ msgstr "Admin"
msgid "All" msgid "All"
msgstr "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 #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "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."
@@ -123,9 +131,13 @@ msgstr "Back"
msgid "Back to log in" msgid "Back to log in"
msgstr "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 #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "Browser extentions" msgstr "Browser extention"
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -160,16 +172,16 @@ msgid "Check that the feed is working"
msgstr "Check that the feed is working" msgstr "Check that the feed is working"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed is an open-source project. Sources are hosted on <0>GitHub</0>." msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "CommaFeed is an open-source project. Sources are hosted on <0>GitHub</0>." msgstr "CommaFeed browser extension version {browserExtensionVersion}."
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed next unread item" msgstr "CommaFeed next unread item"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "CommaFeed version {version} ({revision})" msgstr "CommaFeed version {version} ({revision})."
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -308,6 +320,11 @@ msgstr "Expanded"
msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services" msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services"
msgstr "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services" msgstr "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services"
#: src/components/header/Header.tsx
#: src/pages/WelcomePage.tsx
msgid "Extension options"
msgstr "Extension options"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
msgid "Feed name" msgid "Feed name"
msgstr "Feed name" msgstr "Feed name"
@@ -513,6 +530,7 @@ msgid "Newest first"
msgstr "Newest first" msgstr "Newest first"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "Next" msgstr "Next"
@@ -540,6 +558,10 @@ msgstr "Oldest first"
msgid "Oops!" msgid "Oops!"
msgstr "Oops!" msgstr "Oops!"
#: src/components/header/Header.tsx
msgid "Open CommaFeed"
msgstr "Open CommaFeed"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Open current entry in a new tab" msgid "Open current entry in a new tab"
msgstr "Open current entry in a new tab" msgstr "Open current entry in a new tab"
@@ -618,6 +640,10 @@ msgstr "Passwords do not match"
msgid "Position" msgid "Position"
msgstr "Position" msgstr "Position"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr "Previous"
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "Profile" msgstr "Profile"
@@ -759,10 +785,12 @@ msgid "Swipe header to the right"
msgstr "Swipe header to the right" msgstr "Swipe header to the right"
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to dark theme" msgid "Switch to dark theme"
msgstr "Switch to dark theme" msgstr "Switch to dark theme"
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to light theme" msgid "Switch to light theme"
msgstr "Switch to light theme" msgstr "Switch to light theme"
@@ -782,6 +810,10 @@ msgstr "Theme"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "Toggle read status of current entry" msgstr "Toggle read status of current entry"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr "Toggle sidebar"
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Try out CommaFeed with the demo account: demo/demo" msgstr "Try out CommaFeed with the demo account: demo/demo"

View File

@@ -17,6 +17,10 @@ msgstr ""
msgid "{0} (in {1})" msgid "{0} (in {1})"
msgstr "" msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "<0>Complete syntax is available </0><1>here</1>." msgid "<0>Complete syntax is available </0><1>here</1>."
msgstr "" msgstr ""
@@ -67,6 +71,10 @@ msgstr "Administrador"
msgid "All" msgid "All"
msgstr "Todo" 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 #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "Se ha enviado un correo electrónico si se registró esta dirección. " msgstr "Se ha enviado un correo electrónico si se registró esta dirección. "
@@ -123,9 +131,13 @@ msgstr "Atrás"
msgid "Back to log in" msgid "Back to log in"
msgstr "Volver a iniciar sesión" msgstr "Volver a iniciar sesión"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "Extensiones del navegador" msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -160,16 +172,16 @@ msgid "Check that the feed is working"
msgstr "Compruebe que el feed funciona" msgstr "Compruebe que el feed funciona"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed is an open-source project. Sources are hosted on <0>GitHub</0>." msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "CommaFeed es un proyecto de código abierto. " msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed siguiente elemento no leído" msgstr "CommaFeed siguiente elemento no leído"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "versión de CommaFeed {versión} ({revisión})" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -308,6 +320,11 @@ msgstr "Expandido"
msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services" msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services"
msgstr "Exporte sus suscripciones y categorías como un archivo OPML que se puede importar en otros servicios de lectura de feeds" msgstr "Exporte sus suscripciones y categorías como un archivo OPML que se puede importar en otros servicios de lectura de feeds"
#: src/components/header/Header.tsx
#: src/pages/WelcomePage.tsx
msgid "Extension options"
msgstr ""
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
msgid "Feed name" msgid "Feed name"
msgstr "Nombre de alimentación" msgstr "Nombre de alimentación"
@@ -513,6 +530,7 @@ msgid "Newest first"
msgstr "más reciente primero" msgstr "más reciente primero"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "Siguiente" msgstr "Siguiente"
@@ -540,6 +558,10 @@ msgstr "más antigua primero"
msgid "Oops!" msgid "Oops!"
msgstr "¡Ups!" msgstr "¡Ups!"
#: src/components/header/Header.tsx
msgid "Open CommaFeed"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Open current entry in a new tab" msgid "Open current entry in a new tab"
msgstr "Abrir la entrada actual en una nueva pestaña" msgstr "Abrir la entrada actual en una nueva pestaña"
@@ -618,6 +640,10 @@ msgstr "Las contraseñas no coinciden"
msgid "Position" msgid "Position"
msgstr "Posición" msgstr "Posición"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "Perfil" msgstr "Perfil"
@@ -759,10 +785,12 @@ msgid "Swipe header to the right"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to dark theme" msgid "Switch to dark theme"
msgstr "Cambiar a tema oscuro" msgstr "Cambiar a tema oscuro"
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to light theme" msgid "Switch to light theme"
msgstr "Cambiar a tema claro" msgstr "Cambiar a tema claro"
@@ -782,6 +810,10 @@ msgstr "Tema"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "Alternar estado de lectura de la entrada actual" msgstr "Alternar estado de lectura de la entrada actual"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Pruebe CommaFeed con la cuenta demo: demo/demo" msgstr "Pruebe CommaFeed con la cuenta demo: demo/demo"

View File

@@ -17,6 +17,10 @@ msgstr ""
msgid "{0} (in {1})" msgid "{0} (in {1})"
msgstr "" msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "<0>Complete syntax is available </0><1>here</1>." msgid "<0>Complete syntax is available </0><1>here</1>."
msgstr "" msgstr ""
@@ -67,6 +71,10 @@ msgstr "مدیر"
msgid "All" msgid "All"
msgstr "همه" 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 #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "اگر این آدرس ثبت شده باشد ایمیل ارسال شده است. " msgstr "اگر این آدرس ثبت شده باشد ایمیل ارسال شده است. "
@@ -123,9 +131,13 @@ msgstr "برگشت"
msgid "Back to log in" msgid "Back to log in"
msgstr "بازگشت برای ورود به سیستم" msgstr "بازگشت برای ورود به سیستم"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "گسترش مرورگر" msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -160,16 +172,16 @@ msgid "Check that the feed is working"
msgstr "بررسی کنید که خوراک کار می کند" msgstr "بررسی کنید که خوراک کار می کند"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed is an open-source project. Sources are hosted on <0>GitHub</0>." msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "CommaFeed یک پروژه منبع باز است. " msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "مورد خوانده نشده بعدی CommaFeed" msgstr "مورد خوانده نشده بعدی CommaFeed"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "نسخه {نسخه} CommaFeed ({نسخه})" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -308,6 +320,11 @@ msgstr "گسترش یافت"
msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services" msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services"
msgstr "اشتراک ها و دسته های خود را به عنوان یک فایل OPML صادر کنید که می تواند در سایر خدمات خواندن فید وارد شود" msgstr "اشتراک ها و دسته های خود را به عنوان یک فایل OPML صادر کنید که می تواند در سایر خدمات خواندن فید وارد شود"
#: src/components/header/Header.tsx
#: src/pages/WelcomePage.tsx
msgid "Extension options"
msgstr ""
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
msgid "Feed name" msgid "Feed name"
msgstr "نام فید" msgstr "نام فید"
@@ -513,6 +530,7 @@ msgid "Newest first"
msgstr "ابتدا جدیدترین" msgstr "ابتدا جدیدترین"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "بعد" msgstr "بعد"
@@ -540,6 +558,10 @@ msgstr "قدیمی ترین اول"
msgid "Oops!" msgid "Oops!"
msgstr "اوه!" msgstr "اوه!"
#: src/components/header/Header.tsx
msgid "Open CommaFeed"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Open current entry in a new tab" msgid "Open current entry in a new tab"
msgstr "ورودی فعلی را در یک برگه جدید باز کنید" msgstr "ورودی فعلی را در یک برگه جدید باز کنید"
@@ -618,6 +640,10 @@ msgstr "گذرواژه ها مطابقت ندارند"
msgid "Position" msgid "Position"
msgstr "موقعیت" msgstr "موقعیت"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "نمایه" msgstr "نمایه"
@@ -759,10 +785,12 @@ msgid "Swipe header to the right"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to dark theme" msgid "Switch to dark theme"
msgstr "تغییر به تم تیره" msgstr "تغییر به تم تیره"
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to light theme" msgid "Switch to light theme"
msgstr "روی زمینه روشن" msgstr "روی زمینه روشن"
@@ -782,6 +810,10 @@ msgstr "تم"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "وضعیت خواندن ورودی فعلی را تغییر دهید" msgstr "وضعیت خواندن ورودی فعلی را تغییر دهید"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "CommaFeed را با حساب آزمایشی امتحان کنید: دمو/دمو" msgstr "CommaFeed را با حساب آزمایشی امتحان کنید: دمو/دمو"

View File

@@ -17,6 +17,10 @@ msgstr ""
msgid "{0} (in {1})" msgid "{0} (in {1})"
msgstr "" msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "<0>Complete syntax is available </0><1>here</1>." msgid "<0>Complete syntax is available </0><1>here</1>."
msgstr "" msgstr ""
@@ -67,6 +71,10 @@ msgstr "Järjestelmänvalvoja"
msgid "All" msgid "All"
msgstr "Kaikki" 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 #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "Sähköposti on lähetetty, jos tämä osoite on rekisteröity. " msgstr "Sähköposti on lähetetty, jos tämä osoite on rekisteröity. "
@@ -123,9 +131,13 @@ msgstr "Takaisin"
msgid "Back to log in" msgid "Back to log in"
msgstr "Takaisin sisäänkirjautumiseen" msgstr "Takaisin sisäänkirjautumiseen"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "Selaimen laajennukset" msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -160,16 +172,16 @@ msgid "Check that the feed is working"
msgstr "Tarkista, että syöttö toimii" msgstr "Tarkista, että syöttö toimii"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed is an open-source project. Sources are hosted on <0>GitHub</0>." msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "CommaFeed on avoimen lähdekoodin projekti. " msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed seuraava lukematon kohde" msgstr "CommaFeed seuraava lukematon kohde"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "CommaFeed-versio {version} ({versio})" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -308,6 +320,11 @@ msgstr "Laajennettu"
msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services" msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services"
msgstr "Vie tilauksesi ja luokat OPML-tiedostona, joka voidaan tuoda muihin syötteiden lukupalveluihin" msgstr "Vie tilauksesi ja luokat OPML-tiedostona, joka voidaan tuoda muihin syötteiden lukupalveluihin"
#: src/components/header/Header.tsx
#: src/pages/WelcomePage.tsx
msgid "Extension options"
msgstr ""
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
msgid "Feed name" msgid "Feed name"
msgstr "Syötteen nimi" msgstr "Syötteen nimi"
@@ -513,6 +530,7 @@ msgid "Newest first"
msgstr "Uusin ensin" msgstr "Uusin ensin"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "Seuraava" msgstr "Seuraava"
@@ -540,6 +558,10 @@ msgstr "Vanhin ensin"
msgid "Oops!" msgid "Oops!"
msgstr "Hups!" msgstr "Hups!"
#: src/components/header/Header.tsx
msgid "Open CommaFeed"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Open current entry in a new tab" msgid "Open current entry in a new tab"
msgstr "Avaa nykyinen merkintä uudessa välilehdessä" msgstr "Avaa nykyinen merkintä uudessa välilehdessä"
@@ -618,6 +640,10 @@ msgstr "Salasanat eivät täsmää"
msgid "Position" msgid "Position"
msgstr "Sijainti" msgstr "Sijainti"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "Profiili" msgstr "Profiili"
@@ -759,10 +785,12 @@ msgid "Swipe header to the right"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to dark theme" msgid "Switch to dark theme"
msgstr "Vaihda tummaan teemaan" msgstr "Vaihda tummaan teemaan"
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to light theme" msgid "Switch to light theme"
msgstr "Vaihda vaaleaan teemaan" msgstr "Vaihda vaaleaan teemaan"
@@ -782,6 +810,10 @@ msgstr "Teema"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "Vaihda nykyisen merkinnän lukutila" msgstr "Vaihda nykyisen merkinnän lukutila"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Kokeile CommaFeediä demotilillä: demo/demo" msgstr "Kokeile CommaFeediä demotilillä: demo/demo"

View File

@@ -15,7 +15,11 @@ msgstr ""
#: src/components/content/add/CategorySelect.tsx #: src/components/content/add/CategorySelect.tsx
msgid "{0} (in {1})" msgid "{0} (in {1})"
msgstr "" msgstr "{0} (sur {1})"
#: src/pages/app/AboutPage.tsx
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
msgstr "<0>CommaFeed est un projet open-source. Les sources sont hébergées sur </0><1>GitHub</1>."
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "<0>Complete syntax is available </0><1>here</1>." msgid "<0>Complete syntax is available </0><1>here</1>."
@@ -27,7 +31,7 @@ msgstr "<0>Déjà un compte ?</0><1>Connectez-vous !</1>"
#: src/pages/app/DonatePage.tsx #: 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>" 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>Salut,</0><1>Je m'appelle Jérémie, je suis belge, et je développe CommaFeed sur mon temps libre depuis maintenant 10 ans. Merci de m'aider à continuer de maintenir CommaFeed.</1>"
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "<0>Need an account?</0><1>Sign up!</1>" msgid "<0>Need an account?</0><1>Sign up!</1>"
@@ -36,7 +40,7 @@ msgstr "<0>Besoin d'un compte ?</0><1>Enregistrez-vous !</1>"
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "About" msgid "About"
msgstr "A propos" msgstr "À propos"
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
msgid "Actions" msgid "Actions"
@@ -67,6 +71,10 @@ msgstr "Administrateur"
msgid "All" msgid "All"
msgstr "Tout" 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 #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "Un e-mail a été envoyé si cette adresse est enregistrée. Vérifiez votre boîte de réception." msgstr "Un e-mail a été envoyé si cette adresse est enregistrée. Vérifiez votre boîte de réception."
@@ -85,11 +93,11 @@ msgstr "Clé API"
#: src/pages/app/CategoryDetailsPage.tsx #: src/pages/app/CategoryDetailsPage.tsx
msgid "Are you sure you want to delete category <0>{categoryName}</0>?" msgid "Are you sure you want to delete category <0>{categoryName}</0>?"
msgstr "Etes-vous sûr de vouloir supprimer la catégorie <0>{categoryName}</0>?" msgstr "Êtes-vous sûr de vouloir supprimer la catégorie <0>{categoryName}</0> ?"
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
msgid "Are you sure you want to delete user <0>{userName}</0> ?" msgid "Are you sure you want to delete user <0>{userName}</0> ?"
msgstr "Etes-vous sûr de vouloir supprimer l'utilisateur <0>{userName}</0> ?" msgstr "Êtes-vous sûr de vouloir supprimer l'utilisateur <0>{userName}</0> ?"
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "Are you sure you want to delete your account? There's no turning back!" msgid "Are you sure you want to delete your account? There's no turning back!"
@@ -97,15 +105,15 @@ msgstr "Êtes-vous sûr de vouloir supprimer définitivement votre compte ?"
#: src/components/header/MarkAllAsReadButton.tsx #: src/components/header/MarkAllAsReadButton.tsx
msgid "Are you sure you want to mark all entries of <0>{sourceLabel}</0> as read?" msgid "Are you sure you want to mark all entries of <0>{sourceLabel}</0> as read?"
msgstr "Etes-vous sûr de vouloir marquer toutes les entrées de <0>{sourceLabel}</0> comme lues?" msgstr "Êtes-vous sûr de vouloir marquer toutes les entrées de <0>{sourceLabel}</0> comme lues ?"
#: src/components/header/MarkAllAsReadButton.tsx #: src/components/header/MarkAllAsReadButton.tsx
msgid "Are you sure you want to mark entries older than {threshold} days of <0>{sourceLabel}</0> as read?" msgid "Are you sure you want to mark entries older than {threshold} days of <0>{sourceLabel}</0> as read?"
msgstr "Etes-vous sûr de vouloir marquer les entrées de <0>{sourceLabel}</0> plus anciennes que {threshold} jours comme lues?" msgstr "Êtes-vous sûr de vouloir marquer les entrées de <0>{sourceLabel}</0> plus anciennes que {threshold} jours comme lues ?"
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "Are you sure you want to unsubscribe from <0>{feedName}</0>?" msgid "Are you sure you want to unsubscribe from <0>{feedName}</0>?"
msgstr "Etes-vous sûr de vouloir vous désabonner de <0>{feedName}</0>?" msgstr "Êtes-vous sûr de vouloir vous désabonner de <0>{feedName}</0> ?"
#: src/components/header/Header.tsx #: src/components/header/Header.tsx
msgid "Asc" msgid "Asc"
@@ -123,9 +131,13 @@ msgstr "Retour"
msgid "Back to log in" msgid "Back to log in"
msgstr "Retour à la connexion" 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 #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "Extensions pour navigateurs" msgstr "Extension navigateur"
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -160,16 +172,16 @@ msgid "Check that the feed is working"
msgstr "Vérifie que le flux fonctionne" msgstr "Vérifie que le flux fonctionne"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed is an open-source project. Sources are hosted on <0>GitHub</0>." msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "CommaFeed est un projet open-source. Les sources sont hébergées sur <0>GitHub</0>." msgstr "Extension CommaFeed pour navigateur version {browserExtensionVersion}."
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed prochain article non lu" msgstr "CommaFeed prochain article non lu"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "CommaFeed version {version} ({revision})" msgstr "CommaFeed version {version} ({revision})."
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -193,7 +205,7 @@ msgstr "Cozy"
#: src/components/content/FeedEntryFooter.tsx #: src/components/content/FeedEntryFooter.tsx
msgid "Create tag: {query}" msgid "Create tag: {query}"
msgstr "Créer le tag: {query}" msgstr "Créer le marqueur : {query}"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Ctrl" msgid "Ctrl"
@@ -205,15 +217,15 @@ msgstr "Mot de passe actuel"
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Custom code" msgid "Custom code"
msgstr "" msgstr "Code personnalisé"
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
msgid "Custom CSS rules that will be applied" msgid "Custom CSS rules that will be applied"
msgstr "" msgstr "Code CSS personnalisé qui sera appliqué"
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
msgid "Custom JS code that will be executed on page load" msgid "Custom JS code that will be executed on page load"
msgstr "" msgstr "Code JS personnalisé qui sera appliqué au chargement des pages"
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
msgid "Date created" msgid "Date created"
@@ -252,7 +264,7 @@ msgstr "Affichage"
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/app/DonatePage.tsx #: src/pages/app/DonatePage.tsx
msgid "Donate" msgid "Donate"
msgstr "" msgstr "Faites un don"
#: src/components/settings/ProfileSettings.tsx #: src/components/settings/ProfileSettings.tsx
msgid "Download" msgid "Download"
@@ -308,6 +320,11 @@ msgstr "Vue étendue"
msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services" msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services"
msgstr "Exporter vos abonnements et catégories en tant que fichier OPML qui peut être importé dans d'autres services de lecture de flux" msgstr "Exporter vos abonnements et catégories en tant que fichier OPML qui peut être importé dans d'autres services de lecture de flux"
#: src/components/header/Header.tsx
#: src/pages/WelcomePage.tsx
msgid "Extension options"
msgstr "Options de l'extension"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
msgid "Feed name" msgid "Feed name"
msgstr "Nom du flux" msgstr "Nom du flux"
@@ -320,7 +337,7 @@ msgstr "URL du flux"
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Fetch all my feeds now" msgid "Fetch all my feeds now"
msgstr "" msgstr "Rafraîchir tous mes flux"
#: src/components/content/add/ImportOpml.tsx #: src/components/content/add/ImportOpml.tsx
msgid "file is required" msgid "file is required"
@@ -352,7 +369,7 @@ msgstr "URL du flux généré"
#: src/components/content/FeedEntryContextMenu.tsx #: src/components/content/FeedEntryContextMenu.tsx
msgid "Go to {0}" msgid "Go to {0}"
msgstr "" msgstr "Aller à {0}"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Go to the All view" msgid "Go to the All view"
@@ -420,19 +437,19 @@ msgstr "Lien"
#: src/hooks/useAppLoading.ts #: src/hooks/useAppLoading.ts
msgid "Loading profile..." msgid "Loading profile..."
msgstr "Chargement du profil ..." msgstr "Chargement du profil..."
#: src/hooks/useAppLoading.ts #: src/hooks/useAppLoading.ts
msgid "Loading settings..." msgid "Loading settings..."
msgstr "Chargement des paramètres ..." msgstr "Chargement des paramètres..."
#: src/hooks/useAppLoading.ts #: src/hooks/useAppLoading.ts
msgid "Loading subscriptions..." msgid "Loading subscriptions..."
msgstr "Chargement des abonnements ..." msgstr "Chargement des abonnements..."
#: src/hooks/useAppLoading.ts #: src/hooks/useAppLoading.ts
msgid "Loading tags..." msgid "Loading tags..."
msgstr "Chargement des tags ..." msgstr "Chargement des marqueurs..."
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
@@ -446,7 +463,7 @@ msgstr "Déconnexion"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Long press" msgid "Long press"
msgstr "" msgstr "Appui long"
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/admin/AdminUsersPage.tsx #: src/pages/admin/AdminUsersPage.tsx
@@ -478,7 +495,7 @@ msgstr "Métriques"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Middle click" msgid "Middle click"
msgstr "" msgstr "Clic milieu"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Move the page down" msgid "Move the page down"
@@ -513,6 +530,7 @@ msgid "Newest first"
msgstr "Plus récent en premier" msgstr "Plus récent en premier"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "Suivant" msgstr "Suivant"
@@ -526,7 +544,7 @@ msgstr "Bookmarklet vers le prochain article non lu"
#: src/pages/app/FeedEntriesPage.tsx #: src/pages/app/FeedEntriesPage.tsx
msgid "No more entries" msgid "No more entries"
msgstr "Plus d'entrées" msgstr "Fin de la liste"
#: src/components/sidebar/TreeSearch.tsx #: src/components/sidebar/TreeSearch.tsx
msgid "Nothing found" msgid "Nothing found"
@@ -540,6 +558,10 @@ msgstr "Du plus ancien au plus récent"
msgid "Oops!" msgid "Oops!"
msgstr "Oups !" msgstr "Oups !"
#: src/components/header/Header.tsx
msgid "Open CommaFeed"
msgstr "Ouvrir CommaFeed"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Open current entry in a new tab" msgid "Open current entry in a new tab"
msgstr "Ouvrir l'entrée actuelle dans un nouvel onglet" msgstr "Ouvrir l'entrée actuelle dans un nouvel onglet"
@@ -554,11 +576,11 @@ msgstr "Ouvrir le lien"
#: src/components/content/FeedEntryContextMenu.tsx #: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new background tab" msgid "Open link in new background tab"
msgstr "" msgstr "Ouvrir le lien dans un nouvel onglet en arrière-plan"
#: src/components/content/FeedEntryContextMenu.tsx #: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new tab" msgid "Open link in new tab"
msgstr "" msgstr "Ouvrir le lien dans un nouvel onglet"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry" msgid "Open next entry"
@@ -618,6 +640,10 @@ msgstr "Les mots de passe ne correspondent pas"
msgid "Position" msgid "Position"
msgstr "Position" msgstr "Position"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr "Précédent"
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
@@ -641,7 +667,7 @@ msgstr "API REST"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click" msgid "Right click"
msgstr "" msgstr "Clic droit"
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx #: src/components/settings/CustomCodeSettings.tsx
@@ -697,11 +723,11 @@ msgstr "Maj"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)" msgid "Show entry menu (desktop)"
msgstr "" msgstr "Afficher les options de l'entrée (ordinateur)"
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (mobile)" msgid "Show entry menu (mobile)"
msgstr "" msgstr "Afficher les options de l'entrée (mobile)"
#: src/components/settings/DisplaySettings.tsx #: src/components/settings/DisplaySettings.tsx
msgid "Show feeds and categories with no unread entries" msgid "Show feeds and categories with no unread entries"
@@ -759,16 +785,18 @@ msgid "Swipe header to the right"
msgstr "Faire glisser le titre vers la droite" msgstr "Faire glisser le titre vers la droite"
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to dark theme" msgid "Switch to dark theme"
msgstr "Activer le mode sombre" msgstr "Activer le mode sombre"
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to light theme" msgid "Switch to light theme"
msgstr "Activer le mode clair" msgstr "Activer le mode clair"
#: src/components/content/FeedEntryFooter.tsx #: src/components/content/FeedEntryFooter.tsx
msgid "Tags" msgid "Tags"
msgstr "Tags" msgstr "Marqueurs"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
msgid "The URL for the feed you want to subscribe to. You can also use the website's url directly and CommaFeed will try to find the feed in the page." msgid "The URL for the feed you want to subscribe to. You can also use the website's url directly and CommaFeed will try to find the feed in the page."
@@ -782,13 +810,17 @@ msgstr "Thème"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "Marquer l'entrée actuelle comme lue/non lue" 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/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Essayez CommaFeed avec le compte de démonstration : demo/demo" msgstr "Essayez CommaFeed avec le compte de démonstration : demo/demo"
#: src/pages/WelcomePage.tsx #: src/pages/WelcomePage.tsx
msgid "Try the demo!" msgid "Try the demo!"
msgstr "" msgstr "Essayez la version de démonstration !"
#: src/components/header/Header.tsx #: src/components/header/Header.tsx
msgid "Unread" msgid "Unread"
@@ -827,4 +859,4 @@ msgstr "Vous n'avez pas encore d'abonnements. Pourquoi ne pas essayer d'en ajout
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Your feeds have been queued for refresh." msgid "Your feeds have been queued for refresh."
msgstr "" msgstr "Vos flux sont en cours de rafraîchissement"

View File

@@ -17,6 +17,10 @@ msgstr ""
msgid "{0} (in {1})" msgid "{0} (in {1})"
msgstr "" msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "<0>Complete syntax is available </0><1>here</1>." msgid "<0>Complete syntax is available </0><1>here</1>."
msgstr "" msgstr ""
@@ -67,6 +71,10 @@ msgstr "Administración"
msgid "All" msgid "All"
msgstr "Todos" 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 #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "Enviouse un correo electrónico se este enderezo estaba rexistrado. " msgstr "Enviouse un correo electrónico se este enderezo estaba rexistrado. "
@@ -123,9 +131,13 @@ msgstr "Atrás"
msgid "Back to log in" msgid "Back to log in"
msgstr "Volver para iniciar sesión" msgstr "Volver para iniciar sesión"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "Extensións do navegador" msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -160,16 +172,16 @@ msgid "Check that the feed is working"
msgstr "Comproba que a fonte funciona" msgstr "Comproba que a fonte funciona"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed is an open-source project. Sources are hosted on <0>GitHub</0>." msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "CommaFeed é un proxecto de código aberto. " msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed seguinte elemento non lido" msgstr "CommaFeed seguinte elemento non lido"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "Versión de CommaFeed {versión} ({revisión})" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -308,6 +320,11 @@ msgstr "Ampliado"
msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services" msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services"
msgstr "Exporta as túas subscricións e categorías como ficheiro OPML que se pode importar noutros servizos de lectura de feeds" msgstr "Exporta as túas subscricións e categorías como ficheiro OPML que se pode importar noutros servizos de lectura de feeds"
#: src/components/header/Header.tsx
#: src/pages/WelcomePage.tsx
msgid "Extension options"
msgstr ""
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
msgid "Feed name" msgid "Feed name"
msgstr "Nome do feed" msgstr "Nome do feed"
@@ -513,6 +530,7 @@ msgid "Newest first"
msgstr "o máis novo primeiro" msgstr "o máis novo primeiro"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "Seguinte" msgstr "Seguinte"
@@ -540,6 +558,10 @@ msgstr "O máis vello primeiro"
msgid "Oops!" msgid "Oops!"
msgstr "Vaia!" msgstr "Vaia!"
#: src/components/header/Header.tsx
msgid "Open CommaFeed"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Open current entry in a new tab" msgid "Open current entry in a new tab"
msgstr "Abrir a entrada actual nunha nova pestana" msgstr "Abrir a entrada actual nunha nova pestana"
@@ -618,6 +640,10 @@ msgstr "Os contrasinais non coinciden"
msgid "Position" msgid "Position"
msgstr "Posición" msgstr "Posición"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "Perfil" msgstr "Perfil"
@@ -759,10 +785,12 @@ msgid "Swipe header to the right"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to dark theme" msgid "Switch to dark theme"
msgstr "Cambiar ao tema escuro" msgstr "Cambiar ao tema escuro"
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to light theme" msgid "Switch to light theme"
msgstr "Cambiar ao tema claro" msgstr "Cambiar ao tema claro"
@@ -782,6 +810,10 @@ msgstr "Tema"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "alternar o estado de lectura da entrada actual" msgstr "alternar o estado de lectura da entrada actual"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Proba CommaFeed coa conta de demostración: demo/demo" msgstr "Proba CommaFeed coa conta de demostración: demo/demo"

View File

@@ -17,6 +17,10 @@ msgstr ""
msgid "{0} (in {1})" msgid "{0} (in {1})"
msgstr "" msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "<0>Complete syntax is available </0><1>here</1>." msgid "<0>Complete syntax is available </0><1>here</1>."
msgstr "" msgstr ""
@@ -67,6 +71,10 @@ msgstr ""
msgid "All" msgid "All"
msgstr "Mind" 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 #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "E-mailt küldtünk, ha ez a cím regisztrálva volt. " msgstr "E-mailt küldtünk, ha ez a cím regisztrálva volt. "
@@ -123,9 +131,13 @@ msgstr "Vissza"
msgid "Back to log in" msgid "Back to log in"
msgstr "Vissza a bejelentkezéshez" msgstr "Vissza a bejelentkezéshez"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "Böngészőbővítések" msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -160,7 +172,7 @@ msgid "Check that the feed is working"
msgstr "Ellenőrizze, hogy a feed működik-e" msgstr "Ellenőrizze, hogy a feed működik-e"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed is an open-source project. Sources are hosted on <0>GitHub</0>." msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "" msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
@@ -168,8 +180,8 @@ msgid "CommaFeed next unread item"
msgstr "CommaFeed következő olvasatlan elem" msgstr "CommaFeed következő olvasatlan elem"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "CommaFeed verzió {version} ({revision})" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -308,6 +320,11 @@ msgstr "Kiterjesztve"
msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services" msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services"
msgstr "Exportálja előfizetéseit és kategóriáit OPML-fájlként, amely importálható más feedolvasó szolgáltatásokba" msgstr "Exportálja előfizetéseit és kategóriáit OPML-fájlként, amely importálható más feedolvasó szolgáltatásokba"
#: src/components/header/Header.tsx
#: src/pages/WelcomePage.tsx
msgid "Extension options"
msgstr ""
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
msgid "Feed name" msgid "Feed name"
msgstr "Hírcsatorna neve" msgstr "Hírcsatorna neve"
@@ -513,6 +530,7 @@ msgid "Newest first"
msgstr "A legújabbak először" msgstr "A legújabbak először"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "Következő" msgstr "Következő"
@@ -540,6 +558,10 @@ msgstr "A legidősebb első"
msgid "Oops!" msgid "Oops!"
msgstr "Hoppá!" msgstr "Hoppá!"
#: src/components/header/Header.tsx
msgid "Open CommaFeed"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Open current entry in a new tab" msgid "Open current entry in a new tab"
msgstr "Az aktuális bejegyzés megnyitása új lapon" msgstr "Az aktuális bejegyzés megnyitása új lapon"
@@ -618,6 +640,10 @@ msgstr "A jelszavak nem egyeznek"
msgid "Position" msgid "Position"
msgstr "Pozíció" msgstr "Pozíció"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
@@ -759,10 +785,12 @@ msgid "Swipe header to the right"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to dark theme" msgid "Switch to dark theme"
msgstr "Váltás sötét témára" msgstr "Váltás sötét témára"
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to light theme" msgid "Switch to light theme"
msgstr "Váltás világos témára" msgstr "Váltás világos témára"
@@ -782,6 +810,10 @@ msgstr "Téma"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "Az aktuális bejegyzés olvasási állapotának váltása" msgstr "Az aktuális bejegyzés olvasási állapotának váltása"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Próbálja ki a CommaFeed-et a demo fiókkal: demo/demo" msgstr "Próbálja ki a CommaFeed-et a demo fiókkal: demo/demo"

View File

@@ -17,6 +17,10 @@ msgstr ""
msgid "{0} (in {1})" msgid "{0} (in {1})"
msgstr "" msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "<0>Complete syntax is available </0><1>here</1>." msgid "<0>Complete syntax is available </0><1>here</1>."
msgstr "" msgstr ""
@@ -67,6 +71,10 @@ msgstr ""
msgid "All" msgid "All"
msgstr "Semua" 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 #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "Email telah dikirim jika alamat ini terdaftar. " msgstr "Email telah dikirim jika alamat ini terdaftar. "
@@ -123,9 +131,13 @@ msgstr "Kembali"
msgid "Back to log in" msgid "Back to log in"
msgstr "Kembali untuk masuk" msgstr "Kembali untuk masuk"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "Ekstensi peramban" msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -160,16 +172,16 @@ msgid "Check that the feed is working"
msgstr "Periksa apakah umpannya berfungsi" msgstr "Periksa apakah umpannya berfungsi"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed is an open-source project. Sources are hosted on <0>GitHub</0>." msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "CommaFeed adalah proyek sumber terbuka. " msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed item yang belum dibaca berikutnya" msgstr "CommaFeed item yang belum dibaca berikutnya"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "CommaFeed versi {versi} ({revisi})" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -308,6 +320,11 @@ msgstr "Diperluas"
msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services" msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services"
msgstr "Ekspor langganan dan kategori Anda sebagai file OPML yang dapat diimpor ke layanan membaca feed lainnya" msgstr "Ekspor langganan dan kategori Anda sebagai file OPML yang dapat diimpor ke layanan membaca feed lainnya"
#: src/components/header/Header.tsx
#: src/pages/WelcomePage.tsx
msgid "Extension options"
msgstr ""
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
msgid "Feed name" msgid "Feed name"
msgstr "Nama umpan" msgstr "Nama umpan"
@@ -513,6 +530,7 @@ msgid "Newest first"
msgstr "Terbaru dulu" msgstr "Terbaru dulu"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "Selanjutnya" msgstr "Selanjutnya"
@@ -540,6 +558,10 @@ msgstr "Tertua dulu"
msgid "Oops!" msgid "Oops!"
msgstr "Ups!" msgstr "Ups!"
#: src/components/header/Header.tsx
msgid "Open CommaFeed"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Open current entry in a new tab" msgid "Open current entry in a new tab"
msgstr "Buka entri saat ini di tab baru" msgstr "Buka entri saat ini di tab baru"
@@ -618,6 +640,10 @@ msgstr "Kata sandi tidak cocok"
msgid "Position" msgid "Position"
msgstr "Posisi" msgstr "Posisi"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
@@ -759,10 +785,12 @@ msgid "Swipe header to the right"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to dark theme" msgid "Switch to dark theme"
msgstr "Beralih ke tema gelap" msgstr "Beralih ke tema gelap"
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to light theme" msgid "Switch to light theme"
msgstr "Beralih ke tema terang" msgstr "Beralih ke tema terang"
@@ -782,6 +810,10 @@ msgstr "Tema"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "Beralih status baca entri saat ini" msgstr "Beralih status baca entri saat ini"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Cobalah CommaFeed dengan akun demo: demo/demo" msgstr "Cobalah CommaFeed dengan akun demo: demo/demo"

View File

@@ -17,6 +17,10 @@ msgstr ""
msgid "{0} (in {1})" msgid "{0} (in {1})"
msgstr "" msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "<0>Complete syntax is available </0><1>here</1>." msgid "<0>Complete syntax is available </0><1>here</1>."
msgstr "" msgstr ""
@@ -67,6 +71,10 @@ msgstr "Ammin"
msgid "All" msgid "All"
msgstr "Tutto" 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 #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "È stata inviata un'e-mail se questo indirizzo è stato registrato. " msgstr "È stata inviata un'e-mail se questo indirizzo è stato registrato. "
@@ -123,9 +131,13 @@ msgstr "Indietro"
msgid "Back to log in" msgid "Back to log in"
msgstr "Torna per accedere" msgstr "Torna per accedere"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "Estensioni del browser" msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -160,16 +172,16 @@ msgid "Check that the feed is working"
msgstr "Verifica che il feed funzioni" msgstr "Verifica che il feed funzioni"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed is an open-source project. Sources are hosted on <0>GitHub</0>." msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "CommaFeed è un progetto open source. " msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed successivo elemento non letto" msgstr "CommaFeed successivo elemento non letto"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "Versione CommaFeed {versione} ({revisione})" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -308,6 +320,11 @@ msgstr "Espanso"
msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services" msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services"
msgstr "Esporta le tue iscrizioni e categorie come file OPML che può essere importato in altri servizi di lettura feed" msgstr "Esporta le tue iscrizioni e categorie come file OPML che può essere importato in altri servizi di lettura feed"
#: src/components/header/Header.tsx
#: src/pages/WelcomePage.tsx
msgid "Extension options"
msgstr ""
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
msgid "Feed name" msgid "Feed name"
msgstr "Nome del feed" msgstr "Nome del feed"
@@ -513,6 +530,7 @@ msgid "Newest first"
msgstr "Il più recente prima" msgstr "Il più recente prima"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "Avanti" msgstr "Avanti"
@@ -540,6 +558,10 @@ msgstr "Il più vecchio prima"
msgid "Oops!" msgid "Oops!"
msgstr "Ops!" msgstr "Ops!"
#: src/components/header/Header.tsx
msgid "Open CommaFeed"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Open current entry in a new tab" msgid "Open current entry in a new tab"
msgstr "Apri la voce corrente in una nuova scheda" msgstr "Apri la voce corrente in una nuova scheda"
@@ -618,6 +640,10 @@ msgstr "Le password non corrispondono"
msgid "Position" msgid "Position"
msgstr "Posizione" msgstr "Posizione"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "Profilo" msgstr "Profilo"
@@ -759,10 +785,12 @@ msgid "Swipe header to the right"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to dark theme" msgid "Switch to dark theme"
msgstr "Passa al tema scuro" msgstr "Passa al tema scuro"
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to light theme" msgid "Switch to light theme"
msgstr "Passa al tema della luce" msgstr "Passa al tema della luce"
@@ -782,6 +810,10 @@ msgstr "Tema"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "Commuta lo stato di lettura della voce corrente" msgstr "Commuta lo stato di lettura della voce corrente"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Prova CommaFeed con il conto demo: demo/demo" msgstr "Prova CommaFeed con il conto demo: demo/demo"

View File

@@ -17,6 +17,10 @@ msgstr ""
msgid "{0} (in {1})" msgid "{0} (in {1})"
msgstr "" msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "<0>Complete syntax is available </0><1>here</1>." msgid "<0>Complete syntax is available </0><1>here</1>."
msgstr "" msgstr ""
@@ -67,6 +71,10 @@ msgstr "管理人"
msgid "All" msgid "All"
msgstr "全員" 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 #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "このアドレスが登録されていれば、メールが送信されました。" msgstr "このアドレスが登録されていれば、メールが送信されました。"
@@ -123,9 +131,13 @@ msgstr "裏"
msgid "Back to log in" msgid "Back to log in"
msgstr "ログインに戻る" msgstr "ログインに戻る"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "ブラウザ拡張機能" msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -160,16 +172,16 @@ msgid "Check that the feed is working"
msgstr "フィードが動作していることを確認してください" msgstr "フィードが動作していることを確認してください"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed is an open-source project. Sources are hosted on <0>GitHub</0>." msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "CommaFeed はオープンソース プロジェクトです。" msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "次の未読アイテムをカンマフィード" msgstr "次の未読アイテムをカンマフィード"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "コンマフィードのバージョン {version} ({revision})" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -308,6 +320,11 @@ msgstr "拡張"
msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services" msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services"
msgstr "サブスクリプションとカテゴリを、他のフィード読み取りサービスにインポートできる OPML ファイルとしてエクスポートします" msgstr "サブスクリプションとカテゴリを、他のフィード読み取りサービスにインポートできる OPML ファイルとしてエクスポートします"
#: src/components/header/Header.tsx
#: src/pages/WelcomePage.tsx
msgid "Extension options"
msgstr ""
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
msgid "Feed name" msgid "Feed name"
msgstr "フィード名" msgstr "フィード名"
@@ -513,6 +530,7 @@ msgid "Newest first"
msgstr "最新順" msgstr "最新順"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "次へ" msgstr "次へ"
@@ -540,6 +558,10 @@ msgstr "古い順"
msgid "Oops!" msgid "Oops!"
msgstr "おっと!" msgstr "おっと!"
#: src/components/header/Header.tsx
msgid "Open CommaFeed"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Open current entry in a new tab" msgid "Open current entry in a new tab"
msgstr "現在のエントリを新しいタブで開く" msgstr "現在のエントリを新しいタブで開く"
@@ -618,6 +640,10 @@ msgstr "パスワードが一致しません"
msgid "Position" msgid "Position"
msgstr "位置" msgstr "位置"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "プロフィール" msgstr "プロフィール"
@@ -759,10 +785,12 @@ msgid "Swipe header to the right"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to dark theme" msgid "Switch to dark theme"
msgstr "ダークテーマに切り替え" msgstr "ダークテーマに切り替え"
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to light theme" msgid "Switch to light theme"
msgstr "ライトテーマに切り替え" msgstr "ライトテーマに切り替え"
@@ -782,6 +810,10 @@ msgstr "テーマ"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "現在のエントリの読み取りステータスを切り替えます" msgstr "現在のエントリの読み取りステータスを切り替えます"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "デモアカウントで CommaFeed を試す: demo/demo" msgstr "デモアカウントで CommaFeed を試す: demo/demo"

View File

@@ -17,6 +17,10 @@ msgstr ""
msgid "{0} (in {1})" msgid "{0} (in {1})"
msgstr "" msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "<0>Complete syntax is available </0><1>here</1>." msgid "<0>Complete syntax is available </0><1>here</1>."
msgstr "" msgstr ""
@@ -67,6 +71,10 @@ msgstr "관리자"
msgid "All" msgid "All"
msgstr "전체" 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 #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "이 주소가 등록된 경우 이메일이 전송되었습니다. " msgstr "이 주소가 등록된 경우 이메일이 전송되었습니다. "
@@ -123,9 +131,13 @@ msgstr "뒤로"
msgid "Back to log in" msgid "Back to log in"
msgstr "로그인으로 돌아가기" msgstr "로그인으로 돌아가기"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "브라우저 확장" msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -160,16 +172,16 @@ msgid "Check that the feed is working"
msgstr "피드가 작동하는지 확인" msgstr "피드가 작동하는지 확인"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed is an open-source project. Sources are hosted on <0>GitHub</0>." msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "CommaFeed는 오픈 소스 프로젝트입니다. " msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "다음 읽지 않은 항목을 쉼표로 피드" msgstr "다음 읽지 않은 항목을 쉼표로 피드"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "쉼표 피드 버전 {버전}({개정})" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -308,6 +320,11 @@ msgstr "확장"
msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services" msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services"
msgstr "구독 및 카테고리를 다른 피드 읽기 서비스에서 가져올 수 있는 OPML 파일로 내보내기" msgstr "구독 및 카테고리를 다른 피드 읽기 서비스에서 가져올 수 있는 OPML 파일로 내보내기"
#: src/components/header/Header.tsx
#: src/pages/WelcomePage.tsx
msgid "Extension options"
msgstr ""
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
msgid "Feed name" msgid "Feed name"
msgstr "피드 이름" msgstr "피드 이름"
@@ -513,6 +530,7 @@ msgid "Newest first"
msgstr "최신순" msgstr "최신순"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "다음" msgstr "다음"
@@ -540,6 +558,10 @@ msgstr "가장 오래된 것부터"
msgid "Oops!" msgid "Oops!"
msgstr "앗!" msgstr "앗!"
#: src/components/header/Header.tsx
msgid "Open CommaFeed"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Open current entry in a new tab" msgid "Open current entry in a new tab"
msgstr "새 탭에서 현재 항목 열기" msgstr "새 탭에서 현재 항목 열기"
@@ -618,6 +640,10 @@ msgstr "비밀번호가 일치하지 않습니다"
msgid "Position" msgid "Position"
msgstr "위치" msgstr "위치"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "프로필" msgstr "프로필"
@@ -759,10 +785,12 @@ msgid "Swipe header to the right"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to dark theme" msgid "Switch to dark theme"
msgstr "어두운 테마로 전환" msgstr "어두운 테마로 전환"
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to light theme" msgid "Switch to light theme"
msgstr "밝은 테마로 전환" msgstr "밝은 테마로 전환"
@@ -782,6 +810,10 @@ msgstr "테마"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "현재 항목의 읽기 상태 전환" msgstr "현재 항목의 읽기 상태 전환"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "데모 계정으로 CommaFeed를 사용해 보세요: demo/demo" msgstr "데모 계정으로 CommaFeed를 사용해 보세요: demo/demo"

View File

@@ -17,6 +17,10 @@ msgstr ""
msgid "{0} (in {1})" msgid "{0} (in {1})"
msgstr "" msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "<0>Complete syntax is available </0><1>here</1>." msgid "<0>Complete syntax is available </0><1>here</1>."
msgstr "" msgstr ""
@@ -67,6 +71,10 @@ msgstr "Pentadbir"
msgid "All" msgid "All"
msgstr "Semua" 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 #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "E-mel telah dihantar jika alamat ini didaftarkan. " msgstr "E-mel telah dihantar jika alamat ini didaftarkan. "
@@ -123,9 +131,13 @@ msgstr "Kembali"
msgid "Back to log in" msgid "Back to log in"
msgstr "Kembali untuk log masuk" msgstr "Kembali untuk log masuk"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "Peluasan penyemak imbas" msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -160,16 +172,16 @@ msgid "Check that the feed is working"
msgstr "Semak sama ada suapan berfungsi" msgstr "Semak sama ada suapan berfungsi"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed is an open-source project. Sources are hosted on <0>GitHub</0>." msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "CommaFeed ialah projek sumber terbuka. " msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed item belum dibaca seterusnya" msgstr "CommaFeed item belum dibaca seterusnya"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "Versi CommaFeed {versi} ({semakan})" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -308,6 +320,11 @@ msgstr "Dikembangkan"
msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services" msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services"
msgstr "Eksport langganan dan kategori anda sebagai fail OPML yang boleh diimport dalam perkhidmatan membaca suapan lain" msgstr "Eksport langganan dan kategori anda sebagai fail OPML yang boleh diimport dalam perkhidmatan membaca suapan lain"
#: src/components/header/Header.tsx
#: src/pages/WelcomePage.tsx
msgid "Extension options"
msgstr ""
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
msgid "Feed name" msgid "Feed name"
msgstr "Nama suapan" msgstr "Nama suapan"
@@ -513,6 +530,7 @@ msgid "Newest first"
msgstr "Terbaharu dahulu" msgstr "Terbaharu dahulu"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "Seterusnya" msgstr "Seterusnya"
@@ -540,6 +558,10 @@ msgstr "Tertua dahulu"
msgid "Oops!" msgid "Oops!"
msgstr "Aduh!" msgstr "Aduh!"
#: src/components/header/Header.tsx
msgid "Open CommaFeed"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Open current entry in a new tab" msgid "Open current entry in a new tab"
msgstr "Buka entri semasa dalam tab baharu" msgstr "Buka entri semasa dalam tab baharu"
@@ -618,6 +640,10 @@ msgstr "Kata laluan tidak sepadan"
msgid "Position" msgid "Position"
msgstr "Kedudukan" msgstr "Kedudukan"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
@@ -759,10 +785,12 @@ msgid "Swipe header to the right"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to dark theme" msgid "Switch to dark theme"
msgstr "Tukar kepada tema gelap" msgstr "Tukar kepada tema gelap"
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to light theme" msgid "Switch to light theme"
msgstr "Tukar kepada tema cahaya" msgstr "Tukar kepada tema cahaya"
@@ -782,6 +810,10 @@ msgstr "Tema"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "Togol status bacaan entri semasa" msgstr "Togol status bacaan entri semasa"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Cuba CommaFeed dengan akaun demo: demo/demo" msgstr "Cuba CommaFeed dengan akaun demo: demo/demo"

View File

@@ -17,6 +17,10 @@ msgstr ""
msgid "{0} (in {1})" msgid "{0} (in {1})"
msgstr "" msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "<0>Complete syntax is available </0><1>here</1>." msgid "<0>Complete syntax is available </0><1>here</1>."
msgstr "" msgstr ""
@@ -67,6 +71,10 @@ msgstr ""
msgid "All" msgid "All"
msgstr "Alle" 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 #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "En e-post er sendt hvis denne adressen var registrert. " msgstr "En e-post er sendt hvis denne adressen var registrert. "
@@ -123,9 +131,13 @@ msgstr "Tilbake"
msgid "Back to log in" msgid "Back to log in"
msgstr "Tilbake for å logge inn" msgstr "Tilbake for å logge inn"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "Nettleserutvidelser" msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -160,16 +172,16 @@ msgid "Check that the feed is working"
msgstr "Sjekk at feeden fungerer" msgstr "Sjekk at feeden fungerer"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed is an open-source project. Sources are hosted on <0>GitHub</0>." msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "CommaFeed er et åpen kildekode-prosjekt. " msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed neste uleste element" msgstr "CommaFeed neste uleste element"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "CommaFeed versjon {versjon} ({revisjon})" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -308,6 +320,11 @@ msgstr "Utvidet"
msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services" msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services"
msgstr "Eksporter abonnementene og kategoriene dine som en OPML-fil som kan importeres i andre feedlesetjenester" msgstr "Eksporter abonnementene og kategoriene dine som en OPML-fil som kan importeres i andre feedlesetjenester"
#: src/components/header/Header.tsx
#: src/pages/WelcomePage.tsx
msgid "Extension options"
msgstr ""
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
msgid "Feed name" msgid "Feed name"
msgstr "Feednavn" msgstr "Feednavn"
@@ -513,6 +530,7 @@ msgid "Newest first"
msgstr "Nyeste først" msgstr "Nyeste først"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "Neste" msgstr "Neste"
@@ -540,6 +558,10 @@ msgstr "Eldste først"
msgid "Oops!" msgid "Oops!"
msgstr "Beklager!" msgstr "Beklager!"
#: src/components/header/Header.tsx
msgid "Open CommaFeed"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Open current entry in a new tab" msgid "Open current entry in a new tab"
msgstr "Åpne gjeldende oppføring i en ny fane" msgstr "Åpne gjeldende oppføring i en ny fane"
@@ -618,6 +640,10 @@ msgstr "Passordene samsvarer ikke"
msgid "Position" msgid "Position"
msgstr "Posisjon" msgstr "Posisjon"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
@@ -759,10 +785,12 @@ msgid "Swipe header to the right"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to dark theme" msgid "Switch to dark theme"
msgstr "Bytt til mørkt tema" msgstr "Bytt til mørkt tema"
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to light theme" msgid "Switch to light theme"
msgstr "Bytt til lystema" msgstr "Bytt til lystema"
@@ -782,6 +810,10 @@ msgstr "Tema"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "Veksle lesestatus for gjeldende oppføring" msgstr "Veksle lesestatus for gjeldende oppføring"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Prøv CommaFeed med demokontoen: demo/demo" msgstr "Prøv CommaFeed med demokontoen: demo/demo"

View File

@@ -17,6 +17,10 @@ msgstr ""
msgid "{0} (in {1})" msgid "{0} (in {1})"
msgstr "" msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "<0>Complete syntax is available </0><1>here</1>." msgid "<0>Complete syntax is available </0><1>here</1>."
msgstr "" msgstr ""
@@ -67,6 +71,10 @@ msgstr "Beheerder"
msgid "All" msgid "All"
msgstr "Alles" 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 #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "Er is een e-mail verzonden als dit adres is geregistreerd. " msgstr "Er is een e-mail verzonden als dit adres is geregistreerd. "
@@ -123,9 +131,13 @@ msgstr "Terug"
msgid "Back to log in" msgid "Back to log in"
msgstr "Terug naar inloggen" msgstr "Terug naar inloggen"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "Browserextensies" msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -160,16 +172,16 @@ msgid "Check that the feed is working"
msgstr "Controleer of de feed werkt" msgstr "Controleer of de feed werkt"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed is an open-source project. Sources are hosted on <0>GitHub</0>." msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "CommaFeed is een open-sourceproject. " msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed volgende ongelezen item" msgstr "CommaFeed volgende ongelezen item"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "CommaFeed-versie {versie} ({revisie})" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -308,6 +320,11 @@ msgstr "Uitgebreid"
msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services" msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services"
msgstr "Exporteer uw abonnementen en categorieën als een OPML-bestand dat kan worden geïmporteerd in andere feedleesservices" msgstr "Exporteer uw abonnementen en categorieën als een OPML-bestand dat kan worden geïmporteerd in andere feedleesservices"
#: src/components/header/Header.tsx
#: src/pages/WelcomePage.tsx
msgid "Extension options"
msgstr ""
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
msgid "Feed name" msgid "Feed name"
msgstr "Feednaam" msgstr "Feednaam"
@@ -513,6 +530,7 @@ msgid "Newest first"
msgstr "Nieuwste eerst" msgstr "Nieuwste eerst"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "Volgende" msgstr "Volgende"
@@ -540,6 +558,10 @@ msgstr "Oudste eerst"
msgid "Oops!" msgid "Oops!"
msgstr "Oeps!" msgstr "Oeps!"
#: src/components/header/Header.tsx
msgid "Open CommaFeed"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Open current entry in a new tab" msgid "Open current entry in a new tab"
msgstr "Huidige invoer openen in een nieuw tabblad" msgstr "Huidige invoer openen in een nieuw tabblad"
@@ -618,6 +640,10 @@ msgstr "Wachtwoorden komen niet overeen"
msgid "Position" msgid "Position"
msgstr "Positie" msgstr "Positie"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "Profiel" msgstr "Profiel"
@@ -759,10 +785,12 @@ msgid "Swipe header to the right"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to dark theme" msgid "Switch to dark theme"
msgstr "Overschakelen naar donker thema" msgstr "Overschakelen naar donker thema"
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to light theme" msgid "Switch to light theme"
msgstr "Overschakelen naar lichtthema" msgstr "Overschakelen naar lichtthema"
@@ -782,6 +810,10 @@ msgstr "Thema"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "Toggle leesstatus van huidige invoer" msgstr "Toggle leesstatus van huidige invoer"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Probeer CommaFeed uit met het demo-account: demo/demo" msgstr "Probeer CommaFeed uit met het demo-account: demo/demo"

View File

@@ -17,6 +17,10 @@ msgstr ""
msgid "{0} (in {1})" msgid "{0} (in {1})"
msgstr "" msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "<0>Complete syntax is available </0><1>here</1>." msgid "<0>Complete syntax is available </0><1>here</1>."
msgstr "" msgstr ""
@@ -67,6 +71,10 @@ msgstr ""
msgid "All" msgid "All"
msgstr "Alle" 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 #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "En e-post er sendt hvis denne adressen var registrert. " msgstr "En e-post er sendt hvis denne adressen var registrert. "
@@ -123,9 +131,13 @@ msgstr "Tilbake"
msgid "Back to log in" msgid "Back to log in"
msgstr "Tilbake for å logge inn" msgstr "Tilbake for å logge inn"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "Nettleserutvidelser" msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -160,16 +172,16 @@ msgid "Check that the feed is working"
msgstr "Sjekk at feeden fungerer" msgstr "Sjekk at feeden fungerer"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed is an open-source project. Sources are hosted on <0>GitHub</0>." msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "CommaFeed er et åpen kildekode-prosjekt. " msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed neste uleste element" msgstr "CommaFeed neste uleste element"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "CommaFeed versjon {versjon} ({revisjon})" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -308,6 +320,11 @@ msgstr "Utvidet"
msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services" msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services"
msgstr "Eksporter abonnementene og kategoriene dine som en OPML-fil som kan importeres i andre feedlesetjenester" msgstr "Eksporter abonnementene og kategoriene dine som en OPML-fil som kan importeres i andre feedlesetjenester"
#: src/components/header/Header.tsx
#: src/pages/WelcomePage.tsx
msgid "Extension options"
msgstr ""
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
msgid "Feed name" msgid "Feed name"
msgstr "Feednavn" msgstr "Feednavn"
@@ -513,6 +530,7 @@ msgid "Newest first"
msgstr "Nyeste først" msgstr "Nyeste først"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "Neste" msgstr "Neste"
@@ -540,6 +558,10 @@ msgstr "Eldste først"
msgid "Oops!" msgid "Oops!"
msgstr "Beklager!" msgstr "Beklager!"
#: src/components/header/Header.tsx
msgid "Open CommaFeed"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Open current entry in a new tab" msgid "Open current entry in a new tab"
msgstr "Åpne gjeldende oppføring i en ny fane" msgstr "Åpne gjeldende oppføring i en ny fane"
@@ -618,6 +640,10 @@ msgstr "Passordene samsvarer ikke"
msgid "Position" msgid "Position"
msgstr "Posisjon" msgstr "Posisjon"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
@@ -759,10 +785,12 @@ msgid "Swipe header to the right"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to dark theme" msgid "Switch to dark theme"
msgstr "Bytt til mørkt tema" msgstr "Bytt til mørkt tema"
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to light theme" msgid "Switch to light theme"
msgstr "Bytt til lystema" msgstr "Bytt til lystema"
@@ -782,6 +810,10 @@ msgstr "Tema"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "Veksle lesestatus for gjeldende oppføring" msgstr "Veksle lesestatus for gjeldende oppføring"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Prøv CommaFeed med demokontoen: demo/demo" msgstr "Prøv CommaFeed med demokontoen: demo/demo"

View File

@@ -17,6 +17,10 @@ msgstr ""
msgid "{0} (in {1})" msgid "{0} (in {1})"
msgstr "" msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "<0>Complete syntax is available </0><1>here</1>." msgid "<0>Complete syntax is available </0><1>here</1>."
msgstr "" msgstr ""
@@ -67,6 +71,10 @@ msgstr "Administracja"
msgid "All" msgid "All"
msgstr "Wszystkie" 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 #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "E-mail został wysłany, jeśli ten adres został zarejestrowany. " msgstr "E-mail został wysłany, jeśli ten adres został zarejestrowany. "
@@ -123,9 +131,13 @@ msgstr "Powrót"
msgid "Back to log in" msgid "Back to log in"
msgstr "Powrót do logowania" msgstr "Powrót do logowania"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "Rozszerzenia przeglądarki" msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -160,16 +172,16 @@ msgid "Check that the feed is working"
msgstr "Sprawdź, czy kanał działa" msgstr "Sprawdź, czy kanał działa"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed is an open-source project. Sources are hosted on <0>GitHub</0>." msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "CommaFeed to projekt typu open source. " msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "Przecinek następny nieprzeczytany element" msgstr "Przecinek następny nieprzeczytany element"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "Wersja CommaFeed {wersja} ({wersja})" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -308,6 +320,11 @@ msgstr "Rozszerzony"
msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services" msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services"
msgstr "Eksportuj swoje subskrypcje i kategorie jako plik OPML, który można zaimportować do innych usług odczytu kanałów" msgstr "Eksportuj swoje subskrypcje i kategorie jako plik OPML, który można zaimportować do innych usług odczytu kanałów"
#: src/components/header/Header.tsx
#: src/pages/WelcomePage.tsx
msgid "Extension options"
msgstr ""
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
msgid "Feed name" msgid "Feed name"
msgstr "nazwa kanału" msgstr "nazwa kanału"
@@ -513,6 +530,7 @@ msgid "Newest first"
msgstr "Najnowsze jako pierwsze" msgstr "Najnowsze jako pierwsze"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "Dalej" msgstr "Dalej"
@@ -540,6 +558,10 @@ msgstr "Najstarsze jako pierwsze"
msgid "Oops!" msgid "Oops!"
msgstr "Ups!" msgstr "Ups!"
#: src/components/header/Header.tsx
msgid "Open CommaFeed"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Open current entry in a new tab" msgid "Open current entry in a new tab"
msgstr "Otwórz bieżący wpis w nowej karcie" msgstr "Otwórz bieżący wpis w nowej karcie"
@@ -618,6 +640,10 @@ msgstr "Hasła nie pasują"
msgid "Position" msgid "Position"
msgstr "Pozycja" msgstr "Pozycja"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
@@ -759,10 +785,12 @@ msgid "Swipe header to the right"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to dark theme" msgid "Switch to dark theme"
msgstr "Przełącz na ciemny motyw" msgstr "Przełącz na ciemny motyw"
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to light theme" msgid "Switch to light theme"
msgstr "Przełącz na jasny motyw" msgstr "Przełącz na jasny motyw"
@@ -782,6 +810,10 @@ msgstr "Motyw"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "Przełącz stan odczytu bieżącego wpisu" msgstr "Przełącz stan odczytu bieżącego wpisu"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Wypróbuj CommaFeed z kontem demo: demo/demo" msgstr "Wypróbuj CommaFeed z kontem demo: demo/demo"

View File

@@ -17,6 +17,10 @@ msgstr ""
msgid "{0} (in {1})" msgid "{0} (in {1})"
msgstr "" msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "<0>Complete syntax is available </0><1>here</1>." msgid "<0>Complete syntax is available </0><1>here</1>."
msgstr "" msgstr ""
@@ -67,6 +71,10 @@ msgstr "Administrador"
msgid "All" msgid "All"
msgstr "Todos" 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 #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "Um email foi enviado se este endereço foi registrado. " msgstr "Um email foi enviado se este endereço foi registrado. "
@@ -123,9 +131,13 @@ msgstr "Voltar"
msgid "Back to log in" msgid "Back to log in"
msgstr "Voltar para logar" msgstr "Voltar para logar"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "Extensões do navegador" msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -160,16 +172,16 @@ msgid "Check that the feed is working"
msgstr "Verifique se o feed está funcionando" msgstr "Verifique se o feed está funcionando"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed is an open-source project. Sources are hosted on <0>GitHub</0>." msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "CommaFeed é um projeto de código aberto. " msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed próximo item não lido" msgstr "CommaFeed próximo item não lido"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "Versão do CommaFeed {versão} ({revisão})" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -308,6 +320,11 @@ msgstr "Expandido"
msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services" msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services"
msgstr "Exporte suas inscrições e categorias como um arquivo OPML que pode ser importado em outros serviços de leitura de feed" msgstr "Exporte suas inscrições e categorias como um arquivo OPML que pode ser importado em outros serviços de leitura de feed"
#: src/components/header/Header.tsx
#: src/pages/WelcomePage.tsx
msgid "Extension options"
msgstr ""
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
msgid "Feed name" msgid "Feed name"
msgstr "Nome do feed" msgstr "Nome do feed"
@@ -513,6 +530,7 @@ msgid "Newest first"
msgstr "Mais novo primeiro" msgstr "Mais novo primeiro"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "Próximo" msgstr "Próximo"
@@ -540,6 +558,10 @@ msgstr "Mais antigo primeiro"
msgid "Oops!" msgid "Oops!"
msgstr "Opa!" msgstr "Opa!"
#: src/components/header/Header.tsx
msgid "Open CommaFeed"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Open current entry in a new tab" msgid "Open current entry in a new tab"
msgstr "Abrir a entrada atual em uma nova aba" msgstr "Abrir a entrada atual em uma nova aba"
@@ -618,6 +640,10 @@ msgstr "Senhas não coincidem"
msgid "Position" msgid "Position"
msgstr "Posição" msgstr "Posição"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "Perfil" msgstr "Perfil"
@@ -759,10 +785,12 @@ msgid "Swipe header to the right"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to dark theme" msgid "Switch to dark theme"
msgstr "Mudar para tema escuro" msgstr "Mudar para tema escuro"
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to light theme" msgid "Switch to light theme"
msgstr "Mudar para tema claro" msgstr "Mudar para tema claro"
@@ -782,6 +810,10 @@ msgstr "Tema"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "Alternar o status de leitura da entrada atual" msgstr "Alternar o status de leitura da entrada atual"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Experimente o CommaFeed com a conta demo: demo/demo" msgstr "Experimente o CommaFeed com a conta demo: demo/demo"

View File

@@ -17,6 +17,10 @@ msgstr ""
msgid "{0} (in {1})" msgid "{0} (in {1})"
msgstr "" msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "<0>Complete syntax is available </0><1>here</1>." msgid "<0>Complete syntax is available </0><1>here</1>."
msgstr "" msgstr ""
@@ -67,6 +71,10 @@ msgstr "Админ"
msgid "All" msgid "All"
msgstr "Все" 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 #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "Электронное письмо было отправлено, если этот адрес был зарегистрирован. " msgstr "Электронное письмо было отправлено, если этот адрес был зарегистрирован. "
@@ -123,9 +131,13 @@ msgstr "Назад"
msgid "Back to log in" msgid "Back to log in"
msgstr "Вернуться к входу" msgstr "Вернуться к входу"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "Расширения браузера" msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -160,16 +172,16 @@ msgid "Check that the feed is working"
msgstr "Проверьте, работает ли лента." msgstr "Проверьте, работает ли лента."
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed is an open-source project. Sources are hosted on <0>GitHub</0>." msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "CommaFeed — это проект с открытым исходным кодом. " msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed следующий непрочитанный элемент" msgstr "CommaFeed следующий непрочитанный элемент"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "CommaFeed версия {версия} ({редакция})" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -308,6 +320,11 @@ msgstr "Расширенный"
msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services" msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services"
msgstr "Экспортируйте свои подписки и категории в виде файла OPML, который можно импортировать в другие службы чтения каналов." msgstr "Экспортируйте свои подписки и категории в виде файла OPML, который можно импортировать в другие службы чтения каналов."
#: src/components/header/Header.tsx
#: src/pages/WelcomePage.tsx
msgid "Extension options"
msgstr ""
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
msgid "Feed name" msgid "Feed name"
msgstr "Имя фида" msgstr "Имя фида"
@@ -513,6 +530,7 @@ msgid "Newest first"
msgstr "Сначала новые" msgstr "Сначала новые"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "Далее" msgstr "Далее"
@@ -540,6 +558,10 @@ msgstr "Сначала самые старые"
msgid "Oops!" msgid "Oops!"
msgstr "Ой!" msgstr "Ой!"
#: src/components/header/Header.tsx
msgid "Open CommaFeed"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Open current entry in a new tab" msgid "Open current entry in a new tab"
msgstr "Открыть текущую запись в новой вкладке" msgstr "Открыть текущую запись в новой вкладке"
@@ -618,6 +640,10 @@ msgstr "Пароли не совпадают"
msgid "Position" msgid "Position"
msgstr "Позиция" msgstr "Позиция"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "Профиль" msgstr "Профиль"
@@ -759,10 +785,12 @@ msgid "Swipe header to the right"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to dark theme" msgid "Switch to dark theme"
msgstr "Переключиться на темную тему" msgstr "Переключиться на темную тему"
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to light theme" msgid "Switch to light theme"
msgstr "Переключиться на светлую тему" msgstr "Переключиться на светлую тему"
@@ -782,6 +810,10 @@ msgstr "Тема"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "Переключить статус чтения текущей записи" msgstr "Переключить статус чтения текущей записи"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Попробуйте CommaFeed на демо-счете: demo/demo" msgstr "Попробуйте CommaFeed на демо-счете: demo/demo"

View File

@@ -17,6 +17,10 @@ msgstr ""
msgid "{0} (in {1})" msgid "{0} (in {1})"
msgstr "" msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "<0>Complete syntax is available </0><1>here</1>." msgid "<0>Complete syntax is available </0><1>here</1>."
msgstr "" msgstr ""
@@ -67,6 +71,10 @@ msgstr "Správca"
msgid "All" msgid "All"
msgstr "Všetky" 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 #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "E-mail bol odoslaný, ak bola táto adresa zaregistrovaná. " msgstr "E-mail bol odoslaný, ak bola táto adresa zaregistrovaná. "
@@ -123,9 +131,13 @@ msgstr "Späť"
msgid "Back to log in" msgid "Back to log in"
msgstr "Späť na prihlásenie" msgstr "Späť na prihlásenie"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "Rozšírenia prehliadača" msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -160,16 +172,16 @@ msgid "Check that the feed is working"
msgstr "Skontrolujte, či feed funguje" msgstr "Skontrolujte, či feed funguje"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed is an open-source project. Sources are hosted on <0>GitHub</0>." msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "CommaFeed je projekt s otvoreným zdrojom. " msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed ďalšia neprečítaná položka" msgstr "CommaFeed ďalšia neprečítaná položka"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "CommaFeed verzia {version} ({revision})" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -308,6 +320,11 @@ msgstr "Rozšírené"
msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services" msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services"
msgstr "Exportujte svoje odbery a kategórie ako súbor OPML, ktorý je možné importovať do iných služieb na čítanie informačných kanálov" msgstr "Exportujte svoje odbery a kategórie ako súbor OPML, ktorý je možné importovať do iných služieb na čítanie informačných kanálov"
#: src/components/header/Header.tsx
#: src/pages/WelcomePage.tsx
msgid "Extension options"
msgstr ""
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
msgid "Feed name" msgid "Feed name"
msgstr "Názov informačného kanála" msgstr "Názov informačného kanála"
@@ -513,6 +530,7 @@ msgid "Newest first"
msgstr "Najnovšie ako prvé" msgstr "Najnovšie ako prvé"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "Ďalej" msgstr "Ďalej"
@@ -540,6 +558,10 @@ msgstr "Najprv najstarší"
msgid "Oops!" msgid "Oops!"
msgstr "Ojoj!" msgstr "Ojoj!"
#: src/components/header/Header.tsx
msgid "Open CommaFeed"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Open current entry in a new tab" msgid "Open current entry in a new tab"
msgstr "Otvorte aktuálny záznam na novej karte" msgstr "Otvorte aktuálny záznam na novej karte"
@@ -618,6 +640,10 @@ msgstr "Heslá sa nezhodujú"
msgid "Position" msgid "Position"
msgstr "Pozícia" msgstr "Pozícia"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
@@ -759,10 +785,12 @@ msgid "Swipe header to the right"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to dark theme" msgid "Switch to dark theme"
msgstr "Prepnúť na tmavú tému" msgstr "Prepnúť na tmavú tému"
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to light theme" msgid "Switch to light theme"
msgstr "Prepnite na svetlú tému" msgstr "Prepnite na svetlú tému"
@@ -782,6 +810,10 @@ msgstr "Téma"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "Prepne stav čítania aktuálneho záznamu" msgstr "Prepne stav čítania aktuálneho záznamu"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Vyskúšajte CommaFeed s demo účtom: demo/demo" msgstr "Vyskúšajte CommaFeed s demo účtom: demo/demo"

View File

@@ -17,6 +17,10 @@ msgstr ""
msgid "{0} (in {1})" msgid "{0} (in {1})"
msgstr "" msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "<0>Complete syntax is available </0><1>here</1>." msgid "<0>Complete syntax is available </0><1>here</1>."
msgstr "" msgstr ""
@@ -67,6 +71,10 @@ msgstr ""
msgid "All" msgid "All"
msgstr "Alla" 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 #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "Ett e-postmeddelande har skickats om denna adress var registrerad. " msgstr "Ett e-postmeddelande har skickats om denna adress var registrerad. "
@@ -123,9 +131,13 @@ msgstr "Tillbaka"
msgid "Back to log in" msgid "Back to log in"
msgstr "Tillbaka för att logga in" msgstr "Tillbaka för att logga in"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "Webbläsartillägg" msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -160,15 +172,15 @@ msgid "Check that the feed is working"
msgstr "Kontrollera att matningen fungerar" msgstr "Kontrollera att matningen fungerar"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed is an open-source project. Sources are hosted on <0>GitHub</0>." msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "CommaFeed är ett projekt med öppen källkod. " msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed nästa olästa objekt" msgstr "CommaFeed nästa olästa objekt"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
@@ -308,6 +320,11 @@ msgstr "Utökad"
msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services" msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services"
msgstr "Exportera dina prenumerationer och kategorier som en OPML-fil som kan importeras i andra flödesläsningstjänster" msgstr "Exportera dina prenumerationer och kategorier som en OPML-fil som kan importeras i andra flödesläsningstjänster"
#: src/components/header/Header.tsx
#: src/pages/WelcomePage.tsx
msgid "Extension options"
msgstr ""
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
msgid "Feed name" msgid "Feed name"
msgstr "Flödesnamn" msgstr "Flödesnamn"
@@ -513,6 +530,7 @@ msgid "Newest first"
msgstr "Nyast först" msgstr "Nyast först"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "Nästa" msgstr "Nästa"
@@ -540,6 +558,10 @@ msgstr "Äldst först"
msgid "Oops!" msgid "Oops!"
msgstr "Hoppsan!" msgstr "Hoppsan!"
#: src/components/header/Header.tsx
msgid "Open CommaFeed"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Open current entry in a new tab" msgid "Open current entry in a new tab"
msgstr "Öppna aktuell post i en ny flik" msgstr "Öppna aktuell post i en ny flik"
@@ -618,6 +640,10 @@ msgstr "Lösenorden matchar inte"
msgid "Position" msgid "Position"
msgstr "" msgstr ""
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
@@ -759,10 +785,12 @@ msgid "Swipe header to the right"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to dark theme" msgid "Switch to dark theme"
msgstr "Byt till mörkt tema" msgstr "Byt till mörkt tema"
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to light theme" msgid "Switch to light theme"
msgstr "Byt till ljustema" msgstr "Byt till ljustema"
@@ -782,6 +810,10 @@ msgstr "Tema"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "Växla lässtatus för aktuell post" msgstr "Växla lässtatus för aktuell post"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Prova CommaFeed med demokontot: demo/demo" msgstr "Prova CommaFeed med demokontot: demo/demo"

View File

@@ -17,6 +17,10 @@ msgstr ""
msgid "{0} (in {1})" msgid "{0} (in {1})"
msgstr "" msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "<0>Complete syntax is available </0><1>here</1>." msgid "<0>Complete syntax is available </0><1>here</1>."
msgstr "" msgstr ""
@@ -67,6 +71,10 @@ msgstr "Yönetici"
msgid "All" msgid "All"
msgstr "Tümü" 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 ""
#: src/pages/auth/PasswordRecoveryPage.tsx #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "Bu adres kayıtlıysa bir e-posta gönderildi. " msgstr "Bu adres kayıtlıysa bir e-posta gönderildi. "
@@ -123,9 +131,13 @@ msgstr "Geri"
msgid "Back to log in" msgid "Back to log in"
msgstr "Giriş yapmak için geri dön" msgstr "Giriş yapmak için geri dön"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "Tarayıcı uzantıları" msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -160,16 +172,16 @@ msgid "Check that the feed is working"
msgstr "Feed'in çalışıp çalışmadığını kontrol edin" msgstr "Feed'in çalışıp çalışmadığını kontrol edin"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed is an open-source project. Sources are hosted on <0>GitHub</0>." msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "CommaFeed açık kaynaklı bir projedir. " msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed sonraki okunmamış öğe" msgstr "CommaFeed sonraki okunmamış öğe"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "CommaFeed sürümü {sürüm} ({revizyon})" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -308,6 +320,11 @@ msgstr "Genişletilmiş"
msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services" msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services"
msgstr "Aboneliklerinizi ve kategorilerinizi diğer besleme okuma hizmetlerinde içe aktarılabilen bir OPML dosyası olarak dışa aktarın" msgstr "Aboneliklerinizi ve kategorilerinizi diğer besleme okuma hizmetlerinde içe aktarılabilen bir OPML dosyası olarak dışa aktarın"
#: src/components/header/Header.tsx
#: src/pages/WelcomePage.tsx
msgid "Extension options"
msgstr ""
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
msgid "Feed name" msgid "Feed name"
msgstr "Yayın adı" msgstr "Yayın adı"
@@ -513,6 +530,7 @@ msgid "Newest first"
msgstr "Önce en yenisi" msgstr "Önce en yenisi"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "Sonraki" msgstr "Sonraki"
@@ -540,6 +558,10 @@ msgstr "Önce en eski"
msgid "Oops!" msgid "Oops!"
msgstr "Hata!" msgstr "Hata!"
#: src/components/header/Header.tsx
msgid "Open CommaFeed"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Open current entry in a new tab" msgid "Open current entry in a new tab"
msgstr "Geçerli girişi yeni bir sekmede aç" msgstr "Geçerli girişi yeni bir sekmede aç"
@@ -618,6 +640,10 @@ msgstr "Parolalar eşleşmiyor"
msgid "Position" msgid "Position"
msgstr "Konum" msgstr "Konum"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
@@ -759,10 +785,12 @@ msgid "Swipe header to the right"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to dark theme" msgid "Switch to dark theme"
msgstr "Karanlık temaya geç" msgstr "Karanlık temaya geç"
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to light theme" msgid "Switch to light theme"
msgstr "Açık temaya geç" msgstr "Açık temaya geç"
@@ -782,6 +810,10 @@ msgstr "Tema"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "Geçerli girişin okuma durumunu değiştir" msgstr "Geçerli girişin okuma durumunu değiştir"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "CommaFeed'i demo hesabıyla deneyin: demo/demo" msgstr "CommaFeed'i demo hesabıyla deneyin: demo/demo"

View File

@@ -17,6 +17,10 @@ msgstr ""
msgid "{0} (in {1})" msgid "{0} (in {1})"
msgstr "" msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx #: src/pages/app/FeedDetailsPage.tsx
msgid "<0>Complete syntax is available </0><1>here</1>." msgid "<0>Complete syntax is available </0><1>here</1>."
msgstr "" msgstr ""
@@ -67,6 +71,10 @@ msgstr "管理员"
msgid "All" msgid "All"
msgstr "全部" 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 #: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox." msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "如果此地址已注册,则已发送电子邮件。" msgstr "如果此地址已注册,则已发送电子邮件。"
@@ -123,9 +131,13 @@ msgstr "返回"
msgid "Back to log in" msgid "Back to log in"
msgstr "返回登录" msgstr "返回登录"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "Browser extentions" msgid "Browser extention"
msgstr "浏览器扩展" msgstr ""
#: src/components/admin/UserEdit.tsx #: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx #: src/components/content/add/AddCategory.tsx
@@ -160,16 +172,16 @@ msgid "Check that the feed is working"
msgstr "检查提要是否正常工作" msgstr "检查提要是否正常工作"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed is an open-source project. Sources are hosted on <0>GitHub</0>." msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "CommaFeed 是一个开源项目。" msgstr ""
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item" msgid "CommaFeed next unread item"
msgstr "CommaFeed 下一个未读项目" msgstr "CommaFeed 下一个未读项目"
#: src/pages/app/AboutPage.tsx #: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})" msgid "CommaFeed version {version} ({revision})."
msgstr "CommaFeed 版本 {version} ({revision})" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
msgid "Compact" msgid "Compact"
@@ -308,6 +320,11 @@ msgstr "展开"
msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services" msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services"
msgstr "将您的订阅和类别导出为 OPML 文件,可以在其他提要阅读服务中导入" msgstr "将您的订阅和类别导出为 OPML 文件,可以在其他提要阅读服务中导入"
#: src/components/header/Header.tsx
#: src/pages/WelcomePage.tsx
msgid "Extension options"
msgstr ""
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
msgid "Feed name" msgid "Feed name"
msgstr "提要名称" msgstr "提要名称"
@@ -513,6 +530,7 @@ msgid "Newest first"
msgstr "最新优先" msgstr "最新优先"
#: src/components/content/add/Subscribe.tsx #: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next" msgid "Next"
msgstr "下一个" msgstr "下一个"
@@ -540,6 +558,10 @@ msgstr "最早的优先"
msgid "Oops!" msgid "Oops!"
msgstr "哎呀!" msgstr "哎呀!"
#: src/components/header/Header.tsx
msgid "Open CommaFeed"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx #: src/components/KeyboardShortcutsHelp.tsx
msgid "Open current entry in a new tab" msgid "Open current entry in a new tab"
msgstr "在新选项卡中打开当前条目" msgstr "在新选项卡中打开当前条目"
@@ -618,6 +640,10 @@ msgstr "密码不匹配"
msgid "Position" msgid "Position"
msgstr "位置" msgstr "位置"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx #: src/pages/app/SettingsPage.tsx
msgid "Profile" msgid "Profile"
msgstr "配置文件" msgstr "配置文件"
@@ -759,10 +785,12 @@ msgid "Swipe header to the right"
msgstr "" msgstr ""
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to dark theme" msgid "Switch to dark theme"
msgstr "切换到深色主题" msgstr "切换到深色主题"
#: src/components/header/ProfileMenu.tsx #: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to light theme" msgid "Switch to light theme"
msgstr "切换到浅色主题" msgstr "切换到浅色主题"
@@ -782,6 +810,10 @@ msgstr "主题"
msgid "Toggle read status of current entry" msgid "Toggle read status of current entry"
msgstr "切换当前条目的读取状态" msgstr "切换当前条目的读取状态"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx #: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo" msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "使用演示帐户试用 CommaFeeddemo/demo" msgstr "使用演示帐户试用 CommaFeeddemo/demo"

View File

@@ -0,0 +1,4 @@
html, body {
/* disable pull-to-refresh on mobile as it messes with vertical scrolling */
overscroll-behavior: none;
}

View File

@@ -1,11 +1,12 @@
import "@fontsource/open-sans" import "@fontsource/open-sans"
import { App } from "App"
import { store } from "app/store" import { store } from "app/store"
import dayjs from "dayjs" import dayjs from "dayjs"
import relativeTime from "dayjs/plugin/relativeTime" import relativeTime from "dayjs/plugin/relativeTime"
import "main.css"
import "react-contexify/ReactContexify.css" import "react-contexify/ReactContexify.css"
import ReactDOM from "react-dom/client" import ReactDOM from "react-dom/client"
import { Provider } from "react-redux" import { Provider } from "react-redux"
import { App } from "./App"
dayjs.extend(relativeTime) dayjs.extend(relativeTime)

View File

@@ -1,23 +1,32 @@
import { Trans } from "@lingui/macro" import { Trans } from "@lingui/macro"
import { Anchor, Box, Center, Container, Divider, Group, Image, Title, useMantineColorScheme } from "@mantine/core" import { Anchor, Box, Center, Container, Divider, Group, Image, Title, useMantineColorScheme } from "@mantine/core"
import { useMediaQuery } from "@mantine/hooks"
import { client } from "app/client" import { client } from "app/client"
import { Constants } from "app/constants" import { redirectToApiDocumentation, redirectToLogin, redirectToRegistration, redirectToRootCategory } from "app/slices/redirect"
import { redirectToLogin, redirectToRegistration, redirectToRootCategory } from "app/slices/redirect"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import welcome_page_dark from "assets/welcome_page_dark.png" import welcome_page_dark from "assets/welcome_page_dark.png"
import welcome_page_light from "assets/welcome_page_light.png" import welcome_page_light from "assets/welcome_page_light.png"
import { ActionButton } from "components/ActionButtton" import { ActionButton } from "components/ActionButton"
import { ButtonToolbar } from "components/ButtonToolbar" import { useBrowserExtension } from "hooks/useBrowserExtension"
import { useMobile } from "hooks/useMobile"
import { useAsyncCallback } from "react-async-hook" import { useAsyncCallback } from "react-async-hook"
import { SiGithub, TbKey, TbUserPlus } from "react-icons/all" import { SiGithub, SiTwitter } from "react-icons/si"
import { SiTwitter } from "react-icons/si" import { TbClock, TbKey, TbMoon, TbSettings, TbSun, TbUserPlus } from "react-icons/tb"
import { TbClock, TbMoon, TbSun } from "react-icons/tb"
import { PageTitle } from "./PageTitle" import { PageTitle } from "./PageTitle"
const iconSize = 18
export function WelcomePage() { export function WelcomePage() {
const serverInfos = useAppSelector(state => state.server.serverInfos)
const { colorScheme } = useMantineColorScheme() const { colorScheme } = useMantineColorScheme()
const dispatch = useAppDispatch()
const image = colorScheme === "light" ? welcome_page_light : welcome_page_dark const image = colorScheme === "light" ? welcome_page_light : welcome_page_dark
const login = useAsyncCallback(client.user.login, {
onSuccess: () => {
dispatch(redirectToRootCategory())
},
})
return ( return (
<Container> <Container>
<Header /> <Header />
@@ -26,6 +35,18 @@ export function WelcomePage() {
<Title order={3}>Bloat-free feed reader</Title> <Title order={3}>Bloat-free feed reader</Title>
</Center> </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" /> <Divider my="xl" />
<Image src={image} /> <Image src={image} />
@@ -38,7 +59,7 @@ export function WelcomePage() {
} }
function Header() { function Header() {
const mobile = !useMediaQuery(`(min-width: ${Constants.layout.mobileBreakpoint})`) const mobile = useMobile()
if (mobile) { if (mobile) {
return ( return (
@@ -60,29 +81,14 @@ function Header() {
} }
function Buttons() { function Buttons() {
const iconSize = 18
const serverInfos = useAppSelector(state => state.server.serverInfos) const serverInfos = useAppSelector(state => state.server.serverInfos)
const { colorScheme, toggleColorScheme } = useMantineColorScheme() const { colorScheme, toggleColorScheme } = useMantineColorScheme()
const { isBrowserExtensionPopup, openSettingsPage } = useBrowserExtension()
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const dark = colorScheme === "dark"
const login = useAsyncCallback(client.user.login, {
onSuccess: () => {
dispatch(redirectToRootCategory())
},
})
return ( return (
<ButtonToolbar> <Group spacing={14}>
{serverInfos?.demoAccountEnabled && (
<ActionButton
label={<Trans>Try the demo!</Trans>}
icon={<TbClock size={iconSize} />}
variant="outline"
onClick={() => login.execute({ name: "demo", password: "demo" })}
showLabelOnMobile
/>
)}
<ActionButton <ActionButton
label={<Trans>Log in</Trans>} label={<Trans>Log in</Trans>}
icon={<TbKey size={iconSize} />} icon={<TbKey size={iconSize} />}
@@ -101,19 +107,30 @@ function Buttons() {
)} )}
<ActionButton <ActionButton
label={dark ? <Trans>Switch to light theme</Trans> : <Trans>Switch to dark theme</Trans>}
icon={colorScheme === "dark" ? <TbSun size={18} /> : <TbMoon size={iconSize} />} icon={colorScheme === "dark" ? <TbSun size={18} /> : <TbMoon size={iconSize} />}
onClick={() => toggleColorScheme()} onClick={() => toggleColorScheme()}
hideLabelOnDesktop
/> />
</ButtonToolbar>
{isBrowserExtensionPopup && (
<ActionButton
label={<Trans>Extension options</Trans>}
icon={<TbSettings size={iconSize} />}
onClick={() => openSettingsPage()}
hideLabelOnDesktop
/>
)}
</Group>
) )
} }
function Footer() { function Footer() {
const dispatch = useAppDispatch()
return ( return (
<Box> <Group position="apart">
<Group> <Group>
<span>© CommaFeed</span> <span>© CommaFeed</span>
<span> - </span>
<Anchor variant="text" href="https://github.com/Athou/commafeed/" target="_blank" rel="noreferrer"> <Anchor variant="text" href="https://github.com/Athou/commafeed/" target="_blank" rel="noreferrer">
<SiGithub /> <SiGithub />
</Anchor> </Anchor>
@@ -121,6 +138,11 @@ function Footer() {
<SiTwitter /> <SiTwitter />
</Anchor> </Anchor>
</Group> </Group>
</Box> <Box>
<Anchor variant="text" onClick={() => dispatch(redirectToApiDocumentation())}>
API documentation
</Anchor>
</Box>
</Group>
) )
} }

View File

@@ -1,6 +1,7 @@
import { Accordion, Tabs } from "@mantine/core" import { Accordion, Box, Tabs } from "@mantine/core"
import { client } from "app/client" import { client } from "app/client"
import { Loader } from "components/Loader" import { Loader } from "components/Loader"
import { Gauge } from "components/metrics/Gauge"
import { Meter } from "components/metrics/Meter" import { Meter } from "components/metrics/Meter"
import { MetricAccordionItem } from "components/metrics/MetricAccordionItem" import { MetricAccordionItem } from "components/metrics/MetricAccordionItem"
import { Timer } from "components/metrics/Timer" import { Timer } from "components/metrics/Timer"
@@ -15,11 +16,19 @@ const shownMeters: { [key: string]: string } = {
"com.commafeed.backend.feed.FeedRefreshUpdater.entryCacheMiss": "Entry cache miss rate", "com.commafeed.backend.feed.FeedRefreshUpdater.entryCacheMiss": "Entry cache miss rate",
} }
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() { export function MetricsPage() {
const query = useAsync(() => client.admin.getMetrics(), []) const query = useAsync(() => client.admin.getMetrics(), [])
if (!query.result) return <Loader /> if (!query.result) return <Loader />
const { meters, timers } = query.result.data const { meters, gauges, timers } = query.result.data
return ( return (
<Tabs defaultValue="stats"> <Tabs defaultValue="stats">
<Tabs.List> <Tabs.List>
@@ -39,6 +48,15 @@ export function MetricsPage() {
</MetricAccordionItem> </MetricAccordionItem>
))} ))}
</Accordion> </Accordion>
<Box pt="xs">
{Object.keys(shownGauges).map(g => (
<Box key={g}>
<span>{shownGauges[g]}&nbsp;</span>
<Gauge gauge={gauges[g]} />
</Box>
))}
</Box>
</Tabs.Panel> </Tabs.Panel>
<Tabs.Panel value="timers" pt="xs"> <Tabs.Panel value="timers" pt="xs">

View File

@@ -5,6 +5,7 @@ import { redirectToApiDocumentation } from "app/slices/redirect"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { CategorySelect } from "components/content/add/CategorySelect" import { CategorySelect } from "components/content/add/CategorySelect"
import { KeyboardShortcutsHelp } from "components/KeyboardShortcutsHelp" import { KeyboardShortcutsHelp } from "components/KeyboardShortcutsHelp"
import { useBrowserExtension } from "hooks/useBrowserExtension"
import React, { useState } from "react" import React, { useState } from "react"
import { TbHelp, TbKeyboard, TbPuzzle, TbRocket } from "react-icons/tb" import { TbHelp, TbKeyboard, TbPuzzle, TbRocket } from "react-icons/tb"
@@ -60,19 +61,26 @@ function NextUnreadBookmarklet() {
export function AboutPage() { export function AboutPage() {
const version = useAppSelector(state => state.server.serverInfos?.version) const version = useAppSelector(state => state.server.serverInfos?.version)
const revision = useAppSelector(state => state.server.serverInfos?.gitCommit) const revision = useAppSelector(state => state.server.serverInfos?.gitCommit)
const { isBrowserExtensionInstalled, browserExtensionVersion, isBrowserExtensionInstallable } = useBrowserExtension()
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
return ( return (
<Container size="xl"> <Container size="xl">
<SimpleGrid cols={2} breakpoints={[{ maxWidth: Constants.layout.mobileBreakpoint, cols: 1 }]}> <SimpleGrid cols={2} breakpoints={[{ maxWidth: Constants.layout.mobileBreakpoint, cols: 1 }]}>
<Section title={<Trans>About</Trans>} icon={<TbHelp size={24} />}> <Section title={<Trans>About</Trans>} icon={<TbHelp size={24} />}>
<Box> <Box>
<Trans> <Trans>
CommaFeed version {version} ({revision}) CommaFeed version {version} ({revision}).
</Trans> </Trans>
</Box> </Box>
{isBrowserExtensionInstallable && isBrowserExtensionInstalled && (
<Box>
<Trans>CommaFeed browser extension version {browserExtensionVersion}.</Trans>
</Box>
)}
<Box mt="md"> <Box mt="md">
<Trans> <Trans>
CommaFeed is an open-source project. Sources are hosted on&nbsp; <span>CommaFeed is an open-source project. Sources are hosted on </span>
<Anchor href="https://github.com/Athou/commafeed" target="_blank" rel="noreferrer"> <Anchor href="https://github.com/Athou/commafeed" target="_blank" rel="noreferrer">
GitHub GitHub
</Anchor> </Anchor>
@@ -86,28 +94,9 @@ export function AboutPage() {
<Section title={<Trans>Goodies</Trans>} icon={<TbPuzzle size={24} />}> <Section title={<Trans>Goodies</Trans>} icon={<TbPuzzle size={24} />}>
<List> <List>
<List.Item> <List.Item>
<Trans>Browser extentions</Trans> <Anchor href={Constants.browserExtensionUrl} target="_blank" rel="noreferrer">
<List withPadding> <Trans>Browser extention</Trans>
<List.Item> </Anchor>
<Anchor
href="https://addons.mozilla.org/en-US/firefox/addon/commafeed/"
target="_blank"
rel="noreferrer"
>
Firefox
</Anchor>
</List.Item>
<List.Item>
<Anchor href="https://github.com/Athou/commafeed-chrome" target="_blank" rel="noreferrer">
Chrome
</Anchor>
</List.Item>
<List.Item>
<Anchor href="https://github.com/Athou/commafeed-opera" target="_blank" rel="noreferrer">
Opera
</Anchor>
</List.Item>
</List>
</List.Item> </List.Item>
<List.Item> <List.Item>
<Trans>Subscribe URL</Trans> <Trans>Subscribe URL</Trans>

View File

@@ -1,8 +1,14 @@
import { Box } from "@mantine/core"
import SwaggerUI from "swagger-ui-react" import SwaggerUI from "swagger-ui-react"
import "swagger-ui-react/swagger-ui.css" import "swagger-ui-react/swagger-ui.css"
function ApiDocumentationPage() { 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 export default ApiDocumentationPage

View File

@@ -13,10 +13,9 @@ import {
Title, Title,
useMantineTheme, useMantineTheme,
} from "@mantine/core" } from "@mantine/core"
import { useViewportSize } from "@mantine/hooks"
import { Constants } from "app/constants" import { Constants } from "app/constants"
import { redirectToAdd, redirectToRootCategory } from "app/slices/redirect" 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 { reloadProfile, reloadSettings, reloadTags } from "app/slices/user"
import { useAppDispatch, useAppSelector } from "app/store" import { useAppDispatch, useAppSelector } from "app/store"
import { Loader } from "components/Loader" import { Loader } from "components/Loader"
@@ -24,30 +23,42 @@ import { Logo } from "components/Logo"
import { OnDesktop } from "components/responsive/OnDesktop" import { OnDesktop } from "components/responsive/OnDesktop"
import { OnMobile } from "components/responsive/OnMobile" import { OnMobile } from "components/responsive/OnMobile"
import { useAppLoading } from "hooks/useAppLoading" import { useAppLoading } from "hooks/useAppLoading"
import { useMobile } from "hooks/useMobile"
import { useWebSocket } from "hooks/useWebSocket" import { useWebSocket } from "hooks/useWebSocket"
import { LoadingPage } from "pages/LoadingPage" import { LoadingPage } from "pages/LoadingPage"
import { Resizable } from "re-resizable"
import { ReactNode, Suspense, useEffect } from "react" import { ReactNode, Suspense, useEffect } from "react"
import { TbPlus } from "react-icons/tb" import { TbPlus } from "react-icons/tb"
import { Outlet } from "react-router-dom" import { Outlet } from "react-router-dom"
interface LayoutProps { interface LayoutProps {
sidebar: ReactNode sidebar: ReactNode
sidebarWidth: number
header: ReactNode header: ReactNode
} }
const sidebarPadding = DEFAULT_THEME.spacing.xs const sidebarPadding = DEFAULT_THEME.spacing.xs
const sidebarRightBorderWidth = "1px" 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: { sidebarContent: {
maxWidth: `calc(${Constants.layout.sidebarWidth}px - ${sidebarPadding} * 2 - ${sidebarRightBorderWidth})`, maxWidth: `calc(${props.sidebarWidth}px - ${sidebarPadding} * 2 - ${sidebarRightBorderWidth})`,
[theme.fn.smallerThan(Constants.layout.mobileBreakpoint)]: { [theme.fn.smallerThan(Constants.layout.mobileBreakpoint)]: {
maxWidth: `calc(100vw - ${sidebarPadding} * 2 - ${sidebarRightBorderWidth})`, maxWidth: `calc(100vw - ${sidebarPadding} * 2 - ${sidebarRightBorderWidth})`,
}, },
}, },
mainContentWrapper: { mainContentWrapper: {
paddingTop: Constants.layout.headerHeight, paddingTop: Constants.layout.headerHeight,
paddingLeft: Constants.layout.sidebarWidth, paddingLeft: props.sidebarWidth,
paddingRight: 0, paddingRight: 0,
paddingBottom: 0, paddingBottom: 0,
[theme.fn.smallerThan(Constants.layout.mobileBreakpoint)]: { [theme.fn.smallerThan(Constants.layout.mobileBreakpoint)]: {
@@ -55,7 +66,7 @@ const useStyles = createStyles(theme => ({
}, },
}, },
mainContent: { mainContent: {
maxWidth: `calc(100vw - ${Constants.layout.sidebarWidth}px)`, maxWidth: `calc(100vw - ${props.sidebarWidth}px)`,
padding: theme.spacing.md, padding: theme.spacing.md,
[theme.fn.smallerThan(Constants.layout.mobileBreakpoint)]: { [theme.fn.smallerThan(Constants.layout.mobileBreakpoint)]: {
maxWidth: "100vw", maxWidth: "100vw",
@@ -76,15 +87,18 @@ function LogoAndTitle() {
) )
} }
export default function Layout({ sidebar, header }: LayoutProps) { export default function Layout(props: LayoutProps) {
const { classes } = useStyles() const { classes } = useStyles(props)
const theme = useMantineTheme() const theme = useMantineTheme()
const viewport = useViewportSize()
const { loading } = useAppLoading() const { loading } = useAppLoading()
const mobile = useMobile()
const mobileMenuOpen = useAppSelector(state => state.tree.mobileMenuOpen) const mobileMenuOpen = useAppSelector(state => state.tree.mobileMenuOpen)
const sidebarHidden = props.sidebarWidth === 0
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
useWebSocket() useWebSocket()
const handleResize = (element: HTMLElement) => dispatch(setSidebarWidth(element.offsetWidth))
useEffect(() => { useEffect(() => {
dispatch(reloadSettings()) dispatch(reloadSettings())
dispatch(reloadProfile()) dispatch(reloadProfile())
@@ -122,13 +136,29 @@ export default function Layout({ sidebar, header }: LayoutProps) {
navbar={ navbar={
<Navbar <Navbar
id="sidebar" id="sidebar"
p={sidebarPadding} hiddenBreakpoint={sidebarHidden ? 99999999 : Constants.layout.mobileBreakpoint}
hiddenBreakpoint={Constants.layout.mobileBreakpoint} hidden={sidebarHidden || !mobileMenuOpen}
hidden={!mobileMenuOpen} width={{ md: props.sidebarWidth }}
width={{ md: Constants.layout.sidebarWidth }} className={classes.sidebar}
> >
<Navbar.Section grow component={ScrollArea} mx="-xs" px="xs"> <Navbar.Section grow component={ScrollArea} mx={mobile ? 0 : "-sm"} px={mobile ? 0 : "sm"}>
<Box className={classes.sidebarContent}>{sidebar}</Box> <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.Section>
</Navbar> </Navbar>
} }
@@ -146,37 +176,30 @@ export default function Layout({ sidebar, header }: LayoutProps) {
)} )}
{!mobileMenuOpen && ( {!mobileMenuOpen && (
<Group> <Group>
<Box mr="sm">{burger}</Box> <Box>{burger}</Box>
<Box sx={{ flexGrow: 1 }}>{header}</Box> <Box sx={{ flexGrow: 1 }}>{props.header}</Box>
</Group> </Group>
)} )}
</OnMobile> </OnMobile>
<OnDesktop> <OnDesktop>
<Group> <Group>
<Group position="apart" sx={{ width: Constants.layout.sidebarWidth - 16 }}> <Group position="apart" sx={{ width: props.sidebarWidth - 16 }}>
<Box> <Box>
<LogoAndTitle /> <LogoAndTitle />
</Box> </Box>
<Box>{addButton}</Box> <Box>{addButton}</Box>
</Group> </Group>
<Box sx={{ flexGrow: 1 }}>{header}</Box> <Box sx={{ flexGrow: 1 }}>{props.header}</Box>
</Group> </Group>
</OnDesktop> </OnDesktop>
</Header> </Header>
} }
> >
<ScrollArea <Box id="content" className={classes.mainContent}>
sx={{ height: viewport.height - Constants.layout.headerHeight }} <Suspense fallback={<Loader />}>
viewportRef={ref => { <Outlet />
if (ref) ref.id = Constants.dom.mainScrollAreaId </Suspense>
}} </Box>
>
<Box id="content" className={classes.mainContent}>
<Suspense fallback={<Loader />}>
<Outlet />
</Suspense>
</Box>
</ScrollArea>
</AppShell> </AppShell>
) )
} }

View File

@@ -8,7 +8,7 @@ import { TbCode, TbPhoto, TbUser } from "react-icons/tb"
export function SettingsPage() { export function SettingsPage() {
return ( return (
<Container size="sm" px={0}> <Container size="sm" px={0}>
<Tabs defaultValue="display"> <Tabs defaultValue="display" keepMounted={false}>
<Tabs.List> <Tabs.List>
<Tabs.Tab value="display" icon={<TbPhoto size={16} />}> <Tabs.Tab value="display" icon={<TbPhoto size={16} />}>
<Trans>Display</Trans> <Trans>Display</Trans>

View File

@@ -1,13 +1,41 @@
import { lingui } from "@lingui/vite-plugin" import { lingui } from "@lingui/vite-plugin"
import react from "@vitejs/plugin-react" import react from "@vitejs/plugin-react"
import { visualizer } from "rollup-plugin-visualizer" import { visualizer } from "rollup-plugin-visualizer"
import { defineConfig } from "vite" import { defineConfig, PluginOption } from "vite"
import eslint from "vite-plugin-eslint" import eslint from "vite-plugin-eslint"
import tsconfigPaths from "vite-tsconfig-paths" 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/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
customCodeInjector,
react({ react({
babel: { babel: {
// babel-macro is needed for lingui // babel-macro is needed for lingui
@@ -24,6 +52,7 @@ export default defineConfig({
port: 8082, port: 8082,
proxy: { proxy: {
"/rest": "http://localhost:8083", "/rest": "http://localhost:8083",
"/next": "http://localhost:8083",
"/ws": "ws://localhost:8083", "/ws": "ws://localhost:8083",
"/swagger": "http://localhost:8083", "/swagger": "http://localhost:8083",
"/custom_css.css": "http://localhost:8083", "/custom_css.css": "http://localhost:8083",
@@ -31,7 +60,7 @@ export default defineConfig({
}, },
}, },
build: { build: {
chunkSizeWarningLimit: 1000, chunkSizeWarningLimit: 3000,
rollupOptions: { rollupOptions: {
output: { output: {
manualChunks: id => { manualChunks: id => {

View File

@@ -3,9 +3,15 @@
app: app:
# url used to access commafeed # url used to access commafeed
publicUrl: http://localhost:8082/ publicUrl: http://localhost:8082/
# whether to expose a robots.txt file that disallows web crawlers and search engine indexers
hideFromWebCrawlers: true
# wether to allow user registrations # whether to allow user registrations
allowRegistrations: true allowRegistrations: true
# whether to enable strict password validation (1 uppercase char, 1 lowercase char, 1 digit, 1 special char)
strictPasswordPolicy: true
# create a demo account the first time the app starts # create a demo account the first time the app starts
createDemoAccount: true createDemoAccount: true
@@ -37,14 +43,14 @@ app:
graphitePort: 2003 graphitePort: 2003
graphiteInterval: 60 graphiteInterval: 60
# wether this commafeed instance has a lot of feeds to refresh # whether this commafeed instance has a lot of feeds to refresh
# leave this to false in almost all cases # leave this to false in almost all cases
heavyLoad: false heavyLoad: false
# minimum amount of time commafeed will wait before refreshing the same feed # minimum amount of time commafeed will wait before refreshing the same feed
refreshIntervalMinutes: 5 refreshIntervalMinutes: 5
# wether to enable pubsub # whether to enable pubsub
# probably not needed if refreshIntervalMinutes is low # probably not needed if refreshIntervalMinutes is low
pubsubhubbub: false pubsubhubbub: false
@@ -60,7 +66,10 @@ app:
# entries to keep per feed, old entries will be deleted, 0 to disable # entries to keep per feed, old entries will be deleted, 0 to disable
maxFeedCapacity: 500 maxFeedCapacity: 500
# limit the number of feeds a user can subscribe to, 0 to disable
maxFeedsPerUser: 0
# cache service to use, possible values are 'noop' and 'redis' # cache service to use, possible values are 'noop' and 'redis'
cache: noop cache: noop
@@ -86,7 +95,7 @@ app:
database: database:
driverClass: org.h2.Driver driverClass: org.h2.Driver
url: jdbc:h2:./target/example url: jdbc:h2:./target/commafeed
user: sa user: sa
password: sa password: sa
properties: properties:
@@ -108,7 +117,6 @@ logging:
liquibase: INFO liquibase: INFO
org.hibernate.SQL: INFO # or ALL for sql debugging org.hibernate.SQL: INFO # or ALL for sql debugging
org.hibernate.engine.internal.StatisticalLoggingSessionEventListener: WARN org.hibernate.engine.internal.StatisticalLoggingSessionEventListener: WARN
org.hibernate.orm.deprecation: "OFF"
appenders: appenders:
- type: console - type: console
- type: file - type: file

View File

@@ -3,9 +3,15 @@
app: app:
# url used to access commafeed # url used to access commafeed
publicUrl: http://localhost:8082/ 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 # whether to allow user registrations
allowRegistrations: false 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 # create a demo account the first time the app starts
createDemoAccount: false createDemoAccount: false
@@ -38,14 +44,14 @@ app:
graphitePort: 2003 graphitePort: 2003
graphiteInterval: 60 graphiteInterval: 60
# wether this commafeed instance has a lot of feeds to refresh # whether this commafeed instance has a lot of feeds to refresh
# leave this to false in almost all cases # leave this to false in almost all cases
heavyLoad: false heavyLoad: false
# minimum amount of time commafeed will wait before refreshing the same feed # minimum amount of time commafeed will wait before refreshing the same feed
refreshIntervalMinutes: 5 refreshIntervalMinutes: 5
# wether to enable pubsub # whether to enable pubsub
# probably not needed if refreshIntervalMinutes is low # probably not needed if refreshIntervalMinutes is low
pubsubhubbub: false pubsubhubbub: false
@@ -61,7 +67,10 @@ app:
# entries to keep per feed, old entries will be deleted, 0 to disable # entries to keep per feed, old entries will be deleted, 0 to disable
maxFeedCapacity: 500 maxFeedCapacity: 500
# limit the number of feeds a user can subscribe to, 0 to disable
maxFeedsPerUser: 0
# cache service to use, possible values are 'noop' and 'redis' # cache service to use, possible values are 'noop' and 'redis'
cache: noop cache: noop
@@ -87,7 +96,7 @@ app:
database: database:
driverClass: org.h2.Driver driverClass: org.h2.Driver
url: jdbc:h2:/commafeed/data/db url: jdbc:h2:./db/commafeed
user: sa user: sa
password: sa password: sa
properties: properties:
@@ -113,7 +122,6 @@ logging:
com.commafeed: INFO com.commafeed: INFO
liquibase: INFO liquibase: INFO
io.dropwizard.server.ServerFactory: INFO io.dropwizard.server.ServerFactory: INFO
org.hibernate.orm.deprecation: "OFF"
appenders: appenders:
- type: console - type: console
- type: file - type: file

View File

@@ -6,7 +6,7 @@
<parent> <parent>
<groupId>com.commafeed</groupId> <groupId>com.commafeed</groupId>
<artifactId>commafeed</artifactId> <artifactId>commafeed</artifactId>
<version>3.4.0</version> <version>3.8.1</version>
</parent> </parent>
<artifactId>commafeed-server</artifactId> <artifactId>commafeed-server</artifactId>
<name>CommaFeed Server</name> <name>CommaFeed Server</name>
@@ -226,7 +226,7 @@
<dependency> <dependency>
<groupId>com.commafeed</groupId> <groupId>com.commafeed</groupId>
<artifactId>commafeed-client</artifactId> <artifactId>commafeed-client</artifactId>
<version>3.4.0</version> <version>3.8.1</version>
</dependency> </dependency>
<dependency> <dependency>
@@ -278,11 +278,6 @@
<groupId>io.dropwizard.metrics</groupId> <groupId>io.dropwizard.metrics</groupId>
<artifactId>metrics-json</artifactId> <artifactId>metrics-json</artifactId>
</dependency> </dependency>
<dependency>
<groupId>io.dropwizard.modules</groupId>
<artifactId>dropwizard-web</artifactId>
<version>1.5.0</version>
</dependency>
<dependency> <dependency>
<groupId>be.tomcools</groupId> <groupId>be.tomcools</groupId>
<artifactId>dropwizard-websocket-jee7-bundle</artifactId> <artifactId>dropwizard-websocket-jee7-bundle</artifactId>
@@ -374,7 +369,7 @@
<dependency> <dependency>
<groupId>redis.clients</groupId> <groupId>redis.clients</groupId>
<artifactId>jedis</artifactId> <artifactId>jedis</artifactId>
<version>4.3.2</version> <version>4.4.1</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.sun.mail</groupId> <groupId>com.sun.mail</groupId>

View File

@@ -33,6 +33,7 @@ import com.commafeed.backend.model.UserSettings;
import com.commafeed.backend.service.DatabaseStartupService; import com.commafeed.backend.service.DatabaseStartupService;
import com.commafeed.backend.service.UserService; import com.commafeed.backend.service.UserService;
import com.commafeed.backend.task.ScheduledTask; import com.commafeed.backend.task.ScheduledTask;
import com.commafeed.frontend.auth.PasswordConstraintValidator;
import com.commafeed.frontend.auth.SecurityCheckFactoryProvider; import com.commafeed.frontend.auth.SecurityCheckFactoryProvider;
import com.commafeed.frontend.resource.AdminREST; import com.commafeed.frontend.resource.AdminREST;
import com.commafeed.frontend.resource.CategoryREST; import com.commafeed.frontend.resource.CategoryREST;
@@ -46,6 +47,7 @@ import com.commafeed.frontend.servlet.CustomCssServlet;
import com.commafeed.frontend.servlet.CustomJsServlet; import com.commafeed.frontend.servlet.CustomJsServlet;
import com.commafeed.frontend.servlet.LogoutServlet; import com.commafeed.frontend.servlet.LogoutServlet;
import com.commafeed.frontend.servlet.NextUnreadServlet; import com.commafeed.frontend.servlet.NextUnreadServlet;
import com.commafeed.frontend.servlet.RobotsTxtDisallowAllServlet;
import com.commafeed.frontend.session.SessionHelperFactoryProvider; import com.commafeed.frontend.session.SessionHelperFactoryProvider;
import com.commafeed.frontend.ws.WebSocketConfigurator; import com.commafeed.frontend.ws.WebSocketConfigurator;
import com.commafeed.frontend.ws.WebSocketEndpoint; import com.commafeed.frontend.ws.WebSocketEndpoint;
@@ -69,8 +71,6 @@ import io.dropwizard.migrations.MigrationsBundle;
import io.dropwizard.servlets.CacheBustingFilter; import io.dropwizard.servlets.CacheBustingFilter;
import io.dropwizard.setup.Bootstrap; import io.dropwizard.setup.Bootstrap;
import io.dropwizard.setup.Environment; import io.dropwizard.setup.Environment;
import io.dropwizard.web.WebBundle;
import io.dropwizard.web.conf.WebConfiguration;
import io.whitfin.dropwizard.configuration.EnvironmentSubstitutor; import io.whitfin.dropwizard.configuration.EnvironmentSubstitutor;
public class CommaFeedApplication extends Application<CommaFeedConfiguration> { public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
@@ -118,24 +118,16 @@ public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
public DataSourceFactory getDataSourceFactory(CommaFeedConfiguration configuration) { public DataSourceFactory getDataSourceFactory(CommaFeedConfiguration configuration) {
DataSourceFactory factory = configuration.getDataSourceFactory(); DataSourceFactory factory = configuration.getDataSourceFactory();
// keep using old id generator for backward compatibility factory.getProperties().put(AvailableSettings.PREFERRED_POOLED_OPTIMIZER, "pooled-lo");
factory.getProperties().put(AvailableSettings.USE_NEW_ID_GENERATOR_MAPPINGS, "false");
factory.getProperties().put(AvailableSettings.STATEMENT_BATCH_SIZE, "50"); factory.getProperties().put(AvailableSettings.STATEMENT_BATCH_SIZE, "50");
factory.getProperties().put(AvailableSettings.BATCH_VERSIONED_DATA, "true"); factory.getProperties().put(AvailableSettings.BATCH_VERSIONED_DATA, "true");
factory.getProperties().put(AvailableSettings.ORDER_INSERTS, "true");
factory.getProperties().put(AvailableSettings.ORDER_UPDATES, "true");
return factory; return factory;
} }
}); });
bootstrap.addBundle(new WebBundle<CommaFeedConfiguration>() {
@Override
public WebConfiguration getWebConfiguration(CommaFeedConfiguration configuration) {
WebConfiguration config = new WebConfiguration();
config.getFrameOptionsHeaderFactory().setEnabled(true);
return config;
}
});
bootstrap.addBundle(new MigrationsBundle<CommaFeedConfiguration>() { bootstrap.addBundle(new MigrationsBundle<CommaFeedConfiguration>() {
@Override @Override
public DataSourceFactory getDataSourceFactory(CommaFeedConfiguration configuration) { public DataSourceFactory getDataSourceFactory(CommaFeedConfiguration configuration) {
@@ -149,6 +141,8 @@ public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
@Override @Override
public void run(CommaFeedConfiguration config, Environment environment) throws Exception { public void run(CommaFeedConfiguration config, Environment environment) throws Exception {
PasswordConstraintValidator.setStrict(config.getApplicationSettings().getStrictPasswordPolicy());
// guice init // guice init
Injector injector = Guice.createInjector(new CommaFeedModule(hibernateBundle.getSessionFactory(), config, environment.metrics())); Injector injector = Guice.createInjector(new CommaFeedModule(hibernateBundle.getSessionFactory(), config, environment.metrics()));
@@ -176,6 +170,11 @@ public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
environment.servlets().addServlet("customCss", injector.getInstance(CustomCssServlet.class)).addMapping("/custom_css.css"); 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("customJs", injector.getInstance(CustomJsServlet.class)).addMapping("/custom_js.js");
environment.servlets().addServlet("analytics.js", injector.getInstance(AnalyticsServlet.class)).addMapping("/analytics.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 // WebSocket endpoint
ServerEndpointConfig serverEndpointConfig = ServerEndpointConfig.Builder.create(WebSocketEndpoint.class, "/ws") ServerEndpointConfig serverEndpointConfig = ServerEndpointConfig.Builder.create(WebSocketEndpoint.class, "/ws")

View File

@@ -17,8 +17,10 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import io.dropwizard.Configuration; import io.dropwizard.Configuration;
import io.dropwizard.db.DataSourceFactory; import io.dropwizard.db.DataSourceFactory;
import lombok.Getter; import lombok.Getter;
import lombok.Setter;
@Getter @Getter
@Setter
public class CommaFeedConfiguration extends Configuration { public class CommaFeedConfiguration extends Configuration {
public enum CacheType { public enum CacheType {
@@ -56,16 +58,25 @@ public class CommaFeedConfiguration extends Configuration {
} }
@Getter @Getter
@Setter
public static class ApplicationSettings { public static class ApplicationSettings {
@NotNull @NotNull
@NotBlank @NotBlank
@Valid @Valid
private String publicUrl; private String publicUrl;
@NotNull
@Valid
private Boolean hideFromWebCrawlers = true;
@NotNull @NotNull
@Valid @Valid
private Boolean allowRegistrations; private Boolean allowRegistrations;
@NotNull
@Valid
private Boolean strictPasswordPolicy = true;
@NotNull @NotNull
@Valid @Valid
private Boolean createDemoAccount; private Boolean createDemoAccount;
@@ -124,6 +135,10 @@ public class CommaFeedConfiguration extends Configuration {
@Valid @Valid
private Integer maxFeedCapacity; private Integer maxFeedCapacity;
@NotNull
@Valid
private Integer maxFeedsPerUser = 0;
@NotNull @NotNull
@Min(0) @Min(0)
@Valid @Valid

View File

@@ -12,7 +12,7 @@ import java.util.List;
*/ */
public class FixedSizeSortedSet<E> { public class FixedSizeSortedSet<E> {
private List<E> inner; private final List<E> inner;
private final Comparator<? super E> comparator; private final Comparator<? super E> comparator;
private final int capacity; private final int capacity;

View File

@@ -18,7 +18,7 @@ import com.querydsl.core.types.Predicate;
@Singleton @Singleton
public class FeedCategoryDAO extends GenericDAO<FeedCategory> { public class FeedCategoryDAO extends GenericDAO<FeedCategory> {
private QFeedCategory category = QFeedCategory.feedCategory; private final QFeedCategory category = QFeedCategory.feedCategory;
@Inject @Inject
public FeedCategoryDAO(SessionFactory sessionFactory) { public FeedCategoryDAO(SessionFactory sessionFactory) {

View File

@@ -13,15 +13,15 @@ import org.hibernate.SessionFactory;
import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.QFeed; import com.commafeed.backend.model.QFeed;
import com.commafeed.backend.model.QFeedSubscription; import com.commafeed.backend.model.QFeedSubscription;
import com.commafeed.backend.model.QUser;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.querydsl.jpa.JPAExpressions; import com.querydsl.jpa.JPAExpressions;
import com.querydsl.jpa.JPQLQuery; import com.querydsl.jpa.impl.JPAQuery;
@Singleton @Singleton
public class FeedDAO extends GenericDAO<Feed> { public class FeedDAO extends GenericDAO<Feed> {
private final QFeed feed = QFeed.feed; private final QFeed feed = QFeed.feed;
private final QFeedSubscription subscription = QFeedSubscription.feedSubscription;
@Inject @Inject
public FeedDAO(SessionFactory sessionFactory) { public FeedDAO(SessionFactory sessionFactory) {
@@ -29,14 +29,13 @@ public class FeedDAO extends GenericDAO<Feed> {
} }
public List<Feed> findNextUpdatable(int count, Date lastLoginThreshold) { public List<Feed> findNextUpdatable(int count, Date lastLoginThreshold) {
JPQLQuery<Feed> query = query().selectFrom(feed); JPAQuery<Feed> query = query().selectFrom(feed).where(feed.disabledUntil.isNull().or(feed.disabledUntil.lt(new Date())));
query.where(feed.disabledUntil.isNull().or(feed.disabledUntil.lt(new Date())));
if (lastLoginThreshold != null) { if (lastLoginThreshold != null) {
QFeedSubscription subs = QFeedSubscription.feedSubscription; query.where(JPAExpressions.selectOne()
QUser user = QUser.user; .from(subscription)
.join(subscription.user)
query.join(subs).on(subs.feed.id.eq(feed.id)).join(subs.user, user).where(user.lastLogin.gt(lastLoginThreshold)); .where(subscription.feed.id.eq(feed.id), subscription.user.lastLogin.gt(lastLoginThreshold))
.exists());
} }
return query.orderBy(feed.disabledUntil.asc()).limit(count).fetch(); return query.orderBy(feed.disabledUntil.asc()).limit(count).fetch();

View File

@@ -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(); 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)); 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(); return deleteQuery(content).where(content.id.in(ids)).execute();
delete(list);
return deleted;
} }
} }

View File

@@ -21,7 +21,7 @@ import lombok.Getter;
@Singleton @Singleton
public class FeedEntryDAO extends GenericDAO<FeedEntry> { public class FeedEntryDAO extends GenericDAO<FeedEntry> {
private QFeedEntry entry = QFeedEntry.feedEntry; private final QFeedEntry entry = QFeedEntry.feedEntry;
@Inject @Inject
public FeedEntryDAO(SessionFactory sessionFactory) { public FeedEntryDAO(SessionFactory sessionFactory) {
@@ -48,7 +48,6 @@ public class FeedEntryDAO extends GenericDAO<FeedEntry> {
} }
public int delete(Long feedId, long max) { public int delete(Long feedId, long max) {
List<FeedEntry> list = query().selectFrom(entry).where(entry.feed.id.eq(feedId)).limit(max).fetch(); List<FeedEntry> list = query().selectFrom(entry).where(entry.feed.id.eq(feedId)).limit(max).fetch();
return delete(list); return delete(list);
} }

View File

@@ -77,7 +77,7 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
private FeedEntryStatus handleStatus(User user, FeedEntryStatus status, FeedSubscription sub, FeedEntry entry) { private FeedEntryStatus handleStatus(User user, FeedEntryStatus status, FeedSubscription sub, FeedEntry entry) {
if (status == null) { if (status == null) {
Date unreadThreshold = config.getApplicationSettings().getUnreadThreshold(); Date unreadThreshold = config.getApplicationSettings().getUnreadThreshold();
boolean read = unreadThreshold == null ? false : entry.getUpdated().before(unreadThreshold); boolean read = unreadThreshold != null && entry.getUpdated().before(unreadThreshold);
status = new FeedEntryStatus(user, sub, entry); status = new FeedEntryStatus(user, sub, entry);
status.setRead(read); status.setRead(read);
status.setMarkable(!read); status.setMarkable(!read);
@@ -270,8 +270,13 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
return results; return results;
} }
public List<FeedEntryStatus> getOldStatuses(Date olderThan, int limit) { public long deleteOldStatuses(Date olderThan, int limit) {
return query().selectFrom(status).where(status.entryInserted.lt(olderThan), status.starred.isFalse()).limit(limit).fetch(); 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();
} }
} }

View File

@@ -15,7 +15,7 @@ import com.commafeed.backend.model.User;
@Singleton @Singleton
public class FeedEntryTagDAO extends GenericDAO<FeedEntryTag> { public class FeedEntryTagDAO extends GenericDAO<FeedEntryTag> {
private QFeedEntryTag tag = QFeedEntryTag.feedEntryTag; private final QFeedEntryTag tag = QFeedEntryTag.feedEntryTag;
@Inject @Inject
public FeedEntryTagDAO(SessionFactory sessionFactory) { public FeedEntryTagDAO(SessionFactory sessionFactory) {

View File

@@ -21,7 +21,7 @@ import com.querydsl.jpa.JPQLQuery;
@Singleton @Singleton
public class FeedSubscriptionDAO extends GenericDAO<FeedSubscription> { public class FeedSubscriptionDAO extends GenericDAO<FeedSubscription> {
private QFeedSubscription sub = QFeedSubscription.feedSubscription; private final QFeedSubscription sub = QFeedSubscription.feedSubscription;
@Inject @Inject
public FeedSubscriptionDAO(SessionFactory sessionFactory) { public FeedSubscriptionDAO(SessionFactory sessionFactory) {
@@ -59,6 +59,10 @@ public class FeedSubscriptionDAO extends GenericDAO<FeedSubscription> {
return initRelations(subs); return initRelations(subs);
} }
public Long count(User user) {
return query().select(sub.count()).from(sub).where(sub.user.eq(user)).fetchOne();
}
public List<FeedSubscription> findByCategory(User user, FeedCategory category) { public List<FeedSubscription> findByCategory(User user, FeedCategory category) {
JPQLQuery<FeedSubscription> query = query().selectFrom(sub).where(sub.user.eq(user)); JPQLQuery<FeedSubscription> query = query().selectFrom(sub).where(sub.user.eq(user));
if (category == null) { if (category == null) {

View File

@@ -7,6 +7,7 @@ import org.hibernate.annotations.QueryHints;
import com.commafeed.backend.model.AbstractModel; import com.commafeed.backend.model.AbstractModel;
import com.querydsl.core.types.EntityPath; import com.querydsl.core.types.EntityPath;
import com.querydsl.jpa.impl.JPADeleteClause;
import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory; import com.querydsl.jpa.impl.JPAQueryFactory;
import com.querydsl.jpa.impl.JPAUpdateClause; import com.querydsl.jpa.impl.JPAUpdateClause;
@@ -15,7 +16,7 @@ import io.dropwizard.hibernate.AbstractDAO;
public abstract class GenericDAO<T extends AbstractModel> extends AbstractDAO<T> { public abstract class GenericDAO<T extends AbstractModel> extends AbstractDAO<T> {
private JPAQueryFactory factory; private final JPAQueryFactory factory;
protected GenericDAO(SessionFactory sessionFactory) { protected GenericDAO(SessionFactory sessionFactory) {
super(sessionFactory); super(sessionFactory);
@@ -30,6 +31,10 @@ public abstract class GenericDAO<T extends AbstractModel> extends AbstractDAO<T>
return new JPAUpdateClause(currentSession(), entityPath); return new JPAUpdateClause(currentSession(), entityPath);
} }
protected JPADeleteClause deleteQuery(EntityPath<T> entityPath) {
return new JPADeleteClause(currentSession(), entityPath);
}
public void saveOrUpdate(T model) { public void saveOrUpdate(T model) {
persist(model); persist(model);
} }

View File

@@ -1,53 +1,63 @@
package com.commafeed.backend.dao; package com.commafeed.backend.dao;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.hibernate.Session; import org.hibernate.Session;
import org.hibernate.SessionFactory; import org.hibernate.SessionFactory;
import org.hibernate.Transaction; import org.hibernate.Transaction;
import org.hibernate.context.internal.ManagedSessionContext; import org.hibernate.context.internal.ManagedSessionContext;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
@Singleton
public class UnitOfWork { public class UnitOfWork {
public static void run(SessionFactory sessionFactory, SessionRunner sessionRunner) { private final SessionFactory sessionFactory;
call(sessionFactory, () -> {
public void run(SessionRunner sessionRunner) {
call(() -> {
sessionRunner.runInSession(); sessionRunner.runInSession();
return null; return null;
}); });
} }
public static <T> T call(SessionFactory sessionFactory, SessionRunnerReturningValue<T> sessionRunner) { public <T> T call(SessionRunnerReturningValue<T> sessionRunner) {
final Session session = sessionFactory.openSession();
if (ManagedSessionContext.hasBind(sessionFactory)) {
throw new IllegalStateException("Already in a unit of work!");
}
T t = null; T t = null;
try {
ManagedSessionContext.bind(session); boolean sessionAlreadyBound = ManagedSessionContext.hasBind(sessionFactory);
session.beginTransaction(); try (Session session = sessionFactory.openSession()) {
if (!sessionAlreadyBound) {
ManagedSessionContext.bind(session);
}
Transaction tx = session.beginTransaction();
try { try {
t = sessionRunner.runInSession(); t = sessionRunner.runInSession();
commitTransaction(session); commitTransaction(tx);
} catch (Exception e) { } catch (Exception e) {
rollbackTransaction(session); rollbackTransaction(tx);
UnitOfWork.<RuntimeException> rethrow(e); UnitOfWork.rethrow(e);
} }
} finally { } finally {
session.close(); if (!sessionAlreadyBound) {
ManagedSessionContext.unbind(sessionFactory); ManagedSessionContext.unbind(sessionFactory);
}
} }
return t; return t;
} }
private static void rollbackTransaction(Session session) { private static void rollbackTransaction(Transaction tx) {
final Transaction txn = session.getTransaction(); if (tx != null && tx.isActive()) {
if (txn != null && txn.isActive()) { tx.rollback();
txn.rollback();
} }
} }
private static void commitTransaction(Session session) { private static void commitTransaction(Transaction tx) {
final Transaction txn = session.getTransaction(); if (tx != null && tx.isActive()) {
if (txn != null && txn.isActive()) { tx.commit();
txn.commit();
} }
} }

View File

@@ -11,7 +11,7 @@ import com.commafeed.backend.model.User;
@Singleton @Singleton
public class UserDAO extends GenericDAO<User> { public class UserDAO extends GenericDAO<User> {
private QUser user = QUser.user; private final QUser user = QUser.user;
@Inject @Inject
public UserDAO(SessionFactory sessionFactory) { public UserDAO(SessionFactory sessionFactory) {

View File

@@ -17,7 +17,7 @@ import com.commafeed.backend.model.UserRole.Role;
@Singleton @Singleton
public class UserRoleDAO extends GenericDAO<UserRole> { public class UserRoleDAO extends GenericDAO<UserRole> {
private QUserRole role = QUserRole.userRole; private final QUserRole role = QUserRole.userRole;
@Inject @Inject
public UserRoleDAO(SessionFactory sessionFactory) { public UserRoleDAO(SessionFactory sessionFactory) {

View File

@@ -12,7 +12,7 @@ import com.commafeed.backend.model.UserSettings;
@Singleton @Singleton
public class UserSettingsDAO extends GenericDAO<UserSettings> { public class UserSettingsDAO extends GenericDAO<UserSettings> {
private QUserSettings settings = QUserSettings.userSettings; private final QUserSettings settings = QUserSettings.userSettings;
@Inject @Inject
public UserSettingsDAO(SessionFactory sessionFactory) { public UserSettingsDAO(SessionFactory sessionFactory) {

View File

@@ -16,7 +16,7 @@ import lombok.RequiredArgsConstructor;
public class FeedEntryKeyword { public class FeedEntryKeyword {
public enum Mode { public enum Mode {
INCLUDE, EXCLUDE; INCLUDE, EXCLUDE
} }
private final String keyword; private final String keyword;

View File

@@ -73,7 +73,7 @@ public class FeedFetcher {
boolean etagHeaderValueChanged = !StringUtils.equals(eTag, result.getETag()); boolean etagHeaderValueChanged = !StringUtils.equals(eTag, result.getETag());
String hash = DigestUtils.sha1Hex(content); String hash = DigestUtils.sha1Hex(content);
if (lastContentHash != null && hash != null && lastContentHash.equals(hash)) { if (lastContentHash != null && lastContentHash.equals(hash)) {
log.debug("content hash not modified: {}", feedUrl); log.debug("content hash not modified: {}", feedUrl);
throw new NotModifiedException("content hash not modified", throw new NotModifiedException("content hash not modified",
lastModifiedHeaderValueChanged ? result.getLastModifiedSince() : null, lastModifiedHeaderValueChanged ? result.getLastModifiedSince() : null,

View File

@@ -16,8 +16,8 @@ import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import org.apache.commons.lang3.time.DateUtils; import org.apache.commons.lang3.time.DateUtils;
import org.hibernate.SessionFactory;
import com.codahale.metrics.Gauge;
import com.codahale.metrics.Meter; import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.MetricRegistry;
import com.commafeed.CommaFeedConfiguration; import com.commafeed.CommaFeedConfiguration;
@@ -33,7 +33,7 @@ import lombok.extern.slf4j.Slf4j;
@Singleton @Singleton
public class FeedRefreshEngine implements Managed { public class FeedRefreshEngine implements Managed {
private final SessionFactory sessionFactory; private final UnitOfWork unitOfWork;
private final FeedDAO feedDAO; private final FeedDAO feedDAO;
private final FeedRefreshWorker worker; private final FeedRefreshWorker worker;
private final FeedRefreshUpdater updater; private final FeedRefreshUpdater updater;
@@ -45,13 +45,13 @@ public class FeedRefreshEngine implements Managed {
private final ExecutorService feedProcessingLoopExecutor; private final ExecutorService feedProcessingLoopExecutor;
private final ExecutorService refillLoopExecutor; private final ExecutorService refillLoopExecutor;
private final ExecutorService refillExecutor; private final ExecutorService refillExecutor;
private final ExecutorService workerExecutor; private final ThreadPoolExecutor workerExecutor;
private final ExecutorService databaseUpdaterExecutor; private final ThreadPoolExecutor databaseUpdaterExecutor;
@Inject @Inject
public FeedRefreshEngine(SessionFactory sessionFactory, FeedDAO feedDAO, FeedRefreshWorker worker, FeedRefreshUpdater updater, public FeedRefreshEngine(UnitOfWork unitOfWork, FeedDAO feedDAO, FeedRefreshWorker worker, FeedRefreshUpdater updater,
CommaFeedConfiguration config, MetricRegistry metrics) { CommaFeedConfiguration config, MetricRegistry metrics) {
this.sessionFactory = sessionFactory; this.unitOfWork = unitOfWork;
this.feedDAO = feedDAO; this.feedDAO = feedDAO;
this.worker = worker; this.worker = worker;
this.updater = updater; this.updater = updater;
@@ -65,6 +65,10 @@ public class FeedRefreshEngine implements Managed {
this.refillExecutor = newDiscardingSingleThreadExecutorService(); this.refillExecutor = newDiscardingSingleThreadExecutorService();
this.workerExecutor = newBlockingExecutorService(config.getApplicationSettings().getBackgroundThreads()); this.workerExecutor = newBlockingExecutorService(config.getApplicationSettings().getBackgroundThreads());
this.databaseUpdaterExecutor = newBlockingExecutorService(config.getApplicationSettings().getDatabaseUpdateThreads()); this.databaseUpdaterExecutor = newBlockingExecutorService(config.getApplicationSettings().getDatabaseUpdateThreads());
metrics.register(MetricRegistry.name(getClass(), "queue", "size"), (Gauge<Integer>) queue::size);
metrics.register(MetricRegistry.name(getClass(), "worker", "active"), (Gauge<Integer>) workerExecutor::getActiveCount);
metrics.register(MetricRegistry.name(getClass(), "updater", "active"), (Gauge<Integer>) databaseUpdaterExecutor::getActiveCount);
} }
@Override @Override
@@ -82,10 +86,12 @@ public class FeedRefreshEngine implements Managed {
Feed feed = queue.take(); Feed feed = queue.take();
// send the feed to be processed // send the feed to be processed
log.debug("got feed {} from the queue, send it for processing", feed.getId());
processFeedAsync(feed); processFeedAsync(feed);
// we removed a feed from the queue, try to refill it as it may now be empty // we removed a feed from the queue, try to refill it as it may now be empty
if (queue.isEmpty()) { if (queue.isEmpty()) {
log.debug("took the last feed from the queue, try to refill");
refillQueueAsync(); refillQueueAsync();
} }
} catch (InterruptedException e) { } catch (InterruptedException e) {
@@ -104,9 +110,11 @@ public class FeedRefreshEngine implements Managed {
while (!refillLoopExecutor.isShutdown()) { while (!refillLoopExecutor.isShutdown()) {
try { try {
if (queue.isEmpty()) { if (queue.isEmpty()) {
log.debug("refilling queue");
refillQueueAsync(); refillQueueAsync();
} }
log.debug("sleeping for 15s");
TimeUnit.SECONDS.sleep(15); TimeUnit.SECONDS.sleep(15);
} catch (InterruptedException e) { } catch (InterruptedException e) {
log.debug("interrupted while sleeping"); log.debug("interrupted while sleeping");
@@ -119,6 +127,7 @@ public class FeedRefreshEngine implements Managed {
} }
public void refreshImmediately(Feed feed) { 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 // 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.removeIf(f -> f.getId().equals(feed.getId()));
queue.addFirst(feed); queue.addFirst(feed);
@@ -132,7 +141,9 @@ public class FeedRefreshEngine implements Managed {
refill.mark(); 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 // add the feed only if it was not already queued
if (queue.stream().noneMatch(f -> f.getId().equals(feed.getId()))) { if (queue.stream().noneMatch(f -> f.getId().equals(feed.getId()))) {
queue.addLast(feed); queue.addLast(feed);
@@ -156,8 +167,11 @@ public class FeedRefreshEngine implements Managed {
} }
private List<Feed> getNextUpdatableFeeds(int max) { private List<Feed> getNextUpdatableFeeds(int max) {
return UnitOfWork.call(sessionFactory, () -> { return unitOfWork.call(() -> {
List<Feed> feeds = feedDAO.findNextUpdatable(max, getLastLoginThreshold()); 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() // update disabledUntil to prevent feeds from being returned again by feedDAO.findNextUpdatable()
Date nextUpdateDate = DateUtils.addMinutes(new Date(), config.getApplicationSettings().getRefreshIntervalMinutes()); Date nextUpdateDate = DateUtils.addMinutes(new Date(), config.getApplicationSettings().getRefreshIntervalMinutes());
feedDAO.setDisabledUntil(feeds.stream().map(AbstractModel::getId).collect(Collectors.toList()), nextUpdateDate); feedDAO.setDisabledUntil(feeds.stream().map(AbstractModel::getId).collect(Collectors.toList()), nextUpdateDate);
@@ -169,10 +183,6 @@ public class FeedRefreshEngine implements Managed {
return Math.min(100, 3 * config.getApplicationSettings().getBackgroundThreads()); return Math.min(100, 3 * config.getApplicationSettings().getBackgroundThreads());
} }
private Date getLastLoginThreshold() {
return Boolean.TRUE.equals(config.getApplicationSettings().getHeavyLoad()) ? DateUtils.addDays(new Date(), -30) : null;
}
@Override @Override
public void stop() { public void stop() {
this.feedProcessingLoopExecutor.shutdownNow(); this.feedProcessingLoopExecutor.shutdownNow();
@@ -185,7 +195,7 @@ public class FeedRefreshEngine implements Managed {
/** /**
* returns an ExecutorService with a single thread that discards tasks if a task is already running * returns an ExecutorService with a single thread that discards tasks if a task is already running
*/ */
private ExecutorService newDiscardingSingleThreadExecutorService() { private ThreadPoolExecutor newDiscardingSingleThreadExecutorService() {
ThreadPoolExecutor pool = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new SynchronousQueue<>()); ThreadPoolExecutor pool = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new SynchronousQueue<>());
pool.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy()); pool.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
return pool; return pool;
@@ -194,7 +204,7 @@ public class FeedRefreshEngine implements Managed {
/** /**
* returns an ExecutorService that blocks submissions until a thread is available * returns an ExecutorService that blocks submissions until a thread is available
*/ */
private ExecutorService newBlockingExecutorService(int threads) { private ThreadPoolExecutor newBlockingExecutorService(int threads) {
ThreadPoolExecutor pool = new ThreadPoolExecutor(threads, threads, 0L, TimeUnit.MILLISECONDS, new SynchronousQueue<>()); ThreadPoolExecutor pool = new ThreadPoolExecutor(threads, threads, 0L, TimeUnit.MILLISECONDS, new SynchronousQueue<>());
pool.setRejectedExecutionHandler((r, e) -> { pool.setRejectedExecutionHandler((r, e) -> {
if (e.isShutdown()) { if (e.isShutdown()) {

View File

@@ -13,8 +13,8 @@ import com.commafeed.backend.model.Feed;
@Singleton @Singleton
public class FeedRefreshIntervalCalculator { public class FeedRefreshIntervalCalculator {
private boolean heavyLoad; private final boolean heavyLoad;
private int refreshIntervalMinutes; private final int refreshIntervalMinutes;
@Inject @Inject
public FeedRefreshIntervalCalculator(CommaFeedConfiguration config) { public FeedRefreshIntervalCalculator(CommaFeedConfiguration config) {

View File

@@ -15,7 +15,6 @@ import javax.inject.Singleton;
import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateUtils; import org.apache.commons.lang3.time.DateUtils;
import org.hibernate.SessionFactory;
import com.codahale.metrics.Meter; import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.MetricRegistry;
@@ -46,7 +45,7 @@ import lombok.extern.slf4j.Slf4j;
@Singleton @Singleton
public class FeedRefreshUpdater implements Managed { public class FeedRefreshUpdater implements Managed {
private final SessionFactory sessionFactory; private final UnitOfWork unitOfWork;
private final FeedService feedService; private final FeedService feedService;
private final FeedEntryService feedEntryService; private final FeedEntryService feedEntryService;
private final PubSubService pubSubService; private final PubSubService pubSubService;
@@ -63,10 +62,10 @@ public class FeedRefreshUpdater implements Managed {
private final Meter entryInserted; private final Meter entryInserted;
@Inject @Inject
public FeedRefreshUpdater(SessionFactory sessionFactory, FeedService feedService, FeedEntryService feedEntryService, public FeedRefreshUpdater(UnitOfWork unitOfWork, FeedService feedService, FeedEntryService feedEntryService,
PubSubService pubSubService, CommaFeedConfiguration config, MetricRegistry metrics, FeedSubscriptionDAO feedSubscriptionDAO, PubSubService pubSubService, CommaFeedConfiguration config, MetricRegistry metrics, FeedSubscriptionDAO feedSubscriptionDAO,
CacheService cache, WebSocketSessions webSocketSessions) { CacheService cache, WebSocketSessions webSocketSessions) {
this.sessionFactory = sessionFactory; this.unitOfWork = unitOfWork;
this.feedService = feedService; this.feedService = feedService;
this.feedEntryService = feedEntryService; this.feedEntryService = feedEntryService;
this.pubSubService = pubSubService; this.pubSubService = pubSubService;
@@ -89,7 +88,7 @@ public class FeedRefreshUpdater implements Managed {
// lock on feed, make sure we are not updating the same feed twice at // lock on feed, make sure we are not updating the same feed twice at
// the same time // the same time
String key1 = StringUtils.trimToEmpty("" + feed.getId()); String key1 = StringUtils.trimToEmpty(String.valueOf(feed.getId()));
// lock on content, make sure we are not updating the same entry // lock on content, make sure we are not updating the same entry
// twice at the same time // twice at the same time
@@ -107,7 +106,7 @@ public class FeedRefreshUpdater implements Managed {
locked2 = lock2.tryLock(1, TimeUnit.MINUTES); locked2 = lock2.tryLock(1, TimeUnit.MINUTES);
if (locked1 && locked2) { if (locked1 && locked2) {
processed = true; processed = true;
inserted = UnitOfWork.call(sessionFactory, () -> feedEntryService.addEntry(feed, entry, subscriptions)); inserted = unitOfWork.call(() -> feedEntryService.addEntry(feed, entry, subscriptions));
if (inserted) { if (inserted) {
entryInserted.mark(); entryInserted.mark();
} }
@@ -164,7 +163,7 @@ public class FeedRefreshUpdater implements Managed {
if (!lastEntries.contains(cacheKey)) { if (!lastEntries.contains(cacheKey)) {
log.debug("cache miss for {}", entry.getUrl()); log.debug("cache miss for {}", entry.getUrl());
if (subscriptions == null) { if (subscriptions == null) {
subscriptions = UnitOfWork.call(sessionFactory, () -> feedSubscriptionDAO.findByFeed(feed)); subscriptions = unitOfWork.call(() -> feedSubscriptionDAO.findByFeed(feed));
} }
AddEntryResult addEntryResult = addEntry(feed, entry, subscriptions); AddEntryResult addEntryResult = addEntry(feed, entry, subscriptions);
processed &= addEntryResult.processed; processed &= addEntryResult.processed;
@@ -204,7 +203,7 @@ public class FeedRefreshUpdater implements Managed {
feedUpdated.mark(); feedUpdated.mark();
} }
UnitOfWork.run(sessionFactory, () -> feedService.save(feed)); unitOfWork.run(() -> feedService.save(feed));
return processed; return processed;
} }

View File

@@ -154,7 +154,7 @@ public class FeedUtils {
for (Emit emit : emits) { for (Emit emit : emits) {
int matchIndex = emit.getStart(); int matchIndex = emit.getStart();
sb.append(source.substring(prevIndex, matchIndex)); sb.append(source, prevIndex, matchIndex);
sb.append(HtmlEntities.HTML_TO_NUMERIC_MAP.get(emit.getKeyword())); sb.append(HtmlEntities.HTML_TO_NUMERIC_MAP.get(emit.getKeyword()));
prevIndex = emit.getEnd() + 1; prevIndex = emit.getEnd() + 1;
} }
@@ -228,7 +228,7 @@ public class FeedUtils {
if (index == -1) { if (index == -1) {
return null; return null;
} }
String encoding = pi.substring(index + 10, pi.length()); String encoding = pi.substring(index + 10);
encoding = encoding.substring(0, encoding.indexOf('"')); encoding = encoding.substring(0, encoding.indexOf('"'));
return encoding; return encoding;
} }

View File

@@ -38,6 +38,6 @@ public class FeedCategory extends AbstractModel {
private boolean collapsed; private boolean collapsed;
private Integer position; private int position;
} }

View File

@@ -38,7 +38,7 @@ public class FeedSubscription extends AbstractModel {
@OneToMany(mappedBy = "subscription", cascade = CascadeType.REMOVE) @OneToMany(mappedBy = "subscription", cascade = CascadeType.REMOVE)
private Set<FeedEntryStatus> statuses; private Set<FeedEntryStatus> statuses;
private Integer position; private int position;
@Column(name = "filtering_expression", length = 4096) @Column(name = "filtering_expression", length = 4096)
private String filter; private String filter;

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