Compare commits

...

69 Commits
3.5.0 ... 3.8.0

Author SHA1 Message Date
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
canoine
24eaff61f2 Update fr/messages.po
Adds, updates and fixes.
2023-06-01 06:25:39 +02:00
97 changed files with 2361 additions and 843 deletions

View File

@@ -1,5 +1,36 @@
# Changelog
## [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
@@ -17,7 +48,8 @@
- add divider to visually separate read-only information from form on the profile settings page
- reduce javascript bundle size by 30% by loading only the necessary translations
- add a standalone donate page with all ways to support CommaFeed
- fix an issue introduced in 3.1.0 that could make CommaFeed not refresh feeds as fast as before on instances with lots of feeds
- fix an issue introduced in 3.1.0 that could make CommaFeed not refresh feeds as fast as before on instances with lots
of feeds
- fix alignment of icon with text for category tree nodes
- fix alignment of burger button with the rest of the header on mobile
@@ -58,10 +90,10 @@
## [3.0.1]
- allow env variable substitution in config.yml
- e.g. having a custom config.yml file with `app.session.path=${SOME_ENV_VAR}` will substitute `SOME_ENV_VAR` with
its value
- e.g. having a custom config.yml file with `app.session.path=${SOME_ENV_VAR}` will substitute `SOME_ENV_VAR` with
its value
- allow env variable prefixed with `CF_` to override config.yml properties
- e.g. setting `CF_APP_ALLOWREGISTRATIONS=true` will set `app.allowRegistrations` to `true`
- e.g. setting `CF_APP_ALLOWREGISTRATIONS=true` will set `app.allowRegistrations` to `true`
## [3.0.0]

View File

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

View File

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

View File

@@ -20,11 +20,15 @@
"@mantine/notifications": "^6.0.11",
"@mantine/spotlight": "^6.0.11",
"@mantine/styles": "^6.0.11",
"@monaco-editor/react": "^4.5.1",
"@reduxjs/toolkit": "^1.9.5",
"axios": "^1.4.0",
"dayjs": "^1.11.7",
"escape-string-regexp": "^5.0.0",
"interweave": "^13.1.0",
"monaco-editor": "^0.38.0",
"mousetrap": "^1.6.5",
"re-resizable": "^6.9.9",
"react": "^18.2.0",
"react-async-hook": "^4.0.0",
"react-contexify": "^6.0.0",
@@ -66,7 +70,7 @@
"prettier": "^2.8.8",
"rollup-plugin-visualizer": "^5.9.0",
"typescript": "^5.0.4",
"vite": "^4.3.8",
"vite": "^4.3.9",
"vite-plugin-eslint": "^1.8.1",
"vite-tsconfig-paths": "^4.2.0",
"vitest": "^0.31.1",
@@ -2054,6 +2058,17 @@
"stylis": "4.2.0"
}
},
"node_modules/@emotion/babel-plugin/node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@emotion/cache": {
"version": "11.11.0",
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz",
@@ -3048,6 +3063,30 @@
"moo": "^0.5.1"
}
},
"node_modules/@monaco-editor/loader": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.3.3.tgz",
"integrity": "sha512-6KKF4CTzcJiS8BJwtxtfyYt9shBiEv32ateQ9T4UVogwn4HM/uPo9iJd2Dmbkpz8CM6Y0PDUpjnZzCwC+eYo2Q==",
"dependencies": {
"state-local": "^1.0.6"
},
"peerDependencies": {
"monaco-editor": ">= 0.21.0 < 1"
}
},
"node_modules/@monaco-editor/react": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.5.1.tgz",
"integrity": "sha512-NNDFdP+2HojtNhCkRfE6/D6ro6pBNihaOzMbGK84lNWzRu+CfBjwzGt4jmnqimLuqp5yE5viHS2vi+QOAnD5FQ==",
"dependencies": {
"@monaco-editor/loader": "^1.3.3"
},
"peerDependencies": {
"monaco-editor": ">= 0.25.0 < 1",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/@nicolo-ribaudo/eslint-scope-5-internals": {
"version": "5.1.1-v1",
"resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz",
@@ -6666,11 +6705,11 @@
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
},
"node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
"integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
"engines": {
"node": ">=10"
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
@@ -7148,6 +7187,18 @@
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint/node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true,
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/eslint/node_modules/eslint-scope": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz",
@@ -8998,6 +9049,11 @@
"ufo": "^1.1.2"
}
},
"node_modules/monaco-editor": {
"version": "0.38.0",
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.38.0.tgz",
"integrity": "sha512-11Fkh6yzEmwx7O0YoLxeae0qEGFwmyPRlVxpg7oF9czOOCB/iCjdJrG5I67da5WiXK3YJCxoz9TJFE8Tfq/v9A=="
},
"node_modules/moo": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz",
@@ -10035,6 +10091,15 @@
"node": ">=0.10.0"
}
},
"node_modules/re-resizable": {
"version": "6.9.9",
"resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-6.9.9.tgz",
"integrity": "sha512-l+MBlKZffv/SicxDySKEEh42hR6m5bAHfNu3Tvxks2c4Ah+ldnWjfnVRwxo/nxF27SsUsxDS0raAzFuJNKABXA==",
"peerDependencies": {
"react": "^16.13.1 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
@@ -11000,6 +11065,11 @@
"resolved": "https://registry.npmjs.org/stampit/-/stampit-4.3.2.tgz",
"integrity": "sha512-pE2org1+ZWQBnIxRPrBM2gVupkuDD0TTNIo1H6GdT/vO82NXli2z8lRE8cu/nBIHrcOCXFBAHpb9ZldrB2/qOA=="
},
"node_modules/state-local": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz",
"integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w=="
},
"node_modules/std-env": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.3.3.tgz",
@@ -11829,9 +11899,9 @@
"devOptional": true
},
"node_modules/vite": {
"version": "4.3.8",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.3.8.tgz",
"integrity": "sha512-uYB8PwN7hbMrf4j1xzGDk/lqjsZvCDbt/JC5dyfxc19Pg8kRm14LinK/uq+HSLNswZEoKmweGdtpbnxRtrAXiQ==",
"version": "4.3.9",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.3.9.tgz",
"integrity": "sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==",
"dev": true,
"dependencies": {
"esbuild": "^0.17.5",

View File

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

View File

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

View File

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

View File

@@ -30,7 +30,10 @@ const axiosInstance = axios.create({ baseURL: "./rest", withCredentials: true })
axiosInstance.interceptors.response.use(
response => response,
error => {
if (error.response.status === 401 && error.response.data === "Credentials are required to access this resource.") {
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"
}
throw error

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,4 @@
import { throttle } from "throttle-debounce"
import { Category } from "./types"
export function visitCategoryTree(category: Category, visitor: (category: Category) => void): void {
@@ -26,43 +27,21 @@ export const calculatePlaceholderSize = ({ width, height, maxWidth }: { width?:
return { width: placeholderWidth, height: placeholderHeight }
}
export const scrollToWithCallback = ({
element,
options,
onScrollEnded,
}: {
element: HTMLElement
options: ScrollToOptions
onScrollEnded: () => void
}) => {
export const scrollToWithCallback = ({ options, onScrollEnded }: { options: ScrollToOptions; onScrollEnded: () => void }) => {
const offset = (options.top ?? 0).toFixed()
const onScroll = () => {
if (element.offsetTop.toFixed() === offset) {
element.removeEventListener("scroll", onScroll)
const onScroll = throttle(100, () => {
if (window.scrollY.toFixed() === offset) {
window.removeEventListener("scroll", onScroll)
onScrollEnded()
}
}
element.addEventListener("scroll", onScroll)
})
window.addEventListener("scroll", onScroll)
// scrollTo does not trigger if there's nothing to do, trigger it manually
onScroll()
element.scrollTo(options)
}
export const openLinkInBackgroundTab = (url: string) => {
// simulate ctrl+click to open tab in background
const a = document.createElement("a")
a.href = url
a.rel = "noreferrer"
a.dispatchEvent(
new MouseEvent("click", {
ctrlKey: true,
metaKey: true,
})
)
window.scrollTo(options)
}
export const truncate = (str: string, n: number) => (str.length > n ? `${str.slice(0, n - 1)}\u2026` : str)

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 { ButtonProps } from "@mantine/core/lib/Button/Button"
import { useMediaQuery } from "@mantine/hooks"
import { useActionButton } from "hooks/useActionButton"
import { forwardRef, MouseEventHandler, ReactNode } from "react"
interface ActionButtonProps {
className?: string
icon?: ReactNode
label?: ReactNode
label: ReactNode
onClick?: MouseEventHandler
variant?: ActionIconProps["variant"] & ButtonProps["variant"]
hideLabelOnDesktop?: boolean
showLabelOnMobile?: boolean
}
@@ -17,14 +18,16 @@ interface ActionButtonProps {
* Switches between Button with label (desktop) and ActionIcon (mobile)
*/
export const ActionButton = forwardRef<HTMLButtonElement, ActionButtonProps>((props: ActionButtonProps, ref) => {
const { mobile } = useActionButton()
const theme = useMantineTheme()
const variant = props.variant ?? "subtle"
const mobile = !useMediaQuery(`(min-width: ${theme.breakpoints.lg})`)
const iconOnly = !props.showLabelOnMobile && (mobile || !props.label)
const iconOnly = (mobile && !props.showLabelOnMobile) || (!mobile && props.hideLabelOnDesktop)
return iconOnly ? (
<ActionIcon ref={ref} color={theme.primaryColor} variant={variant} className={props.className} onClick={props.onClick}>
{props.icon}
</ActionIcon>
<Tooltip label={props.label} openDelay={500}>
<ActionIcon ref={ref} color={theme.primaryColor} variant={variant} className={props.className} onClick={props.onClick}>
{props.icon}
</ActionIcon>
</Tooltip>
) : (
<Button ref={ref} variant={variant} size="xs" className={props.className} leftIcon={props.icon} onClick={props.onClick}>
{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 { Kbd, Table } from "@mantine/core"
import { Anchor, Box, Kbd, Stack, Table } from "@mantine/core"
import { Constants } from "app/constants"
export function KeyboardShortcutsHelp() {
return (
<Table striped highlightOnHover>
<tbody>
<tr>
<td>
<Trans>Refresh</Trans>
</td>
<td>
<Kbd>R</Kbd>
</td>
</tr>
<tr>
<td>
<Trans>Open next entry</Trans>
</td>
<td>
<Kbd>J</Kbd>
</td>
</tr>
<tr>
<td>
<Trans>Open previous entry</Trans>
</td>
<td>
<Kbd>K</Kbd>
</td>
</tr>
<tr>
<td>
<Trans>Set focus on next entry without opening it</Trans>
</td>
<td>
<Kbd>N</Kbd>
</td>
</tr>
<tr>
<td>
<Trans>Set focus on previous entry without opening it</Trans>
</td>
<td>
<Kbd>P</Kbd>
</td>
</tr>
<tr>
<td>
<Trans>Move the page down</Trans>
</td>
<td>
<Kbd>
<Trans>Space</Trans>
</Kbd>
</td>
</tr>
<tr>
<td>
<Trans>Move the page up</Trans>
</td>
<td>
<Kbd>
<Trans>Shift</Trans>
</Kbd>
<span> + </span>
<Kbd>
<Trans>Space</Trans>
</Kbd>
</td>
</tr>
<tr>
<td>
<Trans>Open/close current entry</Trans>
</td>
<td>
<Kbd>O</Kbd>
<span>, </span>
<Kbd>
<Trans>Enter</Trans>
</Kbd>
</td>
</tr>
<tr>
<td>
<Trans>Open current entry in a new tab</Trans>
</td>
<td>
<Kbd>V</Kbd>
</td>
</tr>
<tr>
<td>
<Trans>Open current entry in a new tab in the background</Trans>
</td>
<td>
<Kbd>B</Kbd>
<span>, </span>
<Kbd>
<Trans>Middle click</Trans>
</Kbd>
</td>
</tr>
<tr>
<td>
<Trans>Toggle read status of current entry</Trans>
</td>
<td>
<Kbd>M</Kbd>
<span>, </span>
<Trans>Swipe header to the right</Trans>
</td>
</tr>
<tr>
<td>
<Trans>Mark all entries as read</Trans>
</td>
<td>
<Kbd>
<Trans>Shift</Trans>
</Kbd>
<span> + </span>
<Kbd>A</Kbd>
</td>
</tr>
<tr>
<td>
<Trans>Go to the All view</Trans>
</td>
<td>
<Kbd>G</Kbd>
<span> </span>
<Kbd>A</Kbd>
</td>
</tr>
<tr>
<td>
<Trans>Navigate to a subscription by entering its name</Trans>
</td>
<td>
<Kbd>
<Trans>Ctrl</Trans>
</Kbd>
<span> + </span>
<Kbd>K</Kbd>
<span>, </span>
<Kbd>G</Kbd>
<span> </span>
<Kbd>U</Kbd>
</td>
</tr>
<tr>
<td>
<Trans>Show entry menu (desktop)</Trans>
</td>
<td>
<Kbd>
<Trans>Right click</Trans>
</Kbd>
</td>
</tr>
<tr>
<td>
<Trans>Show entry menu (mobile)</Trans>
</td>
<td>
<Kbd>
<Trans>Long press</Trans>
</Kbd>
</td>
</tr>
<tr>
<td>
<Trans>Show keyboard shortcut help</Trans>
</td>
<td>
<Kbd>?</Kbd>
</td>
</tr>
</tbody>
</Table>
<Stack spacing="xs">
<Table striped highlightOnHover>
<tbody>
<tr>
<td>
<Trans>Refresh</Trans>
</td>
<td>
<Kbd>R</Kbd>
</td>
</tr>
<tr>
<td>
<Trans>Open next entry</Trans>
</td>
<td>
<Kbd>J</Kbd>
</td>
</tr>
<tr>
<td>
<Trans>Open previous entry</Trans>
</td>
<td>
<Kbd>K</Kbd>
</td>
</tr>
<tr>
<td>
<Trans>Set focus on next entry without opening it</Trans>
</td>
<td>
<Kbd>N</Kbd>
</td>
</tr>
<tr>
<td>
<Trans>Set focus on previous entry without opening it</Trans>
</td>
<td>
<Kbd>P</Kbd>
</td>
</tr>
<tr>
<td>
<Trans>Move the page down</Trans>
</td>
<td>
<Kbd>
<Trans>Space</Trans>
</Kbd>
</td>
</tr>
<tr>
<td>
<Trans>Move the page up</Trans>
</td>
<td>
<Kbd>
<Trans>Shift</Trans>
</Kbd>
<span> + </span>
<Kbd>
<Trans>Space</Trans>
</Kbd>
</td>
</tr>
<tr>
<td>
<Trans>Open/close current entry</Trans>
</td>
<td>
<Kbd>O</Kbd>
<span>, </span>
<Kbd>
<Trans>Enter</Trans>
</Kbd>
</td>
</tr>
<tr>
<td>
<Trans>Open current entry in a new tab</Trans>
</td>
<td>
<Kbd>V</Kbd>
</td>
</tr>
<tr>
<td>
<Trans>Open current entry in a new tab in the background</Trans>
</td>
<td>
<Kbd>B</Kbd>
<span>*, </span>
<Kbd>
<Trans>Middle click</Trans>
</Kbd>
</td>
</tr>
<tr>
<td>
<Trans>Toggle read status of current entry</Trans>
</td>
<td>
<Kbd>M</Kbd>
<span>, </span>
<Trans>Swipe header to the right</Trans>
</td>
</tr>
<tr>
<td>
<Trans>Mark all entries as read</Trans>
</td>
<td>
<Kbd>
<Trans>Shift</Trans>
</Kbd>
<span> + </span>
<Kbd>A</Kbd>
</td>
</tr>
<tr>
<td>
<Trans>Go to the All view</Trans>
</td>
<td>
<Kbd>G</Kbd>
<span> </span>
<Kbd>A</Kbd>
</td>
</tr>
<tr>
<td>
<Trans>Navigate to a subscription by entering its name</Trans>
</td>
<td>
<Kbd>
<Trans>Ctrl</Trans>
</Kbd>
<span> + </span>
<Kbd>K</Kbd>
<span>, </span>
<Kbd>G</Kbd>
<span> </span>
<Kbd>U</Kbd>
</td>
</tr>
<tr>
<td>
<Trans>Show entry menu (desktop)</Trans>
</td>
<td>
<Kbd>
<Trans>Right click</Trans>
</Kbd>
</td>
</tr>
<tr>
<td>
<Trans>Show entry menu (mobile)</Trans>
</td>
<td>
<Kbd>
<Trans>Long press</Trans>
</Kbd>
</td>
</tr>
<tr>
<td>
<Trans>Toggle sidebar</Trans>
</td>
<td>
<Kbd>F</Kbd>
</td>
</tr>
<tr>
<td>
<Trans>Show keyboard shortcut help</Trans>
</td>
<td>
<Kbd>?</Kbd>
</td>
</tr>
</tbody>
</Table>
<Box>
<span>* </span>
<Anchor href={Constants.browserExtensionUrl} target="_blank" rel="noreferrer">
<Trans>Browser extension required for Chrome</Trans>
</Anchor>
</Box>
</Stack>
)
}

View File

@@ -1,4 +1,5 @@
import { Trans } from "@lingui/macro"
import { Tooltip } from "@mantine/core"
import dayjs from "dayjs"
import { useEffect, useState } from "react"
@@ -10,5 +11,10 @@ export function RelativeDate(props: { date: Date | number | undefined }) {
}, [])
if (!props.date) return <Trans>N/A</Trans>
return <>{dayjs(props.date).from(dayjs(now))}</>
const date = dayjs(props.date)
return (
<Tooltip label={date.toDate().toLocaleString()} openDelay={500}>
<span>{date.from(dayjs(now))}</span>
</Tooltip>
)
}

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

View File

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

View File

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

View File

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

View File

@@ -5,11 +5,10 @@ import { markEntriesUpToEntry, markEntry, starEntry } from "app/slices/entries"
import { redirectToFeed } from "app/slices/redirect"
import { useAppDispatch, useAppSelector } from "app/store"
import { Entry } from "app/types"
import { openLinkInBackgroundTab, truncate } from "app/utils"
import { useEffect } from "react"
import { Item, Menu, Separator, useContextMenu } from "react-contexify"
import { truncate } from "app/utils"
import { useBrowserExtension } from "hooks/useBrowserExtension"
import { Item, Menu, Separator } from "react-contexify"
import { TbArrowBarToDown, TbExternalLink, TbEyeCheck, TbEyeOff, TbRss, TbStar, TbStarOff } from "react-icons/tb"
import { throttle } from "throttle-debounce"
interface FeedEntryContextMenuProps {
entry: Entry
@@ -28,15 +27,14 @@ const useStyles = createStyles(theme => ({
},
}))
const menuId = (entry: Entry) => entry.id
export function FeedEntryContextMenu(props: FeedEntryContextMenuProps) {
const { classes, theme } = useStyles()
const sourceType = useAppSelector(state => state.entries.source.type)
const dispatch = useAppDispatch()
const { openLinkInBackgroundTab } = useBrowserExtension()
return (
<Menu id={menuId(props.entry)} theme={theme.colorScheme} animation={false} className={classes.menu}>
<Menu id={Constants.dom.entryContextMenuId(props.entry)} theme={theme.colorScheme} animation={false} className={classes.menu}>
<Item
onClick={() => {
window.open(props.entry.url, "_blank", "noreferrer")
@@ -100,29 +98,3 @@ export function FeedEntryContextMenu(props: FeedEntryContextMenuProps) {
</Menu>
)
}
export function useFeedEntryContextMenu(entry: Entry) {
const contextMenu = useContextMenu({
id: menuId(entry),
})
const onContextMenu = (event: React.MouseEvent) => {
event.preventDefault()
contextMenu.show({
event,
})
}
// close context menu on scroll
useEffect(() => {
const scrollArea = document.getElementById(Constants.dom.mainScrollAreaId)
const listener = () => contextMenu.hideAll()
const throttledListener = throttle(100, listener)
scrollArea?.addEventListener("scroll", throttledListener)
return () => scrollArea?.removeEventListener("scroll", throttledListener)
}, [contextMenu])
return { onContextMenu }
}

View File

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

View File

@@ -1,14 +1,29 @@
import { t, Trans } from "@lingui/macro"
import { ActionIcon, Center, Divider, Indicator, Popover, TextInput } from "@mantine/core"
import { ActionIcon, Box, Center, Divider, Group, Indicator, Popover, TextInput } from "@mantine/core"
import { useForm } from "@mantine/form"
import { reloadEntries, search } from "app/slices/entries"
import { reloadEntries, search, selectNextEntry, selectPreviousEntry } from "app/slices/entries"
import { changeReadingMode, changeReadingOrder } from "app/slices/user"
import { useAppDispatch, useAppSelector } from "app/store"
import { ActionButton } from "components/ActionButtton"
import { ButtonToolbar } from "components/ButtonToolbar"
import { ActionButton } from "components/ActionButton"
import { Loader } from "components/Loader"
import { useActionButton } from "hooks/useActionButton"
import { useBrowserExtension } from "hooks/useBrowserExtension"
import { useMobile } from "hooks/useMobile"
import { useEffect } from "react"
import { TbArrowDown, TbArrowUp, 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 { ProfileMenu } from "./ProfileMenu"
@@ -16,12 +31,32 @@ function HeaderDivider() {
return <Divider orientation="vertical" />
}
function HeaderToolbar(props: { children: React.ReactNode }) {
const { spacing } = useActionButton()
const mobile = useMobile("480px")
return mobile ? (
// on mobile use all available width
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "space-between",
}}
>
{props.children}
</Box>
) : (
<Group spacing={spacing}>{props.children}</Group>
)
}
const iconSize = 18
export function Header() {
const settings = useAppSelector(state => state.user.settings)
const profile = useAppSelector(state => state.user.profile)
const searchFromStore = useAppSelector(state => state.entries.search)
const { isBrowserExtensionPopup, openSettingsPage, openAppInNewTab } = useBrowserExtension()
const dispatch = useAppDispatch()
const searchForm = useForm<{ search: string }>({
@@ -40,7 +75,36 @@ export function Header() {
if (!settings) return <Loader />
return (
<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
icon={<TbRefresh size={iconSize} />}
label={<Trans>Refresh</Trans>}
@@ -56,7 +120,7 @@ export function Header() {
onClick={() => dispatch(changeReadingMode(settings.readingMode === "all" ? "unread" : "all"))}
/>
<ActionButton
icon={settings.readingOrder === "asc" ? <TbArrowUp size={iconSize} /> : <TbArrowDown size={iconSize} />}
icon={settings.readingOrder === "asc" ? <TbSortAscending size={iconSize} /> : <TbSortDescending size={iconSize} />}
label={settings.readingOrder === "asc" ? <Trans>Asc</Trans> : <Trans>Desc</Trans>}
onClick={() => dispatch(changeReadingOrder(settings.readingOrder === "asc" ? "desc" : "asc"))}
/>
@@ -87,7 +151,24 @@ export function Header() {
<HeaderDivider />
<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>
)
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,14 @@
import { Trans } from "@lingui/macro"
import { Divider, Select, SimpleGrid, Stack, Switch } from "@mantine/core"
import { Constants } from "app/constants"
import { changeLanguage, changeScrollMarks, changeScrollSpeed, changeSharingSetting, changeShowRead } from "app/slices/user"
import {
changeAlwaysScrollToEntry,
changeLanguage,
changeScrollMarks,
changeScrollSpeed,
changeSharingSetting,
changeShowRead,
} from "app/slices/user"
import { useAppDispatch, useAppSelector } from "app/store"
import { SharingSettings } from "app/types"
import { locales } from "i18n"
@@ -11,6 +18,7 @@ export function DisplaySettings() {
const scrollSpeed = useAppSelector(state => state.user.settings?.scrollSpeed)
const showRead = useAppSelector(state => state.user.settings?.showRead)
const scrollMarks = useAppSelector(state => state.user.settings?.scrollMarks)
const alwaysScrollToEntry = useAppSelector(state => state.user.settings?.alwaysScrollToEntry)
const sharingSettings = useAppSelector(state => state.user.settings?.sharingSettings)
const dispatch = useAppDispatch()
@@ -32,6 +40,12 @@ export function DisplaySettings() {
onChange={e => dispatch(changeScrollSpeed(e.currentTarget.checked))}
/>
<Switch
label={<Trans>Always scroll selected entry to the top of the page, even if it fits entirely on screen</Trans>}
checked={alwaysScrollToEntry}
onChange={e => dispatch(changeAlwaysScrollToEntry(e.currentTarget.checked))}
/>
<Switch
label={<Trans>Show feeds and categories with no unread entries</Trans>}
checked={showRead}

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,6 +17,10 @@ msgstr ""
msgid "{0} (in {1})"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx
msgid "<0>Complete syntax is available </0><1>here</1>."
msgstr ""
@@ -67,6 +71,10 @@ msgstr "Verwaltung"
msgid "All"
msgstr "Alle"
#: src/components/settings/DisplaySettings.tsx
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "Eine E-Mail wurde gesendet, wenn diese Adresse registriert wurde. "
@@ -123,9 +131,13 @@ msgstr "Zurück"
msgid "Back to log in"
msgstr "Zurück zum Anmelden"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "Browser extentions"
msgstr "Browsererweiterungen"
msgid "Browser extention"
msgstr ""
#: src/components/admin/UserEdit.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"
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed is an open-source project. Sources are hosted on <0>GitHub</0>."
msgstr "CommaFeed ist ein Open-Source-Projekt. "
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed nächstes ungelesenes Element"
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})"
msgstr "CommaFeed-Version {Version} ({Revision})"
msgid "CommaFeed version {version} ({revision})."
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Compact"
@@ -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"
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
msgid "Feed name"
msgstr "Feedname"
@@ -513,6 +530,7 @@ msgid "Newest first"
msgstr "Neueste zuerst"
#: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next"
msgstr "Weiter"
@@ -540,6 +558,10 @@ msgstr "Älteste zuerst"
msgid "Oops!"
msgstr "Ups!"
#: src/components/header/Header.tsx
msgid "Open CommaFeed"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Open current entry in a new tab"
msgstr "Aktuellen Eintrag in neuem Tab öffnen"
@@ -618,6 +640,10 @@ msgstr "Passwörter stimmen nicht überein"
msgid "Position"
msgstr "Stellung"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Profile"
msgstr "Profil"
@@ -759,10 +785,12 @@ msgid "Swipe header to the right"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to dark theme"
msgstr "Zum dunklen Design wechseln"
#: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to light theme"
msgstr "Wechseln Sie zum Lichtdesign"
@@ -782,6 +810,10 @@ msgstr "Thema"
msgid "Toggle read status of current entry"
msgstr "Lesestatus des aktuellen Eintrags umschalten"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Testen Sie CommaFeed mit dem Demokonto: demo/demo"

View File

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

View File

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

View File

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

View File

@@ -15,7 +15,11 @@ msgstr ""
#: src/components/content/add/CategorySelect.tsx
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
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
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
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/pages/app/AboutPage.tsx
msgid "About"
msgstr "A propos"
msgstr "À propos"
#: src/pages/admin/AdminUsersPage.tsx
msgid "Actions"
@@ -67,6 +71,10 @@ msgstr "Administrateur"
msgid "All"
msgstr "Tout"
#: src/components/settings/DisplaySettings.tsx
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
msgstr "Toujours remonter l'entrée sélectionnée en haut de la page, même si elle s'affiche complètement à l'écran"
#: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "Un e-mail a été envoyé si cette adresse est enregistrée. Vérifiez votre boîte de réception."
@@ -85,11 +93,11 @@ msgstr "Clé API"
#: src/pages/app/CategoryDetailsPage.tsx
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
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
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
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
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
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
msgid "Asc"
@@ -123,9 +131,13 @@ msgstr "Retour"
msgid "Back to log in"
msgstr "Retour à la connexion"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr "L'extension navigateur est nécessaire sur Chrome"
#: src/pages/app/AboutPage.tsx
msgid "Browser extentions"
msgstr "Extensions pour navigateurs"
msgid "Browser extention"
msgstr "Extension navigateur"
#: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx
@@ -160,16 +172,16 @@ msgid "Check that the feed is working"
msgstr "Vérifie que le flux fonctionne"
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed is an open-source project. Sources are hosted on <0>GitHub</0>."
msgstr "CommaFeed est un projet open-source. Les sources sont hébergées sur <0>GitHub</0>."
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "Extension CommaFeed pour navigateur version {browserExtensionVersion}."
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed prochain article non lu"
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})"
msgstr "CommaFeed version {version} ({revision})"
msgid "CommaFeed version {version} ({revision})."
msgstr "CommaFeed version {version} ({revision})."
#: src/components/header/ProfileMenu.tsx
msgid "Compact"
@@ -193,7 +205,7 @@ msgstr "Cozy"
#: src/components/content/FeedEntryFooter.tsx
msgid "Create tag: {query}"
msgstr "Créer le tag: {query}"
msgstr "Créer le marqueur : {query}"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Ctrl"
@@ -205,15 +217,15 @@ msgstr "Mot de passe actuel"
#: src/pages/app/SettingsPage.tsx
msgid "Custom code"
msgstr ""
msgstr "Code personnalisé"
#: src/components/settings/CustomCodeSettings.tsx
msgid "Custom CSS rules that will be applied"
msgstr ""
msgstr "Code CSS personnalisé qui sera appliqué"
#: src/components/settings/CustomCodeSettings.tsx
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
msgid "Date created"
@@ -252,7 +264,7 @@ msgstr "Affichage"
#: src/components/header/ProfileMenu.tsx
#: src/pages/app/DonatePage.tsx
msgid "Donate"
msgstr ""
msgstr "Faites un don"
#: src/components/settings/ProfileSettings.tsx
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"
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
msgid "Feed name"
msgstr "Nom du flux"
@@ -320,7 +337,7 @@ msgstr "URL du flux"
#: src/components/header/ProfileMenu.tsx
msgid "Fetch all my feeds now"
msgstr ""
msgstr "Rafraîchir tous mes flux"
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
@@ -352,7 +369,7 @@ msgstr "URL du flux généré"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Go to {0}"
msgstr ""
msgstr "Aller à {0}"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Go to the All view"
@@ -420,19 +437,19 @@ msgstr "Lien"
#: src/hooks/useAppLoading.ts
msgid "Loading profile..."
msgstr "Chargement du profil ..."
msgstr "Chargement du profil..."
#: src/hooks/useAppLoading.ts
msgid "Loading settings..."
msgstr "Chargement des paramètres ..."
msgstr "Chargement des paramètres..."
#: src/hooks/useAppLoading.ts
msgid "Loading subscriptions..."
msgstr "Chargement des abonnements ..."
msgstr "Chargement des abonnements..."
#: src/hooks/useAppLoading.ts
msgid "Loading tags..."
msgstr "Chargement des tags ..."
msgstr "Chargement des marqueurs..."
#: src/pages/auth/LoginPage.tsx
#: src/pages/auth/LoginPage.tsx
@@ -446,7 +463,7 @@ msgstr "Déconnexion"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Long press"
msgstr ""
msgstr "Appui long"
#: src/components/header/ProfileMenu.tsx
#: src/pages/admin/AdminUsersPage.tsx
@@ -478,7 +495,7 @@ msgstr "Métriques"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Middle click"
msgstr ""
msgstr "Clic milieu"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Move the page down"
@@ -513,6 +530,7 @@ msgid "Newest first"
msgstr "Plus récent en premier"
#: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next"
msgstr "Suivant"
@@ -526,7 +544,7 @@ msgstr "Bookmarklet vers le prochain article non lu"
#: src/pages/app/FeedEntriesPage.tsx
msgid "No more entries"
msgstr "Plus d'entrées"
msgstr "Fin de la liste"
#: src/components/sidebar/TreeSearch.tsx
msgid "Nothing found"
@@ -540,6 +558,10 @@ msgstr "Du plus ancien au plus récent"
msgid "Oops!"
msgstr "Oups !"
#: src/components/header/Header.tsx
msgid "Open CommaFeed"
msgstr "Ouvrir CommaFeed"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Open current entry in a new tab"
msgstr "Ouvrir l'entrée actuelle dans un nouvel onglet"
@@ -554,11 +576,11 @@ msgstr "Ouvrir le lien"
#: src/components/content/FeedEntryContextMenu.tsx
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
msgid "Open link in new tab"
msgstr ""
msgstr "Ouvrir le lien dans un nouvel onglet"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry"
@@ -618,6 +640,10 @@ msgstr "Les mots de passe ne correspondent pas"
msgid "Position"
msgstr "Position"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr "Précédent"
#: src/pages/app/SettingsPage.tsx
msgid "Profile"
msgstr "Profil"
@@ -641,7 +667,7 @@ msgstr "API REST"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
msgstr "Clic droit"
#: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx
@@ -697,11 +723,11 @@ msgstr "Maj"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)"
msgstr ""
msgstr "Afficher les options de l'entrée (ordinateur)"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (mobile)"
msgstr ""
msgstr "Afficher les options de l'entrée (mobile)"
#: src/components/settings/DisplaySettings.tsx
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"
#: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to dark theme"
msgstr "Activer le mode sombre"
#: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to light theme"
msgstr "Activer le mode clair"
#: src/components/content/FeedEntryFooter.tsx
msgid "Tags"
msgstr "Tags"
msgstr "Marqueurs"
#: 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."
@@ -782,13 +810,17 @@ msgstr "Thème"
msgid "Toggle read status of current entry"
msgstr "Marquer l'entrée actuelle comme lue/non lue"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr "Montrer/cacher la barre latérale"
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Essayez CommaFeed avec le compte de démonstration : demo/demo"
#: src/pages/WelcomePage.tsx
msgid "Try the demo!"
msgstr ""
msgstr "Essayez la version de démonstration !"
#: src/components/header/Header.tsx
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
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})"
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
msgid "<0>Complete syntax is available </0><1>here</1>."
msgstr ""
@@ -67,6 +71,10 @@ msgstr "Administración"
msgid "All"
msgstr "Todos"
#: src/components/settings/DisplaySettings.tsx
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "Enviouse un correo electrónico se este enderezo estaba rexistrado. "
@@ -123,9 +131,13 @@ msgstr "Atrás"
msgid "Back to log in"
msgstr "Volver para iniciar sesión"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "Browser extentions"
msgstr "Extensións do navegador"
msgid "Browser extention"
msgstr ""
#: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx
@@ -160,16 +172,16 @@ msgid "Check that the feed is working"
msgstr "Comproba que a fonte funciona"
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed is an open-source project. Sources are hosted on <0>GitHub</0>."
msgstr "CommaFeed é un proxecto de código aberto. "
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed seguinte elemento non lido"
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})"
msgstr "Versión de CommaFeed {versión} ({revisión})"
msgid "CommaFeed version {version} ({revision})."
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Compact"
@@ -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"
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
msgid "Feed name"
msgstr "Nome do feed"
@@ -513,6 +530,7 @@ msgid "Newest first"
msgstr "o máis novo primeiro"
#: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next"
msgstr "Seguinte"
@@ -540,6 +558,10 @@ msgstr "O máis vello primeiro"
msgid "Oops!"
msgstr "Vaia!"
#: src/components/header/Header.tsx
msgid "Open CommaFeed"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Open current entry in a new tab"
msgstr "Abrir a entrada actual nunha nova pestana"
@@ -618,6 +640,10 @@ msgstr "Os contrasinais non coinciden"
msgid "Position"
msgstr "Posición"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Profile"
msgstr "Perfil"
@@ -759,10 +785,12 @@ msgid "Swipe header to the right"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to dark theme"
msgstr "Cambiar ao tema escuro"
#: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to light theme"
msgstr "Cambiar ao tema claro"
@@ -782,6 +810,10 @@ msgstr "Tema"
msgid "Toggle read status of current entry"
msgstr "alternar o estado de lectura da entrada actual"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Proba CommaFeed coa conta de demostración: demo/demo"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,6 +17,10 @@ msgstr ""
msgid "{0} (in {1})"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
msgstr ""
#: src/pages/app/FeedDetailsPage.tsx
msgid "<0>Complete syntax is available </0><1>here</1>."
msgstr ""
@@ -67,6 +71,10 @@ msgstr "Yönetici"
msgid "All"
msgstr "Tümü"
#: src/components/settings/DisplaySettings.tsx
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "Bu adres kayıtlıysa bir e-posta gönderildi. "
@@ -123,9 +131,13 @@ msgstr "Geri"
msgid "Back to log in"
msgstr "Giriş yapmak için geri dön"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "Browser extentions"
msgstr "Tarayıcı uzantıları"
msgid "Browser extention"
msgstr ""
#: src/components/admin/UserEdit.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"
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed is an open-source project. Sources are hosted on <0>GitHub</0>."
msgstr "CommaFeed açık kaynaklı bir projedir. "
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed sonraki okunmamış öğe"
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})"
msgstr "CommaFeed sürümü {sürüm} ({revizyon})"
msgid "CommaFeed version {version} ({revision})."
msgstr ""
#: src/components/header/ProfileMenu.tsx
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"
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
msgid "Feed name"
msgstr "Yayın adı"
@@ -513,6 +530,7 @@ msgid "Newest first"
msgstr "Önce en yenisi"
#: src/components/content/add/Subscribe.tsx
#: src/components/header/Header.tsx
msgid "Next"
msgstr "Sonraki"
@@ -540,6 +558,10 @@ msgstr "Önce en eski"
msgid "Oops!"
msgstr "Hata!"
#: src/components/header/Header.tsx
msgid "Open CommaFeed"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Open current entry in a new tab"
msgstr "Geçerli girişi yeni bir sekmede aç"
@@ -618,6 +640,10 @@ msgstr "Parolalar eşleşmiyor"
msgid "Position"
msgstr "Konum"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
#: src/pages/app/SettingsPage.tsx
msgid "Profile"
msgstr "Profil"
@@ -759,10 +785,12 @@ msgid "Swipe header to the right"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to dark theme"
msgstr "Karanlık temaya geç"
#: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to light theme"
msgstr "Açık temaya geç"
@@ -782,6 +810,10 @@ msgstr "Tema"
msgid "Toggle read status of current entry"
msgstr "Geçerli girişin okuma durumunu değiştir"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "CommaFeed'i demo hesabıyla deneyin: demo/demo"

View File

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

View File

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

View File

@@ -20,6 +20,8 @@ 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() {

View File

@@ -5,6 +5,7 @@ import { redirectToApiDocumentation } from "app/slices/redirect"
import { useAppDispatch, useAppSelector } from "app/store"
import { CategorySelect } from "components/content/add/CategorySelect"
import { KeyboardShortcutsHelp } from "components/KeyboardShortcutsHelp"
import { useBrowserExtension } from "hooks/useBrowserExtension"
import React, { useState } from "react"
import { TbHelp, TbKeyboard, TbPuzzle, TbRocket } from "react-icons/tb"
@@ -60,19 +61,26 @@ function NextUnreadBookmarklet() {
export function AboutPage() {
const version = useAppSelector(state => state.server.serverInfos?.version)
const revision = useAppSelector(state => state.server.serverInfos?.gitCommit)
const { isBrowserExtensionInstalled, browserExtensionVersion, isBrowserExtensionInstallable } = useBrowserExtension()
const dispatch = useAppDispatch()
return (
<Container size="xl">
<SimpleGrid cols={2} breakpoints={[{ maxWidth: Constants.layout.mobileBreakpoint, cols: 1 }]}>
<Section title={<Trans>About</Trans>} icon={<TbHelp size={24} />}>
<Box>
<Trans>
CommaFeed version {version} ({revision})
CommaFeed version {version} ({revision}).
</Trans>
</Box>
{isBrowserExtensionInstallable && isBrowserExtensionInstalled && (
<Box>
<Trans>CommaFeed browser extension version {browserExtensionVersion}.</Trans>
</Box>
)}
<Box mt="md">
<Trans>
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">
GitHub
</Anchor>
@@ -86,8 +94,8 @@ export function AboutPage() {
<Section title={<Trans>Goodies</Trans>} icon={<TbPuzzle size={24} />}>
<List>
<List.Item>
<Anchor href="https://github.com/Athou/commafeed-browser-extension" target="_blank" rel="noreferrer">
<Trans>Browser extentions</Trans>
<Anchor href={Constants.browserExtensionUrl} target="_blank" rel="noreferrer">
<Trans>Browser extention</Trans>
</Anchor>
</List.Item>
<List.Item>

View File

@@ -1,8 +1,14 @@
import { Box } from "@mantine/core"
import SwaggerUI from "swagger-ui-react"
import "swagger-ui-react/swagger-ui.css"
function ApiDocumentationPage() {
return <SwaggerUI url="swagger/swagger.json" />
return (
// force white background because swagger is unreadable with dark theme
<Box style={{ backgroundColor: "#fff" }}>
<SwaggerUI url="swagger/swagger.json" />
</Box>
)
}
export default ApiDocumentationPage

View File

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

View File

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

View File

@@ -1,13 +1,41 @@
import { lingui } from "@lingui/vite-plugin"
import react from "@vitejs/plugin-react"
import { visualizer } from "rollup-plugin-visualizer"
import { defineConfig } from "vite"
import { defineConfig, PluginOption } from "vite"
import eslint from "vite-plugin-eslint"
import tsconfigPaths from "vite-tsconfig-paths"
// inject custom js and css links in html
const customCodeInjector: PluginOption = {
name: "customCodeInjector",
transformIndexHtml: html => {
return {
html,
tags: [
{
tag: "script",
attrs: {
src: "custom_js.js",
},
injectTo: "body",
},
{
tag: "link",
attrs: {
rel: "stylesheet",
href: "custom_css.css",
},
injectTo: "head",
},
],
}
},
}
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
customCodeInjector,
react({
babel: {
// babel-macro is needed for lingui
@@ -24,6 +52,7 @@ export default defineConfig({
port: 8082,
proxy: {
"/rest": "http://localhost:8083",
"/next": "http://localhost:8083",
"/ws": "ws://localhost:8083",
"/swagger": "http://localhost:8083",
"/custom_css.css": "http://localhost:8083",
@@ -31,7 +60,7 @@ export default defineConfig({
},
},
build: {
chunkSizeWarningLimit: 1000,
chunkSizeWarningLimit: 3000,
rollupOptions: {
output: {
manualChunks: id => {

View File

@@ -3,6 +3,9 @@
app:
# url used to access commafeed
publicUrl: http://localhost:8082/
# whether to expose a robots.txt file that disallows web crawlers and search engine indexers
hideFromWebCrawlers: true
# whether to allow user registrations
allowRegistrations: true
@@ -92,7 +95,7 @@ app:
database:
driverClass: org.h2.Driver
url: jdbc:h2:./target/example
url: jdbc:h2:./target/commafeed
user: sa
password: sa
properties:
@@ -114,7 +117,6 @@ logging:
liquibase: INFO
org.hibernate.SQL: INFO # or ALL for sql debugging
org.hibernate.engine.internal.StatisticalLoggingSessionEventListener: WARN
org.hibernate.orm.deprecation: "OFF"
appenders:
- type: console
- type: file

View File

@@ -3,6 +3,9 @@
app:
# url used to access commafeed
publicUrl: http://localhost:8082/
# whether to expose a robots.txt file that disallows web crawlers and search engine indexers
hideFromWebCrawlers: true
# whether to allow user registrations
allowRegistrations: false
@@ -93,7 +96,7 @@ app:
database:
driverClass: org.h2.Driver
url: jdbc:h2:/commafeed/data/db
url: jdbc:h2:./db/commafeed
user: sa
password: sa
properties:
@@ -119,7 +122,6 @@ logging:
com.commafeed: INFO
liquibase: INFO
io.dropwizard.server.ServerFactory: INFO
org.hibernate.orm.deprecation: "OFF"
appenders:
- type: console
- type: file

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>com.commafeed</groupId>
<artifactId>commafeed</artifactId>
<version>3.5.0</version>
<version>3.8.0</version>
</parent>
<artifactId>commafeed-server</artifactId>
<name>CommaFeed Server</name>
@@ -226,7 +226,7 @@
<dependency>
<groupId>com.commafeed</groupId>
<artifactId>commafeed-client</artifactId>
<version>3.5.0</version>
<version>3.8.0</version>
</dependency>
<dependency>

View File

@@ -47,6 +47,7 @@ import com.commafeed.frontend.servlet.CustomCssServlet;
import com.commafeed.frontend.servlet.CustomJsServlet;
import com.commafeed.frontend.servlet.LogoutServlet;
import com.commafeed.frontend.servlet.NextUnreadServlet;
import com.commafeed.frontend.servlet.RobotsTxtDisallowAllServlet;
import com.commafeed.frontend.session.SessionHelperFactoryProvider;
import com.commafeed.frontend.ws.WebSocketConfigurator;
import com.commafeed.frontend.ws.WebSocketEndpoint;
@@ -169,6 +170,11 @@ public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
environment.servlets().addServlet("customCss", injector.getInstance(CustomCssServlet.class)).addMapping("/custom_css.css");
environment.servlets().addServlet("customJs", injector.getInstance(CustomJsServlet.class)).addMapping("/custom_js.js");
environment.servlets().addServlet("analytics.js", injector.getInstance(AnalyticsServlet.class)).addMapping("/analytics.js");
if (Boolean.TRUE.equals(config.getApplicationSettings().getHideFromWebCrawlers())) {
environment.servlets()
.addServlet("robots.txt", injector.getInstance(RobotsTxtDisallowAllServlet.class))
.addMapping("/robots.txt");
}
// WebSocket endpoint
ServerEndpointConfig serverEndpointConfig = ServerEndpointConfig.Builder.create(WebSocketEndpoint.class, "/ws")

View File

@@ -65,6 +65,10 @@ public class CommaFeedConfiguration extends Configuration {
@Valid
private String publicUrl;
@NotNull
@Valid
private Boolean hideFromWebCrawlers = true;
@NotNull
@Valid
private Boolean allowRegistrations;

View File

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

View File

@@ -86,10 +86,12 @@ public class FeedRefreshEngine implements Managed {
Feed feed = queue.take();
// send the feed to be processed
log.debug("got feed {} from the queue, send it for processing", feed.getId());
processFeedAsync(feed);
// we removed a feed from the queue, try to refill it as it may now be empty
if (queue.isEmpty()) {
log.debug("took the last feed from the queue, try to refill");
refillQueueAsync();
}
} catch (InterruptedException e) {
@@ -108,9 +110,11 @@ public class FeedRefreshEngine implements Managed {
while (!refillLoopExecutor.isShutdown()) {
try {
if (queue.isEmpty()) {
log.debug("refilling queue");
refillQueueAsync();
}
log.debug("sleeping for 15s");
TimeUnit.SECONDS.sleep(15);
} catch (InterruptedException e) {
log.debug("interrupted while sleeping");
@@ -123,6 +127,7 @@ public class FeedRefreshEngine implements Managed {
}
public void refreshImmediately(Feed feed) {
log.debug("add feed {} at the start of the queue", feed.getId());
// remove the feed from the queue if it was already queued to avoid refreshing it twice
queue.removeIf(f -> f.getId().equals(feed.getId()));
queue.addFirst(feed);
@@ -136,7 +141,9 @@ public class FeedRefreshEngine implements Managed {
refill.mark();
for (Feed feed : getNextUpdatableFeeds(getBatchSize())) {
List<Feed> nextUpdatableFeeds = getNextUpdatableFeeds(getBatchSize());
log.debug("found {} feeds that are up for refresh", nextUpdatableFeeds.size());
for (Feed feed : nextUpdatableFeeds) {
// add the feed only if it was not already queued
if (queue.stream().noneMatch(f -> f.getId().equals(feed.getId()))) {
queue.addLast(feed);
@@ -161,7 +168,10 @@ public class FeedRefreshEngine implements Managed {
private List<Feed> getNextUpdatableFeeds(int max) {
return unitOfWork.call(() -> {
List<Feed> feeds = feedDAO.findNextUpdatable(max);
Date lastLoginThreshold = Boolean.TRUE.equals(config.getApplicationSettings().getHeavyLoad())
? DateUtils.addDays(new Date(), -30)
: null;
List<Feed> feeds = feedDAO.findNextUpdatable(max, lastLoginThreshold);
// update disabledUntil to prevent feeds from being returned again by feedDAO.findNextUpdatable()
Date nextUpdateDate = DateUtils.addMinutes(new Date(), config.getApplicationSettings().getRefreshIntervalMinutes());
feedDAO.setDisabledUntil(feeds.stream().map(AbstractModel::getId).collect(Collectors.toList()), nextUpdateDate);

View File

@@ -38,6 +38,6 @@ public class FeedCategory extends AbstractModel {
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)
private Set<FeedEntryStatus> statuses;
private Integer position;
private int position;
@Column(name = "filtering_expression", length = 4096)
private String filter;

View File

@@ -8,8 +8,6 @@ import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import org.apache.commons.lang3.time.DateUtils;
import lombok.Getter;
import lombok.Setter;
@@ -49,17 +47,4 @@ public class User extends AbstractModel {
@Temporal(TemporalType.TIMESTAMP)
private Date recoverPasswordTokenDate;
@Column(name = "last_full_refresh")
@Temporal(TemporalType.TIMESTAMP)
private Date lastFullRefresh;
public boolean shouldRefreshFeedsAt(Date when) {
return lastFullRefresh == null || lastFullRefreshMoreThan30MinutesBefore(when);
}
private boolean lastFullRefreshMoreThan30MinutesBefore(Date when) {
return lastFullRefresh.before(DateUtils.addMinutes(when, -30));
}
}

View File

@@ -65,6 +65,8 @@ public class UserSettings extends AbstractModel {
@Column(name = "scroll_speed")
private int scrollSpeed;
private boolean alwaysScrollToEntry;
private boolean email;
private boolean gmail;
private boolean facebook;

View File

@@ -1,5 +1,6 @@
package com.commafeed.backend.service;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@@ -107,6 +108,17 @@ public class FeedSubscriptionService {
}
}
public void refreshAllUpForRefresh(User user) {
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user);
for (FeedSubscription sub : subs) {
Date disabledUntil = sub.getFeed().getDisabledUntil();
if (disabledUntil == null || disabledUntil.before(new Date())) {
Feed feed = sub.getFeed();
feedRefreshEngine.refreshImmediately(feed);
}
}
}
public Map<Long, UnreadCount> getUnreadCount(User user) {
return feedSubscriptionDAO.findAll(user).stream().collect(Collectors.toMap(FeedSubscription::getId, s -> getUnreadCount(user, s)));
}

View File

@@ -25,25 +25,20 @@ public class PostLoginActivities {
private final CommaFeedConfiguration config;
public void executeFor(User user) {
Date lastLogin = user.getLastLogin();
// only update lastLogin every once in a while in order to avoid invalidating the cache every time someone logs in
Date now = new Date();
boolean saveUser = false;
// only update lastLogin field every hour in order to not
// invalidate the cache every time someone logs in
if (lastLogin == null || lastLogin.before(DateUtils.addHours(now, -1))) {
Date lastLogin = user.getLastLogin();
if (lastLogin == null || lastLogin.before(DateUtils.addMinutes(now, -30))) {
user.setLastLogin(now);
saveUser = true;
}
if (Boolean.TRUE.equals(config.getApplicationSettings().getHeavyLoad()) && user.shouldRefreshFeedsAt(now)) {
feedSubscriptionService.refreshAll(user);
user.setLastFullRefresh(now);
saveUser = true;
}
boolean heavyLoad = Boolean.TRUE.equals(config.getApplicationSettings().getHeavyLoad());
if (heavyLoad) {
// the amount of feeds in the database that are up for refresh might be very large since we're in heavy load mode
// the feed refresh engine might not be able to catch up quickly enough
// put feeds from online users that are up for refresh at the top of the queue
feedSubscriptionService.refreshAllUpForRefresh(user);
}
if (saveUser) {
// Post login activites are susceptible to run for any webservice call.
// We update the user in a new transaction to update the user immediately.
// If we didn't and the webservice call takes time, subsequent webservice calls would have to wait for the first call to
@@ -51,5 +46,4 @@ public class PostLoginActivities {
unitOfWork.run(() -> userDAO.saveOrUpdate(user));
}
}
}

View File

@@ -35,5 +35,5 @@ public class Category implements Serializable {
private boolean expanded;
@ApiModelProperty(value = "position of the category in the list", required = true)
private Integer position;
private int position;
}

View File

@@ -35,6 +35,9 @@ public class Settings implements Serializable {
@ApiModelProperty(value = "user's preferred scroll speed when navigating between entries", required = true)
private int scrollSpeed;
@ApiModelProperty(value = "always scroll selected entry to the top of the page, even if it fits entirely on screen", required = true)
private boolean alwaysScrollToEntry;
@ApiModelProperty(value = "sharing settings", required = true)
private SharingSettings sharingSettings = new SharingSettings();

View File

@@ -54,7 +54,7 @@ public class Subscription implements Serializable {
private String categoryId;
@ApiModelProperty("position of the subscription's in the list")
private Integer position;
private int position;
@ApiModelProperty(value = "date of the newest item", dataType = "number")
private Date newestItemTime;

View File

@@ -458,7 +458,7 @@ public class CategoryREST {
category.getChildren().add(child);
}
}
Collections.sort(category.getChildren(), (o1, o2) -> ObjectUtils.compare(o1.getPosition(), o2.getPosition()));
category.getChildren().sort(Comparator.comparing(Category::getPosition).thenComparing(Category::getName));
for (FeedSubscription subscription : subscriptions) {
if (id == null && subscription.getCategory() == null
@@ -468,7 +468,7 @@ public class CategoryREST {
category.getFeeds().add(sub);
}
}
Collections.sort(category.getFeeds(), (o1, o2) -> ObjectUtils.compare(o1.getPosition(), o2.getPosition()));
category.getFeeds().sort(Comparator.comparing(Subscription::getPosition).thenComparing(Subscription::getName));
return category;
}

View File

@@ -103,6 +103,7 @@ public class UserREST {
s.setCustomJs(settings.getCustomJs());
s.setLanguage(settings.getLanguage());
s.setScrollSpeed(settings.getScrollSpeed());
s.setAlwaysScrollToEntry(settings.isAlwaysScrollToEntry());
} else {
s.setReadingMode(ReadingMode.unread.name());
s.setReadingOrder(ReadingOrder.desc.name());
@@ -120,6 +121,7 @@ public class UserREST {
s.setScrollMarks(true);
s.setLanguage("en");
s.setScrollSpeed(400);
s.setAlwaysScrollToEntry(false);
}
return Response.ok(s).build();
}
@@ -145,6 +147,7 @@ public class UserREST {
s.setCustomJs(settings.getCustomJs());
s.setLanguage(settings.getLanguage());
s.setScrollSpeed(settings.getScrollSpeed());
s.setAlwaysScrollToEntry(settings.isAlwaysScrollToEntry());
s.setEmail(settings.getSharingSettings().isEmail());
s.setGmail(settings.getSharingSettings().isGmail());

View File

@@ -23,6 +23,7 @@ import com.commafeed.backend.model.FeedEntryStatus;
import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.User;
import com.commafeed.backend.model.UserSettings.ReadingOrder;
import com.commafeed.backend.service.FeedEntryService;
import com.commafeed.backend.service.UserService;
import com.commafeed.frontend.resource.CategoryREST;
import com.commafeed.frontend.session.SessionHelper;
@@ -43,6 +44,7 @@ public class NextUnreadServlet extends HttpServlet {
private final FeedEntryStatusDAO feedEntryStatusDAO;
private final FeedCategoryDAO feedCategoryDAO;
private final UserService userService;
private final FeedEntryService feedEntryService;
private final CommaFeedConfiguration config;
@Override
@@ -80,8 +82,7 @@ public class NextUnreadServlet extends HttpServlet {
}
}
if (s != null) {
s.setRead(true);
feedEntryStatusDAO.saveOrUpdate(s);
feedEntryService.markEntry(user.get(), s.getEntry().getId(), true);
}
return s;
});

View File

@@ -0,0 +1,22 @@
package com.commafeed.frontend.servlet;
import java.io.IOException;
import javax.inject.Singleton;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Singleton
public class RobotsTxtDisallowAllServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.setContentType("text/plain");
resp.getWriter().write("User-agent: *");
resp.getWriter().write("\n");
resp.getWriter().write("Disallow: /");
}
}

View File

@@ -5,9 +5,12 @@ import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.websocket.Session;
import com.codahale.metrics.Gauge;
import com.codahale.metrics.MetricRegistry;
import com.commafeed.backend.model.User;
import lombok.extern.slf4j.Slf4j;
@@ -16,15 +19,23 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j
public class WebSocketSessions {
// a user may have multiple sessions (two tabs, on mobile, ...)
// a user may have multiple sessions (two tabs, two devices, ...)
private final Map<Long, Set<Session>> sessions = new ConcurrentHashMap<>();
@Inject
public WebSocketSessions(MetricRegistry metrics) {
metrics.register(MetricRegistry.name(getClass(), "users"),
(Gauge<Long>) () -> sessions.values().stream().filter(v -> !v.isEmpty()).count());
metrics.register(MetricRegistry.name(getClass(), "sessions"),
(Gauge<Long>) () -> sessions.values().stream().mapToLong(Set::size).sum());
}
public void add(Long userId, Session session) {
sessions.computeIfAbsent(userId, v -> ConcurrentHashMap.newKeySet()).add(session);
}
public void remove(Session session) {
sessions.values().forEach(v -> v.removeIf(e -> e.equals(session)));
sessions.values().forEach(v -> v.remove(session));
}
public void sendMessage(User user, String text) {

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<changeSet id="position-required" author="athou">
<update tableName="FEEDCATEGORIES">
<column name="position" valueNumeric="0" />
<where>position is null</where>
</update>
<addNotNullConstraint tableName="FEEDCATEGORIES" columnName="position" columnDataType="int" />
<update tableName="FEEDSUBSCRIPTIONS">
<column name="position" valueNumeric="0" />
<where>position is null</where>
</update>
<addNotNullConstraint tableName="FEEDSUBSCRIPTIONS" columnName="position" columnDataType="int" />
</changeSet>
<changeSet id="drop-unused-last-full-refresh" author="athou">
<dropColumn tableName="USERS" columnName="last_full_refresh" />
</changeSet>
</databaseChangeLog>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<changeSet id="always-scroll-to-entry" author="athou">
<addColumn tableName="USERSETTINGS">
<column name="alwaysScrollToEntry" type="BOOLEAN" defaultValueBoolean="false">
<constraints nullable="false" />
</column>
</addColumn>
</changeSet>
</databaseChangeLog>

View File

@@ -19,5 +19,7 @@
<include file="changelogs/db.changelog-2.6.xml" />
<include file="changelogs/db.changelog-3.2.xml" />
<include file="changelogs/db.changelog-3.5.xml" />
<include file="changelogs/db.changelog-3.6.xml" />
<include file="changelogs/db.changelog-3.8.xml" />
</databaseChangeLog>

View File

@@ -3,6 +3,9 @@
app:
# url used to access commafeed
publicUrl: http://localhost:8082/
# whether to expose a robots.txt file that disallows web crawlers and search engine indexers
hideFromWebCrawlers: true
# whether to allow user registrations
allowRegistrations: true

View File

@@ -5,7 +5,7 @@
<groupId>com.commafeed</groupId>
<artifactId>commafeed</artifactId>
<version>3.5.0</version>
<version>3.8.0</version>
<name>CommaFeed</name>
<packaging>pom</packaging>

View File

@@ -13,8 +13,8 @@ if [[ "$BRANCH" != "master" ]]; then
exit
fi
# make sure README.md has been updated
read -r -p "Has README.md been updated? (Y/n) " CONFIRM
# make sure CHANGELOG.md has been updated
read -r -p "Has CHANGELOG.md been updated? (Y/n) " CONFIRM
case "$CONFIRM" in
n | N) exit ;;
esac