Compare commits

...

269 Commits
3.9.0 ... 4.3.1

Author SHA1 Message Date
Athou
fafd4c9d54 release 4.3.1 2024-02-12 16:13:34 +01:00
Jérémie Panzer
73b472bc8a Merge pull request #1242 from Athou/dependabot/npm_and_yarn/commafeed-client/mantine-42bf712b6b
Bump the mantine group in /commafeed-client with 6 updates
2024-02-12 11:00:20 +01:00
Jérémie Panzer
1c3be67f76 Merge pull request #1243 from Athou/dependabot/npm_and_yarn/commafeed-client/fontsource/open-sans-5.0.23
Bump @fontsource/open-sans from 5.0.22 to 5.0.23 in /commafeed-client
2024-02-12 10:59:21 +01:00
dependabot[bot]
2a5988b3e7 Bump @fontsource/open-sans from 5.0.22 to 5.0.23 in /commafeed-client
Bumps [@fontsource/open-sans](https://github.com/fontsource/font-files/tree/HEAD/fonts/google/open-sans) from 5.0.22 to 5.0.23.
- [Changelog](https://github.com/fontsource/font-files/blob/main/fonts/google/open-sans/CHANGELOG.md)
- [Commits](https://github.com/fontsource/font-files/commits/HEAD/fonts/google/open-sans)

---
updated-dependencies:
- dependency-name: "@fontsource/open-sans"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-12 09:30:55 +00:00
dependabot[bot]
c5757849f3 Bump the mantine group in /commafeed-client with 6 updates
Bumps the mantine group in /commafeed-client with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [@mantine/core](https://github.com/mantinedev/mantine/tree/HEAD/packages/@mantine/core) | `7.5.1` | `7.5.2` |
| [@mantine/form](https://github.com/mantinedev/mantine/tree/HEAD/packages/@mantine/form) | `7.5.1` | `7.5.2` |
| [@mantine/hooks](https://github.com/mantinedev/mantine/tree/HEAD/packages/@mantine/hooks) | `7.5.1` | `7.5.2` |
| [@mantine/modals](https://github.com/mantinedev/mantine/tree/HEAD/packages/@mantine/modals) | `7.5.1` | `7.5.2` |
| [@mantine/notifications](https://github.com/mantinedev/mantine/tree/HEAD/packages/@mantine/notifications) | `7.5.1` | `7.5.2` |
| [@mantine/spotlight](https://github.com/mantinedev/mantine/tree/HEAD/packages/@mantine/spotlight) | `7.5.1` | `7.5.2` |


Updates `@mantine/core` from 7.5.1 to 7.5.2
- [Release notes](https://github.com/mantinedev/mantine/releases)
- [Changelog](https://github.com/mantinedev/mantine/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mantinedev/mantine/commits/7.5.2/packages/@mantine/core)

Updates `@mantine/form` from 7.5.1 to 7.5.2
- [Release notes](https://github.com/mantinedev/mantine/releases)
- [Changelog](https://github.com/mantinedev/mantine/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mantinedev/mantine/commits/7.5.2/packages/@mantine/form)

Updates `@mantine/hooks` from 7.5.1 to 7.5.2
- [Release notes](https://github.com/mantinedev/mantine/releases)
- [Changelog](https://github.com/mantinedev/mantine/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mantinedev/mantine/commits/7.5.2/packages/@mantine/hooks)

Updates `@mantine/modals` from 7.5.1 to 7.5.2
- [Release notes](https://github.com/mantinedev/mantine/releases)
- [Changelog](https://github.com/mantinedev/mantine/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mantinedev/mantine/commits/7.5.2/packages/@mantine/modals)

Updates `@mantine/notifications` from 7.5.1 to 7.5.2
- [Release notes](https://github.com/mantinedev/mantine/releases)
- [Changelog](https://github.com/mantinedev/mantine/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mantinedev/mantine/commits/7.5.2/packages/@mantine/notifications)

Updates `@mantine/spotlight` from 7.5.1 to 7.5.2
- [Release notes](https://github.com/mantinedev/mantine/releases)
- [Changelog](https://github.com/mantinedev/mantine/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mantinedev/mantine/commits/7.5.2/packages/@mantine/spotlight)

---
updated-dependencies:
- dependency-name: "@mantine/core"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: mantine
- dependency-name: "@mantine/form"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: mantine
- dependency-name: "@mantine/hooks"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: mantine
- dependency-name: "@mantine/modals"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: mantine
- dependency-name: "@mantine/notifications"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: mantine
- dependency-name: "@mantine/spotlight"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: mantine
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-12 09:30:22 +00:00
Athou
b6107c3330 pass theme and colorscheme in tss context to avoid repetitions (#1241) 2024-02-12 07:38:57 +01:00
Athou
3efeed6c85 make sure videos don't overflow parent (#1240) 2024-02-10 21:50:17 +01:00
Athou
be44b0aad1 make sure timestamps are stored in UTC (#1239) 2024-02-10 12:42:22 +01:00
Athou
36152dc47f add an additional day to make sure the timestamp fits in all timezones (#1239) 2024-02-10 12:41:32 +01:00
Athou
32e9cd3e35 release 4.3.0 2024-02-09 20:02:17 +01:00
Athou
4bf8b5696d fix metrics page 2024-02-09 18:41:43 +01:00
Athou
0bf44dbc7b make sure we clean any existing file before starting 2024-02-09 18:32:43 +01:00
Athou
bda3ba4b5c mysql/mariadb lowest timestamp is actually 1970-01-01 00:00:01 (#1239) 2024-02-09 17:31:22 +01:00
Athou
23cff9c1e9 columnDataType is required for addNotNullConstraint on mysql 2024-02-09 17:19:47 +01:00
Athou
9691517335 add null check to userSessions 2024-02-09 17:18:18 +01:00
Jérémie Panzer
b38bd8c312 Merge pull request #1234 from Athou/dependabot/npm_and_yarn/commafeed-client/types/react-18.2.55
Bump @types/react from 18.2.53 to 18.2.55 in /commafeed-client
2024-02-09 11:10:17 +01:00
Jérémie Panzer
d8ca58389d Merge pull request #1238 from Athou/dependabot/npm_and_yarn/commafeed-client/vite-5.1.1
Bump vite from 5.0.12 to 5.1.1 in /commafeed-client
2024-02-09 11:09:44 +01:00
dependabot[bot]
20a0cd7192 Bump @types/react from 18.2.53 to 18.2.55 in /commafeed-client
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 18.2.53 to 18.2.55.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

---
updated-dependencies:
- dependency-name: "@types/react"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-09 10:06:24 +00:00
dependabot[bot]
9b895328be Bump vite from 5.0.12 to 5.1.1 in /commafeed-client
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.0.12 to 5.1.1.
- [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/v5.1.1/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-09 10:05:37 +00:00
Jérémie Panzer
cd39ab5f95 Merge pull request #1235 from Athou/dependabot/npm_and_yarn/commafeed-client/monaco-editor-0.46.0
Bump monaco-editor from 0.45.0 to 0.46.0 in /commafeed-client
2024-02-09 11:05:33 +01:00
Jérémie Panzer
a7152a97a6 Merge pull request #1237 from Athou/dependabot/npm_and_yarn/commafeed-client/typescript-eslint/eslint-plugin-6.21.0
Bump @typescript-eslint/eslint-plugin from 6.20.0 to 6.21.0 in /commafeed-client
2024-02-09 11:05:15 +01:00
Jérémie Panzer
3e6e0a0f00 Merge pull request #1233 from Athou/dependabot/npm_and_yarn/commafeed-client/types/react-dom-18.2.19
Bump @types/react-dom from 18.2.18 to 18.2.19 in /commafeed-client
2024-02-09 11:04:49 +01:00
dependabot[bot]
2936dd0d32 Bump @typescript-eslint/eslint-plugin in /commafeed-client
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 6.20.0 to 6.21.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.21.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-09 09:54:35 +00:00
dependabot[bot]
38a838d210 Bump monaco-editor from 0.45.0 to 0.46.0 in /commafeed-client
Bumps [monaco-editor](https://github.com/microsoft/monaco-editor) from 0.45.0 to 0.46.0.
- [Changelog](https://github.com/microsoft/monaco-editor/blob/main/CHANGELOG.md)
- [Commits](https://github.com/microsoft/monaco-editor/compare/v0.45.0...v0.46.0)

---
updated-dependencies:
- dependency-name: monaco-editor
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-09 09:53:12 +00:00
dependabot[bot]
0136fa883d Bump @types/react-dom from 18.2.18 to 18.2.19 in /commafeed-client
Bumps [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom) from 18.2.18 to 18.2.19.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

---
updated-dependencies:
- dependency-name: "@types/react-dom"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-09 09:52:12 +00:00
Athou
b0890df2f3 add a css class for view mode (#1232) 2024-02-09 10:31:27 +01:00
Athou
91acad0dbf don't try to migrate h2 if database does not exist yet 2024-02-09 10:30:15 +01:00
Athou
14e7d70106 simplify websocket session retrieval 2024-02-05 20:27:26 +01:00
Jérémie Panzer
1cc76ba3ee Merge pull request #1230 from Athou/dependabot/npm_and_yarn/commafeed-client/types/react-18.2.53
Bump @types/react from 18.2.52 to 18.2.53 in /commafeed-client
2024-02-05 09:46:37 +01:00
dependabot[bot]
206800c091 Bump @types/react from 18.2.52 to 18.2.53 in /commafeed-client
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 18.2.52 to 18.2.53.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

---
updated-dependencies:
- dependency-name: "@types/react"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-05 08:42:50 +00:00
Jérémie Panzer
33749c94e3 Merge pull request #1229 from Athou/dependabot/npm_and_yarn/commafeed-client/prettier-3.2.5
Bump prettier from 3.2.4 to 3.2.5 in /commafeed-client
2024-02-04 19:55:52 +01:00
Jérémie Panzer
8bce887e4c Merge pull request #1228 from Athou/dependabot/maven/io.github.hakky54-sslcontext-kickstart-for-apache5-8.3.1
Bump io.github.hakky54:sslcontext-kickstart-for-apache5 from 8.3.0 to 8.3.1
2024-02-04 19:55:46 +01:00
dependabot[bot]
ca4f73fff6 Bump prettier from 3.2.4 to 3.2.5 in /commafeed-client
Bumps [prettier](https://github.com/prettier/prettier) from 3.2.4 to 3.2.5.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.2.4...3.2.5)

---
updated-dependencies:
- dependency-name: prettier
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-04 18:47:35 +00:00
dependabot[bot]
26443310c9 Bump io.github.hakky54:sslcontext-kickstart-for-apache5
Bumps [io.github.hakky54:sslcontext-kickstart-for-apache5](https://github.com/Hakky54/sslcontext-kickstart) from 8.3.0 to 8.3.1.
- [Changelog](https://github.com/Hakky54/sslcontext-kickstart/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Hakky54/sslcontext-kickstart/compare/v8.3.0...v8.3.1)

---
updated-dependencies:
- dependency-name: io.github.hakky54:sslcontext-kickstart-for-apache5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-04 18:47:02 +00:00
Athou
870593bae8 add H2 migration tool 2024-02-04 18:40:59 +01:00
Jérémie Panzer
cfd5d0faab Merge pull request #1226 from aniol/patch-1
Update messages.po
2024-02-04 08:11:14 +01:00
Aniol
9391c05968 Update messages.po
updated catalan translation
2024-02-04 07:33:44 +01:00
Jérémie Panzer
a13c75981b Merge pull request #1221 from Athou/dependabot/npm_and_yarn/commafeed-client/react-router-dom-6.22.0
Bump react-router-dom from 6.21.1 to 6.22.0 in /commafeed-client
2024-02-03 17:21:28 +01:00
dependabot[bot]
a05baf63c1 Bump react-router-dom from 6.21.1 to 6.22.0 in /commafeed-client
Bumps [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) from 6.21.1 to 6.22.0.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@6.22.0/packages/react-router-dom)

---
updated-dependencies:
- dependency-name: react-router-dom
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-03 16:16:38 +00:00
Jérémie Panzer
32ce265cff Merge pull request #1222 from Athou/dependabot/npm_and_yarn/commafeed-client/prettier-3.2.4
Bump prettier from 3.1.1 to 3.2.4 in /commafeed-client
2024-02-03 17:16:23 +01:00
Jérémie Panzer
b2ad24e7f6 Merge pull request #1223 from Athou/dependabot/npm_and_yarn/commafeed-client/react-icons-5.0.1
Bump react-icons from 4.12.0 to 5.0.1 in /commafeed-client
2024-02-03 17:16:17 +01:00
Jérémie Panzer
fe626ebbe3 Merge pull request #1224 from Athou/dependabot/npm_and_yarn/commafeed-client/react-redux-9.1.0
Bump react-redux from 9.0.4 to 9.1.0 in /commafeed-client
2024-02-03 17:16:10 +01:00
Jérémie Panzer
4431a898a0 Merge pull request #1225 from Athou/dependabot/npm_and_yarn/commafeed-client/vitest-1.2.2
Bump vitest from 1.1.3 to 1.2.2 in /commafeed-client
2024-02-03 17:15:59 +01:00
dependabot[bot]
89bfcfa240 Bump vitest from 1.1.3 to 1.2.2 in /commafeed-client
Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 1.1.3 to 1.2.2.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v1.2.2/packages/vitest)

---
updated-dependencies:
- dependency-name: vitest
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-03 16:10:30 +00:00
dependabot[bot]
d046d26f4e Bump react-redux from 9.0.4 to 9.1.0 in /commafeed-client
Bumps [react-redux](https://github.com/reduxjs/react-redux) from 9.0.4 to 9.1.0.
- [Release notes](https://github.com/reduxjs/react-redux/releases)
- [Changelog](https://github.com/reduxjs/react-redux/blob/master/CHANGELOG.md)
- [Commits](https://github.com/reduxjs/react-redux/compare/v9.0.4...v9.1.0)

---
updated-dependencies:
- dependency-name: react-redux
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-03 16:10:14 +00:00
dependabot[bot]
26b634b1a3 Bump react-icons from 4.12.0 to 5.0.1 in /commafeed-client
Bumps [react-icons](https://github.com/react-icons/react-icons) from 4.12.0 to 5.0.1.
- [Release notes](https://github.com/react-icons/react-icons/releases)
- [Commits](https://github.com/react-icons/react-icons/compare/v4.12.0...v5.0.1)

---
updated-dependencies:
- dependency-name: react-icons
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-03 16:09:58 +00:00
dependabot[bot]
3ca18bbd36 Bump prettier from 3.1.1 to 3.2.4 in /commafeed-client
Bumps [prettier](https://github.com/prettier/prettier) from 3.1.1 to 3.2.4.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.1.1...3.2.4)

---
updated-dependencies:
- dependency-name: prettier
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-03 16:09:46 +00:00
Jérémie Panzer
7645731fff Merge pull request #1211 from Athou/dependabot/npm_and_yarn/commafeed-client/mantine-5b631ac874
Bump the mantine group in /commafeed-client with 6 updates
2024-02-03 11:51:24 +01:00
Athou
3c116dbabe fix build 2024-02-03 11:44:58 +01:00
dependabot[bot]
3026fd116c Bump the mantine group in /commafeed-client with 6 updates
Bumps the mantine group in /commafeed-client with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [@mantine/core](https://github.com/mantinedev/mantine/tree/HEAD/packages/@mantine/core) | `7.3.2` | `7.5.1` |
| [@mantine/form](https://github.com/mantinedev/mantine/tree/HEAD/packages/@mantine/form) | `7.3.2` | `7.5.1` |
| [@mantine/hooks](https://github.com/mantinedev/mantine/tree/HEAD/packages/@mantine/hooks) | `7.3.2` | `7.5.1` |
| [@mantine/modals](https://github.com/mantinedev/mantine/tree/HEAD/packages/@mantine/modals) | `7.3.2` | `7.5.1` |
| [@mantine/notifications](https://github.com/mantinedev/mantine/tree/HEAD/packages/@mantine/notifications) | `7.3.2` | `7.5.1` |
| [@mantine/spotlight](https://github.com/mantinedev/mantine/tree/HEAD/packages/@mantine/spotlight) | `7.3.2` | `7.5.1` |


Updates `@mantine/core` from 7.3.2 to 7.5.1
- [Release notes](https://github.com/mantinedev/mantine/releases)
- [Changelog](https://github.com/mantinedev/mantine/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mantinedev/mantine/commits/7.5.1/packages/@mantine/core)

Updates `@mantine/form` from 7.3.2 to 7.5.1
- [Release notes](https://github.com/mantinedev/mantine/releases)
- [Changelog](https://github.com/mantinedev/mantine/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mantinedev/mantine/commits/7.5.1/packages/@mantine/form)

Updates `@mantine/hooks` from 7.3.2 to 7.5.1
- [Release notes](https://github.com/mantinedev/mantine/releases)
- [Changelog](https://github.com/mantinedev/mantine/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mantinedev/mantine/commits/7.5.1/packages/@mantine/hooks)

Updates `@mantine/modals` from 7.3.2 to 7.5.1
- [Release notes](https://github.com/mantinedev/mantine/releases)
- [Changelog](https://github.com/mantinedev/mantine/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mantinedev/mantine/commits/7.5.1/packages/@mantine/modals)

Updates `@mantine/notifications` from 7.3.2 to 7.5.1
- [Release notes](https://github.com/mantinedev/mantine/releases)
- [Changelog](https://github.com/mantinedev/mantine/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mantinedev/mantine/commits/7.5.1/packages/@mantine/notifications)

Updates `@mantine/spotlight` from 7.3.2 to 7.5.1
- [Release notes](https://github.com/mantinedev/mantine/releases)
- [Changelog](https://github.com/mantinedev/mantine/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mantinedev/mantine/commits/7.5.1/packages/@mantine/spotlight)

---
updated-dependencies:
- dependency-name: "@mantine/core"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: mantine
- dependency-name: "@mantine/form"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: mantine
- dependency-name: "@mantine/hooks"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: mantine
- dependency-name: "@mantine/modals"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: mantine
- dependency-name: "@mantine/notifications"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: mantine
- dependency-name: "@mantine/spotlight"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: mantine
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-03 09:20:11 +00:00
Jérémie Panzer
63fa725a13 Merge pull request #1220 from Athou/dependabot/npm_and_yarn/commafeed-client/reduxjs/toolkit-2.1.0
Bump @reduxjs/toolkit from 2.0.1 to 2.1.0 in /commafeed-client
2024-02-03 10:19:05 +01:00
Jérémie Panzer
ede4e07ff3 Merge pull request #1218 from Athou/dependabot/npm_and_yarn/commafeed-client/eslint-config-standard-with-typescript-43.0.1
Bump eslint-config-standard-with-typescript from 43.0.0 to 43.0.1 in /commafeed-client
2024-02-03 10:18:56 +01:00
dependabot[bot]
de6dfbe8b2 Bump @reduxjs/toolkit from 2.0.1 to 2.1.0 in /commafeed-client
Bumps [@reduxjs/toolkit](https://github.com/reduxjs/redux-toolkit) from 2.0.1 to 2.1.0.
- [Release notes](https://github.com/reduxjs/redux-toolkit/releases)
- [Commits](https://github.com/reduxjs/redux-toolkit/compare/v2.0.1...v2.1.0)

---
updated-dependencies:
- dependency-name: "@reduxjs/toolkit"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-03 09:13:40 +00:00
Jérémie Panzer
164a57bef5 Merge pull request #1217 from Athou/dependabot/npm_and_yarn/commafeed-client/types/react-18.2.52
Bump @types/react from 18.2.46 to 18.2.52 in /commafeed-client
2024-02-03 10:13:03 +01:00
Jérémie Panzer
fd82b8aaee Merge pull request #1216 from Athou/dependabot/npm_and_yarn/commafeed-client/fontsource/open-sans-5.0.22
Bump @fontsource/open-sans from 5.0.20 to 5.0.22 in /commafeed-client
2024-02-03 10:12:53 +01:00
Jérémie Panzer
facf8b43f2 Merge pull request #1214 from Athou/dependabot/npm_and_yarn/commafeed-client/axios-1.6.7
Bump axios from 1.6.3 to 1.6.7 in /commafeed-client
2024-02-03 10:12:38 +01:00
Jérémie Panzer
3184dfe178 Merge pull request #1213 from Athou/dependabot/npm_and_yarn/commafeed-client/vite-tsconfig-paths-4.3.1
Bump vite-tsconfig-paths from 4.2.3 to 4.3.1 in /commafeed-client
2024-02-03 10:12:27 +01:00
Jérémie Panzer
cc584fd8c8 Merge pull request #1212 from Athou/dependabot/npm_and_yarn/commafeed-client/tss-react-4.9.4
Bump tss-react from 4.9.3 to 4.9.4 in /commafeed-client
2024-02-03 10:12:10 +01:00
Jérémie Panzer
0f8fa1f2e1 Merge pull request #1210 from Athou/dependabot/maven/com.microsoft.playwright-playwright-1.41.2
Bump com.microsoft.playwright:playwright from 1.41.1 to 1.41.2
2024-02-03 10:11:47 +01:00
Jérémie Panzer
d93f7bd20e Merge pull request #1219 from Athou/dependabot/npm_and_yarn/commafeed-client/typescript-eslint/eslint-plugin-6.20.0
Bump @typescript-eslint/eslint-plugin from 6.16.0 to 6.20.0 in /commafeed-client
2024-02-03 10:11:17 +01:00
dependabot[bot]
96aa06d2dd Bump eslint-config-standard-with-typescript in /commafeed-client
Bumps [eslint-config-standard-with-typescript](https://github.com/mightyiam/eslint-config-standard-with-typescript) from 43.0.0 to 43.0.1.
- [Release notes](https://github.com/mightyiam/eslint-config-standard-with-typescript/releases)
- [Changelog](https://github.com/mightyiam/eslint-config-standard-with-typescript/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mightyiam/eslint-config-standard-with-typescript/compare/v43.0.0...v43.0.1)

---
updated-dependencies:
- dependency-name: eslint-config-standard-with-typescript
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-03 09:10:57 +00:00
Jérémie Panzer
fdaff46008 Merge pull request #1215 from Athou/dependabot/npm_and_yarn/commafeed-client/eslint-plugin-prettier-5.1.3
Bump eslint-plugin-prettier from 5.1.2 to 5.1.3 in /commafeed-client
2024-02-03 10:10:12 +01:00
dependabot[bot]
71066cd768 Bump @typescript-eslint/eslint-plugin in /commafeed-client
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 6.16.0 to 6.20.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.20.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-03 08:15:57 +00:00
dependabot[bot]
b9610a9058 Bump @types/react from 18.2.46 to 18.2.52 in /commafeed-client
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 18.2.46 to 18.2.52.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

---
updated-dependencies:
- dependency-name: "@types/react"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-03 08:15:29 +00:00
dependabot[bot]
ca027d5a4d Bump @fontsource/open-sans from 5.0.20 to 5.0.22 in /commafeed-client
Bumps [@fontsource/open-sans](https://github.com/fontsource/font-files/tree/HEAD/fonts/google/open-sans) from 5.0.20 to 5.0.22.
- [Changelog](https://github.com/fontsource/font-files/blob/main/fonts/google/open-sans/CHANGELOG.md)
- [Commits](https://github.com/fontsource/font-files/commits/HEAD/fonts/google/open-sans)

---
updated-dependencies:
- dependency-name: "@fontsource/open-sans"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-03 08:15:21 +00:00
dependabot[bot]
1dea51c705 Bump eslint-plugin-prettier from 5.1.2 to 5.1.3 in /commafeed-client
Bumps [eslint-plugin-prettier](https://github.com/prettier/eslint-plugin-prettier) from 5.1.2 to 5.1.3.
- [Release notes](https://github.com/prettier/eslint-plugin-prettier/releases)
- [Changelog](https://github.com/prettier/eslint-plugin-prettier/blob/master/CHANGELOG.md)
- [Commits](https://github.com/prettier/eslint-plugin-prettier/compare/v5.1.2...v5.1.3)

---
updated-dependencies:
- dependency-name: eslint-plugin-prettier
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-03 08:15:11 +00:00
dependabot[bot]
8edc89f3cc Bump axios from 1.6.3 to 1.6.7 in /commafeed-client
Bumps [axios](https://github.com/axios/axios) from 1.6.3 to 1.6.7.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.6.3...v1.6.7)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-03 08:15:02 +00:00
dependabot[bot]
4cbf32cbb8 Bump vite-tsconfig-paths from 4.2.3 to 4.3.1 in /commafeed-client
Bumps [vite-tsconfig-paths](https://github.com/aleclarson/vite-tsconfig-paths) from 4.2.3 to 4.3.1.
- [Release notes](https://github.com/aleclarson/vite-tsconfig-paths/releases)
- [Commits](https://github.com/aleclarson/vite-tsconfig-paths/compare/v4.2.3...v4.3.1)

---
updated-dependencies:
- dependency-name: vite-tsconfig-paths
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-03 08:14:53 +00:00
dependabot[bot]
a5dc551b6b Bump tss-react from 4.9.3 to 4.9.4 in /commafeed-client
Bumps [tss-react](https://github.com/garronej/tss-react) from 4.9.3 to 4.9.4.
- [Release notes](https://github.com/garronej/tss-react/releases)
- [Commits](https://github.com/garronej/tss-react/compare/v4.9.3...v4.9.4)

---
updated-dependencies:
- dependency-name: tss-react
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-03 08:14:43 +00:00
dependabot[bot]
b1e0dbd0b3 Bump com.microsoft.playwright:playwright from 1.41.1 to 1.41.2
Bumps [com.microsoft.playwright:playwright](https://github.com/microsoft/playwright-java) from 1.41.1 to 1.41.2.
- [Release notes](https://github.com/microsoft/playwright-java/releases)
- [Commits](https://github.com/microsoft/playwright-java/compare/v1.41.1...v1.41.2)

---
updated-dependencies:
- dependency-name: com.microsoft.playwright:playwright
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-03 08:13:26 +00:00
Athou
58789b15a3 configure dependabot for client dependencies 2024-02-03 09:13:00 +01:00
Athou
c5f58a2fe9 add test to make sure the session has been invalidated 2024-02-02 20:25:27 +01:00
Athou
253ba5f18b remove unused eslint plugins 2024-01-31 20:13:52 +01:00
Athou
ae859178c0 increase pull request limit 2024-01-30 22:42:55 +01:00
Jérémie Panzer
942dc0befe Merge pull request #1206 from Athou/dependabot/maven/com.microsoft.playwright-playwright-1.41.1
Bump com.microsoft.playwright:playwright from 1.40.0 to 1.41.1
2024-01-30 22:42:02 +01:00
Jérémie Panzer
66c4510fd3 Merge pull request #1205 from Athou/dependabot/maven/org.jsoup-jsoup-1.17.2
Bump org.jsoup:jsoup from 1.17.1 to 1.17.2
2024-01-30 22:41:48 +01:00
Jérémie Panzer
feb7de504c Merge pull request #1204 from Athou/dependabot/maven/io.dropwizard-dropwizard-dependencies-4.0.6
Bump io.dropwizard:dropwizard-dependencies from 4.0.5 to 4.0.6
2024-01-30 22:41:38 +01:00
Jérémie Panzer
ec4b809ff9 Merge pull request #1207 from Athou/dependabot/maven/org.gwtproject-gwt-servlet-2.11.0
Bump org.gwtproject:gwt-servlet from 2.10.0 to 2.11.0
2024-01-30 22:41:28 +01:00
Jérémie Panzer
b8d6a5742b Merge pull request #1203 from Athou/dependabot/maven/com.mysql-mysql-connector-j-8.3.0
Bump com.mysql:mysql-connector-j from 8.2.0 to 8.3.0
2024-01-30 22:41:17 +01:00
dependabot[bot]
31c42403a1 Bump org.gwtproject:gwt-servlet from 2.10.0 to 2.11.0
Bumps org.gwtproject:gwt-servlet from 2.10.0 to 2.11.0.

---
updated-dependencies:
- dependency-name: org.gwtproject:gwt-servlet
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-30 21:36:11 +00:00
dependabot[bot]
ead97be3cf Bump com.microsoft.playwright:playwright from 1.40.0 to 1.41.1
Bumps [com.microsoft.playwright:playwright](https://github.com/microsoft/playwright-java) from 1.40.0 to 1.41.1.
- [Release notes](https://github.com/microsoft/playwright-java/releases)
- [Commits](https://github.com/microsoft/playwright-java/compare/v1.40.0...v1.41.1)

---
updated-dependencies:
- dependency-name: com.microsoft.playwright:playwright
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-30 21:36:05 +00:00
dependabot[bot]
3ee43f75d6 Bump org.jsoup:jsoup from 1.17.1 to 1.17.2
Bumps [org.jsoup:jsoup](https://github.com/jhy/jsoup) from 1.17.1 to 1.17.2.
- [Release notes](https://github.com/jhy/jsoup/releases)
- [Changelog](https://github.com/jhy/jsoup/blob/master/CHANGES.md)
- [Commits](https://github.com/jhy/jsoup/compare/jsoup-1.17.1...jsoup-1.17.2)

---
updated-dependencies:
- dependency-name: org.jsoup:jsoup
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-30 21:35:53 +00:00
dependabot[bot]
f77b91540d Bump io.dropwizard:dropwizard-dependencies from 4.0.5 to 4.0.6
Bumps io.dropwizard:dropwizard-dependencies from 4.0.5 to 4.0.6.

---
updated-dependencies:
- dependency-name: io.dropwizard:dropwizard-dependencies
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-30 21:35:34 +00:00
dependabot[bot]
34915c93b8 Bump com.mysql:mysql-connector-j from 8.2.0 to 8.3.0
Bumps [com.mysql:mysql-connector-j](https://github.com/mysql/mysql-connector-j) from 8.2.0 to 8.3.0.
- [Changelog](https://github.com/mysql/mysql-connector-j/blob/release/8.x/CHANGES)
- [Commits](https://github.com/mysql/mysql-connector-j/compare/8.2.0...8.3.0)

---
updated-dependencies:
- dependency-name: com.mysql:mysql-connector-j
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-30 21:35:26 +00:00
Jérémie Panzer
365044d205 Merge pull request #1202 from Athou/dependabot/maven/io.github.hakky54-sslcontext-kickstart-for-apache5-8.3.0
Bump io.github.hakky54:sslcontext-kickstart-for-apache5 from 8.2.0 to 8.3.0
2024-01-30 22:33:57 +01:00
Jérémie Panzer
46f84ab29e Merge pull request #1198 from Athou/dependabot/maven/org.apache.maven.plugins-maven-surefire-plugin-3.2.5
Bump org.apache.maven.plugins:maven-surefire-plugin from 3.2.3 to 3.2.5
2024-01-30 22:33:50 +01:00
Jérémie Panzer
2041823f0d Merge pull request #1199 from Athou/dependabot/maven/org.mariadb.jdbc-mariadb-java-client-3.3.2
Bump org.mariadb.jdbc:mariadb-java-client from 3.3.1 to 3.3.2
2024-01-30 22:33:42 +01:00
Jérémie Panzer
ff01d7a87c Merge pull request #1200 from Athou/dependabot/maven/querydsl.version-5.1.0
Bump querydsl.version from 5.0.0 to 5.1.0
2024-01-30 22:33:35 +01:00
Jérémie Panzer
4d905b118a Merge pull request #1201 from Athou/dependabot/maven/com.diffplug.spotless-spotless-maven-plugin-2.43.0
Bump com.diffplug.spotless:spotless-maven-plugin from 2.41.1 to 2.43.0
2024-01-30 22:33:21 +01:00
Athou
6d74b50751 login to docker hub only if we need to be logged in 2024-01-30 22:28:05 +01:00
dependabot[bot]
f12bdf5841 Bump io.github.hakky54:sslcontext-kickstart-for-apache5
Bumps [io.github.hakky54:sslcontext-kickstart-for-apache5](https://github.com/Hakky54/sslcontext-kickstart) from 8.2.0 to 8.3.0.
- [Changelog](https://github.com/Hakky54/sslcontext-kickstart/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Hakky54/sslcontext-kickstart/compare/v8.2.0...v8.3.0)

---
updated-dependencies:
- dependency-name: io.github.hakky54:sslcontext-kickstart-for-apache5
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-30 21:16:26 +00:00
dependabot[bot]
6b89e211d8 Bump com.diffplug.spotless:spotless-maven-plugin from 2.41.1 to 2.43.0
Bumps [com.diffplug.spotless:spotless-maven-plugin](https://github.com/diffplug/spotless) from 2.41.1 to 2.43.0.
- [Changelog](https://github.com/diffplug/spotless/blob/main/CHANGES.md)
- [Commits](https://github.com/diffplug/spotless/compare/maven/2.41.1...lib/2.43.0)

---
updated-dependencies:
- dependency-name: com.diffplug.spotless:spotless-maven-plugin
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-30 21:16:21 +00:00
dependabot[bot]
1b00e5613a Bump querydsl.version from 5.0.0 to 5.1.0
Bumps `querydsl.version` from 5.0.0 to 5.1.0.

Updates `com.querydsl:querydsl-apt` from 5.0.0 to 5.1.0

Updates `com.querydsl:querydsl-jpa` from 5.0.0 to 5.1.0

---
updated-dependencies:
- dependency-name: com.querydsl:querydsl-apt
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: com.querydsl:querydsl-jpa
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-30 21:16:16 +00:00
dependabot[bot]
050a8b24fc Bump org.mariadb.jdbc:mariadb-java-client from 3.3.1 to 3.3.2
Bumps [org.mariadb.jdbc:mariadb-java-client](https://github.com/mariadb-corporation/mariadb-connector-j) from 3.3.1 to 3.3.2.
- [Release notes](https://github.com/mariadb-corporation/mariadb-connector-j/releases)
- [Changelog](https://github.com/mariadb-corporation/mariadb-connector-j/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mariadb-corporation/mariadb-connector-j/compare/3.3.1...3.3.2)

---
updated-dependencies:
- dependency-name: org.mariadb.jdbc:mariadb-java-client
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-30 21:16:12 +00:00
dependabot[bot]
c5b1ea486c Bump org.apache.maven.plugins:maven-surefire-plugin from 3.2.3 to 3.2.5
Bumps [org.apache.maven.plugins:maven-surefire-plugin](https://github.com/apache/maven-surefire) from 3.2.3 to 3.2.5.
- [Release notes](https://github.com/apache/maven-surefire/releases)
- [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.2.3...surefire-3.2.5)

---
updated-dependencies:
- dependency-name: org.apache.maven.plugins:maven-surefire-plugin
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-30 21:16:04 +00:00
Jérémie Panzer
7b8c0ac6ff Merge pull request #1193 from Athou/dependabot/maven/com.google.apis-google-api-services-youtube-v3-rev20240123-2.0.0
Bump com.google.apis:google-api-services-youtube from v3-rev20231011-2.0.0 to v3-rev20240123-2.0.0
2024-01-30 22:12:22 +01:00
Jérémie Panzer
d43039cf9f Merge pull request #1197 from Athou/dependabot/maven/org.apache.maven.plugins-maven-compiler-plugin-3.12.1
Bump org.apache.maven.plugins:maven-compiler-plugin from 3.11.0 to 3.12.1
2024-01-30 22:12:14 +01:00
Jérémie Panzer
3adc043740 Merge pull request #1196 from Athou/dependabot/maven/org.apache.maven.plugins-maven-failsafe-plugin-3.2.5
Bump org.apache.maven.plugins:maven-failsafe-plugin from 3.2.3 to 3.2.5
2024-01-30 22:11:36 +01:00
Jérémie Panzer
08b95ff3dd Merge pull request #1195 from Athou/dependabot/maven/io.swagger.core.v3-swagger-maven-plugin-jakarta-2.2.20
Bump io.swagger.core.v3:swagger-maven-plugin-jakarta from 2.2.19 to 2.2.20
2024-01-30 22:11:28 +01:00
Jérémie Panzer
e043ce71c3 Merge pull request #1194 from Athou/dependabot/maven/io.swagger.core.v3-swagger-annotations-2.2.20
Bump io.swagger.core.v3:swagger-annotations from 2.2.19 to 2.2.20
2024-01-30 22:11:17 +01:00
Athou
b345319f68 don't try to login to docker hub for pull requests 2024-01-30 22:09:29 +01:00
dependabot[bot]
4c298df9c9 Bump org.apache.maven.plugins:maven-compiler-plugin
Bumps [org.apache.maven.plugins:maven-compiler-plugin](https://github.com/apache/maven-compiler-plugin) from 3.11.0 to 3.12.1.
- [Release notes](https://github.com/apache/maven-compiler-plugin/releases)
- [Commits](https://github.com/apache/maven-compiler-plugin/compare/maven-compiler-plugin-3.11.0...maven-compiler-plugin-3.12.1)

---
updated-dependencies:
- dependency-name: org.apache.maven.plugins:maven-compiler-plugin
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-30 21:03:03 +00:00
dependabot[bot]
067e01660a Bump org.apache.maven.plugins:maven-failsafe-plugin from 3.2.3 to 3.2.5
Bumps [org.apache.maven.plugins:maven-failsafe-plugin](https://github.com/apache/maven-surefire) from 3.2.3 to 3.2.5.
- [Release notes](https://github.com/apache/maven-surefire/releases)
- [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.2.3...surefire-3.2.5)

---
updated-dependencies:
- dependency-name: org.apache.maven.plugins:maven-failsafe-plugin
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-30 21:02:57 +00:00
dependabot[bot]
c649a04891 Bump io.swagger.core.v3:swagger-maven-plugin-jakarta
Bumps io.swagger.core.v3:swagger-maven-plugin-jakarta from 2.2.19 to 2.2.20.

---
updated-dependencies:
- dependency-name: io.swagger.core.v3:swagger-maven-plugin-jakarta
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-30 21:02:51 +00:00
dependabot[bot]
9b9a4f98f4 Bump io.swagger.core.v3:swagger-annotations from 2.2.19 to 2.2.20
Bumps io.swagger.core.v3:swagger-annotations from 2.2.19 to 2.2.20.

---
updated-dependencies:
- dependency-name: io.swagger.core.v3:swagger-annotations
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-30 21:02:48 +00:00
dependabot[bot]
f8b6f2f237 Bump com.google.apis:google-api-services-youtube
Bumps com.google.apis:google-api-services-youtube from v3-rev20231011-2.0.0 to v3-rev20240123-2.0.0.

---
updated-dependencies:
- dependency-name: com.google.apis:google-api-services-youtube
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-30 21:02:44 +00:00
Jérémie Panzer
371ce0d160 Create dependabot.yml 2024-01-30 22:01:04 +01:00
Athou
a92a7217ff add a setting to completely disable scrolling to selected entry (#1157) 2024-01-29 20:30:49 +01:00
Athou
e69c230678 bump github action versions to remove warnings 2024-01-26 10:11:41 +01:00
Athou
b82077d3ca release 4.2.1 2024-01-26 09:54:07 +01:00
Athou
c624955ea4 websocket notification now takes entry filtering into account (#1191) 2024-01-24 15:47:37 +01:00
Athou
9354fb8e18 release 4.2.0 2024-01-22 15:07:17 +01:00
Jérémie Panzer
664ed317a0 Merge pull request #1189 from Athou/dependabot/npm_and_yarn/commafeed-client/vite-5.0.12
Bump vite from 5.0.11 to 5.0.12 in /commafeed-client
2024-01-20 08:33:52 +01:00
dependabot[bot]
5bf121782b Bump vite from 5.0.11 to 5.0.12 in /commafeed-client
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.0.11 to 5.0.12.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.0.12/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.0.12/packages/vite)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-20 07:29:09 +00:00
Athou
66c361e6a6 no need to render the header twice 2024-01-18 09:39:53 +01:00
Athou
0946c0248e show footer on the bottom of the page on mobile (#1121) 2024-01-18 09:29:12 +01:00
Athou
a8be8f2edf remove unnecessary usage of headerHeight 2024-01-18 09:05:32 +01:00
Athou
99db85328b scroll to the correct position regardless of the position or height of the header 2024-01-18 09:05:32 +01:00
Athou
5f29838bd2 clarify some descriptions in the profile settings 2024-01-16 07:07:49 +01:00
Athou
7d2c0e7576 remove the license from the README because there's already a LICENSE file 2024-01-15 11:10:01 +01:00
Athou
b8211e69e9 remove websocket bundle because it doesn't add much, use jetty directly 2024-01-15 09:53:52 +01:00
Athou
d7b2c5a6e3 add fix for fever api for Unread (#1188) 2024-01-14 18:34:50 +01:00
Athou
18358d5991 don't return last_refreshed_on_time when not set 2024-01-14 18:21:52 +01:00
Athou
e9b4895b0f move timezone logic to a reusable timestamp_type property 2024-01-13 17:23:16 +01:00
Athou
c4fbf98200 convert datetime fields to timestamp fields since we want to store UTC timestamps (#1187) 2024-01-13 14:58:03 +01:00
Athou
b0aa6ae524 the "new-feed-entries" websocket event no longer needs to reload the entire tree 2024-01-13 09:20:56 +01:00
Athou
11dd151a3b fix typo 2024-01-12 08:22:36 +01:00
Athou
874e7dcee6 release 4.1.0 2024-01-12 08:15:40 +01:00
Athou
8297edaf71 redirect to login page instead of welcome page if allowRegistrations is false (#1185) 2024-01-11 16:44:40 +01:00
Athou
9e4e629a1a prevent caching openapi files, so that the documentation is always up to date 2024-01-11 08:01:51 +01:00
Athou
8b86617f18 marking an entry as read/unread now requires to swipe to the left since swiping to the right now opens the mobile menu 2024-01-10 19:57:56 +01:00
Athou
bbda35f868 open sidebar on swipe (#1098) 2024-01-10 19:57:38 +01:00
Athou
df68405fef allow users without email to change their profile (#1184) 2024-01-10 19:28:03 +01:00
Athou
65194d948f ignore vscode files 2024-01-10 11:21:37 +01:00
Athou
d49297216c cleanup 2024-01-10 11:19:47 +01:00
Athou
e3e50f8456 improve artifact upload speed (https://github.com/actions/upload-artifact/issues/199) 2024-01-09 22:08:04 +01:00
Athou
e90b3730ef add JavaTimeModule to RedisCacheService object mapper to be able to serialize java.time.Instant 2024-01-09 22:01:34 +01:00
Athou
7675a24eb6 store only user id in session in order to avoid invalidating all sessions when user model changes 2024-01-09 21:22:20 +01:00
Athou
2bf9186135 only show sidebar resizer when sidebar is actually shown 2024-01-09 16:20:46 +01:00
Athou
d4ea51c145 fix vulnerability 2024-01-09 14:59:33 +01:00
Athou
6e0e99694e use properties file of git-commit-id-maven-plugin so we don't need to filter resources 2024-01-09 14:56:59 +01:00
Athou
9ede8d1c46 remove the Managed interface for classes that are not managed by dropwizard 2024-01-09 14:09:24 +01:00
Athou
fd0425a2be clear all sessions because the session model changed 2024-01-09 11:26:54 +01:00
Athou
2b976cadeb add a memory management section to the readme 2024-01-09 09:39:56 +01:00
Athou
023c27a565 setupListeners is only used for rtk query and we don't use it anymore 2024-01-09 07:24:24 +01:00
Athou
69c9988404 migrate from java.util.Date to java.time 2024-01-08 21:58:40 +01:00
Athou
b1a4debb95 replace toSorted usage with sort (#1183) 2024-01-08 13:48:27 +01:00
Athou
5663d619aa show category hierarchy (#1045) 2024-01-08 13:26:20 +01:00
Athou
2ef9e8d274 add null check 2024-01-07 22:14:00 +01:00
Athou
1292018de0 add setting to delete old entries 2024-01-07 20:49:02 +01:00
Athou
039e91414e prevent demo account from registering custom js code 2024-01-07 17:51:22 +01:00
Athou
662d0f754f avoid flash of light theme when using system color scheme 2024-01-07 17:51:22 +01:00
Athou
7fb7efbdf7 add missing truncate lost in refactoring 2024-01-07 17:51:22 +01:00
Athou
a841c80261 simplify trie building 2024-01-07 17:51:22 +01:00
Athou
da4143fa13 multiple feeds may have the same url hash 2024-01-07 17:51:22 +01:00
Athou
789857b09f compare feed entry content after cleanup because that's what saved in the database 2024-01-07 17:51:22 +01:00
Athou
ed45746f52 extract html cleaning code to its own service 2024-01-07 17:51:22 +01:00
Athou
deb51f2ccc rename FixedSizeSortedSet to FixedSizeSortedList because it's actually a list 2024-01-07 17:51:22 +01:00
Athou
5fec4a4c5f improve lookup by using a set because we only use contains() 2024-01-07 17:51:22 +01:00
Athou
7b335e2fd4 feed refresh engine now uses its own immutable model 2024-01-07 17:51:22 +01:00
Athou
60b6c69020 close the HTTP client after each test to close idle connections (https://github.com/dropwizard/dropwizard/issues/8174) 2024-01-06 08:37:12 +01:00
Athou
08ab32c4c2 we don't need the admin connector for tests 2024-01-05 21:20:56 +01:00
Athou
ff24fe4c7c eslint is already run by vite-plugin-eslint during build 2024-01-05 20:51:41 +01:00
Athou
50c62fb468 remove warning: 'typeParameters' property is deprecated 2024-01-05 20:48:25 +01:00
Athou
201331afc3 update vite to 5.x 2024-01-05 20:38:01 +01:00
Athou
cf3100081e add test for unauthorized websocket usage 2024-01-03 21:08:25 +01:00
Athou
860aab7495 fix typo 2024-01-02 11:11:45 +01:00
Athou
b084c8d108 remove line break 2024-01-02 11:10:33 +01:00
Athou
8e0a53fc49 release 4.0.0 2024-01-02 10:56:52 +01:00
Athou
4ea2bad083 test all redirect codes 2024-01-02 08:08:19 +01:00
Athou
46065d938d extract new i18n labels 2024-01-01 19:42:36 +01:00
Athou
16389824f7 fix wrong labels 2024-01-01 19:39:55 +01:00
Athou
92b624ca8a add option to follow system dark/light mode (#1083) 2024-01-01 19:37:52 +01:00
Athou
1ae5111f76 use slightly less dark gray for selected tree node background to improve unread count readability 2024-01-01 18:24:39 +01:00
Athou
d9a9a01a60 add missing pathname to websocket url (#1167) 2024-01-01 18:15:04 +01:00
Athou
bbbb9c10a6 align buttons to the right to match other dialogs 2024-01-01 10:47:39 +01:00
Athou
50cf9718a3 fix wrong clear button style 2024-01-01 10:43:54 +01:00
Athou
99a7ede82d restore bold font for unread items 2024-01-01 10:10:05 +01:00
Athou
7b1218ef1e correctly trim long feed names when sidebar is too narrow 2024-01-01 08:34:00 +01:00
Athou
8dab16090f display links and image placeholders in entries in the same color as the text so that they are not mistaken for commafeed actions 2023-12-31 18:36:55 +01:00
Athou
6e5f362a8e load custom js when the app is done loading to ease custom code usage (#1093) 2023-12-31 09:27:49 +01:00
Athou
96212afd27 save sidebar width in local storage (#1093) 2023-12-30 22:13:35 +01:00
Athou
7e02380858 update to mantine 7 2023-12-30 22:13:35 +01:00
Athou
2742b7fff6 remove usage of createStyles from mantine that is removed in v7 2023-12-29 22:27:54 +01:00
Athou
dade873420 file not needed anymore 2023-12-29 20:15:34 +01:00
Athou
e7925e6330 add tests for the new insertedBefore mechanic 2023-12-29 15:32:06 +01:00
Athou
f845f225cf add a "insertedBefore" field to mark as read requests to make sure the user does not mark entries that were fetched but never seen before (fixes a regression from #1007) 2023-12-29 13:40:30 +01:00
Athou
39ba4a1c97 disable redoc url sync because it causes issues with hashrouter 2023-12-29 12:11:33 +01:00
Athou
a491b95a02 generate a nicer url in documentation (/rest instead of /openapi/../rest) 2023-12-29 11:21:28 +01:00
Athou
e0c05c8e5d redux update 2023-12-29 11:08:33 +01:00
Athou
2f1aa12e30 use redoc instead of swagger ui to be able to update redux 2023-12-29 11:01:57 +01:00
Athou
4c532cf028 fix wrong endpoint name in documentation 2023-12-29 10:53:38 +01:00
Athou
dc95044fbc group swagger api definitions by endpoint 2023-12-29 09:11:47 +01:00
Athou
418cb4797d use latest node and npm now that everything is up to date 2023-12-29 07:27:53 +01:00
Athou
c646503501 update other dependencies 2023-12-28 22:37:12 +01:00
Athou
0ea0db48db split thunks from slices to avoid circular dependencies 2023-12-28 22:11:03 +01:00
Athou
bb4bb0c7d7 createAppAsyncThunk needs to be in its own file (https://stackoverflow.com/a/77136003/1885506) 2023-12-28 21:49:18 +01:00
Athou
97781d5551 eslint update 2023-12-28 21:49:18 +01:00
Athou
f4e48383cc use typed createAsyncThunk 2023-12-28 19:49:38 +01:00
Athou
aa009c366d prettier update 2023-12-28 15:26:07 +01:00
Athou
1289dbae84 add test for websocket ping/pong 2023-12-28 10:25:44 +01:00
Athou
8c69dd355c fix warnings 2023-12-27 11:19:34 +01:00
Athou
fdf4fdcc87 use latest dropwizard release 2023-12-27 09:32:24 +01:00
Athou
9cd1cde571 apply intellij fixes 2023-12-27 09:22:55 +01:00
Athou
1b4b3ca52c fix wrong JPA mapping 2023-12-27 08:48:51 +01:00
Athou
6a76c8b8c3 reduce svg size by removing unused inkscape tags 2023-12-27 08:47:41 +01:00
Athou
b49d35f181 remove all remaining references to httpclient4 2023-12-26 08:21:35 +01:00
Athou
5ba248eaba update to httpclient5 2023-12-25 20:00:47 +01:00
Athou
11aff68052 java http client is unfortunately sometimes not honoring timeouts (https://bugs.openjdk.org/browse/JDK-8258397), use httpclient again 2023-12-25 17:15:33 +01:00
Athou
07dd10848f return default content type if invalid instead of crashing 2023-12-25 10:30:48 +01:00
Athou
b2bd386e9c reset database completely after each test so that tests cannot impact each other 2023-12-24 10:52:09 +01:00
Athou
d09cabb8c6 avoid modifying the admin user because it impacts the test in UserIT 2023-12-24 09:55:02 +01:00
Athou
818d847607 CookieManager parses the cookie header even if we ask to ignore them, use our own cookie handler that does nothing 2023-12-22 22:27:42 +01:00
Athou
1db53e48c6 reduce connection keepalive timeout to 30s, default is 20 minutes 2023-12-22 20:22:00 +01:00
Athou
5601d150c3 restore the connect timeout feature 2023-12-22 16:10:34 +01:00
Athou
a35f55cde6 compact h2 database on exit 2023-12-21 22:27:50 +01:00
Athou
3714bfaccc add test for password recovery 2023-12-21 22:15:39 +01:00
Athou
5541cc9fbe websocket can now be disabled, the websocket ping interval and the tree reload interval can now be configured (#1132) 2023-12-21 21:20:26 +01:00
Athou
bdabd9db0d ran npm audit fix 2023-12-18 18:03:38 +01:00
Athou
2762c535d6 cleanup 2023-12-18 18:03:38 +01:00
Athou
241c465eba add tests for PasswordEncryptionService 2023-12-18 16:06:54 +01:00
Athou
6c3895e60a make sure we ignore cookies 2023-12-18 15:39:11 +01:00
Athou
a30bf18102 add support for youtube playlist favicons 2023-12-18 13:45:25 +01:00
Athou
d9ccdf1caf use java standard http client because apache http clients should be reused because they support pooling but we don't need that 2023-12-18 11:53:14 +01:00
Athou
155e7ba1aa add tests for HttpGetter 2023-12-18 10:24:40 +01:00
Athou
00faf44c94 remove wonky pubsub support 2023-12-18 10:15:43 +01:00
Athou
c45f832131 increase websocket idle timeout above ping interval 2023-12-17 17:40:28 +01:00
Athou
6f781216cd keep using h2 2.1 because 2.2 uses a different file format 2023-12-17 17:40:28 +01:00
Athou
fd0e5426e5 upgrade to dropwizard 4.x 2023-12-17 15:10:57 +01:00
Athou
b5d99b9661 migrate from swagger to openapi3 2023-12-17 13:51:12 +01:00
Athou
50fcdece86 update various dependencies 2023-12-17 13:51:12 +01:00
Athou
d882553644 java 17 is now the new baseline 2023-12-17 13:51:12 +01:00
Athou
bf71e825a4 update code formatter version 2023-12-17 08:49:44 +01:00
Athou
351701d674 add tests for the security layer 2023-12-16 21:20:14 +01:00
Athou
cb4a8df0d2 add more tests 2023-12-16 18:16:52 +01:00
Athou
7ef865506f use non-existing urls 2023-12-15 18:05:48 +01:00
Athou
e4863e8881 add a GET method to the fever api (#1176) 2023-12-15 17:53:47 +01:00
Athou
c86a060170 remove unused AnalyticsServlet, it's handled directly by the client since 3.0 2023-12-15 17:45:15 +01:00
Athou
6ed5637e57 add more IT tests to ease transition to dropwizard 4 2023-12-15 17:35:51 +01:00
Athou
929df60f09 no need to expose admin connector in production 2023-12-13 07:21:27 +01:00
Athou
2b51de8e5b release 3.10.1 2023-12-08 17:19:31 +01:00
Athou
0ba70d29bd readme tweaks 2023-11-24 08:37:41 +01:00
Athou
197b3b258b also build with jdk 21 now that it's been released 2023-11-17 08:52:30 +01:00
Athou
850f66999c use less memory by returning unused memory to the OS (https://openjdk.org/jeps/346) 2023-11-16 08:41:29 +01:00
Athou
d7d3574e36 swap next and previous buttons (#1159) 2023-11-15 07:59:17 +01:00
Jérémie Panzer
435d612cbf Merge pull request #1164 from canoine/master
Update fr/messages.po
2023-11-02 12:53:05 +01:00
canoine
3d3a7c6496 Merge pull request #1 from canoine/canoine-patch-1
Update fr/messages.po
2023-10-18 09:10:41 +02:00
canoine
fba57fe0a7 Update fr/messages.po
Translation of the new fields.
2023-10-18 09:09:15 +02:00
Athou
ce7933f320 add mention of PikaPods 2023-10-02 19:38:06 +02:00
Athou
8ac452afc9 shorten count starting at 10k and add a tooltip with the exact count(#1150) 2023-09-23 16:44:25 +02:00
Jérémie Panzer
a11cb3ac7a Merge pull request #1154 from joerg376/patch-1
Update messages.po
2023-09-23 16:44:11 +02:00
joerg376
39808bbafc Update messages.po 2023-09-23 11:48:10 +02:00
Athou
aee56e3dbe no need to reload everything when websocket connection status changes 2023-09-19 12:31:19 +02:00
Athou
40f451c762 increase websocket ping interval to just under a minute instead of the default 15s 2023-09-12 20:22:34 +02:00
Athou
d633803ab5 only poll tree if websocket connection is unavailable 2023-09-12 20:22:03 +02:00
Athou
d7a3b75687 indicate that the feedLink property is not always filled (#1146) 2023-09-08 07:10:44 +02:00
Athou
df8c4056b6 indicate that the method returns the id of the newly created feed (#1147) 2023-09-08 07:07:29 +02:00
Athou
06319c1eb0 release 3.10.0 2023-09-06 09:04:21 +02:00
Athou
b7ede8eba2 add instructions for the Fever API 2023-09-05 11:04:52 +02:00
Athou
1a4517d6a3 add support for FeedMe 2023-09-05 11:04:52 +02:00
Athou
a402c5d7d8 add support for FocusReader 2023-09-05 11:04:52 +02:00
Athou
408809787e add support for Raven Reader 2023-09-05 11:04:52 +02:00
Athou
d7b0d572c1 add fever-compatible api 2023-09-05 11:04:52 +02:00
Athou
b356be3e6f show the whole title in the detailed view (#1097 #1144) 2023-09-05 09:10:26 +02:00
Athou
998385334b add metric for deleted entries 2023-09-03 12:16:43 +02:00
Athou
c6d613d81a add "s" keyboard shortcut to star/unstar entries (#1142) 2023-08-27 11:43:30 +02:00
Athou
9981d8763d don't set default values for env variables (#1141) 2023-08-24 07:51:10 +02:00
Athou
b37680333c clean database after each test 2023-08-23 20:36:57 +02:00
Athou
66d1eb3f1f store sessions in database 2023-08-23 20:34:29 +02:00
316 changed files with 13038 additions and 12086 deletions

24
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
version: 2
updates:
- package-ecosystem: "maven"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
- package-ecosystem: "npm"
directory: "/commafeed-client"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
groups:
mantine:
patterns:
- "@mantine/*"
lingui:
patterns:
- "@lingui/*"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10

View File

@@ -7,23 +7,23 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
java: [ "8", "11", "17" ]
java: [ "17", "21" ]
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
# Setup
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
- name: Set up Java
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java }}
distribution: "temurin"
@@ -34,23 +34,23 @@ jobs:
run: mvn --batch-mode --update-snapshots verify
- name: Upload JAR
uses: actions/upload-artifact@v3
if: ${{ matrix.java == '8' }}
uses: actions/upload-artifact@v4
if: ${{ matrix.java == '17' }}
with:
name: commafeed.jar
path: commafeed-server/target/commafeed.jar
# Docker
- name: Login to Container Registry
uses: docker/login-action@v2
if: ${{ matrix.java == '8' }}
uses: docker/login-action@v3
if: ${{ matrix.java == '17' && (github.ref_type == 'tag' || github.ref_name == 'master') }}
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Docker build and push tag
uses: docker/build-push-action@v4
if: ${{ matrix.java == '8' && github.ref_type == 'tag' }}
uses: docker/build-push-action@v5
if: ${{ matrix.java == '17' && github.ref_type == 'tag' }}
with:
context: .
push: true
@@ -60,8 +60,8 @@ jobs:
athou/commafeed:${{ github.ref_name }}
- name: Docker build and push master
uses: docker/build-push-action@v4
if: ${{ matrix.java == '8' && github.ref_name == 'master' }}
uses: docker/build-push-action@v5
if: ${{ matrix.java == '17' && github.ref_name == 'master' }}
with:
context: .
push: true
@@ -71,14 +71,14 @@ jobs:
# Create GitHub release after Docker image has been published
- name: Extract Changelog Entry
uses: mindsers/changelog-reader-action@v2
if: ${{ matrix.java == '8' && github.ref_type == 'tag' }}
if: ${{ matrix.java == '17' && github.ref_type == 'tag' }}
id: changelog_reader
with:
version: ${{ github.ref_name }}
- name: Create GitHub release
uses: softprops/action-gh-release@v1
if: ${{ matrix.java == '8' && github.ref_type == 'tag' }}
if: ${{ matrix.java == '17' && github.ref_type == 'tag' }}
with:
name: CommaFeed ${{ github.ref_name }}
body: ${{ steps.changelog_reader.outputs.changes }}

3
.gitignore vendored
View File

@@ -35,5 +35,8 @@ src/main/app/lib
# Sublime
*.sublime*
# VSCode
.vscode
# Macs
*.DS_Store

View File

@@ -1,5 +1,94 @@
# Changelog
## [4.3.1]
- fix an issue that prevents new feeds from being added when mysql/mariadb is used as the database and the database
timezone is not UTC (#1239)
- videos in enclosures can no longer have a width larger than the page (#1240)
## [4.3.0]
- h2 (the embedded database) has been upgraded to 2.2.224
- this version uses a different file format than 2.1.x, the first time you start CommaFeed with this version, the
database will be automatically converted to the new format
- add a setting to completely disable scrolling to selected entry (#1157)
- add a css class reflecting the current view mode to ease custom css rules (#1232)
- fix an issue that prevents new feeds from being added when mysql/mariadb is used as the database (#1239)
## [4.2.1]
- fix an issue that caused the tree to show an incorrect unread count after a websocket notification because entries
that were already marked as read by a filtering expression were not ignored (#1191)
## [4.2.0]
- add a setting to display the action buttons in the footer instead of in the header on mobile (#1121)
- the websocket notification now contains everything needed to update the UI, the client no longer needs to make an API
call to get the latest data when receiving the notification
- add a workaround to the Fever API for the Unread iOS app (#1188)
- fix an issue that caused dates to be saved incorrectly if the database server and the application server were in
different timezones (#1187)
## [4.1.0]
- it is now possible to open the sidebar on mobile by swiping to the right (#1098)
- swiping to mark entries as read/unread changed from swiping right to left because swiping right now opens the sidebar
- the full hierarchy of categories are now displayed in the category dropdown (#1045)
- added a setting `maxEntriesAgeDays` to delete old entries based on their age during database cleanup.
The setting is disabled by default for existing installations, except for the docker image where it is enabled and set
to 365 days
- if user registrations are disabled on your instance which is the default behavior, users are redirected on the login
page instead of the welcome page when not logged in (#1185)
- the sidebar resizer is no longer shown in the middle of the screen on mobile
- when using the system color scheme and the system is using a dark theme, feed entries no longer flicker on load
- the demo account (if enabled) cannot register custom javascript code anymore
- removed the usage of `toSorted` in the client because older browsers do not support it (#1183)
- the openapi documentation is no longer cached by the browser so you always have access to the latest version
- added a memory management section to the readme, reading it is recommended if you are running CommaFeed on a server
with limited memory
- fixed an issue that caused users without an email address set to be unable to edit their profile (#1184)
## [4.0.0]
- migrated from dropwizard 2 to dropwizard 4, Java 17+ is now required
- entries that were fetched and inserted in the database but not yet shown in the UI are no longer marked as read when
marking all entries as read
- your custom sidebar width is now persisted in the local storage of your browser
- there is now a third color scheme option in addition to light and dark: system (follows the system color scheme)
- added support for youtube playlist favicons
- custom JS code is now executed when the app is done loading instead of when the page is loaded
- the favicon is now correctly returned for feeds that return an invalid content type
- the feed refresh engine now uses httpclient5 with connection pooling and no longer creates a new client for each
request, reducing CPU usage
- updated UI library Mantine to 7.0, improving performance
- the h2 embedded database is now compacted on shutdown to reclaim unused space
- the admin connector on port 8084 is now disabled in config.yml.example. Disabling it in your config.yml is
recommended (see https://github.com/Athou/commafeed/commit/929df60f09cce56020b0962ab111cd8349b271b0)
- migrated documentation from swagger 2 to openapi 3
- added a GET method to the fever api to indicate that the endpoint is working correctly when accessed from a browser
- the websocket connection can now be disabled, the websocket ping interval and the tree reload interval can now be
configured (see config.yml.example)
- the websocket connection now works correctly when the context root of the application is not "/"
- unstable pubsubhubbub support was removed
## [3.10.1]
- swap next and previous buttons (#1159)
- unread count for subscriptions will now be shortened starting at 10k instead of 1k
- increased websocket ping interval to just under a minute to reduce data and battery usage on mobile
- only refresh subscription tree on a timer if websocket connection is unavailable
- the Docker image now uses less memory by returning unused memory to the OS
- add support for Java 21
## [3.10.0]
- added a Fever-compatible API that is usable with mobile clients that support the Fever API (see instructions in
Settings -> Profile)
- long entry titles are no longer shortened in the detailed view
- added the "s" keyboard shortcut to star/unstar entries
- http sessions are now stored in the database (they were stored on disk before)
- fixed an issue that made it impossible to override the database url in a config.yml mounted in the Docker image
## [3.9.0]
- improve performance by disabling the loader when nothing is loading (most noticeable on mobile)
@@ -109,7 +198,8 @@
## [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`

View File

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

View File

@@ -7,22 +7,32 @@ Google Reader inspired self-hosted RSS reader, based on Dropwizard and React/Typ
## Features
- 4 different layouts
- Dark theme
- Light/Dark theme
- Fully responsive
- Keyboard shortcuts for almost everything
- Support for right-to-left feeds
- Translated in 25+ languages
- Supports thousands of users and millions of feeds
- OPML import/export
- REST API
- REST API and a Fever-compatible API for native mobile apps
- [Browser extension](https://github.com/Athou/commafeed-browser-extension)
## Deployment on your own server
## Deployment
### Docker
Docker is the easiest way to get started with CommaFeed.
Docker images are built automatically and are available at https://hub.docker.com/r/athou/commafeed
### Cloud hosting
[PikaPods](https://www.pikapods.com) offers 1-click cloud hosting solutions starting at $1/month with a free $5
welcome credit and officially supports CommaFeed.
PikaPods shares 20% of the revenue back to CommaFeed.
[![PikaPods](https://www.pikapods.com/static/run-button.svg)](https://www.pikapods.com/pods?run=commafeed)
### Download precompiled package
mkdir commafeed && cd commafeed
@@ -44,6 +54,29 @@ user is `admin` and the default password is `admin`.
The server will listen on http://localhost:8082. The default
user is `admin` and the default password is `admin`.
### Memory management
The Java Virtual Machine (JVM) is rather greedy by default and will not release unused memory to the
operating system. This is because acquiring memory from the operating system is a relatively expensive operation.
However, this can be problematic on systems with limited memory.
#### Hard limit
The JVM can be configured to use a maximum amount of memory with the `-Xmx` parameter.
For example, to limit the JVM to 256MB of memory, use `-Xmx256m`.
#### Dynamic sizing
The JVM can be configured to release unused memory to the operating system with the following parameters:
-Xms20m -XX:+UseG1GC -XX:-ShrinkHeapInSteps -XX:G1PeriodicGCInterval=10000 -XX:-G1PeriodicGCInvokesConcurrent -XX:MinHeapFreeRatio=5 -XX:MaxHeapFreeRatio=10
This is how the Docker image is configured.
See [here](https://docs.oracle.com/en/java/javase/17/gctuning/garbage-first-g1-garbage-collector1.html)
and [here](https://docs.oracle.com/en/java/javase/17/gctuning/factors-affecting-garbage-collection-performance.html) for
more
information.
## Translation
Files for internationalization are
@@ -76,19 +109,3 @@ two-letters [ISO-639-1 language code](http://en.wikipedia.org/wiki/List_of_ISO_6
The frontend server is now running at http://localhost:8082 and is proxying REST requests to the backend running on
port 8083
## Copyright and license
Copyright 2013-2023 CommaFeed.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this work except in compliance with the License.
You may obtain a copy of the License in the LICENSE file, or at:
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

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

View File

@@ -0,0 +1,41 @@
module.exports = {
env: {
browser: true,
es2021: true
},
extends: ["standard-with-typescript", "plugin:react/recommended", "plugin:react-hooks/recommended", "plugin:prettier/recommended"],
settings: {
react: {
version: "detect"
}
},
overrides: [
{
env: {
node: true
},
files: [".eslintrc.{js,cjs}"],
parserOptions: {
sourceType: "script"
}
}
],
parserOptions: {
ecmaVersion: "latest",
sourceType: "module"
},
plugins: ["react"],
rules: {
"@typescript-eslint/consistent-type-assertions": ["error", { assertionStyle: "as" }],
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-confusing-void-expression": ["error", { ignoreArrowShorthand: true }],
"@typescript-eslint/no-floating-promises": "off",
"@typescript-eslint/no-misused-promises": "off",
"@typescript-eslint/prefer-nullish-coalescing": ["error", { ignoreConditionalTests: true }],
"@typescript-eslint/strict-boolean-expressions": "off",
"@typescript-eslint/unbound-method": "off",
"react/no-unescaped-entities": "off",
"react/react-in-jsx-scope": "off",
"react-hooks/exhaustive-deps": "error"
}
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -14,72 +14,69 @@
"i18n:extract": "lingui extract --clean"
},
"dependencies": {
"@emotion/react": "^11.11.0",
"@fontsource/open-sans": "^5.0.1",
"@lingui/core": "^4.1.2",
"@lingui/macro": "^4.1.2",
"@lingui/react": "^4.1.2",
"@mantine/core": "^6.0.11",
"@mantine/form": "^6.0.11",
"@mantine/hooks": "^6.0.11",
"@mantine/modals": "^6.0.11",
"@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",
"@emotion/react": "^11.11.3",
"@fontsource/open-sans": "^5.0.23",
"@lingui/core": "^4.7.0",
"@lingui/macro": "^4.7.0",
"@lingui/react": "^4.7.0",
"@mantine/core": "^7.5.2",
"@mantine/form": "^7.5.2",
"@mantine/hooks": "^7.5.2",
"@mantine/modals": "^7.5.2",
"@mantine/notifications": "^7.5.2",
"@mantine/spotlight": "^7.5.2",
"@monaco-editor/react": "^4.6.0",
"@reduxjs/toolkit": "^2.1.0",
"axios": "^1.6.7",
"dayjs": "^1.11.10",
"escape-string-regexp": "^5.0.0",
"interweave": "^13.1.0",
"monaco-editor": "^0.38.0",
"monaco-editor": "^0.46.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",
"react-dom": "^18.2.0",
"react-draggable": "^4.4.6",
"react-ga4": "^2.1.0",
"react-icons": "^4.8.0",
"react-icons": "^5.0.1",
"react-infinite-scroller": "^1.2.6",
"react-redux": "^8.0.5",
"react-router-dom": "^6.11.2",
"react-swipeable": "^7.0.0",
"swagger-ui-react": "^4.18.3",
"react-redux": "^9.1.0",
"react-router-dom": "^6.22.0",
"react-swipeable": "^7.0.1",
"redoc": "^2.1.3",
"throttle-debounce": "^5.0.0",
"tinycon": "^0.6.8",
"tss-react": "^4.9.4",
"use-local-storage": "^3.0.0",
"websocket-heartbeat-js": "^1.1.2"
"websocket-heartbeat-js": "^1.1.3"
},
"devDependencies": {
"@lingui/cli": "^4.1.2",
"@lingui/vite-plugin": "^4.1.2",
"@types/eslint": "^8.40.0",
"@types/mousetrap": "^1.6.11",
"@types/react": "^18.2.6",
"@types/react-dom": "^18.2.4",
"@types/react-infinite-scroller": "^1.2.3",
"@types/swagger-ui-react": "^4.18.0",
"@types/throttle-debounce": "^5.0.0",
"@types/tinycon": "^0.6.3",
"@typescript-eslint/eslint-plugin": "^5.59.7",
"@typescript-eslint/parser": "^5.59.7",
"@vitejs/plugin-react": "^4.0.0",
"@lingui/cli": "^4.7.0",
"@lingui/vite-plugin": "^4.7.0",
"@types/mousetrap": "^1.6.15",
"@types/react": "^18.2.55",
"@types/react-dom": "^18.2.19",
"@types/react-infinite-scroller": "^1.2.5",
"@types/swagger-ui-react": "^4.18.3",
"@types/throttle-debounce": "^5.0.2",
"@types/tinycon": "^0.6.5",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@vitejs/plugin-react": "^4.2.1",
"babel-plugin-macros": "^3.1.0",
"eslint": "^8.41.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^17.0.0",
"eslint-config-prettier": "^8.8.0",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-hooks": "^0.4.3",
"eslint-plugin-prettier": "^4.2.1",
"prettier": "^2.8.8",
"rollup-plugin-visualizer": "^5.9.0",
"typescript": "^5.0.4",
"vite": "^4.3.9",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-config-standard-with-typescript": "^43.0.1",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"prettier": "^3.2.5",
"rollup-plugin-visualizer": "^5.12.0",
"typescript": "^5.3.3",
"vite": "^5.1.1",
"vite-plugin-eslint": "^1.8.1",
"vite-tsconfig-paths": "^4.2.0",
"vitest": "^0.31.1",
"vitest-mock-extended": "^1.1.3"
"vite-tsconfig-paths": "^4.3.1",
"vitest": "^1.2.2",
"vitest-mock-extended": "^1.3.1"
}
}

View File

@@ -5,7 +5,7 @@
<parent>
<groupId>com.commafeed</groupId>
<artifactId>commafeed</artifactId>
<version>3.9.0</version>
<version>4.3.1</version>
</parent>
<artifactId>commafeed-client</artifactId>
<name>CommaFeed Client</name>
@@ -15,7 +15,7 @@
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<version>1.12.1</version>
<version>1.15.0</version>
<?m2e ignore?>
<executions>
<execution>
@@ -25,8 +25,8 @@
</goals>
<phase>compile</phase>
<configuration>
<nodeVersion>v16.16.0</nodeVersion>
<npmVersion>8.15.0</npmVersion>
<nodeVersion>v20.10.0</nodeVersion>
<npmVersion>10.2.5</npmVersion>
</configuration>
</execution>
<execution>
@@ -39,16 +39,6 @@
<arguments>ci</arguments>
</configuration>
</execution>
<execution>
<id>npm run eslint</id>
<goals>
<goal>npm</goal>
</goals>
<phase>compile</phase>
<configuration>
<arguments>run eslint</arguments>
</configuration>
</execution>
<execution>
<id>npm run test</id>
<goals>
@@ -73,7 +63,7 @@
</plugin>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.3.0</version>
<version>3.3.1</version>
<executions>
<execution>
<id>copy web interface to resources</id>

View File

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

View File

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

View File

@@ -1,40 +1,42 @@
import axios from "axios"
import {
AddCategoryRequest,
Category,
CategoryModificationRequest,
CollapseRequest,
Entries,
FeedInfo,
FeedInfoRequest,
FeedModificationRequest,
GetEntriesPaginatedRequest,
IDRequest,
LoginRequest,
MarkRequest,
Metrics,
MultipleMarkRequest,
PasswordResetRequest,
ProfileModificationRequest,
RegistrationRequest,
ServerInfo,
Settings,
StarRequest,
SubscribeRequest,
Subscription,
TagRequest,
UserModel,
type AddCategoryRequest,
type AdminSaveUserRequest,
type Category,
type CategoryModificationRequest,
type CollapseRequest,
type Entries,
type FeedInfo,
type FeedInfoRequest,
type FeedModificationRequest,
type GetEntriesPaginatedRequest,
type IDRequest,
type LoginRequest,
type MarkRequest,
type Metrics,
type MultipleMarkRequest,
type PasswordResetRequest,
type ProfileModificationRequest,
type RegistrationRequest,
type ServerInfo,
type Settings,
type StarRequest,
type SubscribeRequest,
type Subscription,
type TagRequest,
type UserModel,
} from "./types"
const axiosInstance = axios.create({ baseURL: "./rest", withCredentials: true })
axiosInstance.interceptors.response.use(
response => response,
error => {
const { status, data } = error.response
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.")
(status === 401 && data?.message === "Credentials are required to access this resource.") ||
(status === 403 && data?.message === "You don't have the required role to access this resource.")
) {
window.location.hash = "/welcome"
window.location.hash = data?.allowRegistrations ? "/welcome" : "/login"
}
throw error
}
@@ -42,34 +44,34 @@ axiosInstance.interceptors.response.use(
export const client = {
category: {
getRoot: () => axiosInstance.get<Category>("category/get"),
modify: (req: CategoryModificationRequest) => axiosInstance.post("category/modify", req),
collapse: (req: CollapseRequest) => axiosInstance.post("category/collapse", req),
getEntries: (req: GetEntriesPaginatedRequest) => axiosInstance.get<Entries>("category/entries", { params: req }),
markEntries: (req: MarkRequest) => axiosInstance.post("category/mark", req),
add: (req: AddCategoryRequest) => axiosInstance.post("category/add", req),
delete: (req: IDRequest) => axiosInstance.post("category/delete", req),
getRoot: async () => await axiosInstance.get<Category>("category/get"),
modify: async (req: CategoryModificationRequest) => await axiosInstance.post("category/modify", req),
collapse: async (req: CollapseRequest) => await axiosInstance.post("category/collapse", req),
getEntries: async (req: GetEntriesPaginatedRequest) => await axiosInstance.get<Entries>("category/entries", { params: req }),
markEntries: async (req: MarkRequest) => await axiosInstance.post("category/mark", req),
add: async (req: AddCategoryRequest) => await axiosInstance.post("category/add", req),
delete: async (req: IDRequest) => await axiosInstance.post("category/delete", req),
},
entry: {
mark: (req: MarkRequest) => axiosInstance.post("entry/mark", req),
markMultiple: (req: MultipleMarkRequest) => axiosInstance.post("entry/markMultiple", req),
star: (req: StarRequest) => axiosInstance.post("entry/star", req),
getTags: () => axiosInstance.get<string[]>("entry/tags"),
tag: (req: TagRequest) => axiosInstance.post("entry/tag", req),
mark: async (req: MarkRequest) => await axiosInstance.post("entry/mark", req),
markMultiple: async (req: MultipleMarkRequest) => await axiosInstance.post("entry/markMultiple", req),
star: async (req: StarRequest) => await axiosInstance.post("entry/star", req),
getTags: async () => await axiosInstance.get<string[]>("entry/tags"),
tag: async (req: TagRequest) => await axiosInstance.post("entry/tag", req),
},
feed: {
get: (id: string) => axiosInstance.get<Subscription>(`feed/get/${id}`),
modify: (req: FeedModificationRequest) => axiosInstance.post("feed/modify", req),
getEntries: (req: GetEntriesPaginatedRequest) => axiosInstance.get<Entries>("feed/entries", { params: req }),
markEntries: (req: MarkRequest) => axiosInstance.post("feed/mark", req),
fetchFeed: (req: FeedInfoRequest) => axiosInstance.post<FeedInfo>("feed/fetch", req),
refreshAll: () => axiosInstance.get("feed/refreshAll"),
subscribe: (req: SubscribeRequest) => axiosInstance.post<number>("feed/subscribe", req),
unsubscribe: (req: IDRequest) => axiosInstance.post("feed/unsubscribe", req),
importOpml: (req: File) => {
get: async (id: string) => await axiosInstance.get<Subscription>(`feed/get/${id}`),
modify: async (req: FeedModificationRequest) => await axiosInstance.post("feed/modify", req),
getEntries: async (req: GetEntriesPaginatedRequest) => await axiosInstance.get<Entries>("feed/entries", { params: req }),
markEntries: async (req: MarkRequest) => await axiosInstance.post("feed/mark", req),
fetchFeed: async (req: FeedInfoRequest) => await axiosInstance.post<FeedInfo>("feed/fetch", req),
refreshAll: async () => await axiosInstance.get("feed/refreshAll"),
subscribe: async (req: SubscribeRequest) => await axiosInstance.post<number>("feed/subscribe", req),
unsubscribe: async (req: IDRequest) => await axiosInstance.post("feed/unsubscribe", req),
importOpml: async (req: File) => {
const formData = new FormData()
formData.append("file", req)
return axiosInstance.post("feed/import", formData, {
return await axiosInstance.post("feed/import", formData, {
headers: {
"Content-Type": "multipart/form-data",
},
@@ -77,23 +79,23 @@ export const client = {
},
},
user: {
login: (req: LoginRequest) => axiosInstance.post("user/login", req),
register: (req: RegistrationRequest) => axiosInstance.post("user/register", req),
passwordReset: (req: PasswordResetRequest) => axiosInstance.post("user/passwordReset", req),
getSettings: () => axiosInstance.get<Settings>("user/settings"),
saveSettings: (settings: Settings) => axiosInstance.post("user/settings", settings),
getProfile: () => axiosInstance.get<UserModel>("user/profile"),
saveProfile: (req: ProfileModificationRequest) => axiosInstance.post("user/profile", req),
deleteProfile: () => axiosInstance.post("user/profile/deleteAccount"),
login: async (req: LoginRequest) => await axiosInstance.post("user/login", req),
register: async (req: RegistrationRequest) => await axiosInstance.post("user/register", req),
passwordReset: async (req: PasswordResetRequest) => await axiosInstance.post("user/passwordReset", req),
getSettings: async () => await axiosInstance.get<Settings>("user/settings"),
saveSettings: async (settings: Settings) => await axiosInstance.post("user/settings", settings),
getProfile: async () => await axiosInstance.get<UserModel>("user/profile"),
saveProfile: async (req: ProfileModificationRequest) => await axiosInstance.post("user/profile", req),
deleteProfile: async () => await axiosInstance.post("user/profile/deleteAccount"),
},
server: {
getServerInfos: () => axiosInstance.get<ServerInfo>("server/get"),
getServerInfos: async () => await axiosInstance.get<ServerInfo>("server/get"),
},
admin: {
getAllUsers: () => axiosInstance.get<UserModel[]>("admin/user/getAll"),
saveUser: (req: UserModel) => axiosInstance.post("admin/user/save", req),
deleteUser: (req: IDRequest) => axiosInstance.post("admin/user/delete", req),
getMetrics: () => axiosInstance.get<Metrics>("admin/metrics"),
getAllUsers: async () => await axiosInstance.get<UserModel[]>("admin/user/getAll"),
saveUser: async (req: AdminSaveUserRequest) => await axiosInstance.post("admin/user/save", req),
deleteUser: async (req: IDRequest) => await axiosInstance.post("admin/user/delete", req),
getMetrics: async () => await axiosInstance.get<Metrics>("admin/metrics"),
},
}
@@ -109,7 +111,7 @@ export const errorToStrings = (err: unknown) => {
if (err.response) {
const { data } = err.response
if (typeof data === "string") strings.push(data)
if (typeof data === "object" && data.message) strings.push(data.message)
if (typeof data === "object" && data.message) strings.push(data.message as string)
if (typeof data === "object" && data.errors) strings = [...strings, ...data.errors]
}
}

View File

@@ -1,11 +1,10 @@
import { t } from "@lingui/macro"
import { DEFAULT_THEME } from "@mantine/core"
import { IconType } from "react-icons"
import { type IconType } from "react-icons"
import { FaAt } from "react-icons/fa"
import { SiBuffer, SiFacebook, SiGmail, SiInstapaper, SiPocket, SiTumblr, SiTwitter } from "react-icons/si"
import { Category, Entry, SharingSettings } from "./types"
import { type Category, type Entry, type SharingSettings } from "./types"
const categories: { [key: string]: Category } = {
const categories: Record<string, Category> = {
all: {
id: "all",
name: t`All`,
@@ -86,13 +85,22 @@ export const Constants = {
categories,
sharing,
layout: {
mobileBreakpoint: DEFAULT_THEME.breakpoints.md,
mobileBreakpoint: 992,
mobileBreakpointName: "md",
headerHeight: 60,
entryMaxWidth: 650,
isTopVisible: (div: HTMLElement) => div.getBoundingClientRect().top >= Constants.layout.headerHeight,
isBottomVisible: (div: HTMLElement) => div.getBoundingClientRect().bottom <= window.innerHeight,
isTopVisible: (div: HTMLElement) => {
const header = document.getElementById(Constants.dom.headerId)?.getBoundingClientRect()
return div.getBoundingClientRect().top >= (header?.bottom ?? 0)
},
isBottomVisible: (div: HTMLElement) => {
const footer = document.getElementById(Constants.dom.footerId)?.getBoundingClientRect()
return div.getBoundingClientRect().bottom <= (footer?.top ?? window.innerHeight)
},
},
dom: {
headerId: "header",
footerId: "footer",
entryId: (entry: Entry) => `entry-id-${entry.id}`,
entryContextMenuId: (entry: Entry) => entry.id,
},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,29 +1,21 @@
import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit"
import { client } from "app/client"
import { Category, CollapseRequest } from "app/types"
import { createSlice, type PayloadAction } from "@reduxjs/toolkit"
import { markEntry } from "app/entries/thunks"
import { redirectTo } from "app/redirect/slice"
import { collapseTreeCategory, reloadTree } from "app/tree/thunks"
import { type Category } from "app/types"
import { visitCategoryTree } from "app/utils"
// eslint-disable-next-line import/no-cycle
import { markEntry } from "./entries"
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))
export const collapseTreeCategory = createAsyncThunk("tree/category/collapse", async (req: CollapseRequest) =>
client.category.collapse(req)
)
export const treeSlice = createSlice({
name: "tree",
initialState,
@@ -31,12 +23,25 @@ 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
},
incrementUnreadCount: (
state,
action: PayloadAction<{
feedId: number
amount: number
}>
) => {
if (!state.rootCategory) return
visitCategoryTree(state.rootCategory, c =>
c.feeds
.filter(f => f.id === action.payload.feedId)
.forEach(f => {
f.unread += action.payload.amount
})
)
},
},
extraReducers: builder => {
builder.addCase(reloadTree.fulfilled, (state, action) => {
@@ -64,5 +69,4 @@ export const treeSlice = createSlice({
},
})
export const { setMobileMenuOpen, setSidebarWidth, toggleSidebar } = treeSlice.actions
export default treeSlice.reducer
export const { setMobileMenuOpen, toggleSidebar, incrementUnreadCount } = treeSlice.actions

View File

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

View File

@@ -113,6 +113,7 @@ export interface MarkRequest {
id: string
read: boolean
olderThan?: number
insertedBefore?: number
keywords?: string
excludedSubscriptions?: number[]
}
@@ -134,7 +135,7 @@ export interface MetricMeter {
units: string
}
export type MetricTimer = {
export interface MetricTimer {
count: number
max: number
mean: number
@@ -155,10 +156,10 @@ export type MetricTimer = {
}
export interface Metrics {
counters: { [key: string]: MetricCounter }
gauges: { [key: string]: MetricGauge }
meters: { [key: string]: MetricMeter }
timers: { [key: string]: MetricTimer }
counters: Record<string, MetricCounter>
gauges: Record<string, MetricGauge>
meters: Record<string, MetricMeter>
timers: Record<string, MetricTimer>
}
export interface MultipleMarkRequest {
@@ -190,6 +191,9 @@ export interface ServerInfo {
googleAnalyticsCode?: string
smtpEnabled: boolean
demoAccountEnabled: boolean
websocketEnabled: boolean
websocketPingInterval: number
treeReloadInterval: number
}
export interface Settings {
@@ -201,9 +205,10 @@ export interface Settings {
customCss?: string
customJs?: string
scrollSpeed: number
alwaysScrollToEntry: boolean
scrollMode: ScrollMode
markAllAsReadConfirmation: boolean
customContextMenu: boolean
mobileFooter: boolean
sharingSettings: SharingSettings
}
@@ -270,8 +275,19 @@ export interface UserModel {
admin: boolean
}
export interface AdminSaveUserRequest {
id?: number
name: string
email?: string
password?: string
enabled: boolean
admin: boolean
}
export type ReadingMode = "all" | "unread"
export type ReadingOrder = "asc" | "desc"
export type ViewMode = "title" | "cozy" | "detailed" | "expanded"
export type ScrollMode = "always" | "never" | "if_needed"

View File

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

View File

@@ -0,0 +1,83 @@
import { createAppAsyncThunk } from "app/async-thunk"
import { client } from "app/client"
import { reloadEntries } from "app/entries/thunks"
import type { ReadingMode, ReadingOrder, ScrollMode, SharingSettings } from "app/types"
export const reloadSettings = createAppAsyncThunk("settings/reload", async () => await client.user.getSettings().then(r => r.data))
export const reloadProfile = createAppAsyncThunk("profile/reload", async () => await client.user.getProfile().then(r => r.data))
export const reloadTags = createAppAsyncThunk("entries/tags", async () => await client.entry.getTags().then(r => r.data))
export const changeReadingMode = createAppAsyncThunk("settings/readingMode", (readingMode: ReadingMode, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, readingMode })
thunkApi.dispatch(reloadEntries())
})
export const changeReadingOrder = createAppAsyncThunk("settings/readingOrder", (readingOrder: ReadingOrder, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, readingOrder })
thunkApi.dispatch(reloadEntries())
})
export const changeLanguage = createAppAsyncThunk("settings/language", (language: string, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, language })
})
export const changeScrollSpeed = createAppAsyncThunk("settings/scrollSpeed", (speed: boolean, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, scrollSpeed: speed ? 400 : 0 })
})
export const changeShowRead = createAppAsyncThunk("settings/showRead", (showRead: boolean, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, showRead })
})
export const changeScrollMarks = createAppAsyncThunk("settings/scrollMarks", (scrollMarks: boolean, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, scrollMarks })
})
export const changeScrollMode = createAppAsyncThunk("settings/scrollMode", (scrollMode: ScrollMode, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, scrollMode })
})
export const changeMarkAllAsReadConfirmation = createAppAsyncThunk(
"settings/markAllAsReadConfirmation",
(markAllAsReadConfirmation: boolean, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, markAllAsReadConfirmation })
}
)
export const changeCustomContextMenu = createAppAsyncThunk("settings/customContextMenu", (customContextMenu: boolean, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, customContextMenu })
})
export const changeMobileFooter = createAppAsyncThunk("settings/mobileFooter", (mobileFooter: boolean, thunkApi) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({ ...settings, mobileFooter })
})
export const changeSharingSetting = createAppAsyncThunk(
"settings/sharingSetting",
(
sharingSetting: {
site: keyof SharingSettings
value: boolean
},
thunkApi
) => {
const { settings } = thunkApi.getState().user
if (!settings) return
client.user.saveSettings({
...settings,
sharingSettings: {
...settings.sharingSettings,
[sharingSetting.site]: sharingSetting.value,
},
})
}
)

View File

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

View File

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

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 791 B

View File

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

View File

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

View File

@@ -24,7 +24,7 @@ export function AnnouncementDialog() {
return (
<Dialog opened={opened} withCloseButton onClose={onClosed} size="xl" radius="md">
<Box>
<Text weight="bold">
<Text fw="bold">
<Trans>Announcement</Trans>
</Text>
</Box>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,12 @@
import { Input, Textarea } from "@mantine/core"
import RichCodeEditor from "components/code/RichCodeEditor"
import { useMobile } from "hooks/useMobile"
import { ReactNode } from "react"
import { type ReactNode } from "react"
interface CodeEditorProps {
description?: ReactNode
language: "css" | "javascript"
value: string
value?: string
onChange: (value: string | undefined) => void
}

View File

@@ -1,5 +1,5 @@
import { useMantineTheme } from "@mantine/core"
import { Loader } from "components/Loader"
import { useColorScheme } from "hooks/useColorScheme"
import { useAsync } from "react-async-hook"
const init = async () => {
@@ -27,13 +27,13 @@ const init = async () => {
interface RichCodeEditorProps {
height: number | string
language: "css" | "javascript"
value: string
value?: string
onChange: (value: string | undefined) => void
}
function RichCodeEditor(props: RichCodeEditorProps) {
const theme = useMantineTheme()
const editorTheme = theme.colorScheme === "dark" ? "vs-dark" : "light"
const colorScheme = useColorScheme()
const editorTheme = colorScheme === "dark" ? "vs-dark" : "light"
const { result: Editor } = useAsync(init, [])
if (!Editor) return <Loader />

View File

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

View File

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

View File

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

View File

@@ -2,8 +2,8 @@ import { Trans } from "@lingui/macro"
import { Box } from "@mantine/core"
import { openModal } from "@mantine/modals"
import { Constants } from "app/constants"
import { type ExpendableEntry } from "app/entries/slice"
import {
ExpendableEntry,
loadMoreEntries,
markAllEntries,
markEntry,
@@ -11,10 +11,11 @@ import {
selectEntry,
selectNextEntry,
selectPreviousEntry,
} from "app/slices/entries"
import { redirectToRootCategory } from "app/slices/redirect"
import { toggleSidebar } from "app/slices/tree"
starEntry,
} from "app/entries/thunks"
import { redirectToRootCategory } from "app/redirect/thunks"
import { useAppDispatch, useAppSelector } from "app/store"
import { toggleSidebar } from "app/tree/slice"
import { KeyboardShortcutsHelp } from "components/KeyboardShortcutsHelp"
import { Loader } from "components/Loader"
import { useBrowserExtension } from "hooks/useBrowserExtension"
@@ -90,7 +91,7 @@ export function FeedEntries() {
)
}
const swipedRight = (entry: ExpendableEntry) => dispatch(markEntry({ entry, read: !entry.read }))
const swipedLeft = async (entry: ExpendableEntry) => await dispatch(markEntry({ entry, read: !entry.read }))
// close context menu on scroll
useEffect(() => {
@@ -127,42 +128,50 @@ export function FeedEntries() {
return () => window.removeEventListener("scroll", listener)
}, [dispatch, contextMenu, entries, viewMode, scrollMarks, scrollingToEntry])
useMousetrap("r", () => dispatch(reloadEntries()))
useMousetrap("j", () =>
dispatch(
selectNextEntry({
expand: true,
markAsRead: true,
scrollToEntry: true,
})
)
useMousetrap("r", async () => await dispatch(reloadEntries()))
useMousetrap(
"j",
async () =>
await dispatch(
selectNextEntry({
expand: true,
markAsRead: true,
scrollToEntry: true,
})
)
)
useMousetrap("n", () =>
dispatch(
selectNextEntry({
expand: false,
markAsRead: false,
scrollToEntry: true,
})
)
useMousetrap(
"n",
async () =>
await dispatch(
selectNextEntry({
expand: false,
markAsRead: false,
scrollToEntry: true,
})
)
)
useMousetrap("k", () =>
dispatch(
selectPreviousEntry({
expand: true,
markAsRead: true,
scrollToEntry: true,
})
)
useMousetrap(
"k",
async () =>
await dispatch(
selectPreviousEntry({
expand: true,
markAsRead: true,
scrollToEntry: true,
})
)
)
useMousetrap("p", () =>
dispatch(
selectPreviousEntry({
expand: false,
markAsRead: false,
scrollToEntry: true,
})
)
useMousetrap(
"p",
async () =>
await dispatch(
selectPreviousEntry({
expand: false,
markAsRead: false,
scrollToEntry: true,
})
)
)
useMousetrap("space", () => {
if (selectedEntry) {
@@ -257,6 +266,11 @@ export function FeedEntries() {
if (!selectedEntry) return
dispatch(markEntry({ entry: selectedEntry, read: !selectedEntry.read }))
})
useMousetrap("s", () => {
// toggle starred status
if (!selectedEntry) return
dispatch(starEntry({ entry: selectedEntry, starred: !selectedEntry.starred }))
})
useMousetrap("shift+a", () => {
// mark all entries as read
dispatch(
@@ -265,12 +279,13 @@ export function FeedEntries() {
req: {
id: source.id,
read: true,
olderThan: entriesTimestamp,
olderThan: Date.now(),
insertedBefore: entriesTimestamp,
},
})
)
})
useMousetrap("g a", () => dispatch(redirectToRootCategory()))
useMousetrap("g a", async () => await dispatch(redirectToRootCategory()))
useMousetrap("f", () => dispatch(toggleSidebar()))
useMousetrap("?", () =>
openModal({
@@ -284,8 +299,9 @@ export function FeedEntries() {
return (
<InfiniteScroll
id="entries"
className={`view-mode-${viewMode}`}
initialLoad={false}
loadMore={() => !loading && dispatch(loadMoreEntries())}
loadMore={async () => await (!loading && dispatch(loadMoreEntries()))}
hasMore={hasMore}
loader={<Box key={0}>{loading && <Loader />}</Box>}
>
@@ -305,7 +321,7 @@ export function FeedEntries() {
onHeaderClick={event => headerClicked(entry, event)}
onHeaderRightClick={event => headerRightClicked(entry, event)}
onBodyClick={() => bodyClicked(entry)}
onSwipedRight={() => swipedRight(entry)}
onSwipedLeft={async () => await swipedLeft(entry)}
/>
</div>
))}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,9 @@
import { t } from "@lingui/macro"
import { Select, SelectItem, SelectProps } from "@mantine/core"
import { Select, type SelectProps } from "@mantine/core"
import { type ComboboxItem } from "@mantine/core/lib/components/Combobox/Combobox.types"
import { Constants } from "app/constants"
import { useAppSelector } from "app/store"
import { type Category } from "app/types"
import { flattenCategoryTree } from "app/utils"
type CategorySelectProps = Partial<SelectProps> & {
@@ -12,14 +14,32 @@ type CategorySelectProps = Partial<SelectProps> & {
export function CategorySelect(props: CategorySelectProps) {
const rootCategory = useAppSelector(state => state.tree.rootCategory)
const categories = rootCategory && flattenCategoryTree(rootCategory)
const selectData: SelectItem[] | undefined = categories
const categoriesById = categories?.reduce((map, c) => {
map.set(c.id, c)
return map
}, new Map<string, Category>())
const categoryLabel = (cat: Category) => {
let label = cat.name
while (cat.parentId) {
const parent = categoriesById?.get(cat.parentId)
if (!parent) {
break
}
label = `${parent.name}${label}`
cat = parent
}
return label
}
const selectData: ComboboxItem[] | undefined = categories
?.filter(c => c.id !== Constants.categories.all.id)
.filter(c => !props.withoutCategoryIds || !props.withoutCategoryIds.includes(c.id))
.sort((c1, c2) => c1.name.localeCompare(c2.name))
.filter(c => !props.withoutCategoryIds?.includes(c.id))
.map(c => ({
label: c.parentName ? t`${c.name} (in ${c.parentName})` : c.name,
label: categoryLabel(c),
value: c.id,
}))
.sort((c1, c2) => c1.label.localeCompare(c2.label))
if (props.withAll) {
selectData?.unshift({
label: t`All`,

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import { Trans } from "@lingui/macro"
import { Button, Code, Group, Modal, Slider, Stack, Text } from "@mantine/core"
import { markAllEntries } from "app/slices/entries"
import { markAllEntries } from "app/entries/thunks"
import { useAppDispatch, useAppSelector } from "app/store"
import { ActionButton } from "components/ActionButton"
import { useState } from "react"
@@ -27,7 +27,8 @@ export function MarkAllAsReadButton(props: { iconSize: number }) {
req: {
id: source.id,
read: true,
olderThan: entriesTimestamp,
olderThan: Date.now(),
insertedBefore: entriesTimestamp,
},
})
)
@@ -64,7 +65,7 @@ export function MarkAllAsReadButton(props: { iconSize: number }) {
value={threshold}
onChange={setThreshold}
/>
<Group position="right">
<Group justify="flex-end">
<Button variant="default" onClick={() => setOpened(false)}>
<Trans>Cancel</Trans>
</Button>
@@ -78,7 +79,8 @@ export function MarkAllAsReadButton(props: { iconSize: number }) {
req: {
id: source.id,
read: true,
olderThan: entriesTimestamp - threshold * 24 * 60 * 60 * 1000,
olderThan: Date.now() - threshold * 24 * 60 * 60 * 1000,
insertedBefore: entriesTimestamp,
},
})
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@ 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 { redirectToSelectedSource } from "app/redirect/thunks"
import { useAppDispatch, useAppSelector } from "app/store"
import { Alert } from "components/Alert"
import { CodeEditor } from "components/code/CodeEditor"
@@ -69,10 +69,10 @@ export function CustomCodeSettings() {
/>
<Group>
<Button variant="default" onClick={() => dispatch(redirectToSelectedSource())}>
<Button variant="default" onClick={async () => await dispatch(redirectToSelectedSource())}>
<Trans>Cancel</Trans>
</Button>
<Button type="submit" leftIcon={<TbDeviceFloppy size={16} />} loading={saveCustomCode.loading}>
<Button type="submit" leftSection={<TbDeviceFloppy size={16} />} loading={saveCustomCode.loading}>
<Trans>Save</Trans>
</Button>
</Group>

View File

@@ -1,31 +1,40 @@
import { Trans } from "@lingui/macro"
import { Divider, Select, SimpleGrid, Stack, Switch } from "@mantine/core"
import { Divider, Group, Radio, Select, SimpleGrid, Stack, Switch } from "@mantine/core"
import { Constants } from "app/constants"
import { useAppDispatch, useAppSelector } from "app/store"
import { type ScrollMode, type SharingSettings } from "app/types"
import {
changeAlwaysScrollToEntry,
changeCustomContextMenu,
changeLanguage,
changeMarkAllAsReadConfirmation,
changeMobileFooter,
changeScrollMarks,
changeScrollMode,
changeScrollSpeed,
changeSharingSetting,
changeShowRead,
} from "app/slices/user"
import { useAppDispatch, useAppSelector } from "app/store"
import { SharingSettings } from "app/types"
} from "app/user/thunks"
import { locales } from "i18n"
import { type ReactNode } from "react"
export function DisplaySettings() {
const language = useAppSelector(state => state.user.settings?.language)
const scrollSpeed = useAppSelector(state => state.user.settings?.scrollSpeed)
const showRead = useAppSelector(state => state.user.settings?.showRead)
const scrollMarks = useAppSelector(state => state.user.settings?.scrollMarks)
const alwaysScrollToEntry = useAppSelector(state => state.user.settings?.alwaysScrollToEntry)
const scrollMode = useAppSelector(state => state.user.settings?.scrollMode)
const markAllAsReadConfirmation = useAppSelector(state => state.user.settings?.markAllAsReadConfirmation)
const customContextMenu = useAppSelector(state => state.user.settings?.customContextMenu)
const mobileFooter = useAppSelector(state => state.user.settings?.mobileFooter)
const sharingSettings = useAppSelector(state => state.user.settings?.sharingSettings)
const dispatch = useAppDispatch()
const scrollModeOptions: Record<ScrollMode, ReactNode> = {
always: <Trans>Always</Trans>,
never: <Trans>Never</Trans>,
if_needed: <Trans>If the entry doesn't entirely fit on the screen</Trans>,
}
return (
<Stack>
<Select
@@ -35,43 +44,57 @@ export function DisplaySettings() {
value: l.key,
label: l.label,
}))}
onChange={s => s && dispatch(changeLanguage(s))}
/>
<Switch
label={<Trans>Scroll smoothly when navigating between entries</Trans>}
checked={scrollSpeed ? scrollSpeed > 0 : false}
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))}
onChange={async s => await (s && dispatch(changeLanguage(s)))}
/>
<Switch
label={<Trans>Show feeds and categories with no unread entries</Trans>}
checked={showRead}
onChange={e => dispatch(changeShowRead(e.currentTarget.checked))}
/>
<Switch
label={<Trans>In expanded view, scrolling through entries mark them as read</Trans>}
checked={scrollMarks}
onChange={e => dispatch(changeScrollMarks(e.currentTarget.checked))}
onChange={async e => await dispatch(changeShowRead(e.currentTarget.checked))}
/>
<Switch
label={<Trans>Show confirmation when marking all entries as read</Trans>}
checked={markAllAsReadConfirmation}
onChange={e => dispatch(changeMarkAllAsReadConfirmation(e.currentTarget.checked))}
onChange={async e => await dispatch(changeMarkAllAsReadConfirmation(e.currentTarget.checked))}
/>
<Switch
label={<Trans>Show CommaFeed's own context menu on right click</Trans>}
checked={customContextMenu}
onChange={e => dispatch(changeCustomContextMenu(e.currentTarget.checked))}
onChange={async e => await dispatch(changeCustomContextMenu(e.currentTarget.checked))}
/>
<Switch
label={<Trans>On mobile, show action buttons at the bottom of the screen</Trans>}
checked={mobileFooter}
onChange={async e => await dispatch(changeMobileFooter(e.currentTarget.checked))}
/>
<Divider label={<Trans>Scrolling</Trans>} labelPosition="center" />
<Radio.Group
label={<Trans>Scroll selected entry to the top of the page</Trans>}
value={scrollMode}
onChange={async value => await dispatch(changeScrollMode(value as ScrollMode))}
>
<Group mt="xs">
{Object.entries(scrollModeOptions).map(e => (
<Radio key={e[0]} value={e[0]} label={e[1]} />
))}
</Group>
</Radio.Group>
<Switch
label={<Trans>Scroll smoothly when navigating between entries</Trans>}
checked={scrollSpeed ? scrollSpeed > 0 : false}
onChange={async e => await dispatch(changeScrollSpeed(e.currentTarget.checked))}
/>
<Switch
label={<Trans>In expanded view, scrolling through entries mark them as read</Trans>}
checked={scrollMarks}
onChange={async e => await dispatch(changeScrollMarks(e.currentTarget.checked))}
/>
<Divider label={<Trans>Sharing sites</Trans>} labelPosition="center" />
@@ -81,8 +104,15 @@ export function DisplaySettings() {
<Switch
key={site}
label={Constants.sharing[site].label}
checked={sharingSettings && sharingSettings[site]}
onChange={e => dispatch(changeSharingSetting({ site, value: e.currentTarget.checked }))}
checked={sharingSettings?.[site]}
onChange={async e =>
await dispatch(
changeSharingSetting({
site,
value: e.currentTarget.checked,
})
)
}
/>
))}
</SimpleGrid>

View File

@@ -3,10 +3,10 @@ import { Anchor, Box, Button, Checkbox, Divider, Group, Input, PasswordInput, St
import { useForm } from "@mantine/form"
import { openConfirmModal } from "@mantine/modals"
import { client, errorToStrings } from "app/client"
import { redirectToLogin, redirectToSelectedSource } from "app/slices/redirect"
import { reloadProfile } from "app/slices/user"
import { redirectToLogin, redirectToSelectedSource } from "app/redirect/thunks"
import { useAppDispatch, useAppSelector } from "app/store"
import { ProfileModificationRequest } from "app/types"
import { type ProfileModificationRequest } from "app/types"
import { reloadProfile } from "app/user/thunks"
import { Alert } from "components/Alert"
import { useEffect } from "react"
import { useAsyncCallback } from "react-async-hook"
@@ -49,7 +49,7 @@ export function ProfileSettings() {
),
labels: { confirm: <Trans>Confirm</Trans>, cancel: <Trans>Cancel</Trans> },
confirmProps: { color: "red" },
onConfirm: () => deleteProfile.execute(),
onConfirm: async () => await deleteProfile.execute(),
})
useEffect(() => {
@@ -77,11 +77,18 @@ export function ProfileSettings() {
<form onSubmit={form.onSubmit(saveProfile.execute)}>
<Stack>
<Input.Wrapper label={<Trans>User name</Trans>}>
<Box>{profile?.name}</Box>
</Input.Wrapper>
<TextInput label={<Trans>API key</Trans>} readOnly value={profile?.apiKey} />
<TextInput label={<Trans>User name</Trans>} readOnly value={profile?.name} />
<TextInput
label={<Trans>API key</Trans>}
description={
<Trans>
This is your API key. It can be used for some read-only API operations and grants access to the Fever API.
Use the form at the bottom of the page to generate a new API key
</Trans>
}
readOnly
value={profile?.apiKey}
/>
<Input.Wrapper
label={<Trans>OPML export</Trans>}
@@ -98,6 +105,22 @@ export function ProfileSettings() {
</Box>
</Input.Wrapper>
<Input.Wrapper
label={<Trans>Fever API</Trans>}
description={
<Trans>
CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client.
Login with your username and your <u>API key</u>.
</Trans>
}
>
<Box>
<Anchor href={`rest/fever/user/${profile?.id}`} target="_blank">
<Trans>Fever API URL</Trans>
</Anchor>
</Box>
</Input.Wrapper>
<Divider />
<PasswordInput
@@ -116,16 +139,16 @@ export function ProfileSettings() {
<Checkbox label={<Trans>Generate new API key</Trans>} {...form.getInputProps("newApiKey", { type: "checkbox" })} />
<Group>
<Button variant="default" onClick={() => dispatch(redirectToSelectedSource())}>
<Button variant="default" onClick={async () => await dispatch(redirectToSelectedSource())}>
<Trans>Cancel</Trans>
</Button>
<Button type="submit" leftIcon={<TbDeviceFloppy size={16} />} loading={saveProfile.loading}>
<Button type="submit" leftSection={<TbDeviceFloppy size={16} />} loading={saveProfile.loading}>
<Trans>Save</Trans>
</Button>
<Divider orientation="vertical" />
<Button
color="red"
leftIcon={<TbTrash size={16} />}
leftSection={<TbTrash size={16} />}
onClick={() => openDeleteProfileModal()}
loading={deleteProfile.loading}
>

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,20 @@
// the color scheme to use to render components
import { useMantineColorScheme } from "@mantine/core"
import { useMediaQuery } from "@mantine/hooks"
export const useColorScheme = () => {
const systemColorScheme = useMediaQuery(
"(prefers-color-scheme: dark)",
// passing undefined will use window.matchMedia(query) as default value
undefined,
{
// get initial value synchronously and not in useEffect to avoid flash of light theme
getInitialValueInEffect: false,
}
)
? "dark"
: "light"
const { colorScheme } = useMantineColorScheme()
return colorScheme === "auto" ? systemColorScheme : colorScheme
}

View File

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

View File

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

View File

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

View File

@@ -1,24 +1,50 @@
import { reloadTree } from "app/slices/tree"
import { useAppDispatch } from "app/store"
import { setWebSocketConnected } from "app/server/slice"
import { type AppDispatch, useAppDispatch, useAppSelector } from "app/store"
import { incrementUnreadCount } from "app/tree/slice"
import { useEffect } from "react"
import WebsocketHeartbeatJs from "websocket-heartbeat-js"
const handleMessage = (dispatch: AppDispatch, message: string) => {
const parts = message.split(":")
const type = parts[0]
if (type === "new-feed-entries") {
dispatch(
incrementUnreadCount({
feedId: +parts[1],
amount: +parts[2],
})
)
}
}
export const useWebSocket = () => {
const websocketEnabled = useAppSelector(state => state.server.serverInfos?.websocketEnabled)
const websocketPingInterval = useAppSelector(state => state.server.serverInfos?.websocketPingInterval)
const dispatch = useAppDispatch()
useEffect(() => {
const currentUrl = new URL(window.location.href)
const wsProtocol = currentUrl.protocol === "http:" ? "ws" : "wss"
const wsUrl = `${wsProtocol}://${currentUrl.hostname}:${currentUrl.port}/ws`
let ws: WebsocketHeartbeatJs | undefined
const ws = new WebsocketHeartbeatJs({ url: wsUrl, pingMsg: "ping" })
ws.onmessage = event => {
const { data } = event
if (typeof data === "string") {
if (data.startsWith("new-feed-entries:")) dispatch(reloadTree())
if (websocketEnabled && websocketPingInterval) {
const currentUrl = new URL(window.location.href)
const wsProtocol = currentUrl.protocol === "http:" ? "ws" : "wss"
const wsUrl = `${wsProtocol}://${currentUrl.hostname}:${currentUrl.port}${currentUrl.pathname}ws`
ws = new WebsocketHeartbeatJs({
url: wsUrl,
pingMsg: "ping",
pingTimeout: websocketPingInterval,
})
ws.onopen = () => dispatch(setWebSocketConnected(true))
ws.onclose = () => dispatch(setWebSocketConnected(false))
ws.onmessage = event => {
const { data } = event
if (typeof data === "string") {
handleMessage(dispatch, data)
}
}
}
return () => ws.close()
}, [dispatch])
return () => ws?.close()
}, [dispatch, websocketEnabled, websocketPingInterval])
}

View File

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

View File

@@ -13,10 +13,6 @@ msgstr ""
"Language-Team: \n"
"Plural-Forms: \n"
#: src/components/content/add/CategorySelect.tsx
msgid "{0} (in {1})"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
msgstr ""
@@ -72,7 +68,7 @@ msgid "All"
msgstr "الكل"
#: src/components/settings/DisplaySettings.tsx
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
msgid "Always"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx
@@ -175,10 +171,18 @@ msgstr "سيؤدي تغيير كلمة المرور إلى إنشاء مفتاح
msgid "Check that the feed is working"
msgstr "تأكد من عمل الخلاصة"
#: src/pages/app/Layout.tsx
msgid "Close menu"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed التالي العنصر غير المقروء"
@@ -207,10 +211,6 @@ msgstr "تأكيد كلمة المرور"
msgid "Cozy"
msgstr "دافئ"
#: src/components/content/FeedEntryFooter.tsx
msgid "Create tag: {query}"
msgstr "إنشاء علامة: {استعلام}"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Ctrl"
msgstr "السيطرة"
@@ -231,6 +231,10 @@ msgstr ""
msgid "Custom JS code that will be executed on page load"
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Dark"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx
msgid "Date created"
msgstr "تاريخ الإنشاء"
@@ -343,6 +347,14 @@ msgstr "موجز URL"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "الملف مطلوب"
@@ -395,6 +407,10 @@ msgstr "المرجع نفسه"
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
msgstr "إذا لم يكن فارغًا ، فسيتم تقييم التعبير إلى \"صواب\" أو \"خطأ\". "
#: src/components/settings/DisplaySettings.tsx
msgid "If the entry doesn't entirely fit on the screen"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "If you encounter an issue, please report it on the issues page of the GitHub project."
msgstr "إذا واجهت مشكلة ، فالرجاء الإبلاغ عنها على صفحة مشكلات مشروع GitHub."
@@ -433,6 +449,10 @@ msgstr "التحديث الأخير"
msgid "Last refresh message"
msgstr "آخر رسالة تحديث"
#: src/components/header/ProfileMenu.tsx
msgid "Light"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx
#: src/pages/app/TagDetailsPage.tsx
@@ -525,6 +545,10 @@ msgstr "الاسم"
msgid "Navigate to a subscription by entering its name"
msgstr "انتقل إلى اشتراك بإدخال اسمه"
#: src/components/settings/DisplaySettings.tsx
msgid "Never"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "New password"
msgstr "كلمة مرور جديدة"
@@ -558,6 +582,10 @@ msgstr "لم يتم العثور على شيء"
msgid "Oldest first"
msgstr "الأقدم أولا"
#: src/components/settings/DisplaySettings.tsx
msgid "On mobile, show action buttons at the bottom of the screen"
msgstr ""
#: src/pages/ErrorPage.tsx
msgid "Oops!"
msgstr "اوووه!"
@@ -586,6 +614,10 @@ msgstr ""
msgid "Open link in new tab"
msgstr ""
#: src/pages/app/Layout.tsx
msgid "Open menu"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry"
msgstr "فتح الإدخال التالي"
@@ -682,10 +714,18 @@ msgstr ""
msgid "Save"
msgstr "حفظ"
#: src/components/settings/DisplaySettings.tsx
msgid "Scroll selected entry to the top of the page"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Scroll smoothly when navigating between entries"
msgstr "قم بالتمرير بسلاسة عند التنقل بين الإدخالات"
#: src/components/settings/DisplaySettings.tsx
msgid "Scrolling"
msgstr ""
#: src/components/header/Header.tsx
#: src/components/header/Header.tsx
#: src/components/sidebar/TreeSearch.tsx
@@ -709,7 +749,7 @@ msgstr "ضع التركيز على الإدخال السابق دون فتحه"
msgid "Settings"
msgstr "إعدادات"
#: src/app/slices/user.ts
#: src/app/user/slice.ts
msgid "Settings saved."
msgstr "تم حفظ الإعدادات."
@@ -799,19 +839,22 @@ msgid "Success"
msgstr "النجاح"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Swipe header to the right"
msgid "Swipe header to the left"
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 "قم بالتبديل إلى النسق الفاتح"
#: src/components/header/ProfileMenu.tsx
msgid "System"
msgstr ""
#: src/components/content/FeedEntryFooter.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Tags"
msgstr "الكلمات"
@@ -824,6 +867,10 @@ msgstr "عنوان URL للتغذية التي تريد الاشتراك فيه
msgid "Theme"
msgstr "الموضوع"
#: src/components/settings/ProfileSettings.tsx
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle read status of current entry"
msgstr "تبديل قراءة حالة الإدخال الحالي"
@@ -832,6 +879,10 @@ msgstr "تبديل قراءة حالة الإدخال الحالي"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "جرب CommaFeed باستخدام الحساب التجريبي: تجريبي / تجريبي"

View File

@@ -13,17 +13,13 @@ msgstr ""
"Language-Team: \n"
"Plural-Forms: \n"
#: src/components/content/add/CategorySelect.tsx
msgid "{0} (in {1})"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
msgstr ""
msgstr "<0>CommaFeed és un projecte de codi obert. El codi font està allotjat a </0><1>GitHub</1>."
#: src/pages/app/FeedDetailsPage.tsx
msgid "<0>Complete syntax is available </0><1>here</1>."
msgstr ""
msgstr "<0>La sintaxi completa està disponible </0><1>aquí</1>."
#: src/pages/auth/RegistrationPage.tsx
msgid "<0>Have an account?</0><1>Log in!</1>"
@@ -31,7 +27,7 @@ msgstr "<0>Teniu un compte?</0><1>Inicieu la sessió!</1>"
#: src/pages/app/DonatePage.tsx
msgid "<0>Hey,</0><1>I'm Jérémie from Belgium and I've been working on CommaFeed in my free time for over 10 years now. Thanks for taking an interest in helping me continue supporting CommaFeed.</1>"
msgstr ""
msgstr "<0>Ei,</0><1> sóc la Jérémie de Bèlgica i fa més de 10 anys que treballo a CommaFeed en el meu temps lliure. Gràcies per interessar-te i ajudar-me a continuar donant suport a CommaFeed.</1>"
#: src/pages/auth/LoginPage.tsx
msgid "<0>Need an account?</0><1>Sign up!</1>"
@@ -72,8 +68,8 @@ msgid "All"
msgstr "Tot"
#: src/components/settings/DisplaySettings.tsx
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
msgstr ""
msgid "Always"
msgstr "Sempre"
#: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox."
@@ -89,7 +85,7 @@ msgstr "Analitzar el feed"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
msgstr "Anunci"
#: src/components/settings/ProfileSettings.tsx
msgid "API key"
@@ -121,7 +117,7 @@ msgstr "Estàs segur que vols cancel·lar la subscripció a <0>{feedName}</0>?"
#: src/components/header/Header.tsx
msgid "Asc"
msgstr ""
msgstr "Asc"
#: src/pages/app/FeedDetailsPage.tsx
msgid "Available variables are 'title', 'content', 'url' 'author' and 'categories' and their content is converted to lower case to ease string comparison."
@@ -137,11 +133,11 @@ msgstr "Tornar a iniciar sessió"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
msgstr "Extensió del navegador necessària per a Chrome"
#: src/pages/app/AboutPage.tsx
msgid "Browser extention"
msgstr ""
msgstr "Extensió del navegador"
#: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx
@@ -175,9 +171,17 @@ msgstr "Canviar la contrasenya generarà una nova clau d'API"
msgid "Check that the feed is working"
msgstr "Comproveu que el canal funciona"
#: src/pages/app/Layout.tsx
msgid "Close menu"
msgstr "Tanca el menu"
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
msgstr "Versió de l'extensió del navegador CommaFeed {browserExtensionVersion}."
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "CommaFeed és compatible amb l'API Fever. Utilitzeu l'URL següent al vostre client mòbil compatible amb Fever. Inicieu sessió amb el vostre nom d'usuari i la vostra <0>clau API</0>."
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
@@ -185,7 +189,7 @@ msgstr "CommaFeed següent element no llegit"
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed version {version} ({revision})."
msgstr ""
msgstr "CommaFeed versió {version} ({version})."
#: src/components/header/ProfileMenu.tsx
msgid "Compact"
@@ -197,7 +201,7 @@ msgstr "Compacte"
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx
msgid "Confirm"
msgstr "Confirmar"
msgstr "Confirma"
#: src/components/settings/ProfileSettings.tsx
msgid "Confirm password"
@@ -207,13 +211,9 @@ msgstr "Confirmeu la contrasenya"
msgid "Cozy"
msgstr "Acollidor"
#: src/components/content/FeedEntryFooter.tsx
msgid "Create tag: {query}"
msgstr "Crea una etiqueta: {consulta}"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Ctrl"
msgstr ""
msgstr "Ctrl"
#: src/components/settings/ProfileSettings.tsx
msgid "Current password"
@@ -221,15 +221,19 @@ msgstr "Contrasenya actual"
#: src/pages/app/SettingsPage.tsx
msgid "Custom code"
msgstr ""
msgstr "Codi personalitzat"
#: src/components/settings/CustomCodeSettings.tsx
msgid "Custom CSS rules that will be applied"
msgstr ""
msgstr "Regles CSS personalitzades que s'aplicaran"
#: src/components/settings/CustomCodeSettings.tsx
msgid "Custom JS code that will be executed on page load"
msgstr ""
msgstr "Codi JS personalitzat que s'executarà en carregar la pàgina"
#: src/components/header/ProfileMenu.tsx
msgid "Dark"
msgstr "Fosc"
#: src/pages/admin/AdminUsersPage.tsx
msgid "Date created"
@@ -254,11 +258,11 @@ msgstr "Suprimeix l'usuari"
#: src/components/header/Header.tsx
msgid "Desc"
msgstr ""
msgstr "Desc"
#: src/components/header/ProfileMenu.tsx
msgid "Detailed"
msgstr ""
msgstr "Detallat"
#: src/components/header/ProfileMenu.tsx
#: src/pages/app/SettingsPage.tsx
@@ -268,7 +272,7 @@ msgstr "Mostra"
#: src/components/header/ProfileMenu.tsx
#: src/pages/app/DonatePage.tsx
msgid "Donate"
msgstr ""
msgstr "Donar"
#: src/components/settings/ProfileSettings.tsx
msgid "Download"
@@ -310,7 +314,7 @@ msgstr "introduïu la vostra contrasenya actual per canviar la configuració del
#: src/components/Alert.tsx
msgid "Error"
msgstr ""
msgstr "Error"
#: src/pages/app/FeedDetailsPage.tsx
msgid "Example: {example}."
@@ -327,7 +331,7 @@ msgstr "exporteu les vostres subscripcions i categories com a fitxer OPML que es
#: src/components/header/Header.tsx
#: src/pages/WelcomePage.tsx
msgid "Extension options"
msgstr ""
msgstr "Opcions de l'extensió"
#: src/components/content/add/Subscribe.tsx
msgid "Feed name"
@@ -341,6 +345,14 @@ msgstr "URL del canal"
#: src/components/header/ProfileMenu.tsx
msgid "Fetch all my feeds now"
msgstr "Carrega tots els meus feeds ara"
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
@@ -373,7 +385,7 @@ msgstr "URL del feed generat"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Go to {0}"
msgstr ""
msgstr "Vés a {0}"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Go to the All view"
@@ -389,12 +401,16 @@ msgstr "Bones"
#: src/pages/admin/AdminUsersPage.tsx
msgid "Id"
msgstr ""
msgstr "Id"
#: src/pages/app/FeedDetailsPage.tsx
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
msgstr "Si no està buida, una expressió que s'avalua com a \"vertader\" o \"fals\". "
#: src/components/settings/DisplaySettings.tsx
msgid "If the entry doesn't entirely fit on the screen"
msgstr "Si l'entrada no encaixa del tot a la pantalla"
#: src/pages/app/AboutPage.tsx
msgid "If you encounter an issue, please report it on the issues page of the GitHub project."
msgstr "Si trobeu un problema, informeu-lo a la pàgina de problemes del projecte GitHub."
@@ -433,6 +449,10 @@ msgstr "Última actualització"
msgid "Last refresh message"
msgstr "últim missatge d'actualització"
#: src/components/header/ProfileMenu.tsx
msgid "Light"
msgstr "Clar"
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx
#: src/pages/app/TagDetailsPage.tsx
@@ -499,7 +519,7 @@ msgstr "mètriques"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Middle click"
msgstr ""
msgstr "Clic central"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Move the page down"
@@ -525,6 +545,10 @@ msgstr "Nom"
msgid "Navigate to a subscription by entering its name"
msgstr "Navegueu a una subscripció introduint-ne el nom"
#: src/components/settings/DisplaySettings.tsx
msgid "Never"
msgstr "Mai"
#: src/components/settings/ProfileSettings.tsx
msgid "New password"
msgstr "Contrasenya nova"
@@ -558,13 +582,17 @@ msgstr "No s'ha trobat res"
msgid "Oldest first"
msgstr "el més vell primer"
#: src/components/settings/DisplaySettings.tsx
msgid "On mobile, show action buttons at the bottom of the screen"
msgstr "Al mòbil, mostra els botons d'acció a la part inferior de la pantalla"
#: src/pages/ErrorPage.tsx
msgid "Oops!"
msgstr "Vaja!"
#: src/components/header/Header.tsx
msgid "Open CommaFeed"
msgstr ""
msgstr "Obre CommaFeed"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Open current entry in a new tab"
@@ -576,15 +604,19 @@ msgstr "Obre l'entrada actual en una pestanya nova al fons"
#: src/components/content/FeedEntryFooter.tsx
msgid "Open link"
msgstr "Enllaç obert"
msgstr "Obre l'enllaç obert"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new background tab"
msgstr ""
msgstr "Obre l'enllaç a una pestanya de fons nova"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new tab"
msgstr ""
msgstr "Obre l'enllaç en una pestanya nova"
#: src/pages/app/Layout.tsx
msgid "Open menu"
msgstr "Obre el menú"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry"
@@ -600,7 +632,7 @@ msgstr "Obrir/tancar l'entrada actual"
#: src/pages/app/AddPage.tsx
msgid "OPML"
msgstr ""
msgstr "OPML"
#: src/components/settings/ProfileSettings.tsx
msgid "OPML export"
@@ -646,7 +678,7 @@ msgstr "Posició"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
msgstr "Anterior"
#: src/pages/app/SettingsPage.tsx
msgid "Profile"
@@ -672,7 +704,7 @@ msgstr "API REST"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
msgstr "Clic dret"
#: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx
@@ -682,10 +714,18 @@ msgstr ""
msgid "Save"
msgstr "Desa"
#: src/components/settings/DisplaySettings.tsx
msgid "Scroll selected entry to the top of the page"
msgstr "Desplaceu-vos per l'entrada seleccionada fins a la part superior de la pàgina"
#: src/components/settings/DisplaySettings.tsx
msgid "Scroll smoothly when navigating between entries"
msgstr "Desplaceu-vos suaument quan navegueu entre entrades"
#: src/components/settings/DisplaySettings.tsx
msgid "Scrolling"
msgstr "Desplaçament"
#: src/components/header/Header.tsx
#: src/components/header/Header.tsx
#: src/components/sidebar/TreeSearch.tsx
@@ -709,7 +749,7 @@ msgstr "Estableix el focus en l'entrada anterior sense obrir-la"
msgid "Settings"
msgstr "Configuració"
#: src/app/slices/user.ts
#: src/app/user/slice.ts
msgid "Settings saved."
msgstr "Configuració desada."
@@ -729,19 +769,19 @@ msgstr "canvi"
#: src/components/settings/DisplaySettings.tsx
msgid "Show CommaFeed's own context menu on right click"
msgstr ""
msgstr "Mostra el menú contextual de CommaFeed fent clic amb el botó dret"
#: src/components/settings/DisplaySettings.tsx
msgid "Show confirmation when marking all entries as read"
msgstr ""
msgstr "Mostra la confirmació en marcar totes les entrades com a llegides"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)"
msgstr ""
msgstr "Mostra el menú d'entrada (escriptori)"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (mobile)"
msgstr ""
msgstr "Mostra el menú d'entrada (mòbil)"
#: src/components/settings/DisplaySettings.tsx
msgid "Show feeds and categories with no unread entries"
@@ -753,13 +793,13 @@ msgstr "Mostra l'ajuda de la drecera del teclat"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show native menu (desktop)"
msgstr ""
msgstr "Mostra el menú natiu (escriptori)"
#: src/pages/auth/RegistrationPage.tsx
#: src/pages/auth/RegistrationPage.tsx
#: src/pages/WelcomePage.tsx
msgid "Sign up"
msgstr "Inscriu-te"
msgstr "Registra't"
#: src/pages/ErrorPage.tsx
msgid "Something bad just happened..."
@@ -799,19 +839,22 @@ msgid "Success"
msgstr "Éxit"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Swipe header to the right"
msgstr ""
msgid "Swipe header to the left"
msgstr "Feu lliscar la capçalera cap a l'esquerra"
#: src/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"
#: src/components/header/ProfileMenu.tsx
msgid "System"
msgstr "Sistema"
#: src/components/content/FeedEntryFooter.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Tags"
msgstr "Etiquetes"
@@ -824,13 +867,21 @@ msgstr "l'URL del canal al qual us voleu subscriure. "
msgid "Theme"
msgstr "Tema"
#: src/components/settings/ProfileSettings.tsx
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
msgstr "Aquesta és la vostra clau de l'API. Es pot utilitzar per a algunes operacions de l'API de només lectura i permet accedir a l'API Fever. Utilitzeu el formulari de la part inferior de la pàgina per generar una nova clau d'API."
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle read status of current entry"
msgstr "Canvia l'estat de lectura de l'entrada actual"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
msgstr "Canvia la barra lateral"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr "Commuta l'estat destacat de l'entrada actual"
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
@@ -838,7 +889,7 @@ msgstr "Proveu CommaFeed amb el compte de demostració: demo/demo"
#: src/pages/WelcomePage.tsx
msgid "Try the demo!"
msgstr ""
msgstr "Prova la demostració!"
#: src/components/header/Header.tsx
msgid "Unread"
@@ -877,4 +928,4 @@ msgstr "Encara no teniu cap subscripció. "
#: src/components/header/ProfileMenu.tsx
msgid "Your feeds have been queued for refresh."
msgstr ""
msgstr "Els vostres feeds s'han posat a la cua per actualitzar-los."

View File

@@ -13,10 +13,6 @@ msgstr ""
"Language-Team: \n"
"Plural-Forms: \n"
#: src/components/content/add/CategorySelect.tsx
msgid "{0} (in {1})"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
msgstr ""
@@ -72,7 +68,7 @@ msgid "All"
msgstr "Všechny"
#: src/components/settings/DisplaySettings.tsx
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
msgid "Always"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx
@@ -175,10 +171,18 @@ msgstr "Změna hesla vygeneruje nový klíč API"
msgid "Check that the feed is working"
msgstr "Zkontrolujte, zda zdroj funguje"
#: src/pages/app/Layout.tsx
msgid "Close menu"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed další nepřečtená položka"
@@ -207,10 +211,6 @@ msgstr "Potvrďte heslo"
msgid "Cozy"
msgstr "Útulný"
#: src/components/content/FeedEntryFooter.tsx
msgid "Create tag: {query}"
msgstr "Vytvořit značku: {query}"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Ctrl"
msgstr ""
@@ -231,6 +231,10 @@ msgstr ""
msgid "Custom JS code that will be executed on page load"
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Dark"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx
msgid "Date created"
msgstr "Datum vytvoření"
@@ -343,6 +347,14 @@ msgstr "URL zdroje"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr ""
@@ -395,6 +407,10 @@ msgstr ""
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
msgstr "Pokud není prázdný, výraz vyhodnocený jako 'true' nebo 'false'. "
#: src/components/settings/DisplaySettings.tsx
msgid "If the entry doesn't entirely fit on the screen"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "If you encounter an issue, please report it on the issues page of the GitHub project."
msgstr "Pokud narazíte na problém, nahlaste jej prosím na stránce problémů projektu GitHub."
@@ -433,6 +449,10 @@ msgstr "Poslední aktualizace"
msgid "Last refresh message"
msgstr "Poslední obnovovací zpráva"
#: src/components/header/ProfileMenu.tsx
msgid "Light"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx
#: src/pages/app/TagDetailsPage.tsx
@@ -525,6 +545,10 @@ msgstr "Jméno"
msgid "Navigate to a subscription by entering its name"
msgstr "Přejděte na předplatné zadáním jeho názvu"
#: src/components/settings/DisplaySettings.tsx
msgid "Never"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "New password"
msgstr "Nové heslo"
@@ -558,6 +582,10 @@ msgstr "Nic nebylo nalezeno"
msgid "Oldest first"
msgstr "Nejdříve nejstarší"
#: src/components/settings/DisplaySettings.tsx
msgid "On mobile, show action buttons at the bottom of the screen"
msgstr ""
#: src/pages/ErrorPage.tsx
msgid "Oops!"
msgstr "Jejda!"
@@ -586,6 +614,10 @@ msgstr ""
msgid "Open link in new tab"
msgstr ""
#: src/pages/app/Layout.tsx
msgid "Open menu"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry"
msgstr "Otevřete další položku"
@@ -682,10 +714,18 @@ msgstr ""
msgid "Save"
msgstr "Uložit"
#: src/components/settings/DisplaySettings.tsx
msgid "Scroll selected entry to the top of the page"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Scroll smoothly when navigating between entries"
msgstr "Posouvejte plynule při navigaci mezi položkami"
#: src/components/settings/DisplaySettings.tsx
msgid "Scrolling"
msgstr ""
#: src/components/header/Header.tsx
#: src/components/header/Header.tsx
#: src/components/sidebar/TreeSearch.tsx
@@ -709,7 +749,7 @@ msgstr "Nastavit fokus na předchozí záznam bez jeho otevření"
msgid "Settings"
msgstr "Nastavení"
#: src/app/slices/user.ts
#: src/app/user/slice.ts
msgid "Settings saved."
msgstr "Nastavení uloženo."
@@ -799,19 +839,22 @@ msgid "Success"
msgstr "Úspěch"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Swipe header to the right"
msgid "Swipe header to the left"
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"
#: src/components/header/ProfileMenu.tsx
msgid "System"
msgstr ""
#: src/components/content/FeedEntryFooter.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Tags"
msgstr "Značky"
@@ -824,6 +867,10 @@ msgstr "Adresa URL kanálu, k jehož odběru se chcete přihlásit. "
msgid "Theme"
msgstr "Téma"
#: src/components/settings/ProfileSettings.tsx
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle read status of current entry"
msgstr "Přepne stav čtení aktuálního záznamu"
@@ -832,6 +879,10 @@ msgstr "Přepne stav čtení aktuálního záznamu"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Vyzkoušejte CommaFeed s demo účtem: demo/demo"

View File

@@ -13,10 +13,6 @@ msgstr ""
"Language-Team: \n"
"Plural-Forms: \n"
#: src/components/content/add/CategorySelect.tsx
msgid "{0} (in {1})"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
msgstr ""
@@ -72,7 +68,7 @@ msgid "All"
msgstr "Pawb"
#: src/components/settings/DisplaySettings.tsx
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
msgid "Always"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx
@@ -175,10 +171,18 @@ msgstr "Bydd newid cyfrinair yn cynhyrchu allwedd API newydd"
msgid "Check that the feed is working"
msgstr "Gwiriwch fod y porthiant yn gweithio"
#: src/pages/app/Layout.tsx
msgid "Close menu"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed eitem nesaf heb ei darllen"
@@ -207,10 +211,6 @@ msgstr "Cadarnhau'r cyfrinair"
msgid "Cozy"
msgstr "clyd"
#: src/components/content/FeedEntryFooter.tsx
msgid "Create tag: {query}"
msgstr "Creu tag: {query}"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Ctrl"
msgstr ""
@@ -231,6 +231,10 @@ msgstr ""
msgid "Custom JS code that will be executed on page load"
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Dark"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx
msgid "Date created"
msgstr "Dyddiad creu"
@@ -343,6 +347,14 @@ msgstr "URL porthiant"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "mae angen y ffeil"
@@ -395,6 +407,10 @@ msgstr ""
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
msgstr "Os nad yw'n wag, mynegiad sy'n gwerthuso i 'gwir' neu 'anghywir'. "
#: src/components/settings/DisplaySettings.tsx
msgid "If the entry doesn't entirely fit on the screen"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "If you encounter an issue, please report it on the issues page of the GitHub project."
msgstr "Os byddwch yn dod ar draws mater, rhowch wybod amdano ar dudalen materion y prosiect GitHub."
@@ -433,6 +449,10 @@ msgstr "adnewyddu diwethaf"
msgid "Last refresh message"
msgstr "Neges adnewyddu ddiwethaf"
#: src/components/header/ProfileMenu.tsx
msgid "Light"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx
#: src/pages/app/TagDetailsPage.tsx
@@ -525,6 +545,10 @@ msgstr "Enw"
msgid "Navigate to a subscription by entering its name"
msgstr "Llywiwch i danysgrifiad trwy nodi ei enw"
#: src/components/settings/DisplaySettings.tsx
msgid "Never"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "New password"
msgstr "Cyfrinair newydd"
@@ -558,6 +582,10 @@ msgstr "Dim wedi'i ddarganfod"
msgid "Oldest first"
msgstr "Hynaf yn gyntaf"
#: src/components/settings/DisplaySettings.tsx
msgid "On mobile, show action buttons at the bottom of the screen"
msgstr ""
#: src/pages/ErrorPage.tsx
msgid "Oops!"
msgstr "Wps!"
@@ -586,6 +614,10 @@ msgstr ""
msgid "Open link in new tab"
msgstr ""
#: src/pages/app/Layout.tsx
msgid "Open menu"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry"
msgstr "Agor y cofnod nesaf"
@@ -682,10 +714,18 @@ msgstr ""
msgid "Save"
msgstr "Arbed"
#: src/components/settings/DisplaySettings.tsx
msgid "Scroll selected entry to the top of the page"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Scroll smoothly when navigating between entries"
msgstr "Sgroliwch yn esmwyth wrth lywio rhwng cofnodion"
#: src/components/settings/DisplaySettings.tsx
msgid "Scrolling"
msgstr ""
#: src/components/header/Header.tsx
#: src/components/header/Header.tsx
#: src/components/sidebar/TreeSearch.tsx
@@ -709,7 +749,7 @@ msgstr "Gosod ffocws ar gofnod blaenorol heb ei agor"
msgid "Settings"
msgstr "Gosodiadau"
#: src/app/slices/user.ts
#: src/app/user/slice.ts
msgid "Settings saved."
msgstr "Gosodiadau wedi'u cadw."
@@ -799,19 +839,22 @@ msgid "Success"
msgstr "Llwyddiant"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Swipe header to the right"
msgid "Swipe header to the left"
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"
#: src/components/header/ProfileMenu.tsx
msgid "System"
msgstr ""
#: src/components/content/FeedEntryFooter.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Tags"
msgstr "Tagiau"
@@ -824,6 +867,10 @@ msgstr "Y URL ar gyfer y porthwr rydych chi am danysgrifio iddo. "
msgid "Theme"
msgstr "Thema"
#: src/components/settings/ProfileSettings.tsx
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle read status of current entry"
msgstr "Toglo statws darllen y cofnod cyfredol"
@@ -832,6 +879,10 @@ msgstr "Toglo statws darllen y cofnod cyfredol"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Rhowch gynnig ar CommaFeed gyda'r cyfrif demo: demo / demo"

View File

@@ -13,10 +13,6 @@ msgstr ""
"Language-Team: \n"
"Plural-Forms: \n"
#: src/components/content/add/CategorySelect.tsx
msgid "{0} (in {1})"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
msgstr ""
@@ -72,7 +68,7 @@ msgid "All"
msgstr "Alle"
#: src/components/settings/DisplaySettings.tsx
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
msgid "Always"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx
@@ -175,10 +171,18 @@ msgstr "Ændring af adgangskode vil generere en ny API-nøgle"
msgid "Check that the feed is working"
msgstr "Tjek, at foderet virker"
#: src/pages/app/Layout.tsx
msgid "Close menu"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed næste ulæste element"
@@ -207,10 +211,6 @@ msgstr "Bekræft adgangskode"
msgid "Cozy"
msgstr "Hyggeligt"
#: src/components/content/FeedEntryFooter.tsx
msgid "Create tag: {query}"
msgstr "Opret tag: {query}"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Ctrl"
msgstr ""
@@ -231,6 +231,10 @@ msgstr ""
msgid "Custom JS code that will be executed on page load"
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Dark"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx
msgid "Date created"
msgstr "Dato oprettet"
@@ -343,6 +347,14 @@ msgstr ""
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "fil er påkrævet"
@@ -395,6 +407,10 @@ msgstr ""
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
msgstr "Hvis det ikke er tomt, et udtryk, der vurderes til 'sand' eller 'falsk'. "
#: src/components/settings/DisplaySettings.tsx
msgid "If the entry doesn't entirely fit on the screen"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "If you encounter an issue, please report it on the issues page of the GitHub project."
msgstr "Hvis du støder på et problem, bedes du rapportere det på problemsiden for GitHub-projektet."
@@ -433,6 +449,10 @@ msgstr "Sidste opdatering"
msgid "Last refresh message"
msgstr "Sidste opdateringsmeddelelse"
#: src/components/header/ProfileMenu.tsx
msgid "Light"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx
#: src/pages/app/TagDetailsPage.tsx
@@ -525,6 +545,10 @@ msgstr "Navn"
msgid "Navigate to a subscription by entering its name"
msgstr "Naviger til et abonnement ved at indtaste dets navn"
#: src/components/settings/DisplaySettings.tsx
msgid "Never"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "New password"
msgstr "Ny adgangskode"
@@ -558,6 +582,10 @@ msgstr "Intet fundet"
msgid "Oldest first"
msgstr "Ældst først"
#: src/components/settings/DisplaySettings.tsx
msgid "On mobile, show action buttons at the bottom of the screen"
msgstr ""
#: src/pages/ErrorPage.tsx
msgid "Oops!"
msgstr "Hovsa!"
@@ -586,6 +614,10 @@ msgstr ""
msgid "Open link in new tab"
msgstr ""
#: src/pages/app/Layout.tsx
msgid "Open menu"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry"
msgstr "Åbn næste post"
@@ -682,10 +714,18 @@ msgstr ""
msgid "Save"
msgstr "Gem"
#: src/components/settings/DisplaySettings.tsx
msgid "Scroll selected entry to the top of the page"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Scroll smoothly when navigating between entries"
msgstr "Rul jævnt, når du navigerer mellem poster"
#: src/components/settings/DisplaySettings.tsx
msgid "Scrolling"
msgstr ""
#: src/components/header/Header.tsx
#: src/components/header/Header.tsx
#: src/components/sidebar/TreeSearch.tsx
@@ -709,7 +749,7 @@ msgstr "Sæt fokus på forrige indtastning uden at åbne den"
msgid "Settings"
msgstr "Indstillinger"
#: src/app/slices/user.ts
#: src/app/user/slice.ts
msgid "Settings saved."
msgstr "Indstillinger gemt."
@@ -799,19 +839,22 @@ msgid "Success"
msgstr "Succes"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Swipe header to the right"
msgid "Swipe header to the left"
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"
#: src/components/header/ProfileMenu.tsx
msgid "System"
msgstr ""
#: src/components/content/FeedEntryFooter.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Tags"
msgstr ""
@@ -824,6 +867,10 @@ msgstr "URL'en til det feed, du vil abonnere på. "
msgid "Theme"
msgstr "Tema"
#: src/components/settings/ProfileSettings.tsx
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle read status of current entry"
msgstr "Skift læsestatus for den aktuelle post"
@@ -832,6 +879,10 @@ msgstr "Skift læsestatus for den aktuelle post"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Prøv CommaFeed med demokontoen: demo/demo"

View File

@@ -13,17 +13,13 @@ msgstr ""
"Language-Team: \n"
"Plural-Forms: \n"
#: src/components/content/add/CategorySelect.tsx
msgid "{0} (in {1})"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
msgstr ""
msgstr "CommaFeed ist ein Open Source Projekt. Der Quellcode wird auf auf </0><1>GitHub</1> gehostet."
#: src/pages/app/FeedDetailsPage.tsx
msgid "<0>Complete syntax is available </0><1>here</1>."
msgstr ""
msgstr "Vollständiger Syntax ist </0><1>hier</1> verfügbar."
#: src/pages/auth/RegistrationPage.tsx
msgid "<0>Have an account?</0><1>Log in!</1>"
@@ -35,12 +31,12 @@ msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "<0>Need an account?</0><1>Sign up!</1>"
msgstr "<0>Benötigen Sie ein Konto?</0><1>Melden Sie sich an!</1>"
msgstr "<0>Benötigen Sie ein Konto?</0><1>Hier geht's zur Registrierung!</1>"
#: src/components/header/ProfileMenu.tsx
#: src/pages/app/AboutPage.tsx
msgid "About"
msgstr "Ungefähr"
msgstr "Über"
#: src/pages/admin/AdminUsersPage.tsx
msgid "Actions"
@@ -72,16 +68,16 @@ msgid "All"
msgstr "Alle"
#: src/components/settings/DisplaySettings.tsx
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
msgid "Always"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox."
msgstr "Eine E-Mail wurde gesendet, wenn diese Adresse registriert wurde. "
msgstr "Eine E-Mail wurde gesendet, wenn diese Adresse registriert wurde. Bitte den Posteingang prüfen."
#: src/components/content/add/ImportOpml.tsx
msgid "An opml file is an XML file containing feed URLs and categories. You can get an OPML file by exporting your data from other feed reading services."
msgstr "Eine opml-Datei ist eine XML-Datei, die Feed-URLs und Kategorien enthält. "
msgstr "Eine opml-Datei ist eine XML-Datei, die Feed-URLs und Kategorien enthält."
#: src/components/content/add/Subscribe.tsx
msgid "Analyze feed"
@@ -89,7 +85,7 @@ msgstr "Feed analysieren"
#: src/components/AnnouncementDialog.tsx
msgid "Announcement"
msgstr ""
msgstr "Ankündigung"
#: src/components/settings/ProfileSettings.tsx
msgid "API key"
@@ -105,7 +101,7 @@ msgstr "Sind Sie sicher, dass Sie Benutzer <0>{userName}</0> löschen möchten?"
#: src/components/settings/ProfileSettings.tsx
msgid "Are you sure you want to delete your account? There's no turning back!"
msgstr "Sind Sie sicher, dass Sie Ihr Konto löschen möchten? "
msgstr "Sind Sie sicher, dass Sie Ihr Konto löschen möchten?"
#: src/components/header/MarkAllAsReadButton.tsx
msgid "Are you sure you want to mark all entries of <0>{sourceLabel}</0> as read?"
@@ -121,7 +117,7 @@ msgstr "Sind Sie sicher, dass Sie <0>{feedName}</0> abbestellen möchten?"
#: src/components/header/Header.tsx
msgid "Asc"
msgstr "Asz"
msgstr "Aufsteigend"
#: src/pages/app/FeedDetailsPage.tsx
msgid "Available variables are 'title', 'content', 'url' 'author' and 'categories' and their content is converted to lower case to ease string comparison."
@@ -133,15 +129,15 @@ msgstr "Zurück"
#: src/pages/auth/PasswordRecoveryPage.tsx
msgid "Back to log in"
msgstr "Zurück zum Anmelden"
msgstr "Zurück zum Login"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Browser extension required for Chrome"
msgstr ""
msgstr "Browser-Erweiterung für Chrome benötigt"
#: src/pages/app/AboutPage.tsx
msgid "Browser extention"
msgstr ""
msgstr "Browser-Erweiterung"
#: src/components/admin/UserEdit.tsx
#: src/components/content/add/AddCategory.tsx
@@ -175,10 +171,18 @@ msgstr "Das Ändern des Passworts generiert einen neuen API-Schlüssel"
msgid "Check that the feed is working"
msgstr "Überprüfen Sie, ob der Feed funktioniert"
#: src/pages/app/Layout.tsx
msgid "Close menu"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed nächstes ungelesenes Element"
@@ -207,10 +211,6 @@ msgstr "Passwort bestätigen"
msgid "Cozy"
msgstr "Gemütlich"
#: src/components/content/FeedEntryFooter.tsx
msgid "Create tag: {query}"
msgstr "Tag erstellen: {query}"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Ctrl"
msgstr "Strg"
@@ -231,6 +231,10 @@ msgstr ""
msgid "Custom JS code that will be executed on page load"
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Dark"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx
msgid "Date created"
msgstr "Erstellungsdatum"
@@ -268,7 +272,7 @@ msgstr "Anzeige"
#: src/components/header/ProfileMenu.tsx
#: src/pages/app/DonatePage.tsx
msgid "Donate"
msgstr ""
msgstr "Spenden"
#: src/components/settings/ProfileSettings.tsx
msgid "Download"
@@ -327,7 +331,7 @@ msgstr "Exportieren Sie Ihre Abonnements und Kategorien als OPML-Datei, die in a
#: src/components/header/Header.tsx
#: src/pages/WelcomePage.tsx
msgid "Extension options"
msgstr ""
msgstr "Erweiterungsoptionen"
#: src/components/content/add/Subscribe.tsx
msgid "Feed name"
@@ -341,6 +345,14 @@ msgstr "Feed-URL"
#: src/components/header/ProfileMenu.tsx
msgid "Fetch all my feeds now"
msgstr "Alle Feeds jetzt abrufen"
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
@@ -373,7 +385,7 @@ msgstr "Generierte Feed-URL"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Go to {0}"
msgstr ""
msgstr "Gehe zu {0}"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Go to the All view"
@@ -385,7 +397,7 @@ msgstr "Gehen Sie zur API-Dokumentation."
#: src/pages/app/AboutPage.tsx
msgid "Goodies"
msgstr "Gutes"
msgstr "Goodies"
#: src/pages/admin/AdminUsersPage.tsx
msgid "Id"
@@ -395,6 +407,10 @@ msgstr ""
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
msgstr "Wenn nicht leer, ein Ausdruck, der als „wahr“ oder „falsch“ ausgewertet wird. "
#: src/components/settings/DisplaySettings.tsx
msgid "If the entry doesn't entirely fit on the screen"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "If you encounter an issue, please report it on the issues page of the GitHub project."
msgstr "Wenn Sie auf ein Problem stoßen, melden Sie es bitte auf der Problemseite des GitHub-Projekts."
@@ -415,7 +431,7 @@ msgstr "Ungelesen lassen"
#: src/components/content/FeedEntries.tsx
#: src/pages/app/AboutPage.tsx
msgid "Keyboard shortcuts"
msgstr "Tastenkürzel"
msgstr "Tastaturkürzel"
#: src/components/settings/DisplaySettings.tsx
msgid "Language"
@@ -433,6 +449,10 @@ msgstr "Letzte Aktualisierung"
msgid "Last refresh message"
msgstr "Letzte Aktualisierungsmeldung"
#: src/components/header/ProfileMenu.tsx
msgid "Light"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx
#: src/pages/app/TagDetailsPage.tsx
@@ -467,7 +487,7 @@ msgstr "Abmelden"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Long press"
msgstr ""
msgstr "Langer Tastendruck"
#: src/components/header/ProfileMenu.tsx
#: src/pages/admin/AdminUsersPage.tsx
@@ -499,7 +519,7 @@ msgstr "Metriken"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Middle click"
msgstr ""
msgstr "Mittelklick"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Move the page down"
@@ -512,7 +532,7 @@ msgstr "Bewege die Seite nach oben"
#: src/components/RelativeDate.tsx
#: src/pages/app/FeedDetailsPage.tsx
msgid "N/A"
msgstr "n. z"
msgstr "n.v."
#: src/components/admin/UserEdit.tsx
#: src/pages/admin/AdminUsersPage.tsx
@@ -525,6 +545,10 @@ msgstr ""
msgid "Navigate to a subscription by entering its name"
msgstr "Navigieren Sie zu einem Abonnement, indem Sie seinen Namen eingeben"
#: src/components/settings/DisplaySettings.tsx
msgid "Never"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "New password"
msgstr "Neues Passwort"
@@ -558,6 +582,10 @@ msgstr "Nichts gefunden"
msgid "Oldest first"
msgstr "Älteste zuerst"
#: src/components/settings/DisplaySettings.tsx
msgid "On mobile, show action buttons at the bottom of the screen"
msgstr ""
#: src/pages/ErrorPage.tsx
msgid "Oops!"
msgstr "Ups!"
@@ -580,10 +608,14 @@ msgstr "Link öffnen"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new background tab"
msgstr ""
msgstr "Link in neuem Tab im Hintergrund öffnen"
#: src/components/content/FeedEntryContextMenu.tsx
msgid "Open link in new tab"
msgstr "Link in neuem Tab öffnen"
#: src/pages/app/Layout.tsx
msgid "Open menu"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
@@ -617,11 +649,11 @@ msgstr "Bestellung"
#: src/components/content/add/AddCategory.tsx
msgid "Parent"
msgstr "Elternteil"
msgstr "Übergeordnet"
#: src/pages/app/CategoryDetailsPage.tsx
msgid "Parent Category"
msgstr "Elternkategorie"
msgstr "Übergeordnete Kategorie"
#: src/components/admin/UserEdit.tsx
#: src/pages/auth/LoginPage.tsx
@@ -642,11 +674,11 @@ msgstr "Passwörter stimmen nicht überein"
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx
msgid "Position"
msgstr "Stellung"
msgstr "Position"
#: src/components/header/Header.tsx
msgid "Previous"
msgstr ""
msgstr "Vorheriges"
#: src/pages/app/SettingsPage.tsx
msgid "Profile"
@@ -672,7 +704,7 @@ msgstr "REST-API"
#: src/components/KeyboardShortcutsHelp.tsx
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Right click"
msgstr ""
msgstr "Rechtsklick"
#: src/components/admin/UserEdit.tsx
#: src/components/settings/CustomCodeSettings.tsx
@@ -682,9 +714,17 @@ msgstr ""
msgid "Save"
msgstr "Speichern"
#: src/components/settings/DisplaySettings.tsx
msgid "Scroll selected entry to the top of the page"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Scroll smoothly when navigating between entries"
msgstr "Geschwindes Scrollen beim Navigieren zwischen Einträgen"
msgstr "Schnelles Scrollen beim Navigieren zwischen Einträgen"
#: src/components/settings/DisplaySettings.tsx
msgid "Scrolling"
msgstr ""
#: src/components/header/Header.tsx
#: src/components/header/Header.tsx
@@ -709,7 +749,7 @@ msgstr "Fokus auf vorherigen Eintrag setzen, ohne ihn zu öffnen"
msgid "Settings"
msgstr "Einstellungen"
#: src/app/slices/user.ts
#: src/app/user/slice.ts
msgid "Settings saved."
msgstr "Einstellungen gespeichert."
@@ -729,11 +769,11 @@ msgstr "Verschiebung"
#: src/components/settings/DisplaySettings.tsx
msgid "Show CommaFeed's own context menu on right click"
msgstr ""
msgstr "CommaFeed-Kontextmenü anzeigen bei Rechtsklick"
#: src/components/settings/DisplaySettings.tsx
msgid "Show confirmation when marking all entries as read"
msgstr ""
msgstr "Bestätigung beim Markieren von allen Einträgen als gelesen"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Show entry menu (desktop)"
@@ -799,19 +839,22 @@ msgid "Success"
msgstr "Erfolg"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Swipe header to the right"
msgid "Swipe header to the left"
msgstr ""
#: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to dark theme"
msgstr "Zum dunklen Design wechseln"
msgstr "Zum Darkmode wechseln"
#: src/components/header/ProfileMenu.tsx
#: src/pages/WelcomePage.tsx
msgid "Switch to light theme"
msgstr "Wechseln Sie zum Lichtdesign"
msgstr "Zum Lightmode wechseln"
#: src/components/header/ProfileMenu.tsx
msgid "System"
msgstr ""
#: src/components/content/FeedEntryFooter.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Tags"
msgstr ""
@@ -824,13 +867,21 @@ msgstr "Die URL für den Feed, den Sie abonnieren möchten. "
msgid "Theme"
msgstr "Thema"
#: src/components/settings/ProfileSettings.tsx
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle read status of current entry"
msgstr "Lesestatus des aktuellen Eintrags umschalten"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle sidebar"
msgstr ""
msgstr "Sidebar an- und ausschalten"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr "Markierungsstatus des aktuellen Eintrags ändern"
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
@@ -838,7 +889,7 @@ msgstr "Testen Sie CommaFeed mit dem Demokonto: demo/demo"
#: src/pages/WelcomePage.tsx
msgid "Try the demo!"
msgstr ""
msgstr "Testen Sie die Demo!"
#: src/components/header/Header.tsx
msgid "Unread"
@@ -877,4 +928,4 @@ msgstr "Sie haben noch keine Abonnements. "
#: src/components/header/ProfileMenu.tsx
msgid "Your feeds have been queued for refresh."
msgstr ""
msgstr "Ihr Feed wurde für die Aktualisierung eingereiht."

View File

@@ -13,10 +13,6 @@ msgstr ""
"Language-Team: \n"
"Plural-Forms: \n"
#: src/components/content/add/CategorySelect.tsx
msgid "{0} (in {1})"
msgstr "{0} (in {1})"
#: src/pages/app/AboutPage.tsx
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
msgstr "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
@@ -72,8 +68,8 @@ msgid "All"
msgstr "All"
#: src/components/settings/DisplaySettings.tsx
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
msgstr "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
msgid "Always"
msgstr "Always"
#: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox."
@@ -175,10 +171,18 @@ msgstr "Changing password will generate a new API key"
msgid "Check that the feed is working"
msgstr "Check that the feed is working"
#: src/pages/app/Layout.tsx
msgid "Close menu"
msgstr "Close menu"
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "CommaFeed browser extension version {browserExtensionVersion}."
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed next unread item"
@@ -207,10 +211,6 @@ msgstr "Confirm password"
msgid "Cozy"
msgstr "Cozy"
#: src/components/content/FeedEntryFooter.tsx
msgid "Create tag: {query}"
msgstr "Create tag: {query}"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Ctrl"
msgstr "Ctrl"
@@ -231,6 +231,10 @@ msgstr "Custom CSS rules that will be applied"
msgid "Custom JS code that will be executed on page load"
msgstr "Custom JS code that will be executed on page load"
#: src/components/header/ProfileMenu.tsx
msgid "Dark"
msgstr "Dark"
#: src/pages/admin/AdminUsersPage.tsx
msgid "Date created"
msgstr "Date created"
@@ -343,6 +347,14 @@ msgstr "Feed URL"
msgid "Fetch all my feeds now"
msgstr "Fetch all my feeds now"
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr "Fever API"
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr "Fever API URL"
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "file is required"
@@ -395,6 +407,10 @@ msgstr "Id"
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
msgstr "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
#: src/components/settings/DisplaySettings.tsx
msgid "If the entry doesn't entirely fit on the screen"
msgstr "If the entry doesn't entirely fit on the screen"
#: src/pages/app/AboutPage.tsx
msgid "If you encounter an issue, please report it on the issues page of the GitHub project."
msgstr "If you encounter an issue, please report it on the issues page of the GitHub project."
@@ -433,6 +449,10 @@ msgstr "Last refresh"
msgid "Last refresh message"
msgstr "Last refresh message"
#: src/components/header/ProfileMenu.tsx
msgid "Light"
msgstr "Light"
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx
#: src/pages/app/TagDetailsPage.tsx
@@ -525,6 +545,10 @@ msgstr "Name"
msgid "Navigate to a subscription by entering its name"
msgstr "Navigate to a subscription by entering its name"
#: src/components/settings/DisplaySettings.tsx
msgid "Never"
msgstr "Never"
#: src/components/settings/ProfileSettings.tsx
msgid "New password"
msgstr "New password"
@@ -558,6 +582,10 @@ msgstr "Nothing found"
msgid "Oldest first"
msgstr "Oldest first"
#: src/components/settings/DisplaySettings.tsx
msgid "On mobile, show action buttons at the bottom of the screen"
msgstr "On mobile, show action buttons at the bottom of the screen"
#: src/pages/ErrorPage.tsx
msgid "Oops!"
msgstr "Oops!"
@@ -586,6 +614,10 @@ msgstr "Open link in new background tab"
msgid "Open link in new tab"
msgstr "Open link in new tab"
#: src/pages/app/Layout.tsx
msgid "Open menu"
msgstr "Open menu"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry"
msgstr "Open next entry"
@@ -682,10 +714,18 @@ msgstr "Right click"
msgid "Save"
msgstr "Save"
#: src/components/settings/DisplaySettings.tsx
msgid "Scroll selected entry to the top of the page"
msgstr "Scroll selected entry to the top of the page"
#: src/components/settings/DisplaySettings.tsx
msgid "Scroll smoothly when navigating between entries"
msgstr "Scroll smoothly when navigating between entries"
#: src/components/settings/DisplaySettings.tsx
msgid "Scrolling"
msgstr "Scrolling"
#: src/components/header/Header.tsx
#: src/components/header/Header.tsx
#: src/components/sidebar/TreeSearch.tsx
@@ -709,7 +749,7 @@ msgstr "Set focus on previous entry without opening it"
msgid "Settings"
msgstr "Settings"
#: src/app/slices/user.ts
#: src/app/user/slice.ts
msgid "Settings saved."
msgstr "Settings saved."
@@ -799,19 +839,22 @@ msgid "Success"
msgstr "Success"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Swipe header to the right"
msgstr "Swipe header to the right"
msgid "Swipe header to the left"
msgstr "Swipe header to the left"
#: src/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"
#: src/components/header/ProfileMenu.tsx
msgid "System"
msgstr "System"
#: src/components/content/FeedEntryFooter.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Tags"
msgstr "Tags"
@@ -824,6 +867,10 @@ msgstr "The URL for the feed you want to subscribe to. You can also use the webs
msgid "Theme"
msgstr "Theme"
#: src/components/settings/ProfileSettings.tsx
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
msgstr "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle read status of current entry"
msgstr "Toggle read status of current entry"
@@ -832,6 +879,10 @@ msgstr "Toggle read status of current entry"
msgid "Toggle sidebar"
msgstr "Toggle sidebar"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr "Toggle starred status of current entry"
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Try out CommaFeed with the demo account: demo/demo"

View File

@@ -13,10 +13,6 @@ msgstr ""
"Language-Team: \n"
"Plural-Forms: \n"
#: src/components/content/add/CategorySelect.tsx
msgid "{0} (in {1})"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
msgstr ""
@@ -72,7 +68,7 @@ msgid "All"
msgstr "Todo"
#: src/components/settings/DisplaySettings.tsx
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
msgid "Always"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx
@@ -175,10 +171,18 @@ msgstr "Cambiar la contraseña generará una nueva clave API"
msgid "Check that the feed is working"
msgstr "Compruebe que el feed funciona"
#: src/pages/app/Layout.tsx
msgid "Close menu"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed siguiente elemento no leído"
@@ -207,10 +211,6 @@ msgstr "Confirmar contraseña"
msgid "Cozy"
msgstr "Acogedor"
#: src/components/content/FeedEntryFooter.tsx
msgid "Create tag: {query}"
msgstr "Crear etiqueta: {consulta}"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Ctrl"
msgstr ""
@@ -231,6 +231,10 @@ msgstr ""
msgid "Custom JS code that will be executed on page load"
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Dark"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx
msgid "Date created"
msgstr "Fecha de creación"
@@ -343,6 +347,14 @@ msgstr "URL de fuente"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "archivo requerido"
@@ -395,6 +407,10 @@ msgstr "Identificación"
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
msgstr "Si no está vacío, una expresión que se evalúa como 'verdadero' o 'falso'. "
#: src/components/settings/DisplaySettings.tsx
msgid "If the entry doesn't entirely fit on the screen"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "If you encounter an issue, please report it on the issues page of the GitHub project."
msgstr "Si encuentra un problema, infórmelo en la página de problemas del proyecto GitHub."
@@ -433,6 +449,10 @@ msgstr "Última actualización"
msgid "Last refresh message"
msgstr "Último mensaje de actualización"
#: src/components/header/ProfileMenu.tsx
msgid "Light"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx
#: src/pages/app/TagDetailsPage.tsx
@@ -525,6 +545,10 @@ msgstr "Nombre"
msgid "Navigate to a subscription by entering its name"
msgstr "Navegar a una suscripción ingresando su nombre"
#: src/components/settings/DisplaySettings.tsx
msgid "Never"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "New password"
msgstr "Nueva contraseña"
@@ -558,6 +582,10 @@ msgstr "Nada encontrado"
msgid "Oldest first"
msgstr "más antigua primero"
#: src/components/settings/DisplaySettings.tsx
msgid "On mobile, show action buttons at the bottom of the screen"
msgstr ""
#: src/pages/ErrorPage.tsx
msgid "Oops!"
msgstr "¡Ups!"
@@ -586,6 +614,10 @@ msgstr ""
msgid "Open link in new tab"
msgstr ""
#: src/pages/app/Layout.tsx
msgid "Open menu"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry"
msgstr "Abrir siguiente entrada"
@@ -682,10 +714,18 @@ msgstr ""
msgid "Save"
msgstr "Guardar"
#: src/components/settings/DisplaySettings.tsx
msgid "Scroll selected entry to the top of the page"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Scroll smoothly when navigating between entries"
msgstr "Desplazarse suavemente al navegar entre entradas"
#: src/components/settings/DisplaySettings.tsx
msgid "Scrolling"
msgstr ""
#: src/components/header/Header.tsx
#: src/components/header/Header.tsx
#: src/components/sidebar/TreeSearch.tsx
@@ -709,7 +749,7 @@ msgstr "Poner el foco en la entrada anterior sin abrirla"
msgid "Settings"
msgstr "Configuraciones"
#: src/app/slices/user.ts
#: src/app/user/slice.ts
msgid "Settings saved."
msgstr "Ajustes guardados."
@@ -799,19 +839,22 @@ msgid "Success"
msgstr "Éxito"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Swipe header to the right"
msgid "Swipe header to the left"
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"
#: src/components/header/ProfileMenu.tsx
msgid "System"
msgstr ""
#: src/components/content/FeedEntryFooter.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Tags"
msgstr "Etiquetas"
@@ -824,6 +867,10 @@ msgstr "La URL de la fuente a la que desea suscribirse. "
msgid "Theme"
msgstr "Tema"
#: src/components/settings/ProfileSettings.tsx
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle read status of current entry"
msgstr "Alternar estado de lectura de la entrada actual"
@@ -832,6 +879,10 @@ msgstr "Alternar estado de lectura de la entrada actual"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Pruebe CommaFeed con la cuenta demo: demo/demo"

View File

@@ -13,10 +13,6 @@ msgstr ""
"Language-Team: \n"
"Plural-Forms: \n"
#: src/components/content/add/CategorySelect.tsx
msgid "{0} (in {1})"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
msgstr ""
@@ -72,7 +68,7 @@ msgid "All"
msgstr "همه"
#: src/components/settings/DisplaySettings.tsx
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
msgid "Always"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx
@@ -175,10 +171,18 @@ msgstr "تغییر رمز عبور یک کلید API جدید ایجاد می ک
msgid "Check that the feed is working"
msgstr "بررسی کنید که خوراک کار می کند"
#: src/pages/app/Layout.tsx
msgid "Close menu"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "مورد خوانده نشده بعدی CommaFeed"
@@ -207,10 +211,6 @@ msgstr "رمز عبور را تأیید کنید"
msgid "Cozy"
msgstr "دنج"
#: src/components/content/FeedEntryFooter.tsx
msgid "Create tag: {query}"
msgstr "ایجاد برچسب: {query}"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Ctrl"
msgstr ""
@@ -231,6 +231,10 @@ msgstr ""
msgid "Custom JS code that will be executed on page load"
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Dark"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx
msgid "Date created"
msgstr "تاریخ ایجاد"
@@ -343,6 +347,14 @@ msgstr "URL فید"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "فایل مورد نیاز است"
@@ -395,6 +407,10 @@ msgstr "شناسه"
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
msgstr "اگر خالی نباشد، عبارتی به \"درست\" یا \"نادرست\" ارزیابی می شود. "
#: src/components/settings/DisplaySettings.tsx
msgid "If the entry doesn't entirely fit on the screen"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "If you encounter an issue, please report it on the issues page of the GitHub project."
msgstr "اگر با مشکلی مواجه شدید، لطفاً آن را در صفحه مشکلات پروژه GitHub گزارش دهید."
@@ -433,6 +449,10 @@ msgstr "آخرین به روز رسانی"
msgid "Last refresh message"
msgstr "آخرین پیام تازه کردن"
#: src/components/header/ProfileMenu.tsx
msgid "Light"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx
#: src/pages/app/TagDetailsPage.tsx
@@ -525,6 +545,10 @@ msgstr "نام"
msgid "Navigate to a subscription by entering its name"
msgstr "با وارد کردن نام اشتراک، به آن بروید"
#: src/components/settings/DisplaySettings.tsx
msgid "Never"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "New password"
msgstr "رمز عبور جدید"
@@ -558,6 +582,10 @@ msgstr "چیزی پیدا نشد"
msgid "Oldest first"
msgstr "قدیمی ترین اول"
#: src/components/settings/DisplaySettings.tsx
msgid "On mobile, show action buttons at the bottom of the screen"
msgstr ""
#: src/pages/ErrorPage.tsx
msgid "Oops!"
msgstr "اوه!"
@@ -586,6 +614,10 @@ msgstr ""
msgid "Open link in new tab"
msgstr ""
#: src/pages/app/Layout.tsx
msgid "Open menu"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry"
msgstr "ورودی بعدی را باز کنید"
@@ -682,10 +714,18 @@ msgstr ""
msgid "Save"
msgstr "ذخیره کنید"
#: src/components/settings/DisplaySettings.tsx
msgid "Scroll selected entry to the top of the page"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Scroll smoothly when navigating between entries"
msgstr "هنگام پیمایش بین ورودی‌ها به آرامی حرکت کنید"
#: src/components/settings/DisplaySettings.tsx
msgid "Scrolling"
msgstr ""
#: src/components/header/Header.tsx
#: src/components/header/Header.tsx
#: src/components/sidebar/TreeSearch.tsx
@@ -709,7 +749,7 @@ msgstr "فوکوس را روی ورودی قبلی بدون باز کردن آن
msgid "Settings"
msgstr "تنظیمات"
#: src/app/slices/user.ts
#: src/app/user/slice.ts
msgid "Settings saved."
msgstr "تنظیمات ذخیره شد."
@@ -799,19 +839,22 @@ msgid "Success"
msgstr "موفقیت"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Swipe header to the right"
msgid "Swipe header to the left"
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 "روی زمینه روشن"
#: src/components/header/ProfileMenu.tsx
msgid "System"
msgstr ""
#: src/components/content/FeedEntryFooter.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Tags"
msgstr "برچسب ها"
@@ -824,6 +867,10 @@ msgstr "URL فیدی که می خواهید در آن مشترک شوید. "
msgid "Theme"
msgstr "تم"
#: src/components/settings/ProfileSettings.tsx
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle read status of current entry"
msgstr "وضعیت خواندن ورودی فعلی را تغییر دهید"
@@ -832,6 +879,10 @@ msgstr "وضعیت خواندن ورودی فعلی را تغییر دهید"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "CommaFeed را با حساب آزمایشی امتحان کنید: دمو/دمو"

View File

@@ -13,10 +13,6 @@ msgstr ""
"Language-Team: \n"
"Plural-Forms: \n"
#: src/components/content/add/CategorySelect.tsx
msgid "{0} (in {1})"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
msgstr ""
@@ -72,7 +68,7 @@ msgid "All"
msgstr "Kaikki"
#: src/components/settings/DisplaySettings.tsx
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
msgid "Always"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx
@@ -175,10 +171,18 @@ msgstr "Salasanan vaihtaminen luo uuden API-avaimen"
msgid "Check that the feed is working"
msgstr "Tarkista, että syöttö toimii"
#: src/pages/app/Layout.tsx
msgid "Close menu"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed seuraava lukematon kohde"
@@ -207,10 +211,6 @@ msgstr "Vahvista salasana"
msgid "Cozy"
msgstr "Viihtyisä"
#: src/components/content/FeedEntryFooter.tsx
msgid "Create tag: {query}"
msgstr "Luo tunniste: {query}"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Ctrl"
msgstr ""
@@ -231,6 +231,10 @@ msgstr ""
msgid "Custom JS code that will be executed on page load"
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Dark"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx
msgid "Date created"
msgstr "Luontipäivämäärä"
@@ -343,6 +347,14 @@ msgstr "Syötteen URL-osoite"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "tiedosto vaaditaan"
@@ -395,6 +407,10 @@ msgstr ""
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
msgstr "Jos ei tyhjä, lauseke, jonka arvo on \"tosi\" tai \"epätosi\". "
#: src/components/settings/DisplaySettings.tsx
msgid "If the entry doesn't entirely fit on the screen"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "If you encounter an issue, please report it on the issues page of the GitHub project."
msgstr "Jos kohtaat ongelman, ilmoita siitä GitHub-projektin ongelmasivulla."
@@ -433,6 +449,10 @@ msgstr "Viimeinen päivitys"
msgid "Last refresh message"
msgstr "Viimeinen päivitysviesti"
#: src/components/header/ProfileMenu.tsx
msgid "Light"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx
#: src/pages/app/TagDetailsPage.tsx
@@ -525,6 +545,10 @@ msgstr "Nimi"
msgid "Navigate to a subscription by entering its name"
msgstr "Siirry tilaukseen kirjoittamalla sen nimi"
#: src/components/settings/DisplaySettings.tsx
msgid "Never"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "New password"
msgstr "Uusi salasana"
@@ -558,6 +582,10 @@ msgstr "Mitään ei löytynyt"
msgid "Oldest first"
msgstr "Vanhin ensin"
#: src/components/settings/DisplaySettings.tsx
msgid "On mobile, show action buttons at the bottom of the screen"
msgstr ""
#: src/pages/ErrorPage.tsx
msgid "Oops!"
msgstr "Hups!"
@@ -586,6 +614,10 @@ msgstr ""
msgid "Open link in new tab"
msgstr ""
#: src/pages/app/Layout.tsx
msgid "Open menu"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry"
msgstr "Avaa seuraava merkintä"
@@ -682,10 +714,18 @@ msgstr ""
msgid "Save"
msgstr "Tallenna"
#: src/components/settings/DisplaySettings.tsx
msgid "Scroll selected entry to the top of the page"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Scroll smoothly when navigating between entries"
msgstr "Selaa sujuvasti navigoidessasi merkintöjen välillä"
#: src/components/settings/DisplaySettings.tsx
msgid "Scrolling"
msgstr ""
#: src/components/header/Header.tsx
#: src/components/header/Header.tsx
#: src/components/sidebar/TreeSearch.tsx
@@ -709,7 +749,7 @@ msgstr "Aseta kohdistus edelliseen merkintään avaamatta sitä"
msgid "Settings"
msgstr "Asetukset"
#: src/app/slices/user.ts
#: src/app/user/slice.ts
msgid "Settings saved."
msgstr "Asetukset tallennettu."
@@ -799,19 +839,22 @@ msgid "Success"
msgstr "Onnistui"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Swipe header to the right"
msgid "Swipe header to the left"
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"
#: src/components/header/ProfileMenu.tsx
msgid "System"
msgstr ""
#: src/components/content/FeedEntryFooter.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Tags"
msgstr ""
@@ -824,6 +867,10 @@ msgstr "Sen syötteen URL-osoite, jonka haluat tilata. "
msgid "Theme"
msgstr "Teema"
#: src/components/settings/ProfileSettings.tsx
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle read status of current entry"
msgstr "Vaihda nykyisen merkinnän lukutila"
@@ -832,6 +879,10 @@ msgstr "Vaihda nykyisen merkinnän lukutila"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Kokeile CommaFeediä demotilillä: demo/demo"

View File

@@ -13,10 +13,6 @@ msgstr ""
"Language-Team: \n"
"Plural-Forms: \n"
#: src/components/content/add/CategorySelect.tsx
msgid "{0} (in {1})"
msgstr "{0} (sur {1})"
#: src/pages/app/AboutPage.tsx
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
msgstr "<0>CommaFeed est un projet open-source. Les sources sont hébergées sur </0><1>GitHub</1>."
@@ -72,8 +68,8 @@ msgid "All"
msgstr "Tout"
#: src/components/settings/DisplaySettings.tsx
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
msgstr "Toujours remonter l'entrée sélectionnée en haut de la page, même si elle s'affiche complètement à l'écran"
msgid "Always"
msgstr "Toujours"
#: src/pages/auth/PasswordRecoveryPage.tsx
msgid "An email has been sent if this address was registered. Check your inbox."
@@ -175,10 +171,18 @@ msgstr "Changer de mot de passe générera une nouvelle clé API"
msgid "Check that the feed is working"
msgstr "Vérifie que le flux fonctionne"
#: src/pages/app/Layout.tsx
msgid "Close menu"
msgstr "Fermer le menu"
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr "Extension CommaFeed pour navigateur version {browserExtensionVersion}."
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr "Commafeed est compatible avec l'API Fever, en inscrivant l'URL suivante dans votre client mobile compatible. Entrez votre nom d'utilisateur habituel, et votre <0>clef API</0> comme mot de passe."
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed prochain article non lu"
@@ -207,10 +211,6 @@ msgstr "Confirmer le mot de passe"
msgid "Cozy"
msgstr "Cozy"
#: src/components/content/FeedEntryFooter.tsx
msgid "Create tag: {query}"
msgstr "Créer le marqueur : {query}"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Ctrl"
msgstr "Ctrl"
@@ -231,6 +231,10 @@ msgstr "Code CSS personnalisé qui sera appliqué"
msgid "Custom JS code that will be executed on page load"
msgstr "Code JS personnalisé qui sera appliqué au chargement des pages"
#: src/components/header/ProfileMenu.tsx
msgid "Dark"
msgstr "Foncé"
#: src/pages/admin/AdminUsersPage.tsx
msgid "Date created"
msgstr "Date de création"
@@ -343,6 +347,14 @@ msgstr "URL du flux"
msgid "Fetch all my feeds now"
msgstr "Rafraîchir tous mes flux"
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr "API Fever"
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr "URL API Fever"
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "fichier requis"
@@ -395,6 +407,10 @@ msgstr "Identifiant"
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
msgstr "Si non vide, une expression évaluant à 'vrai' ou 'faux'. Si faux, les nouvelles entrées de ce flux seront marquées comme lues automatiquement."
#: src/components/settings/DisplaySettings.tsx
msgid "If the entry doesn't entirely fit on the screen"
msgstr "Si l'entrée ne tient pas entièrement sur l'écran"
#: src/pages/app/AboutPage.tsx
msgid "If you encounter an issue, please report it on the issues page of the GitHub project."
msgstr "Si vous rencontrez un problème, merci de le signaler sur la page du projet GitHub."
@@ -433,6 +449,10 @@ msgstr "Dernière mise à jour"
msgid "Last refresh message"
msgstr "Dernier message de mise à jour"
#: src/components/header/ProfileMenu.tsx
msgid "Light"
msgstr "Clair"
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx
#: src/pages/app/TagDetailsPage.tsx
@@ -525,6 +545,10 @@ msgstr "Nom"
msgid "Navigate to a subscription by entering its name"
msgstr "Naviguer vers un abonnement en entrant son nom"
#: src/components/settings/DisplaySettings.tsx
msgid "Never"
msgstr "Jamais"
#: src/components/settings/ProfileSettings.tsx
msgid "New password"
msgstr "Nouveau mot de passe"
@@ -558,6 +582,10 @@ msgstr "Aucun résultat"
msgid "Oldest first"
msgstr "Du plus ancien au plus récent"
#: src/components/settings/DisplaySettings.tsx
msgid "On mobile, show action buttons at the bottom of the screen"
msgstr "Sur mobile, afficher les boutons d'action en bas de l'écran"
#: src/pages/ErrorPage.tsx
msgid "Oops!"
msgstr "Oups !"
@@ -586,6 +614,10 @@ msgstr "Ouvrir le lien dans un nouvel onglet en arrière-plan"
msgid "Open link in new tab"
msgstr "Ouvrir le lien dans un nouvel onglet"
#: src/pages/app/Layout.tsx
msgid "Open menu"
msgstr "Ouvrir le menu"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry"
msgstr "Ouvrir l'entrée suivante"
@@ -682,10 +714,18 @@ msgstr "Clic droit"
msgid "Save"
msgstr "Enregistrer"
#: src/components/settings/DisplaySettings.tsx
msgid "Scroll selected entry to the top of the page"
msgstr "Faire défiler l'entrée sélectionnée vers le haut de la page"
#: src/components/settings/DisplaySettings.tsx
msgid "Scroll smoothly when navigating between entries"
msgstr "Défilement animé lors de la navigation entre les entrées"
#: src/components/settings/DisplaySettings.tsx
msgid "Scrolling"
msgstr "Défilement"
#: src/components/header/Header.tsx
#: src/components/header/Header.tsx
#: src/components/sidebar/TreeSearch.tsx
@@ -709,7 +749,7 @@ msgstr "Sélectionner l'article précédent sans l'ouvrir"
msgid "Settings"
msgstr "Réglages"
#: src/app/slices/user.ts
#: src/app/user/slice.ts
msgid "Settings saved."
msgstr "Réglages enregistrés."
@@ -729,7 +769,7 @@ msgstr "Maj"
#: src/components/settings/DisplaySettings.tsx
msgid "Show CommaFeed's own context menu on right click"
msgstr ""
msgstr "Afficher le menu contextuel de Commafeed lors d'un clic droit"
#: src/components/settings/DisplaySettings.tsx
msgid "Show confirmation when marking all entries as read"
@@ -799,19 +839,22 @@ msgid "Success"
msgstr "Succès"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Swipe header to the right"
msgstr "Faire glisser le titre vers la droite"
msgid "Swipe header to the left"
msgstr "Faire glisser le titre vers la gauche"
#: src/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/header/ProfileMenu.tsx
msgid "System"
msgstr "Système"
#: src/components/content/FeedEntryFooter.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Tags"
msgstr "Marqueurs"
@@ -824,6 +867,10 @@ msgstr "L'URL du flux auquel vous souhaitez vous abonner. Vous pouvez aussi util
msgid "Theme"
msgstr "Thème"
#: src/components/settings/ProfileSettings.tsx
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
msgstr "Ceci est votre clef API. Elle peut être utilisée pour certaines opérations en lecture seule et donne accès à l'API Fever. Utilisez le formulaire en bas de la page pour générer une nouvelle clef API"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle read status of current entry"
msgstr "Marquer l'entrée actuelle comme lue/non lue"
@@ -832,6 +879,10 @@ msgstr "Marquer l'entrée actuelle comme lue/non lue"
msgid "Toggle sidebar"
msgstr "Montrer/cacher la barre latérale"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr "Montrer/cacher le statut favori de l'entrée"
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Essayez CommaFeed avec le compte de démonstration : demo/demo"

View File

@@ -13,10 +13,6 @@ msgstr ""
"Language-Team: \n"
"Plural-Forms: \n"
#: src/components/content/add/CategorySelect.tsx
msgid "{0} (in {1})"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
msgstr ""
@@ -72,7 +68,7 @@ msgid "All"
msgstr "Todos"
#: src/components/settings/DisplaySettings.tsx
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
msgid "Always"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx
@@ -175,10 +171,18 @@ msgstr "O cambio de contrasinal xerará unha nova clave de API"
msgid "Check that the feed is working"
msgstr "Comproba que a fonte funciona"
#: src/pages/app/Layout.tsx
msgid "Close menu"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed seguinte elemento non lido"
@@ -207,10 +211,6 @@ msgstr "Confirmar contrasinal"
msgid "Cozy"
msgstr "acolledor"
#: src/components/content/FeedEntryFooter.tsx
msgid "Create tag: {query}"
msgstr "Crear etiqueta: {consulta}"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Ctrl"
msgstr ""
@@ -231,6 +231,10 @@ msgstr ""
msgid "Custom JS code that will be executed on page load"
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Dark"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx
msgid "Date created"
msgstr "Data de creación"
@@ -343,6 +347,14 @@ msgstr "URL da fonte"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "é necesario o ficheiro"
@@ -395,6 +407,10 @@ msgstr ""
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
msgstr "Se non está baleira, unha expresión que se avalía como \"verdadeiro\" ou \"falso\". "
#: src/components/settings/DisplaySettings.tsx
msgid "If the entry doesn't entirely fit on the screen"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "If you encounter an issue, please report it on the issues page of the GitHub project."
msgstr "Se atopas algún problema, infórmao na páxina de problemas do proxecto GitHub."
@@ -433,6 +449,10 @@ msgstr "Última actualización"
msgid "Last refresh message"
msgstr "Última mensaxe de actualización"
#: src/components/header/ProfileMenu.tsx
msgid "Light"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx
#: src/pages/app/TagDetailsPage.tsx
@@ -525,6 +545,10 @@ msgstr "Nome"
msgid "Navigate to a subscription by entering its name"
msgstr "Navega a unha subscrición introducindo o seu nome"
#: src/components/settings/DisplaySettings.tsx
msgid "Never"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "New password"
msgstr "novo contrasinal"
@@ -558,6 +582,10 @@ msgstr "Non se atopou nada"
msgid "Oldest first"
msgstr "O máis vello primeiro"
#: src/components/settings/DisplaySettings.tsx
msgid "On mobile, show action buttons at the bottom of the screen"
msgstr ""
#: src/pages/ErrorPage.tsx
msgid "Oops!"
msgstr "Vaia!"
@@ -586,6 +614,10 @@ msgstr ""
msgid "Open link in new tab"
msgstr ""
#: src/pages/app/Layout.tsx
msgid "Open menu"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry"
msgstr "Abrir a seguinte entrada"
@@ -682,10 +714,18 @@ msgstr ""
msgid "Save"
msgstr "Gardar"
#: src/components/settings/DisplaySettings.tsx
msgid "Scroll selected entry to the top of the page"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Scroll smoothly when navigating between entries"
msgstr "Desprácese suavemente ao navegar entre as entradas"
#: src/components/settings/DisplaySettings.tsx
msgid "Scrolling"
msgstr ""
#: src/components/header/Header.tsx
#: src/components/header/Header.tsx
#: src/components/sidebar/TreeSearch.tsx
@@ -709,7 +749,7 @@ msgstr "Establecer o foco na entrada anterior sen abrila"
msgid "Settings"
msgstr "Configuración"
#: src/app/slices/user.ts
#: src/app/user/slice.ts
msgid "Settings saved."
msgstr "axustes gardados."
@@ -799,19 +839,22 @@ msgid "Success"
msgstr "Éxito"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Swipe header to the right"
msgid "Swipe header to the left"
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"
#: src/components/header/ProfileMenu.tsx
msgid "System"
msgstr ""
#: src/components/content/FeedEntryFooter.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Tags"
msgstr "Etiquetas"
@@ -824,6 +867,10 @@ msgstr "O URL do feed ao que quere subscribirse. "
msgid "Theme"
msgstr "Tema"
#: src/components/settings/ProfileSettings.tsx
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle read status of current entry"
msgstr "alternar o estado de lectura da entrada actual"
@@ -832,6 +879,10 @@ msgstr "alternar o estado de lectura da entrada actual"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Proba CommaFeed coa conta de demostración: demo/demo"

View File

@@ -13,10 +13,6 @@ msgstr ""
"Language-Team: \n"
"Plural-Forms: \n"
#: src/components/content/add/CategorySelect.tsx
msgid "{0} (in {1})"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
msgstr ""
@@ -72,7 +68,7 @@ msgid "All"
msgstr "Mind"
#: src/components/settings/DisplaySettings.tsx
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
msgid "Always"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx
@@ -175,10 +171,18 @@ msgstr "A jelszó megváltoztatása új API-kulcsot generál"
msgid "Check that the feed is working"
msgstr "Ellenőrizze, hogy a feed működik-e"
#: src/pages/app/Layout.tsx
msgid "Close menu"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed következő olvasatlan elem"
@@ -207,10 +211,6 @@ msgstr "Erősítse meg a jelszót"
msgid "Cozy"
msgstr "Hangulatos"
#: src/components/content/FeedEntryFooter.tsx
msgid "Create tag: {query}"
msgstr "Címke létrehozása: {query}"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Ctrl"
msgstr ""
@@ -231,6 +231,10 @@ msgstr ""
msgid "Custom JS code that will be executed on page load"
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Dark"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx
msgid "Date created"
msgstr "Létrehozás dátuma"
@@ -343,6 +347,14 @@ msgstr ""
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "fájl szükséges"
@@ -395,6 +407,10 @@ msgstr ""
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
msgstr "Ha nem üres, akkor 'igaz' vagy 'hamis' értékre kiértékelő kifejezés. "
#: src/components/settings/DisplaySettings.tsx
msgid "If the entry doesn't entirely fit on the screen"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "If you encounter an issue, please report it on the issues page of the GitHub project."
msgstr "Ha problémát tapasztal, kérjük, jelentse azt a GitHub projekt problémák oldalán."
@@ -433,6 +449,10 @@ msgstr "Utolsó frissítés"
msgid "Last refresh message"
msgstr "Utolsó frissítési üzenet"
#: src/components/header/ProfileMenu.tsx
msgid "Light"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx
#: src/pages/app/TagDetailsPage.tsx
@@ -525,6 +545,10 @@ msgstr "Név"
msgid "Navigate to a subscription by entering its name"
msgstr "Navigáljon egy előfizetéshez a nevének megadásával"
#: src/components/settings/DisplaySettings.tsx
msgid "Never"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "New password"
msgstr "Új jelszó"
@@ -558,6 +582,10 @@ msgstr "Semmi sem található"
msgid "Oldest first"
msgstr "A legidősebb első"
#: src/components/settings/DisplaySettings.tsx
msgid "On mobile, show action buttons at the bottom of the screen"
msgstr ""
#: src/pages/ErrorPage.tsx
msgid "Oops!"
msgstr "Hoppá!"
@@ -586,6 +614,10 @@ msgstr ""
msgid "Open link in new tab"
msgstr ""
#: src/pages/app/Layout.tsx
msgid "Open menu"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry"
msgstr "Következő bejegyzés megnyitása"
@@ -682,10 +714,18 @@ msgstr ""
msgid "Save"
msgstr "Mentés"
#: src/components/settings/DisplaySettings.tsx
msgid "Scroll selected entry to the top of the page"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Scroll smoothly when navigating between entries"
msgstr "Sima görgetés, amikor a bejegyzések között navigál"
#: src/components/settings/DisplaySettings.tsx
msgid "Scrolling"
msgstr ""
#: src/components/header/Header.tsx
#: src/components/header/Header.tsx
#: src/components/sidebar/TreeSearch.tsx
@@ -709,7 +749,7 @@ msgstr "Állítsa a fókuszt az előző bejegyzésre anélkül, hogy megnyitná
msgid "Settings"
msgstr "Beállítások"
#: src/app/slices/user.ts
#: src/app/user/slice.ts
msgid "Settings saved."
msgstr "Beállítások mentve."
@@ -799,19 +839,22 @@ msgid "Success"
msgstr "Siker"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Swipe header to the right"
msgid "Swipe header to the left"
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"
#: src/components/header/ProfileMenu.tsx
msgid "System"
msgstr ""
#: src/components/content/FeedEntryFooter.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Tags"
msgstr "Címkék"
@@ -824,6 +867,10 @@ msgstr "Az előfizetni kívánt hírcsatorna URL-je. "
msgid "Theme"
msgstr "Téma"
#: src/components/settings/ProfileSettings.tsx
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle read status of current entry"
msgstr "Az aktuális bejegyzés olvasási állapotának váltása"
@@ -832,6 +879,10 @@ msgstr "Az aktuális bejegyzés olvasási állapotának váltása"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Próbálja ki a CommaFeed-et a demo fiókkal: demo/demo"

View File

@@ -13,10 +13,6 @@ msgstr ""
"Language-Team: \n"
"Plural-Forms: \n"
#: src/components/content/add/CategorySelect.tsx
msgid "{0} (in {1})"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
msgstr ""
@@ -72,7 +68,7 @@ msgid "All"
msgstr "Semua"
#: src/components/settings/DisplaySettings.tsx
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
msgid "Always"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx
@@ -175,10 +171,18 @@ msgstr "Mengubah kata sandi akan menghasilkan kunci API baru"
msgid "Check that the feed is working"
msgstr "Periksa apakah umpannya berfungsi"
#: src/pages/app/Layout.tsx
msgid "Close menu"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed item yang belum dibaca berikutnya"
@@ -207,10 +211,6 @@ msgstr "Konfirmasi kata sandi"
msgid "Cozy"
msgstr "Nyaman"
#: src/components/content/FeedEntryFooter.tsx
msgid "Create tag: {query}"
msgstr "Buat tag: {query}"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Ctrl"
msgstr ""
@@ -231,6 +231,10 @@ msgstr ""
msgid "Custom JS code that will be executed on page load"
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Dark"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx
msgid "Date created"
msgstr "Tanggal dibuat"
@@ -343,6 +347,14 @@ msgstr "URL Umpan"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "file diperlukan"
@@ -395,6 +407,10 @@ msgstr ""
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
msgstr "Jika tidak kosong, ekspresi mengevaluasi ke 'benar' atau 'salah'. "
#: src/components/settings/DisplaySettings.tsx
msgid "If the entry doesn't entirely fit on the screen"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "If you encounter an issue, please report it on the issues page of the GitHub project."
msgstr "Jika Anda mengalami masalah, harap laporkan di halaman masalah proyek GitHub."
@@ -433,6 +449,10 @@ msgstr "Penyegaran terakhir"
msgid "Last refresh message"
msgstr "Pesan penyegaran terakhir"
#: src/components/header/ProfileMenu.tsx
msgid "Light"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx
#: src/pages/app/TagDetailsPage.tsx
@@ -525,6 +545,10 @@ msgstr "Nama"
msgid "Navigate to a subscription by entering its name"
msgstr "Navigasikan ke langganan dengan memasukkan namanya"
#: src/components/settings/DisplaySettings.tsx
msgid "Never"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "New password"
msgstr "Kata sandi baru"
@@ -558,6 +582,10 @@ msgstr "Tidak ada yang ditemukan"
msgid "Oldest first"
msgstr "Tertua dulu"
#: src/components/settings/DisplaySettings.tsx
msgid "On mobile, show action buttons at the bottom of the screen"
msgstr ""
#: src/pages/ErrorPage.tsx
msgid "Oops!"
msgstr "Ups!"
@@ -586,6 +614,10 @@ msgstr ""
msgid "Open link in new tab"
msgstr ""
#: src/pages/app/Layout.tsx
msgid "Open menu"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry"
msgstr "Buka entri berikutnya"
@@ -682,10 +714,18 @@ msgstr ""
msgid "Save"
msgstr "Simpan"
#: src/components/settings/DisplaySettings.tsx
msgid "Scroll selected entry to the top of the page"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Scroll smoothly when navigating between entries"
msgstr "Gulir dengan lancar saat menavigasi antar entri"
#: src/components/settings/DisplaySettings.tsx
msgid "Scrolling"
msgstr ""
#: src/components/header/Header.tsx
#: src/components/header/Header.tsx
#: src/components/sidebar/TreeSearch.tsx
@@ -709,7 +749,7 @@ msgstr "Tetapkan fokus pada entri sebelumnya tanpa membukanya"
msgid "Settings"
msgstr "Pengaturan"
#: src/app/slices/user.ts
#: src/app/user/slice.ts
msgid "Settings saved."
msgstr "Pengaturan disimpan."
@@ -799,19 +839,22 @@ msgid "Success"
msgstr "Sukses"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Swipe header to the right"
msgid "Swipe header to the left"
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"
#: src/components/header/ProfileMenu.tsx
msgid "System"
msgstr ""
#: src/components/content/FeedEntryFooter.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Tags"
msgstr "Tag"
@@ -824,6 +867,10 @@ msgstr "URL untuk umpan yang ingin Anda langgani. "
msgid "Theme"
msgstr "Tema"
#: src/components/settings/ProfileSettings.tsx
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle read status of current entry"
msgstr "Beralih status baca entri saat ini"
@@ -832,6 +879,10 @@ msgstr "Beralih status baca entri saat ini"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Cobalah CommaFeed dengan akun demo: demo/demo"

View File

@@ -13,10 +13,6 @@ msgstr ""
"Language-Team: \n"
"Plural-Forms: \n"
#: src/components/content/add/CategorySelect.tsx
msgid "{0} (in {1})"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
msgstr ""
@@ -72,7 +68,7 @@ msgid "All"
msgstr "Tutto"
#: src/components/settings/DisplaySettings.tsx
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
msgid "Always"
msgstr ""
#: src/pages/auth/PasswordRecoveryPage.tsx
@@ -175,10 +171,18 @@ msgstr "La modifica della password genererà una nuova chiave API"
msgid "Check that the feed is working"
msgstr "Verifica che il feed funzioni"
#: src/pages/app/Layout.tsx
msgid "Close menu"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed browser extension version {browserExtensionVersion}."
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "CommaFeed next unread item"
msgstr "CommaFeed successivo elemento non letto"
@@ -207,10 +211,6 @@ msgstr "Conferma password"
msgid "Cozy"
msgstr "Accogliente"
#: src/components/content/FeedEntryFooter.tsx
msgid "Create tag: {query}"
msgstr "Crea tag: {query}"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Ctrl"
msgstr "ctrl"
@@ -231,6 +231,10 @@ msgstr ""
msgid "Custom JS code that will be executed on page load"
msgstr ""
#: src/components/header/ProfileMenu.tsx
msgid "Dark"
msgstr ""
#: src/pages/admin/AdminUsersPage.tsx
msgid "Date created"
msgstr "Data di creazione"
@@ -343,6 +347,14 @@ msgstr "URL feed"
msgid "Fetch all my feeds now"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "Fever API URL"
msgstr ""
#: src/components/content/add/ImportOpml.tsx
msgid "file is required"
msgstr "è richiesto il file"
@@ -395,6 +407,10 @@ msgstr ""
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
msgstr "Se non è vuota, un'espressione valutata come 'vero' o 'falso'. "
#: src/components/settings/DisplaySettings.tsx
msgid "If the entry doesn't entirely fit on the screen"
msgstr ""
#: src/pages/app/AboutPage.tsx
msgid "If you encounter an issue, please report it on the issues page of the GitHub project."
msgstr "Se riscontri un problema, segnalalo nella pagina dei problemi del progetto GitHub."
@@ -433,6 +449,10 @@ msgstr "Ultimo aggiornamento"
msgid "Last refresh message"
msgstr "Ultimo messaggio di aggiornamento"
#: src/components/header/ProfileMenu.tsx
msgid "Light"
msgstr ""
#: src/pages/app/CategoryDetailsPage.tsx
#: src/pages/app/FeedDetailsPage.tsx
#: src/pages/app/TagDetailsPage.tsx
@@ -525,6 +545,10 @@ msgstr "Nome"
msgid "Navigate to a subscription by entering its name"
msgstr "Navigare verso un abbonamento inserendo il suo nome"
#: src/components/settings/DisplaySettings.tsx
msgid "Never"
msgstr ""
#: src/components/settings/ProfileSettings.tsx
msgid "New password"
msgstr "Nuova password"
@@ -558,6 +582,10 @@ msgstr "Non è stato trovato nulla"
msgid "Oldest first"
msgstr "Il più vecchio prima"
#: src/components/settings/DisplaySettings.tsx
msgid "On mobile, show action buttons at the bottom of the screen"
msgstr ""
#: src/pages/ErrorPage.tsx
msgid "Oops!"
msgstr "Ops!"
@@ -586,6 +614,10 @@ msgstr ""
msgid "Open link in new tab"
msgstr ""
#: src/pages/app/Layout.tsx
msgid "Open menu"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Open next entry"
msgstr "Apri voce successiva"
@@ -682,10 +714,18 @@ msgstr ""
msgid "Save"
msgstr "Salva"
#: src/components/settings/DisplaySettings.tsx
msgid "Scroll selected entry to the top of the page"
msgstr ""
#: src/components/settings/DisplaySettings.tsx
msgid "Scroll smoothly when navigating between entries"
msgstr "Scorrere senza problemi durante la navigazione tra le voci"
#: src/components/settings/DisplaySettings.tsx
msgid "Scrolling"
msgstr ""
#: src/components/header/Header.tsx
#: src/components/header/Header.tsx
#: src/components/sidebar/TreeSearch.tsx
@@ -709,7 +749,7 @@ msgstr "Imposta il focus sulla voce precedente senza aprirla"
msgid "Settings"
msgstr "Impostazioni"
#: src/app/slices/user.ts
#: src/app/user/slice.ts
msgid "Settings saved."
msgstr "Impostazioni salvate."
@@ -799,19 +839,22 @@ msgid "Success"
msgstr "Successo"
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Swipe header to the right"
msgid "Swipe header to the left"
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"
#: src/components/header/ProfileMenu.tsx
msgid "System"
msgstr ""
#: src/components/content/FeedEntryFooter.tsx
#: src/components/content/FeedEntryFooter.tsx
msgid "Tags"
msgstr "Tag"
@@ -824,6 +867,10 @@ msgstr "L'URL del feed a cui vuoi iscriverti. "
msgid "Theme"
msgstr "Tema"
#: src/components/settings/ProfileSettings.tsx
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle read status of current entry"
msgstr "Commuta lo stato di lettura della voce corrente"
@@ -832,6 +879,10 @@ msgstr "Commuta lo stato di lettura della voce corrente"
msgid "Toggle sidebar"
msgstr ""
#: src/components/KeyboardShortcutsHelp.tsx
msgid "Toggle starred status of current entry"
msgstr ""
#: src/pages/auth/LoginPage.tsx
msgid "Try out CommaFeed with the demo account: demo/demo"
msgstr "Prova CommaFeed con il conto demo: demo/demo"

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