forked from Archives/Athou_commafeed
Compare commits
1185 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1f6937802 | ||
|
|
0c0834b30f | ||
|
|
5ad4b97205 | ||
|
|
c4ec249bc4 | ||
|
|
cf8d3965d5 | ||
|
|
3903fd9374 | ||
|
|
77d59dabe8 | ||
|
|
56ca737297 | ||
|
|
9edb539be3 | ||
|
|
31a773d200 | ||
|
|
61355eabf7 | ||
|
|
569874e51f | ||
|
|
00d47901fc | ||
|
|
d8b4ef55ce | ||
|
|
da41a4cab9 | ||
|
|
8a90ef0471 | ||
|
|
b4ab32a578 | ||
|
|
03aa53abc8 | ||
|
|
2ae5c0cd8e | ||
|
|
cacc632443 | ||
|
|
28f865ccfa | ||
|
|
a4c949e8b3 | ||
|
|
6098994397 | ||
|
|
5763ca30d6 | ||
|
|
7d039d1001 | ||
|
|
7fe74af906 | ||
|
|
80b72aa30b | ||
|
|
3ba0d241f9 | ||
|
|
67428aa0c7 | ||
|
|
b9a0256031 | ||
|
|
f3c2296636 | ||
|
|
b6e8f21975 | ||
|
|
284f80045f | ||
|
|
f589477aa8 | ||
|
|
29cb296d09 | ||
|
|
86caa1450a | ||
|
|
9dd4b9e67f | ||
|
|
e2e654f05b | ||
|
|
72dbc62b41 | ||
|
|
0a21014668 | ||
|
|
b6d9d2a26c | ||
|
|
25c3a7748c | ||
|
|
b2bcfdd6eb | ||
|
|
2a978db406 | ||
|
|
9e40d0d066 | ||
|
|
c912650d59 | ||
|
|
464ebcb471 | ||
|
|
463e0e59d7 | ||
|
|
b4e5d8ef20 | ||
|
|
126905aeb3 | ||
|
|
1af10d3364 | ||
|
|
6ad854c019 | ||
|
|
b30117aa4d | ||
|
|
5a66482d1e | ||
|
|
2628ec49bb | ||
|
|
f3d15cf173 | ||
|
|
bbcf55ce57 | ||
|
|
72fc3716e7 | ||
|
|
81a6cfaa88 | ||
|
|
aed5165ef3 | ||
|
|
eaf2933726 | ||
|
|
39da4d9d36 | ||
|
|
e5ebd7ff39 | ||
|
|
b6ae3e4e1e | ||
|
|
32d1488352 | ||
|
|
b08d0a388f | ||
|
|
7fe004a696 | ||
|
|
f620d033b0 | ||
|
|
ba071ba71f | ||
|
|
6f3197302d | ||
|
|
131a8ebf68 | ||
|
|
8b24c125c2 | ||
|
|
52293376ec | ||
|
|
f8ac59af6a | ||
|
|
5c791e2305 | ||
|
|
6641bc0631 | ||
|
|
da690aa750 | ||
|
|
fb7f041454 | ||
|
|
ec4554c76e | ||
|
|
068e85fe6e | ||
|
|
ba926c674e | ||
|
|
836f8f14c0 | ||
|
|
eeecac96e1 | ||
|
|
ecc62f222a | ||
|
|
9022f93811 | ||
|
|
e7225d35b2 | ||
|
|
454fc03038 | ||
|
|
9c0674fd83 | ||
|
|
7a20482ddf | ||
|
|
32ad47ba16 | ||
|
|
fc562cce0f | ||
|
|
b029b251db | ||
|
|
e3e28e727f | ||
|
|
50cb728db7 | ||
|
|
c654ba4d1b | ||
|
|
846e29b15e | ||
|
|
f2b4062d73 | ||
|
|
9051e6a6db | ||
|
|
b733129043 | ||
|
|
d46b571444 | ||
|
|
7d744b4ce0 | ||
|
|
801dda912c | ||
|
|
a20005409a | ||
|
|
6f1411d075 | ||
|
|
1aa263a6c0 | ||
|
|
9d511ac7dd | ||
|
|
122e98cc76 | ||
|
|
e445e5ea39 | ||
|
|
5b9212015b | ||
|
|
293292f341 | ||
|
|
57d8a4dbb1 | ||
|
|
e104f531f9 | ||
|
|
bf1361926f | ||
|
|
cc4f4d9eb4 | ||
|
|
706bad26f1 | ||
|
|
4ecefe6491 | ||
|
|
937e7353ce | ||
|
|
1dcf76fc0a | ||
|
|
9d794dcad7 | ||
|
|
d11b666755 | ||
|
|
7a444e4861 | ||
|
|
5992795579 | ||
|
|
4441d76a7f | ||
|
|
c1305b56e3 | ||
|
|
cc0440c029 | ||
|
|
f65591c170 | ||
|
|
9a32dce9d1 | ||
|
|
789bd3edae | ||
|
|
256cd426d9 | ||
|
|
58af2da105 | ||
|
|
e0de397273 | ||
|
|
75cc3cf29c | ||
|
|
af60758e2a | ||
|
|
01180e95a2 | ||
|
|
fa683ef7e1 | ||
|
|
462d17a429 | ||
|
|
17f71a40d4 | ||
|
|
de91a3a05a | ||
|
|
ead587ee88 | ||
|
|
62b3e6fb3a | ||
|
|
037ff15045 | ||
|
|
ed35b06934 | ||
|
|
3cfb1a13a7 | ||
|
|
d04745d859 | ||
|
|
58b18f36c5 | ||
|
|
7282d18d8f | ||
|
|
8e58fa22b4 | ||
|
|
58d6eb2c5a | ||
|
|
2f7c7498e2 | ||
|
|
bcf8dcd551 | ||
|
|
511f0a60bb | ||
|
|
72db0d815f | ||
|
|
280d0b7fdd | ||
|
|
42e4575cb7 | ||
|
|
28a4bb403a | ||
|
|
cca3c907db | ||
|
|
1a5b932742 | ||
|
|
a1d3f3008a | ||
|
|
902f2efbd2 | ||
|
|
2e534af146 | ||
|
|
23ca30c3c2 | ||
|
|
517eedad00 | ||
|
|
216ea1fb42 | ||
|
|
640d1a0ce3 | ||
|
|
bba7425b5f | ||
|
|
7a1a49bfb4 | ||
|
|
e451e6698c | ||
|
|
9af3f21404 | ||
|
|
7b14a9c0c2 | ||
|
|
0b65cc9510 | ||
|
|
7879ab9b61 | ||
|
|
e6bebcafb3 | ||
|
|
3b465cebb7 | ||
|
|
aeb211be06 | ||
|
|
ad992aea7b | ||
|
|
d848f72a0b | ||
|
|
0db087908d | ||
|
|
42138d04d6 | ||
|
|
4522a9d0d5 | ||
|
|
7440fcad0e | ||
|
|
fc51c1882f | ||
|
|
e24498b31f | ||
|
|
60fdc79563 | ||
|
|
6729ebc6ea | ||
|
|
c8ff216ce5 | ||
|
|
98c4150cfe | ||
|
|
128332d710 | ||
|
|
eabcb519a4 | ||
|
|
5e14cead3d | ||
|
|
b601f938ff | ||
|
|
4acfda32d0 | ||
|
|
54da4e6839 | ||
|
|
3a6b4c588c | ||
|
|
48071b9fd1 | ||
|
|
f519aa039f | ||
|
|
dc3e5476a1 | ||
|
|
903035ecfc | ||
|
|
13ad57da10 | ||
|
|
44bc24c22a | ||
|
|
97f90405fc | ||
|
|
0fc2a0b022 | ||
|
|
89eb641704 | ||
|
|
c53da9f631 | ||
|
|
998868e63a | ||
|
|
93f22d2351 | ||
|
|
c3782bd7d2 | ||
|
|
f330349397 | ||
|
|
99c973c8c2 | ||
|
|
469420b5bf | ||
|
|
bde556d41f | ||
|
|
bf6c2d7beb | ||
|
|
fa62ca21e0 | ||
|
|
7dcf76da84 | ||
|
|
3dc80fa762 | ||
|
|
dbce12492b | ||
|
|
85f5eaffec | ||
|
|
106276351e | ||
|
|
961fb6a464 | ||
|
|
ac3d9ef57f | ||
|
|
3478ee4815 | ||
|
|
3dc02d7ba1 | ||
|
|
c886f8b83c | ||
|
|
4a2154d0b3 | ||
|
|
ba530d5019 | ||
|
|
85b6209c52 | ||
|
|
7ff86a5e31 | ||
|
|
8edd6a1e2d | ||
|
|
6e65ed49e9 | ||
|
|
711b01abfa | ||
|
|
c7014ca2a1 | ||
|
|
a3984cd959 | ||
|
|
8d85b1bcba | ||
|
|
c451eee406 | ||
|
|
8f42135996 | ||
|
|
2c26aeed17 | ||
|
|
e2c4aa998b | ||
|
|
c9e3b7f349 | ||
|
|
ebb4e52ba7 | ||
|
|
1ddfdfb12e | ||
|
|
81f16aea62 | ||
|
|
429ec193c8 | ||
|
|
732b714448 | ||
|
|
82e0405ad9 | ||
|
|
9ef002fcd1 | ||
|
|
ec938e416c | ||
|
|
37cf711cbc | ||
|
|
de441e4ff7 | ||
|
|
46251526b6 | ||
|
|
67eeea0b06 | ||
|
|
b49ccc4cd9 | ||
|
|
8586a8b57b | ||
|
|
d9f63786a8 | ||
|
|
8f0c8b68b9 | ||
|
|
15e574c5c4 | ||
|
|
fe532242b4 | ||
|
|
fb48ff0858 | ||
|
|
8d850639d7 | ||
|
|
ee73195915 | ||
|
|
72d9dad61b | ||
|
|
fde8dab8cd | ||
|
|
dae5efa787 | ||
|
|
3c067140fd | ||
|
|
4ccbe81e87 | ||
|
|
3d5d93bb72 | ||
|
|
4138b6eb9b | ||
|
|
9c39c95a9b | ||
|
|
32b2bf99a4 | ||
|
|
cf459876af | ||
|
|
6698bd74b5 | ||
|
|
c81d06e5f3 | ||
|
|
b12a78dc84 | ||
|
|
b076587e44 | ||
|
|
bb12f16bea | ||
|
|
e80caadd12 | ||
|
|
846d93f2b2 | ||
|
|
0ed6f6ef9c | ||
|
|
15992dcb80 | ||
|
|
1a5c399b54 | ||
|
|
5e92f9ffb8 | ||
|
|
71164d1b69 | ||
|
|
6947670fe6 | ||
|
|
30810e37b9 | ||
|
|
b17b2767b0 | ||
|
|
d37cf5bbcf | ||
|
|
045baba705 | ||
|
|
3623dc8e1d | ||
|
|
2610c37067 | ||
|
|
286b69a646 | ||
|
|
9673f27090 | ||
|
|
0722599f6d | ||
|
|
1df40d8370 | ||
|
|
457e4ec69e | ||
|
|
647310a45f | ||
|
|
e85c92f216 | ||
|
|
d93b0dbfd4 | ||
|
|
b4e61ef547 | ||
|
|
71dffbba46 | ||
|
|
2c0b0c4e3b | ||
|
|
d868e58e1e | ||
|
|
90eb2095bf | ||
|
|
62d3ed16e6 | ||
|
|
74f7c48818 | ||
|
|
23fe9c29ed | ||
|
|
8f7be8278a | ||
|
|
49118b6ea0 | ||
|
|
d97bd04ae2 | ||
|
|
8d11309b64 | ||
|
|
68c24e4cb8 | ||
|
|
4e43e0235f | ||
|
|
62b79a9625 | ||
|
|
cb0706808c | ||
|
|
ffd5704b1e | ||
|
|
3987077e5a | ||
|
|
2e01a76784 | ||
|
|
8254093f5f | ||
|
|
0b06526756 | ||
|
|
06731ae76d | ||
|
|
9a59453792 | ||
|
|
c195a52c89 | ||
|
|
3d7924f953 | ||
|
|
f29efd7fae | ||
|
|
157bff3c83 | ||
|
|
5c17bbc36d | ||
|
|
c85e72e70c | ||
|
|
01150f67e1 | ||
|
|
75aca7aa6f | ||
|
|
affde7e43c | ||
|
|
b9b1b53235 | ||
|
|
708ebb8abc | ||
|
|
83e763df0a | ||
|
|
0ff812c1ea | ||
|
|
3e9dd6d8e2 | ||
|
|
23af73e847 | ||
|
|
e79e4719fd | ||
|
|
23fef98432 | ||
|
|
22478252e7 | ||
|
|
76b1f3cd35 | ||
|
|
420d73ec6a | ||
|
|
e0211cfa0c | ||
|
|
25a92c651c | ||
|
|
0781205c69 | ||
|
|
5102dd5e30 | ||
|
|
6ccfc3fd67 | ||
|
|
2791ed91ab | ||
|
|
f40c198233 | ||
|
|
003dc63121 | ||
|
|
f8ef1e2a99 | ||
|
|
14c7078940 | ||
|
|
074836d3e8 | ||
|
|
0cdbc144b3 | ||
|
|
dc63ec24c0 | ||
|
|
6d4c6c36a5 | ||
|
|
464af5f4d9 | ||
|
|
aa94a46a3d | ||
|
|
8542197dc3 | ||
|
|
64d77eaef4 | ||
|
|
675ef8794c | ||
|
|
4bcdbeb516 | ||
|
|
a9f37739fb | ||
|
|
5ab0fc19da | ||
|
|
7b232425f3 | ||
|
|
c0e7668140 | ||
|
|
ae3f059257 | ||
|
|
d44c7c1e95 | ||
|
|
6cd9d134cf | ||
|
|
6f21ba8afc | ||
|
|
b2fe13c117 | ||
|
|
03ece7a262 | ||
|
|
697fde2d0e | ||
|
|
7f0f85b356 | ||
|
|
a7d41debfe | ||
|
|
57bf758108 | ||
|
|
b37d933047 | ||
|
|
80ffef4555 | ||
|
|
af5a0002aa | ||
|
|
cd24e412e3 | ||
|
|
a073d843ab | ||
|
|
8ccb59ed18 | ||
|
|
e6dc7d2d0d | ||
|
|
f7b21ca3f6 | ||
|
|
df3a1bcdb6 | ||
|
|
5bec494a7c | ||
|
|
d8eef4dd9f | ||
|
|
d80138caf3 | ||
|
|
d26c103aa5 | ||
|
|
249231f57e | ||
|
|
7a838cddad | ||
|
|
477f2cd6db | ||
|
|
9915f05f73 | ||
|
|
0a16bb2fba | ||
|
|
3d4faf2406 | ||
|
|
63de6fe833 | ||
|
|
9c6219a58a | ||
|
|
3e664d4287 | ||
|
|
4c4ffd84f3 | ||
|
|
f555f0e392 | ||
|
|
124166738b | ||
|
|
8b32dcc576 | ||
|
|
105651215a | ||
|
|
d9c6cbd072 | ||
|
|
b4c52e06fe | ||
|
|
2565dfe528 | ||
|
|
b5036c9148 | ||
|
|
e2c8ddb8f7 | ||
|
|
85fbd284fa | ||
|
|
559fb69a64 | ||
|
|
054c76716a | ||
|
|
ba17c9350f | ||
|
|
781015eea4 | ||
|
|
13e5c0e8d2 | ||
|
|
4d88a30848 | ||
|
|
19c03de9e4 | ||
|
|
e3169c9f2d | ||
|
|
e90fb0b56f | ||
|
|
69607a5122 | ||
|
|
21eb8e6d9f | ||
|
|
a28a6b9dc4 | ||
|
|
a9cadbafeb | ||
|
|
d491af5a8d | ||
|
|
39c7934fb8 | ||
|
|
76eba8cc63 | ||
|
|
7549ff2491 | ||
|
|
66cd18bc4b | ||
|
|
44d7a2c340 | ||
|
|
d6ee63a01f | ||
|
|
a495c9cacd | ||
|
|
530185d15c | ||
|
|
8dd28c25a7 | ||
|
|
50884b236c | ||
|
|
da9fe09e58 | ||
|
|
3c24c9aa7a | ||
|
|
9d10a4b46f | ||
|
|
d56ed3bd06 | ||
|
|
41c1c429d0 | ||
|
|
1a3d890b40 | ||
|
|
e3ec9b2ccd | ||
|
|
f69878a242 | ||
|
|
ea766706fb | ||
|
|
306507af80 | ||
|
|
67084783b2 | ||
|
|
7cbb75f717 | ||
|
|
1c335492d5 | ||
|
|
de92e74c8e | ||
|
|
9cbbd30618 | ||
|
|
f14f1493c4 | ||
|
|
e68c8fdbe1 | ||
|
|
e094972aa2 | ||
|
|
ff831c6d2b | ||
|
|
9957cda11a | ||
|
|
6809822000 | ||
|
|
8048b1a9aa | ||
|
|
8557bd018a | ||
|
|
183d5fd162 | ||
|
|
411f86fbeb | ||
|
|
5493046f25 | ||
|
|
42015015a5 | ||
|
|
f3d2808f7d | ||
|
|
906c353a7f | ||
|
|
93dea83cd3 | ||
|
|
1fc76ce1ad | ||
|
|
a337b01bc7 | ||
|
|
689e5c0004 | ||
|
|
d5a3c81c85 | ||
|
|
8230fde5d2 | ||
|
|
b35513ea84 | ||
|
|
42a7785ca1 | ||
|
|
ea5ee4f04f | ||
|
|
3e14b12d4f | ||
|
|
78cc30f828 | ||
|
|
6091c84e60 | ||
|
|
6ea95ad254 | ||
|
|
7f888d926e | ||
|
|
5e4e02474f | ||
|
|
bff8611b42 | ||
|
|
f674048af3 | ||
|
|
0265c24cf9 | ||
|
|
f8c3a229ec | ||
|
|
c424f40420 | ||
|
|
b77666cfe5 | ||
|
|
193d1604d9 | ||
|
|
4efc6296b5 | ||
|
|
f753a4bdda | ||
|
|
afaaaf9657 | ||
|
|
4d46896bf0 | ||
|
|
2ad28c927f | ||
|
|
b9680a66ef | ||
|
|
4f687d5857 | ||
|
|
9cca026834 | ||
|
|
058a9cd192 | ||
|
|
57d2ede86e | ||
|
|
e3abea4ec5 | ||
|
|
b831f1f35c | ||
|
|
74bce1308c | ||
|
|
98cfa6d2c8 | ||
|
|
99a02a2186 | ||
|
|
3431a813b1 | ||
|
|
e9e0e8d32b | ||
|
|
2d14409d35 | ||
|
|
a8200e5c58 | ||
|
|
79a8df8b06 | ||
|
|
061a5f0262 | ||
|
|
821bdb3b0f | ||
|
|
606dfa9299 | ||
|
|
131357c616 | ||
|
|
f6d3493bad | ||
|
|
0c6104e25b | ||
|
|
d73735a35d | ||
|
|
e725d2d6b6 | ||
|
|
f0e1279d68 | ||
|
|
c74c74d2c4 | ||
|
|
aa70cf5dcd | ||
|
|
1055259627 | ||
|
|
302d37b6ef | ||
|
|
8532a73d94 | ||
|
|
ffafb272cb | ||
|
|
22e0171a34 | ||
|
|
2b410f040c | ||
|
|
259e8ad4e5 | ||
|
|
21244dd9f5 | ||
|
|
bc6206180d | ||
|
|
6e22d21358 | ||
|
|
95bdb4e700 | ||
|
|
9b7dbc68ab | ||
|
|
dc86c9b0db | ||
|
|
cb92ed753f | ||
|
|
10a085e24e | ||
|
|
3400a39edf | ||
|
|
21efffa345 | ||
|
|
e2e80ba7e5 | ||
|
|
d988dba66e | ||
|
|
403201fbff | ||
|
|
3cc93b51bb | ||
|
|
6a7d83bb45 | ||
|
|
19c8db8b31 | ||
|
|
0d75688ec8 | ||
|
|
e01dcb2f5b | ||
|
|
57757e2c14 | ||
|
|
779cd2fcfe | ||
|
|
94919f22e4 | ||
|
|
d5cf690703 | ||
|
|
191574dace | ||
|
|
ee7c6792c9 | ||
|
|
e2962dc2eb | ||
|
|
8c335cb8fd | ||
|
|
461c18a591 | ||
|
|
363837ab26 | ||
|
|
a184485421 | ||
|
|
f992c3f1a6 | ||
|
|
3219a9e313 | ||
|
|
4717c31058 | ||
|
|
693844828b | ||
|
|
ef4b479638 | ||
|
|
8eefb1bcfb | ||
|
|
ada9a5039b | ||
|
|
cca2d49cc3 | ||
|
|
f4a43e9950 | ||
|
|
9a89b39b62 | ||
|
|
2dba844b6c | ||
|
|
3101dc91de | ||
|
|
83cacd97f3 | ||
|
|
aa179c21f8 | ||
|
|
31cf4d8bb2 | ||
|
|
19bcc2c0da | ||
|
|
ca803ff7ce | ||
|
|
0e26d975aa | ||
|
|
86a3cb67f2 | ||
|
|
6297463445 | ||
|
|
1a3a3076b1 | ||
|
|
7fb9cfeaf1 | ||
|
|
5c7dbe6304 | ||
|
|
c41fd9216a | ||
|
|
91a9ad79f0 | ||
|
|
906458dc96 | ||
|
|
2f4fddf539 | ||
|
|
a8157143b9 | ||
|
|
92576e28e9 | ||
|
|
a6e34a2960 | ||
|
|
306cf7aab7 | ||
|
|
f3b806686d | ||
|
|
6634cfb991 | ||
|
|
9930bb68b2 | ||
|
|
37722131e5 | ||
|
|
5f2d213419 | ||
|
|
e119941762 | ||
|
|
ba496c1395 | ||
|
|
9c3fc84542 | ||
|
|
b017ce936a | ||
|
|
d696b0581b | ||
|
|
e4b2880f2b | ||
|
|
e071049969 | ||
|
|
5e1f592951 | ||
|
|
231f82da28 | ||
|
|
9a28bc7334 | ||
|
|
00907e92ff | ||
|
|
5669798881 | ||
|
|
f3a62a5f75 | ||
|
|
3b20ed222c | ||
|
|
1f40f3f59c | ||
|
|
a8d890524f | ||
|
|
b635799e80 | ||
|
|
50ea66620d | ||
|
|
46581d0e22 | ||
|
|
a3562867a6 | ||
|
|
c0117ada93 | ||
|
|
a3dcb2c03e | ||
|
|
8f8aaa5a1d | ||
|
|
85482422fd | ||
|
|
643c969faf | ||
|
|
85f9469d6d | ||
|
|
0df0d50695 | ||
|
|
5e9256c197 | ||
|
|
b67e1a92f5 | ||
|
|
d250e4bc26 | ||
|
|
dcf1f41f2d | ||
|
|
3df6ba1457 | ||
|
|
b89928f6c6 | ||
|
|
2e014484e3 | ||
|
|
3b2b18fd2e | ||
|
|
ebf1e592ff | ||
|
|
88404b91d8 | ||
|
|
9cbb60313c | ||
|
|
b95d417f5e | ||
|
|
994f1fb121 | ||
|
|
e533c1fa4b | ||
|
|
d0d946ffe9 | ||
|
|
e3bcc824c7 | ||
|
|
357e4e207f | ||
|
|
2aee961600 | ||
|
|
3aa1987319 | ||
|
|
ae15f61fc2 | ||
|
|
e58f92a812 | ||
|
|
46383924b1 | ||
|
|
071920e864 | ||
|
|
012238e6a9 | ||
|
|
a565566c50 | ||
|
|
550804c666 | ||
|
|
f7a4a33f5e | ||
|
|
f1b19ebae3 | ||
|
|
4049fa2e17 | ||
|
|
28808cf4f5 | ||
|
|
870b46cf9d | ||
|
|
9c20dea99c | ||
|
|
63c7679067 | ||
|
|
764c1a6430 | ||
|
|
bb6578bdd0 | ||
|
|
748c8531ad | ||
|
|
a734fe68d2 | ||
|
|
cc5ebc55a4 | ||
|
|
aa396c1e1c | ||
|
|
fbf87ff291 | ||
|
|
e9f3ffddf4 | ||
|
|
695518d68b | ||
|
|
5d96c1e12b | ||
|
|
3a72a1cc04 | ||
|
|
54f5714108 | ||
|
|
04811c7eca | ||
|
|
6856736ddb | ||
|
|
db6c43993a | ||
|
|
508a22576a | ||
|
|
8fb012b3a1 | ||
|
|
133781d314 | ||
|
|
50cb12896e | ||
|
|
79a4315941 | ||
|
|
33a2f76521 | ||
|
|
d4041a1d88 | ||
|
|
09f2f56446 | ||
|
|
a0c3eda506 | ||
|
|
84de3199cc | ||
|
|
a7e8309d63 | ||
|
|
7e74d2f6f4 | ||
|
|
dc25d53dc0 | ||
|
|
ac1a927836 | ||
|
|
b50b69adb2 | ||
|
|
b112e912af | ||
|
|
ece55727d3 | ||
|
|
181dd24b57 | ||
|
|
10008ca0e8 | ||
|
|
134c4621a8 | ||
|
|
51f15bf487 | ||
|
|
49ae2c88ad | ||
|
|
43a628fc55 | ||
|
|
7f71f95f7c | ||
|
|
e8d5eab419 | ||
|
|
de3a6b1f20 | ||
|
|
849742e19a | ||
|
|
b6392b114c | ||
|
|
4db0c775ff | ||
|
|
ff9374f1ed | ||
|
|
ea86c9bb1f | ||
|
|
e6dd088abe | ||
|
|
c039d8f3a4 | ||
|
|
bffa6329fd | ||
|
|
b88e5d2847 | ||
|
|
0fc4fcd406 | ||
|
|
f04ca21394 | ||
|
|
a82fca130f | ||
|
|
70d494798c | ||
|
|
cf02bf221b | ||
|
|
9eb03d7455 | ||
|
|
12c8fdeec2 | ||
|
|
851babfe2a | ||
|
|
859490806b | ||
|
|
2c828b50da | ||
|
|
ede7834cb8 | ||
|
|
3627ee369d | ||
|
|
c4c41d1494 | ||
|
|
c577e77f8f | ||
|
|
9218f19832 | ||
|
|
ecbc2133a4 | ||
|
|
e38ca66c51 | ||
|
|
2395a2670e | ||
|
|
e7748d787f | ||
|
|
012ce71134 | ||
|
|
1b1a3f49c1 | ||
|
|
5b77860189 | ||
|
|
b333e8d90a | ||
|
|
ab6457ef3f | ||
|
|
5c69daec08 | ||
|
|
1bfa3ebb8e | ||
|
|
2694fea211 | ||
|
|
720eddeb66 | ||
|
|
ab334a7bc6 | ||
|
|
214dfe580a | ||
|
|
4ef53eab3a | ||
|
|
2f51547f0d | ||
|
|
da910ac336 | ||
|
|
643954f7c9 | ||
|
|
63061482d0 | ||
|
|
86d4f5a670 | ||
|
|
815093f1c6 | ||
|
|
47d39831d3 | ||
|
|
c18ed829aa | ||
|
|
e757e61b79 | ||
|
|
d612d83874 | ||
|
|
e170dfe60b | ||
|
|
69cd90edd8 | ||
|
|
f506f722c2 | ||
|
|
857736adad | ||
|
|
a92df774bd | ||
|
|
f2c6734c79 | ||
|
|
77b6cf75a5 | ||
|
|
3b56496196 | ||
|
|
aabbf0a5d1 | ||
|
|
9a43fd434f | ||
|
|
21ce9db4b0 | ||
|
|
044694487d | ||
|
|
3af8485326 | ||
|
|
f7adef0648 | ||
|
|
dc16e43154 | ||
|
|
78a5267198 | ||
|
|
04af355e0c | ||
|
|
89405009ec | ||
|
|
6b0aa32da2 | ||
|
|
aaf237d111 | ||
|
|
1fd48a0a40 | ||
|
|
09e0a51b46 | ||
|
|
cc32f8ad16 | ||
|
|
2f6ddf0e70 | ||
|
|
c3973755da | ||
|
|
42537a65b9 | ||
|
|
906c92e54f | ||
|
|
cc69968d78 | ||
|
|
dcde2083ec | ||
|
|
7469784059 | ||
|
|
c13a693456 | ||
|
|
e3c482d664 | ||
|
|
1fd33a5585 | ||
|
|
0742778e6a | ||
|
|
152479c888 | ||
|
|
a297f8c0c8 | ||
|
|
92aeee0572 | ||
|
|
050756517e | ||
|
|
0bb46f291a | ||
|
|
1eecabf105 | ||
|
|
da1bd8d32e | ||
|
|
124983a396 | ||
|
|
43613688da | ||
|
|
43aa69cd18 | ||
|
|
780b7666c5 | ||
|
|
70b4534e14 | ||
|
|
24666fd7fc | ||
|
|
de80aa6bb3 | ||
|
|
6c7e2ea847 | ||
|
|
6ea318acd3 | ||
|
|
2f4ee7cff8 | ||
|
|
9d9d758fa6 | ||
|
|
a071b7c265 | ||
|
|
3a57b68fa3 | ||
|
|
f2f36baf1b | ||
|
|
1aaf9e747a | ||
|
|
92611772a9 | ||
|
|
fb159dc46b | ||
|
|
78ea0873f2 | ||
|
|
faabc01dbc | ||
|
|
5acfe9e92a | ||
|
|
4388a8b6ce | ||
|
|
7414bd15b0 | ||
|
|
52d6021f3c | ||
|
|
f7acc27fcb | ||
|
|
175a293327 | ||
|
|
21dd6519b0 | ||
|
|
72f55c34b7 | ||
|
|
1b1d3c791b | ||
|
|
159c2c01a7 | ||
|
|
272f5b42f9 | ||
|
|
2395d0782e | ||
|
|
da81830e43 | ||
|
|
63a602cf8a | ||
|
|
0244b5c3e3 | ||
|
|
9592e86fa9 | ||
|
|
e6840bb50c | ||
|
|
b6890378a1 | ||
|
|
ba72ed0b93 | ||
|
|
e2fb576858 | ||
|
|
608b099b4d | ||
|
|
c2e0c81f7e | ||
|
|
7071d01a59 | ||
|
|
30cd2b9b53 | ||
|
|
abc498b09c | ||
|
|
31081e1089 | ||
|
|
4a16b8d072 | ||
|
|
9c04095292 | ||
|
|
643f98d59e | ||
|
|
f4da19183e | ||
|
|
de40f253b5 | ||
|
|
f6543e407a | ||
|
|
4d462a8e9e | ||
|
|
018ee1f3e6 | ||
|
|
752268fed1 | ||
|
|
8fe9a6cc3a | ||
|
|
b17a17ba10 | ||
|
|
b3545b60ea | ||
|
|
e6da3f693d | ||
|
|
4ab09da434 | ||
|
|
5e8daf29bf | ||
|
|
024a1067bb | ||
|
|
c427da72b9 | ||
|
|
346fb6b1ea | ||
|
|
1b658c76a3 | ||
|
|
1593ed62ba | ||
|
|
085eddd4b0 | ||
|
|
0db77ad2c0 | ||
|
|
6f8bcb6c6a | ||
|
|
4196dee896 | ||
|
|
6d49e0f0df | ||
|
|
d99f572989 | ||
|
|
fa197c33f1 | ||
|
|
1ce39a419e | ||
|
|
f0e3ac8fcb | ||
|
|
30947cea05 | ||
|
|
9134f36d3b | ||
|
|
dc526316a0 | ||
|
|
6593174668 | ||
|
|
0891c41abc | ||
|
|
6ecb6254aa | ||
|
|
84bd9eeeff | ||
|
|
2549c4d47b | ||
|
|
8750aa3dd6 | ||
|
|
262094a736 | ||
|
|
035201f917 | ||
|
|
ae9cbc5214 | ||
|
|
78d5bf129a | ||
|
|
1f02ddd163 | ||
|
|
eff1e8cc7b | ||
|
|
dc8475b59a | ||
|
|
921968662d | ||
|
|
4d83173dbd | ||
|
|
f13368cb96 | ||
|
|
ec7e97e1de | ||
|
|
d4c9bd1dd7 | ||
|
|
6bff657d4d | ||
|
|
613d286be1 | ||
|
|
fd48108f8b | ||
|
|
c3cbd18df9 | ||
|
|
6685057dae | ||
|
|
0dec0e3788 | ||
|
|
1a73dd4004 | ||
|
|
eae80a6450 | ||
|
|
21a32ce0eb | ||
|
|
325533c5d9 | ||
|
|
7d819022f6 | ||
|
|
dba944874b | ||
|
|
ce9c12ec92 | ||
|
|
22dfc5774f | ||
|
|
d59091ab2b | ||
|
|
f69146a6bf | ||
|
|
43cdf3db3b | ||
|
|
280a354228 | ||
|
|
573b0431f9 | ||
|
|
9878b60e97 | ||
|
|
964033c2a7 | ||
|
|
d2e45aca91 | ||
|
|
daa99a2efc | ||
|
|
e986e9999a | ||
|
|
98d302cb94 | ||
|
|
bf11c4a7e4 | ||
|
|
e1cab952f8 | ||
|
|
bc28d4de27 | ||
|
|
bb901564e3 | ||
|
|
93acc9ded1 | ||
|
|
9b1c6a371e | ||
|
|
82bf8cd807 | ||
|
|
c2f2780c3f | ||
|
|
08f71d1f6f | ||
|
|
f498088beb | ||
|
|
347b41cf35 | ||
|
|
61ae90ad28 | ||
|
|
9a42fbafb2 | ||
|
|
938f9e9434 | ||
|
|
9004e453c2 | ||
|
|
7d33542691 | ||
|
|
c99348862c | ||
|
|
ac86db3966 | ||
|
|
e368810731 | ||
|
|
edae2f5a61 | ||
|
|
ab17c6f44e | ||
|
|
59dbae4f66 | ||
|
|
d7956292df | ||
|
|
1075497559 | ||
|
|
2d99fa03d3 | ||
|
|
72b64b6f0d | ||
|
|
a2096d3622 | ||
|
|
c81f9fb7b1 | ||
|
|
cc7e9e21fb | ||
|
|
803d537e51 | ||
|
|
9a83e5b6ef | ||
|
|
4323da9007 | ||
|
|
30b9b24be4 | ||
|
|
b191b00003 | ||
|
|
7e5cdcba34 | ||
|
|
45b30ad333 | ||
|
|
7ca087b0a6 | ||
|
|
188e4594fd | ||
|
|
2da80ce7d8 | ||
|
|
d5820f9aa5 | ||
|
|
b1a0aae0a5 | ||
|
|
cdd4d4b063 | ||
|
|
21f675e80b | ||
|
|
380724d73e | ||
|
|
2d26c5dee3 | ||
|
|
29bcc5ccf5 | ||
|
|
91497ab45a | ||
|
|
be77968570 | ||
|
|
a42dacc48d | ||
|
|
cd06055246 | ||
|
|
62c1f25ffc | ||
|
|
415dc15d6c | ||
|
|
1d0c87c679 | ||
|
|
e51c486a04 | ||
|
|
73808c1a70 | ||
|
|
fbcc2ecd0f | ||
|
|
3997606774 | ||
|
|
b988b599d5 | ||
|
|
3e2ff2959d | ||
|
|
5714a63d27 | ||
|
|
12b18d1e04 | ||
|
|
232141cb56 | ||
|
|
c4334e5e6e | ||
|
|
ddf78f880b | ||
|
|
b3651f3fba | ||
|
|
24943b868c | ||
|
|
ef71a691ef | ||
|
|
01593d94eb | ||
|
|
b793cc66d1 | ||
|
|
3810dedf47 | ||
|
|
9115797dee | ||
|
|
232658b934 | ||
|
|
f99fe57695 | ||
|
|
ec89d41112 | ||
|
|
f6d26a77cc | ||
|
|
860852cc12 | ||
|
|
d06d76401c | ||
|
|
f5b04a783e | ||
|
|
964e470951 | ||
|
|
612f8722dd | ||
|
|
e118dc9b7f | ||
|
|
6e42cdaf2d | ||
|
|
5198792ca5 | ||
|
|
10a71213f3 | ||
|
|
a5d0979d9f | ||
|
|
d84225ab1c | ||
|
|
cd86947e64 | ||
|
|
f6b3114a91 | ||
|
|
cd50b6b058 | ||
|
|
b0c7ef18db | ||
|
|
24171faf86 | ||
|
|
941f14dd41 | ||
|
|
d46ef787db | ||
|
|
ec7447a38c | ||
|
|
2a3fc3ae15 | ||
|
|
ef25582bcb | ||
|
|
55bbb2542d | ||
|
|
8e94ac74a8 | ||
|
|
90ecb9253c | ||
|
|
6721842d98 | ||
|
|
8b487ec414 | ||
|
|
d6382861c3 | ||
|
|
2cdea99a69 | ||
|
|
b1ae1c8afd | ||
|
|
c09cd0c717 | ||
|
|
f50e0ae272 | ||
|
|
b99b91a2a8 | ||
|
|
d9759de6f1 | ||
|
|
cf2b7f9e4f | ||
|
|
ee880c06ed | ||
|
|
bc2e13ef22 | ||
|
|
39ecfe2782 | ||
|
|
3295d82f69 | ||
|
|
1cd27a59e2 | ||
|
|
e1602edff1 | ||
|
|
ef8e61d6fc | ||
|
|
0057030442 | ||
|
|
6fabe46d6e | ||
|
|
37c58f2755 | ||
|
|
bb982c3caf | ||
|
|
7e4c3737a8 | ||
|
|
23596b5ac6 | ||
|
|
2fdeb7acd8 | ||
|
|
c62cac478c | ||
|
|
e9026e0371 | ||
|
|
7446d906ae | ||
|
|
62ad09ac93 | ||
|
|
01d1f920a8 | ||
|
|
057810470c | ||
|
|
5a6d6be8e5 | ||
|
|
c6c813a4ee | ||
|
|
ad5787a38b | ||
|
|
387ceabf30 | ||
|
|
ffe6962c36 | ||
|
|
6d599fc77d | ||
|
|
9fcff1342c | ||
|
|
f7dbc2e9aa | ||
|
|
468f2e4c76 | ||
|
|
883c9c79aa | ||
|
|
f171d05088 | ||
|
|
f85745fe40 | ||
|
|
5ad93bb3ba | ||
|
|
d80ed9d4dd | ||
|
|
69b5f5418a | ||
|
|
06aa37659c | ||
|
|
d5c98de839 | ||
|
|
920975059c | ||
|
|
7c6e4c3356 | ||
|
|
143971da5e | ||
|
|
8976e9c01a | ||
|
|
20c6355efd | ||
|
|
f86f38ef7a | ||
|
|
24311df551 | ||
|
|
d02aa78def | ||
|
|
b131020f46 | ||
|
|
4ab82782b0 | ||
|
|
6f9ebd5d78 | ||
|
|
7ebbf26369 | ||
|
|
dbc93f9928 | ||
|
|
ad6ebd7e4d | ||
|
|
ab86247c8c | ||
|
|
884516be28 | ||
|
|
c236b1adda | ||
|
|
222117dafe | ||
|
|
38cd27df57 | ||
|
|
5e07e74bb2 | ||
|
|
fe779e361f | ||
|
|
1a51799497 | ||
|
|
6ea926cdb0 | ||
|
|
439d61946a | ||
|
|
426c8d7dfb | ||
|
|
f1b51e8342 | ||
|
|
9de19e9f2d | ||
|
|
fc2eac7f2c | ||
|
|
bc6fc01c3f | ||
|
|
85ae70f278 | ||
|
|
e415d1d945 | ||
|
|
acb06c3405 | ||
|
|
a137ecb293 | ||
|
|
b4c1aea7c4 | ||
|
|
a0d86ce94a | ||
|
|
6ce0e2f151 | ||
|
|
628f7aca90 | ||
|
|
a4a7d53670 | ||
|
|
e76e7879cd | ||
|
|
7a00e743eb | ||
|
|
6e0e692ae8 | ||
|
|
321b3d4819 | ||
|
|
211708255e | ||
|
|
dcc32cb539 | ||
|
|
c9367afd9d | ||
|
|
af724fbb87 | ||
|
|
9d052f2f59 | ||
|
|
e9a9334c03 | ||
|
|
80c9adcf0f | ||
|
|
13c402d9d0 | ||
|
|
5d5dc67a46 | ||
|
|
f2330d8346 | ||
|
|
ee061f3362 | ||
|
|
e071cb457f | ||
|
|
7972dec827 | ||
|
|
41c0200270 | ||
|
|
9e5fa5472a | ||
|
|
0ebab27588 | ||
|
|
0d081bc47e | ||
|
|
92853a164a | ||
|
|
5d75885352 | ||
|
|
83b8886846 | ||
|
|
f3869f92dc | ||
|
|
b1c1f2adc4 | ||
|
|
fd8c6c5531 | ||
|
|
b37346ad20 | ||
|
|
1b2e2e6915 | ||
|
|
f483d569f0 | ||
|
|
b9bbcf1e60 | ||
|
|
3097272179 | ||
|
|
d0b92774bc | ||
|
|
c82a142c96 | ||
|
|
5b43d416fc | ||
|
|
40e1c70fca | ||
|
|
d610f980c7 | ||
|
|
68c8ce1ef3 | ||
|
|
218a602c0b | ||
|
|
cad65e953e | ||
|
|
39bc9713e4 | ||
|
|
812da21b6f | ||
|
|
a756783604 | ||
|
|
16199c5b54 | ||
|
|
3964977a0a | ||
|
|
f5b4d037ef | ||
|
|
5929581fee | ||
|
|
92d0d6af47 | ||
|
|
413253e4a9 | ||
|
|
9c98e7eca1 | ||
|
|
d3d1aba834 | ||
|
|
6dd3ce2e72 | ||
|
|
398648ac91 | ||
|
|
28ef9ccfd2 | ||
|
|
acab5295cc | ||
|
|
3d73435446 | ||
|
|
eab08d2197 | ||
|
|
d13b96edd1 | ||
|
|
624aa9cb23 | ||
|
|
e76ee6dc9b | ||
|
|
9bf7dbe893 | ||
|
|
0610080d2a | ||
|
|
19d91cf07f | ||
|
|
d9b9b8c3da | ||
|
|
0889dc145c | ||
|
|
113a8d49f0 | ||
|
|
f60a968fb1 | ||
|
|
02b060178b | ||
|
|
b1f3afd494 | ||
|
|
35acac7b93 | ||
|
|
9334e7b7a8 | ||
|
|
f424314b0d | ||
|
|
c4a9025160 | ||
|
|
85a134ef53 | ||
|
|
db1fe0fe91 | ||
|
|
30a45fc329 | ||
|
|
ce90fc356c | ||
|
|
4f50e34b21 | ||
|
|
8ccf148eaa | ||
|
|
fa343bda20 | ||
|
|
a66f8d7065 | ||
|
|
819003e43c | ||
|
|
27800296fb | ||
|
|
b869ef072a | ||
|
|
03dfee468c | ||
|
|
d3275074bb | ||
|
|
cc0965f69c | ||
|
|
8e2fa3e153 | ||
|
|
df10bd7351 | ||
|
|
080289ca4e | ||
|
|
28b821f085 | ||
|
|
700f3ec029 | ||
|
|
d7b3ed0baa | ||
|
|
f1711014e5 | ||
|
|
29f2270443 | ||
|
|
39ee4e771c | ||
|
|
6e4d2d57fa | ||
|
|
7cd850a2e8 | ||
|
|
3280023823 | ||
|
|
3646a9610e | ||
|
|
ab639b3ee6 | ||
|
|
c85ba3fa75 | ||
|
|
193c2aecfb | ||
|
|
336de875ca | ||
|
|
eb5012f67e | ||
|
|
5c764b9b25 |
@@ -1,6 +1 @@
|
||||
# ignore everything
|
||||
*
|
||||
|
||||
# allow only what we need
|
||||
!commafeed-server/target/commafeed.jar
|
||||
!commafeed-server/config.yml.example
|
||||
commafeed-client
|
||||
3
.gitattributes
vendored
Normal file
3
.gitattributes
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
* text eol=lf
|
||||
*.cmd text eol=crlf
|
||||
*.png binary
|
||||
7
.github/FUNDING.yml
vendored
7
.github/FUNDING.yml
vendored
@@ -1,2 +1,5 @@
|
||||
github: [athou]
|
||||
custom: ['https://www.paypal.com/donate/?business=9CNQHMJG2ZJVY&no_recurring=0&item_name=CommaFeed¤cy_code=EUR']
|
||||
github: [ athou ]
|
||||
custom: [
|
||||
'https://github.com/sponsors/Athou',
|
||||
'https://www.paypal.com/donate/?business=9CNQHMJG2ZJVY&no_recurring=0&item_name=CommaFeed¤cy_code=EUR'
|
||||
]
|
||||
|
||||
3
.github/ISSUE_TEMPLATE/bug_report.md
vendored
3
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -25,7 +25,8 @@ If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Environment (please complete the following information):**
|
||||
|
||||
- CommaFeed version (or "commafeed.com"): 3.2.1
|
||||
- commafeed.com or self-hosted:
|
||||
- If self-hosted, CommaFeed version [e.g. 5.1.0 (a3dcb2c)]:
|
||||
- Browser [e.g. chrome, firefox]:
|
||||
- Device [e.g. desktop, mobile]:
|
||||
|
||||
|
||||
24
.github/dependabot.yml
vendored
24
.github/dependabot.yml
vendored
@@ -1,24 +0,0 @@
|
||||
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
|
||||
89
.github/workflows/build.yml
vendored
89
.github/workflows/build.yml
vendored
@@ -1,89 +0,0 @@
|
||||
name: Java CI
|
||||
|
||||
on: [ push ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
java: [ "17", "21" ]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# Setup
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: ${{ matrix.java }}
|
||||
distribution: "temurin"
|
||||
cache: "maven"
|
||||
|
||||
# Build
|
||||
- name: Build with Maven
|
||||
run: mvn --batch-mode --update-snapshots verify
|
||||
|
||||
- name: Upload JAR
|
||||
uses: actions/upload-artifact@v4
|
||||
if: ${{ matrix.java == '17' }}
|
||||
with:
|
||||
name: commafeed.jar
|
||||
path: commafeed-server/target/commafeed.jar
|
||||
|
||||
# Docker
|
||||
- name: Login to Container Registry
|
||||
uses: docker/login-action@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@v5
|
||||
if: ${{ matrix.java == '17' && github.ref_type == 'tag' }}
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm/v7,linux/arm64/v8
|
||||
tags: |
|
||||
athou/commafeed:latest
|
||||
athou/commafeed:${{ github.ref_name }}
|
||||
|
||||
- name: Docker build and push master
|
||||
uses: docker/build-push-action@v5
|
||||
if: ${{ matrix.java == '17' && github.ref_name == 'master' }}
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm/v7,linux/arm64/v8
|
||||
tags: athou/commafeed:master
|
||||
|
||||
# Create GitHub release after Docker image has been published
|
||||
- name: Extract Changelog Entry
|
||||
uses: mindsers/changelog-reader-action@v2
|
||||
if: ${{ matrix.java == '17' && github.ref_type == 'tag' }}
|
||||
id: changelog_reader
|
||||
with:
|
||||
version: ${{ github.ref_name }}
|
||||
|
||||
- name: Create GitHub release
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: ${{ matrix.java == '17' && github.ref_type == 'tag' }}
|
||||
with:
|
||||
name: CommaFeed ${{ github.ref_name }}
|
||||
body: ${{ steps.changelog_reader.outputs.changes }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
files: |
|
||||
commafeed-server/target/commafeed.jar
|
||||
commafeed-server/config.yml.example
|
||||
269
.github/workflows/ci.yml
vendored
Normal file
269
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,269 @@
|
||||
name: ci
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
JAVA_VERSION: 21
|
||||
DOCKER_BUILD_SUMMARY: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: github.event_name != 'pull_request' || github.actor != 'renovate[bot]' # renovate already triggers the build on pushes
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ "ubuntu-latest", "ubuntu-22.04-arm", "windows-latest" ]
|
||||
database: [ "h2", "postgresql", "mysql", "mariadb" ]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
# Checkout
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# Setup
|
||||
- name: Set up GraalVM
|
||||
uses: graalvm/setup-graalvm@01ed653ac833fe80569f1ef9f25585ba2811baab # v1
|
||||
with:
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
distribution: "graalvm"
|
||||
cache: "maven"
|
||||
|
||||
- name: Install Playwright dependencies
|
||||
run: sudo apt-get install -y libgbm1
|
||||
if: matrix.os != 'windows-latest'
|
||||
|
||||
# Build & Test
|
||||
- name: Build with Maven
|
||||
run: mvn --batch-mode --no-transfer-progress install -Pnative -P${{ matrix.database }} -DskipTests=${{ matrix.os == 'windows-latest' && matrix.database != 'h2' }}
|
||||
|
||||
# Build pages
|
||||
- name: Copy generated markdown documentation to /documentation
|
||||
run: mkdir documentation && cp ./commafeed-server/target/quarkus-generated-doc/config/commafeed-server.md ./documentation/README.md
|
||||
|
||||
- name: Generate pages
|
||||
uses: wranders/markdown-to-pages-action@8d8a750832932ac785f5424c8c5543aa0b26bb9a # v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
out_path: target/pages
|
||||
files: |-
|
||||
README.md
|
||||
documentation/README.md
|
||||
|
||||
# Upload artifacts
|
||||
- name: Upload cross-platform app
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
if: matrix.os == 'ubuntu-latest' # we only need to upload the cross-platform artifact once per database
|
||||
with:
|
||||
name: commafeed-${{ matrix.database }}-jvm
|
||||
path: commafeed-server/target/commafeed-*.zip
|
||||
|
||||
- name: Upload native executable
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: commafeed-${{ matrix.database }}-${{ runner.os }}-${{ runner.arch }}
|
||||
path: commafeed-server/target/commafeed-*-runner*
|
||||
|
||||
- name: Upload pages
|
||||
uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3
|
||||
if: matrix.os == 'ubuntu-latest' && matrix.database == 'h2' # we only need to upload the pages once
|
||||
with:
|
||||
path: target/pages
|
||||
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
env:
|
||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
database: [ "h2", "postgresql", "mysql", "mariadb" ]
|
||||
|
||||
steps:
|
||||
# Checkout
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# Setup
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3
|
||||
|
||||
- name: Install required packages
|
||||
run: sudo apt-get install -y rename unzip
|
||||
|
||||
# Prepare artifacts
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
|
||||
with:
|
||||
pattern: commafeed-${{ matrix.database }}-*
|
||||
path: ./artifacts
|
||||
merge-multiple: true
|
||||
|
||||
- name: Set the exec flag on the native executables
|
||||
run: chmod +x artifacts/*-runner
|
||||
|
||||
- name: Rename native executables to match buildx TARGETARCH
|
||||
run: |
|
||||
rename 's/x86_64/amd64/g' artifacts/*
|
||||
rename 's/aarch_64/arm64/g' artifacts/*
|
||||
|
||||
- name: Unzip jvm package
|
||||
run: |
|
||||
unzip artifacts/*-jvm.zip -d artifacts/extracted-jvm-package
|
||||
rename 's/commafeed-.*/quarkus-app/g' artifacts/extracted-jvm-package/*
|
||||
|
||||
# Docker
|
||||
- name: Login to Container Registry
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
|
||||
if: ${{ env.DOCKERHUB_USERNAME != '' }}
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
## build but don't push for PRs and renovate
|
||||
- name: Docker build - native
|
||||
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6
|
||||
with:
|
||||
context: .
|
||||
file: commafeed-server/src/main/docker/Dockerfile.native
|
||||
push: false
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
|
||||
- name: Docker build - jvm
|
||||
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6
|
||||
with:
|
||||
context: .
|
||||
file: commafeed-server/src/main/docker/Dockerfile.jvm
|
||||
push: false
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
|
||||
## build and push tag
|
||||
- name: Docker build and push tag - native
|
||||
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6
|
||||
if: ${{ github.ref_type == 'tag' }}
|
||||
with:
|
||||
context: .
|
||||
file: commafeed-server/src/main/docker/Dockerfile.native
|
||||
push: ${{ env.DOCKERHUB_USERNAME != '' }}
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
tags: |
|
||||
athou/commafeed:latest-${{ matrix.database }}
|
||||
athou/commafeed:${{ github.ref_name }}-${{ matrix.database }}
|
||||
|
||||
- name: Docker build and push tag - jvm
|
||||
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6
|
||||
if: ${{ github.ref_type == 'tag' }}
|
||||
with:
|
||||
context: .
|
||||
file: commafeed-server/src/main/docker/Dockerfile.jvm
|
||||
push: ${{ env.DOCKERHUB_USERNAME != '' }}
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
tags: |
|
||||
athou/commafeed:latest-${{ matrix.database }}-jvm
|
||||
athou/commafeed:${{ github.ref_name }}-${{ matrix.database }}-jvm
|
||||
|
||||
## build and push master
|
||||
- name: Docker build and push master - native
|
||||
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6
|
||||
if: ${{ github.ref_name == 'master' }}
|
||||
with:
|
||||
context: .
|
||||
file: commafeed-server/src/main/docker/Dockerfile.native
|
||||
push: ${{ env.DOCKERHUB_USERNAME != '' }}
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
tags: athou/commafeed:master-${{ matrix.database }}
|
||||
|
||||
- name: Docker build and push master - jvm
|
||||
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6
|
||||
if: ${{ github.ref_name == 'master' }}
|
||||
with:
|
||||
context: .
|
||||
file: commafeed-server/src/main/docker/Dockerfile.jvm
|
||||
push: ${{ env.DOCKERHUB_USERNAME != '' }}
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
tags: athou/commafeed:master-${{ matrix.database }}-jvm
|
||||
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build
|
||||
- docker
|
||||
permissions:
|
||||
contents: write
|
||||
if: github.ref_type == 'tag'
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
|
||||
with:
|
||||
pattern: commafeed-*
|
||||
path: ./artifacts
|
||||
merge-multiple: true
|
||||
|
||||
- name: Set the exec flag on the native executables
|
||||
run: chmod +x artifacts/*-runner
|
||||
|
||||
- name: Extract Changelog Entry
|
||||
uses: mindsers/changelog-reader-action@32aa5b4c155d76c94e4ec883a223c947b2f02656 # v2
|
||||
id: changelog_reader
|
||||
with:
|
||||
version: ${{ github.ref_name }}
|
||||
|
||||
- name: Create GitHub release
|
||||
uses: ncipollo/release-action@440c8c1cb0ed28b9f43e4d1d670870f059653174 # v1
|
||||
with:
|
||||
name: CommaFeed ${{ github.ref_name }}
|
||||
body: ${{ steps.changelog_reader.outputs.changes }}
|
||||
artifacts: ./artifacts/*
|
||||
|
||||
|
||||
update-dockerhub-description:
|
||||
runs-on: ubuntu-latest
|
||||
needs: release
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Update Docker Hub Description
|
||||
uses: peter-evans/dockerhub-description@432a30c9e07499fd01da9f8a49f0faf9e0ca5b77 # v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
repository: athou/commafeed
|
||||
short-description: ${{ github.event.repository.description }}
|
||||
readme-filepath: commafeed-server/src/main/docker/README.md
|
||||
|
||||
|
||||
deploy-pages:
|
||||
runs-on: ubuntu-latest
|
||||
needs: release
|
||||
permissions:
|
||||
pages: write
|
||||
id-token: write
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
|
||||
steps:
|
||||
- uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4
|
||||
id: deployment
|
||||
BIN
.mvn/wrapper/maven-wrapper.jar
vendored
BIN
.mvn/wrapper/maven-wrapper.jar
vendored
Binary file not shown.
6
.mvn/wrapper/maven-wrapper.properties
vendored
6
.mvn/wrapper/maven-wrapper.properties
vendored
@@ -6,7 +6,7 @@
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
@@ -14,5 +14,5 @@
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.3/apache-maven-3.8.3-bin.zip
|
||||
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar
|
||||
distributionType=only-script
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
|
||||
|
||||
157
CHANGELOG.md
157
CHANGELOG.md
@@ -1,5 +1,162 @@
|
||||
# Changelog
|
||||
|
||||
## [5.7.0]
|
||||
|
||||
- Add Shift+J/Shift+K keyboard shortcuts to navigate to the next/previous feed or category with unread entries (#1746)
|
||||
- Add the referrer "no-referrer" meta to index.html (#1724)
|
||||
- Load custom JS code when the app is done loading (#1724)
|
||||
- Correctly handle feeds that return an unmodified Last-Modified header but a different ETag header (#1730)
|
||||
- Restore gzip compression of responses that was accidentaly disabled since 5.0.0
|
||||
- Fix tooltips not showing up in mobile view
|
||||
- Fix the bookmarklet generator on the About page
|
||||
|
||||
## [5.6.1]
|
||||
|
||||
- Restore support for iframes in feed entries (#1688)
|
||||
- There is now a package available for Arch Linux thanks to @dcelasun (#1691)
|
||||
|
||||
## [5.6.0]
|
||||
|
||||
- To better respect the bandwidth of feed owners, the default value of `commafeed.feed-refresh.interval-empirical` is now true. This means feeds no longer refresh exactly every 5 minutes (the default value of `commafeed.feed-refresh.interval`) but between 5 minutes and 4 hours (the default value of the new `commafeed.feed-refresh.max-interval` setting). The interval is calculated based on feed activity, so highly active feeds refresh more often (#1677)
|
||||
- Many previously hardcoded values used in feed refresh interval calculation are now exposed as settings (#1677)
|
||||
- Access to local addresses is now blocked to mitigate server-side request forgery (SSRF) attacks, which could potentially expose internal resources. You might want to disable the new `commafeed.http-client.block-local-addresses` setting if you subscribe to feeds only available on your local network and you trust all your users
|
||||
- If a feed responds with a "429 - Too many requests" response, a backoff mechanism is triggered when the response does not contain a "Retry-After" header
|
||||
|
||||
## [5.5.0]
|
||||
|
||||
- CommaFeed now honors the Retry-After response header and will not try to refresh a feed sooner than the value of this header
|
||||
- Audio enclosures (e.g. podcasts) now fill available entry width
|
||||
- Fix an issue with some labels not correctly internationalized
|
||||
|
||||
## [5.4.0]
|
||||
|
||||
- An arm64 native executable is now available for download on the releases page
|
||||
- The native executable Docker image now supports arm64
|
||||
- Fixed an issue with feeds that declared an invalid DOCTYPE (#1260)
|
||||
|
||||
## [5.3.6]
|
||||
|
||||
- Ignore invalid Cache-Control header values (#1619)
|
||||
|
||||
## [5.3.5]
|
||||
|
||||
- Fixed an issue with the aspect ratio of images of some feeds (#1595)
|
||||
- CommaFeed now honors the Cache-Control response header and will not try to refresh a feed sooner than its max-age property (#1615)
|
||||
- Added support for compilation with JDK 23+. If you're building CommaFeed from sources with a JDK 17 or 21, you may need to update it to the most recent patch version to support `-proc:full` (#1618)
|
||||
|
||||
## [5.3.4]
|
||||
|
||||
- Added support for Internationalized Domain Names (#1588)
|
||||
|
||||
## [5.3.3]
|
||||
|
||||
- Removed image bottom margins (#1587)
|
||||
|
||||
## [5.3.2]
|
||||
|
||||
- Fixed an issue that could cause some images from not being rendered correctly (#1587)
|
||||
|
||||
## [5.3.1]
|
||||
|
||||
- Fixed an issue that could cause some HTTP feeds to return a 400 error (#1572)
|
||||
|
||||
## [5.3.0]
|
||||
|
||||
- Added a setting to set a cooldown on the "fetch all my feeds" action, disabled by default (#1556)
|
||||
- Fixed an issue that could cause entries to not correctly load when using the "next" header button (#1557)
|
||||
|
||||
## [5.2.0]
|
||||
|
||||
- Added an option to keep a number of entries above the selected entry when scrolling
|
||||
- Added a cache to the HTTP client to reduce the number of requests made to feeds when subscribing (#1431)
|
||||
- Feeds are no longer refreshed between the moment its last user unsubscribes and the moment the feed is cleaned up (every hour)
|
||||
- Fixed an issue that could cause entries to not correctly load when using keyboard navigation (#1557)
|
||||
|
||||
## [5.1.1]
|
||||
|
||||
- Fixed database migration issue when upgrading from 5.0.0 to 5.1.0 on MariaDB (#1544)
|
||||
- When feeds without unread entries are hidden from the tree, the feed is displayed in the tree until another one is selected (#1543)
|
||||
|
||||
## [5.1.0]
|
||||
|
||||
- Added a setting for showing/hiding unread count in the browser's tab title/favicon (#1518)
|
||||
- Fixed an issue that could prevent the app from starting on some systems (#1532)
|
||||
- Added a cache busting filter for the webapp index.html and openapi documentation to make sure they are always up to date
|
||||
- Reduced database cleanup log verbosity
|
||||
|
||||
## [5.0.2]
|
||||
|
||||
- Fix favicon fetching for Youtube channels in native mode when Google auth key is set
|
||||
- Fix an error that appears in the logs when fetching some favicons
|
||||
|
||||
## [5.0.1]
|
||||
|
||||
- Configure native compilation to support older CPU architectures (#1524)
|
||||
|
||||
## [5.0.0]
|
||||
|
||||
CommaFeed is now powered by Quarkus instead of Dropwizard. Read the rationale behind this change in
|
||||
the [announcement](https://github.com/Athou/commafeed/discussions/1517).
|
||||
The gist of it is that CommaFeed can now be compiled to a native binary, resulting in blazing fast startup times (around
|
||||
0.3s) and very low memory footprint (< 50M).
|
||||
|
||||
- CommaFeed now has a different package for each supported database.
|
||||
- If you are deploying CommaFeed with a precompiled package, please
|
||||
read [this section of the README](https://github.com/Athou/commafeed/tree/master?tab=readme-ov-file#download-a-precompiled-package).
|
||||
- If you are building CommaFeed from sources, please
|
||||
read [this section of the README](https://github.com/Athou/commafeed/tree/master?tab=readme-ov-file#build-from-sources).
|
||||
- If you are using the Docker image, please read the instructions on
|
||||
the [Docker Hub page](https://hub.docker.com/r/athou/commafeed).
|
||||
- Due to the switch to Quarkus, the way CommaFeed is configured is very different (the `config.yml` file is gone).
|
||||
Please
|
||||
read [this section of the README](https://github.com/Athou/commafeed/tree/master?tab=readme-ov-file#configuration).
|
||||
Note that a lot of configuration elements have been removed or renamed and are now nested/grouped by feature.
|
||||
- Added a setting to prevent parsing large feeds to avoid out of memory errors. The default is 5MB.
|
||||
- Use a different icon for filtering unread entries and marking an entry as read (#1506)
|
||||
- Added various HTML attributes to ease custom JS/CSS customization (#1507)
|
||||
- The Redis cache has been removed. There have been multiple enhancements to the feed refresh engine and it is no longer
|
||||
needed, even for instances with a large number of feeds.
|
||||
- The H2 migration tool that automatically upgrades H2 databases from format 2 to 3 has been removed. If you're using
|
||||
the H2 embedded database, please upgrade to at least version 4.3.0 before upgrading to CommaFeed 5.0.0.
|
||||
|
||||
## [4.6.0]
|
||||
|
||||
- switched from Temurin to OpenJ9 as the JVM used in the Docker image, resulting in memory usage reduction by up to 50%
|
||||
- fix an issue that could cause old entries to reappear if they were updated by their author (#1486)
|
||||
- show all entries regardless of their read status when searching with keywords, even if the ui is configured to show
|
||||
unread entries only
|
||||
|
||||
## [4.5.0]
|
||||
|
||||
- significantly reduce the time needed to retrieve entries or mark them as read, especially when there are a lot of
|
||||
entries (#1452)
|
||||
- fix a race condition where a feed could be refreshed before it was created in the database
|
||||
- fix an issue that could cause the websocket notification to contain the wrong number of unread entries when using
|
||||
mysql/mariadb
|
||||
- fix an error when trying to mark all starred entries as read
|
||||
- remove the `onlyIds` parameter from REST endpoints since retrieving all the entries is now just as fast
|
||||
- remove support for microsoft sqlserver because it's not covered with integration tests (please open an issue if you'd
|
||||
like it back)
|
||||
|
||||
## [4.4.1]
|
||||
|
||||
- fix vertical scrolling issues with Safari (#1168)
|
||||
- the default value for new users for the "star entry" button and the "open in new tab" button in the entry headers is
|
||||
now "on desktop" instead of "always"
|
||||
- the "keyboard shortcuts" help page now shows "Cmd" instead of "Ctrl" on macOS (#1389)
|
||||
- remove a superfluous feed fetch when subscribing to a feed (#1431)
|
||||
- the Docker image now uses Java 21
|
||||
|
||||
## [4.4.0]
|
||||
|
||||
- add support for sharing using the browser native capabilities if available (#1255)
|
||||
- add a button in the entry headers to star an entry (#1025)
|
||||
- add a button in the entry headers to open links in a new tab (#1333)
|
||||
- add two options in the settings to toggle those buttons
|
||||
- accept .opml file extension when importing and export with the .opml extension
|
||||
- the "mark as read" option is no longer shown in the context menu for entries that are too old to be marked as read (
|
||||
older than `keepStatusDays`) (#1303)
|
||||
|
||||
## [4.3.3]
|
||||
|
||||
- fix OPML import (#1279)
|
||||
|
||||
12
Dockerfile
12
Dockerfile
@@ -1,12 +0,0 @@
|
||||
FROM eclipse-temurin:17-jre
|
||||
|
||||
EXPOSE 8082
|
||||
|
||||
RUN mkdir -p /commafeed/data
|
||||
VOLUME /commafeed/data
|
||||
|
||||
COPY commafeed-server/config.yml.example config.yml
|
||||
COPY commafeed-server/target/commafeed.jar .
|
||||
|
||||
ENV JAVA_TOOL_OPTIONS -Djava.net.preferIPv4Stack=true -Xms20m -XX:+UseG1GC -XX:-ShrinkHeapInSteps -XX:G1PeriodicGCInterval=10000 -XX:-G1PeriodicGCInvokesConcurrent -XX:MinHeapFreeRatio=5 -XX:MaxHeapFreeRatio=10
|
||||
CMD ["java", "-jar", "commafeed.jar", "server", "config.yml"]
|
||||
122
README.md
122
README.md
@@ -1,6 +1,6 @@
|
||||
# CommaFeed
|
||||
|
||||
Google Reader inspired self-hosted RSS reader, based on Dropwizard and React/TypeScript.
|
||||
Google Reader inspired self-hosted RSS reader, based on Quarkus and React/TypeScript.
|
||||
|
||||

|
||||
|
||||
@@ -8,14 +8,22 @@ Google Reader inspired self-hosted RSS reader, based on Dropwizard and React/Typ
|
||||
|
||||
- 4 different layouts
|
||||
- Light/Dark theme
|
||||
- Fully responsive
|
||||
- Fully responsive, works great on both mobile and desktop
|
||||
- Keyboard shortcuts for almost everything
|
||||
- Support for right-to-left feeds
|
||||
- Translated in 25+ languages
|
||||
- Supports thousands of users and millions of feeds
|
||||
- OPML import/export
|
||||
- REST API and a Fever-compatible API for native mobile apps
|
||||
- REST API
|
||||
- Fever-compatible API for native mobile apps
|
||||
- Can automatically mark articles as read based on user-defined rules
|
||||
- [Browser extension](https://github.com/Athou/commafeed-browser-extension)
|
||||
- Compiles to native code for blazing fast startup and low memory usage
|
||||
- Supports 4 databases
|
||||
- H2 (embedded database)
|
||||
- PostgreSQL
|
||||
- MySQL
|
||||
- MariaDB
|
||||
|
||||
## Deployment
|
||||
|
||||
@@ -33,50 +41,116 @@ PikaPods shares 20% of the revenue back to CommaFeed.
|
||||
|
||||
[](https://www.pikapods.com/pods?run=commafeed)
|
||||
|
||||
### Download precompiled package
|
||||
### Download a precompiled package
|
||||
|
||||
mkdir commafeed && cd commafeed
|
||||
wget https://github.com/Athou/commafeed/releases/latest/download/commafeed.jar
|
||||
wget https://github.com/Athou/commafeed/releases/latest/download/config.yml.example -O config.yml
|
||||
java -Djava.net.preferIPv4Stack=true -jar commafeed.jar server config.yml
|
||||
Go to the [release page](https://github.com/Athou/commafeed/releases) and download the latest version for your operating
|
||||
system and database of choice.
|
||||
|
||||
The server will listen on http://localhost:8082. The default
|
||||
user is `admin` and the default password is `admin`.
|
||||
There are two types of packages:
|
||||
|
||||
- The `linux-x86_64`, `linux-aarch_64` and `windows-x86_64` packages are compiled natively and contain an executable that can be run
|
||||
directly.
|
||||
- The `jvm` package is a zip file containing all `.jar` files required to run the application. This package works on all
|
||||
platforms but requires a JRE and is started with `java -jar quarkus-run.jar`.
|
||||
|
||||
If available for your operating system, the native package is recommended because it has a faster startup time and lower
|
||||
memory usage.
|
||||
|
||||
### Build from sources
|
||||
|
||||
git clone https://github.com/Athou/commafeed.git
|
||||
cd commafeed
|
||||
./mvnw clean package
|
||||
cp commafeed-server/config.yml.example config.yml
|
||||
java -Djava.net.preferIPv4Stack=true -jar commafeed-server/target/commafeed.jar server config.yml
|
||||
./mvnw clean package [-P<database> [-Pnative]] [-DskipTests]
|
||||
|
||||
The server will listen on http://localhost:8082. The default
|
||||
user is `admin` and the default password is `admin`.
|
||||
- `<database>` can be one of `h2`, `postgresql`, `mysql` or `mariadb`. The default is `h2`.
|
||||
- `-Pnative` compiles the application to native code. This requires GraalVM to be installed (`GRAALVM_HOME` environment
|
||||
variable pointing to a GraalVM installation).
|
||||
- `-DskipTests` to speed up the build process by skipping tests.
|
||||
|
||||
When the build is complete:
|
||||
|
||||
- a zip containing all jars required to run the application is located at
|
||||
`commafeed-server/target/commafeed-<version>-<database>-jvm.zip`. Extract it and run the application with
|
||||
`java -jar quarkus-run.jar`
|
||||
- if you used the native profile, the executable is located at
|
||||
`commafeed-server/target/commafeed-<version>-<database>-<platform>-<arch>-runner[.exe]`
|
||||
|
||||
### Distribution packages
|
||||
|
||||
- Arch Linux users can use [the CommaFeed package on AUR](https://aur.archlinux.org/pkgbase/commafeed), which builds native binaries with GraalVM for all supported databases.
|
||||
|
||||
## Configuration
|
||||
|
||||
CommaFeed doesn't require any configuration to run with its embedded database (H2). The database file will be stored in
|
||||
the `data` directory of the current directory.
|
||||
|
||||
To use a different database, you will need to configure the following properties:
|
||||
|
||||
- `quarkus.datasource.jdbc.url`
|
||||
- e.g. for H2: `jdbc:h2:./data/db;DEFRAG_ALWAYS=TRUE`
|
||||
- e.g. for PostgreSQL: `jdbc:postgresql://localhost:5432/commafeed`
|
||||
- e.g. for MySQL:
|
||||
`jdbc:mysql://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC`
|
||||
- e.g. for MariaDB:
|
||||
`jdbc:mariadb://localhost/commafeed?autoReconnect=true&failOverReadOnly=false&maxReconnects=20&rewriteBatchedStatements=true&timezone=UTC`
|
||||
- `quarkus.datasource.username`
|
||||
- `quarkus.datasource.password`
|
||||
|
||||
There are multiple ways to configure CommaFeed:
|
||||
|
||||
- a `config/application.properties` [properties](https://en.wikipedia.org/wiki/.properties) file relative to the working
|
||||
directory (keys in kebab-case)
|
||||
- Command line arguments each prefixed with `-D` (keys in kebab-case)
|
||||
- Environment variables (keys in UPPER_CASE)
|
||||
- a `.env` file in the working directory (keys in UPPER_CASE)
|
||||
|
||||
The properties file is recommended because CommaFeed will be able to warn about invalid properties and typos.
|
||||
|
||||
All [CommaFeed settings](https://athou.github.io/commafeed/documentation) are optional and have sensible default values.
|
||||
|
||||
When logging in, credentials are stored in an encrypted cookie. The encryption key is randomly generated at startup,
|
||||
meaning that you will have to log back in after each restart of the application. To prevent this, you can set the
|
||||
`quarkus.http.auth.session.encryption-key` property to a fixed value (min. 16 characters).
|
||||
All other Quarkus settings can be found [here](https://quarkus.io/guides/all-config).
|
||||
|
||||
When started, the server will listen on http://localhost:8082.
|
||||
The default user is `admin` and the default password is `admin`.
|
||||
|
||||
### Updates
|
||||
|
||||
When CommaFeed is up and running, you can subscribe to [this feed](https://github.com/Athou/commafeed/releases.atom) to be notified of new releases.
|
||||
|
||||
### Memory management
|
||||
|
||||
The Java Virtual Machine (JVM) is rather greedy by default and will not release unused memory to the
|
||||
operating system. This is because acquiring memory from the operating system is a relatively expensive operation.
|
||||
However, this can be problematic on systems with limited memory.
|
||||
This can be problematic on systems with limited memory.
|
||||
|
||||
#### Hard limit
|
||||
#### Hard limit (`native` and `jvm` packages)
|
||||
|
||||
The JVM can be configured to use a maximum amount of memory with the `-Xmx` parameter.
|
||||
For example, to limit the JVM to 256MB of memory, use `-Xmx256m`.
|
||||
|
||||
#### Dynamic sizing
|
||||
#### Dynamic sizing (`jvm` package)
|
||||
|
||||
The JVM can be configured to release unused memory to the operating system with the following parameters:
|
||||
In addition to the previous setting, the JVM can be configured to release unused memory to the operating system with the
|
||||
following parameters:
|
||||
|
||||
-Xms20m -XX:+UseG1GC -XX:-ShrinkHeapInSteps -XX:G1PeriodicGCInterval=10000 -XX:-G1PeriodicGCInvokesConcurrent -XX:MinHeapFreeRatio=5 -XX:MaxHeapFreeRatio=10
|
||||
-Xms20m -XX:+UseG1GC -XX:+UseStringDeduplication -XX:-ShrinkHeapInSteps -XX:G1PeriodicGCInterval=10000 -XX:-G1PeriodicGCInvokesConcurrent -XX:MinHeapFreeRatio=5 -XX:MaxHeapFreeRatio=10
|
||||
|
||||
This is how the Docker image is configured.
|
||||
See [here](https://docs.oracle.com/en/java/javase/17/gctuning/garbage-first-g1-garbage-collector1.html)
|
||||
and [here](https://docs.oracle.com/en/java/javase/17/gctuning/factors-affecting-garbage-collection-performance.html) for
|
||||
more
|
||||
information.
|
||||
|
||||
#### OpenJ9 (`jvm` package)
|
||||
|
||||
The [OpenJ9](https://eclipse.dev/openj9/) JVM is a more memory-efficient alternative to the HotSpot JVM, at the cost of
|
||||
slightly slower throughput.
|
||||
|
||||
IBM provides precompiled binaries for OpenJ9
|
||||
named [Semeru](https://developer.ibm.com/languages/java/semeru-runtimes/downloads/).
|
||||
This is the JVM used in
|
||||
the [Docker image](https://github.com/Athou/commafeed/blob/master/commafeed-server/src/main/docker/Dockerfile.jvm).
|
||||
|
||||
## Translation
|
||||
|
||||
Files for internationalization are
|
||||
@@ -99,7 +173,7 @@ two-letters [ISO-639-1 language code](http://en.wikipedia.org/wiki/List_of_ISO_6
|
||||
|
||||
- Open `commafeed-server` in your preferred Java IDE.
|
||||
- CommaFeed uses Lombok, you need the Lombok plugin for your IDE.
|
||||
- Start `CommaFeedApplication.java` in debug mode with `server config.dev.yml` as arguments
|
||||
- run `./mvnw quarkus:dev`
|
||||
|
||||
### Frontend
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
dist
|
||||
node_modules
|
||||
|
||||
vite.config.ts
|
||||
|
||||
# compiled linguijs locales
|
||||
# they no longer exist but we keep this to avoid issues with people still having those files on disk
|
||||
src/locales/**/*.ts
|
||||
@@ -1,47 +0,0 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
browser: true,
|
||||
es2021: true,
|
||||
},
|
||||
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",
|
||||
},
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
env: {
|
||||
node: true,
|
||||
},
|
||||
files: [".eslintrc.{js,cjs}"],
|
||||
parserOptions: {
|
||||
sourceType: "script",
|
||||
},
|
||||
},
|
||||
],
|
||||
parserOptions: {
|
||||
project: true,
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
},
|
||||
plugins: ["react"],
|
||||
rules: {
|
||||
"@typescript-eslint/consistent-type-assertions": ["error", { assertionStyle: "as" }],
|
||||
"@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 }],
|
||||
"react/no-unescaped-entities": "off",
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"react-hooks/exhaustive-deps": "error",
|
||||
},
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
{
|
||||
"locales": [
|
||||
"ar",
|
||||
"ca",
|
||||
"cs",
|
||||
"cy",
|
||||
"da",
|
||||
"de",
|
||||
"en",
|
||||
"es",
|
||||
"fa",
|
||||
"fi",
|
||||
"fr",
|
||||
"gl",
|
||||
"hu",
|
||||
"id",
|
||||
"it",
|
||||
"ja",
|
||||
"ko",
|
||||
"ms",
|
||||
"nb",
|
||||
"nl",
|
||||
"nn",
|
||||
"pl",
|
||||
"pt",
|
||||
"ru",
|
||||
"sk",
|
||||
"sv",
|
||||
"tr",
|
||||
"zh"
|
||||
],
|
||||
"catalogs": [
|
||||
{
|
||||
"path": "src/locales/{locale}/messages",
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"exclude": [
|
||||
"src/locales/**"
|
||||
]
|
||||
}
|
||||
],
|
||||
"format": "po",
|
||||
"formatOptions": {
|
||||
"origins": true,
|
||||
"lineNumbers": false
|
||||
},
|
||||
"sourceLocale": "en",
|
||||
"fallbackLocales": {
|
||||
"default": "en"
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"printWidth": 140,
|
||||
"semi": false,
|
||||
"tabWidth": 4,
|
||||
"arrowParens": "avoid",
|
||||
"endOfLine": "auto",
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
19
commafeed-client/biome.json
Normal file
19
commafeed-client/biome.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
||||
"formatter": {
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 4,
|
||||
"lineEnding": "lf",
|
||||
"lineWidth": 140
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"trailingCommas": "es5",
|
||||
"semicolons": "asNeeded",
|
||||
"arrowParentheses": "asNeeded"
|
||||
}
|
||||
},
|
||||
"files": {
|
||||
"ignore": ["dist", "node_modules", "target", "target-ide"]
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||
<link rel="manifest" href="manifest.json" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<title>CommaFeed</title>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||
<link rel="manifest" href="manifest.json" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name="referrer" content="no-referrer" />
|
||||
|
||||
<title>CommaFeed</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
52
commafeed-client/lingui.config.ts
Normal file
52
commafeed-client/lingui.config.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import type { LinguiConfig } from "@lingui/conf"
|
||||
|
||||
const config: LinguiConfig = {
|
||||
locales: [
|
||||
"ar",
|
||||
"ca",
|
||||
"cs",
|
||||
"cy",
|
||||
"da",
|
||||
"de",
|
||||
"en",
|
||||
"es",
|
||||
"fa",
|
||||
"fi",
|
||||
"fr",
|
||||
"gl",
|
||||
"hu",
|
||||
"id",
|
||||
"it",
|
||||
"ja",
|
||||
"ko",
|
||||
"ms",
|
||||
"nb",
|
||||
"nl",
|
||||
"nn",
|
||||
"pl",
|
||||
"pt",
|
||||
"ru",
|
||||
"sk",
|
||||
"sv",
|
||||
"tr",
|
||||
"zh",
|
||||
],
|
||||
catalogs: [
|
||||
{
|
||||
path: "src/locales/{locale}/messages",
|
||||
include: ["src"],
|
||||
exclude: ["src/locales/**"],
|
||||
},
|
||||
],
|
||||
format: "po",
|
||||
formatOptions: {
|
||||
origins: true,
|
||||
lineNumbers: false,
|
||||
},
|
||||
sourceLocale: "en",
|
||||
fallbackLocales: {
|
||||
default: "en",
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
||||
18146
commafeed-client/package-lock.json
generated
18146
commafeed-client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,82 +1,84 @@
|
||||
{
|
||||
"name": "commafeed-client",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host",
|
||||
"dev:typescript": "tsc --watch",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest",
|
||||
"test:ci": "vitest run",
|
||||
"eslint": "eslint --ext=.js,.jsx,.ts,.tsx src",
|
||||
"i18n:extract": "lingui extract --clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@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.2.1",
|
||||
"axios": "^1.6.7",
|
||||
"dayjs": "^1.11.10",
|
||||
"escape-string-regexp": "^5.0.0",
|
||||
"interweave": "^13.1.0",
|
||||
"monaco-editor": "^0.46.0",
|
||||
"mousetrap": "^1.6.5",
|
||||
"react": "^18.2.0",
|
||||
"react-async-hook": "^4.0.0",
|
||||
"react-contexify": "^6.0.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-draggable": "^4.4.6",
|
||||
"react-ga4": "^2.1.0",
|
||||
"react-icons": "^5.0.1",
|
||||
"react-infinite-scroller": "^1.2.6",
|
||||
"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.4",
|
||||
"use-local-storage": "^3.0.0",
|
||||
"websocket-heartbeat-js": "^1.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lingui/cli": "^4.7.1",
|
||||
"@lingui/vite-plugin": "^4.7.1",
|
||||
"@types/mousetrap": "^1.6.15",
|
||||
"@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": "^7.1.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"babel-plugin-macros": "^3.1.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"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.2.5",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.1.4",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
"vite-tsconfig-paths": "^4.3.1",
|
||||
"vitest": "^1.3.1",
|
||||
"vitest-mock-extended": "^1.3.1"
|
||||
}
|
||||
"name": "commafeed-client",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev:host": "vite --host",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest",
|
||||
"test:ci": "vitest run",
|
||||
"lint": "biome check",
|
||||
"lint:fix": "biome check --write",
|
||||
"i18n:extract": "lingui extract --clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@fontsource/open-sans": "^5.2.5",
|
||||
"@lingui/core": "^5.3.0",
|
||||
"@lingui/react": "^5.3.0",
|
||||
"@mantine/core": "^7.17.3",
|
||||
"@mantine/form": "^7.17.3",
|
||||
"@mantine/hooks": "^7.17.3",
|
||||
"@mantine/modals": "^7.17.3",
|
||||
"@mantine/notifications": "^7.17.3",
|
||||
"@mantine/spotlight": "^7.17.3",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@reduxjs/toolkit": "^2.6.1",
|
||||
"axios": "^1.8.4",
|
||||
"dayjs": "^1.11.13",
|
||||
"escape-string-regexp": "^5.0.0",
|
||||
"interweave": "^13.1.1",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"mousetrap": "^1.6.5",
|
||||
"react": "^19.1.0",
|
||||
"react-async-hook": "^4.0.0",
|
||||
"react-contexify": "^6.0.0",
|
||||
"react-device-detect": "^2.2.3",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-draggable": "^4.4.6",
|
||||
"react-ga4": "^2.1.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-infinite-scroller": "^1.2.6",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router-dom": "^7.4.1",
|
||||
"react-swipeable": "^7.0.2",
|
||||
"redoc": "^2.4.0",
|
||||
"style-to-object": "^1.0.8",
|
||||
"throttle-debounce": "^5.0.2",
|
||||
"tinycon": "^0.6.8",
|
||||
"tss-react": "^4.9.16",
|
||||
"websocket-heartbeat-js": "^1.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@lingui/babel-plugin-lingui-macro": "^5.3.0",
|
||||
"@lingui/cli": "^5.3.0",
|
||||
"@lingui/vite-plugin": "^5.3.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/mousetrap": "^1.6.15",
|
||||
"@types/react": "^19.1.0",
|
||||
"@types/react-dom": "^19.1.1",
|
||||
"@types/react-infinite-scroller": "^1.2.5",
|
||||
"@types/throttle-debounce": "^5.0.2",
|
||||
"@types/tinycon": "^0.6.7",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"babel-plugin-macros": "^3.1.0",
|
||||
"jsdom": "^26.0.0",
|
||||
"rollup-plugin-visualizer": "^5.14.0",
|
||||
"typescript": "^5.8.2",
|
||||
"vite": "^6.2.5",
|
||||
"vite-plugin-checker": "^0.9.1",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^3.1.1"
|
||||
},
|
||||
"overrides": {
|
||||
"react-infinite-scroller": {
|
||||
"react": "^19.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,29 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.commafeed</groupId>
|
||||
<artifactId>commafeed</artifactId>
|
||||
<version>4.3.3</version>
|
||||
<version>5.7.0</version>
|
||||
</parent>
|
||||
<artifactId>commafeed-client</artifactId>
|
||||
<name>CommaFeed Client</name>
|
||||
|
||||
<properties>
|
||||
<!-- renovate: datasource=node-version depName=node -->
|
||||
<node.version>v22.14.0</node.version>
|
||||
<!-- renovate: datasource=npm depName=npm -->
|
||||
<npm.version>11.2.0</npm.version>
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>com.github.eirslett</groupId>
|
||||
<artifactId>frontend-maven-plugin</artifactId>
|
||||
<version>1.15.0</version>
|
||||
<version>1.15.1</version>
|
||||
<?m2e ignore?>
|
||||
<executions>
|
||||
<execution>
|
||||
@@ -25,8 +33,8 @@
|
||||
</goals>
|
||||
<phase>compile</phase>
|
||||
<configuration>
|
||||
<nodeVersion>v20.10.0</nodeVersion>
|
||||
<npmVersion>10.2.5</npmVersion>
|
||||
<nodeVersion>${node.version}</nodeVersion>
|
||||
<npmVersion>${npm.version}</npmVersion>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
@@ -47,6 +55,7 @@
|
||||
<phase>compile</phase>
|
||||
<configuration>
|
||||
<arguments>run test:ci</arguments>
|
||||
<skip>${skipTests}</skip>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
@@ -72,7 +81,7 @@
|
||||
<goal>copy-resources</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<outputDirectory>${project.build.directory}/classes/assets</outputDirectory>
|
||||
<outputDirectory>${project.build.directory}/classes/META-INF/resources</outputDirectory>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>dist</directory>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { i18n } from "@lingui/core"
|
||||
import { I18nProvider } from "@lingui/react"
|
||||
import { MantineProvider } from "@mantine/core"
|
||||
import { useDidUpdate } from "@mantine/hooks"
|
||||
import { ModalsProvider } from "@mantine/modals"
|
||||
import { Notifications } from "@mantine/notifications"
|
||||
import { Constants } from "app/constants"
|
||||
@@ -9,11 +8,14 @@ import { redirectTo } from "app/redirect/slice"
|
||||
import { reloadServerInfos } from "app/server/thunks"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import { categoryUnreadCount } from "app/utils"
|
||||
import { DisablePullToRefresh } from "components/DisablePullToRefresh"
|
||||
import { ErrorBoundary } from "components/ErrorBoundary"
|
||||
import { Header } from "components/header/Header"
|
||||
import { Tree } from "components/sidebar/Tree"
|
||||
import { useAppLoading } from "hooks/useAppLoading"
|
||||
import { useBrowserExtension } from "hooks/useBrowserExtension"
|
||||
import { useI18n } from "i18n"
|
||||
import { WelcomePage } from "pages/WelcomePage"
|
||||
import { AdminUsersPage } from "pages/admin/AdminUsersPage"
|
||||
import { MetricsPage } from "pages/admin/MetricsPage"
|
||||
import { AboutPage } from "pages/app/AboutPage"
|
||||
@@ -28,8 +30,8 @@ import { TagDetailsPage } from "pages/app/TagDetailsPage"
|
||||
import { LoginPage } from "pages/auth/LoginPage"
|
||||
import { PasswordRecoveryPage } from "pages/auth/PasswordRecoveryPage"
|
||||
import { RegistrationPage } from "pages/auth/RegistrationPage"
|
||||
import { WelcomePage } from "pages/WelcomePage"
|
||||
import React, { useEffect, useRef } from "react"
|
||||
import React, { useEffect, useState } from "react"
|
||||
import { isSafari } from "react-device-detect"
|
||||
import ReactGA from "react-ga4"
|
||||
import { HashRouter, Navigate, Route, Routes, useLocation, useNavigate } from "react-router-dom"
|
||||
import Tinycon from "tinycon"
|
||||
@@ -69,7 +71,7 @@ function Providers(props: { children: React.ReactNode }) {
|
||||
)
|
||||
}
|
||||
|
||||
// swagger-ui is very large, load only on-demand
|
||||
// api documentation page is very large, load only on-demand
|
||||
const ApiDocumentationPage = React.lazy(async () => await import("pages/app/ApiDocumentationPage"))
|
||||
|
||||
function AppRoutes() {
|
||||
@@ -140,16 +142,18 @@ function GoogleAnalyticsHandler() {
|
||||
return null
|
||||
}
|
||||
|
||||
function FaviconHandler() {
|
||||
const root = useAppSelector(state => state.tree.rootCategory)
|
||||
function UnreadCountTitleHandler({ unreadCount, enabled }: { unreadCount: number; enabled?: boolean }) {
|
||||
return <title>{enabled && unreadCount > 0 ? `(${unreadCount}) CommaFeed` : "CommaFeed"}</title>
|
||||
}
|
||||
|
||||
function UnreadCountFaviconHandler({ unreadCount, enabled }: { unreadCount: number; enabled?: boolean }) {
|
||||
useEffect(() => {
|
||||
const unreadCount = categoryUnreadCount(root)
|
||||
if (unreadCount === 0) {
|
||||
Tinycon.reset()
|
||||
} else {
|
||||
if (enabled && unreadCount > 0) {
|
||||
Tinycon.setBubble(unreadCount)
|
||||
} else {
|
||||
Tinycon.reset()
|
||||
}
|
||||
}, [root])
|
||||
}, [unreadCount, enabled])
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -166,14 +170,12 @@ function BrowserExtensionBadgeUnreadCountHandler() {
|
||||
return null
|
||||
}
|
||||
|
||||
function CustomJs() {
|
||||
const scriptLoaded = useRef(false)
|
||||
function CustomJsHandler() {
|
||||
const [scriptLoaded, setScriptLoaded] = useState(false)
|
||||
const { loading } = useAppLoading()
|
||||
|
||||
// useDidUpdate is used instead of useEffect because we want to skip the first render
|
||||
// the first render is the render of react-router, the routes are actually loaded in a second render
|
||||
// we want the script to be executed when the first route is done loading
|
||||
useDidUpdate(() => {
|
||||
if (scriptLoaded.current) {
|
||||
useEffect(() => {
|
||||
if (scriptLoaded || loading) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -182,13 +184,13 @@ function CustomJs() {
|
||||
script.async = true
|
||||
document.body.appendChild(script)
|
||||
|
||||
scriptLoaded.current = true
|
||||
})
|
||||
setScriptLoaded(true)
|
||||
}, [scriptLoaded, loading])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function CustomCss() {
|
||||
function CustomCssHandler() {
|
||||
useEffect(() => {
|
||||
const link = document.createElement("link")
|
||||
link.rel = "stylesheet"
|
||||
@@ -202,8 +204,13 @@ function CustomCss() {
|
||||
|
||||
export function App() {
|
||||
useI18n()
|
||||
const root = useAppSelector(state => state.tree.rootCategory)
|
||||
const unreadCountTitle = useAppSelector(state => state.user.settings?.unreadCountTitle)
|
||||
const unreadCountFavicon = useAppSelector(state => state.user.settings?.unreadCountFavicon)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const unreadCount = categoryUnreadCount(root)
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(reloadServerInfos())
|
||||
}, [dispatch])
|
||||
@@ -211,14 +218,22 @@ export function App() {
|
||||
return (
|
||||
<Providers>
|
||||
<>
|
||||
<FaviconHandler />
|
||||
<UnreadCountTitleHandler unreadCount={unreadCount} enabled={unreadCountTitle} />
|
||||
<UnreadCountFaviconHandler unreadCount={unreadCount} enabled={unreadCountFavicon} />
|
||||
<BrowserExtensionBadgeUnreadCountHandler />
|
||||
<CustomJsHandler />
|
||||
<CustomCssHandler />
|
||||
|
||||
{/* disable pull-to-refresh as it messes with vertical scrolling
|
||||
safari behaves weirdly when overscroll-behavior is set to none so we disable it only for other browsers
|
||||
https://github.com/Athou/commafeed/issues/1168
|
||||
*/}
|
||||
{!isSafari && <DisablePullToRefresh />}
|
||||
|
||||
<HashRouter>
|
||||
<GoogleAnalyticsHandler />
|
||||
<RedirectHandler />
|
||||
<AppRoutes />
|
||||
<CustomJs />
|
||||
<CustomCss />
|
||||
</HashRouter>
|
||||
</>
|
||||
</Providers>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createAsyncThunk } from "@reduxjs/toolkit"
|
||||
import { type AppDispatch, type RootState } from "app/store"
|
||||
import type { AppDispatch, RootState } from "app/store"
|
||||
|
||||
export const createAppAsyncThunk = createAsyncThunk.withTypes<{
|
||||
state: RootState
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
import axios, { AxiosError } from "axios"
|
||||
import {
|
||||
type AddCategoryRequest,
|
||||
type AdminSaveUserRequest,
|
||||
import axios, { type AxiosError } from "axios"
|
||||
import type {
|
||||
AddCategoryRequest,
|
||||
AdminSaveUserRequest,
|
||||
AuthenticationError,
|
||||
type Category,
|
||||
type CategoryModificationRequest,
|
||||
type CollapseRequest,
|
||||
type Entries,
|
||||
type FeedInfo,
|
||||
type FeedInfoRequest,
|
||||
type FeedModificationRequest,
|
||||
type GetEntriesPaginatedRequest,
|
||||
type IDRequest,
|
||||
type LoginRequest,
|
||||
type MarkRequest,
|
||||
type Metrics,
|
||||
type MultipleMarkRequest,
|
||||
type PasswordResetRequest,
|
||||
type ProfileModificationRequest,
|
||||
type RegistrationRequest,
|
||||
type ServerInfo,
|
||||
type Settings,
|
||||
type StarRequest,
|
||||
type SubscribeRequest,
|
||||
type Subscription,
|
||||
type TagRequest,
|
||||
type UserModel,
|
||||
Category,
|
||||
CategoryModificationRequest,
|
||||
CollapseRequest,
|
||||
Entries,
|
||||
FeedInfo,
|
||||
FeedInfoRequest,
|
||||
FeedModificationRequest,
|
||||
GetEntriesPaginatedRequest,
|
||||
IDRequest,
|
||||
LoginRequest,
|
||||
MarkRequest,
|
||||
Metrics,
|
||||
MultipleMarkRequest,
|
||||
PasswordResetRequest,
|
||||
ProfileModificationRequest,
|
||||
RegistrationRequest,
|
||||
ServerInfo,
|
||||
Settings,
|
||||
StarRequest,
|
||||
SubscribeRequest,
|
||||
Subscription,
|
||||
TagRequest,
|
||||
UserModel,
|
||||
} from "./types"
|
||||
|
||||
const axiosInstance = axios.create({ baseURL: "./rest", withCredentials: true })
|
||||
@@ -81,7 +81,17 @@ export const client = {
|
||||
},
|
||||
},
|
||||
user: {
|
||||
login: async (req: LoginRequest) => await axiosInstance.post("user/login", req),
|
||||
login: async (req: LoginRequest) => {
|
||||
const formData = new URLSearchParams()
|
||||
formData.append("j_username", req.name)
|
||||
formData.append("j_password", req.password)
|
||||
return await axiosInstance.post("j_security_check", formData, {
|
||||
baseURL: ".",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
})
|
||||
},
|
||||
register: async (req: RegistrationRequest) => await axiosInstance.post("user/register", req),
|
||||
passwordReset: async (req: PasswordResetRequest) => await axiosInstance.post("user/passwordReset", req),
|
||||
getSettings: async () => await axiosInstance.get<Settings>("user/settings"),
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { t } from "@lingui/macro"
|
||||
import { type IconType } from "react-icons"
|
||||
import type { IconType } from "react-icons"
|
||||
import { FaAt } from "react-icons/fa"
|
||||
import { SiBuffer, SiFacebook, SiGmail, SiInstapaper, SiPocket, SiTumblr, SiTwitter } from "react-icons/si"
|
||||
import { type Category, type Entry, type SharingSettings } from "./types"
|
||||
import { SiBuffer, SiFacebook, SiGmail, SiInstapaper, SiPocket, SiTumblr, SiX } from "react-icons/si"
|
||||
import type { Category, Entry, SharingSettings } from "./types"
|
||||
|
||||
const categories: Record<string, Category> = {
|
||||
const categories: Record<string, Omit<Category, "name">> = {
|
||||
all: {
|
||||
id: "all",
|
||||
name: t`All`,
|
||||
expanded: false,
|
||||
children: [],
|
||||
feeds: [],
|
||||
@@ -15,7 +13,6 @@ const categories: Record<string, Category> = {
|
||||
},
|
||||
starred: {
|
||||
id: "starred",
|
||||
name: t`Starred`,
|
||||
expanded: false,
|
||||
children: [],
|
||||
feeds: [],
|
||||
@@ -50,10 +47,10 @@ const sharing: {
|
||||
url: url => `https://www.facebook.com/sharer/sharer.php?u=${url}`,
|
||||
},
|
||||
twitter: {
|
||||
label: "Twitter",
|
||||
icon: SiTwitter,
|
||||
color: "#1D9BF0",
|
||||
url: (url, desc) => `https://twitter.com/share?text=${desc}&url=${url}`,
|
||||
label: "X",
|
||||
icon: SiX,
|
||||
color: "#000000",
|
||||
url: (url, desc) => `https://x.com/share?text=${desc}&url=${url}`,
|
||||
},
|
||||
tumblr: {
|
||||
label: "Tumblr",
|
||||
@@ -104,6 +101,9 @@ export const Constants = {
|
||||
entryId: (entry: Entry) => `entry-id-${entry.id}`,
|
||||
entryContextMenuId: (entry: Entry) => entry.id,
|
||||
},
|
||||
tooltip: {
|
||||
delay: 500,
|
||||
},
|
||||
browserExtensionUrl: "https://github.com/Athou/commafeed-browser-extension",
|
||||
bitcoinWalletAddress: "1dymfUxqCWpyD7a6rQSqNy4rLVDBsAr5e",
|
||||
}
|
||||
|
||||
@@ -1,25 +1,20 @@
|
||||
import { configureStore } from "@reduxjs/toolkit"
|
||||
import { type client } from "app/client"
|
||||
import { client } from "app/client"
|
||||
import { loadEntries, loadMoreEntries, markAllEntries, markEntry } from "app/entries/thunks"
|
||||
import { reducers, type RootState } from "app/store"
|
||||
import { type Entries, type Entry } from "app/types"
|
||||
import { type AxiosResponse } from "axios"
|
||||
import { type RootState, reducers } from "app/store"
|
||||
import type { Entries, Entry } from "app/types"
|
||||
import type { AxiosResponse } from "axios"
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest"
|
||||
import { mockReset } from "vitest-mock-extended"
|
||||
|
||||
const mockClient = await vi.hoisted(async () => {
|
||||
const mockModule = await import("vitest-mock-extended")
|
||||
return mockModule.mockDeep<typeof client>()
|
||||
})
|
||||
vi.mock("app/client", () => ({ client: mockClient }))
|
||||
vi.mock(import("app/client"))
|
||||
|
||||
describe("entries", () => {
|
||||
beforeEach(() => {
|
||||
mockReset(mockClient)
|
||||
vi.resetAllMocks()
|
||||
})
|
||||
|
||||
it("loads entries", async () => {
|
||||
mockClient.feed.getEntries.mockResolvedValue({
|
||||
vi.mocked(client.feed.getEntries).mockResolvedValue({
|
||||
data: {
|
||||
entries: [{ id: "3" } as Entry],
|
||||
hasMore: false,
|
||||
@@ -53,7 +48,7 @@ describe("entries", () => {
|
||||
})
|
||||
|
||||
it("loads more entries", async () => {
|
||||
mockClient.category.getEntries.mockResolvedValue({
|
||||
vi.mocked(client.category.getEntries).mockResolvedValue({
|
||||
data: {
|
||||
entries: [{ id: "4" } as Entry],
|
||||
hasMore: false,
|
||||
@@ -113,7 +108,7 @@ describe("entries", () => {
|
||||
{ id: "3", read: true },
|
||||
{ id: "4", read: false },
|
||||
])
|
||||
expect(mockClient.entry.mark).toHaveBeenCalledWith({ id: "3", read: true })
|
||||
expect(client.entry.mark).toHaveBeenCalledWith({ id: "3", read: true })
|
||||
})
|
||||
|
||||
it("marks all entries as read", () => {
|
||||
@@ -140,6 +135,6 @@ describe("entries", () => {
|
||||
{ id: "3", read: true },
|
||||
{ id: "4", read: true },
|
||||
])
|
||||
expect(mockClient.category.markEntries).toHaveBeenCalledWith({ id: "all", read: true })
|
||||
expect(client.category.markEntries).toHaveBeenCalledWith({ id: "all", read: true })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createSlice, type PayloadAction } from "@reduxjs/toolkit"
|
||||
import { type PayloadAction, createSlice } from "@reduxjs/toolkit"
|
||||
import { Constants } from "app/constants"
|
||||
import { loadEntries, loadMoreEntries, markAllEntries, markEntry, markMultipleEntries, starEntry, tagEntry } from "app/entries/thunks"
|
||||
import { type Entry } from "app/types"
|
||||
import type { Entry } from "app/types"
|
||||
|
||||
export type EntrySourceType = "category" | "feed" | "tag"
|
||||
|
||||
@@ -51,11 +51,9 @@ export const entriesSlice = createSlice({
|
||||
state.selectedEntryId = action.payload.id
|
||||
},
|
||||
setEntryExpanded: (state, action: PayloadAction<{ entry: Entry; expanded: boolean }>) => {
|
||||
state.entries
|
||||
.filter(e => e.id === action.payload.entry.id)
|
||||
.forEach(e => {
|
||||
e.expanded = action.payload.expanded
|
||||
})
|
||||
for (const e of state.entries.filter(e => e.id === action.payload.entry.id)) {
|
||||
e.expanded = action.payload.expanded
|
||||
}
|
||||
},
|
||||
setScrollingToEntry: (state, action: PayloadAction<boolean>) => {
|
||||
state.scrollingToEntry = action.payload
|
||||
@@ -66,32 +64,24 @@ export const entriesSlice = createSlice({
|
||||
},
|
||||
extraReducers: builder => {
|
||||
builder.addCase(markEntry.pending, (state, action) => {
|
||||
state.entries
|
||||
.filter(e => e.id === action.meta.arg.entry.id)
|
||||
.forEach(e => {
|
||||
e.read = action.meta.arg.read
|
||||
})
|
||||
for (const e of state.entries.filter(e => e.id === action.meta.arg.entry.id)) {
|
||||
e.read = action.meta.arg.read
|
||||
}
|
||||
})
|
||||
builder.addCase(markMultipleEntries.pending, (state, action) => {
|
||||
state.entries
|
||||
.filter(e => action.meta.arg.entries.some(e2 => e2.id === e.id))
|
||||
.forEach(e => {
|
||||
e.read = action.meta.arg.read
|
||||
})
|
||||
for (const e of state.entries.filter(e => action.meta.arg.entries.some(e2 => e2.id === e.id))) {
|
||||
e.read = action.meta.arg.read
|
||||
}
|
||||
})
|
||||
builder.addCase(markAllEntries.pending, (state, action) => {
|
||||
state.entries
|
||||
.filter(e => (action.meta.arg.req.olderThan ? e.date < action.meta.arg.req.olderThan : true))
|
||||
.forEach(e => {
|
||||
e.read = true
|
||||
})
|
||||
for (const e of state.entries.filter(e => (action.meta.arg.req.olderThan ? e.date < action.meta.arg.req.olderThan : true))) {
|
||||
e.read = true
|
||||
}
|
||||
})
|
||||
builder.addCase(starEntry.pending, (state, action) => {
|
||||
state.entries
|
||||
.filter(e => action.meta.arg.entry.id === e.id && action.meta.arg.entry.feedId === e.feedId)
|
||||
.forEach(e => {
|
||||
e.starred = action.meta.arg.starred
|
||||
})
|
||||
for (const e of state.entries.filter(e => action.meta.arg.entry.id === e.id && action.meta.arg.entry.feedId === e.feedId)) {
|
||||
e.starred = action.meta.arg.starred
|
||||
}
|
||||
})
|
||||
builder.addCase(loadEntries.pending, (state, action) => {
|
||||
state.source = action.meta.arg.source
|
||||
@@ -122,11 +112,9 @@ export const entriesSlice = createSlice({
|
||||
state.loading = false
|
||||
})
|
||||
builder.addCase(tagEntry.pending, (state, action) => {
|
||||
state.entries
|
||||
.filter(e => +e.id === action.meta.arg.entryId)
|
||||
.forEach(e => {
|
||||
e.tags = action.meta.arg.tags
|
||||
})
|
||||
for (const e of state.entries.filter(e => +e.id === action.meta.arg.entryId)) {
|
||||
e.tags = action.meta.arg.tags
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createAppAsyncThunk } from "app/async-thunk"
|
||||
import { client } from "app/client"
|
||||
import { Constants } from "app/constants"
|
||||
import { entriesSlice, type EntrySource, type EntrySourceType, setSearch } from "app/entries/slice"
|
||||
import { type EntrySource, type EntrySourceType, entriesSlice, setSearch } from "app/entries/slice"
|
||||
import type { RootState } from "app/store"
|
||||
import { reloadTree } from "app/tree/thunks"
|
||||
import type { Entry, MarkRequest, TagRequest } from "app/types"
|
||||
@@ -11,6 +11,7 @@ import { flushSync } from "react-dom"
|
||||
|
||||
const getEndpoint = (sourceType: EntrySourceType) =>
|
||||
sourceType === "category" || sourceType === "tag" ? client.category.getEntries : client.feed.getEntries
|
||||
|
||||
export const loadEntries = createAppAsyncThunk(
|
||||
"entries/load",
|
||||
async (
|
||||
@@ -28,6 +29,7 @@ export const loadEntries = createAppAsyncThunk(
|
||||
return result.data
|
||||
}
|
||||
)
|
||||
|
||||
export const loadMoreEntries = createAppAsyncThunk("entries/loadMore", async (_, thunkApi) => {
|
||||
const state = thunkApi.getState()
|
||||
const { source } = state.entries
|
||||
@@ -37,24 +39,28 @@ export const loadMoreEntries = createAppAsyncThunk("entries/loadMore", async (_,
|
||||
const result = await endpoint(buildGetEntriesPaginatedRequest(state, source, offset))
|
||||
return result.data
|
||||
})
|
||||
|
||||
const buildGetEntriesPaginatedRequest = (state: RootState, source: EntrySource, offset: number) => ({
|
||||
id: source.type === "tag" ? Constants.categories.all.id : source.id,
|
||||
order: state.user.settings?.readingOrder,
|
||||
readType: state.user.settings?.readingMode,
|
||||
readType: state.entries.search ? "all" : state.user.settings?.readingMode,
|
||||
offset,
|
||||
limit: 50,
|
||||
tag: source.type === "tag" ? source.id : undefined,
|
||||
keywords: state.entries.search,
|
||||
})
|
||||
|
||||
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", (arg: string, thunkApi) => {
|
||||
const state = thunkApi.getState()
|
||||
thunkApi.dispatch(setSearch(arg))
|
||||
thunkApi.dispatch(loadEntries({ source: state.entries.source, clearSearch: false }))
|
||||
})
|
||||
|
||||
export const markEntry = createAppAsyncThunk(
|
||||
"entries/entry/mark",
|
||||
(arg: { entry: Entry; read: boolean }) => {
|
||||
@@ -64,9 +70,10 @@ export const markEntry = createAppAsyncThunk(
|
||||
})
|
||||
},
|
||||
{
|
||||
condition: arg => arg.entry.read !== arg.read,
|
||||
condition: arg => arg.entry.markable && arg.entry.read !== arg.read,
|
||||
}
|
||||
)
|
||||
|
||||
export const markMultipleEntries = createAppAsyncThunk(
|
||||
"entries/entry/markMultiple",
|
||||
async (
|
||||
@@ -84,6 +91,7 @@ export const markMultipleEntries = createAppAsyncThunk(
|
||||
thunkApi.dispatch(reloadTree())
|
||||
}
|
||||
)
|
||||
|
||||
export const markEntriesUpToEntry = createAppAsyncThunk("entries/entry/upToEntry", (arg: Entry, thunkApi) => {
|
||||
const state = thunkApi.getState()
|
||||
const { entries } = state.entries
|
||||
@@ -98,6 +106,7 @@ export const markEntriesUpToEntry = createAppAsyncThunk("entries/entry/upToEntry
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
export const markAllEntries = createAppAsyncThunk(
|
||||
"entries/entry/markAll",
|
||||
async (
|
||||
@@ -113,13 +122,21 @@ export const markAllEntries = createAppAsyncThunk(
|
||||
thunkApi.dispatch(reloadTree())
|
||||
}
|
||||
)
|
||||
export const starEntry = createAppAsyncThunk("entries/entry/star", (arg: { entry: Entry; starred: boolean }) => {
|
||||
client.entry.star({
|
||||
id: arg.entry.id,
|
||||
feedId: +arg.entry.feedId,
|
||||
starred: arg.starred,
|
||||
})
|
||||
})
|
||||
|
||||
export const starEntry = createAppAsyncThunk(
|
||||
"entries/entry/star",
|
||||
(arg: { entry: Entry; starred: boolean }) => {
|
||||
client.entry.star({
|
||||
id: arg.entry.id,
|
||||
feedId: +arg.entry.feedId,
|
||||
starred: arg.starred,
|
||||
})
|
||||
},
|
||||
{
|
||||
condition: arg => arg.entry.markable && arg.entry.starred !== arg.starred,
|
||||
}
|
||||
)
|
||||
|
||||
export const selectEntry = createAppAsyncThunk(
|
||||
"entries/entry/select",
|
||||
(
|
||||
@@ -160,22 +177,35 @@ export const selectEntry = createAppAsyncThunk(
|
||||
})
|
||||
|
||||
if (arg.scrollToEntry) {
|
||||
const viewMode = state.user.localSettings.viewMode
|
||||
|
||||
const entryIndex = state.entries.entries.indexOf(entry)
|
||||
const entriesToKeepOnTopWhenScrolling =
|
||||
viewMode === "expanded" ? 0 : Math.min(state.user.settings?.entriesToKeepOnTopWhenScrolling ?? 0, entryIndex)
|
||||
const entryToScrollTo = state.entries.entries[entryIndex - entriesToKeepOnTopWhenScrolling]
|
||||
|
||||
const entryElement = document.getElementById(Constants.dom.entryId(entry))
|
||||
if (entryElement) {
|
||||
const entryElementToScrollTo = document.getElementById(Constants.dom.entryId(entryToScrollTo))
|
||||
if (entryElement && entryElementToScrollTo) {
|
||||
const scrollMode = state.user.settings?.scrollMode
|
||||
const entryEntirelyVisible = Constants.layout.isTopVisible(entryElement) && Constants.layout.isBottomVisible(entryElement)
|
||||
const entryEntirelyVisible =
|
||||
Constants.layout.isTopVisible(entryElementToScrollTo) && Constants.layout.isBottomVisible(entryElement)
|
||||
if (scrollMode === "always" || (scrollMode === "if_needed" && !entryEntirelyVisible)) {
|
||||
const scrollSpeed = state.user.settings?.scrollSpeed
|
||||
const margin = viewMode === "detailed" ? 8 : 3
|
||||
thunkApi.dispatch(entriesSlice.actions.setScrollingToEntry(true))
|
||||
scrollToEntry(entryElement, scrollSpeed, () => thunkApi.dispatch(entriesSlice.actions.setScrollingToEntry(false)))
|
||||
scrollToEntry(entryElementToScrollTo, margin, scrollSpeed, () =>
|
||||
thunkApi.dispatch(entriesSlice.actions.setScrollingToEntry(false))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
const scrollToEntry = (entryElement: HTMLElement, scrollSpeed: number | undefined, onScrollEnded: () => void) => {
|
||||
|
||||
const scrollToEntry = (entryElement: HTMLElement, margin: number, scrollSpeed: number | undefined, onScrollEnded: () => void) => {
|
||||
const header = document.getElementById(Constants.dom.headerId)?.getBoundingClientRect()
|
||||
const offset = (header?.bottom ?? 0) + 3
|
||||
const offset = (header?.bottom ?? 0) + margin
|
||||
scrollToWithCallback({
|
||||
options: {
|
||||
top: entryElement.offsetTop - offset,
|
||||
@@ -210,9 +240,10 @@ export const selectPreviousEntry = createAppAsyncThunk(
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export const selectNextEntry = createAppAsyncThunk(
|
||||
"entries/entry/selectNext",
|
||||
(
|
||||
async (
|
||||
arg: {
|
||||
expand: boolean
|
||||
markAsRead: boolean
|
||||
@@ -221,12 +252,20 @@ export const selectNextEntry = createAppAsyncThunk(
|
||||
thunkApi
|
||||
) => {
|
||||
const state = thunkApi.getState()
|
||||
const { entries } = state.entries
|
||||
const { entries, hasMore, loading } = state.entries
|
||||
const nextIndex = entries.findIndex(e => e.id === state.entries.selectedEntryId) + 1
|
||||
if (nextIndex < entries.length) {
|
||||
|
||||
// load more entries if needed
|
||||
// this can happen if the last entry is too large to fit on the screen and the infinite loader doesn't trigger
|
||||
if (nextIndex >= entries.length && hasMore && !loading) {
|
||||
await thunkApi.dispatch(loadMoreEntries())
|
||||
}
|
||||
|
||||
const entriesAfterLoading = thunkApi.getState().entries.entries
|
||||
if (nextIndex < entriesAfterLoading.length) {
|
||||
thunkApi.dispatch(
|
||||
selectEntry({
|
||||
entry: entries[nextIndex],
|
||||
entry: entriesAfterLoading[nextIndex],
|
||||
expand: arg.expand,
|
||||
markAsRead: arg.markAsRead,
|
||||
scrollToEntry: arg.scrollToEntry,
|
||||
@@ -235,6 +274,7 @@ export const selectNextEntry = createAppAsyncThunk(
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export const tagEntry = createAppAsyncThunk("entries/entry/tag", async (arg: TagRequest, thunkApi) => {
|
||||
await client.entry.tag(arg)
|
||||
thunkApi.dispatch(reloadTags())
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createSlice, type PayloadAction } from "@reduxjs/toolkit"
|
||||
import { type PayloadAction, createSlice } from "@reduxjs/toolkit"
|
||||
|
||||
interface RedirectState {
|
||||
to?: string
|
||||
|
||||
@@ -3,43 +3,55 @@ import { Constants } from "app/constants"
|
||||
import { redirectTo } from "app/redirect/slice"
|
||||
|
||||
export const redirectToLogin = createAppAsyncThunk("redirect/login", (_, thunkApi) => thunkApi.dispatch(redirectTo("/login")))
|
||||
|
||||
export const redirectToRegistration = createAppAsyncThunk("redirect/register", (_, thunkApi) => thunkApi.dispatch(redirectTo("/register")))
|
||||
export const redirectToPasswordRecovery = createAppAsyncThunk("redirect/passwordRecovery", (_, thunkApi) =>
|
||||
thunkApi.dispatch(redirectTo("/passwordRecovery"))
|
||||
)
|
||||
|
||||
export const redirectToApiDocumentation = createAppAsyncThunk("redirect/api", (_, thunkApi) => thunkApi.dispatch(redirectTo("/api")))
|
||||
|
||||
export const redirectToSelectedSource = createAppAsyncThunk("redirect/selectedSource", (_, thunkApi) => {
|
||||
const { source } = thunkApi.getState().entries
|
||||
thunkApi.dispatch(redirectTo(`/app/${source.type}/${source.id}`))
|
||||
})
|
||||
|
||||
export const redirectToCategory = createAppAsyncThunk("redirect/category", (id: string, thunkApi) =>
|
||||
thunkApi.dispatch(redirectTo(`/app/category/${id}`))
|
||||
)
|
||||
|
||||
export const redirectToRootCategory = createAppAsyncThunk(
|
||||
"redirect/category/root",
|
||||
async (_, thunkApi) => await thunkApi.dispatch(redirectToCategory(Constants.categories.all.id))
|
||||
)
|
||||
|
||||
export const redirectToCategoryDetails = createAppAsyncThunk("redirect/category/details", (id: string, thunkApi) =>
|
||||
thunkApi.dispatch(redirectTo(`/app/category/${id}/details`))
|
||||
)
|
||||
|
||||
export const redirectToFeed = createAppAsyncThunk("redirect/feed", (id: string | number, thunkApi) =>
|
||||
thunkApi.dispatch(redirectTo(`/app/feed/${id}`))
|
||||
)
|
||||
|
||||
export const redirectToFeedDetails = createAppAsyncThunk("redirect/feed/details", (id: string, thunkApi) =>
|
||||
thunkApi.dispatch(redirectTo(`/app/feed/${id}/details`))
|
||||
)
|
||||
|
||||
export const redirectToTag = createAppAsyncThunk("redirect/tag", (id: string, thunkApi) => thunkApi.dispatch(redirectTo(`/app/tag/${id}`)))
|
||||
|
||||
export const redirectToTagDetails = createAppAsyncThunk("redirect/tag/details", (id: string, thunkApi) =>
|
||||
thunkApi.dispatch(redirectTo(`/app/tag/${id}/details`))
|
||||
)
|
||||
|
||||
export const redirectToAdd = createAppAsyncThunk("redirect/add", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/add")))
|
||||
|
||||
export const redirectToSettings = createAppAsyncThunk("redirect/settings", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/settings")))
|
||||
|
||||
export const redirectToAdminUsers = createAppAsyncThunk("redirect/admin/users", (_, thunkApi) =>
|
||||
thunkApi.dispatch(redirectTo("/app/admin/users"))
|
||||
)
|
||||
|
||||
export const redirectToMetrics = createAppAsyncThunk("redirect/admin/metrics", (_, thunkApi) =>
|
||||
thunkApi.dispatch(redirectTo("/app/admin/metrics"))
|
||||
)
|
||||
|
||||
export const redirectToDonate = createAppAsyncThunk("redirect/donate", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/donate")))
|
||||
|
||||
export const redirectToAbout = createAppAsyncThunk("redirect/about", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/about")))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createSlice, type PayloadAction } from "@reduxjs/toolkit"
|
||||
import { type PayloadAction, createSlice } from "@reduxjs/toolkit"
|
||||
import { reloadServerInfos } from "app/server/thunks"
|
||||
import { type ServerInfo } from "app/types"
|
||||
import type { ServerInfo } from "app/types"
|
||||
|
||||
interface ServerState {
|
||||
serverInfos?: ServerInfo
|
||||
|
||||
@@ -3,7 +3,8 @@ import { entriesSlice } from "app/entries/slice"
|
||||
import { redirectSlice } from "app/redirect/slice"
|
||||
import { serverSlice } from "app/server/slice"
|
||||
import { treeSlice } from "app/tree/slice"
|
||||
import { userSlice } from "app/user/slice"
|
||||
import type { LocalSettings } from "app/types"
|
||||
import { initialLocalSettings, userSlice } from "app/user/slice"
|
||||
import { type TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"
|
||||
|
||||
export const reducers = {
|
||||
@@ -14,7 +15,36 @@ export const reducers = {
|
||||
user: userSlice.reducer,
|
||||
}
|
||||
|
||||
export const store = configureStore({ reducer: reducers })
|
||||
const loadLocalSettings = (): LocalSettings => {
|
||||
const json = localStorage.getItem("commafeed-local-settings")
|
||||
if (json) {
|
||||
return JSON.parse(json)
|
||||
}
|
||||
|
||||
// load old settings
|
||||
const viewMode = localStorage.getItem("view-mode")
|
||||
const sidebarWidth = localStorage.getItem("sidebar-width")
|
||||
const announcementHash = localStorage.getItem("announcement-hash")
|
||||
return {
|
||||
...initialLocalSettings,
|
||||
viewMode: viewMode ? JSON.parse(viewMode) : initialLocalSettings.viewMode,
|
||||
sidebarWidth: sidebarWidth ? JSON.parse(sidebarWidth) : initialLocalSettings.sidebarWidth,
|
||||
announcementHash: announcementHash ? JSON.parse(announcementHash) : initialLocalSettings.announcementHash,
|
||||
}
|
||||
}
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: reducers,
|
||||
preloadedState: {
|
||||
user: {
|
||||
localSettings: loadLocalSettings(),
|
||||
},
|
||||
},
|
||||
})
|
||||
store.subscribe(() => {
|
||||
const localSettings = store.getState().user.localSettings
|
||||
localStorage.setItem("commafeed-local-settings", JSON.stringify(localSettings))
|
||||
})
|
||||
|
||||
export type RootState = ReturnType<typeof store.getState>
|
||||
export type AppDispatch = typeof store.dispatch
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { createSlice, type PayloadAction } from "@reduxjs/toolkit"
|
||||
import { type PayloadAction, createSlice } from "@reduxjs/toolkit"
|
||||
import { markEntry } from "app/entries/thunks"
|
||||
import { redirectTo } from "app/redirect/slice"
|
||||
import { collapseTreeCategory, reloadTree } from "app/tree/thunks"
|
||||
import { type Category } from "app/types"
|
||||
import type { Category } from "app/types"
|
||||
import { visitCategoryTree } from "app/utils"
|
||||
|
||||
interface TreeState {
|
||||
@@ -34,13 +34,11 @@ export const treeSlice = createSlice({
|
||||
}>
|
||||
) => {
|
||||
if (!state.rootCategory) return
|
||||
visitCategoryTree(state.rootCategory, c =>
|
||||
c.feeds
|
||||
.filter(f => f.id === action.payload.feedId)
|
||||
.forEach(f => {
|
||||
f.unread += action.payload.amount
|
||||
})
|
||||
)
|
||||
visitCategoryTree(state.rootCategory, c => {
|
||||
for (const f of c.feeds.filter(f => f.id === action.payload.feedId)) {
|
||||
f.unread += action.payload.amount
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
extraReducers: builder => {
|
||||
@@ -55,13 +53,11 @@ export const treeSlice = createSlice({
|
||||
})
|
||||
builder.addCase(markEntry.pending, (state, action) => {
|
||||
if (!state.rootCategory) return
|
||||
visitCategoryTree(state.rootCategory, c =>
|
||||
c.feeds
|
||||
.filter(f => f.id === +action.meta.arg.entry.feedId)
|
||||
.forEach(f => {
|
||||
f.unread = action.meta.arg.read ? f.unread - 1 : f.unread + 1
|
||||
})
|
||||
)
|
||||
visitCategoryTree(state.rootCategory, c => {
|
||||
for (const f of c.feeds.filter(f => f.id === +action.meta.arg.entry.feedId)) {
|
||||
f.unread = action.meta.arg.read ? f.unread - 1 : f.unread + 1
|
||||
}
|
||||
})
|
||||
})
|
||||
builder.addCase(redirectTo, state => {
|
||||
state.mobileMenuOpen = false
|
||||
|
||||
@@ -1,9 +1,80 @@
|
||||
import { createAppAsyncThunk } from "app/async-thunk"
|
||||
import { client } from "app/client"
|
||||
import type { CollapseRequest } from "app/types"
|
||||
import { redirectToCategory, redirectToFeed } from "app/redirect/thunks"
|
||||
import { incrementUnreadCount } from "app/tree/slice"
|
||||
import type { CollapseRequest, Subscription } from "app/types"
|
||||
import { flattenCategoryTree, visitCategoryTree } from "app/utils"
|
||||
|
||||
export const reloadTree = createAppAsyncThunk("tree/reload", async () => await client.category.getRoot().then(r => r.data))
|
||||
|
||||
export const collapseTreeCategory = createAppAsyncThunk(
|
||||
"tree/category/collapse",
|
||||
async (req: CollapseRequest) => await client.category.collapse(req)
|
||||
)
|
||||
|
||||
export const selectNextUnreadTreeItem = createAppAsyncThunk(
|
||||
"tree/selectNextUnreadItem",
|
||||
(
|
||||
arg: {
|
||||
direction: "forward" | "backward"
|
||||
},
|
||||
thunkApi
|
||||
) => {
|
||||
const state = thunkApi.getState()
|
||||
const root = state.tree.rootCategory
|
||||
if (!root) return
|
||||
|
||||
const { source } = state.entries
|
||||
if (source.type === "category") {
|
||||
const categories = flattenCategoryTree(root)
|
||||
if (arg.direction === "backward") categories.reverse()
|
||||
|
||||
const index = categories.findIndex(c => c.id === source.id)
|
||||
if (index === -1) return
|
||||
|
||||
for (let i = index + 1; i < categories.length; i++) {
|
||||
const c = categories[i]
|
||||
if (c.feeds.some(f => f.unread > 0)) {
|
||||
return thunkApi.dispatch(redirectToCategory(String(c.id)))
|
||||
}
|
||||
}
|
||||
} else if (source.type === "feed") {
|
||||
const feeds: Subscription[] = []
|
||||
visitCategoryTree(root, c => feeds.push(...c.feeds), { childrenFirst: true })
|
||||
if (arg.direction === "backward") feeds.reverse()
|
||||
|
||||
const index = feeds.findIndex(f => f.id === +source.id)
|
||||
if (index === -1) return
|
||||
|
||||
for (let i = index + 1; i < feeds.length; i++) {
|
||||
const f = feeds[i]
|
||||
if (f.unread > 0) {
|
||||
return thunkApi.dispatch(redirectToFeed(String(f.id)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export const newFeedEntriesDiscovered = createAppAsyncThunk(
|
||||
"tree/new-feed-entries-discovered",
|
||||
async ({ feedId, amount }: { feedId: number; amount: number }, thunkApi) => {
|
||||
const root = thunkApi.getState().tree.rootCategory
|
||||
if (!root) return
|
||||
|
||||
const feed = flattenCategoryTree(root)
|
||||
.flatMap(c => c.feeds)
|
||||
.some(f => f.id === feedId)
|
||||
if (!feed) {
|
||||
// feed not found in the tree, reload the tree completely
|
||||
thunkApi.dispatch(reloadTree())
|
||||
} else {
|
||||
thunkApi.dispatch(
|
||||
incrementUnreadCount({
|
||||
feedId,
|
||||
amount,
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
119
commafeed-client/src/app/tree/tree.test.ts
Normal file
119
commafeed-client/src/app/tree/tree.test.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { configureStore } from "@reduxjs/toolkit"
|
||||
import { type RootState, reducers } from "app/store"
|
||||
import { selectNextUnreadTreeItem } from "app/tree/thunks"
|
||||
import type { Category, Subscription } from "app/types"
|
||||
import { describe, expect, it } from "vitest"
|
||||
|
||||
const createCategory = (id: string): Category => ({
|
||||
id,
|
||||
name: id,
|
||||
children: [],
|
||||
feeds: [],
|
||||
expanded: true,
|
||||
position: 0,
|
||||
})
|
||||
|
||||
const createFeed = (id: number, unread: number): Subscription => ({
|
||||
id,
|
||||
name: String(id),
|
||||
unread,
|
||||
errorCount: 0,
|
||||
position: 0,
|
||||
feedUrl: "",
|
||||
feedLink: "",
|
||||
iconUrl: "",
|
||||
})
|
||||
|
||||
const root = createCategory("root")
|
||||
|
||||
const catA = createCategory("catA")
|
||||
catA.feeds.push(createFeed(1, 0), createFeed(2, 0), createFeed(3, 1))
|
||||
|
||||
const catB = createCategory("catB")
|
||||
|
||||
const catC = createCategory("catC")
|
||||
catC.feeds.push(createFeed(4, 1))
|
||||
|
||||
root.children.push(catA, catB, catC)
|
||||
|
||||
describe("selectNextUnreadTreeItem", () => {
|
||||
it("selects the next unread category", async () => {
|
||||
const store = configureStore({
|
||||
reducer: reducers,
|
||||
preloadedState: {
|
||||
tree: {
|
||||
rootCategory: root,
|
||||
},
|
||||
entries: {
|
||||
source: {
|
||||
type: "category",
|
||||
id: "catA",
|
||||
},
|
||||
},
|
||||
} as RootState,
|
||||
})
|
||||
|
||||
await store.dispatch(selectNextUnreadTreeItem({ direction: "forward" }))
|
||||
expect(store.getState().redirect.to).toBe("/app/category/catC")
|
||||
})
|
||||
|
||||
it("selects the previous unread category", async () => {
|
||||
const store = configureStore({
|
||||
reducer: reducers,
|
||||
preloadedState: {
|
||||
tree: {
|
||||
rootCategory: root,
|
||||
},
|
||||
entries: {
|
||||
source: {
|
||||
type: "category",
|
||||
id: "catC",
|
||||
},
|
||||
},
|
||||
} as RootState,
|
||||
})
|
||||
|
||||
await store.dispatch(selectNextUnreadTreeItem({ direction: "backward" }))
|
||||
expect(store.getState().redirect.to).toBe("/app/category/catA")
|
||||
})
|
||||
|
||||
it("selects the next unread feed", async () => {
|
||||
const store = configureStore({
|
||||
reducer: reducers,
|
||||
preloadedState: {
|
||||
tree: {
|
||||
rootCategory: root,
|
||||
},
|
||||
entries: {
|
||||
source: {
|
||||
type: "feed",
|
||||
id: "1",
|
||||
},
|
||||
},
|
||||
} as RootState,
|
||||
})
|
||||
|
||||
await store.dispatch(selectNextUnreadTreeItem({ direction: "forward" }))
|
||||
expect(store.getState().redirect.to).toBe("/app/feed/3")
|
||||
})
|
||||
|
||||
it("selects the previous unread feed", async () => {
|
||||
const store = configureStore({
|
||||
reducer: reducers,
|
||||
preloadedState: {
|
||||
tree: {
|
||||
rootCategory: root,
|
||||
},
|
||||
entries: {
|
||||
source: {
|
||||
type: "feed",
|
||||
id: "4",
|
||||
},
|
||||
},
|
||||
} as RootState,
|
||||
})
|
||||
|
||||
await store.dispatch(selectNextUnreadTreeItem({ direction: "backward" }))
|
||||
expect(store.getState().redirect.to).toBe("/app/feed/3")
|
||||
})
|
||||
})
|
||||
@@ -6,6 +6,8 @@ export type ViewMode = "title" | "cozy" | "detailed" | "expanded"
|
||||
|
||||
export type ScrollMode = "always" | "never" | "if_needed"
|
||||
|
||||
export type IconDisplayMode = "always" | "never" | "on_desktop" | "on_mobile"
|
||||
|
||||
export interface AddCategoryRequest {
|
||||
name: string
|
||||
parentId?: string
|
||||
@@ -115,7 +117,6 @@ export interface GetEntriesRequest {
|
||||
newerThan?: number
|
||||
order?: ReadingOrder
|
||||
keywords?: string
|
||||
onlyIds?: boolean
|
||||
excludedSubscriptionIds?: string
|
||||
tag?: string
|
||||
}
|
||||
@@ -219,6 +220,7 @@ export interface ServerInfo {
|
||||
websocketEnabled: boolean
|
||||
websocketPingInterval: number
|
||||
treeReloadInterval: number
|
||||
forceRefreshCooldownDuration: number
|
||||
}
|
||||
|
||||
export interface SharingSettings {
|
||||
@@ -242,12 +244,23 @@ export interface Settings {
|
||||
customJs?: string
|
||||
scrollSpeed: number
|
||||
scrollMode: ScrollMode
|
||||
entriesToKeepOnTopWhenScrolling: number
|
||||
starIconDisplayMode: IconDisplayMode
|
||||
externalLinkIconDisplayMode: IconDisplayMode
|
||||
markAllAsReadConfirmation: boolean
|
||||
customContextMenu: boolean
|
||||
mobileFooter: boolean
|
||||
unreadCountTitle: boolean
|
||||
unreadCountFavicon: boolean
|
||||
sharingSettings: SharingSettings
|
||||
}
|
||||
|
||||
export interface LocalSettings {
|
||||
viewMode: ViewMode
|
||||
sidebarWidth: number
|
||||
announcementHash: string
|
||||
}
|
||||
|
||||
export interface StarRequest {
|
||||
id: string
|
||||
feedId: number
|
||||
@@ -275,6 +288,7 @@ export interface UserModel {
|
||||
created: number
|
||||
lastLogin?: number
|
||||
admin: boolean
|
||||
lastForceRefresh?: number
|
||||
}
|
||||
|
||||
export interface AdminSaveUserRequest {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { t } from "@lingui/macro"
|
||||
import { t } from "@lingui/core/macro"
|
||||
import { showNotification } from "@mantine/notifications"
|
||||
import { createSlice, isAnyOf } from "@reduxjs/toolkit"
|
||||
import { type Settings, type UserModel } from "app/types"
|
||||
import { type PayloadAction, createSlice, isAnyOf } from "@reduxjs/toolkit"
|
||||
import type { LocalSettings, Settings, UserModel, ViewMode } from "app/types"
|
||||
import {
|
||||
changeCustomContextMenu,
|
||||
changeEntriesToKeepOnTopWhenScrolling,
|
||||
changeExternalLinkIconDisplayMode,
|
||||
changeLanguage,
|
||||
changeMarkAllAsReadConfirmation,
|
||||
changeMobileFooter,
|
||||
@@ -14,6 +16,9 @@ import {
|
||||
changeScrollSpeed,
|
||||
changeSharingSetting,
|
||||
changeShowRead,
|
||||
changeStarIconDisplayMode,
|
||||
changeUnreadCountFavicon,
|
||||
changeUnreadCountTitle,
|
||||
reloadProfile,
|
||||
reloadSettings,
|
||||
reloadTags,
|
||||
@@ -21,16 +26,35 @@ import {
|
||||
|
||||
interface UserState {
|
||||
settings?: Settings
|
||||
localSettings: LocalSettings
|
||||
profile?: UserModel
|
||||
tags?: string[]
|
||||
}
|
||||
|
||||
const initialState: UserState = {}
|
||||
export const initialLocalSettings: LocalSettings = {
|
||||
viewMode: "detailed",
|
||||
sidebarWidth: 360,
|
||||
announcementHash: "no-hash",
|
||||
}
|
||||
|
||||
const initialState: UserState = {
|
||||
localSettings: initialLocalSettings,
|
||||
}
|
||||
|
||||
export const userSlice = createSlice({
|
||||
name: "user",
|
||||
initialState,
|
||||
reducers: {},
|
||||
reducers: {
|
||||
setViewMode: (state, action: PayloadAction<ViewMode>) => {
|
||||
state.localSettings.viewMode = action.payload
|
||||
},
|
||||
setSidebarWidth: (state, action: PayloadAction<number>) => {
|
||||
state.localSettings.sidebarWidth = action.payload
|
||||
},
|
||||
setAnnouncementHash: (state, action: PayloadAction<string>) => {
|
||||
state.localSettings.announcementHash = action.payload
|
||||
},
|
||||
},
|
||||
extraReducers: builder => {
|
||||
builder.addCase(reloadSettings.fulfilled, (state, action) => {
|
||||
state.settings = action.payload
|
||||
@@ -69,6 +93,18 @@ export const userSlice = createSlice({
|
||||
if (!state.settings) return
|
||||
state.settings.scrollMode = action.meta.arg
|
||||
})
|
||||
builder.addCase(changeEntriesToKeepOnTopWhenScrolling.pending, (state, action) => {
|
||||
if (!state.settings) return
|
||||
state.settings.entriesToKeepOnTopWhenScrolling = action.meta.arg
|
||||
})
|
||||
builder.addCase(changeStarIconDisplayMode.pending, (state, action) => {
|
||||
if (!state.settings) return
|
||||
state.settings.starIconDisplayMode = action.meta.arg
|
||||
})
|
||||
builder.addCase(changeExternalLinkIconDisplayMode.pending, (state, action) => {
|
||||
if (!state.settings) return
|
||||
state.settings.externalLinkIconDisplayMode = action.meta.arg
|
||||
})
|
||||
builder.addCase(changeMarkAllAsReadConfirmation.pending, (state, action) => {
|
||||
if (!state.settings) return
|
||||
state.settings.markAllAsReadConfirmation = action.meta.arg
|
||||
@@ -81,6 +117,14 @@ export const userSlice = createSlice({
|
||||
if (!state.settings) return
|
||||
state.settings.mobileFooter = action.meta.arg
|
||||
})
|
||||
builder.addCase(changeUnreadCountTitle.pending, (state, action) => {
|
||||
if (!state.settings) return
|
||||
state.settings.unreadCountTitle = action.meta.arg
|
||||
})
|
||||
builder.addCase(changeUnreadCountFavicon.pending, (state, action) => {
|
||||
if (!state.settings) return
|
||||
state.settings.unreadCountFavicon = action.meta.arg
|
||||
})
|
||||
builder.addCase(changeSharingSetting.pending, (state, action) => {
|
||||
if (!state.settings) return
|
||||
state.settings.sharingSettings[action.meta.arg.site] = action.meta.arg.value
|
||||
@@ -92,9 +136,14 @@ export const userSlice = createSlice({
|
||||
changeShowRead.fulfilled,
|
||||
changeScrollMarks.fulfilled,
|
||||
changeScrollMode.fulfilled,
|
||||
changeEntriesToKeepOnTopWhenScrolling.fulfilled,
|
||||
changeStarIconDisplayMode.fulfilled,
|
||||
changeExternalLinkIconDisplayMode.fulfilled,
|
||||
changeMarkAllAsReadConfirmation.fulfilled,
|
||||
changeCustomContextMenu.fulfilled,
|
||||
changeMobileFooter.fulfilled,
|
||||
changeUnreadCountTitle.fulfilled,
|
||||
changeUnreadCountFavicon.fulfilled,
|
||||
changeSharingSetting.fulfilled
|
||||
),
|
||||
() => {
|
||||
@@ -106,3 +155,5 @@ export const userSlice = createSlice({
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export const { setViewMode, setSidebarWidth, setAnnouncementHash } = userSlice.actions
|
||||
|
||||
@@ -1,48 +1,85 @@
|
||||
import { createAppAsyncThunk } from "app/async-thunk"
|
||||
import { client } from "app/client"
|
||||
import { reloadEntries } from "app/entries/thunks"
|
||||
import type { ReadingMode, ReadingOrder, ScrollMode, SharingSettings } from "app/types"
|
||||
import type { IconDisplayMode, ReadingMode, ReadingOrder, ScrollMode, SharingSettings } from "app/types"
|
||||
|
||||
export const reloadSettings = createAppAsyncThunk("settings/reload", async () => await client.user.getSettings().then(r => r.data))
|
||||
|
||||
export const reloadProfile = createAppAsyncThunk("profile/reload", async () => await client.user.getProfile().then(r => r.data))
|
||||
|
||||
export const reloadTags = createAppAsyncThunk("entries/tags", async () => await client.entry.getTags().then(r => r.data))
|
||||
|
||||
export const changeReadingMode = createAppAsyncThunk("settings/readingMode", (readingMode: ReadingMode, thunkApi) => {
|
||||
const { settings } = thunkApi.getState().user
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, readingMode })
|
||||
thunkApi.dispatch(reloadEntries())
|
||||
})
|
||||
|
||||
export const changeReadingOrder = createAppAsyncThunk("settings/readingOrder", (readingOrder: ReadingOrder, thunkApi) => {
|
||||
const { settings } = thunkApi.getState().user
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, readingOrder })
|
||||
thunkApi.dispatch(reloadEntries())
|
||||
})
|
||||
|
||||
export const changeLanguage = createAppAsyncThunk("settings/language", (language: string, thunkApi) => {
|
||||
const { settings } = thunkApi.getState().user
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, language })
|
||||
})
|
||||
|
||||
export const changeScrollSpeed = createAppAsyncThunk("settings/scrollSpeed", (speed: boolean, thunkApi) => {
|
||||
const { settings } = thunkApi.getState().user
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, scrollSpeed: speed ? 400 : 0 })
|
||||
})
|
||||
|
||||
export const changeShowRead = createAppAsyncThunk("settings/showRead", (showRead: boolean, thunkApi) => {
|
||||
const { settings } = thunkApi.getState().user
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, showRead })
|
||||
})
|
||||
|
||||
export const changeScrollMarks = createAppAsyncThunk("settings/scrollMarks", (scrollMarks: boolean, thunkApi) => {
|
||||
const { settings } = thunkApi.getState().user
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, scrollMarks })
|
||||
})
|
||||
|
||||
export const changeScrollMode = createAppAsyncThunk("settings/scrollMode", (scrollMode: ScrollMode, thunkApi) => {
|
||||
const { settings } = thunkApi.getState().user
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, scrollMode })
|
||||
})
|
||||
|
||||
export const changeEntriesToKeepOnTopWhenScrolling = createAppAsyncThunk(
|
||||
"settings/entriesToKeepOnTopWhenScrolling",
|
||||
(entriesToKeepOnTopWhenScrolling: number, thunkApi) => {
|
||||
const { settings } = thunkApi.getState().user
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, entriesToKeepOnTopWhenScrolling })
|
||||
}
|
||||
)
|
||||
|
||||
export const changeStarIconDisplayMode = createAppAsyncThunk(
|
||||
"settings/starIconDisplayMode",
|
||||
(starIconDisplayMode: IconDisplayMode, thunkApi) => {
|
||||
const { settings } = thunkApi.getState().user
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, starIconDisplayMode })
|
||||
}
|
||||
)
|
||||
|
||||
export const changeExternalLinkIconDisplayMode = createAppAsyncThunk(
|
||||
"settings/externalLinkIconDisplayMode",
|
||||
(externalLinkIconDisplayMode: IconDisplayMode, thunkApi) => {
|
||||
const { settings } = thunkApi.getState().user
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, externalLinkIconDisplayMode })
|
||||
}
|
||||
)
|
||||
|
||||
export const changeMarkAllAsReadConfirmation = createAppAsyncThunk(
|
||||
"settings/markAllAsReadConfirmation",
|
||||
(markAllAsReadConfirmation: boolean, thunkApi) => {
|
||||
@@ -51,16 +88,31 @@ export const changeMarkAllAsReadConfirmation = createAppAsyncThunk(
|
||||
client.user.saveSettings({ ...settings, markAllAsReadConfirmation })
|
||||
}
|
||||
)
|
||||
|
||||
export const changeCustomContextMenu = createAppAsyncThunk("settings/customContextMenu", (customContextMenu: boolean, thunkApi) => {
|
||||
const { settings } = thunkApi.getState().user
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, customContextMenu })
|
||||
})
|
||||
|
||||
export const changeMobileFooter = createAppAsyncThunk("settings/mobileFooter", (mobileFooter: boolean, thunkApi) => {
|
||||
const { settings } = thunkApi.getState().user
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, mobileFooter })
|
||||
})
|
||||
|
||||
export const changeUnreadCountTitle = createAppAsyncThunk("settings/unreadCountTitle", (unreadCountTitle: boolean, thunkApi) => {
|
||||
const { settings } = thunkApi.getState().user
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, unreadCountTitle })
|
||||
})
|
||||
|
||||
export const changeUnreadCountFavicon = createAppAsyncThunk("settings/unreadCountFavicon", (unreadCountFavicon: boolean, thunkApi) => {
|
||||
const { settings } = thunkApi.getState().user
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, unreadCountFavicon })
|
||||
})
|
||||
|
||||
export const changeSharingSetting = createAppAsyncThunk(
|
||||
"settings/sharingSetting",
|
||||
(
|
||||
|
||||
@@ -1,9 +1,22 @@
|
||||
import { throttle } from "throttle-debounce"
|
||||
import { type Category } from "./types"
|
||||
import type { Category } from "./types"
|
||||
|
||||
export function visitCategoryTree(category: Category, visitor: (category: Category) => void): void {
|
||||
visitor(category)
|
||||
category.children.forEach(child => visitCategoryTree(child, visitor))
|
||||
export function visitCategoryTree(
|
||||
category: Category,
|
||||
visitor: (category: Category) => void,
|
||||
options?: {
|
||||
childrenFirst?: boolean
|
||||
}
|
||||
): void {
|
||||
const childrenFirst = options?.childrenFirst
|
||||
|
||||
if (!childrenFirst) visitor(category)
|
||||
|
||||
for (const child of category.children) {
|
||||
visitCategoryTree(child, visitor, options)
|
||||
}
|
||||
|
||||
if (childrenFirst) visitor(category)
|
||||
}
|
||||
|
||||
export function flattenCategoryTree(category: Category): Category[] {
|
||||
|
||||
47
commafeed-client/src/components/ActionButton.test.tsx
Normal file
47
commafeed-client/src/components/ActionButton.test.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { I18nContext } from "@lingui/react"
|
||||
import { MantineProvider } from "@mantine/core"
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react"
|
||||
import { useActionButton } from "hooks/useActionButton"
|
||||
import { describe, expect, it, vi } from "vitest"
|
||||
import { ActionButton } from "./ActionButton"
|
||||
|
||||
vi.mock(import("@lingui/react"), () => ({
|
||||
useLingui: vi.fn().mockReturnValue({
|
||||
_: msg => msg,
|
||||
} as I18nContext),
|
||||
}))
|
||||
vi.mock(import("hooks/useActionButton"))
|
||||
|
||||
const label = "Test Label"
|
||||
const icon = "Test Icon"
|
||||
describe("ActionButton", () => {
|
||||
it("renders Button with label on desktop", () => {
|
||||
vi.mocked(useActionButton).mockReturnValue({ mobile: false, spacing: 0 })
|
||||
|
||||
render(<ActionButton label={label} icon={icon} />, { wrapper: MantineProvider })
|
||||
expect(screen.getByText(label)).toBeInTheDocument()
|
||||
expect(screen.getByText(icon)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders ActionIcon with tooltip on mobile", async () => {
|
||||
vi.mocked(useActionButton).mockReturnValue({ mobile: true, spacing: 0 })
|
||||
|
||||
render(<ActionButton label={label} icon={icon} />, { wrapper: MantineProvider })
|
||||
expect(screen.queryByText(label)).not.toBeInTheDocument()
|
||||
expect(screen.getByText(icon)).toBeInTheDocument()
|
||||
|
||||
fireEvent.mouseEnter(screen.getByRole("button"))
|
||||
const tooltip = await waitFor(() => screen.getByRole("tooltip"))
|
||||
expect(tooltip).toContainHTML(label)
|
||||
})
|
||||
|
||||
it("calls onClick handler when clicked", () => {
|
||||
vi.mocked(useActionButton).mockReturnValue({ mobile: false, spacing: 0 })
|
||||
const clickListener = vi.fn()
|
||||
|
||||
render(<ActionButton label={label} icon={icon} onClick={clickListener} />, { wrapper: MantineProvider })
|
||||
fireEvent.click(screen.getByRole("button"))
|
||||
|
||||
expect(clickListener).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,12 +1,15 @@
|
||||
import { ActionIcon, Button, type ButtonVariant, Tooltip, useMantineTheme } from "@mantine/core"
|
||||
import { type ActionIconVariant } from "@mantine/core/lib/components/ActionIcon/ActionIcon"
|
||||
import type { MessageDescriptor } from "@lingui/core"
|
||||
import { useLingui } from "@lingui/react"
|
||||
import { ActionIcon, Box, Button, type ButtonVariant, Tooltip, useMantineTheme } from "@mantine/core"
|
||||
import type { ActionIconVariant } from "@mantine/core/lib/components/ActionIcon/ActionIcon"
|
||||
import { Constants } from "app/constants"
|
||||
import { useActionButton } from "hooks/useActionButton"
|
||||
import { forwardRef, type MouseEventHandler, type ReactNode } from "react"
|
||||
import { type MouseEventHandler, type ReactNode, forwardRef } from "react"
|
||||
|
||||
interface ActionButtonProps {
|
||||
icon: ReactNode
|
||||
className?: string
|
||||
icon?: ReactNode
|
||||
label: ReactNode
|
||||
label?: string | MessageDescriptor
|
||||
onClick?: MouseEventHandler
|
||||
variant?: ActionIconVariant & ButtonVariant
|
||||
hideLabelOnDesktop?: boolean
|
||||
@@ -16,21 +19,44 @@ interface ActionButtonProps {
|
||||
/**
|
||||
* Switches between Button with label (desktop) and ActionIcon (mobile)
|
||||
*/
|
||||
export const ActionButton = forwardRef<HTMLButtonElement, ActionButtonProps>((props: ActionButtonProps, ref) => {
|
||||
export const ActionButton = forwardRef<HTMLDivElement, ActionButtonProps>((props: ActionButtonProps, ref) => {
|
||||
const { mobile } = useActionButton()
|
||||
const theme = useMantineTheme()
|
||||
const { _ } = useLingui()
|
||||
|
||||
const label = typeof props.label === "string" ? props.label : props.label && _(props.label)
|
||||
const variant = props.variant ?? "subtle"
|
||||
const iconOnly = (mobile && !props.showLabelOnMobile) || (!mobile && props.hideLabelOnDesktop)
|
||||
return iconOnly ? (
|
||||
<Tooltip label={props.label} openDelay={500}>
|
||||
<ActionIcon ref={ref} color={theme.primaryColor} variant={variant} className={props.className} onClick={props.onClick}>
|
||||
{props.icon}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Button ref={ref} variant={variant} size="xs" className={props.className} leftSection={props.icon} onClick={props.onClick}>
|
||||
{props.label}
|
||||
</Button>
|
||||
|
||||
return (
|
||||
<Box ref={ref}>
|
||||
{iconOnly && (
|
||||
<Tooltip label={label} openDelay={Constants.tooltip.delay}>
|
||||
<ActionIcon
|
||||
color={theme.primaryColor}
|
||||
variant={variant}
|
||||
className={props.className}
|
||||
onClick={props.onClick}
|
||||
aria-label={label}
|
||||
>
|
||||
{props.icon}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
{!iconOnly && (
|
||||
<Button
|
||||
variant={variant}
|
||||
size="xs"
|
||||
className={props.className}
|
||||
leftSection={props.icon}
|
||||
onClick={props.onClick}
|
||||
aria-label={label}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
})
|
||||
|
||||
ActionButton.displayName = "HeaderButton"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Trans } from "@lingui/macro"
|
||||
import { Alert as MantineAlert, Box } from "@mantine/core"
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { Box, Alert as MantineAlert } from "@mantine/core"
|
||||
import { Fragment } from "react"
|
||||
import { TbAlertCircle, TbAlertTriangle, TbCircleCheck } from "react-icons/tb"
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Trans } from "@lingui/macro"
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { Box, Dialog, Text } from "@mantine/core"
|
||||
import { useAppSelector } from "app/store"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import { setAnnouncementHash } from "app/user/slice"
|
||||
import { Content } from "components/content/Content"
|
||||
import { useAsync } from "react-async-hook"
|
||||
import useLocalStorage from "use-local-storage"
|
||||
|
||||
const sha256Hex = async (input: string | undefined) => {
|
||||
const data = new TextEncoder().encode(input)
|
||||
@@ -15,10 +15,11 @@ const sha256Hex = async (input: string | undefined) => {
|
||||
export function AnnouncementDialog() {
|
||||
const announcement = useAppSelector(state => state.server.serverInfos?.announcement)
|
||||
const announcementHash = useAsync(sha256Hex, [announcement]).result
|
||||
const [localStorageHash, setLocalStorageHash] = useLocalStorage("announcement-hash", "no-hash")
|
||||
const existingAnnouncementHash = useAppSelector(state => state.user.localSettings.announcementHash)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const opened = !!announcementHash && announcementHash !== localStorageHash
|
||||
const onClosed = () => setLocalStorageHash(announcementHash)
|
||||
const opened = !!announcementHash && announcementHash !== existingAnnouncementHash
|
||||
const onClosed = () => announcementHash && dispatch(setAnnouncementHash(announcementHash))
|
||||
|
||||
if (!announcement) return null
|
||||
return (
|
||||
|
||||
4
commafeed-client/src/components/DisablePullToRefresh.css
Normal file
4
commafeed-client/src/components/DisablePullToRefresh.css
Normal file
@@ -0,0 +1,4 @@
|
||||
html,
|
||||
body {
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
4
commafeed-client/src/components/DisablePullToRefresh.tsx
Normal file
4
commafeed-client/src/components/DisablePullToRefresh.tsx
Normal file
@@ -0,0 +1,4 @@
|
||||
export const DisablePullToRefresh = () => {
|
||||
import("./DisablePullToRefresh.css")
|
||||
return <></>
|
||||
}
|
||||
@@ -9,6 +9,7 @@ interface ImageWithPlaceholderWhileLoadingProps {
|
||||
title?: string
|
||||
width?: number
|
||||
height?: number | "auto"
|
||||
style?: React.CSSProperties
|
||||
placeholderWidth?: number
|
||||
placeholderHeight?: number
|
||||
placeholderBackgroundColor?: string
|
||||
@@ -42,6 +43,7 @@ export function ImageWithPlaceholderWhileLoading({
|
||||
src,
|
||||
title,
|
||||
width,
|
||||
style,
|
||||
}: ImageWithPlaceholderWhileLoadingProps) {
|
||||
const { classes } = useStyles({
|
||||
placeholderWidth,
|
||||
@@ -68,7 +70,11 @@ export function ImageWithPlaceholderWhileLoading({
|
||||
width={width}
|
||||
height={height}
|
||||
onLoad={() => setLoading(false)}
|
||||
style={{ display: loading ? "none" : "block" }}
|
||||
style={{
|
||||
...style,
|
||||
display: loading ? "none" : (style?.display ?? "initial"),
|
||||
height: style?.width ? "auto" : style?.height,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Trans } from "@lingui/macro"
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { Anchor, Box, Kbd, Stack, Table } from "@mantine/core"
|
||||
import { useOs } from "@mantine/hooks"
|
||||
import { Constants } from "app/constants"
|
||||
|
||||
export function KeyboardShortcutsHelp() {
|
||||
const isMacOS = useOs() === "macos"
|
||||
return (
|
||||
<Stack gap="xs">
|
||||
<Table striped highlightOnHover>
|
||||
@@ -31,6 +33,26 @@ export function KeyboardShortcutsHelp() {
|
||||
<Kbd>K</Kbd>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
<Table.Tr>
|
||||
<Table.Td>
|
||||
<Trans>Select next unread feed/category</Trans>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Kbd>Shift</Kbd>
|
||||
<span> + </span>
|
||||
<Kbd>J</Kbd>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
<Table.Tr>
|
||||
<Table.Td>
|
||||
<Trans>Select previous unread feed/category</Trans>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Kbd>Shift</Kbd>
|
||||
<span> + </span>
|
||||
<Kbd>K</Kbd>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
<Table.Tr>
|
||||
<Table.Td>
|
||||
<Trans>Set focus on next entry without opening it</Trans>
|
||||
@@ -148,9 +170,7 @@ export function KeyboardShortcutsHelp() {
|
||||
<Trans>Navigate to a subscription by entering its name</Trans>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Kbd>
|
||||
<Trans>Ctrl</Trans>
|
||||
</Kbd>
|
||||
<Kbd>{isMacOS ? <Trans>Cmd</Trans> : <Trans>Ctrl</Trans>}</Kbd>
|
||||
<span> + </span>
|
||||
<Kbd>K</Kbd>
|
||||
<span>, </span>
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
import { Trans } from "@lingui/macro"
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { Tooltip } from "@mantine/core"
|
||||
import { Constants } from "app/constants"
|
||||
import dayjs from "dayjs"
|
||||
import { useEffect, useState } from "react"
|
||||
import { useNow } from "hooks/useNow"
|
||||
|
||||
export function RelativeDate(props: { date: Date | number | undefined }) {
|
||||
const [now, setNow] = useState(new Date())
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => setNow(new Date()), 60 * 1000)
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
const now = useNow(60 * 1000)
|
||||
|
||||
if (!props.date) return <Trans>N/A</Trans>
|
||||
const date = dayjs(props.date)
|
||||
return (
|
||||
<Tooltip label={date.toDate().toLocaleString()} openDelay={500}>
|
||||
<Tooltip label={date.toDate().toLocaleString()} openDelay={Constants.tooltip.delay}>
|
||||
<span>{date.from(dayjs(now))}</span>
|
||||
</Tooltip>
|
||||
)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Trans } from "@lingui/macro"
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { Box, Button, Checkbox, Group, PasswordInput, Stack, TextInput } from "@mantine/core"
|
||||
import { useForm } from "@mantine/form"
|
||||
import { client, errorToStrings } from "app/client"
|
||||
import { type AdminSaveUserRequest, type UserModel } from "app/types"
|
||||
import type { AdminSaveUserRequest, UserModel } from "app/types"
|
||||
import { Alert } from "components/Alert"
|
||||
import { useAsyncCallback } from "react-async-hook"
|
||||
import { TbDeviceFloppy } from "react-icons/tb"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Input, Textarea } from "@mantine/core"
|
||||
import RichCodeEditor from "components/code/RichCodeEditor"
|
||||
import { useMobile } from "hooks/useMobile"
|
||||
import { type ReactNode } from "react"
|
||||
import type { ReactNode } from "react"
|
||||
|
||||
interface CodeEditorProps {
|
||||
description?: ReactNode
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useAsync } from "react-async-hook"
|
||||
const init = async () => {
|
||||
window.MonacoEnvironment = {
|
||||
async getWorker(_, label) {
|
||||
let worker
|
||||
let worker: typeof import("*?worker")
|
||||
if (label === "css") {
|
||||
worker = await import("monaco-editor/esm/vs/language/css/css.worker?worker")
|
||||
} else if (label === "javascript") {
|
||||
@@ -13,7 +13,6 @@ const init = async () => {
|
||||
} else {
|
||||
worker = await import("monaco-editor/esm/vs/editor/editor.worker?worker")
|
||||
}
|
||||
// eslint-disable-next-line new-cap
|
||||
return new worker.default()
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,11 +1,24 @@
|
||||
import { TypographyStylesProvider } from "@mantine/core"
|
||||
import { type ReactNode } from "react"
|
||||
import type { ReactNode } from "react"
|
||||
import { tss } from "tss"
|
||||
|
||||
/**
|
||||
* This component is used to provide basic styles to html typography elements.
|
||||
*
|
||||
* see https://mantine.dev/core/typography-styles-provider/
|
||||
*/
|
||||
|
||||
const useStyles = tss.create(() => ({
|
||||
// override mantine default typography styles
|
||||
content: {
|
||||
paddingLeft: 0,
|
||||
"& img": {
|
||||
marginBottom: 0,
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
export const BasicHtmlStyles = (props: { children: ReactNode }) => {
|
||||
return <TypographyStylesProvider pl={0}>{props.children}</TypographyStylesProvider>
|
||||
const { classes } = useStyles()
|
||||
return <TypographyStylesProvider className={classes.content}>{props.children}</TypographyStylesProvider>
|
||||
}
|
||||
|
||||
27
commafeed-client/src/components/content/Content.test.tsx
Normal file
27
commafeed-client/src/components/content/Content.test.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { MantineProvider } from "@mantine/core"
|
||||
import { render } from "@testing-library/react"
|
||||
import { Content } from "components/content/Content"
|
||||
import React from "react"
|
||||
import { describe, expect, it } from "vitest"
|
||||
|
||||
describe("Content component", () => {
|
||||
it("renders basic content", () => {
|
||||
const { container } = render(<Content content="<p>Hello World</p>" />, { wrapper: MantineProvider })
|
||||
expect(container.querySelector("p")).toHaveTextContent("Hello World")
|
||||
})
|
||||
|
||||
it("renders highlighted text when highlight prop is provided", () => {
|
||||
const { container } = render(<Content content="Hello World" highlight="World" />, { wrapper: MantineProvider })
|
||||
expect(container.querySelector("mark")).toHaveTextContent("World")
|
||||
})
|
||||
|
||||
it("renders iframe tag when included in content", () => {
|
||||
const { container } = render(<Content content='<iframe src="https://example.com"></iframe>' />, { wrapper: MantineProvider })
|
||||
expect(container.querySelector("iframe")).toHaveAttribute("src", "https://example.com")
|
||||
})
|
||||
|
||||
it("does not render unsupported tags", () => {
|
||||
const { container } = render(<Content content='<script>alert("test")</script>' />, { wrapper: MantineProvider })
|
||||
expect(container.querySelector("script")).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Box, Mark } from "@mantine/core"
|
||||
import { Constants } from "app/constants"
|
||||
import { calculatePlaceholderSize } from "app/utils"
|
||||
import { BasicHtmlStyles } from "components/content/BasicHtmlStyles"
|
||||
import { ImageWithPlaceholderWhileLoading } from "components/ImageWithPlaceholderWhileLoading"
|
||||
import { BasicHtmlStyles } from "components/content/BasicHtmlStyles"
|
||||
import escapeStringRegexp from "escape-string-regexp"
|
||||
import { type ChildrenNode, Interweave, Matcher, type MatchResponse, type Node, type TransformCallback } from "interweave"
|
||||
import { ALLOWED_TAG_LIST, type ChildrenNode, Interweave, type MatchResponse, Matcher, type Node, type TransformCallback } from "interweave"
|
||||
import React from "react"
|
||||
import styleToObject from "style-to-object"
|
||||
import { tss } from "tss"
|
||||
|
||||
export interface ContentProps {
|
||||
@@ -40,8 +41,9 @@ const transform: TransformCallback = node => {
|
||||
const title = node.getAttribute("title") ?? undefined
|
||||
const nodeWidth = node.getAttribute("width")
|
||||
const nodeHeight = node.getAttribute("height")
|
||||
const width = nodeWidth ? parseInt(nodeWidth, 10) : undefined
|
||||
const height = nodeHeight ? parseInt(nodeHeight, 10) : undefined
|
||||
const width = nodeWidth ? Number.parseInt(nodeWidth, 10) : undefined
|
||||
const height = nodeHeight ? Number.parseInt(nodeHeight, 10) : undefined
|
||||
const style = styleToObject(node.getAttribute("style") ?? "") ?? undefined
|
||||
const placeholderSize = calculatePlaceholderSize({
|
||||
width,
|
||||
height,
|
||||
@@ -55,6 +57,7 @@ const transform: TransformCallback = node => {
|
||||
title={title}
|
||||
width={width}
|
||||
height="auto"
|
||||
style={style}
|
||||
placeholderWidth={placeholderSize.width}
|
||||
placeholderHeight={placeholderSize.height}
|
||||
/>
|
||||
@@ -64,20 +67,19 @@ const transform: TransformCallback = node => {
|
||||
}
|
||||
|
||||
class HighlightMatcher extends Matcher {
|
||||
private readonly search: string
|
||||
private readonly regexp: RegExp
|
||||
|
||||
constructor(search: string) {
|
||||
super("highlight")
|
||||
this.search = escapeStringRegexp(search)
|
||||
this.regexp = new RegExp(escapeStringRegexp(search).split(" ").join("|"), "i")
|
||||
}
|
||||
|
||||
match(string: string): MatchResponse<unknown> | null {
|
||||
const pattern = this.search.split(" ").join("|")
|
||||
return this.doMatch(string, new RegExp(pattern, "i"), () => ({}))
|
||||
return this.doMatch(string, this.regexp, () => ({}))
|
||||
}
|
||||
|
||||
replaceWith(children: ChildrenNode): Node {
|
||||
return <Mark>{children}</Mark>
|
||||
return <Mark key={0}>{children}</Mark>
|
||||
}
|
||||
|
||||
asTag(): string {
|
||||
@@ -85,6 +87,9 @@ class HighlightMatcher extends Matcher {
|
||||
}
|
||||
}
|
||||
|
||||
// allow iframe tag
|
||||
const allowList = [...ALLOWED_TAG_LIST, "iframe"]
|
||||
|
||||
// memoize component because Interweave is costly
|
||||
const Content = React.memo((props: ContentProps) => {
|
||||
const { classes } = useStyles()
|
||||
@@ -93,7 +98,7 @@ const Content = React.memo((props: ContentProps) => {
|
||||
return (
|
||||
<BasicHtmlStyles>
|
||||
<Box className={classes.content}>
|
||||
<Interweave content={props.content} transform={transform} matchers={matchers} />
|
||||
<Interweave content={props.content} transform={transform} matchers={matchers} allowList={allowList} />
|
||||
</Box>
|
||||
</BasicHtmlStyles>
|
||||
)
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { BasicHtmlStyles } from "components/content/BasicHtmlStyles"
|
||||
import { ImageWithPlaceholderWhileLoading } from "components/ImageWithPlaceholderWhileLoading"
|
||||
import { BasicHtmlStyles } from "components/content/BasicHtmlStyles"
|
||||
|
||||
export function Enclosure(props: { enclosureType: string; enclosureUrl: string }) {
|
||||
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")
|
||||
@@ -9,12 +12,14 @@ export function Enclosure(props: { enclosureType: string; enclosureUrl: string }
|
||||
return (
|
||||
<BasicHtmlStyles>
|
||||
{hasVideo && (
|
||||
// biome-ignore lint/a11y/useMediaCaption: we don't have any captions for videos
|
||||
<video controls width="100%">
|
||||
<source src={props.enclosureUrl} type={props.enclosureType} />
|
||||
</video>
|
||||
)}
|
||||
{hasAudio && (
|
||||
<audio controls>
|
||||
// biome-ignore lint/a11y/useMediaCaption: we don't have any captions for audio
|
||||
<audio controls style={{ width: "100%" }}>
|
||||
<source src={props.enclosureUrl} type={props.enclosureType} />
|
||||
</audio>
|
||||
)}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Trans } from "@lingui/macro"
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { Box } from "@mantine/core"
|
||||
import { openModal } from "@mantine/modals"
|
||||
import { Constants } from "app/constants"
|
||||
import { type ExpendableEntry } from "app/entries/slice"
|
||||
import type { ExpendableEntry } from "app/entries/slice"
|
||||
import {
|
||||
loadMoreEntries,
|
||||
markAllEntries,
|
||||
@@ -16,11 +16,11 @@ import {
|
||||
import { redirectToRootCategory } from "app/redirect/thunks"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import { toggleSidebar } from "app/tree/slice"
|
||||
import { selectNextUnreadTreeItem } from "app/tree/thunks"
|
||||
import { KeyboardShortcutsHelp } from "components/KeyboardShortcutsHelp"
|
||||
import { Loader } from "components/Loader"
|
||||
import { useBrowserExtension } from "hooks/useBrowserExtension"
|
||||
import { useMousetrap } from "hooks/useMousetrap"
|
||||
import { useViewMode } from "hooks/useViewMode"
|
||||
import { useEffect } from "react"
|
||||
import { useContextMenu } from "react-contexify"
|
||||
import InfiniteScroll from "react-infinite-scroller"
|
||||
@@ -38,7 +38,7 @@ export function FeedEntries() {
|
||||
const scrollingToEntry = useAppSelector(state => state.entries.scrollingToEntry)
|
||||
const sidebarVisible = useAppSelector(state => state.tree.sidebarVisible)
|
||||
const customContextMenu = useAppSelector(state => state.user.settings?.customContextMenu)
|
||||
const { viewMode } = useViewMode()
|
||||
const viewMode = useAppSelector(state => state.user.localSettings.viewMode)
|
||||
const dispatch = useAppDispatch()
|
||||
const { openLinkInBackgroundTab } = useBrowserExtension()
|
||||
|
||||
@@ -126,7 +126,7 @@ export function FeedEntries() {
|
||||
})
|
||||
window.addEventListener("scroll", listener)
|
||||
return () => window.removeEventListener("scroll", listener)
|
||||
}, [dispatch, contextMenu, entries, viewMode, scrollMarks, scrollingToEntry])
|
||||
}, [dispatch, entries, viewMode, scrollMarks, scrollingToEntry])
|
||||
|
||||
useMousetrap("r", async () => await dispatch(reloadEntries()))
|
||||
useMousetrap(
|
||||
@@ -173,6 +173,8 @@ export function FeedEntries() {
|
||||
})
|
||||
)
|
||||
)
|
||||
useMousetrap("shift+j", async () => await dispatch(selectNextUnreadTreeItem({ direction: "forward" })))
|
||||
useMousetrap("shift+k", async () => await dispatch(selectNextUnreadTreeItem({ direction: "backward" })))
|
||||
useMousetrap("space", () => {
|
||||
if (selectedEntry) {
|
||||
if (selectedEntry.expanded) {
|
||||
@@ -305,11 +307,12 @@ export function FeedEntries() {
|
||||
loader={<Box key={0}>{loading && <Loader />}</Box>}
|
||||
>
|
||||
{entries.map(entry => (
|
||||
<div
|
||||
<article
|
||||
key={entry.id}
|
||||
ref={el => {
|
||||
if (el) el.id = Constants.dom.entryId(entry)
|
||||
}}
|
||||
data-id={entry.id}
|
||||
>
|
||||
<FeedEntry
|
||||
entry={entry}
|
||||
@@ -322,7 +325,7 @@ export function FeedEntries() {
|
||||
onBodyClick={() => bodyClicked(entry)}
|
||||
onSwipedLeft={async () => await swipedLeft(entry)}
|
||||
/>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</InfiniteScroll>
|
||||
)
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
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 { useViewMode } from "hooks/useViewMode"
|
||||
import React from "react"
|
||||
import { useAppSelector } from "app/store"
|
||||
import type { Entry, ViewMode } from "app/types"
|
||||
import { FeedEntryCompactHeader } from "components/content/header/FeedEntryCompactHeader"
|
||||
import { FeedEntryHeader } from "components/content/header/FeedEntryHeader"
|
||||
import { useMobile } from "hooks/useMobile"
|
||||
import type React from "react"
|
||||
import { useSwipeable } from "react-swipeable"
|
||||
import { tss } from "tss"
|
||||
import { FeedEntryBody } from "./FeedEntryBody"
|
||||
import { FeedEntryCompactHeader } from "./FeedEntryCompactHeader"
|
||||
import { FeedEntryContextMenu } from "./FeedEntryContextMenu"
|
||||
import { FeedEntryFooter } from "./FeedEntryFooter"
|
||||
import { FeedEntryHeader } from "./FeedEntryHeader"
|
||||
|
||||
interface FeedEntryProps {
|
||||
entry: Entry
|
||||
@@ -33,7 +34,7 @@ const useStyles = tss
|
||||
maxWidth?: number
|
||||
}>()
|
||||
.create(({ theme, colorScheme, read, expanded, viewMode, rtl, showSelectionIndicator, maxWidth }) => {
|
||||
let backgroundColor
|
||||
let backgroundColor: string
|
||||
if (colorScheme === "dark") {
|
||||
backgroundColor = read ? "inherit" : theme.colors.dark[5]
|
||||
} else {
|
||||
@@ -59,7 +60,7 @@ const useStyles = tss
|
||||
backgroundHoverColor = colorScheme === "dark" ? theme.colors.dark[6] : theme.colors.gray[1]
|
||||
}
|
||||
|
||||
let paperBorderLeftColor
|
||||
let paperBorderLeftColor = ""
|
||||
if (showSelectionIndicator) {
|
||||
const borderLeftColor = colorScheme === "dark" ? theme.colors[theme.primaryColor][4] : theme.colors[theme.primaryColor][6]
|
||||
paperBorderLeftColor = `${borderLeftColor} !important`
|
||||
@@ -93,7 +94,7 @@ const useStyles = tss
|
||||
})
|
||||
|
||||
export function FeedEntry(props: FeedEntryProps) {
|
||||
const { viewMode } = useViewMode()
|
||||
const viewMode = useAppSelector(state => state.user.localSettings.viewMode)
|
||||
const { classes, cx } = useStyles({
|
||||
read: props.entry.read,
|
||||
expanded: props.expanded,
|
||||
@@ -103,6 +104,15 @@ export function FeedEntry(props: FeedEntryProps) {
|
||||
maxWidth: props.maxWidth,
|
||||
})
|
||||
|
||||
const externalLinkDisplayMode = useAppSelector(state => state.user.settings?.externalLinkIconDisplayMode)
|
||||
const starIconDisplayMode = useAppSelector(state => state.user.settings?.starIconDisplayMode)
|
||||
const mobile = useMobile()
|
||||
|
||||
const showExternalLinkIcon =
|
||||
externalLinkDisplayMode && ["always", mobile ? "on_mobile" : "on_desktop"].includes(externalLinkDisplayMode)
|
||||
const showStarIcon =
|
||||
props.entry.markable && starIconDisplayMode && ["always", mobile ? "on_mobile" : "on_desktop"].includes(starIconDisplayMode)
|
||||
|
||||
const swipeHandlers = useSwipeable({
|
||||
onSwipedLeft: props.onSwipedLeft,
|
||||
})
|
||||
@@ -147,8 +157,21 @@ export function FeedEntry(props: FeedEntryProps) {
|
||||
onContextMenu={props.onHeaderRightClick}
|
||||
>
|
||||
<Box px={paddingX} py={paddingY} {...swipeHandlers}>
|
||||
{compactHeader && <FeedEntryCompactHeader entry={props.entry} />}
|
||||
{!compactHeader && <FeedEntryHeader entry={props.entry} expanded={props.expanded} />}
|
||||
{compactHeader && (
|
||||
<FeedEntryCompactHeader
|
||||
entry={props.entry}
|
||||
showStarIcon={showStarIcon}
|
||||
showExternalLinkIcon={showExternalLinkIcon}
|
||||
/>
|
||||
)}
|
||||
{!compactHeader && (
|
||||
<FeedEntryHeader
|
||||
entry={props.entry}
|
||||
expanded={props.expanded}
|
||||
showStarIcon={showStarIcon}
|
||||
showExternalLinkIcon={showExternalLinkIcon}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</a>
|
||||
{props.expanded && (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Box } from "@mantine/core"
|
||||
import { useAppSelector } from "app/store"
|
||||
import { type Entry } from "app/types"
|
||||
import type { Entry } from "app/types"
|
||||
import { Content } from "./Content"
|
||||
import { Enclosure } from "./Enclosure"
|
||||
import { Media } from "./Media"
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { Trans } from "@lingui/macro"
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { Group } from "@mantine/core"
|
||||
import { Constants } from "app/constants"
|
||||
import { markEntriesUpToEntry, markEntry, starEntry } from "app/entries/thunks"
|
||||
import { redirectToFeed } from "app/redirect/thunks"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import { type Entry } from "app/types"
|
||||
import type { Entry } from "app/types"
|
||||
import { truncate } from "app/utils"
|
||||
import { useBrowserExtension } from "hooks/useBrowserExtension"
|
||||
import { useColorScheme } from "hooks/useColorScheme"
|
||||
import { Item, Menu, Separator } from "react-contexify"
|
||||
import { TbArrowBarToDown, TbExternalLink, TbEyeCheck, TbEyeOff, TbRss, TbStar, TbStarOff } from "react-icons/tb"
|
||||
import { TbArrowBarToDown, TbExternalLink, TbMail, TbMailOpened, TbRss, TbStar, TbStarOff } from "react-icons/tb"
|
||||
import { tss } from "tss"
|
||||
|
||||
interface FeedEntryContextMenuProps {
|
||||
@@ -67,12 +67,14 @@ export function FeedEntryContextMenu(props: FeedEntryContextMenuProps) {
|
||||
{props.entry.starred ? <Trans>Unstar</Trans> : <Trans>Star</Trans>}
|
||||
</Group>
|
||||
</Item>
|
||||
<Item onClick={async () => await dispatch(markEntry({ entry: props.entry, read: !props.entry.read }))}>
|
||||
<Group>
|
||||
{props.entry.read ? <TbEyeOff size={iconSize} /> : <TbEyeCheck size={iconSize} />}
|
||||
{props.entry.read ? <Trans>Keep unread</Trans> : <Trans>Mark as read</Trans>}
|
||||
</Group>
|
||||
</Item>
|
||||
{props.entry.markable && (
|
||||
<Item onClick={async () => await dispatch(markEntry({ entry: props.entry, read: !props.entry.read }))}>
|
||||
<Group>
|
||||
{props.entry.read ? <TbMail size={iconSize} /> : <TbMailOpened size={iconSize} />}
|
||||
{props.entry.read ? <Trans>Keep unread</Trans> : <Trans>Mark as read</Trans>}
|
||||
</Group>
|
||||
</Item>
|
||||
)}
|
||||
<Item onClick={async () => await dispatch(markEntriesUpToEntry(props.entry))}>
|
||||
<Group>
|
||||
<TbArrowBarToDown size={iconSize} />
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { t, Trans } from "@lingui/macro"
|
||||
import { msg } from "@lingui/core/macro"
|
||||
import { useLingui } from "@lingui/react"
|
||||
import { Group, Indicator, Popover, TagsInput } from "@mantine/core"
|
||||
import { markEntriesUpToEntry, markEntry, starEntry, tagEntry } from "app/entries/thunks"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import { type Entry } from "app/types"
|
||||
import type { Entry } from "app/types"
|
||||
import { ActionButton } from "components/ActionButton"
|
||||
import { useActionButton } from "hooks/useActionButton"
|
||||
import { useMobile } from "hooks/useMobile"
|
||||
import { TbArrowBarToDown, TbExternalLink, TbEyeCheck, TbEyeOff, TbShare, TbStar, TbStarOff, TbTag } from "react-icons/tb"
|
||||
import { TbArrowBarToDown, TbExternalLink, TbMail, TbMailOpened, TbShare, TbStar, TbStarOff, TbTag } from "react-icons/tb"
|
||||
import { ShareButtons } from "./ShareButtons"
|
||||
|
||||
interface FeedEntryFooterProps {
|
||||
@@ -14,13 +15,11 @@ interface FeedEntryFooterProps {
|
||||
}
|
||||
|
||||
export function FeedEntryFooter(props: FeedEntryFooterProps) {
|
||||
const sharingSettings = useAppSelector(state => state.user.settings?.sharingSettings)
|
||||
const tags = useAppSelector(state => state.user.tags)
|
||||
const mobile = useMobile()
|
||||
const { spacing } = useActionButton()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const showSharingButtons = sharingSettings && Object.values(sharingSettings).some(v => v)
|
||||
const { _ } = useLingui()
|
||||
|
||||
const readStatusButtonClicked = async () =>
|
||||
await dispatch(
|
||||
@@ -42,14 +41,14 @@ export function FeedEntryFooter(props: FeedEntryFooterProps) {
|
||||
<Group gap={spacing}>
|
||||
{props.entry.markable && (
|
||||
<ActionButton
|
||||
icon={props.entry.read ? <TbEyeOff size={18} /> : <TbEyeCheck size={18} />}
|
||||
label={props.entry.read ? <Trans>Keep unread</Trans> : <Trans>Mark as read</Trans>}
|
||||
icon={props.entry.read ? <TbMail size={18} /> : <TbMailOpened size={18} />}
|
||||
label={props.entry.read ? msg`Keep unread` : msg`Mark as read`}
|
||||
onClick={readStatusButtonClicked}
|
||||
/>
|
||||
)}
|
||||
<ActionButton
|
||||
icon={props.entry.starred ? <TbStarOff size={18} /> : <TbStar size={18} />}
|
||||
label={props.entry.starred ? <Trans>Unstar</Trans> : <Trans>Star</Trans>}
|
||||
label={props.entry.starred ? msg`Unstar` : msg`Star`}
|
||||
onClick={async () =>
|
||||
await dispatch(
|
||||
starEntry({
|
||||
@@ -60,27 +59,25 @@ export function FeedEntryFooter(props: FeedEntryFooterProps) {
|
||||
}
|
||||
/>
|
||||
|
||||
{showSharingButtons && (
|
||||
<Popover withArrow withinPortal shadow="md" closeOnClickOutside={!mobile}>
|
||||
<Popover.Target>
|
||||
<ActionButton icon={<TbShare size={18} />} label={<Trans>Share</Trans>} />
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown>
|
||||
<ShareButtons url={props.entry.url} description={props.entry.title} />
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
)}
|
||||
<Popover withArrow withinPortal shadow="md" closeOnClickOutside={!mobile}>
|
||||
<Popover.Target>
|
||||
<ActionButton icon={<TbShare size={18} />} label={msg`Share`} />
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown>
|
||||
<ShareButtons url={props.entry.url} description={props.entry.title} />
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
|
||||
{tags && (
|
||||
<Popover withArrow shadow="md" closeOnClickOutside={!mobile}>
|
||||
<Popover.Target>
|
||||
<Indicator label={props.entry.tags.length} disabled={props.entry.tags.length === 0} inline size={16}>
|
||||
<ActionButton icon={<TbTag size={18} />} label={<Trans>Tags</Trans>} />
|
||||
<ActionButton icon={<TbTag size={18} />} label={msg`Tags`} />
|
||||
</Indicator>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown>
|
||||
<TagsInput
|
||||
placeholder={t`Tags`}
|
||||
placeholder={_(msg`Tags`)}
|
||||
data={tags}
|
||||
value={props.entry.tags}
|
||||
onChange={onTagsChange}
|
||||
@@ -93,13 +90,13 @@ export function FeedEntryFooter(props: FeedEntryFooterProps) {
|
||||
)}
|
||||
|
||||
<a href={props.entry.url} target="_blank" rel="noreferrer">
|
||||
<ActionButton icon={<TbExternalLink size={18} />} label={<Trans>Open link</Trans>} />
|
||||
<ActionButton icon={<TbExternalLink size={18} />} label={msg`Open link`} />
|
||||
</a>
|
||||
</Group>
|
||||
|
||||
<ActionButton
|
||||
icon={<TbArrowBarToDown size={18} />}
|
||||
label={<Trans>Mark as read up to here</Trans>}
|
||||
label={msg`Mark as read up to here`}
|
||||
onClick={async () => await dispatch(markEntriesUpToEntry(props.entry))}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Box } from "@mantine/core"
|
||||
import { Constants } from "app/constants"
|
||||
import { calculatePlaceholderSize } from "app/utils"
|
||||
import { BasicHtmlStyles } from "components/content/BasicHtmlStyles"
|
||||
import { ImageWithPlaceholderWhileLoading } from "components/ImageWithPlaceholderWhileLoading"
|
||||
import { BasicHtmlStyles } from "components/content/BasicHtmlStyles"
|
||||
import { Content } from "./Content"
|
||||
|
||||
export interface MediaProps {
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { ActionIcon, Box, SimpleGrid } from "@mantine/core"
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { ActionIcon, Box, CopyButton, Divider, SimpleGrid } from "@mantine/core"
|
||||
import { Constants } from "app/constants"
|
||||
import { useAppSelector } from "app/store"
|
||||
import { type SharingSettings } from "app/types"
|
||||
import { type IconType } from "react-icons"
|
||||
import type { SharingSettings } from "app/types"
|
||||
import { useBrowserExtension } from "hooks/useBrowserExtension"
|
||||
import { useMobile } from "hooks/useMobile"
|
||||
import type { IconType } from "react-icons"
|
||||
import { TbCheck, TbCopy, TbDeviceDesktopShare, TbDeviceMobileShare } from "react-icons/tb"
|
||||
import { tss } from "tss"
|
||||
|
||||
type Color = `#${string}`
|
||||
@@ -12,51 +16,98 @@ const useStyles = tss
|
||||
color: Color
|
||||
}>()
|
||||
.create(({ theme, colorScheme, color }) => ({
|
||||
socialIcon: {
|
||||
icon: {
|
||||
color,
|
||||
backgroundColor: colorScheme === "dark" ? theme.colors.gray[2] : "white",
|
||||
borderRadius: "50%",
|
||||
},
|
||||
}))
|
||||
|
||||
function ShareButton({ url, icon, color }: { url: string; icon: IconType; color: Color }) {
|
||||
function ShareButton({ icon, color, onClick }: { icon: IconType; color: Color; onClick: () => void }) {
|
||||
const { classes } = useStyles({
|
||||
color,
|
||||
})
|
||||
|
||||
const onClick = (e: React.MouseEvent) => {
|
||||
e.preventDefault()
|
||||
return (
|
||||
<ActionIcon variant="transparent" radius="xl" size={32}>
|
||||
<Box p={6} className={classes.icon} onClick={onClick}>
|
||||
{icon({ size: 18 })}
|
||||
</Box>
|
||||
</ActionIcon>
|
||||
)
|
||||
}
|
||||
|
||||
function SiteShareButton({ url, icon, color }: { icon: IconType; color: Color; url: string }) {
|
||||
const onClick = () => {
|
||||
window.open(url, "", "menubar=no,toolbar=no,resizable=yes,scrollbars=yes,width=800,height=600")
|
||||
}
|
||||
|
||||
return <ShareButton icon={icon} color={color} onClick={onClick} />
|
||||
}
|
||||
|
||||
function CopyUrlButton({ url }: { url: string }) {
|
||||
return (
|
||||
<ActionIcon variant="transparent">
|
||||
<a href={url} target="_blank" rel="noreferrer" onClick={onClick}>
|
||||
<Box p={6} className={classes.socialIcon}>
|
||||
{icon({ size: 18 })}
|
||||
</Box>
|
||||
</a>
|
||||
</ActionIcon>
|
||||
<CopyButton value={url}>
|
||||
{({ copied, copy }) => <ShareButton icon={copied ? TbCheck : TbCopy} color="#000" onClick={copy} />}
|
||||
</CopyButton>
|
||||
)
|
||||
}
|
||||
|
||||
function BrowserNativeShareButton({ url, description }: { url: string; description: string }) {
|
||||
const mobile = useMobile()
|
||||
const { isBrowserExtensionPopup } = useBrowserExtension()
|
||||
const onClick = () => {
|
||||
navigator.share({
|
||||
title: description,
|
||||
url,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<ShareButton
|
||||
icon={mobile && !isBrowserExtensionPopup ? TbDeviceMobileShare : TbDeviceDesktopShare}
|
||||
color="#000"
|
||||
onClick={onClick}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function ShareButtons(props: { url: string; description: string }) {
|
||||
const sharingSettings = useAppSelector(state => state.user.settings?.sharingSettings)
|
||||
const enabledSharingSites = (Object.keys(Constants.sharing) as Array<keyof SharingSettings>).filter(site => sharingSettings?.[site])
|
||||
const url = encodeURIComponent(props.url)
|
||||
const desc = encodeURIComponent(props.description)
|
||||
const clipboardAvailable = typeof navigator.clipboard !== "undefined"
|
||||
const nativeSharingAvailable = typeof navigator.share !== "undefined"
|
||||
const showNativeSection = clipboardAvailable || nativeSharingAvailable
|
||||
const showSharingSites = enabledSharingSites.length > 0
|
||||
const showDivider = showNativeSection && showSharingSites
|
||||
const showNoSharingOptionsAvailable = !showNativeSection && !showSharingSites
|
||||
|
||||
return (
|
||||
<SimpleGrid cols={4}>
|
||||
{(Object.keys(Constants.sharing) as (keyof SharingSettings)[])
|
||||
.filter(site => sharingSettings?.[site])
|
||||
.map(site => (
|
||||
<ShareButton
|
||||
key={site}
|
||||
icon={Constants.sharing[site].icon}
|
||||
color={Constants.sharing[site].color}
|
||||
url={Constants.sharing[site].url(url, desc)}
|
||||
/>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
<>
|
||||
{showNativeSection && (
|
||||
<SimpleGrid cols={4}>
|
||||
{clipboardAvailable && <CopyUrlButton url={props.url} />}
|
||||
{nativeSharingAvailable && <BrowserNativeShareButton url={props.url} description={props.description} />}
|
||||
</SimpleGrid>
|
||||
)}
|
||||
|
||||
{showDivider && <Divider my="xs" />}
|
||||
|
||||
{showSharingSites && (
|
||||
<SimpleGrid cols={4}>
|
||||
{enabledSharingSites.map(site => (
|
||||
<SiteShareButton
|
||||
key={site}
|
||||
icon={Constants.sharing[site].icon}
|
||||
color={Constants.sharing[site].color}
|
||||
url={Constants.sharing[site].url(url, desc)}
|
||||
/>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
)}
|
||||
|
||||
{showNoSharingOptionsAvailable && <Trans>No sharing options available.</Trans>}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { t, Trans } from "@lingui/macro"
|
||||
import { msg } from "@lingui/core/macro"
|
||||
import { useLingui } from "@lingui/react"
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { Box, Button, Group, Stack, TextInput } from "@mantine/core"
|
||||
import { useForm } from "@mantine/form"
|
||||
import { client, errorToStrings } from "app/client"
|
||||
import { redirectToSelectedSource } from "app/redirect/thunks"
|
||||
import { useAppDispatch } from "app/store"
|
||||
import { reloadTree } from "app/tree/thunks"
|
||||
import { type AddCategoryRequest } from "app/types"
|
||||
import type { AddCategoryRequest } from "app/types"
|
||||
import { Alert } from "components/Alert"
|
||||
import { useAsyncCallback } from "react-async-hook"
|
||||
import { TbFolderPlus } from "react-icons/tb"
|
||||
@@ -13,6 +15,7 @@ import { CategorySelect } from "./CategorySelect"
|
||||
|
||||
export function AddCategory() {
|
||||
const dispatch = useAppDispatch()
|
||||
const { _ } = useLingui()
|
||||
|
||||
const form = useForm<AddCategoryRequest>()
|
||||
|
||||
@@ -33,7 +36,7 @@ export function AddCategory() {
|
||||
|
||||
<form onSubmit={form.onSubmit(addCategory.execute)}>
|
||||
<Stack>
|
||||
<TextInput label={<Trans>Category</Trans>} placeholder={t`Category`} {...form.getInputProps("name")} required />
|
||||
<TextInput label={<Trans>Category</Trans>} placeholder={_(msg`Category`)} {...form.getInputProps("name")} required />
|
||||
<CategorySelect label={<Trans>Parent</Trans>} {...form.getInputProps("parentId")} clearable />
|
||||
<Group justify="center">
|
||||
<Button variant="default" onClick={async () => await dispatch(redirectToSelectedSource())}>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { t } from "@lingui/macro"
|
||||
import { msg } from "@lingui/core/macro"
|
||||
import { useLingui } from "@lingui/react"
|
||||
import { Select, type SelectProps } from "@mantine/core"
|
||||
import { type ComboboxItem } from "@mantine/core/lib/components/Combobox/Combobox.types"
|
||||
import type { ComboboxItem } from "@mantine/core/lib/components/Combobox/Combobox.types"
|
||||
import { Constants } from "app/constants"
|
||||
import { useAppSelector } from "app/store"
|
||||
import { type Category } from "app/types"
|
||||
import type { Category } from "app/types"
|
||||
import { flattenCategoryTree } from "app/utils"
|
||||
|
||||
type CategorySelectProps = Partial<SelectProps> & {
|
||||
@@ -13,12 +14,15 @@ type CategorySelectProps = Partial<SelectProps> & {
|
||||
|
||||
export function CategorySelect(props: CategorySelectProps) {
|
||||
const rootCategory = useAppSelector(state => state.tree.rootCategory)
|
||||
const { _ } = useLingui()
|
||||
|
||||
const categories = rootCategory && flattenCategoryTree(rootCategory)
|
||||
const categoriesById = categories?.reduce((map, c) => {
|
||||
map.set(c.id, c)
|
||||
return map
|
||||
}, new Map<string, Category>())
|
||||
const categoryLabel = (cat: Category) => {
|
||||
const categoryLabel = (category: Category) => {
|
||||
let cat = category
|
||||
let label = cat.name
|
||||
|
||||
while (cat.parentId) {
|
||||
@@ -42,7 +46,7 @@ export function CategorySelect(props: CategorySelectProps) {
|
||||
.sort((c1, c2) => c1.label.localeCompare(c2.label))
|
||||
if (props.withAll) {
|
||||
selectData?.unshift({
|
||||
label: t`All`,
|
||||
label: _(msg`All`),
|
||||
value: Constants.categories.all.id,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { t, Trans } from "@lingui/macro"
|
||||
import { msg } from "@lingui/core/macro"
|
||||
import { useLingui } from "@lingui/react"
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { Box, Button, FileInput, Group, Stack } from "@mantine/core"
|
||||
import { isNotEmpty, useForm } from "@mantine/form"
|
||||
import { client, errorToStrings } from "app/client"
|
||||
@@ -11,10 +13,11 @@ import { TbFileImport } from "react-icons/tb"
|
||||
|
||||
export function ImportOpml() {
|
||||
const dispatch = useAppDispatch()
|
||||
const { _ } = useLingui()
|
||||
|
||||
const form = useForm<{ file: File }>({
|
||||
validate: {
|
||||
file: isNotEmpty(t`OPML file is required`),
|
||||
file: isNotEmpty(_(msg`OPML file is required`)),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -38,7 +41,7 @@ export function ImportOpml() {
|
||||
<FileInput
|
||||
label={<Trans>OPML file</Trans>}
|
||||
leftSection={<TbFileImport />}
|
||||
placeholder={t`OPML file`}
|
||||
placeholder={_(msg`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
|
||||
@@ -47,7 +50,7 @@ export function ImportOpml() {
|
||||
}
|
||||
{...form.getInputProps("file")}
|
||||
required
|
||||
accept="application/xml"
|
||||
accept=".xml,.opml"
|
||||
/>
|
||||
<Group justify="center">
|
||||
<Button variant="default" onClick={async () => await dispatch(redirectToSelectedSource())}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Trans } from "@lingui/macro"
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { Box, Button, Group, Stack, Stepper, TextInput } from "@mantine/core"
|
||||
import { useForm } from "@mantine/form"
|
||||
import { client, errorToStrings } from "app/client"
|
||||
@@ -6,7 +6,7 @@ import { Constants } from "app/constants"
|
||||
import { redirectToFeed, redirectToSelectedSource } from "app/redirect/thunks"
|
||||
import { useAppDispatch } from "app/store"
|
||||
import { reloadTree } from "app/tree/thunks"
|
||||
import { type FeedInfoRequest, type SubscribeRequest } from "app/types"
|
||||
import type { FeedInfoRequest, SubscribeRequest } from "app/types"
|
||||
import { Alert } from "components/Alert"
|
||||
import { useState } from "react"
|
||||
import { useAsyncCallback } from "react-async-hook"
|
||||
@@ -39,8 +39,8 @@ export function Subscribe() {
|
||||
},
|
||||
})
|
||||
const subscribe = useAsyncCallback(client.feed.subscribe, {
|
||||
onSuccess: sub => {
|
||||
dispatch(reloadTree())
|
||||
onSuccess: async sub => {
|
||||
await dispatch(reloadTree())
|
||||
dispatch(redirectToFeed(sub.data))
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import { Box, Text } from "@mantine/core"
|
||||
import { type Entry } from "app/types"
|
||||
import type { Entry } from "app/types"
|
||||
import { RelativeDate } from "components/RelativeDate"
|
||||
import { FeedFavicon } from "components/content/FeedFavicon"
|
||||
import { OpenExternalLink } from "components/content/header/OpenExternalLink"
|
||||
import { Star } from "components/content/header/Star"
|
||||
import { OnDesktop } from "components/responsive/OnDesktop"
|
||||
import { tss } from "tss"
|
||||
import { FeedEntryTitle } from "./FeedEntryTitle"
|
||||
import { FeedFavicon } from "./FeedFavicon"
|
||||
|
||||
export interface FeedEntryHeaderProps {
|
||||
entry: Entry
|
||||
showStarIcon?: boolean
|
||||
showExternalLinkIcon?: boolean
|
||||
}
|
||||
|
||||
const useStyles = tss
|
||||
@@ -45,6 +49,7 @@ export function FeedEntryCompactHeader(props: FeedEntryHeaderProps) {
|
||||
})
|
||||
return (
|
||||
<Box className={classes.wrapper}>
|
||||
{props.showStarIcon && <Star entry={props.entry} />}
|
||||
<Box>
|
||||
<FeedFavicon url={props.entry.iconUrl} />
|
||||
</Box>
|
||||
@@ -61,6 +66,7 @@ export function FeedEntryCompactHeader(props: FeedEntryHeaderProps) {
|
||||
<RelativeDate date={props.entry.date} />
|
||||
</Text>
|
||||
</OnDesktop>
|
||||
{props.showExternalLinkIcon && <OpenExternalLink entry={props.entry} />}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -1,13 +1,17 @@
|
||||
import { Box, Space, Text } from "@mantine/core"
|
||||
import { type Entry } from "app/types"
|
||||
import { Box, Flex, Space, Text } from "@mantine/core"
|
||||
import type { Entry } from "app/types"
|
||||
import { RelativeDate } from "components/RelativeDate"
|
||||
import { FeedFavicon } from "components/content/FeedFavicon"
|
||||
import { OpenExternalLink } from "components/content/header/OpenExternalLink"
|
||||
import { Star } from "components/content/header/Star"
|
||||
import { tss } from "tss"
|
||||
import { FeedEntryTitle } from "./FeedEntryTitle"
|
||||
import { FeedFavicon } from "./FeedFavicon"
|
||||
|
||||
export interface FeedEntryHeaderProps {
|
||||
entry: Entry
|
||||
expanded: boolean
|
||||
showStarIcon?: boolean
|
||||
showExternalLinkIcon?: boolean
|
||||
}
|
||||
|
||||
const useStyles = tss
|
||||
@@ -15,12 +19,10 @@ const useStyles = tss
|
||||
read: boolean
|
||||
}>()
|
||||
.create(({ colorScheme, read }) => ({
|
||||
headerText: {
|
||||
main: {
|
||||
fontWeight: colorScheme === "light" && !read ? "bold" : "inherit",
|
||||
},
|
||||
headerSubtext: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
details: {
|
||||
fontSize: "90%",
|
||||
},
|
||||
}))
|
||||
@@ -31,10 +33,18 @@ export function FeedEntryHeader(props: FeedEntryHeaderProps) {
|
||||
})
|
||||
return (
|
||||
<Box>
|
||||
<Box className={classes.headerText}>
|
||||
<FeedEntryTitle entry={props.entry} />
|
||||
</Box>
|
||||
<Box className={classes.headerSubtext}>
|
||||
<Flex align="flex-start" justify="space-between">
|
||||
<Flex align="flex-start" className={classes.main}>
|
||||
{props.showStarIcon && (
|
||||
<Box ml={-5}>
|
||||
<Star entry={props.entry} />
|
||||
</Box>
|
||||
)}
|
||||
<FeedEntryTitle entry={props.entry} />
|
||||
</Flex>
|
||||
{props.showExternalLinkIcon && <OpenExternalLink entry={props.entry} />}
|
||||
</Flex>
|
||||
<Flex align="center" className={classes.details}>
|
||||
<FeedFavicon url={props.entry.iconUrl} />
|
||||
<Space w={6} />
|
||||
<Text c="dimmed">
|
||||
@@ -42,9 +52,9 @@ export function FeedEntryHeader(props: FeedEntryHeaderProps) {
|
||||
<span> · </span>
|
||||
<RelativeDate date={props.entry.date} />
|
||||
</Text>
|
||||
</Box>
|
||||
</Flex>
|
||||
{props.expanded && (
|
||||
<Box className={classes.headerSubtext}>
|
||||
<Box className={classes.details}>
|
||||
<Text c="dimmed">
|
||||
{props.entry.author && <span>by {props.entry.author}</span>}
|
||||
{props.entry.author && props.entry.categories && <span> · </span>}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Highlight } from "@mantine/core"
|
||||
import { useAppSelector } from "app/store"
|
||||
import { type Entry } from "app/types"
|
||||
import type { Entry } from "app/types"
|
||||
|
||||
export interface FeedEntryTitleProps {
|
||||
entry: Entry
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { ActionIcon, Anchor, Tooltip } from "@mantine/core"
|
||||
import { Constants } from "app/constants"
|
||||
import { markEntry } from "app/entries/thunks"
|
||||
import { useAppDispatch } from "app/store"
|
||||
import type { Entry } from "app/types"
|
||||
import { TbExternalLink } from "react-icons/tb"
|
||||
|
||||
export function OpenExternalLink(props: { entry: Entry }) {
|
||||
const dispatch = useAppDispatch()
|
||||
const onClick = (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
dispatch(
|
||||
markEntry({
|
||||
entry: props.entry,
|
||||
read: true,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Anchor href={props.entry.url} target="_blank" rel="noreferrer" onClick={onClick}>
|
||||
<Tooltip label={<Trans>Open link</Trans>} openDelay={Constants.tooltip.delay}>
|
||||
<ActionIcon variant="transparent" c="dimmed">
|
||||
<TbExternalLink size={18} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Anchor>
|
||||
)
|
||||
}
|
||||
29
commafeed-client/src/components/content/header/Star.tsx
Normal file
29
commafeed-client/src/components/content/header/Star.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { ActionIcon, Tooltip } from "@mantine/core"
|
||||
import { Constants } from "app/constants"
|
||||
import { starEntry } from "app/entries/thunks"
|
||||
import { useAppDispatch } from "app/store"
|
||||
import type { Entry } from "app/types"
|
||||
import { TbStar, TbStarFilled } from "react-icons/tb"
|
||||
|
||||
export function Star(props: { entry: Entry }) {
|
||||
const dispatch = useAppDispatch()
|
||||
const onClick = (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
dispatch(
|
||||
starEntry({
|
||||
entry: props.entry,
|
||||
starred: !props.entry.starred,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip label={props.entry.starred ? <Trans>Unstar</Trans> : <Trans>Star</Trans>} openDelay={Constants.tooltip.delay}>
|
||||
<ActionIcon variant="transparent" onClick={onClick}>
|
||||
{props.entry.starred ? <TbStarFilled size={18} /> : <TbStar size={18} />}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { t, Trans } from "@lingui/macro"
|
||||
import { msg } from "@lingui/core/macro"
|
||||
import { useLingui } from "@lingui/react"
|
||||
import { Box, Center, CloseButton, Divider, Group, Indicator, Popover, TextInput } from "@mantine/core"
|
||||
import { useForm } from "@mantine/form"
|
||||
import { reloadEntries, search, selectNextEntry, selectPreviousEntry } from "app/entries/thunks"
|
||||
@@ -57,10 +58,11 @@ export function Header() {
|
||||
const searchFromStore = useAppSelector(state => state.entries.search)
|
||||
const { isBrowserExtensionPopup, openSettingsPage, openAppInNewTab } = useBrowserExtension()
|
||||
const dispatch = useAppDispatch()
|
||||
const { _ } = useLingui()
|
||||
|
||||
const searchForm = useForm<{ search: string }>({
|
||||
validate: {
|
||||
search: value => (value.length > 0 && value.length < 3 ? t`Search requires at least 3 characters` : null),
|
||||
search: value => (value.length > 0 && value.length < 3 ? _(msg`Search requires at least 3 characters`) : null),
|
||||
},
|
||||
})
|
||||
const { setValues } = searchForm
|
||||
@@ -77,7 +79,7 @@ export function Header() {
|
||||
<HeaderToolbar>
|
||||
<ActionButton
|
||||
icon={<TbArrowUp size={iconSize} />}
|
||||
label={<Trans>Previous</Trans>}
|
||||
label={msg`Previous`}
|
||||
onClick={async () =>
|
||||
await dispatch(
|
||||
selectPreviousEntry({
|
||||
@@ -90,7 +92,7 @@ export function Header() {
|
||||
/>
|
||||
<ActionButton
|
||||
icon={<TbArrowDown size={iconSize} />}
|
||||
label={<Trans>Next</Trans>}
|
||||
label={msg`Next`}
|
||||
onClick={async () =>
|
||||
await dispatch(
|
||||
selectNextEntry({
|
||||
@@ -106,7 +108,7 @@ export function Header() {
|
||||
|
||||
<ActionButton
|
||||
icon={<TbRefresh size={iconSize} />}
|
||||
label={<Trans>Refresh</Trans>}
|
||||
label={msg`Refresh`}
|
||||
onClick={async () => await dispatch(reloadEntries())}
|
||||
/>
|
||||
<MarkAllAsReadButton iconSize={iconSize} />
|
||||
@@ -115,25 +117,25 @@ export function Header() {
|
||||
|
||||
<ActionButton
|
||||
icon={settings.readingMode === "all" ? <TbEye size={iconSize} /> : <TbEyeOff size={iconSize} />}
|
||||
label={settings.readingMode === "all" ? <Trans>All</Trans> : <Trans>Unread</Trans>}
|
||||
label={settings.readingMode === "all" ? msg`All` : msg`Unread`}
|
||||
onClick={async () => await dispatch(changeReadingMode(settings.readingMode === "all" ? "unread" : "all"))}
|
||||
/>
|
||||
<ActionButton
|
||||
icon={settings.readingOrder === "asc" ? <TbSortAscending size={iconSize} /> : <TbSortDescending size={iconSize} />}
|
||||
label={settings.readingOrder === "asc" ? <Trans>Asc</Trans> : <Trans>Desc</Trans>}
|
||||
label={settings.readingOrder === "asc" ? msg`Asc` : msg`Desc`}
|
||||
onClick={async () => await dispatch(changeReadingOrder(settings.readingOrder === "asc" ? "desc" : "asc"))}
|
||||
/>
|
||||
|
||||
<Popover>
|
||||
<Popover.Target>
|
||||
<Indicator disabled={!searchFromStore}>
|
||||
<ActionButton icon={<TbSearch size={iconSize} />} label={<Trans>Search</Trans>} />
|
||||
<ActionButton icon={<TbSearch size={iconSize} />} label={msg`Search`} />
|
||||
</Indicator>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown>
|
||||
<form onSubmit={searchForm.onSubmit(async values => await dispatch(search(values.search)))}>
|
||||
<TextInput
|
||||
placeholder={t`Search`}
|
||||
placeholder={_(msg`Search`)}
|
||||
{...searchForm.getInputProps("search")}
|
||||
leftSection={<TbSearch size={iconSize} />}
|
||||
rightSection={<CloseButton onClick={async () => await (searchFromStore && dispatch(search("")))} />}
|
||||
@@ -153,12 +155,12 @@ export function Header() {
|
||||
|
||||
<ActionButton
|
||||
icon={<TbSettings size={iconSize} />}
|
||||
label={<Trans>Extension options</Trans>}
|
||||
label={msg`Extension options`}
|
||||
onClick={() => openSettingsPage()}
|
||||
/>
|
||||
<ActionButton
|
||||
icon={<TbExternalLink size={iconSize} />}
|
||||
label={<Trans>Open CommaFeed</Trans>}
|
||||
label={msg`Open CommaFeed`}
|
||||
onClick={() => openAppInNewTab()}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Trans } from "@lingui/macro"
|
||||
import { msg } from "@lingui/core/macro"
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
|
||||
import { Button, Code, Group, Modal, Slider, Stack, Text } from "@mantine/core"
|
||||
import { markAllEntries } from "app/entries/thunks"
|
||||
@@ -91,7 +92,7 @@ export function MarkAllAsReadButton(props: { iconSize: number }) {
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
<ActionButton icon={<TbChecks size={props.iconSize} />} label={<Trans>Mark all as read</Trans>} onClick={buttonClicked} />
|
||||
<ActionButton icon={<TbChecks size={props.iconSize} />} label={msg`Mark all as read`} onClick={buttonClicked} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Trans } from "@lingui/macro"
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import {
|
||||
Box,
|
||||
Divider,
|
||||
@@ -13,8 +13,11 @@ import { showNotification } from "@mantine/notifications"
|
||||
import { client } from "app/client"
|
||||
import { redirectToAbout, redirectToAdminUsers, redirectToDonate, redirectToMetrics, redirectToSettings } from "app/redirect/thunks"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import { type ViewMode } from "app/types"
|
||||
import { useViewMode } from "hooks/useViewMode"
|
||||
import type { ViewMode } from "app/types"
|
||||
import { setViewMode } from "app/user/slice"
|
||||
import { reloadProfile } from "app/user/thunks"
|
||||
import dayjs from "dayjs"
|
||||
import { useNow } from "hooks/useNow"
|
||||
import { type ReactNode, useState } from "react"
|
||||
import {
|
||||
TbChartLine,
|
||||
@@ -92,12 +95,19 @@ const viewModeData: ViewModeControlItem[] = [
|
||||
|
||||
export function ProfileMenu(props: ProfileMenuProps) {
|
||||
const [opened, setOpened] = useState(false)
|
||||
const { viewMode, setViewMode } = useViewMode()
|
||||
const now = useNow()
|
||||
const profile = useAppSelector(state => state.user.profile)
|
||||
const admin = useAppSelector(state => state.user.profile?.admin)
|
||||
const viewMode = useAppSelector(state => state.user.localSettings.viewMode)
|
||||
const forceRefreshCooldownDuration = useAppSelector(state => state.server.serverInfos?.forceRefreshCooldownDuration)
|
||||
const dispatch = useAppDispatch()
|
||||
const { colorScheme, setColorScheme } = useMantineColorScheme()
|
||||
|
||||
const nextAvailableForceRefresh = profile?.lastForceRefresh
|
||||
? profile.lastForceRefresh + (forceRefreshCooldownDuration ?? 0)
|
||||
: now.getTime()
|
||||
const forceRefreshEnabled = nextAvailableForceRefresh <= now.getTime()
|
||||
|
||||
const logout = () => {
|
||||
window.location.href = "logout"
|
||||
}
|
||||
@@ -118,18 +128,32 @@ export function ProfileMenu(props: ProfileMenuProps) {
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
leftSection={<TbWorldDownload size={iconSize} />}
|
||||
onClick={async () =>
|
||||
await client.feed.refreshAll().then(() => {
|
||||
disabled={!forceRefreshEnabled}
|
||||
onClick={async () => {
|
||||
setOpened(false)
|
||||
|
||||
try {
|
||||
await client.feed.refreshAll()
|
||||
|
||||
// reload profile to update last force refresh timestamp
|
||||
await dispatch(reloadProfile())
|
||||
|
||||
showNotification({
|
||||
message: <Trans>Your feeds have been queued for refresh.</Trans>,
|
||||
color: "green",
|
||||
autoClose: 1000,
|
||||
})
|
||||
setOpened(false)
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
showNotification({
|
||||
message: <Trans>Force fetching feeds is not yet available.</Trans>,
|
||||
color: "red",
|
||||
autoClose: 2000,
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Trans>Fetch all my feeds now</Trans>
|
||||
{!forceRefreshEnabled && <span> ({dayjs.duration(nextAvailableForceRefresh - now.getTime()).format("HH:mm:ss")})</span>}
|
||||
</Menu.Item>
|
||||
|
||||
<Divider />
|
||||
@@ -156,7 +180,7 @@ export function ProfileMenu(props: ProfileMenuProps) {
|
||||
orientation="vertical"
|
||||
data={viewModeData}
|
||||
value={viewMode}
|
||||
onChange={e => setViewMode(e as ViewMode)}
|
||||
onChange={e => dispatch(setViewMode(e as ViewMode))}
|
||||
mb="xs"
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { type MetricGauge } from "app/types"
|
||||
import { NumberFormatter } from "@mantine/core"
|
||||
import type { MetricGauge } from "app/types"
|
||||
|
||||
interface MeterProps {
|
||||
gauge: MetricGauge
|
||||
}
|
||||
|
||||
export function Gauge(props: MeterProps) {
|
||||
return <span>{props.gauge.value}</span>
|
||||
return <NumberFormatter value={props.gauge.value} thousandSeparator />
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Box } from "@mantine/core"
|
||||
import { type MetricMeter } from "app/types"
|
||||
import type { MetricMeter } from "app/types"
|
||||
|
||||
interface MeterProps {
|
||||
meter: MetricMeter
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Box } from "@mantine/core"
|
||||
import { type MetricTimer } from "app/types"
|
||||
import type { MetricTimer } from "app/types"
|
||||
|
||||
interface MetricTimerProps {
|
||||
timer: MetricTimer
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Box } from "@mantine/core"
|
||||
import { useMobile } from "hooks/useMobile"
|
||||
import React from "react"
|
||||
import type React from "react"
|
||||
|
||||
export function OnDesktop(props: { children: React.ReactNode }) {
|
||||
const mobile = useMobile()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Box } from "@mantine/core"
|
||||
import { useMobile } from "hooks/useMobile"
|
||||
import React from "react"
|
||||
import type React from "react"
|
||||
|
||||
export function OnMobile(props: { children: React.ReactNode }) {
|
||||
const mobile = useMobile()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Trans } from "@lingui/macro"
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { Box, Button, Group, Stack } from "@mantine/core"
|
||||
import { useForm } from "@mantine/form"
|
||||
import { client, errorToStrings } from "app/client"
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { Trans } from "@lingui/macro"
|
||||
import { Divider, Group, Radio, Select, SimpleGrid, Stack, Switch } from "@mantine/core"
|
||||
import { msg } from "@lingui/core/macro"
|
||||
import { useLingui } from "@lingui/react"
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { Divider, Group, NumberInput, Radio, Select, SimpleGrid, Stack, Switch } from "@mantine/core"
|
||||
import type { ComboboxData } from "@mantine/core/lib/components/Combobox/Combobox.types"
|
||||
import { Constants } from "app/constants"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import { type ScrollMode, type SharingSettings } from "app/types"
|
||||
import type { IconDisplayMode, ScrollMode, SharingSettings } from "app/types"
|
||||
import {
|
||||
changeCustomContextMenu,
|
||||
changeEntriesToKeepOnTopWhenScrolling,
|
||||
changeExternalLinkIconDisplayMode,
|
||||
changeLanguage,
|
||||
changeMarkAllAsReadConfirmation,
|
||||
changeMobileFooter,
|
||||
@@ -13,9 +18,12 @@ import {
|
||||
changeScrollSpeed,
|
||||
changeSharingSetting,
|
||||
changeShowRead,
|
||||
changeStarIconDisplayMode,
|
||||
changeUnreadCountFavicon,
|
||||
changeUnreadCountTitle,
|
||||
} from "app/user/thunks"
|
||||
import { locales } from "i18n"
|
||||
import { type ReactNode } from "react"
|
||||
import type { ReactNode } from "react"
|
||||
|
||||
export function DisplaySettings() {
|
||||
const language = useAppSelector(state => state.user.settings?.language)
|
||||
@@ -23,11 +31,17 @@ export function DisplaySettings() {
|
||||
const showRead = useAppSelector(state => state.user.settings?.showRead)
|
||||
const scrollMarks = useAppSelector(state => state.user.settings?.scrollMarks)
|
||||
const scrollMode = useAppSelector(state => state.user.settings?.scrollMode)
|
||||
const entriesToKeepOnTop = useAppSelector(state => state.user.settings?.entriesToKeepOnTopWhenScrolling)
|
||||
const starIconDisplayMode = useAppSelector(state => state.user.settings?.starIconDisplayMode)
|
||||
const externalLinkIconDisplayMode = useAppSelector(state => state.user.settings?.externalLinkIconDisplayMode)
|
||||
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 unreadCountTitle = useAppSelector(state => state.user.settings?.unreadCountTitle)
|
||||
const unreadCountFavicon = useAppSelector(state => state.user.settings?.unreadCountFavicon)
|
||||
const sharingSettings = useAppSelector(state => state.user.settings?.sharingSettings)
|
||||
const dispatch = useAppDispatch()
|
||||
const { _ } = useLingui()
|
||||
|
||||
const scrollModeOptions: Record<ScrollMode, ReactNode> = {
|
||||
always: <Trans>Always</Trans>,
|
||||
@@ -35,6 +49,25 @@ export function DisplaySettings() {
|
||||
if_needed: <Trans>If the entry doesn't entirely fit on the screen</Trans>,
|
||||
}
|
||||
|
||||
const displayModeData: ComboboxData = [
|
||||
{
|
||||
value: "always",
|
||||
label: _(msg`Always`),
|
||||
},
|
||||
{
|
||||
value: "on_desktop",
|
||||
label: _(msg`On desktop`),
|
||||
},
|
||||
{
|
||||
value: "on_mobile",
|
||||
label: _(msg`On mobile`),
|
||||
},
|
||||
{
|
||||
value: "never",
|
||||
label: _(msg`Never`),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Select
|
||||
@@ -59,18 +92,48 @@ export function DisplaySettings() {
|
||||
onChange={async e => await dispatch(changeMarkAllAsReadConfirmation(e.currentTarget.checked))}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label={<Trans>Show CommaFeed's own context menu on right click</Trans>}
|
||||
checked={customContextMenu}
|
||||
onChange={async e => await dispatch(changeCustomContextMenu(e.currentTarget.checked))}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label={<Trans>On mobile, show action buttons at the bottom of the screen</Trans>}
|
||||
checked={mobileFooter}
|
||||
onChange={async e => await dispatch(changeMobileFooter(e.currentTarget.checked))}
|
||||
/>
|
||||
|
||||
<Divider label={<Trans>Browser tab</Trans>} labelPosition="center" />
|
||||
|
||||
<Switch
|
||||
label={<Trans>Show unread count in tab title</Trans>}
|
||||
checked={unreadCountTitle}
|
||||
onChange={async e => await dispatch(changeUnreadCountTitle(e.currentTarget.checked))}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label={<Trans>Show unread count in tab favicon</Trans>}
|
||||
checked={unreadCountFavicon}
|
||||
onChange={async e => await dispatch(changeUnreadCountFavicon(e.currentTarget.checked))}
|
||||
/>
|
||||
|
||||
<Divider label={<Trans>Entry headers</Trans>} labelPosition="center" />
|
||||
|
||||
<Select
|
||||
description={<Trans>Show star icon</Trans>}
|
||||
value={starIconDisplayMode}
|
||||
data={displayModeData}
|
||||
onChange={async s => await dispatch(changeStarIconDisplayMode(s as IconDisplayMode))}
|
||||
/>
|
||||
|
||||
<Select
|
||||
description={<Trans>Show external link icon</Trans>}
|
||||
value={externalLinkIconDisplayMode}
|
||||
data={displayModeData}
|
||||
onChange={async s => await dispatch(changeExternalLinkIconDisplayMode(s as IconDisplayMode))}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label={<Trans>Show CommaFeed's own context menu on right click</Trans>}
|
||||
checked={customContextMenu}
|
||||
onChange={async e => await dispatch(changeCustomContextMenu(e.currentTarget.checked))}
|
||||
/>
|
||||
|
||||
<Divider label={<Trans>Scrolling</Trans>} labelPosition="center" />
|
||||
|
||||
<Radio.Group
|
||||
@@ -85,6 +148,14 @@ export function DisplaySettings() {
|
||||
</Group>
|
||||
</Radio.Group>
|
||||
|
||||
<NumberInput
|
||||
label={<Trans>Entries to keep above the selected entry when scrolling</Trans>}
|
||||
description={<Trans>Only applies to compact, cozy and detailed modes</Trans>}
|
||||
min={0}
|
||||
value={entriesToKeepOnTop}
|
||||
onChange={async value => await dispatch(changeEntriesToKeepOnTopWhenScrolling(+value))}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label={<Trans>Scroll smoothly when navigating between entries</Trans>}
|
||||
checked={scrollSpeed ? scrollSpeed > 0 : false}
|
||||
@@ -100,7 +171,7 @@ export function DisplaySettings() {
|
||||
<Divider label={<Trans>Sharing sites</Trans>} labelPosition="center" />
|
||||
|
||||
<SimpleGrid cols={2}>
|
||||
{(Object.keys(Constants.sharing) as (keyof SharingSettings)[]).map(site => (
|
||||
{(Object.keys(Constants.sharing) as Array<keyof SharingSettings>).map(site => (
|
||||
<Switch
|
||||
key={site}
|
||||
label={Constants.sharing[site].label}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { t, Trans } from "@lingui/macro"
|
||||
import { msg } from "@lingui/core/macro"
|
||||
import { useLingui } from "@lingui/react"
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { Anchor, Box, Button, Checkbox, Divider, Group, Input, PasswordInput, Stack, Text, TextInput } from "@mantine/core"
|
||||
import { useForm } from "@mantine/form"
|
||||
import { openConfirmModal } from "@mantine/modals"
|
||||
import { client, errorToStrings } from "app/client"
|
||||
import { redirectToLogin, redirectToSelectedSource } from "app/redirect/thunks"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import { type ProfileModificationRequest } from "app/types"
|
||||
import type { ProfileModificationRequest } from "app/types"
|
||||
import { reloadProfile } from "app/user/thunks"
|
||||
import { Alert } from "components/Alert"
|
||||
import { useEffect } from "react"
|
||||
@@ -19,10 +21,11 @@ interface FormData extends ProfileModificationRequest {
|
||||
export function ProfileSettings() {
|
||||
const profile = useAppSelector(state => state.user.profile)
|
||||
const dispatch = useAppDispatch()
|
||||
const { _ } = useLingui()
|
||||
|
||||
const form = useForm<FormData>({
|
||||
validate: {
|
||||
newPasswordConfirmation: (value, values) => (value !== values.newPassword ? t`Passwords do not match` : null),
|
||||
newPasswordConfirmation: (value, values) => (value !== values.newPassword ? _(msg`Passwords do not match`) : null),
|
||||
},
|
||||
})
|
||||
const { setValues } = form
|
||||
@@ -99,7 +102,7 @@ export function ProfileSettings() {
|
||||
}
|
||||
>
|
||||
<Box>
|
||||
<Anchor href="rest/feed/export" download="commafeed_opml.xml">
|
||||
<Anchor href="rest/feed/export" download="commafeed.opml">
|
||||
<Trans>Download</Trans>
|
||||
</Anchor>
|
||||
</Box>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Trans } from "@lingui/macro"
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { Box, Stack } from "@mantine/core"
|
||||
import { Constants } from "app/constants"
|
||||
import {
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from "app/redirect/thunks"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import { collapseTreeCategory } from "app/tree/thunks"
|
||||
import { type Category, type Subscription } from "app/types"
|
||||
import type { Category, Subscription } from "app/types"
|
||||
import { categoryUnreadCount, flattenCategoryTree } from "app/utils"
|
||||
import { Loader } from "components/Loader"
|
||||
import { OnDesktop } from "components/responsive/OnDesktop"
|
||||
@@ -35,6 +35,21 @@ export function Tree() {
|
||||
const showRead = useAppSelector(state => state.user.settings?.showRead)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const isFeedDisplayed = (feed: Subscription) => {
|
||||
const isCurrentFeed = source.type === "feed" && source.id === String(feed.id)
|
||||
return isCurrentFeed || feed.unread > 0 || showRead
|
||||
}
|
||||
|
||||
const isCategoryDisplayed = (category: Category): boolean => {
|
||||
const isCurrentCategory = source.type === "category" && source.id === category.id
|
||||
return (
|
||||
isCurrentCategory ||
|
||||
showRead ||
|
||||
category.children.some(c => isCategoryDisplayed(c)) ||
|
||||
category.feeds.some(f => isFeedDisplayed(f))
|
||||
)
|
||||
}
|
||||
|
||||
const feedClicked = (e: React.MouseEvent, id: string) => {
|
||||
if (e.detail === 2) {
|
||||
dispatch(redirectToFeedDetails(id))
|
||||
@@ -70,6 +85,7 @@ export function Tree() {
|
||||
const allCategoryNode = () => (
|
||||
<TreeNode
|
||||
id={Constants.categories.all.id}
|
||||
type="category"
|
||||
name={<Trans>All</Trans>}
|
||||
icon={allIcon}
|
||||
unread={categoryUnreadCount(root)}
|
||||
@@ -83,6 +99,7 @@ export function Tree() {
|
||||
const starredCategoryNode = () => (
|
||||
<TreeNode
|
||||
id={Constants.categories.starred.id}
|
||||
type="category"
|
||||
name={<Trans>Starred</Trans>}
|
||||
icon={starredIcon}
|
||||
unread={0}
|
||||
@@ -95,16 +112,16 @@ export function Tree() {
|
||||
)
|
||||
|
||||
const categoryNode = (category: Category, level = 0) => {
|
||||
const unreadCount = categoryUnreadCount(category)
|
||||
if (unreadCount === 0 && !showRead) return null
|
||||
if (!isCategoryDisplayed(category)) return null
|
||||
|
||||
const hasError = !category.expanded && flattenCategoryTree(category).some(c => c.feeds.some(f => f.errorCount > errorThreshold))
|
||||
return (
|
||||
<TreeNode
|
||||
id={category.id}
|
||||
type="category"
|
||||
name={category.name}
|
||||
icon={category.expanded ? expandedIcon : collapsedIcon}
|
||||
unread={unreadCount}
|
||||
unread={categoryUnreadCount(category)}
|
||||
selected={source.type === "category" && source.id === category.id}
|
||||
expanded={category.expanded}
|
||||
level={level}
|
||||
@@ -117,11 +134,12 @@ export function Tree() {
|
||||
}
|
||||
|
||||
const feedNode = (feed: Subscription, level = 0) => {
|
||||
if (feed.unread === 0 && !showRead) return null
|
||||
if (!isFeedDisplayed(feed)) return null
|
||||
|
||||
return (
|
||||
<TreeNode
|
||||
id={String(feed.id)}
|
||||
type="feed"
|
||||
name={feed.name}
|
||||
icon={feed.iconUrl}
|
||||
unread={feed.unread}
|
||||
@@ -137,6 +155,7 @@ export function Tree() {
|
||||
const tagNode = (tag: string) => (
|
||||
<TreeNode
|
||||
id={tag}
|
||||
type="tag"
|
||||
name={tag}
|
||||
icon={tagIcon}
|
||||
unread={0}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { Box, Center } from "@mantine/core"
|
||||
import type { EntrySourceType } from "app/entries/slice"
|
||||
import { FeedFavicon } from "components/content/FeedFavicon"
|
||||
import React, { type ReactNode } from "react"
|
||||
import type React from "react"
|
||||
import { tss } from "tss"
|
||||
import { UnreadCount } from "./UnreadCount"
|
||||
|
||||
interface TreeNodeProps {
|
||||
id: string
|
||||
name: ReactNode
|
||||
icon: ReactNode
|
||||
type: EntrySourceType
|
||||
name: React.ReactNode
|
||||
icon: React.ReactNode
|
||||
unread: number
|
||||
selected: boolean
|
||||
expanded?: boolean
|
||||
@@ -27,7 +29,7 @@ const useStyles = tss
|
||||
let backgroundColor = "inherit"
|
||||
if (selected) backgroundColor = colorScheme === "dark" ? theme.colors.dark[4] : theme.colors.gray[1]
|
||||
|
||||
let color
|
||||
let color: string
|
||||
if (hasError) {
|
||||
color = theme.colors.red[6]
|
||||
} else if (colorScheme === "dark") {
|
||||
@@ -63,7 +65,15 @@ export function TreeNode(props: TreeNodeProps) {
|
||||
hasUnread: props.unread > 0,
|
||||
})
|
||||
return (
|
||||
<Box py={1} pl={props.level * 20} className={classes.node} onClick={(e: React.MouseEvent) => props.onClick(e, props.id)}>
|
||||
<Box
|
||||
py={1}
|
||||
pl={props.level * 20}
|
||||
className={classes.node}
|
||||
onClick={(e: React.MouseEvent) => props.onClick(e, props.id)}
|
||||
data-id={props.id}
|
||||
data-type={props.type}
|
||||
data-unread-count={props.unread}
|
||||
>
|
||||
<Box mr={6} onClick={(e: React.MouseEvent) => props.onIconClick?.(e, props.id)}>
|
||||
<Center>{typeof props.icon === "string" ? <FeedFavicon url={props.icon} /> : props.icon}</Center>
|
||||
</Box>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { t, Trans } from "@lingui/macro"
|
||||
import { Box, Center, Kbd, TextInput } from "@mantine/core"
|
||||
import { Spotlight, spotlight, type SpotlightActionData } from "@mantine/spotlight"
|
||||
import { msg } from "@lingui/core/macro"
|
||||
import { useLingui } from "@lingui/react"
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { TextInput } from "@mantine/core"
|
||||
import { Spotlight, type SpotlightActionData, spotlight } from "@mantine/spotlight"
|
||||
import { redirectToFeed } from "app/redirect/thunks"
|
||||
import { useAppDispatch } from "app/store"
|
||||
import { type Subscription } from "app/types"
|
||||
import type { Subscription } from "app/types"
|
||||
import { FeedFavicon } from "components/content/FeedFavicon"
|
||||
import { useMousetrap } from "hooks/useMousetrap"
|
||||
import { TbSearch } from "react-icons/tb"
|
||||
@@ -14,6 +16,7 @@ export interface TreeSearchProps {
|
||||
|
||||
export function TreeSearch(props: TreeSearchProps) {
|
||||
const dispatch = useAppDispatch()
|
||||
const { _ } = useLingui()
|
||||
|
||||
const actions: SpotlightActionData[] = props.feeds
|
||||
.map(f => ({
|
||||
@@ -25,13 +28,6 @@ export function TreeSearch(props: TreeSearchProps) {
|
||||
.sort((f1, f2) => f1.label.localeCompare(f2.label))
|
||||
|
||||
const searchIcon = <TbSearch size={18} />
|
||||
const rightSection = (
|
||||
<Center style={{ cursor: "pointer" }} onClick={() => spotlight.open()}>
|
||||
<Kbd>Ctrl</Kbd>
|
||||
<Box mx={5}>+</Box>
|
||||
<Kbd>K</Kbd>
|
||||
</Center>
|
||||
)
|
||||
|
||||
// additional keyboard shortcut used by commafeed v1
|
||||
useMousetrap("g u", () => spotlight.open())
|
||||
@@ -39,10 +35,9 @@ export function TreeSearch(props: TreeSearchProps) {
|
||||
return (
|
||||
<>
|
||||
<TextInput
|
||||
placeholder={t`Search`}
|
||||
placeholder={_(msg`Search`)}
|
||||
leftSection={searchIcon}
|
||||
rightSectionWidth={100}
|
||||
rightSection={rightSection}
|
||||
styles={{
|
||||
input: {
|
||||
cursor: "pointer",
|
||||
@@ -56,13 +51,13 @@ export function TreeSearch(props: TreeSearchProps) {
|
||||
<Spotlight
|
||||
actions={actions}
|
||||
limit={10}
|
||||
shortcut="ctrl+k"
|
||||
shortcut="mod+k"
|
||||
searchProps={{
|
||||
leftSection: searchIcon,
|
||||
placeholder: t`Search`,
|
||||
placeholder: _(msg`Search`),
|
||||
}}
|
||||
nothingFound={<Trans>Nothing found</Trans>}
|
||||
></Spotlight>
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Badge, Tooltip } from "@mantine/core"
|
||||
import { Constants } from "app/constants"
|
||||
import { tss } from "tss"
|
||||
|
||||
const useStyles = tss.create(() => ({
|
||||
@@ -16,8 +17,8 @@ export function UnreadCount(props: { unreadCount: number }) {
|
||||
|
||||
const count = props.unreadCount >= 10000 ? "10k+" : props.unreadCount
|
||||
return (
|
||||
<Tooltip label={props.unreadCount} disabled={props.unreadCount === count}>
|
||||
<Badge className={classes.badge} variant="light">
|
||||
<Tooltip label={props.unreadCount} disabled={props.unreadCount === count} openDelay={Constants.tooltip.delay}>
|
||||
<Badge className={classes.badge} variant="light" fullWidth>
|
||||
{count}
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { t } from "@lingui/macro"
|
||||
import { msg } from "@lingui/core/macro"
|
||||
import { useLingui } from "@lingui/react"
|
||||
import { useAppSelector } from "app/store"
|
||||
|
||||
interface Step {
|
||||
@@ -11,22 +12,23 @@ export const useAppLoading = () => {
|
||||
const settings = useAppSelector(state => state.user.settings)
|
||||
const rootCategory = useAppSelector(state => state.tree.rootCategory)
|
||||
const tags = useAppSelector(state => state.user.tags)
|
||||
const { _ } = useLingui()
|
||||
|
||||
const steps: Step[] = [
|
||||
{
|
||||
label: t`Loading settings...`,
|
||||
label: _(msg`Loading settings...`),
|
||||
done: !!settings,
|
||||
},
|
||||
{
|
||||
label: t`Loading profile...`,
|
||||
label: _(msg`Loading profile...`),
|
||||
done: !!profile,
|
||||
},
|
||||
{
|
||||
label: t`Loading subscriptions...`,
|
||||
label: _(msg`Loading subscriptions...`),
|
||||
done: !!rootCategory,
|
||||
},
|
||||
{
|
||||
label: t`Loading tags...`,
|
||||
label: _(msg`Loading tags...`),
|
||||
done: !!tags,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -9,13 +9,13 @@ export const useBrowserExtension = () => {
|
||||
// monitor the attribute on the root element as it may change after the page was loaded
|
||||
useEffect(() => {
|
||||
const observer = new MutationObserver(mutations => {
|
||||
mutations.forEach(mutation => {
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.type === "attributes") {
|
||||
const element = mutation.target as Element
|
||||
const version = element.getAttribute("browser-extension-installed")
|
||||
if (version) setBrowserExtensionVersion(version)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
observer.observe(document.documentElement, {
|
||||
@@ -49,7 +49,7 @@ export const useBrowserExtension = () => {
|
||||
)
|
||||
}
|
||||
}
|
||||
const setBadgeUnreadCount = (count: number) => w.postMessage(`set-badge-unread-count:${count}`, "*")
|
||||
const setBadgeUnreadCount = (count: number | string) => w.postMessage(`set-badge-unread-count:${count}`, "*")
|
||||
|
||||
return {
|
||||
browserExtensionVersion,
|
||||
|
||||
10
commafeed-client/src/hooks/useNow.ts
Normal file
10
commafeed-client/src/hooks/useNow.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
export const useNow = (interval = 1000): Date => {
|
||||
const [time, setTime] = useState(new Date())
|
||||
useEffect(() => {
|
||||
const t = setInterval(() => setTime(new Date()), interval)
|
||||
return () => clearInterval(t)
|
||||
}, [interval])
|
||||
return time
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { type ViewMode } from "app/types"
|
||||
import useLocalStorage from "use-local-storage"
|
||||
|
||||
export function useViewMode() {
|
||||
const [viewMode, setViewMode] = useLocalStorage<ViewMode>("view-mode", "detailed")
|
||||
return { viewMode, setViewMode }
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { setWebSocketConnected } from "app/server/slice"
|
||||
import { type AppDispatch, useAppDispatch, useAppSelector } from "app/store"
|
||||
import { incrementUnreadCount } from "app/tree/slice"
|
||||
import { newFeedEntriesDiscovered } from "app/tree/thunks"
|
||||
import { useEffect } from "react"
|
||||
import WebsocketHeartbeatJs from "websocket-heartbeat-js"
|
||||
|
||||
@@ -9,7 +9,7 @@ const handleMessage = (dispatch: AppDispatch, message: string) => {
|
||||
const type = parts[0]
|
||||
if (type === "new-feed-entries") {
|
||||
dispatch(
|
||||
incrementUnreadCount({
|
||||
newFeedEntriesDiscovered({
|
||||
feedId: +parts[1],
|
||||
amount: +parts[2],
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { i18n, type Messages } from "@lingui/core"
|
||||
import { type Messages, i18n } from "@lingui/core"
|
||||
import { useAppSelector } from "app/store"
|
||||
import dayjs from "dayjs"
|
||||
import { useEffect } from "react"
|
||||
@@ -6,40 +6,40 @@ import { useEffect } from "react"
|
||||
interface Locale {
|
||||
key: string
|
||||
label: string
|
||||
daysjsImportFn: () => Promise<ILocale>
|
||||
dayjsImportFn: () => Promise<ILocale>
|
||||
}
|
||||
|
||||
// add an object to the array to add a new locale
|
||||
// don't forget to also add it to the 'locales' array in .linguirc
|
||||
// don't forget to also add it to the 'locales' array in lingui.config.ts
|
||||
export const locales: Locale[] = [
|
||||
{ key: "ar", label: "العربية", daysjsImportFn: async () => await import("dayjs/locale/ar") },
|
||||
{ key: "ca", label: "Català", daysjsImportFn: async () => await import("dayjs/locale/ca") },
|
||||
{ key: "cs", label: "Čeština", daysjsImportFn: async () => await import("dayjs/locale/cs") },
|
||||
{ key: "cy", label: "Cymraeg", daysjsImportFn: async () => await import("dayjs/locale/cy") },
|
||||
{ key: "da", label: "Danish", daysjsImportFn: async () => await import("dayjs/locale/da") },
|
||||
{ key: "de", label: "Deutsch", daysjsImportFn: async () => await import("dayjs/locale/de") },
|
||||
{ key: "en", label: "English", daysjsImportFn: async () => await import("dayjs/locale/en") },
|
||||
{ key: "es", label: "Español", daysjsImportFn: async () => await import("dayjs/locale/es") },
|
||||
{ key: "fa", label: "فارسی", daysjsImportFn: async () => await import("dayjs/locale/fa") },
|
||||
{ key: "fi", label: "Suomi", daysjsImportFn: async () => await import("dayjs/locale/fi") },
|
||||
{ key: "fr", label: "Français", daysjsImportFn: async () => await import("dayjs/locale/fr") },
|
||||
{ key: "gl", label: "Galician", daysjsImportFn: async () => await import("dayjs/locale/gl") },
|
||||
{ key: "hu", label: "Magyar", daysjsImportFn: async () => await import("dayjs/locale/hu") },
|
||||
{ key: "id", label: "Indonesian", daysjsImportFn: async () => await import("dayjs/locale/id") },
|
||||
{ key: "it", label: "Italiano", daysjsImportFn: async () => await import("dayjs/locale/it") },
|
||||
{ key: "ja", label: "日本語", daysjsImportFn: async () => await import("dayjs/locale/ja") },
|
||||
{ key: "ko", label: "한국어", daysjsImportFn: async () => await import("dayjs/locale/ko") },
|
||||
{ key: "ms", label: "Bahasa Malaysian", daysjsImportFn: async () => await import("dayjs/locale/ms") },
|
||||
{ key: "nb", label: "Norsk (bokmål)", daysjsImportFn: async () => await import("dayjs/locale/nb") },
|
||||
{ key: "nl", label: "Nederlands", daysjsImportFn: async () => await import("dayjs/locale/nl") },
|
||||
{ key: "nn", label: "Norsk (nynorsk)", daysjsImportFn: async () => await import("dayjs/locale/nn") },
|
||||
{ key: "pl", label: "Polski", daysjsImportFn: async () => await import("dayjs/locale/pl") },
|
||||
{ key: "pt", label: "Português", daysjsImportFn: async () => await import("dayjs/locale/pt") },
|
||||
{ key: "ru", label: "Русский", daysjsImportFn: async () => await import("dayjs/locale/ru") },
|
||||
{ key: "sk", label: "Slovenčina", daysjsImportFn: async () => await import("dayjs/locale/sk") },
|
||||
{ key: "sv", label: "Svenska", daysjsImportFn: async () => await import("dayjs/locale/sv") },
|
||||
{ key: "tr", label: "Türkçe", daysjsImportFn: async () => await import("dayjs/locale/tr") },
|
||||
{ key: "zh", label: "简体中文", daysjsImportFn: async () => await import("dayjs/locale/zh") },
|
||||
{ key: "ar", label: "العربية", dayjsImportFn: async () => await import("dayjs/locale/ar") },
|
||||
{ key: "ca", label: "Català", dayjsImportFn: async () => await import("dayjs/locale/ca") },
|
||||
{ key: "cs", label: "Čeština", dayjsImportFn: async () => await import("dayjs/locale/cs") },
|
||||
{ key: "cy", label: "Cymraeg", dayjsImportFn: async () => await import("dayjs/locale/cy") },
|
||||
{ key: "da", label: "Danish", dayjsImportFn: async () => await import("dayjs/locale/da") },
|
||||
{ key: "de", label: "Deutsch", dayjsImportFn: async () => await import("dayjs/locale/de") },
|
||||
{ key: "en", label: "English", dayjsImportFn: async () => await import("dayjs/locale/en") },
|
||||
{ key: "es", label: "Español", dayjsImportFn: async () => await import("dayjs/locale/es") },
|
||||
{ key: "fa", label: "فارسی", dayjsImportFn: async () => await import("dayjs/locale/fa") },
|
||||
{ key: "fi", label: "Suomi", dayjsImportFn: async () => await import("dayjs/locale/fi") },
|
||||
{ key: "fr", label: "Français", dayjsImportFn: async () => await import("dayjs/locale/fr") },
|
||||
{ key: "gl", label: "Galician", dayjsImportFn: async () => await import("dayjs/locale/gl") },
|
||||
{ key: "hu", label: "Magyar", dayjsImportFn: async () => await import("dayjs/locale/hu") },
|
||||
{ key: "id", label: "Indonesian", dayjsImportFn: async () => await import("dayjs/locale/id") },
|
||||
{ key: "it", label: "Italiano", dayjsImportFn: async () => await import("dayjs/locale/it") },
|
||||
{ key: "ja", label: "日本語", dayjsImportFn: async () => await import("dayjs/locale/ja") },
|
||||
{ key: "ko", label: "한국어", dayjsImportFn: async () => await import("dayjs/locale/ko") },
|
||||
{ key: "ms", label: "Bahasa Malaysian", dayjsImportFn: async () => await import("dayjs/locale/ms") },
|
||||
{ key: "nb", label: "Norsk (bokmål)", dayjsImportFn: async () => await import("dayjs/locale/nb") },
|
||||
{ key: "nl", label: "Nederlands", dayjsImportFn: async () => await import("dayjs/locale/nl") },
|
||||
{ key: "nn", label: "Norsk (nynorsk)", dayjsImportFn: async () => await import("dayjs/locale/nn") },
|
||||
{ key: "pl", label: "Polski", dayjsImportFn: async () => await import("dayjs/locale/pl") },
|
||||
{ key: "pt", label: "Português", dayjsImportFn: async () => await import("dayjs/locale/pt") },
|
||||
{ key: "ru", label: "Русский", dayjsImportFn: async () => await import("dayjs/locale/ru") },
|
||||
{ key: "sk", label: "Slovenčina", dayjsImportFn: async () => await import("dayjs/locale/sk") },
|
||||
{ key: "sv", label: "Svenska", dayjsImportFn: async () => await import("dayjs/locale/sv") },
|
||||
{ key: "tr", label: "Türkçe", dayjsImportFn: async () => await import("dayjs/locale/tr") },
|
||||
{ key: "zh", label: "简体中文", dayjsImportFn: async () => await import("dayjs/locale/zh") },
|
||||
]
|
||||
|
||||
function activateLocale(locale: string) {
|
||||
@@ -52,7 +52,7 @@ function activateLocale(locale: string) {
|
||||
// dayjs
|
||||
locales
|
||||
.find(l => l.key === locale)
|
||||
?.daysjsImportFn()
|
||||
?.dayjsImportFn()
|
||||
.then(() => dayjs.locale(locale))
|
||||
}
|
||||
|
||||
|
||||
@@ -33,8 +33,8 @@ msgstr ""
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0> هل تحتاج إلى حساب؟ </0> <1> اشترك! </ 1>"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "About"
|
||||
msgstr "حول"
|
||||
|
||||
@@ -54,19 +54,19 @@ msgstr "إضافة فئة"
|
||||
msgid "Add user"
|
||||
msgstr "إضافة مستخدم"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
msgid "Admin"
|
||||
msgstr "إداري"
|
||||
|
||||
#: src/app/constants.ts
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/Tree.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
msgid "All"
|
||||
msgstr "الكل"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always"
|
||||
msgstr ""
|
||||
@@ -139,27 +139,31 @@ msgstr ""
|
||||
msgid "Browser extention"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Browser tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
msgid "Cancel"
|
||||
msgstr "إلغاء"
|
||||
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
msgid "Category"
|
||||
msgstr "الفئة"
|
||||
|
||||
@@ -175,6 +179,10 @@ msgstr "تأكد من عمل الخلاصة"
|
||||
msgid "Close menu"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Cmd"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
@@ -195,11 +203,11 @@ msgstr ""
|
||||
msgid "Compact"
|
||||
msgstr "مضغوط"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
msgid "Confirm"
|
||||
msgstr "تأكيد"
|
||||
|
||||
@@ -264,13 +272,13 @@ msgstr "تنازلي"
|
||||
msgid "Detailed"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Display"
|
||||
msgstr "عرض"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/app/DonatePage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Donate"
|
||||
msgstr ""
|
||||
|
||||
@@ -282,11 +290,11 @@ msgstr "تنزيل"
|
||||
msgid "Drag link to bookmark bar"
|
||||
msgstr "اسحب الرابط إلى شريط الإشارات"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
msgid "E-mail"
|
||||
msgstr "البريد الإلكتروني"
|
||||
|
||||
@@ -299,8 +307,8 @@ msgstr "عنوان البريد الإلكتروني"
|
||||
msgid "Edit user"
|
||||
msgstr "تحرير المستخدم"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
msgid "Enabled"
|
||||
msgstr "ممكن"
|
||||
|
||||
@@ -312,6 +320,14 @@ msgstr "دخول"
|
||||
msgid "Enter your current password to change profile settings"
|
||||
msgstr "أدخل كلمة المرور الحالية لتغيير إعدادات ملف التعريف"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entries to keep above the selected entry when scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entry headers"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/Alert.tsx
|
||||
msgid "Error"
|
||||
msgstr "خطأ"
|
||||
@@ -328,8 +344,8 @@ msgstr "موسع"
|
||||
msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services"
|
||||
msgstr "قم بتصدير اشتراكاتك وفئاتك كملف OPML يمكن استيراده في خدمات قراءة الأعلاف الأخرى"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Extension options"
|
||||
msgstr ""
|
||||
|
||||
@@ -337,9 +353,9 @@ msgstr ""
|
||||
msgid "Feed name"
|
||||
msgstr "اسم الخلاصة"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Feed URL"
|
||||
msgstr "موجز URL"
|
||||
|
||||
@@ -355,21 +371,21 @@ msgstr ""
|
||||
msgid "Fever API URL"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "file is required"
|
||||
msgstr "الملف مطلوب"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Filtering expression"
|
||||
msgstr "تصفية التعبير"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Force fetching feeds is not yet available."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Forgot password?"
|
||||
msgstr "هل نسيت كلمة المرور؟"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Generate an API key in your profile first."
|
||||
msgstr "قم بإنشاء مفتاح API في ملف التعريف الخاص بك أولاً."
|
||||
|
||||
@@ -377,12 +393,13 @@ msgstr "قم بإنشاء مفتاح API في ملف التعريف الخاص
|
||||
msgid "Generate new API key"
|
||||
msgstr "إنشاء مفتاح API جديد"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Generated feed url"
|
||||
msgstr "رابط الخلاصة المولدة"
|
||||
|
||||
#. placeholder {0}: truncate(props.entry.feedName, 30)
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Go to {0}"
|
||||
msgstr ""
|
||||
@@ -423,13 +440,13 @@ msgstr "استيراد"
|
||||
msgid "In expanded view, scrolling through entries mark them as read"
|
||||
msgstr "في العرض الموسع ، التمرير عبر الإدخالات وضع علامة عليها كمقروءة"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Keep unread"
|
||||
msgstr "إبقاء غير مقروءة"
|
||||
|
||||
#: src/components/content/FeedEntries.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/content/FeedEntries.tsx
|
||||
msgid "Keyboard shortcuts"
|
||||
msgstr "اختصارات لوحة المفاتيح"
|
||||
|
||||
@@ -453,9 +470,9 @@ msgstr "آخر رسالة تحديث"
|
||||
msgid "Light"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Link"
|
||||
msgstr "رابط"
|
||||
|
||||
@@ -475,9 +492,9 @@ msgstr "تحميل الاشتراكات ..."
|
||||
msgid "Loading tags..."
|
||||
msgstr "تحميل العلامات ..."
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Log in"
|
||||
msgstr "تسجيل الدخول"
|
||||
|
||||
@@ -489,8 +506,8 @@ msgstr "تسجيل الخروج"
|
||||
msgid "Long press"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Manage users"
|
||||
msgstr "إدارة المستخدمين"
|
||||
|
||||
@@ -498,18 +515,18 @@ msgstr "إدارة المستخدمين"
|
||||
msgid "Mark all as read"
|
||||
msgstr "تعليم الكل كمقروء"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
msgid "Mark all entries as read"
|
||||
msgstr "تعليم كافة الإدخالات كمقروءة"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Mark as read"
|
||||
msgstr "وضع علامة كمقروء"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Mark as read up to here"
|
||||
msgstr "وضع علامة كمقروءة حتى هنا"
|
||||
|
||||
@@ -529,15 +546,15 @@ msgstr "تحريك الصفحة لأسفل"
|
||||
msgid "Move the page up"
|
||||
msgstr "تحريك الصفحة لأعلى"
|
||||
|
||||
#: src/components/RelativeDate.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/components/RelativeDate.tsx
|
||||
msgid "N/A"
|
||||
msgstr "لا"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
msgid "Name"
|
||||
msgstr "الاسم"
|
||||
|
||||
@@ -545,6 +562,7 @@ msgstr "الاسم"
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "انتقل إلى اشتراك بإدخال اسمه"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
msgstr ""
|
||||
@@ -557,8 +575,8 @@ msgstr "كلمة مرور جديدة"
|
||||
msgid "Newest first"
|
||||
msgstr "الأحدث أولاً"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Next"
|
||||
msgstr "التالي"
|
||||
|
||||
@@ -574,6 +592,10 @@ msgstr "اختصار العنصر غير المقروء التالي"
|
||||
msgid "No more entries"
|
||||
msgstr "لا مزيد من الإدخالات"
|
||||
|
||||
#: src/components/content/ShareButtons.tsx
|
||||
msgid "No sharing options available."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
msgid "Nothing found"
|
||||
msgstr "لم يتم العثور على شيء"
|
||||
@@ -582,10 +604,22 @@ msgstr "لم يتم العثور على شيء"
|
||||
msgid "Oldest first"
|
||||
msgstr "الأقدم أولا"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On desktop"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On mobile"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Only applies to compact, cozy and detailed modes"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "اوووه!"
|
||||
@@ -603,6 +637,7 @@ msgid "Open current entry in a new tab in the background"
|
||||
msgstr "فتح الإدخال الحالي في علامة تبويب جديدة في الخلفية"
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
#: src/components/content/header/OpenExternalLink.tsx
|
||||
msgid "Open link"
|
||||
msgstr "افتح الرابط"
|
||||
|
||||
@@ -643,6 +678,10 @@ msgstr "تصدير OPML"
|
||||
msgid "OPML file"
|
||||
msgstr "ملف OPML"
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "OPML file is required"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Order"
|
||||
msgstr "طلب"
|
||||
@@ -655,11 +694,11 @@ msgstr "والد"
|
||||
msgid "Parent Category"
|
||||
msgstr "الفئة الأصل"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "Password"
|
||||
msgstr "كلمة المرور"
|
||||
|
||||
@@ -671,8 +710,8 @@ msgstr "استعادة كلمة المرور"
|
||||
msgid "Passwords do not match"
|
||||
msgstr "كلمات المرور غير متطابقة"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Position"
|
||||
msgstr "المنـصب"
|
||||
|
||||
@@ -688,8 +727,8 @@ msgstr "الملف الشخصي"
|
||||
msgid "Recover password"
|
||||
msgstr "استعادة كلمة السر"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Refresh"
|
||||
msgstr "تحديث"
|
||||
|
||||
@@ -706,11 +745,11 @@ msgstr ""
|
||||
msgid "Right click"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
msgid "Save"
|
||||
msgstr "حفظ"
|
||||
|
||||
@@ -726,10 +765,10 @@ msgstr "قم بالتمرير بسلاسة عند التنقل بين الإدخ
|
||||
msgid "Scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Search"
|
||||
msgstr "بحث"
|
||||
|
||||
@@ -737,6 +776,14 @@ msgstr "بحث"
|
||||
msgid "Search requires at least 3 characters"
|
||||
msgstr "يتطلب البحث 3 أحرف على الأقل"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Select next unread feed/category"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Select previous unread feed/category"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Set focus on next entry without opening it"
|
||||
msgstr "ضع التركيز على الإدخال التالي دون فتحه"
|
||||
@@ -783,6 +830,10 @@ msgstr ""
|
||||
msgid "Show entry menu (mobile)"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show external link icon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show feeds and categories with no unread entries"
|
||||
msgstr "إظهار موجز ويب والفئات التي لا تحتوي على إدخالات غير مقروءة"
|
||||
@@ -795,9 +846,21 @@ msgstr "إظهار تعليمات اختصار لوحة المفاتيح"
|
||||
msgid "Show native menu (desktop)"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show star icon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab favicon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab title"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/WelcomePage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "Sign up"
|
||||
msgstr "قم بالتسجيل"
|
||||
|
||||
@@ -810,19 +873,20 @@ msgstr "شيء سيء حدث للتو ..."
|
||||
msgid "Space"
|
||||
msgstr "فضاء"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/header/Star.tsx
|
||||
msgid "Star"
|
||||
msgstr "النجم"
|
||||
|
||||
#: src/app/constants.ts
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/components/sidebar/Tree.tsx
|
||||
msgid "Starred"
|
||||
msgstr "مميز بنجمة"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/pages/app/AddPage.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Subscribe"
|
||||
msgstr "اشتراك"
|
||||
|
||||
@@ -895,8 +959,9 @@ msgstr ""
|
||||
msgid "Unread"
|
||||
msgstr "غير مقروءة"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/header/Star.tsx
|
||||
msgid "Unstar"
|
||||
msgstr "إلغاء النجم"
|
||||
|
||||
|
||||
@@ -33,8 +33,8 @@ msgstr "<0>Ei,</0><1> sóc la Jérémie de Bèlgica i fa més de 10 anys que tre
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>Necessites un compte?</0><1>Registreu-vos!</1>"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "About"
|
||||
msgstr "Sobre"
|
||||
|
||||
@@ -54,19 +54,19 @@ msgstr "Afegeix categoria"
|
||||
msgid "Add user"
|
||||
msgstr "Afegeix usuari"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
msgid "Admin"
|
||||
msgstr "Administrador"
|
||||
|
||||
#: src/app/constants.ts
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/Tree.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
msgid "All"
|
||||
msgstr "Tot"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always"
|
||||
msgstr "Sempre"
|
||||
@@ -139,27 +139,31 @@ msgstr "Extensió del navegador necessària per a Chrome"
|
||||
msgid "Browser extention"
|
||||
msgstr "Extensió del navegador"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Browser tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
msgid "Cancel"
|
||||
msgstr "Cancel·la"
|
||||
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
msgid "Category"
|
||||
msgstr "Categoria"
|
||||
|
||||
@@ -175,6 +179,10 @@ msgstr "Comproveu que el canal funciona"
|
||||
msgid "Close menu"
|
||||
msgstr "Tanca el menu"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Cmd"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr "Versió de l'extensió del navegador CommaFeed {browserExtensionVersion}."
|
||||
@@ -195,11 +203,11 @@ msgstr "CommaFeed versió {version} ({version})."
|
||||
msgid "Compact"
|
||||
msgstr "Compacte"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
msgid "Confirm"
|
||||
msgstr "Confirma"
|
||||
|
||||
@@ -264,13 +272,13 @@ msgstr "Desc"
|
||||
msgid "Detailed"
|
||||
msgstr "Detallat"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Display"
|
||||
msgstr "Mostra"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/app/DonatePage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Donate"
|
||||
msgstr "Donar"
|
||||
|
||||
@@ -282,11 +290,11 @@ msgstr "Descarrega"
|
||||
msgid "Drag link to bookmark bar"
|
||||
msgstr "Arrossegueu l'enllaç a la barra d'adreces d'interès"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
msgid "E-mail"
|
||||
msgstr "Correu electrònic"
|
||||
|
||||
@@ -299,8 +307,8 @@ msgstr "Adreça de correu electrònic"
|
||||
msgid "Edit user"
|
||||
msgstr "Edita l'usuari"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
msgid "Enabled"
|
||||
msgstr "activat"
|
||||
|
||||
@@ -312,6 +320,14 @@ msgstr "Entra"
|
||||
msgid "Enter your current password to change profile settings"
|
||||
msgstr "introduïu la vostra contrasenya actual per canviar la configuració del perfil"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entries to keep above the selected entry when scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entry headers"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/Alert.tsx
|
||||
msgid "Error"
|
||||
msgstr "Error"
|
||||
@@ -328,8 +344,8 @@ msgstr "Ampliat"
|
||||
msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services"
|
||||
msgstr "exporteu les vostres subscripcions i categories com a fitxer OPML que es pot importar a altres serveis de lectura de feeds"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Extension options"
|
||||
msgstr "Opcions de l'extensió"
|
||||
|
||||
@@ -337,9 +353,9 @@ msgstr "Opcions de l'extensió"
|
||||
msgid "Feed name"
|
||||
msgstr "Nom del canal"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Feed URL"
|
||||
msgstr "URL del canal"
|
||||
|
||||
@@ -355,21 +371,21 @@ msgstr ""
|
||||
msgid "Fever API URL"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "file is required"
|
||||
msgstr "el fitxer és necessari"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Filtering expression"
|
||||
msgstr "Expressió de filtratge"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Force fetching feeds is not yet available."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Forgot password?"
|
||||
msgstr "Heu oblidat la contrasenya?"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Generate an API key in your profile first."
|
||||
msgstr "primer genereu una clau API al vostre perfil."
|
||||
|
||||
@@ -377,12 +393,13 @@ msgstr "primer genereu una clau API al vostre perfil."
|
||||
msgid "Generate new API key"
|
||||
msgstr "Genera una nova clau d'API"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Generated feed url"
|
||||
msgstr "URL del feed generat"
|
||||
|
||||
#. placeholder {0}: truncate(props.entry.feedName, 30)
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Go to {0}"
|
||||
msgstr "Vés a {0}"
|
||||
@@ -423,13 +440,13 @@ msgstr "Importació"
|
||||
msgid "In expanded view, scrolling through entries mark them as read"
|
||||
msgstr "a la vista ampliada, desplaçant-se per les entrades les marqueu com a llegides"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Keep unread"
|
||||
msgstr "Mantenir sense llegir"
|
||||
|
||||
#: src/components/content/FeedEntries.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/content/FeedEntries.tsx
|
||||
msgid "Keyboard shortcuts"
|
||||
msgstr "Dreceres de teclat"
|
||||
|
||||
@@ -453,9 +470,9 @@ msgstr "últim missatge d'actualització"
|
||||
msgid "Light"
|
||||
msgstr "Clar"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Link"
|
||||
msgstr "Enllaç"
|
||||
|
||||
@@ -475,9 +492,9 @@ msgstr "S'estan carregant les subscripcions..."
|
||||
msgid "Loading tags..."
|
||||
msgstr "Carregant les etiquetes..."
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Log in"
|
||||
msgstr "Inicia sessió"
|
||||
|
||||
@@ -489,8 +506,8 @@ msgstr "Tanca sessió"
|
||||
msgid "Long press"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Manage users"
|
||||
msgstr "Gestionar usuaris"
|
||||
|
||||
@@ -498,18 +515,18 @@ msgstr "Gestionar usuaris"
|
||||
msgid "Mark all as read"
|
||||
msgstr "Marca-ho tot com a llegit"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
msgid "Mark all entries as read"
|
||||
msgstr "Marqueu totes les entrades com a llegides"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Mark as read"
|
||||
msgstr "Marca com a llegit"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Mark as read up to here"
|
||||
msgstr "Marca com a llegit fins aquí"
|
||||
|
||||
@@ -529,15 +546,15 @@ msgstr "Mou la pàgina cap avall"
|
||||
msgid "Move the page up"
|
||||
msgstr "Mou la pàgina cap amunt"
|
||||
|
||||
#: src/components/RelativeDate.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/components/RelativeDate.tsx
|
||||
msgid "N/A"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
msgid "Name"
|
||||
msgstr "Nom"
|
||||
|
||||
@@ -545,6 +562,7 @@ 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
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
msgstr "Mai"
|
||||
@@ -557,8 +575,8 @@ msgstr "Contrasenya nova"
|
||||
msgid "Newest first"
|
||||
msgstr "El més nou primer"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Next"
|
||||
msgstr "Següent"
|
||||
|
||||
@@ -574,6 +592,10 @@ msgstr "Següent marcador d'elements no llegit"
|
||||
msgid "No more entries"
|
||||
msgstr "No hi ha més entrades"
|
||||
|
||||
#: src/components/content/ShareButtons.tsx
|
||||
msgid "No sharing options available."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
msgid "Nothing found"
|
||||
msgstr "No s'ha trobat res"
|
||||
@@ -582,10 +604,22 @@ msgstr "No s'ha trobat res"
|
||||
msgid "Oldest first"
|
||||
msgstr "el més vell primer"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On desktop"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On mobile"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr "Al mòbil, mostra els botons d'acció a la part inferior de la pantalla"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Only applies to compact, cozy and detailed modes"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Vaja!"
|
||||
@@ -603,6 +637,7 @@ msgid "Open current entry in a new tab in the background"
|
||||
msgstr "Obre l'entrada actual en una pestanya nova al fons"
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
#: src/components/content/header/OpenExternalLink.tsx
|
||||
msgid "Open link"
|
||||
msgstr "Obre l'enllaç obert"
|
||||
|
||||
@@ -643,6 +678,10 @@ msgstr "Exportació OPML"
|
||||
msgid "OPML file"
|
||||
msgstr "Fitxer OPML"
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "OPML file is required"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Order"
|
||||
msgstr "Ordre"
|
||||
@@ -655,11 +694,11 @@ msgstr "pares"
|
||||
msgid "Parent Category"
|
||||
msgstr "Categoria pare"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "Password"
|
||||
msgstr "Contrasenya"
|
||||
|
||||
@@ -671,8 +710,8 @@ msgstr "Recuperació de contrasenya"
|
||||
msgid "Passwords do not match"
|
||||
msgstr "Les contrasenyes no coincideixen"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Position"
|
||||
msgstr "Posició"
|
||||
|
||||
@@ -688,8 +727,8 @@ msgstr "Perfil"
|
||||
msgid "Recover password"
|
||||
msgstr "Recuperar la contrasenya"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Refresh"
|
||||
msgstr "Actualitzar"
|
||||
|
||||
@@ -706,11 +745,11 @@ msgstr "API REST"
|
||||
msgid "Right click"
|
||||
msgstr "Clic dret"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
msgid "Save"
|
||||
msgstr "Desa"
|
||||
|
||||
@@ -726,10 +765,10 @@ msgstr "Desplaceu-vos suaument quan navegueu entre entrades"
|
||||
msgid "Scrolling"
|
||||
msgstr "Desplaçament"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Search"
|
||||
msgstr "Cerca"
|
||||
|
||||
@@ -737,6 +776,14 @@ msgstr "Cerca"
|
||||
msgid "Search requires at least 3 characters"
|
||||
msgstr "la cerca requereix almenys 3 caràcters"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Select next unread feed/category"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Select previous unread feed/category"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Set focus on next entry without opening it"
|
||||
msgstr "posa el focus a la següent entrada sense obrir-la"
|
||||
@@ -783,6 +830,10 @@ msgstr "Mostra el menú d'entrada (escriptori)"
|
||||
msgid "Show entry menu (mobile)"
|
||||
msgstr "Mostra el menú d'entrada (mòbil)"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show external link icon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show feeds and categories with no unread entries"
|
||||
msgstr "Mostra feeds i categories sense entrades no llegides"
|
||||
@@ -795,9 +846,21 @@ msgstr "Mostra l'ajuda de la drecera del teclat"
|
||||
msgid "Show native menu (desktop)"
|
||||
msgstr "Mostra el menú natiu (escriptori)"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show star icon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab favicon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab title"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/WelcomePage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "Sign up"
|
||||
msgstr "Registra't"
|
||||
|
||||
@@ -810,19 +873,20 @@ msgstr "Acaba de passar una cosa dolenta..."
|
||||
msgid "Space"
|
||||
msgstr "Espai"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/header/Star.tsx
|
||||
msgid "Star"
|
||||
msgstr "Estrella"
|
||||
|
||||
#: src/app/constants.ts
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/components/sidebar/Tree.tsx
|
||||
msgid "Starred"
|
||||
msgstr "Estrellat"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/pages/app/AddPage.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Subscribe"
|
||||
msgstr "Subscriu-te"
|
||||
|
||||
@@ -895,8 +959,9 @@ msgstr "Prova la demostració!"
|
||||
msgid "Unread"
|
||||
msgstr "Sense llegir"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/header/Star.tsx
|
||||
msgid "Unstar"
|
||||
msgstr "Desestrellar"
|
||||
|
||||
|
||||
@@ -33,8 +33,8 @@ msgstr ""
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>Potřebujete účet?</0><1>Zaregistrujte se!</1>"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "About"
|
||||
msgstr "Asi"
|
||||
|
||||
@@ -54,19 +54,19 @@ msgstr "Přidat kategorii"
|
||||
msgid "Add user"
|
||||
msgstr "Přidat uživatele"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
msgid "Admin"
|
||||
msgstr "Správce"
|
||||
|
||||
#: src/app/constants.ts
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/Tree.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
msgid "All"
|
||||
msgstr "Všechny"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always"
|
||||
msgstr ""
|
||||
@@ -139,27 +139,31 @@ msgstr ""
|
||||
msgid "Browser extention"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Browser tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
msgid "Cancel"
|
||||
msgstr "Zrušit"
|
||||
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
msgid "Category"
|
||||
msgstr "Kategorie"
|
||||
|
||||
@@ -175,6 +179,10 @@ msgstr "Zkontrolujte, zda zdroj funguje"
|
||||
msgid "Close menu"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Cmd"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
@@ -195,11 +203,11 @@ msgstr ""
|
||||
msgid "Compact"
|
||||
msgstr "Kompaktní"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
msgid "Confirm"
|
||||
msgstr "Potvrdit"
|
||||
|
||||
@@ -264,13 +272,13 @@ msgstr ""
|
||||
msgid "Detailed"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Display"
|
||||
msgstr "Displej"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/app/DonatePage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Donate"
|
||||
msgstr ""
|
||||
|
||||
@@ -282,11 +290,11 @@ msgstr "Stáhnout"
|
||||
msgid "Drag link to bookmark bar"
|
||||
msgstr "Přetáhněte odkaz na lištu záložek"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
msgid "E-mail"
|
||||
msgstr ""
|
||||
|
||||
@@ -299,8 +307,8 @@ msgstr "E-mailová adresa"
|
||||
msgid "Edit user"
|
||||
msgstr "Upravit uživatele"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
msgid "Enabled"
|
||||
msgstr "Povoleno"
|
||||
|
||||
@@ -312,6 +320,14 @@ msgstr "Vstupte"
|
||||
msgid "Enter your current password to change profile settings"
|
||||
msgstr "Zadejte své aktuální heslo pro změnu nastavení profilu"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entries to keep above the selected entry when scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entry headers"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/Alert.tsx
|
||||
msgid "Error"
|
||||
msgstr "Chyba"
|
||||
@@ -328,8 +344,8 @@ msgstr "Rozbaleno"
|
||||
msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services"
|
||||
msgstr "Exportujte svá předplatná a kategorie jako soubor OPML, který lze importovat do jiných služeb čtení kanálů"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Extension options"
|
||||
msgstr ""
|
||||
|
||||
@@ -337,9 +353,9 @@ msgstr ""
|
||||
msgid "Feed name"
|
||||
msgstr "Název zdroje"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Feed URL"
|
||||
msgstr "URL zdroje"
|
||||
|
||||
@@ -355,21 +371,21 @@ msgstr ""
|
||||
msgid "Fever API URL"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "file is required"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Filtering expression"
|
||||
msgstr "Filtrování výrazu"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Force fetching feeds is not yet available."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Forgot password?"
|
||||
msgstr "Zapomněli jste heslo?"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Generate an API key in your profile first."
|
||||
msgstr "Nejprve ve svém profilu vygenerujte klíč API."
|
||||
|
||||
@@ -377,12 +393,13 @@ msgstr "Nejprve ve svém profilu vygenerujte klíč API."
|
||||
msgid "Generate new API key"
|
||||
msgstr "Vygenerujte nový klíč API"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Generated feed url"
|
||||
msgstr "Generovaná adresa URL zdroje"
|
||||
|
||||
#. placeholder {0}: truncate(props.entry.feedName, 30)
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Go to {0}"
|
||||
msgstr ""
|
||||
@@ -423,13 +440,13 @@ msgstr ""
|
||||
msgid "In expanded view, scrolling through entries mark them as read"
|
||||
msgstr "V rozšířeném zobrazení je procházením označíte jako přečtené"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Keep unread"
|
||||
msgstr "Ponechat nepřečtené"
|
||||
|
||||
#: src/components/content/FeedEntries.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/content/FeedEntries.tsx
|
||||
msgid "Keyboard shortcuts"
|
||||
msgstr "Klávesové zkratky"
|
||||
|
||||
@@ -453,9 +470,9 @@ msgstr "Poslední obnovovací zpráva"
|
||||
msgid "Light"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Link"
|
||||
msgstr "Odkaz"
|
||||
|
||||
@@ -475,9 +492,9 @@ msgstr "Načítání odběrů..."
|
||||
msgid "Loading tags..."
|
||||
msgstr "Načítání značek..."
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Log in"
|
||||
msgstr "Přihlaste se"
|
||||
|
||||
@@ -489,8 +506,8 @@ msgstr "Odhlášení"
|
||||
msgid "Long press"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Manage users"
|
||||
msgstr "Spravujte uživatele"
|
||||
|
||||
@@ -498,18 +515,18 @@ msgstr "Spravujte uživatele"
|
||||
msgid "Mark all as read"
|
||||
msgstr "Označit vše jako přečtené"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
msgid "Mark all entries as read"
|
||||
msgstr "Označte všechny položky jako přečtené"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Mark as read"
|
||||
msgstr "Označit jako přečtené"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Mark as read up to here"
|
||||
msgstr "Označit jako přečtené až sem"
|
||||
|
||||
@@ -529,15 +546,15 @@ msgstr "Přesuňte stránku dolů"
|
||||
msgid "Move the page up"
|
||||
msgstr "Přesuňte stránku nahoru"
|
||||
|
||||
#: src/components/RelativeDate.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/components/RelativeDate.tsx
|
||||
msgid "N/A"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
msgid "Name"
|
||||
msgstr "Jméno"
|
||||
|
||||
@@ -545,6 +562,7 @@ 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
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
msgstr ""
|
||||
@@ -557,8 +575,8 @@ msgstr "Nové heslo"
|
||||
msgid "Newest first"
|
||||
msgstr "Nejnovější jako první"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Next"
|
||||
msgstr "Další"
|
||||
|
||||
@@ -574,6 +592,10 @@ msgstr "Další nepřečtená položka bookmarklet"
|
||||
msgid "No more entries"
|
||||
msgstr "Žádné další záznamy"
|
||||
|
||||
#: src/components/content/ShareButtons.tsx
|
||||
msgid "No sharing options available."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
msgid "Nothing found"
|
||||
msgstr "Nic nebylo nalezeno"
|
||||
@@ -582,10 +604,22 @@ msgstr "Nic nebylo nalezeno"
|
||||
msgid "Oldest first"
|
||||
msgstr "Nejdříve nejstarší"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On desktop"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On mobile"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Only applies to compact, cozy and detailed modes"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Jejda!"
|
||||
@@ -603,6 +637,7 @@ msgid "Open current entry in a new tab in the background"
|
||||
msgstr "Otevřít aktuální položku na nové kartě na pozadí"
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
#: src/components/content/header/OpenExternalLink.tsx
|
||||
msgid "Open link"
|
||||
msgstr "Otevřít odkaz"
|
||||
|
||||
@@ -643,6 +678,10 @@ msgstr "Export OPML"
|
||||
msgid "OPML file"
|
||||
msgstr "soubor OPML"
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "OPML file is required"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Order"
|
||||
msgstr "Objednávka"
|
||||
@@ -655,11 +694,11 @@ msgstr "Rodič"
|
||||
msgid "Parent Category"
|
||||
msgstr "Rodičovská kategorie"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "Password"
|
||||
msgstr "Heslo"
|
||||
|
||||
@@ -671,8 +710,8 @@ msgstr "Obnovení hesla"
|
||||
msgid "Passwords do not match"
|
||||
msgstr "Hesla se neshodují"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Position"
|
||||
msgstr "Pozice"
|
||||
|
||||
@@ -688,8 +727,8 @@ msgstr "Profil"
|
||||
msgid "Recover password"
|
||||
msgstr "Obnovte heslo"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Refresh"
|
||||
msgstr "Obnovit"
|
||||
|
||||
@@ -706,11 +745,11 @@ msgstr ""
|
||||
msgid "Right click"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
msgid "Save"
|
||||
msgstr "Uložit"
|
||||
|
||||
@@ -726,10 +765,10 @@ msgstr "Posouvejte plynule při navigaci mezi položkami"
|
||||
msgid "Scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Search"
|
||||
msgstr "Hledej"
|
||||
|
||||
@@ -737,6 +776,14 @@ msgstr "Hledej"
|
||||
msgid "Search requires at least 3 characters"
|
||||
msgstr "Hledání vyžaduje alespoň 3 znaky"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Select next unread feed/category"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Select previous unread feed/category"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Set focus on next entry without opening it"
|
||||
msgstr "Zaměřte se na další položku, aniž byste ji otevřeli"
|
||||
@@ -783,6 +830,10 @@ msgstr ""
|
||||
msgid "Show entry menu (mobile)"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show external link icon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show feeds and categories with no unread entries"
|
||||
msgstr "Zobrazit kanály a kategorie bez nepřečtených položek"
|
||||
@@ -795,9 +846,21 @@ msgstr "Zobrazit nápovědu ke klávesovým zkratkám"
|
||||
msgid "Show native menu (desktop)"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show star icon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab favicon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab title"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/WelcomePage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "Sign up"
|
||||
msgstr "Zaregistrujte se"
|
||||
|
||||
@@ -810,19 +873,20 @@ msgstr "Právě se stalo něco špatného..."
|
||||
msgid "Space"
|
||||
msgstr "Vesmír"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/header/Star.tsx
|
||||
msgid "Star"
|
||||
msgstr "Hvězda"
|
||||
|
||||
#: src/app/constants.ts
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/components/sidebar/Tree.tsx
|
||||
msgid "Starred"
|
||||
msgstr "S hvězdičkou"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/pages/app/AddPage.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Subscribe"
|
||||
msgstr "Přihlaste se"
|
||||
|
||||
@@ -895,8 +959,9 @@ msgstr ""
|
||||
msgid "Unread"
|
||||
msgstr "Nepřečteno"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/header/Star.tsx
|
||||
msgid "Unstar"
|
||||
msgstr "Odstranit hvězdu"
|
||||
|
||||
|
||||
@@ -33,8 +33,8 @@ msgstr ""
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>Angen cyfrif?</0><1>Ymunwch!</1>"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "About"
|
||||
msgstr "Ynghylch"
|
||||
|
||||
@@ -54,19 +54,19 @@ msgstr "Ychwanegu categori"
|
||||
msgid "Add user"
|
||||
msgstr "Ychwanegu defnyddiwr"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
msgid "Admin"
|
||||
msgstr "Gweinyddol"
|
||||
|
||||
#: src/app/constants.ts
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/Tree.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
msgid "All"
|
||||
msgstr "Pawb"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always"
|
||||
msgstr ""
|
||||
@@ -139,27 +139,31 @@ msgstr ""
|
||||
msgid "Browser extention"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Browser tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
msgid "Cancel"
|
||||
msgstr "Diddymu"
|
||||
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
msgid "Category"
|
||||
msgstr "categori"
|
||||
|
||||
@@ -175,6 +179,10 @@ msgstr "Gwiriwch fod y porthiant yn gweithio"
|
||||
msgid "Close menu"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Cmd"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
@@ -195,11 +203,11 @@ msgstr ""
|
||||
msgid "Compact"
|
||||
msgstr "cryno"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
msgid "Confirm"
|
||||
msgstr "Cadarnhau"
|
||||
|
||||
@@ -264,13 +272,13 @@ msgstr "Rhag"
|
||||
msgid "Detailed"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Display"
|
||||
msgstr "Arddangos"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/app/DonatePage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Donate"
|
||||
msgstr ""
|
||||
|
||||
@@ -282,11 +290,11 @@ msgstr "Lawrlwytho"
|
||||
msgid "Drag link to bookmark bar"
|
||||
msgstr "Llusgwch y ddolen i'r bar nod tudalen"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
msgid "E-mail"
|
||||
msgstr "E-bost"
|
||||
|
||||
@@ -299,8 +307,8 @@ msgstr "cyfeiriad e-bost"
|
||||
msgid "Edit user"
|
||||
msgstr "Golygu defnyddiwr"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
msgid "Enabled"
|
||||
msgstr "Wedi'i alluogi"
|
||||
|
||||
@@ -312,6 +320,14 @@ msgstr "Ewch i mewn"
|
||||
msgid "Enter your current password to change profile settings"
|
||||
msgstr "Rhowch eich cyfrinair presennol i newid gosodiadau proffil"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entries to keep above the selected entry when scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entry headers"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/Alert.tsx
|
||||
msgid "Error"
|
||||
msgstr "Gwall"
|
||||
@@ -328,8 +344,8 @@ msgstr "Ehangu"
|
||||
msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services"
|
||||
msgstr "Allforio eich tanysgrifiadau a'ch categorïau fel ffeil OPML y gellir ei mewnforio i wasanaethau darllen porthiant eraill"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Extension options"
|
||||
msgstr ""
|
||||
|
||||
@@ -337,9 +353,9 @@ msgstr ""
|
||||
msgid "Feed name"
|
||||
msgstr "Enw porthiant"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Feed URL"
|
||||
msgstr "URL porthiant"
|
||||
|
||||
@@ -355,21 +371,21 @@ msgstr ""
|
||||
msgid "Fever API URL"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "file is required"
|
||||
msgstr "mae angen y ffeil"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Filtering expression"
|
||||
msgstr "Hidlo mynegiant"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Force fetching feeds is not yet available."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Forgot password?"
|
||||
msgstr "Wedi anghofio cyfrinair?"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Generate an API key in your profile first."
|
||||
msgstr "Cynhyrchu allwedd API yn eich proffil yn gyntaf."
|
||||
|
||||
@@ -377,12 +393,13 @@ msgstr "Cynhyrchu allwedd API yn eich proffil yn gyntaf."
|
||||
msgid "Generate new API key"
|
||||
msgstr "Cynhyrchu allwedd API newydd"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Generated feed url"
|
||||
msgstr "url porthiant a gynhyrchir"
|
||||
|
||||
#. placeholder {0}: truncate(props.entry.feedName, 30)
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Go to {0}"
|
||||
msgstr ""
|
||||
@@ -423,13 +440,13 @@ msgstr "Mewnforio"
|
||||
msgid "In expanded view, scrolling through entries mark them as read"
|
||||
msgstr "Mewn gwedd estynedig, mae sgrolio trwy gofnodion yn nodi eu bod wedi'u darllen"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Keep unread"
|
||||
msgstr "Cadwch heb ei ddarllen"
|
||||
|
||||
#: src/components/content/FeedEntries.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/content/FeedEntries.tsx
|
||||
msgid "Keyboard shortcuts"
|
||||
msgstr "llwybrau byr bysellfwrdd"
|
||||
|
||||
@@ -453,9 +470,9 @@ msgstr "Neges adnewyddu ddiwethaf"
|
||||
msgid "Light"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Link"
|
||||
msgstr "Cyswllt"
|
||||
|
||||
@@ -475,9 +492,9 @@ msgstr "Yn llwytho tanysgrifiadau..."
|
||||
msgid "Loading tags..."
|
||||
msgstr "Wrthi'n llwytho tagiau..."
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Log in"
|
||||
msgstr "Mewngofnodi"
|
||||
|
||||
@@ -489,8 +506,8 @@ msgstr "Allgofnodi"
|
||||
msgid "Long press"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Manage users"
|
||||
msgstr "Rheoli defnyddwyr"
|
||||
|
||||
@@ -498,18 +515,18 @@ msgstr "Rheoli defnyddwyr"
|
||||
msgid "Mark all as read"
|
||||
msgstr "Marciwch y cyfan wedi'i ddarllen"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
msgid "Mark all entries as read"
|
||||
msgstr "Marciwch bob cofnod wedi'i ddarllen"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Mark as read"
|
||||
msgstr "Marciwch ei fod wedi'i ddarllen"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Mark as read up to here"
|
||||
msgstr "Marciwch fel y darllenwyd hyd yma"
|
||||
|
||||
@@ -529,15 +546,15 @@ msgstr "Symudwch y dudalen i lawr"
|
||||
msgid "Move the page up"
|
||||
msgstr "Symudwch y dudalen i fyny"
|
||||
|
||||
#: src/components/RelativeDate.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/components/RelativeDate.tsx
|
||||
msgid "N/A"
|
||||
msgstr "Amh"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
msgid "Name"
|
||||
msgstr "Enw"
|
||||
|
||||
@@ -545,6 +562,7 @@ msgstr "Enw"
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Llywiwch i danysgrifiad trwy nodi ei enw"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
msgstr ""
|
||||
@@ -557,8 +575,8 @@ msgstr "Cyfrinair newydd"
|
||||
msgid "Newest first"
|
||||
msgstr "Y diweddaraf yn gyntaf"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Next"
|
||||
msgstr "Nesaf"
|
||||
|
||||
@@ -574,6 +592,10 @@ msgstr "Llyfrnod yr eitem nesaf heb ei darllen"
|
||||
msgid "No more entries"
|
||||
msgstr "Dim mwy o gofnodion"
|
||||
|
||||
#: src/components/content/ShareButtons.tsx
|
||||
msgid "No sharing options available."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
msgid "Nothing found"
|
||||
msgstr "Dim wedi'i ddarganfod"
|
||||
@@ -582,10 +604,22 @@ msgstr "Dim wedi'i ddarganfod"
|
||||
msgid "Oldest first"
|
||||
msgstr "Hynaf yn gyntaf"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On desktop"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On mobile"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Only applies to compact, cozy and detailed modes"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Wps!"
|
||||
@@ -603,6 +637,7 @@ msgid "Open current entry in a new tab in the background"
|
||||
msgstr "Agorwch y cofnod cyfredol mewn tab newydd yn y cefndir"
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
#: src/components/content/header/OpenExternalLink.tsx
|
||||
msgid "Open link"
|
||||
msgstr "Dolen agored"
|
||||
|
||||
@@ -643,6 +678,10 @@ msgstr "allforio OPML"
|
||||
msgid "OPML file"
|
||||
msgstr "ffeil OPML"
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "OPML file is required"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Order"
|
||||
msgstr "gorchymyn"
|
||||
@@ -655,11 +694,11 @@ msgstr "rhiant"
|
||||
msgid "Parent Category"
|
||||
msgstr "Categori Rhiant"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "Password"
|
||||
msgstr "cyfrinair"
|
||||
|
||||
@@ -671,8 +710,8 @@ msgstr "Adfer Cyfrinair"
|
||||
msgid "Passwords do not match"
|
||||
msgstr "Nid yw cyfrineiriau yn cyfateb"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Position"
|
||||
msgstr "Swydd"
|
||||
|
||||
@@ -688,8 +727,8 @@ msgstr "Proffil"
|
||||
msgid "Recover password"
|
||||
msgstr "Adfer cyfrinair"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Refresh"
|
||||
msgstr "Adnewyddu"
|
||||
|
||||
@@ -706,11 +745,11 @@ msgstr ""
|
||||
msgid "Right click"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
msgid "Save"
|
||||
msgstr "Arbed"
|
||||
|
||||
@@ -726,10 +765,10 @@ msgstr "Sgroliwch yn esmwyth wrth lywio rhwng cofnodion"
|
||||
msgid "Scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Search"
|
||||
msgstr "Chwilio"
|
||||
|
||||
@@ -737,6 +776,14 @@ msgstr "Chwilio"
|
||||
msgid "Search requires at least 3 characters"
|
||||
msgstr "Mae angen o leiaf 3 nod ar gyfer chwilio"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Select next unread feed/category"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Select previous unread feed/category"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Set focus on next entry without opening it"
|
||||
msgstr "Gosodwch ffocws ar y cofnod nesaf heb ei agor"
|
||||
@@ -783,6 +830,10 @@ msgstr ""
|
||||
msgid "Show entry menu (mobile)"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show external link icon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show feeds and categories with no unread entries"
|
||||
msgstr "Dangos ffrydiau a chategorïau heb unrhyw gofnodion heb eu darllen"
|
||||
@@ -795,9 +846,21 @@ msgstr "Dangos cymorth llwybr byr bysellfwrdd"
|
||||
msgid "Show native menu (desktop)"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show star icon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab favicon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab title"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/WelcomePage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "Sign up"
|
||||
msgstr "Cofrestrwch"
|
||||
|
||||
@@ -810,19 +873,20 @@ msgstr "Mae rhywbeth drwg newydd ddigwydd ..."
|
||||
msgid "Space"
|
||||
msgstr "Gofod"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/header/Star.tsx
|
||||
msgid "Star"
|
||||
msgstr "seren"
|
||||
|
||||
#: src/app/constants.ts
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/components/sidebar/Tree.tsx
|
||||
msgid "Starred"
|
||||
msgstr "serennog"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/pages/app/AddPage.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Subscribe"
|
||||
msgstr "Tanysgrifio"
|
||||
|
||||
@@ -895,8 +959,9 @@ msgstr ""
|
||||
msgid "Unread"
|
||||
msgstr "Heb ei ddarllen"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/header/Star.tsx
|
||||
msgid "Unstar"
|
||||
msgstr "dad-seren"
|
||||
|
||||
|
||||
@@ -33,8 +33,8 @@ msgstr ""
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>Har du brug for en konto?</0><1>Tilmeld dig!</1>"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "About"
|
||||
msgstr "Omkring"
|
||||
|
||||
@@ -54,19 +54,19 @@ msgstr "Tilføj kategori"
|
||||
msgid "Add user"
|
||||
msgstr "Tilføj bruger"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
msgid "Admin"
|
||||
msgstr ""
|
||||
|
||||
#: src/app/constants.ts
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/Tree.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
msgid "All"
|
||||
msgstr "Alle"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always"
|
||||
msgstr ""
|
||||
@@ -139,27 +139,31 @@ msgstr ""
|
||||
msgid "Browser extention"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Browser tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
msgid "Cancel"
|
||||
msgstr "Annuller"
|
||||
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
msgid "Category"
|
||||
msgstr "Kategori"
|
||||
|
||||
@@ -175,6 +179,10 @@ msgstr "Tjek, at foderet virker"
|
||||
msgid "Close menu"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Cmd"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
@@ -195,11 +203,11 @@ msgstr ""
|
||||
msgid "Compact"
|
||||
msgstr "Kompakt"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
msgid "Confirm"
|
||||
msgstr "Bekræft"
|
||||
|
||||
@@ -264,13 +272,13 @@ msgstr ""
|
||||
msgid "Detailed"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Display"
|
||||
msgstr "Skærm"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/app/DonatePage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Donate"
|
||||
msgstr ""
|
||||
|
||||
@@ -282,11 +290,11 @@ msgstr ""
|
||||
msgid "Drag link to bookmark bar"
|
||||
msgstr "Træk linket til bogmærkelinjen"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
msgid "E-mail"
|
||||
msgstr ""
|
||||
|
||||
@@ -299,8 +307,8 @@ msgstr "E-mailadresse"
|
||||
msgid "Edit user"
|
||||
msgstr "Rediger bruger"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
msgid "Enabled"
|
||||
msgstr "Aktiveret"
|
||||
|
||||
@@ -312,6 +320,14 @@ msgstr ""
|
||||
msgid "Enter your current password to change profile settings"
|
||||
msgstr "Indtast din nuværende adgangskode for at ændre profilindstillinger"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entries to keep above the selected entry when scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entry headers"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/Alert.tsx
|
||||
msgid "Error"
|
||||
msgstr "Fejl"
|
||||
@@ -328,8 +344,8 @@ msgstr "Udvidet"
|
||||
msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services"
|
||||
msgstr "Eksporter dine abonnementer og kategorier som en OPML-fil, der kan importeres i andre feed-læsningstjenester"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Extension options"
|
||||
msgstr ""
|
||||
|
||||
@@ -337,9 +353,9 @@ msgstr ""
|
||||
msgid "Feed name"
|
||||
msgstr "Feednavn"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Feed URL"
|
||||
msgstr ""
|
||||
|
||||
@@ -355,21 +371,21 @@ msgstr ""
|
||||
msgid "Fever API URL"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "file is required"
|
||||
msgstr "fil er påkrævet"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Filtering expression"
|
||||
msgstr "Filtrerende udtryk"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Force fetching feeds is not yet available."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Forgot password?"
|
||||
msgstr "Glemt adgangskode?"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Generate an API key in your profile first."
|
||||
msgstr "Generer først en API-nøgle i din profil."
|
||||
|
||||
@@ -377,12 +393,13 @@ msgstr "Generer først en API-nøgle i din profil."
|
||||
msgid "Generate new API key"
|
||||
msgstr "Generer ny API-nøgle"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Generated feed url"
|
||||
msgstr "Genereret feed-url"
|
||||
|
||||
#. placeholder {0}: truncate(props.entry.feedName, 30)
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Go to {0}"
|
||||
msgstr ""
|
||||
@@ -423,13 +440,13 @@ msgstr ""
|
||||
msgid "In expanded view, scrolling through entries mark them as read"
|
||||
msgstr "I udvidet visning markerer du dem som læst, når du ruller gennem poster"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Keep unread"
|
||||
msgstr "Forbehold ulæst"
|
||||
|
||||
#: src/components/content/FeedEntries.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/content/FeedEntries.tsx
|
||||
msgid "Keyboard shortcuts"
|
||||
msgstr "Tastaturgenveje"
|
||||
|
||||
@@ -453,9 +470,9 @@ msgstr "Sidste opdateringsmeddelelse"
|
||||
msgid "Light"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Link"
|
||||
msgstr ""
|
||||
|
||||
@@ -475,9 +492,9 @@ msgstr "Indlæser abonnementer..."
|
||||
msgid "Loading tags..."
|
||||
msgstr "Indlæser tags..."
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Log in"
|
||||
msgstr "Log ind"
|
||||
|
||||
@@ -489,8 +506,8 @@ msgstr "Log ud"
|
||||
msgid "Long press"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Manage users"
|
||||
msgstr "Administrer brugere"
|
||||
|
||||
@@ -498,18 +515,18 @@ msgstr "Administrer brugere"
|
||||
msgid "Mark all as read"
|
||||
msgstr "Marker alle som læst"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
msgid "Mark all entries as read"
|
||||
msgstr "Marker alle poster som læst"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Mark as read"
|
||||
msgstr "Markér som læst"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Mark as read up to here"
|
||||
msgstr "Markér som læst indtil her"
|
||||
|
||||
@@ -529,15 +546,15 @@ msgstr "Flyt siden ned"
|
||||
msgid "Move the page up"
|
||||
msgstr "Flyt siden op"
|
||||
|
||||
#: src/components/RelativeDate.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/components/RelativeDate.tsx
|
||||
msgid "N/A"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
msgid "Name"
|
||||
msgstr "Navn"
|
||||
|
||||
@@ -545,6 +562,7 @@ 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
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
msgstr ""
|
||||
@@ -557,8 +575,8 @@ msgstr "Ny adgangskode"
|
||||
msgid "Newest first"
|
||||
msgstr "Nyeste først"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Next"
|
||||
msgstr "Næste"
|
||||
|
||||
@@ -574,6 +592,10 @@ msgstr "Næste ulæste emne bogmærke"
|
||||
msgid "No more entries"
|
||||
msgstr "Ingen flere poster"
|
||||
|
||||
#: src/components/content/ShareButtons.tsx
|
||||
msgid "No sharing options available."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
msgid "Nothing found"
|
||||
msgstr "Intet fundet"
|
||||
@@ -582,10 +604,22 @@ msgstr "Intet fundet"
|
||||
msgid "Oldest first"
|
||||
msgstr "Ældst først"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On desktop"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On mobile"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Only applies to compact, cozy and detailed modes"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Hovsa!"
|
||||
@@ -603,6 +637,7 @@ msgid "Open current entry in a new tab in the background"
|
||||
msgstr "Åbn den aktuelle post i en ny fane i baggrunden"
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
#: src/components/content/header/OpenExternalLink.tsx
|
||||
msgid "Open link"
|
||||
msgstr "Åbent link"
|
||||
|
||||
@@ -643,6 +678,10 @@ msgstr "OPML eksport"
|
||||
msgid "OPML file"
|
||||
msgstr "OPML fil"
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "OPML file is required"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Order"
|
||||
msgstr "Bestilling"
|
||||
@@ -655,11 +694,11 @@ msgstr "Forælder"
|
||||
msgid "Parent Category"
|
||||
msgstr "Forældrekategori"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "Password"
|
||||
msgstr "Adgangskode"
|
||||
|
||||
@@ -671,8 +710,8 @@ msgstr "Gendannelse af adgangskode"
|
||||
msgid "Passwords do not match"
|
||||
msgstr "Adgangskoder stemmer ikke overens"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Position"
|
||||
msgstr ""
|
||||
|
||||
@@ -688,8 +727,8 @@ msgstr "Profil"
|
||||
msgid "Recover password"
|
||||
msgstr "Gendan adgangskode"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Refresh"
|
||||
msgstr "Opdater"
|
||||
|
||||
@@ -706,11 +745,11 @@ msgstr ""
|
||||
msgid "Right click"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
msgid "Save"
|
||||
msgstr "Gem"
|
||||
|
||||
@@ -726,10 +765,10 @@ msgstr "Rul jævnt, når du navigerer mellem poster"
|
||||
msgid "Scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Search"
|
||||
msgstr "Søg"
|
||||
|
||||
@@ -737,6 +776,14 @@ msgstr "Søg"
|
||||
msgid "Search requires at least 3 characters"
|
||||
msgstr "Søgning kræver mindst 3 tegn"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Select next unread feed/category"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Select previous unread feed/category"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Set focus on next entry without opening it"
|
||||
msgstr "Sæt fokus på næste post uden at åbne den"
|
||||
@@ -783,6 +830,10 @@ msgstr ""
|
||||
msgid "Show entry menu (mobile)"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show external link icon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show feeds and categories with no unread entries"
|
||||
msgstr "Vis feeds og kategorier uden ulæste poster"
|
||||
@@ -795,9 +846,21 @@ msgstr "Vis hjælp til tastaturgenveje"
|
||||
msgid "Show native menu (desktop)"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show star icon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab favicon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab title"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/WelcomePage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "Sign up"
|
||||
msgstr "Tilmeld dig"
|
||||
|
||||
@@ -810,19 +873,20 @@ msgstr "Der er lige sket noget slemt..."
|
||||
msgid "Space"
|
||||
msgstr "Rum"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/header/Star.tsx
|
||||
msgid "Star"
|
||||
msgstr "Stjerne"
|
||||
|
||||
#: src/app/constants.ts
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/components/sidebar/Tree.tsx
|
||||
msgid "Starred"
|
||||
msgstr "Medvirkende"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/pages/app/AddPage.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Subscribe"
|
||||
msgstr "Tilmeld"
|
||||
|
||||
@@ -895,8 +959,9 @@ msgstr ""
|
||||
msgid "Unread"
|
||||
msgstr "Ulæst"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/header/Star.tsx
|
||||
msgid "Unstar"
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -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 "CommaFeed ist ein Open Source Projekt. Der Quellcode wird auf auf </0><1>GitHub</1> gehostet."
|
||||
msgstr "<0>CommaFeed ist ein Open Source Projekt. Der Quellcode wird auf auf </0><1>GitHub</1> gehostet."
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1>."
|
||||
msgstr "Vollständiger Syntax ist </0><1>hier</1> verfügbar."
|
||||
msgstr "<0>Die vollständige Syntax ist </0><1>hier</1> verfügbar."
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "<0>Have an account?</0><1>Log in!</1>"
|
||||
@@ -27,14 +27,14 @@ msgstr "<0>Haben Sie ein Konto?</0><1>Melden Sie sich an!</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>Hey,</0><1>Ich bin Jérémie aus Belgien und arbeite seit über 10 Jahren in meiner Freizeit an CommaFeed. Vielen Dank für das Interesse, CommaFeed weiterhin zu unterstützen.</1>"
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>Benötigen Sie ein Konto?</0><1>Hier geht's zur Registrierung!</1>"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "About"
|
||||
msgstr "Über"
|
||||
|
||||
@@ -54,26 +54,26 @@ msgstr "Kategorie hinzufügen"
|
||||
msgid "Add user"
|
||||
msgstr "Benutzer hinzufügen"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
msgid "Admin"
|
||||
msgstr "Verwaltung"
|
||||
|
||||
#: src/app/constants.ts
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/Tree.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
msgid "All"
|
||||
msgstr "Alle"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always"
|
||||
msgstr ""
|
||||
msgstr "Immer"
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "An email has been sent if this address was registered. Check your inbox."
|
||||
msgstr "Eine E-Mail wurde gesendet, wenn diese Adresse registriert wurde. Bitte den Posteingang prüfen."
|
||||
msgstr "Falls diese Adresse registriert ist, wurde eine E-Mail gesendet. Bitte den Posteingang prüfen."
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "An opml file is an XML file containing feed URLs and categories. You can get an OPML file by exporting your data from other feed reading services."
|
||||
@@ -139,27 +139,31 @@ msgstr "Browser-Erweiterung für Chrome benötigt"
|
||||
msgid "Browser extention"
|
||||
msgstr "Browser-Erweiterung"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Browser tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
msgid "Cancel"
|
||||
msgstr "Abbrechen"
|
||||
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
msgid "Category"
|
||||
msgstr "Kategorie"
|
||||
|
||||
@@ -169,19 +173,23 @@ msgstr "Das Ändern des Passworts generiert einen neuen API-Schlüssel"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Check that the feed is working"
|
||||
msgstr "Überprüfen Sie, ob der Feed funktioniert"
|
||||
msgstr "Überprüfe, ob der Feed funktioniert"
|
||||
|
||||
#: src/pages/app/Layout.tsx
|
||||
msgid "Close menu"
|
||||
msgstr "Menü schließen"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Cmd"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr ""
|
||||
msgstr "CommaFeed Browser Erweiterung Version {browserExtensionVersion}."
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "CommaFeed is compatible with the Fever API. Use the following URL in your Fever-compatible mobile client. Login with your username and your <0>API key</0>."
|
||||
msgstr ""
|
||||
msgstr "CommaFeed ist mit der Fever-API kompatibel. Verwenden Sie die folgende URL in Ihrem Fever-kompatiblen mobilen Client. Melden Sie sich mit Ihrem Benutzernamen und Ihrem <0>API-Schlüssel</0> an."
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed next unread item"
|
||||
@@ -189,17 +197,17 @@ msgstr "CommaFeed nächstes ungelesenes Element"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed version {version} ({revision})."
|
||||
msgstr ""
|
||||
msgstr "CommaFeed version {version} ({revision})."
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Compact"
|
||||
msgstr "Kompakt"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
msgid "Confirm"
|
||||
msgstr "Bestätigen"
|
||||
|
||||
@@ -221,19 +229,19 @@ msgstr "Aktuelles Passwort"
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Custom code"
|
||||
msgstr ""
|
||||
msgstr "Eigener Code"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "Custom CSS rules that will be applied"
|
||||
msgstr ""
|
||||
msgstr "Eigene CSS Regeln die angewandt werden"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "Custom JS code that will be executed on page load"
|
||||
msgstr ""
|
||||
msgstr "Einer JS Code der beim Laden der Seite ausgeführt wird"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Dark"
|
||||
msgstr ""
|
||||
msgstr "Dunkel"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Date created"
|
||||
@@ -262,15 +270,15 @@ msgstr "Beschr"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Detailed"
|
||||
msgstr ""
|
||||
msgstr "Detailliert"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Display"
|
||||
msgstr "Anzeige"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/app/DonatePage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Donate"
|
||||
msgstr "Spenden"
|
||||
|
||||
@@ -282,11 +290,11 @@ msgstr "Herunterladen"
|
||||
msgid "Drag link to bookmark bar"
|
||||
msgstr "Link in Lesezeichenleiste ziehen"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
msgid "E-mail"
|
||||
msgstr "E-Mail"
|
||||
|
||||
@@ -299,8 +307,8 @@ msgstr "E-Mail-Adresse"
|
||||
msgid "Edit user"
|
||||
msgstr "Benutzer bearbeiten"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
msgid "Enabled"
|
||||
msgstr "Aktiviert"
|
||||
|
||||
@@ -312,13 +320,21 @@ msgstr "Eintreten"
|
||||
msgid "Enter your current password to change profile settings"
|
||||
msgstr "Geben Sie Ihr aktuelles Passwort ein, um die Profileinstellungen zu ändern"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entries to keep above the selected entry when scrolling"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entry headers"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/Alert.tsx
|
||||
msgid "Error"
|
||||
msgstr "Fehler"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Example: {example}."
|
||||
msgstr "Beispiel: {Beispiel}."
|
||||
msgstr "Beispiel: {example}."
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Expanded"
|
||||
@@ -328,8 +344,8 @@ msgstr "Erweitert"
|
||||
msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services"
|
||||
msgstr "Exportieren Sie Ihre Abonnements und Kategorien als OPML-Datei, die in andere Feed-Lesedienste importiert werden kann"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Extension options"
|
||||
msgstr "Erweiterungsoptionen"
|
||||
|
||||
@@ -337,9 +353,9 @@ msgstr "Erweiterungsoptionen"
|
||||
msgid "Feed name"
|
||||
msgstr "Feedname"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Feed URL"
|
||||
msgstr "Feed-URL"
|
||||
|
||||
@@ -355,21 +371,21 @@ msgstr ""
|
||||
msgid "Fever API URL"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "file is required"
|
||||
msgstr "Datei ist erforderlich"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Filtering expression"
|
||||
msgstr "Filterausdruck"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Force fetching feeds is not yet available."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Forgot password?"
|
||||
msgstr "Passwort vergessen?"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Generate an API key in your profile first."
|
||||
msgstr "Generieren Sie zuerst einen API-Schlüssel in Ihrem Profil."
|
||||
|
||||
@@ -377,12 +393,13 @@ msgstr "Generieren Sie zuerst einen API-Schlüssel in Ihrem Profil."
|
||||
msgid "Generate new API key"
|
||||
msgstr "Neuen API-Schlüssel generieren"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Generated feed url"
|
||||
msgstr "Generierte Feed-URL"
|
||||
|
||||
#. placeholder {0}: truncate(props.entry.feedName, 30)
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Go to {0}"
|
||||
msgstr "Gehe zu {0}"
|
||||
@@ -405,11 +422,11 @@ msgstr ""
|
||||
|
||||
#: 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 "Wenn nicht leer, ein Ausdruck, der als „wahr“ oder „falsch“ ausgewertet wird. "
|
||||
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 ""
|
||||
msgstr "Wenn der Eintrag nicht ganz auf den Bildschirm passt"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "If you encounter an issue, please report it on the issues page of the GitHub project."
|
||||
@@ -423,13 +440,13 @@ msgstr "Importieren"
|
||||
msgid "In expanded view, scrolling through entries mark them as read"
|
||||
msgstr "In der erweiterten Ansicht werden Einträge beim Scrollen als gelesen markiert"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Keep unread"
|
||||
msgstr "Ungelesen lassen"
|
||||
|
||||
#: src/components/content/FeedEntries.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/content/FeedEntries.tsx
|
||||
msgid "Keyboard shortcuts"
|
||||
msgstr "Tastaturkürzel"
|
||||
|
||||
@@ -451,11 +468,11 @@ msgstr "Letzte Aktualisierungsmeldung"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Light"
|
||||
msgstr ""
|
||||
msgstr "Hell"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Link"
|
||||
msgstr "Verbindung"
|
||||
|
||||
@@ -475,9 +492,9 @@ msgstr "Abonnements werden geladen..."
|
||||
msgid "Loading tags..."
|
||||
msgstr "Tags werden geladen..."
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Log in"
|
||||
msgstr "Einloggen"
|
||||
|
||||
@@ -489,8 +506,8 @@ msgstr "Abmelden"
|
||||
msgid "Long press"
|
||||
msgstr "Langer Tastendruck"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Manage users"
|
||||
msgstr "Benutzer verwalten"
|
||||
|
||||
@@ -498,18 +515,18 @@ msgstr "Benutzer verwalten"
|
||||
msgid "Mark all as read"
|
||||
msgstr "Alle als gelesen markieren"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
msgid "Mark all entries as read"
|
||||
msgstr "Alle Einträge als gelesen markieren"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Mark as read"
|
||||
msgstr "Als gelesen markieren"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Mark as read up to here"
|
||||
msgstr "Bis hierhin als gelesen markieren"
|
||||
|
||||
@@ -529,15 +546,15 @@ msgstr "Seite nach unten verschieben"
|
||||
msgid "Move the page up"
|
||||
msgstr "Bewege die Seite nach oben"
|
||||
|
||||
#: src/components/RelativeDate.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/components/RelativeDate.tsx
|
||||
msgid "N/A"
|
||||
msgstr "n.v."
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
msgid "Name"
|
||||
msgstr ""
|
||||
|
||||
@@ -545,9 +562,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
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
msgstr ""
|
||||
msgstr "Niemals"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "New password"
|
||||
@@ -557,8 +575,8 @@ msgstr "Neues Passwort"
|
||||
msgid "Newest first"
|
||||
msgstr "Neueste zuerst"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Next"
|
||||
msgstr "Weiter"
|
||||
|
||||
@@ -574,6 +592,10 @@ msgstr "Lesezeichen für das nächste ungelesene Element"
|
||||
msgid "No more entries"
|
||||
msgstr "Keine weiteren Einträge"
|
||||
|
||||
#: src/components/content/ShareButtons.tsx
|
||||
msgid "No sharing options available."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
msgid "Nothing found"
|
||||
msgstr "Nichts gefunden"
|
||||
@@ -582,8 +604,20 @@ msgstr "Nichts gefunden"
|
||||
msgid "Oldest first"
|
||||
msgstr "Älteste zuerst"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On desktop"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On mobile"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr "Auf mobilen Geräten die Aktion-Buttons am unteren Ende des Bildschirms anzeigen"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Only applies to compact, cozy and detailed modes"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
@@ -592,7 +626,7 @@ msgstr "Ups!"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Open CommaFeed"
|
||||
msgstr ""
|
||||
msgstr "CommaFeed öffnen"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open current entry in a new tab"
|
||||
@@ -603,6 +637,7 @@ msgid "Open current entry in a new tab in the background"
|
||||
msgstr "Aktuellen Eintrag in neuem Tab im Hintergrund öffnen"
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
#: src/components/content/header/OpenExternalLink.tsx
|
||||
msgid "Open link"
|
||||
msgstr "Link öffnen"
|
||||
|
||||
@@ -616,7 +651,7 @@ msgstr "Link in neuem Tab öffnen"
|
||||
|
||||
#: src/pages/app/Layout.tsx
|
||||
msgid "Open menu"
|
||||
msgstr ""
|
||||
msgstr "Menü öffnen"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open next entry"
|
||||
@@ -643,6 +678,10 @@ msgstr "OPML-Export"
|
||||
msgid "OPML file"
|
||||
msgstr "OPML-Datei"
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "OPML file is required"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Order"
|
||||
msgstr "Bestellung"
|
||||
@@ -655,11 +694,11 @@ msgstr "Übergeordnet"
|
||||
msgid "Parent Category"
|
||||
msgstr "Übergeordnete Kategorie"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "Password"
|
||||
msgstr "Passwort"
|
||||
|
||||
@@ -671,8 +710,8 @@ msgstr "Passwortwiederherstellung"
|
||||
msgid "Passwords do not match"
|
||||
msgstr "Passwörter stimmen nicht überein"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Position"
|
||||
msgstr "Position"
|
||||
|
||||
@@ -688,8 +727,8 @@ msgstr "Profil"
|
||||
msgid "Recover password"
|
||||
msgstr "Kennwort wiederherstellen"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Refresh"
|
||||
msgstr "Aktualisieren"
|
||||
|
||||
@@ -706,17 +745,17 @@ msgstr "REST-API"
|
||||
msgid "Right click"
|
||||
msgstr "Rechtsklick"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
msgid "Save"
|
||||
msgstr "Speichern"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll selected entry to the top of the page"
|
||||
msgstr ""
|
||||
msgstr "Ausgewählten Eintrag an den Anfang der Seite verschieben"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll smoothly when navigating between entries"
|
||||
@@ -724,12 +763,12 @@ msgstr "Schnelles Scrollen beim Navigieren zwischen Einträgen"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scrolling"
|
||||
msgstr ""
|
||||
msgstr "Scrollen"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Search"
|
||||
msgstr "Suche"
|
||||
|
||||
@@ -737,6 +776,14 @@ msgstr "Suche"
|
||||
msgid "Search requires at least 3 characters"
|
||||
msgstr "Suche erfordert mindestens 3 Zeichen"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Select next unread feed/category"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Select previous unread feed/category"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Set focus on next entry without opening it"
|
||||
msgstr "Fokus auf den nächsten Eintrag setzen, ohne ihn zu öffnen"
|
||||
@@ -777,10 +824,14 @@ msgstr "Bestätigung beim Markieren von allen Einträgen als gelesen"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show entry menu (desktop)"
|
||||
msgstr ""
|
||||
msgstr "Eintragsmenü anzeigen (Desktop)"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show entry menu (mobile)"
|
||||
msgstr "Eintragsmenü anzeigen (Handy)"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show external link icon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
@@ -793,11 +844,23 @@ msgstr "Tastenkürzel-Hilfe anzeigen"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show native menu (desktop)"
|
||||
msgstr "Natives Menü anzeigen (Desktop)"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show star icon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab favicon"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab title"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "Sign up"
|
||||
msgstr "Melden Sie sich an"
|
||||
|
||||
@@ -810,19 +873,20 @@ msgstr "Etwas Schlimmes ist gerade passiert..."
|
||||
msgid "Space"
|
||||
msgstr "Raum"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/header/Star.tsx
|
||||
msgid "Star"
|
||||
msgstr "Stern"
|
||||
|
||||
#: src/app/constants.ts
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/components/sidebar/Tree.tsx
|
||||
msgid "Starred"
|
||||
msgstr "Markiert"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/pages/app/AddPage.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Subscribe"
|
||||
msgstr "Abonnieren"
|
||||
|
||||
@@ -840,7 +904,7 @@ msgstr "Erfolg"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Swipe header to the left"
|
||||
msgstr ""
|
||||
msgstr "Kopfzeile nach links schieben"
|
||||
|
||||
#: src/pages/WelcomePage.tsx
|
||||
msgid "Switch to dark theme"
|
||||
@@ -869,7 +933,7 @@ msgstr "Thema"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "This is your API key. It can be used for some read-only API operations and grants access to the Fever API. Use the form at the bottom of the page to generate a new API key"
|
||||
msgstr ""
|
||||
msgstr "Dies ist Ihr API-Schlüssel. Er kann für einige schreibgeschützte API-Vorgänge verwendet werden und ermöglicht den Zugriff auf die Fever-API. Verwenden Sie das Formular unten auf der Seite, um einen neuen API-Schlüssel zu generieren"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle read status of current entry"
|
||||
@@ -895,8 +959,9 @@ msgstr "Testen Sie die Demo!"
|
||||
msgid "Unread"
|
||||
msgstr "Ungelesen"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/header/Star.tsx
|
||||
msgid "Unstar"
|
||||
msgstr "Stern entfernen"
|
||||
|
||||
@@ -924,7 +989,7 @@ msgstr "Webseite"
|
||||
|
||||
#: src/pages/app/FeedEntriesPage.tsx
|
||||
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
||||
msgstr "Sie haben noch keine Abonnements. "
|
||||
msgstr "Sie haben noch keine Abonnements."
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Your feeds have been queued for refresh."
|
||||
|
||||
@@ -33,8 +33,8 @@ msgstr "<0>Hey,</0><1>I'm Jérémie from Belgium and I've been working on CommaF
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>Need an account?</0><1>Sign up!</1>"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "About"
|
||||
msgstr "About"
|
||||
|
||||
@@ -54,19 +54,19 @@ msgstr "Add category"
|
||||
msgid "Add user"
|
||||
msgstr "Add user"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
msgid "Admin"
|
||||
msgstr "Admin"
|
||||
|
||||
#: src/app/constants.ts
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/Tree.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
msgid "All"
|
||||
msgstr "All"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Always"
|
||||
msgstr "Always"
|
||||
@@ -139,27 +139,31 @@ msgstr "Browser extension required for Chrome"
|
||||
msgid "Browser extention"
|
||||
msgstr "Browser extention"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Browser tab"
|
||||
msgstr "Browser tab"
|
||||
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
msgid "Cancel"
|
||||
msgstr "Cancel"
|
||||
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
msgid "Category"
|
||||
msgstr "Category"
|
||||
|
||||
@@ -175,6 +179,10 @@ msgstr "Check that the feed is working"
|
||||
msgid "Close menu"
|
||||
msgstr "Close menu"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Cmd"
|
||||
msgstr "Cmd"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
msgstr "CommaFeed browser extension version {browserExtensionVersion}."
|
||||
@@ -195,11 +203,11 @@ msgstr "CommaFeed version {version} ({revision})."
|
||||
msgid "Compact"
|
||||
msgstr "Compact"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
msgid "Confirm"
|
||||
msgstr "Confirm"
|
||||
|
||||
@@ -264,13 +272,13 @@ msgstr "Desc"
|
||||
msgid "Detailed"
|
||||
msgstr "Detailed"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Display"
|
||||
msgstr "Display"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/app/DonatePage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Donate"
|
||||
msgstr "Donate"
|
||||
|
||||
@@ -282,11 +290,11 @@ msgstr "Download"
|
||||
msgid "Drag link to bookmark bar"
|
||||
msgstr "Drag link to bookmark bar"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
msgid "E-mail"
|
||||
msgstr "E-mail"
|
||||
|
||||
@@ -299,8 +307,8 @@ msgstr "E-mail address"
|
||||
msgid "Edit user"
|
||||
msgstr "Edit user"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
msgid "Enabled"
|
||||
msgstr "Enabled"
|
||||
|
||||
@@ -312,6 +320,14 @@ msgstr "Enter"
|
||||
msgid "Enter your current password to change profile settings"
|
||||
msgstr "Enter your current password to change profile settings"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entries to keep above the selected entry when scrolling"
|
||||
msgstr "Entries to keep above the selected entry when scrolling"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Entry headers"
|
||||
msgstr "Entry headers"
|
||||
|
||||
#: src/components/Alert.tsx
|
||||
msgid "Error"
|
||||
msgstr "Error"
|
||||
@@ -328,8 +344,8 @@ msgstr "Expanded"
|
||||
msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services"
|
||||
msgstr "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Extension options"
|
||||
msgstr "Extension options"
|
||||
|
||||
@@ -337,9 +353,9 @@ msgstr "Extension options"
|
||||
msgid "Feed name"
|
||||
msgstr "Feed name"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Feed URL"
|
||||
msgstr "Feed URL"
|
||||
|
||||
@@ -355,21 +371,21 @@ msgstr "Fever API"
|
||||
msgid "Fever API URL"
|
||||
msgstr "Fever API URL"
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "file is required"
|
||||
msgstr "file is required"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Filtering expression"
|
||||
msgstr "Filtering expression"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Force fetching feeds is not yet available."
|
||||
msgstr "Force fetching feeds is not yet available."
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Forgot password?"
|
||||
msgstr "Forgot password?"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Generate an API key in your profile first."
|
||||
msgstr "Generate an API key in your profile first."
|
||||
|
||||
@@ -377,12 +393,13 @@ msgstr "Generate an API key in your profile first."
|
||||
msgid "Generate new API key"
|
||||
msgstr "Generate new API key"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Generated feed url"
|
||||
msgstr "Generated feed url"
|
||||
|
||||
#. placeholder {0}: truncate(props.entry.feedName, 30)
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Go to {0}"
|
||||
msgstr "Go to {0}"
|
||||
@@ -423,13 +440,13 @@ msgstr "Import"
|
||||
msgid "In expanded view, scrolling through entries mark them as read"
|
||||
msgstr "In expanded view, scrolling through entries mark them as read"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Keep unread"
|
||||
msgstr "Keep unread"
|
||||
|
||||
#: src/components/content/FeedEntries.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/components/content/FeedEntries.tsx
|
||||
msgid "Keyboard shortcuts"
|
||||
msgstr "Keyboard shortcuts"
|
||||
|
||||
@@ -453,9 +470,9 @@ msgstr "Last refresh message"
|
||||
msgid "Light"
|
||||
msgstr "Light"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Link"
|
||||
msgstr "Link"
|
||||
|
||||
@@ -475,9 +492,9 @@ msgstr "Loading subscriptions..."
|
||||
msgid "Loading tags..."
|
||||
msgstr "Loading tags..."
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Log in"
|
||||
msgstr "Log in"
|
||||
|
||||
@@ -489,8 +506,8 @@ msgstr "Logout"
|
||||
msgid "Long press"
|
||||
msgstr "Long press"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Manage users"
|
||||
msgstr "Manage users"
|
||||
|
||||
@@ -498,18 +515,18 @@ msgstr "Manage users"
|
||||
msgid "Mark all as read"
|
||||
msgstr "Mark all as read"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
msgid "Mark all entries as read"
|
||||
msgstr "Mark all entries as read"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Mark as read"
|
||||
msgstr "Mark as read"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Mark as read up to here"
|
||||
msgstr "Mark as read up to here"
|
||||
|
||||
@@ -529,15 +546,15 @@ msgstr "Move the page down"
|
||||
msgid "Move the page up"
|
||||
msgstr "Move the page up"
|
||||
|
||||
#: src/components/RelativeDate.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/components/RelativeDate.tsx
|
||||
msgid "N/A"
|
||||
msgstr "N/A"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
msgid "Name"
|
||||
msgstr "Name"
|
||||
|
||||
@@ -545,6 +562,7 @@ 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
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Never"
|
||||
msgstr "Never"
|
||||
@@ -557,8 +575,8 @@ msgstr "New password"
|
||||
msgid "Newest first"
|
||||
msgstr "Newest first"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Next"
|
||||
msgstr "Next"
|
||||
|
||||
@@ -574,6 +592,10 @@ msgstr "Next unread item bookmarklet"
|
||||
msgid "No more entries"
|
||||
msgstr "No more entries"
|
||||
|
||||
#: src/components/content/ShareButtons.tsx
|
||||
msgid "No sharing options available."
|
||||
msgstr "No sharing options available."
|
||||
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
msgid "Nothing found"
|
||||
msgstr "Nothing found"
|
||||
@@ -582,10 +604,22 @@ msgstr "Nothing found"
|
||||
msgid "Oldest first"
|
||||
msgstr "Oldest first"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On desktop"
|
||||
msgstr "On desktop"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On mobile"
|
||||
msgstr "On mobile"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "On mobile, show action buttons at the bottom of the screen"
|
||||
msgstr "On mobile, show action buttons at the bottom of the screen"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Only applies to compact, cozy and detailed modes"
|
||||
msgstr "Only applies to compact, cozy and detailed modes"
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Oops!"
|
||||
@@ -603,6 +637,7 @@ msgid "Open current entry in a new tab in the background"
|
||||
msgstr "Open current entry in a new tab in the background"
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
#: src/components/content/header/OpenExternalLink.tsx
|
||||
msgid "Open link"
|
||||
msgstr "Open link"
|
||||
|
||||
@@ -643,6 +678,10 @@ msgstr "OPML export"
|
||||
msgid "OPML file"
|
||||
msgstr "OPML file"
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "OPML file is required"
|
||||
msgstr "OPML file is required"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Order"
|
||||
msgstr "Order"
|
||||
@@ -655,11 +694,11 @@ msgstr "Parent"
|
||||
msgid "Parent Category"
|
||||
msgstr "Parent Category"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "Password"
|
||||
msgstr "Password"
|
||||
|
||||
@@ -671,8 +710,8 @@ msgstr "Password Recovery"
|
||||
msgid "Passwords do not match"
|
||||
msgstr "Passwords do not match"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Position"
|
||||
msgstr "Position"
|
||||
|
||||
@@ -688,8 +727,8 @@ msgstr "Profile"
|
||||
msgid "Recover password"
|
||||
msgstr "Recover password"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Refresh"
|
||||
msgstr "Refresh"
|
||||
|
||||
@@ -706,11 +745,11 @@ msgstr "REST API"
|
||||
msgid "Right click"
|
||||
msgstr "Right click"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
msgid "Save"
|
||||
msgstr "Save"
|
||||
|
||||
@@ -726,10 +765,10 @@ msgstr "Scroll smoothly when navigating between entries"
|
||||
msgid "Scrolling"
|
||||
msgstr "Scrolling"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Search"
|
||||
msgstr "Search"
|
||||
|
||||
@@ -737,6 +776,14 @@ msgstr "Search"
|
||||
msgid "Search requires at least 3 characters"
|
||||
msgstr "Search requires at least 3 characters"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Select next unread feed/category"
|
||||
msgstr "Select next unread feed/category"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Select previous unread feed/category"
|
||||
msgstr "Select previous unread feed/category"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Set focus on next entry without opening it"
|
||||
msgstr "Set focus on next entry without opening it"
|
||||
@@ -783,6 +830,10 @@ msgstr "Show entry menu (desktop)"
|
||||
msgid "Show entry menu (mobile)"
|
||||
msgstr "Show entry menu (mobile)"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show external link icon"
|
||||
msgstr "Show external link icon"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show feeds and categories with no unread entries"
|
||||
msgstr "Show feeds and categories with no unread entries"
|
||||
@@ -795,9 +846,21 @@ msgstr "Show keyboard shortcut help"
|
||||
msgid "Show native menu (desktop)"
|
||||
msgstr "Show native menu (desktop)"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show star icon"
|
||||
msgstr "Show star icon"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab favicon"
|
||||
msgstr "Show unread count in tab favicon"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show unread count in tab title"
|
||||
msgstr "Show unread count in tab title"
|
||||
|
||||
#: src/pages/WelcomePage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "Sign up"
|
||||
msgstr "Sign up"
|
||||
|
||||
@@ -810,19 +873,20 @@ msgstr "Something bad just happened..."
|
||||
msgid "Space"
|
||||
msgstr "Space"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/header/Star.tsx
|
||||
msgid "Star"
|
||||
msgstr "Star"
|
||||
|
||||
#: src/app/constants.ts
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/components/sidebar/Tree.tsx
|
||||
msgid "Starred"
|
||||
msgstr "Starred"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/pages/app/AddPage.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Subscribe"
|
||||
msgstr "Subscribe"
|
||||
|
||||
@@ -895,8 +959,9 @@ msgstr "Try the demo!"
|
||||
msgid "Unread"
|
||||
msgstr "Unread"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/header/Star.tsx
|
||||
msgid "Unstar"
|
||||
msgstr "Unstar"
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user