forked from Archives/Athou_commafeed
Compare commits
178 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5da94a7ed0 | ||
|
|
dfb3006c47 | ||
|
|
e626f36c0a | ||
|
|
ff81749559 | ||
|
|
34db9baa7b | ||
|
|
541b5ef085 | ||
|
|
a974164ac8 | ||
|
|
9ca8358900 | ||
|
|
2bd7b46f11 | ||
|
|
eafe56a967 | ||
|
|
c18cd62d24 | ||
|
|
180385d6ab | ||
|
|
838eb8b725 | ||
|
|
b7cb3ee3f7 | ||
|
|
0a97e3f8f0 | ||
|
|
0229292b48 | ||
|
|
c87a965ae1 | ||
|
|
baa4122793 | ||
|
|
e9a4cb3432 | ||
|
|
30fc2cb8a4 | ||
|
|
25ccece76c | ||
|
|
2cb9d2285a | ||
|
|
0bb922fb99 | ||
|
|
1c59ec5857 | ||
|
|
f4e97f6350 | ||
|
|
fb355187ee | ||
|
|
89eb4d0535 | ||
|
|
11fe2f9db8 | ||
|
|
86acc3850a | ||
|
|
77bf97c6d6 | ||
|
|
1a96579292 | ||
|
|
caccd3802c | ||
|
|
dce899186b | ||
|
|
5626b39ffa | ||
|
|
5386c99c6b | ||
|
|
c27fae140f | ||
|
|
b8b67132f4 | ||
|
|
5623039084 | ||
|
|
39ddb256de | ||
|
|
71801718dc | ||
|
|
b728e28081 | ||
|
|
bf2de7aecd | ||
|
|
010fb2dccb | ||
|
|
8517c0f4eb | ||
|
|
09737b4d4c | ||
|
|
88e9a2c2e1 | ||
|
|
0d7300c192 | ||
|
|
cb1a00c5cd | ||
|
|
07a07006cc | ||
|
|
bae7f94f8c | ||
|
|
b0832c5917 | ||
|
|
f72e70cb56 | ||
|
|
8cc24e054f | ||
|
|
48e42228b1 | ||
|
|
46c1af65f0 | ||
|
|
2989407d16 | ||
|
|
2401e36486 | ||
|
|
4ee396e667 | ||
|
|
08180dd373 | ||
|
|
561513b7ed | ||
|
|
9cd7053a90 | ||
|
|
72d510bd47 | ||
|
|
1085d6aa7a | ||
|
|
9e0ef9461f | ||
|
|
650acb62d5 | ||
|
|
ff1c8a1eff | ||
|
|
62a4ac46a0 | ||
|
|
fafd4c9d54 | ||
|
|
73b472bc8a | ||
|
|
1c3be67f76 | ||
|
|
2a5988b3e7 | ||
|
|
c5757849f3 | ||
|
|
b6107c3330 | ||
|
|
3efeed6c85 | ||
|
|
be44b0aad1 | ||
|
|
36152dc47f | ||
|
|
32e9cd3e35 | ||
|
|
4bf8b5696d | ||
|
|
0bf44dbc7b | ||
|
|
bda3ba4b5c | ||
|
|
23cff9c1e9 | ||
|
|
9691517335 | ||
|
|
b38bd8c312 | ||
|
|
d8ca58389d | ||
|
|
20a0cd7192 | ||
|
|
9b895328be | ||
|
|
cd39ab5f95 | ||
|
|
a7152a97a6 | ||
|
|
3e6e0a0f00 | ||
|
|
2936dd0d32 | ||
|
|
38a838d210 | ||
|
|
0136fa883d | ||
|
|
b0890df2f3 | ||
|
|
91acad0dbf | ||
|
|
14e7d70106 | ||
|
|
1cc76ba3ee | ||
|
|
206800c091 | ||
|
|
33749c94e3 | ||
|
|
8bce887e4c | ||
|
|
ca4f73fff6 | ||
|
|
26443310c9 | ||
|
|
870593bae8 | ||
|
|
cfd5d0faab | ||
|
|
9391c05968 | ||
|
|
a13c75981b | ||
|
|
a05baf63c1 | ||
|
|
32ce265cff | ||
|
|
b2ad24e7f6 | ||
|
|
fe626ebbe3 | ||
|
|
4431a898a0 | ||
|
|
89bfcfa240 | ||
|
|
d046d26f4e | ||
|
|
26b634b1a3 | ||
|
|
3ca18bbd36 | ||
|
|
7645731fff | ||
|
|
3c116dbabe | ||
|
|
3026fd116c | ||
|
|
63fa725a13 | ||
|
|
ede4e07ff3 | ||
|
|
de6dfbe8b2 | ||
|
|
164a57bef5 | ||
|
|
fd82b8aaee | ||
|
|
facf8b43f2 | ||
|
|
3184dfe178 | ||
|
|
cc584fd8c8 | ||
|
|
0f8fa1f2e1 | ||
|
|
d93f7bd20e | ||
|
|
96aa06d2dd | ||
|
|
fdaff46008 | ||
|
|
71066cd768 | ||
|
|
b9610a9058 | ||
|
|
ca027d5a4d | ||
|
|
1dea51c705 | ||
|
|
8edc89f3cc | ||
|
|
4cbf32cbb8 | ||
|
|
a5dc551b6b | ||
|
|
b1e0dbd0b3 | ||
|
|
58789b15a3 | ||
|
|
c5f58a2fe9 | ||
|
|
253ba5f18b | ||
|
|
ae859178c0 | ||
|
|
942dc0befe | ||
|
|
66c4510fd3 | ||
|
|
feb7de504c | ||
|
|
ec4b809ff9 | ||
|
|
b8d6a5742b | ||
|
|
31c42403a1 | ||
|
|
ead97be3cf | ||
|
|
3ee43f75d6 | ||
|
|
f77b91540d | ||
|
|
34915c93b8 | ||
|
|
365044d205 | ||
|
|
46f84ab29e | ||
|
|
2041823f0d | ||
|
|
ff01d7a87c | ||
|
|
4d905b118a | ||
|
|
6d74b50751 | ||
|
|
f12bdf5841 | ||
|
|
6b89e211d8 | ||
|
|
1b00e5613a | ||
|
|
050a8b24fc | ||
|
|
c5b1ea486c | ||
|
|
7b8c0ac6ff | ||
|
|
d43039cf9f | ||
|
|
3adc043740 | ||
|
|
08b95ff3dd | ||
|
|
e043ce71c3 | ||
|
|
b345319f68 | ||
|
|
4c298df9c9 | ||
|
|
067e01660a | ||
|
|
c649a04891 | ||
|
|
9b9a4f98f4 | ||
|
|
f8b6f2f237 | ||
|
|
371ce0d160 | ||
|
|
a92a7217ff | ||
|
|
e69c230678 | ||
|
|
b82077d3ca | ||
|
|
c624955ea4 |
24
.github/dependabot.yml
vendored
Normal file
24
.github/dependabot.yml
vendored
Normal 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
|
||||
16
.github/workflows/build.yml
vendored
16
.github/workflows/build.yml
vendored
@@ -11,19 +11,19 @@ jobs:
|
||||
|
||||
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"
|
||||
@@ -42,14 +42,14 @@ jobs:
|
||||
|
||||
# Docker
|
||||
- name: Login to Container Registry
|
||||
uses: docker/login-action@v2
|
||||
if: ${{ matrix.java == '17' }}
|
||||
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
|
||||
uses: docker/build-push-action@v5
|
||||
if: ${{ matrix.java == '17' && github.ref_type == 'tag' }}
|
||||
with:
|
||||
context: .
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
athou/commafeed:${{ github.ref_name }}
|
||||
|
||||
- name: Docker build and push master
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v5
|
||||
if: ${{ matrix.java == '17' && github.ref_name == 'master' }}
|
||||
with:
|
||||
context: .
|
||||
|
||||
28
CHANGELOG.md
28
CHANGELOG.md
@@ -1,5 +1,33 @@
|
||||
# Changelog
|
||||
|
||||
## [4.3.3]
|
||||
|
||||
- fix OPML import (#1279)
|
||||
|
||||
## [4.3.2]
|
||||
|
||||
- added support for unix sockets (#1278)
|
||||
|
||||
## [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)
|
||||
|
||||
@@ -1,41 +1,47 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
browser: true,
|
||||
es2021: true
|
||||
es2021: true,
|
||||
},
|
||||
extends: ["standard-with-typescript", "plugin:react/recommended", "plugin:react-hooks/recommended", "plugin:prettier/recommended"],
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"standard",
|
||||
"plugin:@typescript-eslint/strict-type-checked",
|
||||
"plugin:@typescript-eslint/stylistic-type-checked",
|
||||
"plugin:react/recommended",
|
||||
"plugin:react-hooks/recommended",
|
||||
"plugin:prettier/recommended",
|
||||
],
|
||||
settings: {
|
||||
react: {
|
||||
version: "detect"
|
||||
}
|
||||
version: "detect",
|
||||
},
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
env: {
|
||||
node: true
|
||||
node: true,
|
||||
},
|
||||
files: [".eslintrc.{js,cjs}"],
|
||||
parserOptions: {
|
||||
sourceType: "script"
|
||||
}
|
||||
}
|
||||
sourceType: "script",
|
||||
},
|
||||
},
|
||||
],
|
||||
parserOptions: {
|
||||
project: true,
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module"
|
||||
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"
|
||||
}
|
||||
"react-hooks/exhaustive-deps": "error",
|
||||
},
|
||||
}
|
||||
|
||||
1623
commafeed-client/package-lock.json
generated
1623
commafeed-client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -14,24 +14,24 @@
|
||||
"i18n:extract": "lingui extract --clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.11.3",
|
||||
"@fontsource/open-sans": "^5.0.20",
|
||||
"@lingui/core": "^4.7.0",
|
||||
"@lingui/macro": "^4.7.0",
|
||||
"@lingui/react": "^4.7.0",
|
||||
"@mantine/core": "^7.3.2",
|
||||
"@mantine/form": "^7.3.2",
|
||||
"@mantine/hooks": "^7.3.2",
|
||||
"@mantine/modals": "^7.3.2",
|
||||
"@mantine/notifications": "^7.3.2",
|
||||
"@mantine/spotlight": "^7.3.2",
|
||||
"@emotion/react": "^11.11.4",
|
||||
"@fontsource/open-sans": "^5.0.25",
|
||||
"@lingui/core": "^4.7.1",
|
||||
"@lingui/macro": "^4.7.1",
|
||||
"@lingui/react": "^4.7.1",
|
||||
"@mantine/core": "^7.6.1",
|
||||
"@mantine/form": "^7.6.1",
|
||||
"@mantine/hooks": "^7.6.1",
|
||||
"@mantine/modals": "^7.6.1",
|
||||
"@mantine/notifications": "^7.6.1",
|
||||
"@mantine/spotlight": "^7.6.1",
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@reduxjs/toolkit": "^2.0.1",
|
||||
"axios": "^1.6.3",
|
||||
"@reduxjs/toolkit": "^2.2.1",
|
||||
"axios": "^1.6.7",
|
||||
"dayjs": "^1.11.10",
|
||||
"escape-string-regexp": "^5.0.0",
|
||||
"interweave": "^13.1.0",
|
||||
"monaco-editor": "^0.45.0",
|
||||
"monaco-editor": "^0.46.0",
|
||||
"mousetrap": "^1.6.5",
|
||||
"react": "^18.2.0",
|
||||
"react-async-hook": "^4.0.0",
|
||||
@@ -39,47 +39,44 @@
|
||||
"react-dom": "^18.2.0",
|
||||
"react-draggable": "^4.4.6",
|
||||
"react-ga4": "^2.1.0",
|
||||
"react-icons": "^4.12.0",
|
||||
"react-icons": "^5.0.1",
|
||||
"react-infinite-scroller": "^1.2.6",
|
||||
"react-redux": "^9.0.4",
|
||||
"react-router-dom": "^6.21.1",
|
||||
"react-redux": "^9.1.0",
|
||||
"react-router-dom": "^6.22.2",
|
||||
"react-swipeable": "^7.0.1",
|
||||
"redoc": "^2.1.3",
|
||||
"throttle-debounce": "^5.0.0",
|
||||
"tinycon": "^0.6.8",
|
||||
"tss-react": "^4.9.3",
|
||||
"tss-react": "^4.9.4",
|
||||
"use-local-storage": "^3.0.0",
|
||||
"websocket-heartbeat-js": "^1.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lingui/cli": "^4.7.0",
|
||||
"@lingui/vite-plugin": "^4.7.0",
|
||||
"@lingui/cli": "^4.7.1",
|
||||
"@lingui/vite-plugin": "^4.7.1",
|
||||
"@types/mousetrap": "^1.6.15",
|
||||
"@types/react": "^18.2.46",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/react": "^18.2.61",
|
||||
"@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.16.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.1.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"babel-plugin-macros": "^3.1.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-config-standard-with-typescript": "^43.0.0",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-n": "^16.5.0",
|
||||
"eslint-plugin-prettier": "^5.1.2",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-config-standard": "^17.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-react": "^7.34.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"prettier": "^3.1.1",
|
||||
"prettier": "^3.2.5",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.0.12",
|
||||
"vite": "^5.1.4",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
"vite-tsconfig-paths": "^4.2.3",
|
||||
"vitest": "^1.1.3",
|
||||
"vite-tsconfig-paths": "^4.3.1",
|
||||
"vitest": "^1.3.1",
|
||||
"vitest-mock-extended": "^1.3.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<groupId>com.commafeed</groupId>
|
||||
<artifactId>commafeed</artifactId>
|
||||
<version>4.2.0</version>
|
||||
<version>4.3.3</version>
|
||||
</parent>
|
||||
<artifactId>commafeed-client</artifactId>
|
||||
<name>CommaFeed Client</name>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import axios from "axios"
|
||||
import axios, { AxiosError } from "axios"
|
||||
import {
|
||||
type AddCategoryRequest,
|
||||
type AdminSaveUserRequest,
|
||||
AuthenticationError,
|
||||
type Category,
|
||||
type CategoryModificationRequest,
|
||||
type CollapseRequest,
|
||||
@@ -31,17 +32,18 @@ const axiosInstance = axios.create({ baseURL: "./rest", withCredentials: true })
|
||||
axiosInstance.interceptors.response.use(
|
||||
response => response,
|
||||
error => {
|
||||
const { status, data } = error.response
|
||||
if (
|
||||
(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.")
|
||||
) {
|
||||
if (isAuthenticationError(error)) {
|
||||
const data = error.response?.data
|
||||
window.location.hash = data?.allowRegistrations ? "/welcome" : "/login"
|
||||
}
|
||||
throw error
|
||||
}
|
||||
)
|
||||
|
||||
function isAuthenticationError(error: unknown): error is AxiosError<AuthenticationError> {
|
||||
return axios.isAxiosError(error) && !!error.response && [401, 403].includes(error.response.status)
|
||||
}
|
||||
|
||||
export const client = {
|
||||
category: {
|
||||
getRoot: async () => await axiosInstance.get<Category>("category/get"),
|
||||
@@ -107,14 +109,19 @@ export const client = {
|
||||
export const errorToStrings = (err: unknown) => {
|
||||
let strings: string[] = []
|
||||
|
||||
if (axios.isAxiosError(err)) {
|
||||
if (err.response) {
|
||||
const { data } = err.response
|
||||
if (typeof data === "string") strings.push(data)
|
||||
if (typeof data === "object" && data.message) strings.push(data.message as string)
|
||||
if (typeof data === "object" && data.errors) strings = [...strings, ...data.errors]
|
||||
}
|
||||
if (axios.isAxiosError(err) && err.response) {
|
||||
if (typeof err.response.data === "string") strings.push(err.response.data)
|
||||
if (isMessageError(err)) strings.push(err.response.data.message)
|
||||
if (isMessageArrayError(err)) strings = [...strings, ...err.response.data.errors]
|
||||
}
|
||||
|
||||
return strings
|
||||
}
|
||||
|
||||
function isMessageError(err: AxiosError): err is AxiosError<{ message: string }> {
|
||||
return !!err.response && !!err.response.data && typeof err.response.data === "object" && "message" in err.response.data
|
||||
}
|
||||
|
||||
function isMessageArrayError(err: AxiosError): err is AxiosError<{ errors: string[] }> {
|
||||
return !!err.response && !!err.response.data && typeof err.response.data === "object" && "errors" in err.response.data
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable import/first */
|
||||
import { configureStore } from "@reduxjs/toolkit"
|
||||
import { type client } from "app/client"
|
||||
import { loadEntries, loadMoreEntries, markAllEntries, markEntry } from "app/entries/thunks"
|
||||
@@ -90,7 +89,7 @@ describe("entries", () => {
|
||||
expect(store.getState().entries.hasMore).toBe(false)
|
||||
})
|
||||
|
||||
it("marks an entry as read", async () => {
|
||||
it("marks an entry as read", () => {
|
||||
const store = configureStore({
|
||||
reducer: reducers,
|
||||
preloadedState: {
|
||||
@@ -117,7 +116,7 @@ describe("entries", () => {
|
||||
expect(mockClient.entry.mark).toHaveBeenCalledWith({ id: "3", read: true })
|
||||
})
|
||||
|
||||
it("marks all entries as read", async () => {
|
||||
it("marks all entries as read", () => {
|
||||
const store = configureStore({
|
||||
reducer: reducers,
|
||||
preloadedState: {
|
||||
|
||||
@@ -46,11 +46,11 @@ const buildGetEntriesPaginatedRequest = (state: RootState, source: EntrySource,
|
||||
tag: source.type === "tag" ? source.id : undefined,
|
||||
keywords: state.entries.search,
|
||||
})
|
||||
export const reloadEntries = createAppAsyncThunk("entries/reload", async (arg, thunkApi) => {
|
||||
export const reloadEntries = createAppAsyncThunk("entries/reload", (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) => {
|
||||
export const search = createAppAsyncThunk("entries/search", (arg: string, thunkApi) => {
|
||||
const state = thunkApi.getState()
|
||||
thunkApi.dispatch(setSearch(arg))
|
||||
thunkApi.dispatch(loadEntries({ source: state.entries.source, clearSearch: false }))
|
||||
@@ -84,7 +84,7 @@ export const markMultipleEntries = createAppAsyncThunk(
|
||||
thunkApi.dispatch(reloadTree())
|
||||
}
|
||||
)
|
||||
export const markEntriesUpToEntry = createAppAsyncThunk("entries/entry/upToEntry", async (arg: Entry, thunkApi) => {
|
||||
export const markEntriesUpToEntry = createAppAsyncThunk("entries/entry/upToEntry", (arg: Entry, thunkApi) => {
|
||||
const state = thunkApi.getState()
|
||||
const { entries } = state.entries
|
||||
|
||||
@@ -162,9 +162,9 @@ export const selectEntry = createAppAsyncThunk(
|
||||
if (arg.scrollToEntry) {
|
||||
const entryElement = document.getElementById(Constants.dom.entryId(entry))
|
||||
if (entryElement) {
|
||||
const alwaysScrollToEntry = state.user.settings?.alwaysScrollToEntry
|
||||
const scrollMode = state.user.settings?.scrollMode
|
||||
const entryEntirelyVisible = Constants.layout.isTopVisible(entryElement) && Constants.layout.isBottomVisible(entryElement)
|
||||
if (alwaysScrollToEntry || !entryEntirelyVisible) {
|
||||
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)))
|
||||
|
||||
@@ -1,8 +1,33 @@
|
||||
export type ReadingMode = "all" | "unread"
|
||||
|
||||
export type ReadingOrder = "asc" | "desc"
|
||||
|
||||
export type ViewMode = "title" | "cozy" | "detailed" | "expanded"
|
||||
|
||||
export type ScrollMode = "always" | "never" | "if_needed"
|
||||
|
||||
export interface AddCategoryRequest {
|
||||
name: string
|
||||
parentId?: string
|
||||
}
|
||||
|
||||
export interface Subscription {
|
||||
id: number
|
||||
name: string
|
||||
message?: string
|
||||
errorCount: number
|
||||
lastRefresh?: number
|
||||
nextRefresh?: number
|
||||
feedUrl: string
|
||||
feedLink: string
|
||||
iconUrl: string
|
||||
unread: number
|
||||
categoryId?: string
|
||||
position: number
|
||||
newestItemTime?: number
|
||||
filter?: string
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
id: string
|
||||
parentId?: string
|
||||
@@ -26,19 +51,6 @@ export interface CollapseRequest {
|
||||
collapse: boolean
|
||||
}
|
||||
|
||||
export interface Entries {
|
||||
name: string
|
||||
message?: string
|
||||
errorCount: number
|
||||
feedLink: string
|
||||
timestamp: number
|
||||
hasMore: boolean
|
||||
offset?: number
|
||||
limit?: number
|
||||
entries: Entry[]
|
||||
ignoredReadStatus: boolean
|
||||
}
|
||||
|
||||
export interface Entry {
|
||||
id: string
|
||||
guid: string
|
||||
@@ -67,6 +79,19 @@ export interface Entry {
|
||||
tags: string[]
|
||||
}
|
||||
|
||||
export interface Entries {
|
||||
name: string
|
||||
message?: string
|
||||
errorCount: number
|
||||
feedLink: string
|
||||
timestamp: number
|
||||
hasMore: boolean
|
||||
offset?: number
|
||||
limit?: number
|
||||
entries: Entry[]
|
||||
ignoredReadStatus: boolean
|
||||
}
|
||||
|
||||
export interface FeedInfo {
|
||||
url: string
|
||||
title: string
|
||||
@@ -196,22 +221,6 @@ export interface ServerInfo {
|
||||
treeReloadInterval: number
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
language: string
|
||||
readingMode: ReadingMode
|
||||
readingOrder: ReadingOrder
|
||||
showRead: boolean
|
||||
scrollMarks: boolean
|
||||
customCss?: string
|
||||
customJs?: string
|
||||
scrollSpeed: number
|
||||
alwaysScrollToEntry: boolean
|
||||
markAllAsReadConfirmation: boolean
|
||||
customContextMenu: boolean
|
||||
mobileFooter: boolean
|
||||
sharingSettings: SharingSettings
|
||||
}
|
||||
|
||||
export interface SharingSettings {
|
||||
email: boolean
|
||||
gmail: boolean
|
||||
@@ -223,6 +232,22 @@ export interface SharingSettings {
|
||||
buffer: boolean
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
language: string
|
||||
readingMode: ReadingMode
|
||||
readingOrder: ReadingOrder
|
||||
showRead: boolean
|
||||
scrollMarks: boolean
|
||||
customCss?: string
|
||||
customJs?: string
|
||||
scrollSpeed: number
|
||||
scrollMode: ScrollMode
|
||||
markAllAsReadConfirmation: boolean
|
||||
customContextMenu: boolean
|
||||
mobileFooter: boolean
|
||||
sharingSettings: SharingSettings
|
||||
}
|
||||
|
||||
export interface StarRequest {
|
||||
id: string
|
||||
feedId: number
|
||||
@@ -235,34 +260,11 @@ export interface SubscribeRequest {
|
||||
categoryId?: string
|
||||
}
|
||||
|
||||
export interface Subscription {
|
||||
id: number
|
||||
name: string
|
||||
message?: string
|
||||
errorCount: number
|
||||
lastRefresh?: number
|
||||
nextRefresh?: number
|
||||
feedUrl: string
|
||||
feedLink: string
|
||||
iconUrl: string
|
||||
unread: number
|
||||
categoryId?: string
|
||||
position: number
|
||||
newestItemTime?: number
|
||||
filter?: string
|
||||
}
|
||||
|
||||
export interface TagRequest {
|
||||
entryId: number
|
||||
tags: string[]
|
||||
}
|
||||
|
||||
export interface UnreadCount {
|
||||
feedId?: number
|
||||
unreadCount?: number
|
||||
newestItemTime?: number
|
||||
}
|
||||
|
||||
export interface UserModel {
|
||||
id: number
|
||||
name: string
|
||||
@@ -284,8 +286,7 @@ export interface AdminSaveUserRequest {
|
||||
admin: boolean
|
||||
}
|
||||
|
||||
export type ReadingMode = "all" | "unread"
|
||||
|
||||
export type ReadingOrder = "asc" | "desc"
|
||||
|
||||
export type ViewMode = "title" | "cozy" | "detailed" | "expanded"
|
||||
export interface AuthenticationError {
|
||||
message: string
|
||||
allowRegistrations: boolean
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { showNotification } from "@mantine/notifications"
|
||||
import { createSlice, isAnyOf } from "@reduxjs/toolkit"
|
||||
import { type Settings, type UserModel } from "app/types"
|
||||
import {
|
||||
changeAlwaysScrollToEntry,
|
||||
changeCustomContextMenu,
|
||||
changeLanguage,
|
||||
changeMarkAllAsReadConfirmation,
|
||||
@@ -11,6 +10,7 @@ import {
|
||||
changeReadingMode,
|
||||
changeReadingOrder,
|
||||
changeScrollMarks,
|
||||
changeScrollMode,
|
||||
changeScrollSpeed,
|
||||
changeSharingSetting,
|
||||
changeShowRead,
|
||||
@@ -65,9 +65,9 @@ export const userSlice = createSlice({
|
||||
if (!state.settings) return
|
||||
state.settings.scrollMarks = action.meta.arg
|
||||
})
|
||||
builder.addCase(changeAlwaysScrollToEntry.pending, (state, action) => {
|
||||
builder.addCase(changeScrollMode.pending, (state, action) => {
|
||||
if (!state.settings) return
|
||||
state.settings.alwaysScrollToEntry = action.meta.arg
|
||||
state.settings.scrollMode = action.meta.arg
|
||||
})
|
||||
builder.addCase(changeMarkAllAsReadConfirmation.pending, (state, action) => {
|
||||
if (!state.settings) return
|
||||
@@ -91,7 +91,7 @@ export const userSlice = createSlice({
|
||||
changeScrollSpeed.fulfilled,
|
||||
changeShowRead.fulfilled,
|
||||
changeScrollMarks.fulfilled,
|
||||
changeAlwaysScrollToEntry.fulfilled,
|
||||
changeScrollMode.fulfilled,
|
||||
changeMarkAllAsReadConfirmation.fulfilled,
|
||||
changeCustomContextMenu.fulfilled,
|
||||
changeMobileFooter.fulfilled,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createAppAsyncThunk } from "app/async-thunk"
|
||||
import { client } from "app/client"
|
||||
import { reloadEntries } from "app/entries/thunks"
|
||||
import type { ReadingMode, ReadingOrder, SharingSettings } from "app/types"
|
||||
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))
|
||||
@@ -38,10 +38,10 @@ export const changeScrollMarks = createAppAsyncThunk("settings/scrollMarks", (sc
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, scrollMarks })
|
||||
})
|
||||
export const changeAlwaysScrollToEntry = createAppAsyncThunk("settings/alwaysScrollToEntry", (alwaysScrollToEntry: boolean, thunkApi) => {
|
||||
export const changeScrollMode = createAppAsyncThunk("settings/scrollMode", (scrollMode: ScrollMode, thunkApi) => {
|
||||
const { settings } = thunkApi.getState().user
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, alwaysScrollToEntry })
|
||||
client.user.saveSettings({ ...settings, scrollMode })
|
||||
})
|
||||
export const changeMarkAllAsReadConfirmation = createAppAsyncThunk(
|
||||
"settings/markAllAsReadConfirmation",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Box, Center, type MantineTheme, useMantineTheme } from "@mantine/core"
|
||||
import { useColorScheme } from "hooks/useColorScheme"
|
||||
import { Box, Center } from "@mantine/core"
|
||||
import { useState } from "react"
|
||||
import { TbPhoto } from "react-icons/tb"
|
||||
import { tss } from "tss"
|
||||
@@ -18,8 +17,6 @@ interface ImageWithPlaceholderWhileLoadingProps {
|
||||
|
||||
const useStyles = tss
|
||||
.withParams<{
|
||||
theme: MantineTheme
|
||||
colorScheme: "light" | "dark"
|
||||
placeholderWidth?: number
|
||||
placeholderHeight?: number
|
||||
placeholderBackgroundColor?: string
|
||||
@@ -46,11 +43,7 @@ export function ImageWithPlaceholderWhileLoading({
|
||||
title,
|
||||
width,
|
||||
}: ImageWithPlaceholderWhileLoadingProps) {
|
||||
const theme = useMantineTheme()
|
||||
const colorScheme = useColorScheme()
|
||||
const { classes } = useStyles({
|
||||
theme,
|
||||
colorScheme,
|
||||
placeholderWidth,
|
||||
placeholderHeight,
|
||||
placeholderBackgroundColor,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { type ReactNode } from "react"
|
||||
interface CodeEditorProps {
|
||||
description?: ReactNode
|
||||
language: "css" | "javascript"
|
||||
value: string
|
||||
value?: string
|
||||
onChange: (value: string | undefined) => void
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ const init = async () => {
|
||||
interface RichCodeEditorProps {
|
||||
height: number | string
|
||||
language: "css" | "javascript"
|
||||
value: string
|
||||
value?: string
|
||||
onChange: (value: string | undefined) => void
|
||||
}
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ class HighlightMatcher extends Matcher {
|
||||
return this.doMatch(string, new RegExp(pattern, "i"), () => ({}))
|
||||
}
|
||||
|
||||
replaceWith(children: ChildrenNode, props: unknown): Node {
|
||||
replaceWith(children: ChildrenNode): Node {
|
||||
return <Mark>{children}</Mark>
|
||||
}
|
||||
|
||||
|
||||
@@ -2,14 +2,14 @@ import { BasicHtmlStyles } from "components/content/BasicHtmlStyles"
|
||||
import { ImageWithPlaceholderWhileLoading } from "components/ImageWithPlaceholderWhileLoading"
|
||||
|
||||
export function Enclosure(props: { enclosureType: string; enclosureUrl: string }) {
|
||||
const hasVideo = props.enclosureType?.startsWith("video")
|
||||
const hasAudio = props.enclosureType?.startsWith("audio")
|
||||
const hasImage = props.enclosureType?.startsWith("image")
|
||||
const hasVideo = props.enclosureType.startsWith("video")
|
||||
const hasAudio = props.enclosureType.startsWith("audio")
|
||||
const hasImage = props.enclosureType.startsWith("image")
|
||||
|
||||
return (
|
||||
<BasicHtmlStyles>
|
||||
{hasVideo && (
|
||||
<video controls>
|
||||
<video controls width="100%">
|
||||
<source src={props.enclosureUrl} type={props.enclosureType} />
|
||||
</video>
|
||||
)}
|
||||
|
||||
@@ -295,10 +295,10 @@ export function FeedEntries() {
|
||||
})
|
||||
)
|
||||
|
||||
if (!entries) return <Loader />
|
||||
return (
|
||||
<InfiniteScroll
|
||||
id="entries"
|
||||
className={`view-mode-${viewMode}`}
|
||||
initialLoad={false}
|
||||
loadMore={async () => await (!loading && dispatch(loadMoreEntries()))}
|
||||
hasMore={hasMore}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Box, Divider, type MantineRadius, type MantineSpacing, type MantineTheme, Paper, useMantineTheme } from "@mantine/core"
|
||||
import { Box, Divider, type MantineRadius, type MantineSpacing, Paper } from "@mantine/core"
|
||||
import { Constants } from "app/constants"
|
||||
import { type Entry, type ViewMode } from "app/types"
|
||||
import { useColorScheme } from "hooks/useColorScheme"
|
||||
import { useViewMode } from "hooks/useViewMode"
|
||||
import React from "react"
|
||||
import { useSwipeable } from "react-swipeable"
|
||||
@@ -26,8 +25,6 @@ interface FeedEntryProps {
|
||||
|
||||
const useStyles = tss
|
||||
.withParams<{
|
||||
theme: MantineTheme
|
||||
colorScheme: "light" | "dark"
|
||||
read: boolean
|
||||
expanded: boolean
|
||||
viewMode: ViewMode
|
||||
@@ -96,12 +93,8 @@ const useStyles = tss
|
||||
})
|
||||
|
||||
export function FeedEntry(props: FeedEntryProps) {
|
||||
const theme = useMantineTheme()
|
||||
const colorScheme = useColorScheme()
|
||||
const { viewMode } = useViewMode()
|
||||
const { classes, cx } = useStyles({
|
||||
theme,
|
||||
colorScheme,
|
||||
read: props.entry.read,
|
||||
expanded: props.expanded,
|
||||
viewMode,
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Box, Text } from "@mantine/core"
|
||||
import { type Entry } from "app/types"
|
||||
import { RelativeDate } from "components/RelativeDate"
|
||||
import { OnDesktop } from "components/responsive/OnDesktop"
|
||||
import { useColorScheme } from "hooks/useColorScheme"
|
||||
import { tss } from "tss"
|
||||
import { FeedEntryTitle } from "./FeedEntryTitle"
|
||||
import { FeedFavicon } from "./FeedFavicon"
|
||||
@@ -13,7 +12,6 @@ export interface FeedEntryHeaderProps {
|
||||
|
||||
const useStyles = tss
|
||||
.withParams<{
|
||||
colorScheme: "light" | "dark"
|
||||
read: boolean
|
||||
}>()
|
||||
.create(({ colorScheme, read }) => ({
|
||||
@@ -42,9 +40,7 @@ const useStyles = tss
|
||||
}))
|
||||
|
||||
export function FeedEntryCompactHeader(props: FeedEntryHeaderProps) {
|
||||
const colorScheme = useColorScheme()
|
||||
const { classes } = useStyles({
|
||||
colorScheme,
|
||||
read: props.entry.read,
|
||||
})
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Trans } from "@lingui/macro"
|
||||
import { Group, type MantineTheme, useMantineTheme } from "@mantine/core"
|
||||
import { Group } from "@mantine/core"
|
||||
import { Constants } from "app/constants"
|
||||
import { markEntriesUpToEntry, markEntry, starEntry } from "app/entries/thunks"
|
||||
import { redirectToFeed } from "app/redirect/thunks"
|
||||
@@ -17,28 +17,19 @@ interface FeedEntryContextMenuProps {
|
||||
}
|
||||
|
||||
const iconSize = 16
|
||||
const useStyles = tss
|
||||
.withParams<{
|
||||
theme: MantineTheme
|
||||
colorScheme: "light" | "dark"
|
||||
}>()
|
||||
.create(({ theme, colorScheme }) => ({
|
||||
menu: {
|
||||
// apply mantine theme from MenuItem.styles.ts
|
||||
fontSize: theme.fontSizes.sm,
|
||||
"--contexify-item-color": `${colorScheme === "dark" ? theme.colors.dark[0] : theme.black} !important`,
|
||||
"--contexify-activeItem-color": `${colorScheme === "dark" ? theme.colors.dark[0] : theme.black} !important`,
|
||||
"--contexify-activeItem-bgColor": `${colorScheme === "dark" ? theme.colors.dark[4] : theme.colors.gray[1]} !important`,
|
||||
},
|
||||
}))
|
||||
const useStyles = tss.create(({ theme, colorScheme }) => ({
|
||||
menu: {
|
||||
// apply mantine theme from MenuItem.styles.ts
|
||||
fontSize: theme.fontSizes.sm,
|
||||
"--contexify-item-color": `${colorScheme === "dark" ? theme.colors.dark[0] : theme.black} !important`,
|
||||
"--contexify-activeItem-color": `${colorScheme === "dark" ? theme.colors.dark[0] : theme.black} !important`,
|
||||
"--contexify-activeItem-bgColor": `${colorScheme === "dark" ? theme.colors.dark[4] : theme.colors.gray[1]} !important`,
|
||||
},
|
||||
}))
|
||||
|
||||
export function FeedEntryContextMenu(props: FeedEntryContextMenuProps) {
|
||||
const theme = useMantineTheme()
|
||||
const colorScheme = useColorScheme()
|
||||
const { classes } = useStyles({
|
||||
theme,
|
||||
colorScheme,
|
||||
})
|
||||
const { classes } = useStyles()
|
||||
const sourceType = useAppSelector(state => state.entries.source.type)
|
||||
const dispatch = useAppDispatch()
|
||||
const { openLinkInBackgroundTab } = useBrowserExtension()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Box, Space, Text } from "@mantine/core"
|
||||
import { type Entry } from "app/types"
|
||||
import { RelativeDate } from "components/RelativeDate"
|
||||
import { useColorScheme } from "hooks/useColorScheme"
|
||||
import { tss } from "tss"
|
||||
import { FeedEntryTitle } from "./FeedEntryTitle"
|
||||
import { FeedFavicon } from "./FeedFavicon"
|
||||
@@ -13,7 +12,6 @@ export interface FeedEntryHeaderProps {
|
||||
|
||||
const useStyles = tss
|
||||
.withParams<{
|
||||
colorScheme: "light" | "dark"
|
||||
read: boolean
|
||||
}>()
|
||||
.create(({ colorScheme, read }) => ({
|
||||
@@ -28,9 +26,7 @@ const useStyles = tss
|
||||
}))
|
||||
|
||||
export function FeedEntryHeader(props: FeedEntryHeaderProps) {
|
||||
const colorScheme = useColorScheme()
|
||||
const { classes } = useStyles({
|
||||
colorScheme,
|
||||
read: props.entry.read,
|
||||
})
|
||||
return (
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { ActionIcon, Box, type MantineTheme, SimpleGrid, useMantineTheme } from "@mantine/core"
|
||||
import { ActionIcon, Box, SimpleGrid } from "@mantine/core"
|
||||
import { Constants } from "app/constants"
|
||||
import { useAppSelector } from "app/store"
|
||||
import { type SharingSettings } from "app/types"
|
||||
import { useColorScheme } from "hooks/useColorScheme"
|
||||
import { type IconType } from "react-icons"
|
||||
import { tss } from "tss"
|
||||
|
||||
@@ -10,8 +9,6 @@ type Color = `#${string}`
|
||||
|
||||
const useStyles = tss
|
||||
.withParams<{
|
||||
theme: MantineTheme
|
||||
colorScheme: "light" | "dark"
|
||||
color: Color
|
||||
}>()
|
||||
.create(({ theme, colorScheme, color }) => ({
|
||||
@@ -23,11 +20,7 @@ const useStyles = tss
|
||||
}))
|
||||
|
||||
function ShareButton({ url, icon, color }: { url: string; icon: IconType; color: Color }) {
|
||||
const theme = useMantineTheme()
|
||||
const colorScheme = useColorScheme()
|
||||
const { classes } = useStyles({
|
||||
theme,
|
||||
colorScheme,
|
||||
color,
|
||||
})
|
||||
|
||||
@@ -54,7 +47,7 @@ export function ShareButtons(props: { url: string; description: string }) {
|
||||
|
||||
return (
|
||||
<SimpleGrid cols={4}>
|
||||
{(Object.keys(Constants.sharing) as Array<keyof SharingSettings>)
|
||||
{(Object.keys(Constants.sharing) as (keyof SharingSettings)[])
|
||||
.filter(site => sharingSettings?.[site])
|
||||
.map(site => (
|
||||
<ShareButton
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { t, Trans } from "@lingui/macro"
|
||||
import { Box, Button, FileInput, Group, Stack } from "@mantine/core"
|
||||
import { useForm } from "@mantine/form"
|
||||
import { isNotEmpty, useForm } from "@mantine/form"
|
||||
import { client, errorToStrings } from "app/client"
|
||||
import { redirectToSelectedSource } from "app/redirect/thunks"
|
||||
import { useAppDispatch } from "app/store"
|
||||
@@ -14,7 +14,7 @@ export function ImportOpml() {
|
||||
|
||||
const form = useForm<{ file: File }>({
|
||||
validate: {
|
||||
file: v => (v ? null : t`file is required`),
|
||||
file: isNotEmpty(t`OPML file is required`),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -38,8 +38,7 @@ export function ImportOpml() {
|
||||
<FileInput
|
||||
label={<Trans>OPML file</Trans>}
|
||||
leftSection={<TbFileImport />}
|
||||
// https://github.com/mantinedev/mantine/issues/5401
|
||||
{...{ placeholder: t`OPML file` }}
|
||||
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
|
||||
|
||||
@@ -1,33 +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 SharingSettings } from "app/types"
|
||||
import { type ScrollMode, type SharingSettings } from "app/types"
|
||||
import {
|
||||
changeAlwaysScrollToEntry,
|
||||
changeCustomContextMenu,
|
||||
changeLanguage,
|
||||
changeMarkAllAsReadConfirmation,
|
||||
changeMobileFooter,
|
||||
changeScrollMarks,
|
||||
changeScrollMode,
|
||||
changeScrollSpeed,
|
||||
changeSharingSetting,
|
||||
changeShowRead,
|
||||
} 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
|
||||
@@ -40,30 +47,12 @@ export function DisplaySettings() {
|
||||
onChange={async s => await (s && dispatch(changeLanguage(s)))}
|
||||
/>
|
||||
|
||||
<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>Always scroll selected entry to the top of the page, even if it fits entirely on screen</Trans>}
|
||||
checked={alwaysScrollToEntry}
|
||||
onChange={async e => await dispatch(changeAlwaysScrollToEntry(e.currentTarget.checked))}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label={<Trans>Show feeds and categories with no unread entries</Trans>}
|
||||
checked={showRead}
|
||||
onChange={async e => await dispatch(changeShowRead(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))}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label={<Trans>Show confirmation when marking all entries as read</Trans>}
|
||||
checked={markAllAsReadConfirmation}
|
||||
@@ -82,10 +71,36 @@ export function DisplaySettings() {
|
||||
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" />
|
||||
|
||||
<SimpleGrid cols={2}>
|
||||
{(Object.keys(Constants.sharing) as Array<keyof SharingSettings>).map(site => (
|
||||
{(Object.keys(Constants.sharing) as (keyof SharingSettings)[]).map(site => (
|
||||
<Switch
|
||||
key={site}
|
||||
label={Constants.sharing[site].label}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Box, Center, type MantineTheme, useMantineTheme } from "@mantine/core"
|
||||
import { Box, Center } from "@mantine/core"
|
||||
import { FeedFavicon } from "components/content/FeedFavicon"
|
||||
import { useColorScheme } from "hooks/useColorScheme"
|
||||
import React, { type ReactNode } from "react"
|
||||
import { tss } from "tss"
|
||||
import { UnreadCount } from "./UnreadCount"
|
||||
@@ -20,8 +19,6 @@ interface TreeNodeProps {
|
||||
|
||||
const useStyles = tss
|
||||
.withParams<{
|
||||
theme: MantineTheme
|
||||
colorScheme: "dark" | "light"
|
||||
selected: boolean
|
||||
hasError: boolean
|
||||
hasUnread: boolean
|
||||
@@ -60,11 +57,7 @@ const useStyles = tss
|
||||
})
|
||||
|
||||
export function TreeNode(props: TreeNodeProps) {
|
||||
const theme = useMantineTheme()
|
||||
const colorScheme = useColorScheme()
|
||||
const { classes } = useStyles({
|
||||
theme,
|
||||
colorScheme,
|
||||
selected: props.selected,
|
||||
hasError: props.hasError,
|
||||
hasUnread: props.unread > 0,
|
||||
|
||||
@@ -38,9 +38,8 @@ export const useWebSocket = () => {
|
||||
ws.onopen = () => dispatch(setWebSocketConnected(true))
|
||||
ws.onclose = () => dispatch(setWebSocketConnected(false))
|
||||
ws.onmessage = event => {
|
||||
const { data } = event
|
||||
if (typeof data === "string") {
|
||||
handleMessage(dispatch, data)
|
||||
if (typeof event.data === "string") {
|
||||
handleMessage(dispatch, event.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,8 +44,8 @@ export const locales: Locale[] = [
|
||||
|
||||
function activateLocale(locale: string) {
|
||||
// lingui
|
||||
import(`./locales/${locale}/messages.po`).then(data => {
|
||||
i18n.load(locale, data.messages as Messages)
|
||||
import(`./locales/${locale}/messages.po`).then((data: { messages: Messages }) => {
|
||||
i18n.load(locale, data.messages)
|
||||
i18n.activate(locale)
|
||||
})
|
||||
|
||||
|
||||
@@ -68,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
|
||||
@@ -407,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."
|
||||
@@ -541,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 "كلمة مرور جديدة"
|
||||
@@ -706,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
|
||||
|
||||
@@ -15,11 +15,11 @@ msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "<0>CommaFeed is an open-source project. Sources are hosted on </0><1>GitHub</1>."
|
||||
msgstr ""
|
||||
msgstr "<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>"
|
||||
@@ -27,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>"
|
||||
@@ -68,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."
|
||||
@@ -85,7 +85,7 @@ msgstr "Analitzar el feed"
|
||||
|
||||
#: src/components/AnnouncementDialog.tsx
|
||||
msgid "Announcement"
|
||||
msgstr ""
|
||||
msgstr "Anunci"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "API key"
|
||||
@@ -117,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."
|
||||
@@ -133,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
|
||||
@@ -173,15 +173,15 @@ msgstr "Comproveu que el canal funciona"
|
||||
|
||||
#: src/pages/app/Layout.tsx
|
||||
msgid "Close menu"
|
||||
msgstr ""
|
||||
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 ""
|
||||
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"
|
||||
@@ -189,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"
|
||||
@@ -201,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"
|
||||
@@ -213,7 +213,7 @@ msgstr "Acollidor"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Ctrl"
|
||||
msgstr ""
|
||||
msgstr "Ctrl"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Current password"
|
||||
@@ -221,19 +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 ""
|
||||
msgstr "Fosc"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Date created"
|
||||
@@ -258,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
|
||||
@@ -272,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"
|
||||
@@ -314,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}."
|
||||
@@ -331,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"
|
||||
@@ -345,7 +345,7 @@ msgstr "URL del canal"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Fetch all my feeds now"
|
||||
msgstr ""
|
||||
msgstr "Carrega tots els meus feeds ara"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Fever API"
|
||||
@@ -385,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"
|
||||
@@ -401,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."
|
||||
@@ -447,7 +451,7 @@ msgstr "últim missatge d'actualització"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Light"
|
||||
msgstr ""
|
||||
msgstr "Clar"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
@@ -515,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"
|
||||
@@ -541,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"
|
||||
@@ -576,7 +584,7 @@ msgstr "el més vell primer"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr ""
|
||||
msgstr "Al mòbil, mostra els botons d'acció a la part inferior de la pantalla"
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
@@ -584,7 +592,7 @@ 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"
|
||||
@@ -596,19 +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 ""
|
||||
msgstr "Obre el menú"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open next entry"
|
||||
@@ -624,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"
|
||||
@@ -670,7 +678,7 @@ msgstr "Posició"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Previous"
|
||||
msgstr ""
|
||||
msgstr "Anterior"
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Profile"
|
||||
@@ -696,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
|
||||
@@ -706,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
|
||||
@@ -753,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"
|
||||
@@ -777,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..."
|
||||
@@ -824,7 +840,7 @@ msgstr "Éxit"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Swipe header to the left"
|
||||
msgstr ""
|
||||
msgstr "Feu lliscar la capçalera cap a l'esquerra"
|
||||
|
||||
#: src/pages/WelcomePage.tsx
|
||||
msgid "Switch to dark theme"
|
||||
@@ -836,7 +852,7 @@ msgstr "Canvia al tema clar"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "System"
|
||||
msgstr ""
|
||||
msgstr "Sistema"
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
@@ -853,7 +869,7 @@ 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 ""
|
||||
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"
|
||||
@@ -861,11 +877,11 @@ 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 ""
|
||||
msgstr "Commuta l'estat destacat de l'entrada actual"
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||
@@ -873,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"
|
||||
@@ -912,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."
|
||||
|
||||
@@ -68,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
|
||||
@@ -407,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."
|
||||
@@ -541,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"
|
||||
@@ -706,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
|
||||
|
||||
@@ -68,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
|
||||
@@ -407,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."
|
||||
@@ -541,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"
|
||||
@@ -706,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
|
||||
|
||||
@@ -68,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
|
||||
@@ -407,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."
|
||||
@@ -541,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"
|
||||
@@ -706,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
|
||||
|
||||
@@ -68,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
|
||||
@@ -407,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."
|
||||
@@ -541,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"
|
||||
@@ -706,10 +714,18 @@ msgstr "Rechtsklick"
|
||||
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 "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
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
|
||||
@@ -68,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."
|
||||
@@ -407,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."
|
||||
@@ -541,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"
|
||||
@@ -706,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
|
||||
|
||||
@@ -68,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
|
||||
@@ -407,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."
|
||||
@@ -541,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"
|
||||
@@ -706,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
|
||||
|
||||
@@ -68,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
|
||||
@@ -407,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 گزارش دهید."
|
||||
@@ -541,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 "رمز عبور جدید"
|
||||
@@ -706,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
|
||||
|
||||
@@ -68,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
|
||||
@@ -407,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."
|
||||
@@ -541,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"
|
||||
@@ -706,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
|
||||
|
||||
@@ -68,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."
|
||||
@@ -407,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."
|
||||
@@ -541,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"
|
||||
@@ -576,7 +584,7 @@ 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 ""
|
||||
msgstr "Sur mobile, afficher les boutons d'action en bas de l'écran"
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
@@ -706,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
|
||||
|
||||
@@ -68,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
|
||||
@@ -407,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."
|
||||
@@ -541,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"
|
||||
@@ -706,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
|
||||
|
||||
@@ -68,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
|
||||
@@ -407,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."
|
||||
@@ -541,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ó"
|
||||
@@ -706,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
|
||||
|
||||
@@ -68,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
|
||||
@@ -407,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."
|
||||
@@ -541,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"
|
||||
@@ -706,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
|
||||
|
||||
@@ -68,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
|
||||
@@ -407,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."
|
||||
@@ -541,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"
|
||||
@@ -706,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
|
||||
|
||||
@@ -68,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
|
||||
@@ -407,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 "空でない場合は、'true' または '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 "問題が発生した場合は、GitHub プロジェクトの問題ページで報告してください。"
|
||||
@@ -541,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 "新しいパスワード"
|
||||
@@ -706,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
|
||||
|
||||
@@ -68,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
|
||||
@@ -407,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 "비어 있지 않은 경우 'true' 또는 '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 "문제가 발생하면 GitHub 프로젝트의 문제 페이지에서 보고하세요."
|
||||
@@ -541,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 "새 비밀번호"
|
||||
@@ -706,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
|
||||
|
||||
@@ -68,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
|
||||
@@ -407,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, ungkapan yang menilai kepada 'benar' atau 'palsu'. "
|
||||
|
||||
#: 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 menghadapi isu, sila laporkan pada halaman isu projek GitHub."
|
||||
@@ -541,6 +545,10 @@ msgstr "Nama"
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Navigasi ke langganan dengan memasukkan namanya"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "New password"
|
||||
msgstr "Kata laluan baharu"
|
||||
@@ -706,10 +714,18 @@ msgstr ""
|
||||
msgid "Save"
|
||||
msgstr "Jimat"
|
||||
|
||||
#: 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 "Tatal dengan lancar apabila menavigasi antara entri"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
|
||||
@@ -68,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
|
||||
@@ -407,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 uttrykk som vurderes til 'sant' eller 'usant'. "
|
||||
|
||||
#: 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øter på et problem, vennligst rapporter det på problemsiden til GitHub-prosjektet."
|
||||
@@ -541,6 +545,10 @@ msgstr "Navn"
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Naviger til et abonnement ved å skrive inn navnet"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "New password"
|
||||
msgstr "Nytt passord"
|
||||
@@ -706,10 +714,18 @@ msgstr ""
|
||||
msgid "Save"
|
||||
msgstr "Lagre"
|
||||
|
||||
#: 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 "Rull jevnt når du navigerer mellom oppføringer"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
|
||||
@@ -68,7 +68,7 @@ msgid "All"
|
||||
msgstr "Alles"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgid "Always"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
@@ -407,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 "Indien niet leeg, een uitdrukking die evalueert naar 'true' of '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 "Als je een probleem tegenkomt, meld dit dan op de pagina met problemen van het GitHub-project."
|
||||
@@ -541,6 +545,10 @@ msgstr "Naam"
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Navigeer naar een abonnement door de naam in te voeren"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "New password"
|
||||
msgstr "Nieuw wachtwoord"
|
||||
@@ -706,10 +714,18 @@ msgstr ""
|
||||
msgid "Save"
|
||||
msgstr "Opslaan"
|
||||
|
||||
#: 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 "Vloeiend scrollen bij het navigeren tussen items"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
|
||||
@@ -68,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
|
||||
@@ -407,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 uttrykk som vurderes til 'sant' eller 'usant'. "
|
||||
|
||||
#: 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øter på et problem, vennligst rapporter det på problemsiden til GitHub-prosjektet."
|
||||
@@ -541,6 +545,10 @@ msgstr "Navn"
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Naviger til et abonnement ved å skrive inn navnet"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "New password"
|
||||
msgstr "Nytt passord"
|
||||
@@ -706,10 +714,18 @@ msgstr ""
|
||||
msgid "Save"
|
||||
msgstr "Lagre"
|
||||
|
||||
#: 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 "Rull jevnt når du navigerer mellom oppføringer"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
|
||||
@@ -68,7 +68,7 @@ msgid "All"
|
||||
msgstr "Wszystkie"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgid "Always"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
@@ -407,6 +407,10 @@ msgstr "Identyfikator"
|
||||
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 "Jeśli nie jest puste, wyrażenie oceniające jako „prawda” lub „fałsz”. "
|
||||
|
||||
#: 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 "Jeśli napotkasz problem, zgłoś go na stronie problemów projektu GitHub."
|
||||
@@ -541,6 +545,10 @@ msgstr "Nazwa"
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Przejdź do subskrypcji, wpisując jej nazwę"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "New password"
|
||||
msgstr "Nowe hasło"
|
||||
@@ -706,10 +714,18 @@ msgstr ""
|
||||
msgid "Save"
|
||||
msgstr "Zapisz"
|
||||
|
||||
#: 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 "Przewijaj płynnie podczas nawigowania między wpisami"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
|
||||
@@ -68,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
|
||||
@@ -407,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 "Se não estiver vazio, uma expressão avaliada como 'true' ou '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 "Se você encontrar um problema, informe-o na página de problemas do projeto GitHub."
|
||||
@@ -541,6 +545,10 @@ msgstr "Nome"
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Navegue até uma assinatura digitando seu nome"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "New password"
|
||||
msgstr "Nova senha"
|
||||
@@ -706,10 +714,18 @@ msgstr ""
|
||||
msgid "Save"
|
||||
msgstr "Salvar"
|
||||
|
||||
#: 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 "Rolar 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
|
||||
|
||||
@@ -68,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
|
||||
@@ -407,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."
|
||||
@@ -541,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 "Новый пароль"
|
||||
@@ -706,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
|
||||
|
||||
@@ -68,7 +68,7 @@ msgid "All"
|
||||
msgstr "Všetky"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgid "Always"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
@@ -407,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 "Ak nie je prázdny, výraz vyhodnotený ako 'pravda' alebo 'nepravda'. "
|
||||
|
||||
#: 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 "Ak narazíte na problém, nahláste ho na stránke problémov projektu GitHub."
|
||||
@@ -541,6 +545,10 @@ msgstr "Meno"
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Prejdite na predplatné zadaním jeho názvu"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "New password"
|
||||
msgstr "Nové heslo"
|
||||
@@ -706,10 +714,18 @@ msgstr ""
|
||||
msgid "Save"
|
||||
msgstr "Uložiť"
|
||||
|
||||
#: 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 "Pri navigácii medzi položkami plynulo rolujte"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
|
||||
@@ -68,7 +68,7 @@ msgid "All"
|
||||
msgstr "Alla"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgid "Always"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
@@ -407,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 "Om det inte är tomt, ett uttryck som utvärderas till 'sant' eller 'falskt'. "
|
||||
|
||||
#: 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 "Om du stöter på ett problem, vänligen rapportera det på problemsidan för GitHub-projektet."
|
||||
@@ -541,6 +545,10 @@ msgstr "Namn"
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Navigera till ett abonnemang genom att ange dess namn"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "New password"
|
||||
msgstr "Nytt lösenord"
|
||||
@@ -706,10 +714,18 @@ msgstr ""
|
||||
msgid "Save"
|
||||
msgstr "Spara"
|
||||
|
||||
#: 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 "Bläddra mjukt när du navigerar mellan poster"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
|
||||
@@ -68,8 +68,8 @@ msgid "All"
|
||||
msgstr "Tümü"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always scroll selected entry to the top of the page, even if it fits entirely on screen"
|
||||
msgstr "Seçilen girişi her zaman sayfanın üstüne kaydır, ekrana tamamen sığsa bile"
|
||||
msgid "Always"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "An email has been sent if this address was registered. Check your inbox."
|
||||
@@ -407,6 +407,10 @@ msgstr "Kimlik"
|
||||
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 "Boş değilse, 'doğru' veya 'yanlış' olarak değerlendirilen bir ifade. "
|
||||
|
||||
#: 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 "Bir sorunla karşılaşırsanız lütfen GitHub projesinin sorunlar sayfasında bildirin."
|
||||
@@ -541,6 +545,10 @@ msgstr "İsim"
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Adını girerek bir aboneliğe gidin"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "New password"
|
||||
msgstr "Yeni şifre"
|
||||
@@ -706,10 +714,18 @@ msgstr "Sağ tık"
|
||||
msgid "Save"
|
||||
msgstr "Kaydet"
|
||||
|
||||
#: 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 "Girişler arasında gezinirken sorunsuz ilerleyin"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
|
||||
@@ -68,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
|
||||
@@ -407,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项目的issues页面上报告。"
|
||||
@@ -541,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 "新密码"
|
||||
@@ -706,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
|
||||
|
||||
@@ -1,44 +1,39 @@
|
||||
import { Trans } from "@lingui/macro"
|
||||
import { Box, Button, Container, Group, type MantineTheme, Text, Title, useMantineTheme } from "@mantine/core"
|
||||
import { Box, Button, Container, Group, Text, Title } from "@mantine/core"
|
||||
import { TbRefresh } from "react-icons/tb"
|
||||
import { tss } from "tss"
|
||||
import { PageTitle } from "./PageTitle"
|
||||
|
||||
const useStyles = tss
|
||||
.withParams<{
|
||||
theme: MantineTheme
|
||||
}>()
|
||||
.create(({ theme }) => ({
|
||||
root: {
|
||||
paddingTop: 80,
|
||||
},
|
||||
const useStyles = tss.create(({ theme }) => ({
|
||||
root: {
|
||||
paddingTop: 80,
|
||||
},
|
||||
|
||||
label: {
|
||||
textAlign: "center",
|
||||
fontWeight: "bold",
|
||||
fontSize: 120,
|
||||
lineHeight: 1,
|
||||
marginBottom: `calc(${theme.spacing.xl} * 1.5)`,
|
||||
color: theme.colors[theme.primaryColor][3],
|
||||
},
|
||||
label: {
|
||||
textAlign: "center",
|
||||
fontWeight: "bold",
|
||||
fontSize: 120,
|
||||
lineHeight: 1,
|
||||
marginBottom: `calc(${theme.spacing.xl} * 1.5)`,
|
||||
color: theme.colors[theme.primaryColor][3],
|
||||
},
|
||||
|
||||
title: {
|
||||
textAlign: "center",
|
||||
fontWeight: "bold",
|
||||
fontSize: 32,
|
||||
},
|
||||
title: {
|
||||
textAlign: "center",
|
||||
fontWeight: "bold",
|
||||
fontSize: 32,
|
||||
},
|
||||
|
||||
description: {
|
||||
maxWidth: 540,
|
||||
margin: "auto",
|
||||
marginTop: theme.spacing.xl,
|
||||
marginBottom: `calc(${theme.spacing.xl} * 1.5)`,
|
||||
},
|
||||
}))
|
||||
description: {
|
||||
maxWidth: 540,
|
||||
margin: "auto",
|
||||
marginTop: theme.spacing.xl,
|
||||
marginBottom: `calc(${theme.spacing.xl} * 1.5)`,
|
||||
},
|
||||
}))
|
||||
|
||||
export function ErrorPage(props: { error: Error }) {
|
||||
const theme = useMantineTheme()
|
||||
const { classes } = useStyles({ theme })
|
||||
const { classes } = useStyles()
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
|
||||
@@ -3,8 +3,8 @@ import { Anchor, Box, Center, Container, Divider, Group, Image, Space, Title, us
|
||||
import { client } from "app/client"
|
||||
import { redirectToApiDocumentation, redirectToLogin, redirectToRegistration, redirectToRootCategory } from "app/redirect/thunks"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import welcome_page_dark from "assets/welcome_page_dark.png"
|
||||
import welcome_page_light from "assets/welcome_page_light.png"
|
||||
import welcomePageDark from "assets/welcome_page_dark.png"
|
||||
import welcomePageLight from "assets/welcome_page_light.png"
|
||||
import { ActionButton } from "components/ActionButton"
|
||||
import { useBrowserExtension } from "hooks/useBrowserExtension"
|
||||
import { useMobile } from "hooks/useMobile"
|
||||
@@ -19,7 +19,7 @@ export function WelcomePage() {
|
||||
const serverInfos = useAppSelector(state => state.server.serverInfos)
|
||||
const { colorScheme } = useMantineColorScheme()
|
||||
const dispatch = useAppDispatch()
|
||||
const image = colorScheme === "light" ? welcome_page_light : welcome_page_dark
|
||||
const image = colorScheme === "light" ? welcomePageLight : welcomePageDark
|
||||
|
||||
const login = useAsyncCallback(client.user.login, {
|
||||
onSuccess: () => {
|
||||
|
||||
@@ -108,7 +108,7 @@ export function AdminUsersPage() {
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{users?.map(u => (
|
||||
{users.map(u => (
|
||||
<Table.Tr key={u.id}>
|
||||
<Table.Td>{u.id}</Table.Td>
|
||||
<Table.Td>{u.name}</Table.Td>
|
||||
|
||||
@@ -14,7 +14,7 @@ const shownMeters: Record<string, string> = {
|
||||
"com.commafeed.backend.feed.FeedRefreshUpdater.feedUpdated": "Feed update rate",
|
||||
"com.commafeed.backend.feed.FeedRefreshUpdater.entryCacheHit": "Entry cache hit rate",
|
||||
"com.commafeed.backend.feed.FeedRefreshUpdater.entryCacheMiss": "Entry cache miss rate",
|
||||
"com.commafeed.backend.service.DatabaseCleaningService.entriesDeleted": "Entries deleted",
|
||||
"com.commafeed.backend.service.db.DatabaseCleaningService.entriesDeleted": "Entries deleted",
|
||||
}
|
||||
|
||||
const shownGauges: Record<string, string> = {
|
||||
|
||||
@@ -3,7 +3,9 @@ import { HistoryService, RedocStandalone } from "redoc"
|
||||
|
||||
// disable redoc url sync because it causes issues with hashrouter
|
||||
Object.defineProperty(HistoryService.prototype, "replace", {
|
||||
value: () => {},
|
||||
value: () => {
|
||||
// do nothing
|
||||
},
|
||||
})
|
||||
|
||||
function ApiDocumentationPage() {
|
||||
|
||||
@@ -49,11 +49,17 @@ export function FeedEntriesPage(props: FeedEntriesPageProps) {
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const titleClicked = () => {
|
||||
if (props.sourceType === "category") {
|
||||
dispatch(redirectToCategoryDetails(id))
|
||||
} else if (props.sourceType === "feed") {
|
||||
dispatch(redirectToFeedDetails(id))
|
||||
} else if (props.sourceType === "tag") dispatch(redirectToTagDetails(id))
|
||||
switch (props.sourceType) {
|
||||
case "category":
|
||||
dispatch(redirectToCategoryDetails(id))
|
||||
break
|
||||
case "feed":
|
||||
dispatch(redirectToFeedDetails(id))
|
||||
break
|
||||
case "tag":
|
||||
dispatch(redirectToTagDetails(id))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { useMantineTheme } from "@mantine/core"
|
||||
import { useColorScheme } from "hooks/useColorScheme"
|
||||
import { createTss } from "tss-react"
|
||||
|
||||
const useContext = () => {
|
||||
// return anything here that will be accessible in tss.create()
|
||||
// we don't need anything right now
|
||||
return {}
|
||||
|
||||
const theme = useMantineTheme()
|
||||
const colorScheme = useColorScheme()
|
||||
|
||||
return { theme, colorScheme }
|
||||
}
|
||||
|
||||
export const { tss } = createTss({ useContext })
|
||||
|
||||
export const useStyles = tss.create({})
|
||||
|
||||
@@ -30,6 +30,7 @@ export default defineConfig(env => ({
|
||||
"/openapi.json": "http://localhost:8083",
|
||||
"/custom_css.css": "http://localhost:8083",
|
||||
"/custom_js.js": "http://localhost:8083",
|
||||
"/logout": "http://localhost:8083",
|
||||
},
|
||||
},
|
||||
build: {
|
||||
|
||||
@@ -95,11 +95,11 @@ app:
|
||||
# -------------------
|
||||
# for MariaDB
|
||||
# driverClass is org.mariadb.jdbc.Driver
|
||||
# url is jdbc:mariadb://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true
|
||||
# url is jdbc:mariadb://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC
|
||||
#
|
||||
# for MySQL
|
||||
# driverClass is com.mysql.cj.jdbc.Driver
|
||||
# url is jdbc:mysql://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true
|
||||
# url is jdbc:mysql://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC
|
||||
#
|
||||
# for PostgreSQL
|
||||
# driverClass is org.postgresql.Driver
|
||||
|
||||
@@ -95,11 +95,11 @@ app:
|
||||
# -------------------
|
||||
# for MariaDB
|
||||
# driverClass is org.mariadb.jdbc.Driver
|
||||
# url is jdbc:mariadb://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true
|
||||
# url is jdbc:mariadb://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC
|
||||
#
|
||||
# for MySQL
|
||||
# driverClass is com.mysql.cj.jdbc.Driver
|
||||
# url is jdbc:mysql://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true
|
||||
# url is jdbc:mysql://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC
|
||||
#
|
||||
# for PostgreSQL
|
||||
# driverClass is org.postgresql.Driver
|
||||
|
||||
@@ -6,14 +6,14 @@
|
||||
<parent>
|
||||
<groupId>com.commafeed</groupId>
|
||||
<artifactId>commafeed</artifactId>
|
||||
<version>4.2.0</version>
|
||||
<version>4.3.3</version>
|
||||
</parent>
|
||||
<artifactId>commafeed-server</artifactId>
|
||||
<name>CommaFeed Server</name>
|
||||
|
||||
<properties>
|
||||
<guice.version>7.0.0</guice.version>
|
||||
<querydsl.version>5.0.0</querydsl.version>
|
||||
<querydsl.version>5.1.0</querydsl.version>
|
||||
<rome.version>2.1.0</rome.version>
|
||||
</properties>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-dependencies</artifactId>
|
||||
<version>4.0.5</version>
|
||||
<version>4.0.6</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
@@ -36,12 +36,12 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.2.3</version>
|
||||
<version>3.2.5</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-failsafe-plugin</artifactId>
|
||||
<version>3.2.3</version>
|
||||
<version>3.2.5</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
@@ -72,7 +72,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.5.1</version>
|
||||
<version>3.5.2</version>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.kordamp.shade</groupId>
|
||||
@@ -121,7 +121,7 @@
|
||||
<plugin>
|
||||
<groupId>io.swagger.core.v3</groupId>
|
||||
<artifactId>swagger-maven-plugin-jakarta</artifactId>
|
||||
<version>2.2.19</version>
|
||||
<version>2.2.20</version>
|
||||
<?m2e ignore?>
|
||||
<configuration>
|
||||
<outputPath>${project.build.directory}/classes/assets</outputPath>
|
||||
@@ -184,7 +184,7 @@
|
||||
<plugin>
|
||||
<groupId>com.diffplug.spotless</groupId>
|
||||
<artifactId>spotless-maven-plugin</artifactId>
|
||||
<version>2.41.1</version>
|
||||
<version>2.43.0</version>
|
||||
<?m2e ignore?>
|
||||
<executions>
|
||||
<execution>
|
||||
@@ -211,7 +211,7 @@
|
||||
<dependency>
|
||||
<groupId>com.commafeed</groupId>
|
||||
<artifactId>commafeed-client</artifactId>
|
||||
<version>4.2.0</version>
|
||||
<version>4.3.3</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -239,6 +239,10 @@
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-unix-socket</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-hibernate</artifactId>
|
||||
@@ -276,7 +280,7 @@
|
||||
<dependency>
|
||||
<groupId>io.swagger.core.v3</groupId>
|
||||
<artifactId>swagger-annotations</artifactId>
|
||||
<version>2.2.19</version>
|
||||
<version>2.2.20</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -332,7 +336,7 @@
|
||||
<dependency>
|
||||
<groupId>redis.clients</groupId>
|
||||
<artifactId>jedis</artifactId>
|
||||
<version>5.1.0</version>
|
||||
<version>5.1.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.sun.mail</groupId>
|
||||
@@ -364,7 +368,7 @@
|
||||
<dependency>
|
||||
<groupId>org.jsoup</groupId>
|
||||
<artifactId>jsoup</artifactId>
|
||||
<version>1.17.1</version>
|
||||
<version>1.17.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.ibm.icu</groupId>
|
||||
@@ -384,7 +388,7 @@
|
||||
<dependency>
|
||||
<groupId>org.gwtproject</groupId>
|
||||
<artifactId>gwt-servlet</artifactId>
|
||||
<version>2.10.0</version>
|
||||
<version>2.11.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents.client5</groupId>
|
||||
@@ -393,35 +397,39 @@
|
||||
<dependency>
|
||||
<groupId>io.github.hakky54</groupId>
|
||||
<artifactId>sslcontext-kickstart-for-apache5</artifactId>
|
||||
<version>8.2.0</version>
|
||||
<version>8.3.2</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.google.apis</groupId>
|
||||
<artifactId>google-api-services-youtube</artifactId>
|
||||
<version>v3-rev20231011-2.0.0</version>
|
||||
<version>v3-rev20240225-2.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<!-- stay on 2.1 because 2.2 file format changed to version '3' -->
|
||||
<version>2.1.214</version><!--$NO-MVN-MAN-VER$ -->
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.manticore-projects.tools</groupId>
|
||||
<artifactId>h2migrationtool</artifactId>
|
||||
<version>1.4</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<version>8.2.0</version>
|
||||
<version>8.3.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mariadb.jdbc</groupId>
|
||||
<artifactId>mariadb-java-client</artifactId>
|
||||
<version>3.3.1</version>
|
||||
<version>3.3.3</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<version>42.7.1</version>
|
||||
<version>42.7.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.sourceforge.jtds</groupId>
|
||||
@@ -482,7 +490,7 @@
|
||||
<dependency>
|
||||
<groupId>com.microsoft.playwright</groupId>
|
||||
<artifactId>playwright</artifactId>
|
||||
<version>1.40.0</version>
|
||||
<version>1.41.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.commafeed;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.Instant;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Set;
|
||||
@@ -24,8 +26,9 @@ import com.commafeed.backend.model.FeedSubscription;
|
||||
import com.commafeed.backend.model.User;
|
||||
import com.commafeed.backend.model.UserRole;
|
||||
import com.commafeed.backend.model.UserSettings;
|
||||
import com.commafeed.backend.service.DatabaseStartupService;
|
||||
import com.commafeed.backend.service.UserService;
|
||||
import com.commafeed.backend.service.db.DatabaseStartupService;
|
||||
import com.commafeed.backend.service.db.H2MigrationService;
|
||||
import com.commafeed.backend.task.ScheduledTask;
|
||||
import com.commafeed.frontend.auth.PasswordConstraintValidator;
|
||||
import com.commafeed.frontend.auth.SecurityCheckFactoryProvider;
|
||||
@@ -58,6 +61,7 @@ import io.dropwizard.configuration.DefaultConfigurationFactoryFactory;
|
||||
import io.dropwizard.configuration.EnvironmentVariableSubstitutor;
|
||||
import io.dropwizard.configuration.SubstitutingSourceProvider;
|
||||
import io.dropwizard.core.Application;
|
||||
import io.dropwizard.core.ConfiguredBundle;
|
||||
import io.dropwizard.core.setup.Bootstrap;
|
||||
import io.dropwizard.core.setup.Environment;
|
||||
import io.dropwizard.db.DataSourceFactory;
|
||||
@@ -93,6 +97,30 @@ public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
|
||||
configureEnvironmentSubstitutor(bootstrap);
|
||||
configureObjectMapper(bootstrap.getObjectMapper());
|
||||
|
||||
// run h2 migration as the first bundle because we need to migrate before hibernate is initialized
|
||||
bootstrap.addBundle(new ConfiguredBundle<CommaFeedConfiguration>() {
|
||||
@Override
|
||||
public void run(CommaFeedConfiguration config, Environment environment) throws Exception {
|
||||
DataSourceFactory dataSourceFactory = config.getDataSourceFactory();
|
||||
String url = dataSourceFactory.getUrl();
|
||||
if (isFileBasedH2(url)) {
|
||||
Path path = getFilePath(url);
|
||||
String user = dataSourceFactory.getUser();
|
||||
String password = dataSourceFactory.getPassword();
|
||||
new H2MigrationService().migrateIfNeeded(path, user, password);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isFileBasedH2(String url) {
|
||||
return url.startsWith("jdbc:h2:") && !url.startsWith("jdbc:h2:mem:");
|
||||
}
|
||||
|
||||
private Path getFilePath(String url) {
|
||||
String name = url.substring("jdbc:h2:".length()).split(";")[0];
|
||||
return Paths.get(name + ".mv.db");
|
||||
}
|
||||
});
|
||||
|
||||
bootstrap.addBundle(hibernateBundle = new HibernateBundle<>(AbstractModel.class, Feed.class, FeedCategory.class, FeedEntry.class,
|
||||
FeedEntryContent.class, FeedEntryStatus.class, FeedEntryTag.class, FeedSubscription.class, User.class, UserRole.class,
|
||||
UserSettings.class) {
|
||||
|
||||
@@ -26,8 +26,8 @@ public class FeedEntryDAO extends GenericDAO<FeedEntry> {
|
||||
super(sessionFactory);
|
||||
}
|
||||
|
||||
public Long findExisting(String guidHash, Feed feed) {
|
||||
return query().select(entry.id).from(entry).where(entry.guidHash.eq(guidHash), entry.feed.eq(feed)).limit(1).fetchOne();
|
||||
public FeedEntry findExisting(String guidHash, Feed feed) {
|
||||
return query().select(entry).from(entry).where(entry.guidHash.eq(guidHash), entry.feed.eq(feed)).limit(1).fetchOne();
|
||||
}
|
||||
|
||||
public List<FeedCapacity> findFeedsExceedingCapacity(long maxCapacity, long max) {
|
||||
|
||||
@@ -3,8 +3,11 @@ package com.commafeed.backend.feed;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
@@ -20,7 +23,9 @@ import com.commafeed.backend.dao.UnitOfWork;
|
||||
import com.commafeed.backend.feed.parser.FeedParserResult.Content;
|
||||
import com.commafeed.backend.feed.parser.FeedParserResult.Entry;
|
||||
import com.commafeed.backend.model.Feed;
|
||||
import com.commafeed.backend.model.FeedEntry;
|
||||
import com.commafeed.backend.model.FeedSubscription;
|
||||
import com.commafeed.backend.model.Models;
|
||||
import com.commafeed.backend.model.User;
|
||||
import com.commafeed.backend.service.FeedEntryService;
|
||||
import com.commafeed.backend.service.FeedService;
|
||||
@@ -75,6 +80,7 @@ public class FeedRefreshUpdater {
|
||||
private AddEntryResult addEntry(final Feed feed, final Entry entry, final List<FeedSubscription> subscriptions) {
|
||||
boolean processed = false;
|
||||
boolean inserted = false;
|
||||
Set<FeedSubscription> subscriptionsForWhichEntryIsUnread = new HashSet<>();
|
||||
|
||||
// lock on feed, make sure we are not updating the same feed twice at
|
||||
// the same time
|
||||
@@ -96,10 +102,21 @@ public class FeedRefreshUpdater {
|
||||
locked2 = lock2.tryLock(1, TimeUnit.MINUTES);
|
||||
if (locked1 && locked2) {
|
||||
processed = true;
|
||||
inserted = unitOfWork.call(() -> feedEntryService.addEntry(feed, entry, subscriptions));
|
||||
if (inserted) {
|
||||
entryInserted.mark();
|
||||
}
|
||||
inserted = unitOfWork.call(() -> {
|
||||
Instant now = Instant.now();
|
||||
FeedEntry feedEntry = feedEntryService.findOrCreate(feed, entry);
|
||||
boolean newEntry = !feedEntry.getInserted().isBefore(now);
|
||||
if (newEntry) {
|
||||
entryInserted.mark();
|
||||
for (FeedSubscription sub : subscriptions) {
|
||||
boolean unread = feedEntryService.applyFilter(sub, feedEntry);
|
||||
if (unread) {
|
||||
subscriptionsForWhichEntryIsUnread.add(sub);
|
||||
}
|
||||
}
|
||||
}
|
||||
return newEntry;
|
||||
});
|
||||
} else {
|
||||
log.error("lock timeout for " + feed.getUrl() + " - " + key1);
|
||||
}
|
||||
@@ -113,12 +130,13 @@ public class FeedRefreshUpdater {
|
||||
lock2.unlock();
|
||||
}
|
||||
}
|
||||
return new AddEntryResult(processed, inserted);
|
||||
return new AddEntryResult(processed, inserted, subscriptionsForWhichEntryIsUnread);
|
||||
}
|
||||
|
||||
public boolean update(Feed feed, List<Entry> entries) {
|
||||
boolean processed = true;
|
||||
long inserted = 0;
|
||||
Map<FeedSubscription, Long> unreadCountBySubscription = new HashMap<>();
|
||||
|
||||
if (!entries.isEmpty()) {
|
||||
Set<String> lastEntries = cache.getLastEntries(feed);
|
||||
@@ -135,6 +153,7 @@ public class FeedRefreshUpdater {
|
||||
AddEntryResult addEntryResult = addEntry(feed, entry, subscriptions);
|
||||
processed &= addEntryResult.processed;
|
||||
inserted += addEntryResult.inserted ? 1 : 0;
|
||||
addEntryResult.subscriptionsForWhichEntryIsUnread.forEach(sub -> unreadCountBySubscription.merge(sub, 1L, Long::sum));
|
||||
|
||||
entryCacheMiss.mark();
|
||||
} else {
|
||||
@@ -153,13 +172,13 @@ public class FeedRefreshUpdater {
|
||||
cache.invalidateUnreadCount(subscriptions.toArray(new FeedSubscription[0]));
|
||||
cache.invalidateUserRootCategory(users.toArray(new User[0]));
|
||||
|
||||
notifyOverWebsocket(subscriptions, inserted);
|
||||
notifyOverWebsocket(unreadCountBySubscription);
|
||||
}
|
||||
}
|
||||
|
||||
if (!processed) {
|
||||
// requeue asap
|
||||
feed.setDisabledUntil(Instant.EPOCH);
|
||||
feed.setDisabledUntil(Models.MINIMUM_INSTANT);
|
||||
}
|
||||
|
||||
if (inserted > 0) {
|
||||
@@ -171,14 +190,16 @@ public class FeedRefreshUpdater {
|
||||
return processed;
|
||||
}
|
||||
|
||||
private void notifyOverWebsocket(List<FeedSubscription> subscriptions, long inserted) {
|
||||
subscriptions.forEach(sub -> webSocketSessions.sendMessage(sub.getUser(), WebSocketMessageBuilder.newFeedEntries(sub, inserted)));
|
||||
private void notifyOverWebsocket(Map<FeedSubscription, Long> unreadCountBySubscription) {
|
||||
unreadCountBySubscription.forEach((sub, unreadCount) -> webSocketSessions.sendMessage(sub.getUser(),
|
||||
WebSocketMessageBuilder.newFeedEntries(sub, unreadCount)));
|
||||
}
|
||||
|
||||
@AllArgsConstructor
|
||||
private static class AddEntryResult {
|
||||
private final boolean processed;
|
||||
private final boolean inserted;
|
||||
private final Set<FeedSubscription> subscriptionsForWhichEntryIsUnread;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,12 +1,24 @@
|
||||
package com.commafeed.backend.model;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
import org.hibernate.Hibernate;
|
||||
import org.hibernate.HibernateException;
|
||||
import org.hibernate.proxy.HibernateProxy;
|
||||
import org.hibernate.proxy.LazyInitializer;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
@UtilityClass
|
||||
public class Models {
|
||||
|
||||
public static final Instant MINIMUM_INSTANT = Instant.EPOCH
|
||||
// mariadb timestamp range starts at 1970-01-01 00:00:01
|
||||
.plusSeconds(1)
|
||||
// make sure the timestamp fits for all timezones
|
||||
.plus(Duration.ofHours(24));
|
||||
|
||||
/**
|
||||
* initialize a proxy
|
||||
*/
|
||||
@@ -18,8 +30,8 @@ public class Models {
|
||||
* extract the id from the proxy without initializing it
|
||||
*/
|
||||
public static Long getId(AbstractModel model) {
|
||||
if (model instanceof HibernateProxy) {
|
||||
LazyInitializer lazyInitializer = ((HibernateProxy) model).getHibernateLazyInitializer();
|
||||
if (model instanceof HibernateProxy proxy) {
|
||||
LazyInitializer lazyInitializer = proxy.getHibernateLazyInitializer();
|
||||
if (lazyInitializer.isUninitialized()) {
|
||||
return (Long) lazyInitializer.getIdentifier();
|
||||
}
|
||||
|
||||
@@ -35,6 +35,10 @@ public class UserSettings extends AbstractModel {
|
||||
title, cozy, detailed, expanded
|
||||
}
|
||||
|
||||
public enum ScrollMode {
|
||||
always, never, if_needed
|
||||
}
|
||||
|
||||
@OneToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "user_id", nullable = false, unique = true)
|
||||
private User user;
|
||||
@@ -66,7 +70,10 @@ public class UserSettings extends AbstractModel {
|
||||
@Column(name = "scroll_speed")
|
||||
private int scrollSpeed;
|
||||
|
||||
private boolean alwaysScrollToEntry;
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false)
|
||||
private ScrollMode scrollMode;
|
||||
|
||||
private boolean markAllAsReadConfirmation;
|
||||
private boolean customContextMenu;
|
||||
private boolean mobileFooter;
|
||||
|
||||
@@ -17,6 +17,7 @@ import com.commafeed.backend.model.FeedEntry;
|
||||
import com.commafeed.backend.model.FeedEntryStatus;
|
||||
import com.commafeed.backend.model.FeedSubscription;
|
||||
import com.commafeed.backend.model.User;
|
||||
import com.commafeed.backend.service.FeedEntryFilteringService.FeedEntryFilterException;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
@@ -38,33 +39,34 @@ public class FeedEntryService {
|
||||
/**
|
||||
* this is NOT thread-safe
|
||||
*/
|
||||
public boolean addEntry(Feed feed, Entry entry, List<FeedSubscription> subscriptions) {
|
||||
public FeedEntry findOrCreate(Feed feed, Entry entry) {
|
||||
String guid = FeedUtils.truncate(entry.guid(), 2048);
|
||||
String guidHash = DigestUtils.sha1Hex(entry.guid());
|
||||
Long existing = feedEntryDAO.findExisting(guidHash, feed);
|
||||
FeedEntry existing = feedEntryDAO.findExisting(guidHash, feed);
|
||||
if (existing != null) {
|
||||
return false;
|
||||
return existing;
|
||||
}
|
||||
|
||||
FeedEntry feedEntry = buildEntry(feed, entry, guid, guidHash);
|
||||
feedEntryDAO.saveOrUpdate(feedEntry);
|
||||
return feedEntry;
|
||||
}
|
||||
|
||||
// if filter does not match the entry, mark it as read
|
||||
for (FeedSubscription sub : subscriptions) {
|
||||
boolean matches = true;
|
||||
try {
|
||||
matches = feedEntryFilteringService.filterMatchesEntry(sub.getFilter(), feedEntry);
|
||||
} catch (FeedEntryFilteringService.FeedEntryFilterException e) {
|
||||
log.error("could not evaluate filter {}", sub.getFilter(), e);
|
||||
}
|
||||
if (!matches) {
|
||||
FeedEntryStatus status = new FeedEntryStatus(sub.getUser(), sub, feedEntry);
|
||||
status.setRead(true);
|
||||
feedEntryStatusDAO.saveOrUpdate(status);
|
||||
}
|
||||
public boolean applyFilter(FeedSubscription sub, FeedEntry entry) {
|
||||
boolean matches = true;
|
||||
try {
|
||||
matches = feedEntryFilteringService.filterMatchesEntry(sub.getFilter(), entry);
|
||||
} catch (FeedEntryFilterException e) {
|
||||
log.error("could not evaluate filter {}", sub.getFilter(), e);
|
||||
}
|
||||
|
||||
return true;
|
||||
if (!matches) {
|
||||
FeedEntryStatus status = new FeedEntryStatus(sub.getUser(), sub, entry);
|
||||
status.setRead(true);
|
||||
feedEntryStatusDAO.saveOrUpdate(status);
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
private FeedEntry buildEntry(Feed feed, Entry e, String guid, String guidHash) {
|
||||
|
||||
@@ -12,6 +12,7 @@ import com.commafeed.backend.favicon.AbstractFaviconFetcher;
|
||||
import com.commafeed.backend.favicon.Favicon;
|
||||
import com.commafeed.backend.feed.FeedUtils;
|
||||
import com.commafeed.backend.model.Feed;
|
||||
import com.commafeed.backend.model.Models;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
@@ -45,7 +46,7 @@ public class FeedService {
|
||||
feed.setUrl(url);
|
||||
feed.setNormalizedUrl(normalizedUrl);
|
||||
feed.setNormalizedUrlHash(normalizedUrlHash);
|
||||
feed.setDisabledUntil(Instant.EPOCH);
|
||||
feed.setDisabledUntil(Models.MINIMUM_INSTANT);
|
||||
feedDAO.saveOrUpdate(feed);
|
||||
}
|
||||
return feed;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.commafeed.backend.service;
|
||||
package com.commafeed.backend.service.db;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.commafeed.backend.service;
|
||||
package com.commafeed.backend.service.db;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
@@ -9,6 +9,7 @@ import org.hibernate.SessionFactory;
|
||||
import com.commafeed.CommaFeedConfiguration;
|
||||
import com.commafeed.backend.dao.UnitOfWork;
|
||||
import com.commafeed.backend.dao.UserDAO;
|
||||
import com.commafeed.backend.service.UserService;
|
||||
|
||||
import io.dropwizard.lifecycle.Managed;
|
||||
import jakarta.inject.Inject;
|
||||
@@ -0,0 +1,84 @@
|
||||
package com.commafeed.backend.service.db;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.manticore.h2.H2MigrationTool;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class H2MigrationService {
|
||||
|
||||
public void migrateIfNeeded(Path path, String user, String password) {
|
||||
if (Files.notExists(path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int format;
|
||||
try {
|
||||
format = getH2FileFormat(path);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("could not detect H2 format", e);
|
||||
}
|
||||
|
||||
if (format == 2) {
|
||||
try {
|
||||
migrate(path, user, password, "2.1.214", "2.2.224");
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("could not migrate H2 to format 3", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int getH2FileFormat(Path path) throws IOException {
|
||||
try (BufferedReader reader = Files.newBufferedReader(path)) {
|
||||
String headers = reader.readLine();
|
||||
|
||||
return Stream.of(headers.split(","))
|
||||
.filter(h -> h.startsWith("format:"))
|
||||
.map(h -> h.split(":")[1])
|
||||
.map(Integer::parseInt)
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new RuntimeException("could not find format in H2 file headers"));
|
||||
}
|
||||
}
|
||||
|
||||
private void migrate(Path path, String user, String password, String fromVersion, String toVersion) throws Exception {
|
||||
log.info("migrating H2 database at {} from format {} to format {}", path, fromVersion, toVersion);
|
||||
|
||||
Path scriptPath = path.resolveSibling("script-" + System.currentTimeMillis() + ".sql");
|
||||
Path newVersionPath = path.resolveSibling(path.getFileName() + "." + getPatchVersion(toVersion) + ".mv.db");
|
||||
Path oldVersionBackupPath = path.resolveSibling(path.getFileName() + "." + getPatchVersion(fromVersion) + ".backup");
|
||||
|
||||
Files.deleteIfExists(scriptPath);
|
||||
Files.deleteIfExists(newVersionPath);
|
||||
Files.deleteIfExists(oldVersionBackupPath);
|
||||
|
||||
H2MigrationTool.readDriverRecords();
|
||||
new H2MigrationTool().migrate(fromVersion, toVersion, path.toAbsolutePath().toString(), user, password,
|
||||
scriptPath.toAbsolutePath().toString(), "", "", false, false, "");
|
||||
if (!Files.exists(newVersionPath)) {
|
||||
throw new RuntimeException("H2 migration failed, new version file not found");
|
||||
}
|
||||
|
||||
Files.move(path, oldVersionBackupPath);
|
||||
Files.move(newVersionPath, path);
|
||||
Files.delete(oldVersionBackupPath);
|
||||
Files.delete(scriptPath);
|
||||
|
||||
log.info("migrated H2 database from format {} to format {}", fromVersion, toVersion);
|
||||
}
|
||||
|
||||
private String getPatchVersion(String version) {
|
||||
return StringUtils.substringAfterLast(version, ".");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package com.commafeed.backend.task;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import com.commafeed.CommaFeedConfiguration;
|
||||
import com.commafeed.backend.service.DatabaseCleaningService;
|
||||
import com.commafeed.backend.service.db.DatabaseCleaningService;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
|
||||
@@ -5,7 +5,7 @@ import java.time.Instant;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import com.commafeed.CommaFeedConfiguration;
|
||||
import com.commafeed.backend.service.DatabaseCleaningService;
|
||||
import com.commafeed.backend.service.db.DatabaseCleaningService;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
|
||||
@@ -4,7 +4,7 @@ import java.time.Instant;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import com.commafeed.CommaFeedConfiguration;
|
||||
import com.commafeed.backend.service.DatabaseCleaningService;
|
||||
import com.commafeed.backend.service.db.DatabaseCleaningService;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
|
||||
@@ -2,7 +2,7 @@ package com.commafeed.backend.task;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import com.commafeed.backend.service.DatabaseCleaningService;
|
||||
import com.commafeed.backend.service.db.DatabaseCleaningService;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
|
||||
@@ -2,7 +2,7 @@ package com.commafeed.backend.task;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import com.commafeed.backend.service.DatabaseCleaningService;
|
||||
import com.commafeed.backend.service.db.DatabaseCleaningService;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
|
||||
@@ -42,9 +42,10 @@ public class Settings implements Serializable {
|
||||
private int scrollSpeed;
|
||||
|
||||
@Schema(
|
||||
description = "always scroll selected entry to the top of the page, even if it fits entirely on screen",
|
||||
description = "whether to scroll to the selected entry",
|
||||
allowableValues = "always,never,if_needed",
|
||||
requiredMode = RequiredMode.REQUIRED)
|
||||
private boolean alwaysScrollToEntry;
|
||||
private String scrollMode;
|
||||
|
||||
@Schema(description = "ask for confirmation when marking all entries as read", requiredMode = RequiredMode.REQUIRED)
|
||||
private boolean markAllAsReadConfirmation;
|
||||
|
||||
@@ -24,6 +24,7 @@ import com.commafeed.backend.model.UserRole.Role;
|
||||
import com.commafeed.backend.model.UserSettings;
|
||||
import com.commafeed.backend.model.UserSettings.ReadingMode;
|
||||
import com.commafeed.backend.model.UserSettings.ReadingOrder;
|
||||
import com.commafeed.backend.model.UserSettings.ScrollMode;
|
||||
import com.commafeed.backend.service.MailService;
|
||||
import com.commafeed.backend.service.PasswordEncryptionService;
|
||||
import com.commafeed.backend.service.UserService;
|
||||
@@ -108,7 +109,7 @@ public class UserREST {
|
||||
s.setCustomJs(settings.getCustomJs());
|
||||
s.setLanguage(settings.getLanguage());
|
||||
s.setScrollSpeed(settings.getScrollSpeed());
|
||||
s.setAlwaysScrollToEntry(settings.isAlwaysScrollToEntry());
|
||||
s.setScrollMode(settings.getScrollMode().name());
|
||||
s.setMarkAllAsReadConfirmation(settings.isMarkAllAsReadConfirmation());
|
||||
s.setCustomContextMenu(settings.isCustomContextMenu());
|
||||
s.setMobileFooter(settings.isMobileFooter());
|
||||
@@ -129,7 +130,7 @@ public class UserREST {
|
||||
s.setScrollMarks(true);
|
||||
s.setLanguage("en");
|
||||
s.setScrollSpeed(400);
|
||||
s.setAlwaysScrollToEntry(false);
|
||||
s.setScrollMode(ScrollMode.if_needed.name());
|
||||
s.setMarkAllAsReadConfirmation(true);
|
||||
s.setCustomContextMenu(true);
|
||||
s.setMobileFooter(false);
|
||||
@@ -158,7 +159,7 @@ public class UserREST {
|
||||
s.setCustomJs(CommaFeedApplication.USERNAME_DEMO.equals(user.getName()) ? "" : settings.getCustomJs());
|
||||
s.setLanguage(settings.getLanguage());
|
||||
s.setScrollSpeed(settings.getScrollSpeed());
|
||||
s.setAlwaysScrollToEntry(settings.isAlwaysScrollToEntry());
|
||||
s.setScrollMode(ScrollMode.valueOf(settings.getScrollMode()));
|
||||
s.setMarkAllAsReadConfirmation(settings.isMarkAllAsReadConfirmation());
|
||||
s.setCustomContextMenu(settings.isCustomContextMenu());
|
||||
s.setMobileFooter(settings.isMobileFooter());
|
||||
|
||||
@@ -3,7 +3,6 @@ package com.commafeed.frontend.ws;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.codahale.metrics.Gauge;
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
@@ -38,13 +37,8 @@ public class WebSocketSessions {
|
||||
}
|
||||
|
||||
public void sendMessage(User user, String text) {
|
||||
Set<Session> userSessions = sessions.entrySet()
|
||||
.stream()
|
||||
.filter(e -> e.getKey().equals(user.getId()))
|
||||
.flatMap(e -> e.getValue().stream())
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
if (!userSessions.isEmpty()) {
|
||||
Set<Session> userSessions = sessions.get(user.getId());
|
||||
if (userSessions != null && !userSessions.isEmpty()) {
|
||||
log.debug("sending '{}' to {} users via websocket", text, userSessions.size());
|
||||
for (Session userSession : userSessions) {
|
||||
if (userSession.isOpen()) {
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
|
||||
|
||||
<changeSet id="convert-alwaysScrollToEntry-to-scrollMode" author="athou">
|
||||
<validCheckSum>9:663bcc7c6df5b832ec2109a3afcff5c6</validCheckSum>
|
||||
<addColumn tableName="USERSETTINGS">
|
||||
<column name="scrollMode" type="VARCHAR(32)" />
|
||||
</addColumn>
|
||||
<update tableName="USERSETTINGS">
|
||||
<column name="scrollMode" value="always" />
|
||||
<where>alwaysScrollToEntry = true</where>
|
||||
</update>
|
||||
<update tableName="USERSETTINGS">
|
||||
<column name="scrollMode" value="if_needed" />
|
||||
<where>alwaysScrollToEntry = false</where>
|
||||
</update>
|
||||
<addNotNullConstraint tableName="USERSETTINGS" columnName="scrollMode" columnDataType="VARCHAR(32)" />
|
||||
<dropColumn tableName="USERSETTINGS" columnName="alwaysScrollToEntry" />
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
@@ -29,5 +29,6 @@
|
||||
<include file="changelogs/db.changelog-4.0.xml" />
|
||||
<include file="changelogs/db.changelog-4.1.xml" />
|
||||
<include file="changelogs/db.changelog-4.2.xml" />
|
||||
<include file="changelogs/db.changelog-4.3.xml" />
|
||||
|
||||
</databaseChangeLog>
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.commafeed.backend.service.db;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
class H2MigrationServiceTest {
|
||||
|
||||
@TempDir
|
||||
private Path root;
|
||||
|
||||
@Test
|
||||
void testMigrateIfNeeded() throws IOException {
|
||||
Path path = root.resolve("database.mv.db");
|
||||
Files.copy(Objects.requireNonNull(getClass().getResourceAsStream("/h2-migration/database-v2.1.214.mv.db")), path);
|
||||
|
||||
H2MigrationService service = new H2MigrationService();
|
||||
Assertions.assertEquals(2, service.getH2FileFormat(path));
|
||||
|
||||
service.migrateIfNeeded(path, "sa", "sa");
|
||||
Assertions.assertEquals(3, service.getH2FileFormat(path));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.commafeed.frontend.ws;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Answers;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.commafeed.backend.model.User;
|
||||
|
||||
import jakarta.websocket.Session;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class WebSocketSessionsTest {
|
||||
|
||||
@Mock
|
||||
private MetricRegistry metrics;
|
||||
|
||||
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
|
||||
private Session session1;
|
||||
|
||||
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
|
||||
private Session session2;
|
||||
|
||||
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
|
||||
private Session session3;
|
||||
|
||||
private WebSocketSessions webSocketSessions;
|
||||
|
||||
@BeforeEach
|
||||
void init() {
|
||||
webSocketSessions = new WebSocketSessions(metrics);
|
||||
}
|
||||
|
||||
@Test
|
||||
void sendsMessageToUser() {
|
||||
Mockito.when(session1.isOpen()).thenReturn(true);
|
||||
Mockito.when(session2.isOpen()).thenReturn(true);
|
||||
|
||||
User user1 = newUser(1L);
|
||||
webSocketSessions.add(user1.getId(), session1);
|
||||
webSocketSessions.add(user1.getId(), session2);
|
||||
|
||||
User user2 = newUser(2L);
|
||||
webSocketSessions.add(user2.getId(), session3);
|
||||
|
||||
webSocketSessions.sendMessage(user1, "Hello");
|
||||
Mockito.verify(session1).getAsyncRemote();
|
||||
Mockito.verify(session2).getAsyncRemote();
|
||||
Mockito.verifyNoInteractions(session3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void closedSessionsAreNotNotified() {
|
||||
Mockito.when(session1.isOpen()).thenReturn(false);
|
||||
|
||||
User user1 = newUser(1L);
|
||||
webSocketSessions.add(user1.getId(), session1);
|
||||
|
||||
webSocketSessions.sendMessage(user1, "Hello");
|
||||
Mockito.verify(session1, Mockito.never()).getAsyncRemote();
|
||||
}
|
||||
|
||||
@Test
|
||||
void removedSessionsAreNotNotified() {
|
||||
User user1 = newUser(1L);
|
||||
webSocketSessions.add(user1.getId(), session1);
|
||||
webSocketSessions.remove(session1);
|
||||
|
||||
webSocketSessions.sendMessage(user1, "Hello");
|
||||
Mockito.verifyNoInteractions(session1);
|
||||
}
|
||||
|
||||
private User newUser(Long userId) {
|
||||
User user = new User();
|
||||
user.setId(userId);
|
||||
return user;
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,8 @@ import lombok.Getter;
|
||||
@ExtendWith(MockServerExtension.class)
|
||||
public abstract class BaseIT {
|
||||
|
||||
private static final HttpRequest FEED_REQUEST = HttpRequest.request().withMethod("GET").withPath("/");
|
||||
|
||||
private final CommaFeedDropwizardAppExtension extension = buildExtension();
|
||||
|
||||
private Client client;
|
||||
@@ -50,6 +52,8 @@ public abstract class BaseIT {
|
||||
|
||||
private String webSocketUrl;
|
||||
|
||||
private MockServerClient mockServerClient;
|
||||
|
||||
protected CommaFeedDropwizardAppExtension buildExtension() {
|
||||
return new CommaFeedDropwizardAppExtension() {
|
||||
@Override
|
||||
@@ -61,9 +65,10 @@ public abstract class BaseIT {
|
||||
|
||||
@BeforeEach
|
||||
void init(MockServerClient mockServerClient) throws IOException {
|
||||
this.mockServerClient = mockServerClient;
|
||||
|
||||
URL resource = Objects.requireNonNull(getClass().getResource("/feed/rss.xml"));
|
||||
mockServerClient.when(HttpRequest.request().withMethod("GET").withPath("/"))
|
||||
.respond(HttpResponse.response().withBody(IOUtils.toString(resource, StandardCharsets.UTF_8)));
|
||||
mockServerClient.when(FEED_REQUEST).respond(HttpResponse.response().withBody(IOUtils.toString(resource, StandardCharsets.UTF_8)));
|
||||
|
||||
this.client = extension.client();
|
||||
this.feedUrl = "http://localhost:" + mockServerClient.getPort() + "/";
|
||||
@@ -77,6 +82,13 @@ public abstract class BaseIT {
|
||||
this.client.close();
|
||||
}
|
||||
|
||||
protected void feedNowReturnsMoreEntries() throws IOException {
|
||||
mockServerClient.clear(FEED_REQUEST);
|
||||
|
||||
URL resource = Objects.requireNonNull(getClass().getResource("/feed/rss_2.xml"));
|
||||
mockServerClient.when(FEED_REQUEST).respond(HttpResponse.response().withBody(IOUtils.toString(resource, StandardCharsets.UTF_8)));
|
||||
}
|
||||
|
||||
protected String login() {
|
||||
LoginRequest req = new LoginRequest();
|
||||
req.setName("admin");
|
||||
@@ -112,4 +124,8 @@ public abstract class BaseIT {
|
||||
.get();
|
||||
return response.readEntity(Entries.class);
|
||||
}
|
||||
|
||||
protected void forceRefreshAllFeeds() {
|
||||
client.target(apiBaseUrl + "feed/refreshAll").request().get(Void.class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ import org.awaitility.Awaitility;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import com.commafeed.frontend.model.request.FeedModificationRequest;
|
||||
|
||||
import jakarta.websocket.ClientEndpointConfig;
|
||||
import jakarta.websocket.CloseReason;
|
||||
import jakarta.websocket.ContainerProvider;
|
||||
@@ -20,13 +22,12 @@ import jakarta.websocket.DeploymentException;
|
||||
import jakarta.websocket.Endpoint;
|
||||
import jakarta.websocket.EndpointConfig;
|
||||
import jakarta.websocket.Session;
|
||||
import jakarta.ws.rs.client.Entity;
|
||||
|
||||
class WebSocketIT extends BaseIT {
|
||||
|
||||
@Test
|
||||
void sessionClosedIfNotLoggedIn() throws DeploymentException, IOException {
|
||||
ClientEndpointConfig config = buildConfig("fake-session-id");
|
||||
|
||||
AtomicBoolean connected = new AtomicBoolean();
|
||||
AtomicReference<CloseReason> closeReasonRef = new AtomicReference<>();
|
||||
try (Session ignored = ContainerProvider.getWebSocketContainer().connectToServer(new Endpoint() {
|
||||
@@ -39,7 +40,7 @@ class WebSocketIT extends BaseIT {
|
||||
public void onClose(Session session, CloseReason closeReason) {
|
||||
closeReasonRef.set(closeReason);
|
||||
}
|
||||
}, config, URI.create(getWebSocketUrl()))) {
|
||||
}, buildConfig("fake-session-id"), URI.create(getWebSocketUrl()))) {
|
||||
Awaitility.await().atMost(15, TimeUnit.SECONDS).untilTrue(connected);
|
||||
|
||||
Awaitility.await().atMost(15, TimeUnit.SECONDS).until(() -> closeReasonRef.get() != null);
|
||||
@@ -50,7 +51,6 @@ class WebSocketIT extends BaseIT {
|
||||
@Test
|
||||
void subscribeAndGetsNotified() throws DeploymentException, IOException {
|
||||
String sessionId = login();
|
||||
ClientEndpointConfig config = buildConfig(sessionId);
|
||||
|
||||
AtomicBoolean connected = new AtomicBoolean();
|
||||
AtomicReference<String> messageRef = new AtomicReference<>();
|
||||
@@ -60,7 +60,7 @@ class WebSocketIT extends BaseIT {
|
||||
session.addMessageHandler(String.class, messageRef::set);
|
||||
connected.set(true);
|
||||
}
|
||||
}, config, URI.create(getWebSocketUrl()))) {
|
||||
}, buildConfig(sessionId), URI.create(getWebSocketUrl()))) {
|
||||
Awaitility.await().atMost(15, TimeUnit.SECONDS).untilTrue(connected);
|
||||
|
||||
Long subscriptionId = subscribe(getFeedUrl());
|
||||
@@ -70,10 +70,40 @@ class WebSocketIT extends BaseIT {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void notNotifiedForFilteredEntries() throws DeploymentException, IOException {
|
||||
String sessionId = login();
|
||||
Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl());
|
||||
|
||||
FeedModificationRequest req = new FeedModificationRequest();
|
||||
req.setId(subscriptionId);
|
||||
req.setName("feed-name");
|
||||
req.setFilter("!title.contains('item 4')");
|
||||
getClient().target(getApiBaseUrl() + "feed/modify").request().post(Entity.json(req), Void.class);
|
||||
|
||||
AtomicBoolean connected = new AtomicBoolean();
|
||||
AtomicReference<String> messageRef = new AtomicReference<>();
|
||||
try (Session ignored = ContainerProvider.getWebSocketContainer().connectToServer(new Endpoint() {
|
||||
@Override
|
||||
public void onOpen(Session session, EndpointConfig config) {
|
||||
session.addMessageHandler(String.class, messageRef::set);
|
||||
connected.set(true);
|
||||
}
|
||||
}, buildConfig(sessionId), URI.create(getWebSocketUrl()))) {
|
||||
Awaitility.await().atMost(15, TimeUnit.SECONDS).untilTrue(connected);
|
||||
|
||||
feedNowReturnsMoreEntries();
|
||||
forceRefreshAllFeeds();
|
||||
|
||||
Awaitility.await().atMost(15, TimeUnit.SECONDS).until(() -> messageRef.get() != null);
|
||||
Assertions.assertEquals("new-feed-entries:" + subscriptionId + ":1", messageRef.get());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void pingPong() throws DeploymentException, IOException {
|
||||
String sessionId = login();
|
||||
ClientEndpointConfig config = buildConfig(sessionId);
|
||||
|
||||
AtomicBoolean connected = new AtomicBoolean();
|
||||
AtomicReference<String> messageRef = new AtomicReference<>();
|
||||
@@ -83,7 +113,7 @@ class WebSocketIT extends BaseIT {
|
||||
session.addMessageHandler(String.class, messageRef::set);
|
||||
connected.set(true);
|
||||
}
|
||||
}, config, URI.create(getWebSocketUrl()))) {
|
||||
}, buildConfig(sessionId), URI.create(getWebSocketUrl()))) {
|
||||
Awaitility.await().atMost(15, TimeUnit.SECONDS).untilTrue(connected);
|
||||
|
||||
session.getAsyncRemote().sendText("ping");
|
||||
|
||||
@@ -5,13 +5,23 @@ import org.glassfish.jersey.client.ClientProperties;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import com.commafeed.CommaFeedDropwizardAppExtension;
|
||||
import com.commafeed.frontend.model.UserModel;
|
||||
import com.commafeed.integration.BaseIT;
|
||||
|
||||
import jakarta.ws.rs.NotAuthorizedException;
|
||||
import jakarta.ws.rs.client.Invocation.Builder;
|
||||
import jakarta.ws.rs.core.HttpHeaders;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
|
||||
class LogoutIT extends BaseIT {
|
||||
|
||||
@Override
|
||||
protected CommaFeedDropwizardAppExtension buildExtension() {
|
||||
// override so we don't add http basic auth
|
||||
return new CommaFeedDropwizardAppExtension();
|
||||
}
|
||||
|
||||
@Test
|
||||
void test() {
|
||||
String cookie = login();
|
||||
@@ -22,5 +32,8 @@ class LogoutIT extends BaseIT {
|
||||
.get()) {
|
||||
Assertions.assertEquals(HttpStatus.FOUND_302, response.getStatus());
|
||||
}
|
||||
|
||||
Builder req = getClient().target(getApiBaseUrl() + "user/profile").request().header(HttpHeaders.COOKIE, "JSESSIONID=" + cookie);
|
||||
Assertions.assertThrows(NotAuthorizedException.class, () -> req.get(UserModel.class));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,11 +95,11 @@ app:
|
||||
# -------------------
|
||||
# for MariaDB
|
||||
# driverClass is org.mariadb.jdbc.Driver
|
||||
# url is jdbc:mariadb://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true
|
||||
# url is jdbc:mariadb://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC
|
||||
#
|
||||
# for MySQL
|
||||
# driverClass is com.mysql.cj.jdbc.Driver
|
||||
# url is jdbc:mysql://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true
|
||||
# url is jdbc:mysql://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC
|
||||
#
|
||||
# for PostgreSQL
|
||||
# driverClass is org.postgresql.Driver
|
||||
|
||||
32
commafeed-server/src/test/resources/feed/rss_2.xml
Normal file
32
commafeed-server/src/test/resources/feed/rss_2.xml
Normal file
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<rss version="2.0">
|
||||
<channel>
|
||||
<title>CommaFeed test feed</title>
|
||||
<link>https://hostname.local/commafeed</link>
|
||||
<description>CommaFeed test feed description</description>
|
||||
<item>
|
||||
<title>Item 4</title>
|
||||
<link>https://hostname.local/commafeed/4</link>
|
||||
<description>Item 4 description</description>
|
||||
<pubDate>Sun, 31 Dec 2023 15:00:00 +0100</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title>Item 3</title>
|
||||
<link>https://hostname.local/commafeed/3</link>
|
||||
<description>Item 3 description</description>
|
||||
<pubDate>Sat, 30 Dec 2023 15:00:00 +0100</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title>Item 2</title>
|
||||
<link>https://hostname.local/commafeed/2</link>
|
||||
<description>Item 2 description</description>
|
||||
<pubDate>Fri, 29 Dec 2023 15:02:00 +0100</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title>Item 1</title>
|
||||
<link>https://hostname.local/commafeed/1</link>
|
||||
<description>Item 1 description</description>
|
||||
<pubDate>Wed, 27 Dec 2023 22:24:00 +0100</pubDate>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
Binary file not shown.
4
pom.xml
4
pom.xml
@@ -5,7 +5,7 @@
|
||||
|
||||
<groupId>com.commafeed</groupId>
|
||||
<artifactId>commafeed</artifactId>
|
||||
<version>4.2.0</version>
|
||||
<version>4.3.3</version>
|
||||
<name>CommaFeed</name>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.11.0</version>
|
||||
<version>3.12.1</version>
|
||||
<configuration>
|
||||
<parameters>true</parameters>
|
||||
</configuration>
|
||||
|
||||
Reference in New Issue
Block a user