Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c386e67e2 | ||
|
|
b572876099 | ||
|
|
48c4f28d5d | ||
|
|
8df3899920 | ||
|
|
f9a79bf2c4 | ||
|
|
809dd897f7 | ||
|
|
8d2e5b12aa | ||
|
|
d5a61acd01 | ||
|
|
aa11e1a65e | ||
|
|
839afdda36 | ||
|
|
40b85c76fb | ||
|
|
da8ba46d21 | ||
|
|
b15fd7258d | ||
|
|
701d593b5b | ||
|
|
c8ad6c8416 | ||
|
|
b859daf39b | ||
|
|
d2d58e675a | ||
|
|
518b34873b | ||
|
|
763ee2e1f5 | ||
|
|
9dd8ee05e9 | ||
|
|
b4a49e2321 | ||
|
|
04c2f487fc | ||
|
|
708ac26837 | ||
|
|
9431a1bbf5 | ||
|
|
006e841a84 | ||
|
|
7a9a16ce0e | ||
|
|
342242b2c3 | ||
|
|
06ece835c7 | ||
|
|
78f322b51a | ||
|
|
88b25a54cb | ||
|
|
638b1a9aac | ||
|
|
e526d0e3f0 | ||
|
|
75da650a80 | ||
|
|
8a9243b639 | ||
|
|
2baf2b0dcf | ||
|
|
ec126d52e2 | ||
|
|
3c2cab94d7 | ||
|
|
12dea11a9a | ||
|
|
bf0192412a | ||
|
|
acad35a52b | ||
|
|
671212c23d | ||
|
|
146de47e0c | ||
|
|
7187cc5e1d | ||
|
|
0a656830c8 | ||
|
|
8142025fdd | ||
|
|
98b78b3856 | ||
|
|
5140d729e2 | ||
|
|
3a4b1f56d9 | ||
|
|
bd5d8bded9 | ||
|
|
ee6c96cdc8 | ||
|
|
720118c5df | ||
|
|
3ec6e2f8e8 | ||
|
|
af540cf250 | ||
|
|
d9ed7ced22 | ||
|
|
61a78107c4 | ||
|
|
3de2f1d25d | ||
|
|
e0fa3e18d1 | ||
|
|
3e49fc4c75 | ||
|
|
6fe9d56b3c | ||
|
|
32126939a6 | ||
|
|
7962631a03 | ||
|
|
10dc9a4528 | ||
|
|
e93cdae808 | ||
|
|
cd9ba11d24 | ||
|
|
f7d6802f58 | ||
|
|
b1b1911eba | ||
|
|
d2536d8dcd | ||
|
|
500eaa4569 | ||
|
|
6f068ebf18 | ||
|
|
f840f7b15d | ||
|
|
c172477004 | ||
|
|
9e76878c7c | ||
|
|
06fef55e2c | ||
|
|
0ce5fc462a | ||
|
|
40dd3637d7 | ||
|
|
caa17a48a8 | ||
|
|
2fe980f2ab | ||
|
|
0b38897868 | ||
|
|
f3a7f75be3 | ||
|
|
0bf3c0d43f | ||
|
|
6a25732a9f | ||
|
|
117955bcf9 | ||
|
|
95d2731d13 | ||
|
|
f7da54dded | ||
|
|
87aebe9216 | ||
|
|
6bc140e123 | ||
|
|
4897f9e41e | ||
|
|
4a7797df6c | ||
|
|
f438ed6d54 | ||
|
|
9230389a91 | ||
|
|
9857f818ca | ||
|
|
c6bc6b059e | ||
|
|
7556ebf974 | ||
|
|
2591b22f35 | ||
|
|
748c70cd0f | ||
|
|
d0a6791f21 | ||
|
|
8beef89250 | ||
|
|
b5a19b0186 | ||
|
|
b530f6d29a | ||
|
|
d71b7d2f3d | ||
|
|
170f50f6fc | ||
|
|
d01aebe484 | ||
|
|
881ffa115e | ||
|
|
9cc9958bdf | ||
|
|
16d2bd6786 | ||
|
|
1e607d30e6 | ||
|
|
1923234ffa | ||
|
|
7a25b0fd39 | ||
|
|
1dbf647b1b | ||
|
|
0b3b49189f | ||
|
|
73b8ae6431 | ||
|
|
4402db826d | ||
|
|
95078ea2f1 | ||
|
|
b270ad52b4 | ||
|
|
810f785a1f | ||
|
|
8f5e19b64b | ||
|
|
3017b39e5f | ||
|
|
c2ad509283 | ||
|
|
540c607527 | ||
|
|
17d51ee5f1 | ||
|
|
522e356fe3 | ||
|
|
5c0756855a | ||
|
|
38466f934f | ||
|
|
c198e634d4 | ||
|
|
45e1f87d07 | ||
|
|
89f3741f17 | ||
|
|
dba37cc40a | ||
|
|
a2c3d13bc4 | ||
|
|
c576b52b97 | ||
|
|
4234cf5de6 | ||
|
|
b7cd6e97e4 | ||
|
|
03cf6c0d50 | ||
|
|
9658535f15 | ||
|
|
3c30f1519e | ||
|
|
afbf0f369f | ||
|
|
4527de78f0 | ||
|
|
0dd453e0e2 | ||
|
|
bdb7ce22c0 | ||
|
|
1c7a41ad4f | ||
|
|
42126fa5a9 | ||
|
|
9baf47cd6d | ||
|
|
f25353135d | ||
|
|
f5d2936594 | ||
|
|
8be52aee53 | ||
|
|
0306f604aa | ||
|
|
04789ecc22 | ||
|
|
c55aceb937 | ||
|
|
b8363a53f3 | ||
|
|
372acf88b1 | ||
|
|
4d7336ddec | ||
|
|
d85cf24f8a | ||
|
|
f64c146d2c | ||
|
|
a83e035ac8 | ||
|
|
520438814c | ||
|
|
f716fe50de | ||
|
|
6a7df2444d | ||
|
|
89c644a1c4 | ||
|
|
02a505e2f9 | ||
|
|
b46533d243 | ||
|
|
76ed537d61 | ||
|
|
6da998f13a | ||
|
|
c8a854b60c | ||
|
|
582211dde7 | ||
|
|
9ee4ed6ef9 | ||
|
|
ee11875459 | ||
|
|
fb973e3021 | ||
|
|
bbbef70984 | ||
|
|
ba043e09e9 | ||
|
|
84df59498a | ||
|
|
7e3f134024 | ||
|
|
1eb354a322 | ||
|
|
826c761dc6 | ||
|
|
14b2d10e17 | ||
|
|
404dd1baff | ||
|
|
19e79187fc | ||
|
|
bf9f81b80b | ||
|
|
ae1f141b63 | ||
|
|
971cee45a0 | ||
|
|
0a7e285188 | ||
|
|
0047b54f50 | ||
|
|
8692aed447 | ||
|
|
52cef043e1 | ||
|
|
5c9f4c042e | ||
|
|
47b19d1d74 | ||
|
|
070573e68a | ||
|
|
946d7769f1 | ||
|
|
f0426a3ec2 | ||
|
|
b2d67818a8 | ||
|
|
c0b159d824 | ||
|
|
1c5be170c5 | ||
|
|
a0ec451696 | ||
|
|
a97897ce6f | ||
|
|
f431951ec5 | ||
|
|
5a0389444a | ||
|
|
52d10b279f | ||
|
|
10f52c9027 | ||
|
|
5a8d0f47a3 | ||
|
|
c66ce36d70 | ||
|
|
dfb4859007 | ||
|
|
cb79552103 | ||
|
|
b2846d0a29 | ||
|
|
55844451f2 | ||
|
|
743cba7538 | ||
|
|
2fb58ebb27 | ||
|
|
a68874bc34 | ||
|
|
cc12720856 | ||
|
|
1659103ab8 | ||
|
|
22994253c5 | ||
|
|
52da7649e1 | ||
|
|
8ea3502d8e | ||
|
|
cabe9e3ae7 | ||
|
|
7af9556d59 | ||
|
|
e5ef7ea065 | ||
|
|
49f82746a8 | ||
|
|
e086cf392b | ||
|
|
8e45ffbddc | ||
|
|
7bb59fed76 | ||
|
|
a8e1665214 | ||
|
|
3c4604595a | ||
|
|
fbc4cab3a8 | ||
|
|
e8e1f3cc70 | ||
|
|
b990a087a1 | ||
|
|
84ac7568ee | ||
|
|
093f6e7e25 | ||
|
|
849ae772e2 | ||
|
|
2fd0bf4339 | ||
|
|
68ce33c34b | ||
|
|
882c730738 | ||
|
|
55ad94bbb9 | ||
|
|
54870ab8fd | ||
|
|
edc32041fd | ||
|
|
4b6c8876e7 | ||
|
|
d7e11e20c1 | ||
|
|
d590318153 | ||
|
|
5816ca9c4d | ||
|
|
6627e8c0ec | ||
|
|
379e28e8a4 | ||
|
|
6cdca3af38 | ||
|
|
27b8633bc2 | ||
|
|
a035796503 | ||
|
|
7feed36dac | ||
|
|
6c7a824608 | ||
|
|
ed2c5af7e9 | ||
|
|
0c143d0491 | ||
|
|
9efce05f11 | ||
|
|
4903b1e141 | ||
|
|
5718f31fe6 | ||
|
|
4fb25451a2 | ||
|
|
860dd5bdcd | ||
|
|
6326620cd8 | ||
|
|
c6c9c19be6 | ||
|
|
ac38b8f60a | ||
|
|
91e5a4c894 | ||
|
|
385224077b | ||
|
|
a011bf7ec2 | ||
|
|
58a6a97130 | ||
|
|
f59b846643 | ||
|
|
28d7dfeb67 | ||
|
|
6cc39d2107 | ||
|
|
a92a01095b | ||
|
|
3f4cddd2a2 | ||
|
|
aa734ea976 | ||
|
|
71d9dd84ae | ||
|
|
eaf5875d4c | ||
|
|
caae1ac801 | ||
|
|
746d0196b3 | ||
|
|
f04f54cc82 | ||
|
|
30628004fd | ||
|
|
a6e87b52fd | ||
|
|
8132325e11 | ||
|
|
57684537fa | ||
|
|
167be95bd4 | ||
|
|
6cdf53f1a4 | ||
|
|
6d06d825e5 | ||
|
|
c923e46321 | ||
|
|
b122231485 | ||
|
|
73f94402dd | ||
|
|
0800d5f33a | ||
|
|
337ef44b56 | ||
|
|
70b4effde8 | ||
|
|
9e4c0f5c47 | ||
|
|
535cdc72f7 | ||
|
|
8d258ecab2 | ||
|
|
3079e28f6f | ||
|
|
4970290614 | ||
|
|
6929931cae | ||
|
|
e348310602 | ||
|
|
7abbd2f253 | ||
|
|
c56698492e | ||
|
|
bc4c33fcbb | ||
|
|
05d5ac960c | ||
|
|
80331401b5 | ||
|
|
b4459e1247 | ||
|
|
706c82d3ca | ||
|
|
0e1b086096 | ||
|
|
80359c98f5 | ||
|
|
034e0ec2e3 | ||
|
|
cc6839d13e | ||
|
|
0ce436c2e9 | ||
|
|
966d814281 | ||
|
|
c983bc10ab | ||
|
|
d603398da0 | ||
|
|
1404d2688f | ||
|
|
c72158cf0d | ||
|
|
1e6ed1308f | ||
|
|
fae241373a | ||
|
|
2bf4cd83df | ||
|
|
efd3290ee8 | ||
|
|
aa729d9af8 | ||
|
|
2d6d3658a1 | ||
|
|
0cbdd6281a | ||
|
|
a845a42d0d | ||
|
|
60329e086a | ||
|
|
a3174b96d7 | ||
|
|
29c00066bc | ||
|
|
b90ffc4d3f | ||
|
|
7e030320f5 | ||
|
|
c96bcad32d | ||
|
|
a655cce195 | ||
|
|
aafb0e7964 | ||
|
|
288f7f117f | ||
|
|
125ac2eda8 | ||
|
|
ea80bfae07 | ||
|
|
5e727a3547 | ||
|
|
6672b60d0f | ||
|
|
09fe1a4649 | ||
|
|
e4fa4cc413 | ||
|
|
0be60475cf | ||
|
|
b7e53185b0 | ||
|
|
cd8c76b3eb | ||
|
|
a1d7a1fe48 | ||
|
|
c48153819f | ||
|
|
d2d5590887 | ||
|
|
65b319e127 | ||
|
|
b34c0dd34e | ||
|
|
ddc1a371bf | ||
|
|
a0c0b7b195 | ||
|
|
b07cea9049 | ||
|
|
991388d4cd | ||
|
|
7adc77c823 | ||
|
|
550f93d4ae | ||
|
|
b43187e5b8 | ||
|
|
6230710bb0 | ||
|
|
ff119cbc0a | ||
|
|
2a2ec713af | ||
|
|
41eb560d1a | ||
|
|
f7b9b91144 | ||
|
|
97ab5d066e | ||
|
|
448e19d28c | ||
|
|
391f2c9c00 | ||
|
|
18524ad9eb | ||
|
|
732c35625b | ||
|
|
95857450c2 | ||
|
|
98ea7a542d | ||
|
|
338f6d47d1 | ||
|
|
fdb492c572 | ||
|
|
ce3ff8c3d8 | ||
|
|
1789d9bb67 | ||
|
|
1883b527a4 | ||
|
|
2f4a9d450a | ||
|
|
12fc33a15e | ||
|
|
e4693ecf61 | ||
|
|
c15d709db2 | ||
|
|
e09040b0fb | ||
|
|
4477c3fc2c | ||
|
|
f52a00a156 | ||
|
|
946a063993 | ||
|
|
d3fcbcb6a5 | ||
|
|
f91cfc0f79 | ||
|
|
f0bbc54fd5 | ||
|
|
3ca5f94b9b | ||
|
|
7a35ebec73 | ||
|
|
66d3623f90 | ||
|
|
8c9badd440 | ||
|
|
e0d47a474d | ||
|
|
1a8aff0594 | ||
|
|
cf5e01c213 | ||
|
|
ffae956e21 | ||
|
|
5c4cb7ff6d | ||
|
|
7794c8b89b | ||
|
|
bb78e8e8a7 | ||
|
|
00a2a95adc | ||
|
|
e55dd47f6e | ||
|
|
7b10bda0a4 | ||
|
|
d5b9aefa01 | ||
|
|
cdc3f5e1df | ||
|
|
8fc6f49fb4 | ||
|
|
29e5c14009 | ||
|
|
fe9ff205b9 | ||
|
|
87b59eccee | ||
|
|
42cb5a6c36 | ||
|
|
9826965803 | ||
|
|
47153e41d3 | ||
|
|
677b428e63 | ||
|
|
15936fcef0 | ||
|
|
b2e828e7ef | ||
|
|
ac6e5a3ae8 | ||
|
|
3722477eea | ||
|
|
72d0f76acc | ||
|
|
b7333574aa | ||
|
|
5caccbd1f5 | ||
|
|
698b559935 | ||
|
|
36d5e8f730 | ||
|
|
233721bf51 | ||
|
|
822602a38f | ||
|
|
e69dbf697c | ||
|
|
184d71a1b9 | ||
|
|
ccb9b03287 | ||
|
|
dd4ad6d72b | ||
|
|
195e2cc8f0 | ||
|
|
ef573336d8 | ||
|
|
9377a849f6 | ||
|
|
00ccbf4be2 | ||
|
|
00159d26dc | ||
|
|
b4f93adf05 | ||
|
|
5bbc9003f4 | ||
|
|
c4be926b7c | ||
|
|
147e3911f1 | ||
|
|
c7b814fc01 | ||
|
|
8525571afc | ||
|
|
5c5de91672 | ||
|
|
7dbb0a2e8f | ||
|
|
11b54ffdf9 | ||
|
|
5d76ac33dd | ||
|
|
8e92bf3766 | ||
|
|
56517eec62 | ||
|
|
ed56f0733e | ||
|
|
3fd799ee74 | ||
|
|
4ed8a67eda | ||
|
|
cbb9a8dbd0 | ||
|
|
4280b84459 | ||
|
|
e06e9ac1f8 | ||
|
|
7bea4ad282 | ||
|
|
316c14d845 | ||
|
|
d17be63312 | ||
|
|
2c2253db31 | ||
|
|
a3128e56a3 | ||
|
|
de0cf0ce67 | ||
|
|
e3f33b9d4c | ||
|
|
27143e2663 | ||
|
|
566e2fb35d | ||
|
|
dc5ada1c04 | ||
|
|
603e698f5a | ||
|
|
f893dcb19a | ||
|
|
27b116798e | ||
|
|
ae932e5710 | ||
|
|
bab5a979be | ||
|
|
1b8c9a3e0c | ||
|
|
99d0fa30ed | ||
|
|
5cc146481b | ||
|
|
102da5cba1 | ||
|
|
d374eecb18 | ||
|
|
08d31c0ffe | ||
|
|
c62e40465a | ||
|
|
5367f32d5c | ||
|
|
ad20698a42 | ||
|
|
e865d80a63 | ||
|
|
81a311e7fa | ||
|
|
5397fa8fd3 | ||
|
|
b9e08cd9e3 | ||
|
|
fe86cd6320 | ||
|
|
bba07bb83a | ||
|
|
83a53ee1f0 | ||
|
|
23ba363a0d | ||
|
|
101aee81c0 | ||
|
|
fa60d02129 | ||
|
|
6a9e5fd801 | ||
|
|
12ae7845ec | ||
|
|
045e8ee96f | ||
|
|
993f75b14e | ||
|
|
b48f3746bc | ||
|
|
dcc477acbc | ||
|
|
5f4f007b31 | ||
|
|
9d4788cb7c | ||
|
|
5a1380f9ef | ||
|
|
0d82aa26f3 | ||
|
|
05f2ec19f2 | ||
|
|
25c3c9c565 | ||
|
|
d6e7e3d4a0 | ||
|
|
6148112904 | ||
|
|
ba338e9282 | ||
|
|
2e132690d1 | ||
|
|
8f1b6ca3b7 | ||
|
|
85d0252972 | ||
|
|
077f72a0c5 | ||
|
|
25bec513f3 | ||
|
|
0a6378d84c | ||
|
|
1e6034ce37 | ||
|
|
ef6e5ebcbe | ||
|
|
19a42b7463 | ||
|
|
9d0e608454 | ||
|
|
bd9a98dbfd | ||
|
|
0ffca3dfe4 | ||
|
|
01958559ee | ||
|
|
bb4aef206a | ||
|
|
e74227b0db | ||
|
|
a47aec1cc4 | ||
|
|
cd9d8cde3e | ||
|
|
d4f68d5ec7 | ||
|
|
a910cfcb35 | ||
|
|
962de503fd | ||
|
|
f40e72c882 | ||
|
|
b7993e1d0e | ||
|
|
d6e5e73e82 | ||
|
|
1794f87f99 | ||
|
|
81c7d57f30 | ||
|
|
b4f0149bd8 | ||
|
|
87ec06eef0 | ||
|
|
c764548214 | ||
|
|
c78af7c3ad | ||
|
|
f9cd0d4e29 | ||
|
|
ee03435ca7 | ||
|
|
a9b83256c6 | ||
|
|
7086ddcf58 | ||
|
|
26d226bb17 | ||
|
|
dc4742bf5c | ||
|
|
933df5fae4 | ||
|
|
4b17ac19dc | ||
|
|
43f6a4f499 | ||
|
|
076b99676d | ||
|
|
b3dfa05a4a | ||
|
|
c5946b1a0b | ||
|
|
58a85eabdf | ||
|
|
8a1d9b96dd | ||
|
|
36a3d2f766 | ||
|
|
68010076f9 | ||
|
|
92da783e82 | ||
|
|
7d1e343d25 | ||
|
|
84d17b7255 | ||
|
|
64e06a32ec | ||
|
|
4ee87e197d | ||
|
|
cace667b26 | ||
|
|
67d3e28af7 | ||
|
|
3c5f4e7b84 | ||
|
|
8f913c737c | ||
|
|
8ba147f52f | ||
|
|
9cfbd24b76 | ||
|
|
259abf6d25 | ||
|
|
8c59838a6f | ||
|
|
f7d24c6379 | ||
|
|
0f5466e075 | ||
|
|
7ab0e83f55 | ||
|
|
dc02ede8d7 | ||
|
|
42e484827f | ||
|
|
576c8f97bd | ||
|
|
808b4a687d | ||
|
|
954f2c235b | ||
|
|
51a7bbbfb8 | ||
|
|
9b5f5857bb | ||
|
|
e66f52a8ce | ||
|
|
8023bcea96 | ||
|
|
dfb7491503 | ||
|
|
b43d9c963a | ||
|
|
51c0637134 | ||
|
|
b16c23190b | ||
|
|
fddf2892cd | ||
|
|
53b5715da0 | ||
|
|
3a79fcb366 | ||
|
|
4df034ac55 | ||
|
|
a0bb04fe3a | ||
|
|
a923f28a8d | ||
|
|
78642f1c9b | ||
|
|
f8a85af859 | ||
|
|
bdb5bd0428 | ||
|
|
9e07f55c12 | ||
|
|
6a3e129ed5 | ||
|
|
879ba17760 | ||
|
|
0d2107e72a | ||
|
|
a23a47fded | ||
|
|
7737a29cca | ||
|
|
34278b3bff | ||
|
|
40b0643eaa | ||
|
|
ae7f5fbff2 | ||
|
|
0a776c6a72 | ||
|
|
5caecb1c8f | ||
|
|
0def3f8b2d | ||
|
|
1593acd8e9 | ||
|
|
da5553364a | ||
|
|
b7c03490d8 | ||
|
|
b8dd7b0f71 | ||
|
|
e7a8d5232f | ||
|
|
c638fa38e8 | ||
|
|
f74b054f01 | ||
|
|
b7af33edae | ||
|
|
166211b6f8 | ||
|
|
f8ec47bfdb | ||
|
|
4e84121553 | ||
|
|
ccf4ce8e58 | ||
|
|
6ba3d44fa1 | ||
|
|
c03ed76fec | ||
|
|
31214b10e7 | ||
|
|
5bbc324b2e | ||
|
|
7dcb22950e | ||
|
|
3c06f1f1a3 | ||
|
|
6bcf5161b1 | ||
|
|
ccc6765e2e | ||
|
|
14d40d4e2d | ||
|
|
75d563b320 | ||
|
|
2fdd53b55e | ||
|
|
4ac16a6701 | ||
|
|
e998788d25 | ||
|
|
e23b80b11b | ||
|
|
7eb884dcb9 | ||
|
|
92397adbb3 | ||
|
|
828485789f | ||
|
|
2f84474216 | ||
|
|
762aab397d | ||
|
|
b03cb4590f | ||
|
|
5d40831dd7 | ||
|
|
0d291afb05 | ||
|
|
e107c5c108 | ||
|
|
44905e674d | ||
|
|
5b790e711a | ||
|
|
1a9c54b237 | ||
|
|
0f4434a361 | ||
|
|
7e2823903b | ||
|
|
92937dbb86 | ||
|
|
0868efe095 | ||
|
|
97374f63f9 | ||
|
|
69f7b02ce6 | ||
|
|
b5ee682a41 | ||
|
|
70f82bdfd8 | ||
|
|
e70185560b | ||
|
|
be4be0e7e8 | ||
|
|
b9a5c64113 | ||
|
|
215f398547 | ||
|
|
ace18f9d95 | ||
|
|
c4ec91f61a | ||
|
|
a4881419df | ||
|
|
839359a794 | ||
|
|
d4d9b7c7a7 | ||
|
|
d4526c0c8a | ||
|
|
78f0bc6f02 | ||
|
|
ad3f5afeba | ||
|
|
7c4326939a | ||
|
|
e5428b67b8 | ||
|
|
59616bd8af | ||
|
|
fdc9ac95d2 | ||
|
|
a8f0d1af62 | ||
|
|
f6e377f222 | ||
|
|
c43d7ab92a | ||
|
|
0464e1de5c | ||
|
|
7a7381eac0 | ||
|
|
1379db1e92 | ||
|
|
ee10894f5f | ||
|
|
87d760708d | ||
|
|
ec7155809b | ||
|
|
5af7837468 | ||
|
|
d3f79af2ab | ||
|
|
a6c0fc3984 | ||
|
|
56604de7f1 | ||
|
|
d5e6199775 | ||
|
|
15920abcf7 | ||
|
|
88205d716a | ||
|
|
fa1a16f140 | ||
|
|
4f8075ca6c | ||
|
|
2615d0644d | ||
|
|
d2686937fd | ||
|
|
b60307e28b | ||
|
|
08d49ddc54 | ||
|
|
4cdc80e530 | ||
|
|
acd360efb9 | ||
|
|
87ad89beeb | ||
|
|
c2663ca7d8 | ||
|
|
4e58285cc2 | ||
|
|
ef354fb8af | ||
|
|
79ce535d4a | ||
|
|
50c43c4408 | ||
|
|
8fdec5aadc | ||
|
|
73eccd5308 | ||
|
|
4449677e36 | ||
|
|
8dc3a4b51f | ||
|
|
cd2de3a1db | ||
|
|
1fa0eebc3b | ||
|
|
e13002bab3 | ||
|
|
a750e80ea1 | ||
|
|
66e1b0a089 | ||
|
|
00c0b3f88c | ||
|
|
674311cb4b | ||
|
|
f54b8e00e1 | ||
|
|
c1beac5d0d | ||
|
|
7123345341 | ||
|
|
0e2475e667 | ||
|
|
acf1f1146d | ||
|
|
d136fa4fdf | ||
|
|
61067391b6 | ||
|
|
affebb1ba4 | ||
|
|
cbffa3d4ef | ||
|
|
6ef5ae2517 | ||
|
|
9701496897 | ||
|
|
22d6a3011f | ||
|
|
d22ddd99f6 | ||
|
|
4e8648fca0 | ||
|
|
9f49c357e9 | ||
|
|
d318467039 | ||
|
|
b05c25ac1a | ||
|
|
5dd0d51514 | ||
|
|
c7574a6b83 | ||
|
|
ab64e9343b | ||
|
|
b82cb0e849 | ||
|
|
c44a8fab42 | ||
|
|
9a75eb0a22 | ||
|
|
f994278a8b | ||
|
|
bd882d0e17 | ||
|
|
fddf62409a | ||
|
|
a8967ac764 | ||
|
|
20bc254a42 | ||
|
|
e298d5fbd8 | ||
|
|
ff54c2070d | ||
|
|
e092caa2ef | ||
|
|
feb44be9d0 | ||
|
|
a43a2e8ce2 | ||
|
|
6fcf77829d | ||
|
|
733833e8a4 | ||
|
|
890f998fe0 | ||
|
|
28c708f599 | ||
|
|
f1e536cbcf | ||
|
|
8dc7ad4f01 | ||
|
|
43c8976282 | ||
|
|
8ee63ccc24 | ||
|
|
2900fe07bf | ||
|
|
1d572d1fa9 | ||
|
|
21b8e9ae8f | ||
|
|
cb77b7fc1e | ||
|
|
763141592f | ||
|
|
09fcddfad2 | ||
|
|
b8ce1b5cc1 | ||
|
|
6a34fffcc4 | ||
|
|
75fa636366 | ||
|
|
bee96ebf15 | ||
|
|
9ec23fac5d | ||
|
|
2a7cd3574d | ||
|
|
4f96629e22 | ||
|
|
74bd270423 | ||
|
|
0469f28a54 | ||
|
|
a9c2da3fdb | ||
|
|
2ec37c3ece | ||
|
|
ef812c5697 | ||
|
|
8ab4e7c433 | ||
|
|
23bb30be92 | ||
|
|
a13d44d560 | ||
|
|
42b5b002cc | ||
|
|
f889488915 | ||
|
|
57f3a287c2 | ||
|
|
143b1a73f7 | ||
|
|
54a1860398 | ||
|
|
014b6b2319 | ||
|
|
33763ab865 | ||
|
|
5f5ea892a3 | ||
|
|
a3fc2b2cdd | ||
|
|
2cad70dae0 | ||
|
|
fb56ba2f0d | ||
|
|
7c2b7a09f8 | ||
|
|
abf5531fc5 | ||
|
|
18775e3cea | ||
|
|
9d5e7be105 | ||
|
|
eed647c867 | ||
|
|
072271bbe6 | ||
|
|
a84499d7d4 | ||
|
|
c06db5a2b7 | ||
|
|
fc737b1f51 | ||
|
|
4f068008bd | ||
|
|
953f6dc61f | ||
|
|
01fac1f0ca | ||
|
|
1796ac743d | ||
|
|
60329b3cab | ||
|
|
f489eca5c2 | ||
|
|
1617cae518 | ||
|
|
6c7b99af58 | ||
|
|
ddd242b9df | ||
|
|
c24fe2806c | ||
|
|
8da0a2e239 | ||
|
|
8aff64ce47 | ||
|
|
677f1ce75f | ||
|
|
f99ff95a57 | ||
|
|
7a522acdd9 | ||
|
|
9888a0cf5b | ||
|
|
9b586f7059 | ||
|
|
90666cea99 | ||
|
|
cabf0d388d | ||
|
|
39a33bd77a | ||
|
|
f184915849 | ||
|
|
683b0bc4e6 | ||
|
|
212042556f | ||
|
|
cc4aa6fc1a | ||
|
|
b4b6ef3206 | ||
|
|
ccfd5126bc | ||
|
|
b733b43f8b | ||
|
|
3c0693fdd9 | ||
|
|
938f2be487 | ||
|
|
9f2a09cc97 | ||
|
|
27d5fc4cba | ||
|
|
251b763ea9 | ||
|
|
8a07162e6b | ||
|
|
4097d81b8a | ||
|
|
bf2d72745a | ||
|
|
acc60e6297 | ||
|
|
211b058686 | ||
|
|
95c077a0ba | ||
|
|
0b70017780 | ||
|
|
513e258869 | ||
|
|
7cd046ecc0 | ||
|
|
220a27e030 | ||
|
|
3728fbc367 | ||
|
|
6d41c71542 | ||
|
|
a1b4dde3e3 | ||
|
|
677586e882 | ||
|
|
3177a87690 | ||
|
|
f8fda777db | ||
|
|
e955c3ddc1 | ||
|
|
7e36418276 | ||
|
|
e5616dffa7 | ||
|
|
be3087d2cc | ||
|
|
396721fce6 | ||
|
|
d49af938d9 | ||
|
|
d2ae8522b5 | ||
|
|
a3e27709d0 | ||
|
|
a245553cad | ||
|
|
8bcdc6d109 | ||
|
|
8a1b28a793 | ||
|
|
90b236e6f9 | ||
|
|
804b8cf17b | ||
|
|
ed3eefbbfc | ||
|
|
51564056a1 | ||
|
|
4884caadd4 | ||
|
|
e129e52e01 | ||
|
|
893a0f43c1 | ||
|
|
083cbe9840 | ||
|
|
a7fcbae65f | ||
|
|
7ae80c2e7c | ||
|
|
f1ede2b876 | ||
|
|
8c51adbca1 | ||
|
|
446f293da0 | ||
|
|
bbd7f9c6d6 | ||
|
|
9e5fb90bd6 | ||
|
|
2ed735ad95 | ||
|
|
00be6f0181 | ||
|
|
29a4d367f9 | ||
|
|
df43c61785 | ||
|
|
464c49257a | ||
|
|
bd4a6d0eee | ||
|
|
9410fc6a8c | ||
|
|
bd400c0797 | ||
|
|
54c721c265 | ||
|
|
94cfa4f3d4 | ||
|
|
f21ef7f444 | ||
|
|
66a0cf4782 | ||
|
|
52f27ce743 | ||
|
|
57296ba313 | ||
|
|
47dc1727df | ||
|
|
cd9fe46fdb | ||
|
|
775cdf237c | ||
|
|
63367998bf | ||
|
|
5d3e110a9e | ||
|
|
42267eaef8 | ||
|
|
b716599c90 | ||
|
|
6d80de9216 | ||
|
|
212dc99cec | ||
|
|
6f4f8ec1ea | ||
|
|
24c64daa49 | ||
|
|
bdd9a51d25 | ||
|
|
f27862b036 | ||
|
|
f7e9f725b0 | ||
|
|
4ee430284b | ||
|
|
96458048fc | ||
|
|
a31f9d5a50 | ||
|
|
33d5f0b5bb | ||
|
|
ec43f0a549 | ||
|
|
8e0dc326cb | ||
|
|
9994c87446 | ||
|
|
647ce573b0 | ||
|
|
1ca4ead313 | ||
|
|
1ef5ffab52 | ||
|
|
f4f2ddfb3c | ||
|
|
650b4805be | ||
|
|
22754f32ec | ||
|
|
85a67b533a | ||
|
|
974dbd224a | ||
|
|
b8c7a8b936 | ||
|
|
86844064db | ||
|
|
0c9b9a1b97 | ||
|
|
0a9846b86f | ||
|
|
96ee17b8cc | ||
|
|
4b65792585 | ||
|
|
32b871e338 | ||
|
|
46ddb725db | ||
|
|
d0fdada9f7 | ||
|
|
a343853b38 | ||
|
|
136fbc2f77 | ||
|
|
25bc5643b1 | ||
|
|
a0c03363a7 | ||
|
|
8dd7a97769 | ||
|
|
f70dfaaa28 | ||
|
|
01ceb6dc37 | ||
|
|
d56e50b29d | ||
|
|
0b2c722f61 | ||
|
|
26990f38dd | ||
|
|
b2d87efc99 | ||
|
|
63b7b32e9c | ||
|
|
8925a48822 | ||
|
|
8ad47181a9 | ||
|
|
c1b9ec7a77 | ||
|
|
6daf34e261 | ||
|
|
fc5c865b18 | ||
|
|
083306a1cd | ||
|
|
5ebacbc347 | ||
|
|
08c4adfd46 | ||
|
|
27f87ad09f | ||
|
|
d0b33ca0c1 | ||
|
|
bb44c7cb3e | ||
|
|
66e366d9d0 | ||
|
|
31390eac80 | ||
|
|
543a71f6f2 | ||
|
|
5d29b79dab | ||
|
|
207ff77778 | ||
|
|
b85d25614b | ||
|
|
d1bcdca0d3 | ||
|
|
ffb68b1651 | ||
|
|
49d46cfedc | ||
|
|
9c18e2877b | ||
|
|
a1da5ceabe | ||
|
|
759b92bfd6 | ||
|
|
b4f3d19259 | ||
|
|
35e3271839 | ||
|
|
90da86ebb8 | ||
|
|
ad978d2ed3 | ||
|
|
7140a2d894 | ||
|
|
eb86603b47 | ||
|
|
1e1117c28b | ||
|
|
f3de0d8391 | ||
|
|
4749be20b4 | ||
|
|
3eea00bf6d | ||
|
|
d301287fdc | ||
|
|
07f4090498 | ||
|
|
067d3778d8 | ||
|
|
daa250e8fa | ||
|
|
91f992df39 | ||
|
|
ecaf9a37da | ||
|
|
31cc2fbdd4 | ||
|
|
6e30687919 | ||
|
|
dbf212f0df | ||
|
|
af49645f8d | ||
|
|
59bed7593d | ||
|
|
43ca723c75 | ||
|
|
855370074f | ||
|
|
84f707d6cd | ||
|
|
17be9cf266 | ||
|
|
0e3d01ade6 | ||
|
|
02fe2b1754 | ||
|
|
a0c553b3a9 | ||
|
|
62ea631269 | ||
|
|
b32d842394 | ||
|
|
0c38b5b9f1 | ||
|
|
dd0b20634c | ||
|
|
f8319408f1 | ||
|
|
c451e92a9c | ||
|
|
36cf70202d | ||
|
|
39fe83fdd1 | ||
|
|
cc5a465dc3 | ||
|
|
2018f04bdf | ||
|
|
f0e79eab95 | ||
|
|
fc1b2fbbaa | ||
|
|
a8e89da5e9 | ||
|
|
e369a6b7be | ||
|
|
ae4d4398cd | ||
|
|
5781d20615 | ||
|
|
a6e9521435 | ||
|
|
1e72711bc3 | ||
|
|
1f793e2d00 | ||
|
|
af8d94d102 | ||
|
|
a424c3286a | ||
|
|
5f9b33506a | ||
|
|
819b10e1f6 | ||
|
|
0c170766e5 | ||
|
|
f7a0025b97 | ||
|
|
22776e9088 | ||
|
|
d23d393c39 | ||
|
|
bbf6e796f8 | ||
|
|
1c3c194b3a | ||
|
|
0cc633c5c1 | ||
|
|
9a8f1ad508 | ||
|
|
65f09c6535 | ||
|
|
d7db5d2c77 | ||
|
|
8205b01c65 | ||
|
|
d7ee936b27 | ||
|
|
d9ae6f312f | ||
|
|
aea9a513b4 | ||
|
|
d3b65f6280 | ||
|
|
3bf53aa87f | ||
|
|
5081df4f2f | ||
|
|
b91dbdaa8f | ||
|
|
c313de9f93 | ||
|
|
09b00f3137 | ||
|
|
8342336e8d | ||
|
|
fe245dc95e | ||
|
|
3001afd9db | ||
|
|
48b5c8d4e7 | ||
|
|
21dc4e758f | ||
|
|
5c5fc0271e | ||
|
|
082c6bee93 | ||
|
|
c1d354a6fb | ||
|
|
eab683ec71 | ||
|
|
cd2b87b317 | ||
|
|
4abe63c00f | ||
|
|
7e4e61e005 | ||
|
|
db38e015f8 | ||
|
|
ff20d2a554 | ||
|
|
cb7df205bb | ||
|
|
dfe2c42172 | ||
|
|
4854c0c100 | ||
|
|
33bdf124c2 | ||
|
|
2d752ef144 | ||
|
|
d258bce037 | ||
|
|
21ff37d82e | ||
|
|
312b08ca99 | ||
|
|
a47f0d0ae5 | ||
|
|
c1a6268c2d | ||
|
|
65b6fe2e2e | ||
|
|
0f29e94272 | ||
|
|
dd9b6a1943 | ||
|
|
a45bc13248 | ||
|
|
b7b5caac9d | ||
|
|
0eb5ebab0d | ||
|
|
f34b1011c8 | ||
|
|
33049c21e7 | ||
|
|
6c12822279 | ||
|
|
8a3ff150a6 | ||
|
|
de4bfdf9e0 | ||
|
|
461a45dc51 | ||
|
|
2b24d716a9 | ||
|
|
36b6fd80b8 | ||
|
|
f8cbf51842 | ||
|
|
17fbf9188d | ||
|
|
ec5bacd951 | ||
|
|
56c8649040 | ||
|
|
435eb5779f | ||
|
|
a83e04fe4d | ||
|
|
bdbff334a5 | ||
|
|
f70eb35f69 | ||
|
|
f1a5aba485 | ||
|
|
f8e454dab3 | ||
|
|
9988454af1 | ||
|
|
e0b65bbebf | ||
|
|
09fd37820b | ||
|
|
360194c627 | ||
|
|
21768b1b59 | ||
|
|
325d521f49 | ||
|
|
2deb241ff2 | ||
|
|
a2194435e6 | ||
|
|
448e173f7e | ||
|
|
4b52280549 | ||
|
|
9e09f7b7e7 | ||
|
|
83293d909e | ||
|
|
176553a674 | ||
|
|
a4b22b6c88 | ||
|
|
fb6bcdf5c9 | ||
|
|
776c75d823 | ||
|
|
143f7c6a7a | ||
|
|
b520c58ef4 | ||
|
|
3c47adbf32 | ||
|
|
835acff637 | ||
|
|
a08e3c7d27 | ||
|
|
d9258745d0 | ||
|
|
70bd4f1c4e | ||
|
|
ab3860cdc9 | ||
|
|
9185ef9a55 | ||
|
|
e6826b176e | ||
|
|
f5bb29e61a | ||
|
|
d0d013e046 | ||
|
|
5002c2ce67 | ||
|
|
8e3b218a74 | ||
|
|
51bd80faf4 | ||
|
|
b4fe0af53a | ||
|
|
2ba9a03e2c | ||
|
|
7394754b6f | ||
|
|
b41b291e60 | ||
|
|
0f582e3248 | ||
|
|
b90e84be42 | ||
|
|
ef627c9530 | ||
|
|
b3f021a577 | ||
|
|
96263c11ec | ||
|
|
b2a6e1180c | ||
|
|
817b44b152 | ||
|
|
701bb4caa4 | ||
|
|
a1b8682316 | ||
|
|
8c386dbe57 | ||
|
|
d8e49d993a | ||
|
|
93310eb029 | ||
|
|
f215c26a63 | ||
|
|
b5fddeb627 | ||
|
|
81d4c5c403 | ||
|
|
bf7dec2437 | ||
|
|
efa9b5f9a7 | ||
|
|
1f862a37e9 | ||
|
|
e8a491dba5 | ||
|
|
aa899b8096 | ||
|
|
8de55c450a | ||
|
|
1fafa68793 | ||
|
|
e69219697a | ||
|
|
08a0f6e587 | ||
|
|
7a26d80255 | ||
|
|
4171848655 | ||
|
|
a7193fd783 | ||
|
|
ccce95d4fe | ||
|
|
b9f37c22d9 | ||
|
|
3b3c95e57c | ||
|
|
fadea36232 | ||
|
|
99b687244c | ||
|
|
813b26270f | ||
|
|
b715507dd0 | ||
|
|
0cb502ca67 | ||
|
|
e5073eb65f | ||
|
|
c3b55cd7fa | ||
|
|
e9ef1bd698 | ||
|
|
045fe99158 | ||
|
|
dad95033c0 | ||
|
|
02e4d52db6 | ||
|
|
1f83f34e55 | ||
|
|
1ffb45433c | ||
|
|
b6780dff03 | ||
|
|
5b91dcb272 | ||
|
|
099f1ca9d0 | ||
|
|
c73f81a80a | ||
|
|
177b150c95 | ||
|
|
489eb5598c | ||
|
|
ad571c1e27 | ||
|
|
9827b871fc | ||
|
|
004436e868 | ||
|
|
008b615903 | ||
|
|
451b465e4f | ||
|
|
c91e5de961 | ||
|
|
8251423d95 | ||
|
|
65f66ac60c | ||
|
|
94caa1fca3 | ||
|
|
f7d1708550 | ||
|
|
b888f9da32 | ||
|
|
4d678184f3 | ||
|
|
a2bf157b01 | ||
|
|
1b279119cb | ||
|
|
2075e82381 | ||
|
|
9f60ca8ece | ||
|
|
d6867fb62e | ||
|
|
07e918965b | ||
|
|
24bcc758e7 | ||
|
|
7ae64b0552 | ||
|
|
38e46f6d85 | ||
|
|
530316c25f | ||
|
|
cc750df077 | ||
|
|
3c85694fc2 | ||
|
|
955a1346ff | ||
|
|
8e4566fe5f | ||
|
|
20623e4c42 | ||
|
|
cac31d3350 | ||
|
|
746b56ac57 | ||
|
|
bc140b3fb5 | ||
|
|
5d15411d3d | ||
|
|
c6ce694dfe | ||
|
|
16d303d966 | ||
|
|
f422dbd3db | ||
|
|
5125a44170 | ||
|
|
6e7f2c570f | ||
|
|
db31e65cdf | ||
|
|
3cd9bf6505 | ||
|
|
3b05f6267e | ||
|
|
3e5b0bff4f | ||
|
|
c9cb15d7b7 | ||
|
|
89415c86a4 | ||
|
|
4cd515b19f | ||
|
|
730e2c5ba5 | ||
|
|
3735a6689c | ||
|
|
15c81d8f35 | ||
|
|
154c780281 | ||
|
|
ec9422d22d | ||
|
|
101e8eaa24 | ||
|
|
6a31bba69c | ||
|
|
96ee68dee7 | ||
|
|
5a94c81b11 | ||
|
|
cf8aa6f9a4 | ||
|
|
20e201795e | ||
|
|
e95902d2bb | ||
|
|
79a3b39248 | ||
|
|
c08a0eb8d1 | ||
|
|
a280258d4f | ||
|
|
7985a7a522 | ||
|
|
6ba2e16a91 | ||
|
|
f34ebe2fd3 | ||
|
|
ab3d4a056f | ||
|
|
c88a7c000b | ||
|
|
53b700aa70 | ||
|
|
66c219f6d1 | ||
|
|
06dd153955 | ||
|
|
88a0e63894 | ||
|
|
cac7e53ea8 | ||
|
|
3e390388eb | ||
|
|
2a1b01c0aa | ||
|
|
5a535bb07a | ||
|
|
235ceae228 | ||
|
|
46710aead4 | ||
|
|
277d443d4c | ||
|
|
770c25a273 | ||
|
|
c935de3ec1 | ||
|
|
8350123839 | ||
|
|
02adb81ae0 | ||
|
|
cf26427e68 | ||
|
|
6e357e93ec | ||
|
|
1c8f3932fb | ||
|
|
75671a0a8a | ||
|
|
58296dd896 | ||
|
|
2fff34dc61 | ||
|
|
851ade7e24 | ||
|
|
13585f5317 | ||
|
|
5d0ab36f2c | ||
|
|
be46cf9a25 | ||
|
|
ffdc438091 | ||
|
|
52741cf46a | ||
|
|
9263caed59 | ||
|
|
aa5a9fc6d2 | ||
|
|
316518d63b | ||
|
|
03c44d76c9 | ||
|
|
346d6cb119 | ||
|
|
dae3d57d1e | ||
|
|
aa65cef103 | ||
|
|
e19c923538 | ||
|
|
ec292705b3 | ||
|
|
d36588d148 | ||
|
|
32c171d05b | ||
|
|
f031be4ce2 | ||
|
|
d3ebcabec4 | ||
|
|
f683f14a30 | ||
|
|
0a311e87e4 | ||
|
|
d42ce3e7bb | ||
|
|
4c5d7eb762 | ||
|
|
1d58779d90 | ||
|
|
1434d81d3e | ||
|
|
b44d060fac | ||
|
|
7c89ebc047 | ||
|
|
d140427c94 | ||
|
|
2abdf3693c | ||
|
|
8d1eafe2bc | ||
|
|
510f982579 | ||
|
|
63e6be9867 | ||
|
|
0a2957449f | ||
|
|
ee2ba047ab | ||
|
|
a8590c1b90 | ||
|
|
374f4bc116 | ||
|
|
76fa1f6c9f | ||
|
|
dd68aa342a | ||
|
|
21d614aa97 | ||
|
|
f4e4bbbb8f | ||
|
|
d30c9d9d90 | ||
|
|
e0884a4eec | ||
|
|
446c7f3ab5 | ||
|
|
48a6189fe7 | ||
|
|
e568d06b02 | ||
|
|
243c962667 | ||
|
|
908510ff0b | ||
|
|
d6aab0c470 | ||
|
|
8d2640d99d | ||
|
|
d45748cc32 | ||
|
|
2693dd9366 | ||
|
|
dcfa070176 | ||
|
|
eb577d61d5 | ||
|
|
f79bb90b69 | ||
|
|
4591268063 | ||
|
|
7e849934ff | ||
|
|
92fb322f7b | ||
|
|
abc154738c | ||
|
|
63850f75af | ||
|
|
e0498e5e87 | ||
|
|
232f0b8d69 | ||
|
|
d410dbb17d | ||
|
|
e7c3d0441c | ||
|
|
0119d69641 | ||
|
|
a21fdf50bb | ||
|
|
8fdc5e6d94 | ||
|
|
41e81c2d7a | ||
|
|
10d2f5e641 | ||
|
|
cdfcc6493c | ||
|
|
6b26f6d93f | ||
|
|
a11cf38378 | ||
|
|
7d061c4aaf | ||
|
|
152fcccf60 | ||
|
|
159104a50a | ||
|
|
66b2b77791 | ||
|
|
36ade29e46 | ||
|
|
61c44057cb | ||
|
|
fd9dc1a801 | ||
|
|
57b813196f | ||
|
|
eaf429de3d | ||
|
|
45923d034e | ||
|
|
fbdfacd14d | ||
|
|
a55ddda6cf | ||
|
|
6c66d0791a | ||
|
|
c19d427f3a | ||
|
|
a078081347 | ||
|
|
3a042f25c2 | ||
|
|
6d781a6358 | ||
|
|
349ec59df7 | ||
|
|
833aa3c748 | ||
|
|
eca5b1c7a0 | ||
|
|
0aa925f95d | ||
|
|
2f214b25c7 | ||
|
|
38c3a46395 | ||
|
|
cbb9a29c8d | ||
|
|
dddc4f235e | ||
|
|
c537d0f464 | ||
|
|
8be588afdf | ||
|
|
5b6c707de5 | ||
|
|
8b6dd322a7 | ||
|
|
dfdbd8b092 | ||
|
|
4fad58b1e9 | ||
|
|
6003b21f6a | ||
|
|
963f09057c | ||
|
|
b185c3e102 | ||
|
|
272365dde7 | ||
|
|
da91254cf0 | ||
|
|
70642b464e | ||
|
|
035a4485b7 | ||
|
|
73ac7496e2 | ||
|
|
40d816f86d | ||
|
|
c513fb06e7 | ||
|
|
dec0b4d171 | ||
|
|
03ed25f617 | ||
|
|
487afe799e | ||
|
|
d15366ee24 | ||
|
|
816c7776f7 | ||
|
|
f5eadc5d9e | ||
|
|
8e02c0bd10 | ||
|
|
614f61ce29 | ||
|
|
6c80d50290 | ||
|
|
5492e53ba9 | ||
|
|
e795a317cc | ||
|
|
8eb7366a4e | ||
|
|
b85eab9d10 | ||
|
|
09b0920337 | ||
|
|
502860ac5e | ||
|
|
68d845ff3d | ||
|
|
b2e69b60a6 | ||
|
|
8015da09e8 | ||
|
|
06ed20f59c | ||
|
|
a003d8bfe6 | ||
|
|
468aa28449 | ||
|
|
e0615c751e | ||
|
|
81e4a0eff9 | ||
|
|
b48ece91b2 | ||
|
|
912d2b1e0a | ||
|
|
c97f45e3e5 | ||
|
|
de4230e0d0 | ||
|
|
79ca627e7c | ||
|
|
52045ad2a6 | ||
|
|
a6a9554868 | ||
|
|
d92229095d | ||
|
|
1d1ce0c322 | ||
|
|
cb45eb4b3c | ||
|
|
0b31150a43 | ||
|
|
b626152fda | ||
|
|
3cc2cedaa5 | ||
|
|
e2fb54f88d | ||
|
|
1f334ab3dc | ||
|
|
c39933c051 | ||
|
|
b6719e8972 | ||
|
|
cfaa765858 | ||
|
|
8477df7e5c | ||
|
|
40f3ccc112 | ||
|
|
a40c116617 | ||
|
|
c97219311f | ||
|
|
49d618f066 | ||
|
|
129a77ae7c | ||
|
|
b2d13db01f | ||
|
|
c76cebc469 | ||
|
|
668bb9b5d5 | ||
|
|
ec82af7a68 | ||
|
|
73b7880d73 | ||
|
|
a584eecb70 | ||
|
|
cd0a25f910 | ||
|
|
6cf2bf109c | ||
|
|
f6b7c7bb00 | ||
|
|
e987970969 | ||
|
|
18d8037309 | ||
|
|
4c1dda68b5 | ||
|
|
086f2c73db | ||
|
|
f7eab86166 | ||
|
|
05b2b395ad | ||
|
|
c5298242f6 | ||
|
|
37914d3cb5 | ||
|
|
280ed45199 | ||
|
|
ea20740581 | ||
|
|
3b7d28130b | ||
|
|
72503916a1 | ||
|
|
cb150bdba9 | ||
|
|
7c16a79629 | ||
|
|
1f5cd55272 | ||
|
|
7f4a7f05f5 | ||
|
|
95239fb257 | ||
|
|
f2583bc37f | ||
|
|
00d7ff6604 | ||
|
|
5011c3ba04 | ||
|
|
9baa446675 | ||
|
|
9e7f86e4d6 | ||
|
|
5ac4b42452 | ||
|
|
e9b05fe345 | ||
|
|
13ac013e9f | ||
|
|
d3b635ffd6 | ||
|
|
e09609cc68 | ||
|
|
d2f64630f5 | ||
|
|
0c3d8b77e4 | ||
|
|
905d09899c | ||
|
|
0712c9f24b | ||
|
|
29329f924e | ||
|
|
c6c307c50c | ||
|
|
b96b477f65 | ||
|
|
4f53e39f8d | ||
|
|
5cac16a632 | ||
|
|
a1a02d5098 | ||
|
|
72c77305e4 | ||
|
|
d6980ab0b3 | ||
|
|
4ee0a1edaf | ||
|
|
ef7a4ff583 | ||
|
|
e8979594a1 | ||
|
|
690488abdd | ||
|
|
f7910a1546 | ||
|
|
42b92ec081 | ||
|
|
b32684ab7b | ||
|
|
eedc4374b9 | ||
|
|
76304395bd | ||
|
|
d3dabc35e5 | ||
|
|
039b45e926 | ||
|
|
bf1b162cbc | ||
|
|
be3c7a8314 | ||
|
|
3bfa95a032 | ||
|
|
5f1121a241 | ||
|
|
7ccf38f0df | ||
|
|
61d83319d4 | ||
|
|
1bc8cffbf3 | ||
|
|
a7acfb0291 | ||
|
|
fb34f8890f | ||
|
|
3d3864d535 | ||
|
|
3f9296e38c | ||
|
|
3a17a370f2 | ||
|
|
42833accc0 | ||
|
|
85e9a4c4c6 | ||
|
|
2c0624d5b4 | ||
|
|
347c8fff73 | ||
|
|
fa17aa4a57 | ||
|
|
8021c6bb81 | ||
|
|
c4be2e5d00 | ||
|
|
cbd0c7b228 | ||
|
|
0b13c06a09 | ||
|
|
a5c6180154 | ||
|
|
19e899ba97 | ||
|
|
3d9965c4af | ||
|
|
98c22a4323 | ||
|
|
649516cf32 | ||
|
|
29c0e840d3 | ||
|
|
3a5a7063cd | ||
|
|
f601dbe313 | ||
|
|
0640009752 | ||
|
|
f153d99744 | ||
|
|
85c07dba9c | ||
|
|
7ace45929a | ||
|
|
212a7a7551 | ||
|
|
fd9f579802 | ||
|
|
ee1b451f59 | ||
|
|
a2873fd782 | ||
|
|
fc549fe006 | ||
|
|
b931e5d273 | ||
|
|
25101b5a02 | ||
|
|
9c32293fa8 | ||
|
|
de6255b388 | ||
|
|
6566fbe71c | ||
|
|
556fc31000 | ||
|
|
1072e5778f | ||
|
|
29bad845b1 | ||
|
|
a80061d97f | ||
|
|
bc1d6ce5d8 | ||
|
|
c6c41d5f2d | ||
|
|
4ded2c1848 | ||
|
|
df90fc9c33 | ||
|
|
b1da50133b | ||
|
|
a65d7208c7 | ||
|
|
36a53cc405 | ||
|
|
cccb9ac681 | ||
|
|
0a025d807b | ||
|
|
9b07bbd505 | ||
|
|
f224aea444 | ||
|
|
e21257de0c | ||
|
|
ecf2a65e16 | ||
|
|
1f4f3101f2 | ||
|
|
d66b33fd19 | ||
|
|
5d5913233b | ||
|
|
2b6e8c030d | ||
|
|
b1e0056a3a | ||
|
|
d64eb95388 | ||
|
|
8794b0d37c | ||
|
|
beda8972e8 | ||
|
|
c655defe6d | ||
|
|
6dc459a64f | ||
|
|
90979be600 | ||
|
|
6b1361ad9d | ||
|
|
bede42388f | ||
|
|
604c1bd3d4 | ||
|
|
149c565ee6 | ||
|
|
efca3632cc | ||
|
|
60468b4afa | ||
|
|
50976b2d09 | ||
|
|
0fce37cc73 | ||
|
|
12c7313935 | ||
|
|
5d1162bd32 | ||
|
|
6aa3a71761 | ||
|
|
3e1f1e557e | ||
|
|
0a993fb151 | ||
|
|
e4da7ee601 | ||
|
|
d6ef6ecdd8 | ||
|
|
e1f242d973 | ||
|
|
1f0503907e | ||
|
|
a9da298dd7 | ||
|
|
d3dc13d58a | ||
|
|
c7eb8caf8b | ||
|
|
2ca5f93e8b | ||
|
|
b08ff12760 | ||
|
|
c62193d1fe | ||
|
|
858a9bf93b | ||
|
|
7a22746f67 | ||
|
|
92215e8817 | ||
|
|
b449def7e6 | ||
|
|
b7f020f69c | ||
|
|
be79f67510 | ||
|
|
57af428b9e | ||
|
|
2c9c8b9ab4 | ||
|
|
bc4dbc7de2 | ||
|
|
ce55ca1e0d | ||
|
|
99bcbc288c | ||
|
|
517134c95b | ||
|
|
e866bac25a | ||
|
|
0f5b03ce64 | ||
|
|
4d1cbe7f62 | ||
|
|
69c38cbeca | ||
|
|
936de04b1e | ||
|
|
b8f7a89291 | ||
|
|
11a3995722 | ||
|
|
eb68da635f | ||
|
|
eb2de6f3f2 | ||
|
|
5b118667ef | ||
|
|
f346b73baa | ||
|
|
0fabdac9e0 | ||
|
|
590035cce9 | ||
|
|
f1d5965bb5 | ||
|
|
85027aa504 | ||
|
|
72b7517623 | ||
|
|
29c3e98c05 | ||
|
|
fa15c56f1c | ||
|
|
3eca440e0c | ||
|
|
9679841e22 | ||
|
|
16612bface | ||
|
|
e48f12de4a | ||
|
|
7b4953a049 | ||
|
|
6010bf0ba9 | ||
|
|
320bc0fe06 | ||
|
|
0411fe4cfe | ||
|
|
9513a6eb80 | ||
|
|
dc6afb765a | ||
|
|
b944e36d5c | ||
|
|
458ba2de77 | ||
|
|
77423ad833 | ||
|
|
3c6c929854 | ||
|
|
89a4ef2e4d | ||
|
|
4644dda250 | ||
|
|
8c124f8980 | ||
|
|
769cf19d5c | ||
|
|
42f6c51393 | ||
|
|
2a47afa672 | ||
|
|
5a8927b547 | ||
|
|
cc9e369b6f | ||
|
|
63aee0e876 | ||
|
|
dcce37d99e | ||
|
|
39aaac2d1b | ||
|
|
f1a632fb52 | ||
|
|
37ad3fbf0b | ||
|
|
87cb6f8262 | ||
|
|
60eee04922 | ||
|
|
3250816523 | ||
|
|
7e62173f01 | ||
|
|
6c0124fcf2 | ||
|
|
a361fdac9e | ||
|
|
911772510c | ||
|
|
d2777ba42d | ||
|
|
a4b9d1e98b | ||
|
|
3b8e6d7585 | ||
|
|
7e0565df3b | ||
|
|
e8e2b49993 | ||
|
|
a72aea3190 | ||
|
|
e97572cfab | ||
|
|
3a0ba29e25 | ||
|
|
f699ece732 | ||
|
|
8bfa1fadda | ||
|
|
511fccd084 | ||
|
|
db091b2b39 | ||
|
|
fd51fdddb3 | ||
|
|
bece4a44f1 | ||
|
|
be5dcba4c6 | ||
|
|
ae975cdc2f | ||
|
|
890097885a | ||
|
|
9a8f595514 | ||
|
|
113db1c97d | ||
|
|
11f2355099 | ||
|
|
a275a895cb | ||
|
|
f84a5a2b51 | ||
|
|
3c0444d812 | ||
|
|
1c744d8d6b | ||
|
|
82cad800c2 | ||
|
|
b6be4375bd | ||
|
|
b635281428 | ||
|
|
326b9658fd | ||
|
|
d0cc9b54f4 | ||
|
|
92dec8a7aa | ||
|
|
3e987cc2dc | ||
|
|
20843dd1fb | ||
|
|
29f014abc3 | ||
|
|
49b5b3d41e | ||
|
|
efb0c39d33 | ||
|
|
10460f270d | ||
|
|
0eafc4ac88 | ||
|
|
bfc6d4b3c0 | ||
|
|
9626eb904e | ||
|
|
c9f62c3f3a | ||
|
|
be52cf86b5 | ||
|
|
922c8f2100 | ||
|
|
7dbac45670 | ||
|
|
0dd4fa666a | ||
|
|
94aa1665ab | ||
|
|
009d854622 | ||
|
|
886d452996 | ||
|
|
0afed3f9aa | ||
|
|
e42b28ae9e | ||
|
|
0672180233 | ||
|
|
0cd401c45d | ||
|
|
2913a0421d | ||
|
|
42134b4888 | ||
|
|
8b2df9e1c8 | ||
|
|
adb1fd421a | ||
|
|
f1173c496a | ||
|
|
59aebd3666 | ||
|
|
85b89a2d80 | ||
|
|
17dedb6b8e | ||
|
|
8fb8a9d096 | ||
|
|
26d0cbf2b2 | ||
|
|
bcee01cdd1 | ||
|
|
8060ba6453 | ||
|
|
62f723ff6f | ||
|
|
86b5113c46 | ||
|
|
e9fbd49864 | ||
|
|
9c765ced45 | ||
|
|
4405fdb5fe | ||
|
|
645d904954 | ||
|
|
b8be888f05 | ||
|
|
6aca16c336 | ||
|
|
a01d85411c | ||
|
|
b280c6f723 | ||
|
|
f74e2bcc7f | ||
|
|
ae2d54f454 | ||
|
|
63be5932be | ||
|
|
1565c26823 | ||
|
|
ae72b55a2e | ||
|
|
b83e2519ac | ||
|
|
22f5e61978 | ||
|
|
966097e1ff | ||
|
|
0c2de37cf8 | ||
|
|
328bdd68e2 | ||
|
|
a10ee6c9f2 | ||
|
|
383847d3aa | ||
|
|
2b8dca7748 | ||
|
|
9a19339ec5 | ||
|
|
2f07c87801 | ||
|
|
c14567311b | ||
|
|
bc8aad5062 | ||
|
|
a1a649cd9f | ||
|
|
ba0586d407 | ||
|
|
c2212ac4ad | ||
|
|
4548829595 | ||
|
|
07177222a0 | ||
|
|
723c39de85 | ||
|
|
a20dab859f | ||
|
|
acf3bc945c | ||
|
|
379ea9c208 | ||
|
|
74f282a69d | ||
|
|
ef7ae9ada9 | ||
|
|
64bf07c71e | ||
|
|
8e30d99e94 | ||
|
|
0367966ae6 | ||
|
|
4d7cef70d2 | ||
|
|
2f93e53716 | ||
|
|
ebfdff90c7 | ||
|
|
3a5d6bc2ca | ||
|
|
15518a4ce0 | ||
|
|
c41bd1a4ef | ||
|
|
9b1335720b | ||
|
|
731994c9fe | ||
|
|
3341f3e7f6 | ||
|
|
08d7261488 | ||
|
|
89ccb99395 | ||
|
|
94660d9e71 | ||
|
|
8f4932610c | ||
|
|
b405ca970c | ||
|
|
05b12c2d59 | ||
|
|
268dfee75d | ||
|
|
9c3c9d28fa | ||
|
|
00291da933 | ||
|
|
87eb0468a6 | ||
|
|
f2fe249560 | ||
|
|
42e70c1d05 | ||
|
|
19fa54f9d0 | ||
|
|
ec6a6e08ff | ||
|
|
39e683baf0 | ||
|
|
2fcc6f2ea3 | ||
|
|
7f30350873 | ||
|
|
9859e921d2 | ||
|
|
0dea89c0de | ||
|
|
1aa29237e2 | ||
|
|
727887fb79 | ||
|
|
12079f6ac6 | ||
|
|
f1d9b5c645 | ||
|
|
75b4ed05ab | ||
|
|
a2f938b0a0 | ||
|
|
8869a27ac9 | ||
|
|
927278317e | ||
|
|
ccace95a6c | ||
|
|
813ed6d541 | ||
|
|
e7d13ff254 | ||
|
|
b16dd74492 | ||
|
|
2bcaa1926a | ||
|
|
1a48270a28 | ||
|
|
6606fa8108 | ||
|
|
7730486f30 | ||
|
|
ad0963e412 | ||
|
|
5ae07b721a | ||
|
|
a695b35eca | ||
|
|
2ce1d01535 | ||
|
|
f8f8a7c1d1 | ||
|
|
6277a48419 | ||
|
|
155eb493bf | ||
|
|
de2443eb5a | ||
|
|
c9ac7f2fa3 | ||
|
|
22d1b478f9 | ||
|
|
43b11277ad | ||
|
|
7589a112d0 | ||
|
|
9bbea458c6 | ||
|
|
556c5dd56a | ||
|
|
2688e761ba | ||
|
|
5a10dc1f2f | ||
|
|
9c1de2aca9 | ||
|
|
c83dff46a7 | ||
|
|
3bae948218 | ||
|
|
6551622e07 | ||
|
|
3319478876 | ||
|
|
58b8ed7940 | ||
|
|
bc808889a1 | ||
|
|
5cd19cee56 | ||
|
|
f82791096d | ||
|
|
58726de491 | ||
|
|
60dcfbb071 | ||
|
|
9e7a86a1db | ||
|
|
0028f2293f | ||
|
|
e7d88cce58 | ||
|
|
73635e2784 | ||
|
|
92d4a40228 | ||
|
|
0b8bc1afb0 | ||
|
|
27d86d8185 | ||
|
|
dbc73a73e2 | ||
|
|
f7ef9a7b4a | ||
|
|
b06cb26edc | ||
|
|
56d84e1d18 | ||
|
|
3859beb543 | ||
|
|
1b2f83ea80 | ||
|
|
ba122e786c | ||
|
|
cdedcd9dac | ||
|
|
2214356d27 | ||
|
|
02b7266206 | ||
|
|
e0291b2d52 | ||
|
|
068e49c53c | ||
|
|
0fe67c9e4a | ||
|
|
79dd95facd | ||
|
|
9e5d6403fa | ||
|
|
0a9296c6c4 | ||
|
|
07df05304e | ||
|
|
5d0556debc | ||
|
|
789103a2b8 | ||
|
|
f48713409b | ||
|
|
8ecae7e745 | ||
|
|
1b64dd6d56 | ||
|
|
d4c255e240 | ||
|
|
09894d1c8f | ||
|
|
574fa84f7d | ||
|
|
0953a29b4e | ||
|
|
e82732c8c0 | ||
|
|
e2588e81f8 | ||
|
|
c525d1f9e0 | ||
|
|
b21b9cd24e | ||
|
|
31b0097afb | ||
|
|
e6aef150ee | ||
|
|
aa0d3d329e | ||
|
|
a9ae7ad815 | ||
|
|
65ba766b0f | ||
|
|
bd37cb2c26 | ||
|
|
5703dceb0c | ||
|
|
15b170db5d | ||
|
|
ef71035e0d | ||
|
|
7977bbf6e1 | ||
|
|
a7f769d4f6 | ||
|
|
a634d824d0 | ||
|
|
42f0cc307f | ||
|
|
68e5955d09 | ||
|
|
3df178a026 | ||
|
|
ba57bf9503 | ||
|
|
c216d785aa | ||
|
|
f4359fef92 | ||
|
|
13f90d7a39 | ||
|
|
135c4dc60e | ||
|
|
a78c63b05b | ||
|
|
e3081cba17 | ||
|
|
bfbfaf559a | ||
|
|
6e03932e6d | ||
|
|
c5f2bd4aa3 | ||
|
|
ba56444a6e | ||
|
|
56fbc14893 | ||
|
|
7f20e6cc46 | ||
|
|
6dba3f9bf7 | ||
|
|
47b2c32031 | ||
|
|
7a3281bd23 | ||
|
|
55f4ff8f73 | ||
|
|
c5d8732031 | ||
|
|
2181c9aa91 | ||
|
|
82f841fdf9 | ||
|
|
bebb9a88eb | ||
|
|
8df70602c0 | ||
|
|
ef39b5c79c | ||
|
|
a77dd32d20 | ||
|
|
c4dc65761c | ||
|
|
bc6d3fba55 | ||
|
|
1cf5171526 | ||
|
|
db850ece85 | ||
|
|
be3acbe13a | ||
|
|
670fc425ac | ||
|
|
83bd89fe50 | ||
|
|
c4489d1ea0 | ||
|
|
82a11aaf4f | ||
|
|
04a513556b | ||
|
|
d670deb0cb | ||
|
|
cc089b2bd1 | ||
|
|
e812218919 | ||
|
|
35b531dad0 | ||
|
|
6715c614ac | ||
|
|
1a7975072f | ||
|
|
2dc9959ca4 | ||
|
|
a69a5a5472 | ||
|
|
b689c96870 | ||
|
|
ea00818573 | ||
|
|
cbbf5f833a | ||
|
|
8a258998dd | ||
|
|
80c44c4c03 | ||
|
|
ba122bd9d0 | ||
|
|
58709c33a0 | ||
|
|
087b1937de | ||
|
|
54430cd83d | ||
|
|
a35817c379 | ||
|
|
9375f5c281 | ||
|
|
ba29687865 | ||
|
|
c5c64bee20 | ||
|
|
74e9bb5fc7 | ||
|
|
e6556ca92d | ||
|
|
9737272e19 | ||
|
|
f5b4a2a887 | ||
|
|
d16f335627 | ||
|
|
46adf5dcc1 | ||
|
|
2c38f3c8f2 | ||
|
|
105205661f | ||
|
|
a69cf5cd85 | ||
|
|
7c5b76fda3 | ||
|
|
7e2d531533 | ||
|
|
64c41785f6 | ||
|
|
69a3dd14ba | ||
|
|
2a4ed6f264 | ||
|
|
ba2d9415be | ||
|
|
60dfb3c21f | ||
|
|
9816ab5c80 | ||
|
|
904a1e12d2 | ||
|
|
f8395f7f38 | ||
|
|
0df1ba97d5 | ||
|
|
d581cb13af | ||
|
|
ede597d67e | ||
|
|
fe7078aebf | ||
|
|
2f855d5c8a | ||
|
|
6788b77f32 | ||
|
|
2bb4584235 | ||
|
|
3e3d3571be | ||
|
|
fa4ff0b0c3 | ||
|
|
176c2a6edd | ||
|
|
74a005d0e3 | ||
|
|
02bb56357f | ||
|
|
d9ad69b085 | ||
|
|
824b0d3e3c | ||
|
|
8ab1ec615f | ||
|
|
5fb3228841 | ||
|
|
0e4538650d | ||
|
|
012bc61f4a | ||
|
|
0d126ae0d8 | ||
|
|
2c064989c1 | ||
|
|
51cc550502 | ||
|
|
e2e7176b51 | ||
|
|
de784bd95c | ||
|
|
854e627065 | ||
|
|
84a23e4aea | ||
|
|
68245629e4 | ||
|
|
fd0a8287dc | ||
|
|
ebd0578ea2 | ||
|
|
e1b0409648 | ||
|
|
17a777b7e6 | ||
|
|
336c80e296 | ||
|
|
fcd8a99924 | ||
|
|
a7096b5cdf | ||
|
|
32b4e99c12 | ||
|
|
c2658395c0 | ||
|
|
d3935a49de | ||
|
|
4892e7aa2b | ||
|
|
d96f458a1b | ||
|
|
93b05e448e | ||
|
|
63f68dff04 | ||
|
|
7d0760469a | ||
|
|
065ca2dad4 | ||
|
|
03f566e7cd | ||
|
|
9c8ebde19d | ||
|
|
ebcbe6496a | ||
|
|
a2a5d6d10a | ||
|
|
276afa42da | ||
|
|
2437ae575c | ||
|
|
ce5b0cb303 | ||
|
|
1357c7cb1c | ||
|
|
2c334f4346 | ||
|
|
0a7c0f64d2 | ||
|
|
9427d2191a | ||
|
|
8096353e6e | ||
|
|
3f400a8b2a | ||
|
|
3ddefff1df | ||
|
|
ddd5370f69 | ||
|
|
6f203f0a3e | ||
|
|
c795784417 | ||
|
|
c115ba79c8 | ||
|
|
e6c78c3407 | ||
|
|
20b39b51bf | ||
|
|
47eb538444 | ||
|
|
fa136af90a | ||
|
|
1ab397be0a | ||
|
|
8a47ed864c | ||
|
|
8215d608b7 | ||
|
|
65398b68a4 | ||
|
|
44b4779a9d | ||
|
|
7795006337 | ||
|
|
2820141897 | ||
|
|
62e3068af1 | ||
|
|
4e3b0b73b4 | ||
|
|
f5775c6a02 | ||
|
|
3012d1b4e1 | ||
|
|
35ed03021a | ||
|
|
443a7b4f8d | ||
|
|
172a6cec2a | ||
|
|
6a9640ec29 | ||
|
|
2daa28ec0b | ||
|
|
fb418d3aa0 | ||
|
|
a7d7400309 | ||
|
|
a49eaa2103 | ||
|
|
400a0ab04f | ||
|
|
7126db88c0 | ||
|
|
04fc3ee3d4 | ||
|
|
16bbae35f2 | ||
|
|
cf6fca86f3 | ||
|
|
b434d69fd1 | ||
|
|
7e6892f828 | ||
|
|
7fa8ba6a5a | ||
|
|
2b591b3384 | ||
|
|
36bb2ff175 | ||
|
|
bd75f50a0a | ||
|
|
3841148820 | ||
|
|
1a421a7b34 | ||
|
|
b6bac6d11f | ||
|
|
ef68fe1742 | ||
|
|
197c64ad14 | ||
|
|
f0ff26ebb4 | ||
|
|
e6a8a8c335 | ||
|
|
4ebe99d54e | ||
|
|
9f845c4eb8 | ||
|
|
c6e2e48a98 | ||
|
|
a955fcf12f | ||
|
|
014264d1b6 | ||
|
|
9e640b92f8 | ||
|
|
9312fd105b | ||
|
|
ce527f4af5 | ||
|
|
73e2fa3883 | ||
|
|
40fd5c4a7b | ||
|
|
a2c6f2e366 | ||
|
|
00dcd900e3 | ||
|
|
4f08757c04 | ||
|
|
983a7d8dd9 | ||
|
|
222fc10ed1 | ||
|
|
31986b1d50 | ||
|
|
4ca2eb4c0e | ||
|
|
3d691f5a13 | ||
|
|
f729872d70 | ||
|
|
bcd627663b | ||
|
|
ab0da3f49c | ||
|
|
f0530e2270 | ||
|
|
ddc8359b04 | ||
|
|
1e281644eb | ||
|
|
6cad71e859 | ||
|
|
b2f439ff0b | ||
|
|
2b22a64818 | ||
|
|
243e068fd6 | ||
|
|
b9cedc15b8 | ||
|
|
7d8ddd2408 | ||
|
|
fcc816ffec | ||
|
|
a17723dac9 | ||
|
|
b35465fa8e | ||
|
|
0915e5d6ca | ||
|
|
51a8568d4d | ||
|
|
8cd66607ef | ||
|
|
2f6996c1fb | ||
|
|
e89eac9a32 | ||
|
|
3927d8b1ab | ||
|
|
41a4e7efd4 | ||
|
|
5ebc63833d | ||
|
|
e79e903f25 | ||
|
|
28c9faf48f | ||
|
|
adcc270e51 | ||
|
|
e4ebcfaf99 | ||
|
|
1afc3a414b | ||
|
|
3c105c6091 | ||
|
|
8cc80dbd69 | ||
|
|
44528a40b7 | ||
|
|
f2303f447d | ||
|
|
ddf5b39872 | ||
|
|
6f2cc1c187 | ||
|
|
75eaf88d84 | ||
|
|
7fd72ff687 | ||
|
|
29a250d16f | ||
|
|
903d6dc271 | ||
|
|
591a9456dc | ||
|
|
265b6efc96 | ||
|
|
69e079c319 | ||
|
|
20c651da41 | ||
|
|
da922d2216 | ||
|
|
00fb500287 | ||
|
|
98f1b1087c | ||
|
|
2262aaa181 | ||
|
|
f18d053cbf | ||
|
|
a740c51d82 | ||
|
|
b9ffe62e8b | ||
|
|
d3d1685785 | ||
|
|
adeb58bd6b | ||
|
|
bd33a72ddc | ||
|
|
92cfcf4415 | ||
|
|
dd850197e4 | ||
|
|
59fa9253b3 | ||
|
|
14b953f764 | ||
|
|
572be56d75 | ||
|
|
d9860935c1 | ||
|
|
0c7210b177 | ||
|
|
af26c7d1ee | ||
|
|
5bd7f53fdd | ||
|
|
ee94183e2a | ||
|
|
bd9144628f | ||
|
|
9a364d3294 | ||
|
|
bb76c9a6c3 | ||
|
|
97e89ed8be | ||
|
|
c49d5c0755 | ||
|
|
f89842e753 | ||
|
|
acc3d8f76b | ||
|
|
0e379abcd2 | ||
|
|
090e75b6ed | ||
|
|
f3c4af1cbb | ||
|
|
8b0d741087 | ||
|
|
d9c85d16c8 | ||
|
|
3ff98e06de | ||
|
|
ae8e6e4bc8 | ||
|
|
19545e5cca | ||
|
|
039e6ae8e9 | ||
|
|
845f672650 | ||
|
|
51bc308fa8 | ||
|
|
72b1ea7f5d | ||
|
|
cf3a69d1e2 | ||
|
|
22728d89b9 | ||
|
|
29642c4910 | ||
|
|
a7acd8041f | ||
|
|
01b57cf33f | ||
|
|
9fcba3dac8 | ||
|
|
48c09b89d4 | ||
|
|
5b7723b9b1 | ||
|
|
78bf20e3c3 | ||
|
|
af9d668d2a | ||
|
|
a21ff6233f | ||
|
|
d8894dcd7e | ||
|
|
7f0656649d | ||
|
|
46ea0ac03e | ||
|
|
0a9cf32fa6 | ||
|
|
810db8ce2e | ||
|
|
d69d1a9534 | ||
|
|
6cedfadccf | ||
|
|
a795ec9387 | ||
|
|
5f7eff27d1 | ||
|
|
03cc056558 | ||
|
|
92678730ca | ||
|
|
32ce0cb6ba | ||
|
|
437311fe65 | ||
|
|
89fa0f2d9b | ||
|
|
567f349bb0 | ||
|
|
ccbf9ac9b3 | ||
|
|
b4e24dbb98 | ||
|
|
e8088b232a | ||
|
|
8208bf1d04 | ||
|
|
85f78257ed | ||
|
|
91a15c4b5e | ||
|
|
77ffb13549 | ||
|
|
152bea3547 | ||
|
|
9f836be3c8 | ||
|
|
e3122da6f0 | ||
|
|
1b896f3218 | ||
|
|
85e5af7c77 | ||
|
|
77123756b6 | ||
|
|
6a7bea7120 | ||
|
|
eb9c6cbf69 | ||
|
|
1338cf33dd | ||
|
|
86567521dd | ||
|
|
331c1658d9 | ||
|
|
c810220df6 | ||
|
|
807128bc2f | ||
|
|
0b25fbdef2 | ||
|
|
4b59500f32 | ||
|
|
6f5c173efd | ||
|
|
969892f9e1 | ||
|
|
bd904c3bd6 | ||
|
|
02c355ae43 | ||
|
|
7eadd94292 | ||
|
|
4af4ac52af | ||
|
|
b38c86e42b | ||
|
|
0b7187d98b | ||
|
|
3ee3ca89a4 | ||
|
|
498dfad299 | ||
|
|
7055c9421f | ||
|
|
363ff309e6 | ||
|
|
3b3baf1c27 | ||
|
|
5204350453 | ||
|
|
1669fcc6d4 | ||
|
|
3136abc337 | ||
|
|
55948a1fa7 | ||
|
|
9da3ca872c | ||
|
|
425389051a | ||
|
|
20ed2fd33c | ||
|
|
a6c97e496e | ||
|
|
0aa029a495 | ||
|
|
1e58a5419c | ||
|
|
11b7f2a9dc | ||
|
|
cd09658745 | ||
|
|
72c912d57f | ||
|
|
753fd6ba2f | ||
|
|
b7ecf9d925 | ||
|
|
718eb43e9d | ||
|
|
5e57938aab | ||
|
|
e07777179f | ||
|
|
6202de44df | ||
|
|
57eb6bc85d | ||
|
|
f8a35f9ac3 | ||
|
|
31902697c0 | ||
|
|
94a400d0f4 | ||
|
|
ba420747a6 | ||
|
|
cddd9bb8dc | ||
|
|
fee7ac1e02 | ||
|
|
d738f367de | ||
|
|
81aafce72b | ||
|
|
d3e5115500 | ||
|
|
223827ad76 | ||
|
|
0d85a08955 | ||
|
|
3691a382da | ||
|
|
7cc8e125e5 | ||
|
|
164cc5c346 | ||
|
|
107beaec62 | ||
|
|
3bde78d9f2 | ||
|
|
1dfc91aa66 | ||
|
|
9b9cda384a | ||
|
|
9860fb231b | ||
|
|
389c3859c6 | ||
|
|
b0a07428d0 | ||
|
|
06b3e80880 | ||
|
|
d8dbe0464f | ||
|
|
e23beaf831 | ||
|
|
4a070bbc57 | ||
|
|
415eb11677 | ||
|
|
61ca037357 | ||
|
|
39da546129 | ||
|
|
d2446cd0e1 | ||
|
|
2e54d30b9d | ||
|
|
c8995ad65f | ||
|
|
1e036a3d58 | ||
|
|
9924dff39a | ||
|
|
1054cdcfc4 | ||
|
|
0405ba0321 | ||
|
|
c8dc0aefa0 | ||
|
|
e889f5f5ba | ||
|
|
23477e1d2f | ||
|
|
e344bc993e | ||
|
|
d0c556263a | ||
|
|
3f3ba4d34b | ||
|
|
8f379a1d55 | ||
|
|
00d12b96db | ||
|
|
dbbf759aaa | ||
|
|
50b7df3c45 | ||
|
|
62efa587cb | ||
|
|
1d72600e05 | ||
|
|
8218f39192 | ||
|
|
3e55478f4b | ||
|
|
91f08b0853 | ||
|
|
0f1a248fcc | ||
|
|
2450e3a0b6 | ||
|
|
7a3cc9c863 | ||
|
|
150f258b70 | ||
|
|
797bc4820b | ||
|
|
d95ccdf01a | ||
|
|
3a398a9a26 | ||
|
|
3dae87853c | ||
|
|
94bda624d4 | ||
|
|
f45de3e759 | ||
|
|
3b91dccb9d | ||
|
|
86404544f3 | ||
|
|
204965a693 | ||
|
|
10a0aa75f0 | ||
|
|
c13eb67999 | ||
|
|
48fcdd7a6c | ||
|
|
b121958686 | ||
|
|
6d93a57bc3 | ||
|
|
091de3e4e0 | ||
|
|
a4a878c30b | ||
|
|
26d6844b65 | ||
|
|
7504393e67 | ||
|
|
b95469330f | ||
|
|
1fefaf91af | ||
|
|
bd5f8afe99 | ||
|
|
972b7066dc | ||
|
|
6854531584 | ||
|
|
7c0a3b11e0 | ||
|
|
75e778862c | ||
|
|
07ae35e94f | ||
|
|
90aaf45282 | ||
|
|
4e96402bd4 | ||
|
|
86596b2095 | ||
|
|
5c8e37f70d | ||
|
|
3d22970600 | ||
|
|
4457c0e2b3 | ||
|
|
b4a490b796 | ||
|
|
f3e6caaa46 | ||
|
|
f9598228cd | ||
|
|
f4eccc7805 | ||
|
|
191819d56f | ||
|
|
6c9c0da14d | ||
|
|
4f62d3591f | ||
|
|
0efacb3cd5 | ||
|
|
6edfd721f1 | ||
|
|
cc14a5e7e5 | ||
|
|
7f71546006 | ||
|
|
55bb0fa1df | ||
|
|
9f1cd55ffa | ||
|
|
bd51cd66b6 | ||
|
|
5addbdf0fe | ||
|
|
1f45284639 | ||
|
|
30e3dae3e0 | ||
|
|
f917856698 | ||
|
|
c65f29c2d1 | ||
|
|
8561756335 | ||
|
|
688e363c89 | ||
|
|
0e63e955ce | ||
|
|
a6bd460469 | ||
|
|
460065a95b | ||
|
|
2af6a39a93 | ||
|
|
1ce659f803 | ||
|
|
5ee435be0c | ||
|
|
d0b3c4c9eb | ||
|
|
8f39e4234a | ||
|
|
0779620a66 | ||
|
|
37a54e5ff8 | ||
|
|
23f6b151c7 | ||
|
|
14509eea65 | ||
|
|
5e3be270d7 | ||
|
|
3b913304be | ||
|
|
882cf367ad | ||
|
|
925b7463e9 | ||
|
|
b7276e2f49 | ||
|
|
3b9f69a112 | ||
|
|
06751e26a8 | ||
|
|
a5bd4bb0e5 | ||
|
|
560a7f1d93 | ||
|
|
d5a5795e60 | ||
|
|
ab41f45196 | ||
|
|
9e9fa75a81 | ||
|
|
fa9928c4a7 | ||
|
|
e709c047a6 | ||
|
|
4e6504992e | ||
|
|
bd894a11ac | ||
|
|
3ca10b1dfe | ||
|
|
b774b3cd7a | ||
|
|
b5d14fbddb | ||
|
|
9e25b31e6d | ||
|
|
d5cc174d98 | ||
|
|
968a64ff7b | ||
|
|
857302878c | ||
|
|
31197e44f4 | ||
|
|
e506abf5b0 | ||
|
|
12fe7c495d | ||
|
|
d76cc891e1 | ||
|
|
eb617dda04 | ||
|
|
747ef62411 | ||
|
|
04958db14d | ||
|
|
b7013c548e | ||
|
|
84e9099952 | ||
|
|
ac53b55dd9 | ||
|
|
aec666bfab | ||
|
|
e479595c27 | ||
|
|
ea9875db38 | ||
|
|
93918416b1 | ||
|
|
cc18eb1fa2 | ||
|
|
21a42aab8e | ||
|
|
0eecec2c7c | ||
|
|
9931c746f3 | ||
|
|
60494d2f33 | ||
|
|
5064fc8eca | ||
|
|
27b42ec8b9 | ||
|
|
d7375c35ad | ||
|
|
ea6505f62b | ||
|
|
10d2d9aaaf | ||
|
|
bba52b1eb6 | ||
|
|
2dc8e85563 | ||
|
|
afaf9ebd02 | ||
|
|
70eb727d7a | ||
|
|
84b8c85d59 | ||
|
|
40cdfe40a6 | ||
|
|
2fe9107e5d | ||
|
|
bcfc4af28c | ||
|
|
72de9c7a6f | ||
|
|
66e33aff0f | ||
|
|
8895b5ea6d | ||
|
|
56e685b0d4 | ||
|
|
53255143a7 | ||
|
|
50d6562e51 | ||
|
|
6a16752cde | ||
|
|
b2bf8685d8 | ||
|
|
cded8a41f9 | ||
|
|
376c1a9cbc | ||
|
|
3f2befe3e6 | ||
|
|
d96df170c4 | ||
|
|
102c2804c2 | ||
|
|
977cd00187 | ||
|
|
4b7f6fdf9b | ||
|
|
eedc51f48a | ||
|
|
0e42436082 | ||
|
|
de6c21715b | ||
|
|
cfb1116b7e | ||
|
|
93b60a1731 | ||
|
|
e6f78859a3 | ||
|
|
869b6443e8 | ||
|
|
0941a2666c | ||
|
|
ebccdcb3f7 | ||
|
|
40c3b0b5bd | ||
|
|
b6d507d5bc | ||
|
|
a360ee1308 | ||
|
|
02aa303dfc | ||
|
|
e967056dee | ||
|
|
480fe474d5 | ||
|
|
aa12298111 | ||
|
|
1092fea3a5 | ||
|
|
334e439f94 | ||
|
|
76386cb403 | ||
|
|
0f3807d6ad | ||
|
|
349320c80d | ||
|
|
759134c85b | ||
|
|
ccd89ec2e3 | ||
|
|
c0901835dc | ||
|
|
0e148594fb | ||
|
|
60a85c088e | ||
|
|
88947f4e54 | ||
|
|
07dbb7d7f9 | ||
|
|
e477a6a3e6 | ||
|
|
9b30a17df0 | ||
|
|
4fefe8775e | ||
|
|
a4d5963620 | ||
|
|
c063cd3461 | ||
|
|
b894153c43 | ||
|
|
db3ed73ee6 | ||
|
|
3ad376e2ae | ||
|
|
f8f6424bb1 | ||
|
|
1158e1bef2 | ||
|
|
1557a9a92f | ||
|
|
7c69c1305f | ||
|
|
c59b14026c | ||
|
|
7de3f5b94a | ||
|
|
214c39e4f6 | ||
|
|
09b1c60cc7 | ||
|
|
a5873b8fcc | ||
|
|
f82312b9d0 | ||
|
|
9ee1080240 | ||
|
|
182c9701e6 | ||
|
|
dac5713451 | ||
|
|
aeae97f690 | ||
|
|
abd8f6d20a | ||
|
|
5247599eaa | ||
|
|
9669b72015 | ||
|
|
f8245fac62 | ||
|
|
f886cf6ee2 | ||
|
|
27a0d811be | ||
|
|
2aadc27db8 | ||
|
|
fd11c3011e | ||
|
|
0f47430b8d | ||
|
|
b7135334fd | ||
|
|
7a3643aa62 | ||
|
|
dbb792c337 | ||
|
|
2ec8308f73 | ||
|
|
e0570c09b1 | ||
|
|
75ff0e4311 | ||
|
|
f390b690a7 | ||
|
|
33519c7c60 | ||
|
|
4ec08bf50e | ||
|
|
62cdb32423 | ||
|
|
08fbb9bf79 | ||
|
|
42bea50907 | ||
|
|
78fed18d75 | ||
|
|
51cd1eea9a | ||
|
|
8f8fa6faa1 | ||
|
|
3e5b27e415 | ||
|
|
c60a441b01 | ||
|
|
df6fc53a1f | ||
|
|
9624abbc5e | ||
|
|
7568a59019 | ||
|
|
db36cbaebb | ||
|
|
fb12ce3a6a | ||
|
|
b3cd56b9c4 | ||
|
|
7d13b49093 | ||
|
|
577d23e212 | ||
|
|
32dcb31085 | ||
|
|
5e6cb5fc4a | ||
|
|
168e246c53 | ||
|
|
6dd1d0e1f7 | ||
|
|
04d29ce512 | ||
|
|
c660388ffd | ||
|
|
9d3b904988 | ||
|
|
5a55b1469c | ||
|
|
078af21a01 | ||
|
|
15b8d8362d | ||
|
|
8eaa9c92fa | ||
|
|
ae57a5a65e | ||
|
|
360d3ac6f6 | ||
|
|
8d523f7e85 | ||
|
|
85471d672d | ||
|
|
0591a09ced | ||
|
|
215809d9c2 | ||
|
|
c3c60fd613 | ||
|
|
49b999693c | ||
|
|
34f91ac3af | ||
|
|
c48db9dfca | ||
|
|
954a38b8d8 | ||
|
|
10229e7a4e | ||
|
|
37dc62018d | ||
|
|
21b973f508 | ||
|
|
c1decbcd5a | ||
|
|
9defd2476d | ||
|
|
7961d2a660 | ||
|
|
1ac61799fe | ||
|
|
4c33e60c69 | ||
|
|
d3ffceb1b5 | ||
|
|
42e8d31d86 | ||
|
|
b8346c5381 | ||
|
|
e51bb2ae6e | ||
|
|
3df40af7ab | ||
|
|
624755029f | ||
|
|
18a5f27cb7 | ||
|
|
36f3d18ef0 | ||
|
|
4ab388caf7 | ||
|
|
9ff47971c7 | ||
|
|
b183bd0ebe | ||
|
|
5dd77907fc | ||
|
|
c6b409738b | ||
|
|
134d1c3f0f | ||
|
|
951f604dc8 | ||
|
|
2efea09f2b | ||
|
|
a970a83409 | ||
|
|
55aaea087f | ||
|
|
9bd9785537 | ||
|
|
b29615b5aa | ||
|
|
719d72bd86 | ||
|
|
d312f22b5a | ||
|
|
0e45d80a0a | ||
|
|
afda7b630e | ||
|
|
18a3a4450e | ||
|
|
7a99e42bde | ||
|
|
9f47699a49 | ||
|
|
758d10ed14 | ||
|
|
baa7a32dc7 | ||
|
|
29f88fcadf | ||
|
|
97995fb6f1 | ||
|
|
569c44f78c | ||
|
|
6163fa4ba8 | ||
|
|
a3456b2941 | ||
|
|
ca9d01af79 | ||
|
|
51d172c79a | ||
|
|
bee819cc98 | ||
|
|
ecaf465e16 | ||
|
|
39e2ddc5be | ||
|
|
849b9b06a9 | ||
|
|
b9cda3efd8 | ||
|
|
dbe2ca80a8 | ||
|
|
f59f68bebd | ||
|
|
df81cb5ab3 | ||
|
|
f8cbf0e5bf | ||
|
|
d02a4671cb | ||
|
|
087dd6cae0 | ||
|
|
31f5f2d88a | ||
|
|
c86042b885 | ||
|
|
ac23f70bb5 | ||
|
|
ad446e7adc | ||
|
|
2392329e93 | ||
|
|
0d2f00657c | ||
|
|
9ba645cb81 | ||
|
|
ac00b77fc4 | ||
|
|
b7475a8d4c | ||
|
|
b732eb9a74 | ||
|
|
f4d5e51983 | ||
|
|
1b07e77167 | ||
|
|
69257b82ad | ||
|
|
41656b76b6 | ||
|
|
2efb2911b1 | ||
|
|
857593232c | ||
|
|
e5bfa59c50 | ||
|
|
bcafbde0d6 | ||
|
|
dc20bdae10 | ||
|
|
b730cbdc02 | ||
|
|
a2cfaf8717 | ||
|
|
ecb0449ce6 | ||
|
|
ade2d77662 | ||
|
|
818e547572 | ||
|
|
f083e50fad | ||
|
|
338414f6af | ||
|
|
80c396bb48 | ||
|
|
fc629a7760 | ||
|
|
65a47a8fe4 | ||
|
|
bb8e3390a0 | ||
|
|
f8f81ae07f | ||
|
|
9918b63a4c | ||
|
|
a4a0589512 | ||
|
|
517f662864 | ||
|
|
7564143f92 | ||
|
|
14432e519f | ||
|
|
a3af54cefc | ||
|
|
c51cf1d9b2 | ||
|
|
a39b4e63a1 | ||
|
|
928be7b8e4 | ||
|
|
196bb3385b | ||
|
|
413b643d4b | ||
|
|
6deab5f022 | ||
|
|
ff6fc99920 | ||
|
|
c5f3e75ae5 | ||
|
|
15449602b2 | ||
|
|
99ef3f2b43 | ||
|
|
bacb65b584 | ||
|
|
4634281870 | ||
|
|
eafc81c938 | ||
|
|
2af53d108b | ||
|
|
ce916a5750 | ||
|
|
c827436e32 | ||
|
|
f484d345d8 | ||
|
|
d66bfb4cab | ||
|
|
f554295ab4 | ||
|
|
467306295e |
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"presets": [
|
||||
["@babel/typescript", { "jsxPragma": "h" }]
|
||||
],
|
||||
"plugins": [
|
||||
["@babel/transform-react-jsx", {
|
||||
"runtime": "automatic",
|
||||
"importSource": "preact"
|
||||
}],
|
||||
["@babel/plugin-proposal-class-properties", { "loose": true }],
|
||||
["@babel/plugin-proposal-object-rest-spread", { "loose": true,
|
||||
"useBuiltIns": true
|
||||
}],
|
||||
"@babel/plugin-proposal-optional-catch-binding",
|
||||
"@babel/plugin-proposal-optional-chaining",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator",
|
||||
"@babel/plugin-proposal-logical-assignment-operators",
|
||||
"@babel/plugin-external-helpers"
|
||||
]
|
||||
}
|
||||
@@ -15,17 +15,5 @@ trim_trailing_whitespace = true
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.json]
|
||||
indent_size = 2
|
||||
|
||||
[.eslintrc]
|
||||
indent_size = 4
|
||||
|
||||
[*.scss]
|
||||
indent_size = 2
|
||||
|
||||
[*.css]
|
||||
indent_size = 2
|
||||
|
||||
[*.nsh]
|
||||
[{*.nsh,*.yml,*.yaml,*.json}]
|
||||
indent_size = 2
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"extends": "standard",
|
||||
"rules": {
|
||||
"indent": ["error", 4, { "SwitchCase": 1 }],
|
||||
"semi": ["error", "always"],
|
||||
"one-var": "off",
|
||||
"space-before-function-paren": "off",
|
||||
"no-throw-literal": "off",
|
||||
"camelcase": ["error", { "properties": "always" }],
|
||||
"no-console": "error",
|
||||
"no-alert": "error",
|
||||
"no-debugger": "error",
|
||||
"prefer-arrow-callback": "error",
|
||||
"object-property-newline": "off",
|
||||
"no-useless-escape": "off",
|
||||
"no-var": "error",
|
||||
"prefer-const": "error",
|
||||
"no-unused-expressions": "error",
|
||||
"strict": ["error", "never"]
|
||||
},
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"impliedStrict": true
|
||||
}
|
||||
},
|
||||
"globals": {
|
||||
"_": true,
|
||||
"$": true
|
||||
},
|
||||
"env": {
|
||||
"browser": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
{
|
||||
"plugins": [
|
||||
"prettier",
|
||||
"import",
|
||||
"babel",
|
||||
"react",
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"extends": [
|
||||
"standard",
|
||||
"eslint:recommended",
|
||||
"plugin:prettier/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"one-var": "off",
|
||||
"space-before-function-paren": "off",
|
||||
"no-throw-literal": "off",
|
||||
"camelcase": [
|
||||
"error",
|
||||
{
|
||||
"properties": "always"
|
||||
}
|
||||
],
|
||||
"no-console": "error",
|
||||
"no-alert": "error",
|
||||
"no-debugger": "error",
|
||||
"prefer-arrow-callback": "error",
|
||||
"object-property-newline": "off",
|
||||
"no-useless-escape": "off",
|
||||
"no-var": "error",
|
||||
"prefer-const": "error",
|
||||
"no-unused-expressions": "off",
|
||||
"strict": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"no-mixed-operators": "off",
|
||||
"prefer-promise-reject-errors": "off",
|
||||
"standard/no-callback-literal": "off",
|
||||
"object-curly-spacing": "off",
|
||||
"quote-props": "off",
|
||||
"no-new-object": "error",
|
||||
"object-shorthand": "error",
|
||||
"no-array-constructor": "error",
|
||||
"array-callback-return": "error",
|
||||
"no-eval": "error",
|
||||
"no-new-func": "error",
|
||||
"prefer-rest-params": "error",
|
||||
"prefer-spread": "error",
|
||||
"no-useless-constructor": "error",
|
||||
"no-dupe-class-members": "error",
|
||||
"no-duplicate-imports": "error",
|
||||
"eqeqeq": "error",
|
||||
"no-unneeded-ternary": "error",
|
||||
"curly": "error",
|
||||
"prettier/prettier": "error",
|
||||
"no-empty": "off",
|
||||
"no-restricted-syntax": [
|
||||
"error",
|
||||
{
|
||||
"selector": "ExportDefaultDeclaration",
|
||||
"message": "Prefer named exports"
|
||||
}
|
||||
],
|
||||
"import/no-webpack-loader-syntax": "off",
|
||||
"import/no-relative-parent-imports": "error",
|
||||
"import/first": "error",
|
||||
"import/no-default-export": "error",
|
||||
"babel/no-unused-expressions": "error",
|
||||
"node/no-callback-literal": "off"
|
||||
},
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"impliedStrict": true
|
||||
}
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"*.ts",
|
||||
"*.tsx"
|
||||
],
|
||||
"extends": [
|
||||
"standard",
|
||||
"eslint:recommended",
|
||||
"preact",
|
||||
"plugin:prettier/recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:@typescript-eslint/recommended-requiring-type-checking",
|
||||
"plugin:import/typescript"
|
||||
],
|
||||
"rules": {
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"one-var": "off",
|
||||
"space-before-function-paren": "off",
|
||||
"no-throw-literal": "error",
|
||||
"camelcase": [
|
||||
"error",
|
||||
{
|
||||
"properties": "always"
|
||||
}
|
||||
],
|
||||
"no-console": "error",
|
||||
"no-alert": "error",
|
||||
"no-debugger": "error",
|
||||
"prefer-arrow-callback": "error",
|
||||
"object-property-newline": "off",
|
||||
"no-useless-escape": "off",
|
||||
"no-var": "error",
|
||||
"prefer-const": "error",
|
||||
"no-unused-expressions": "off",
|
||||
"strict": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"no-mixed-operators": "off",
|
||||
"prefer-promise-reject-errors": "off",
|
||||
"standard/no-callback-literal": "off",
|
||||
"object-curly-spacing": "off",
|
||||
"quote-props": "off",
|
||||
"no-new-object": "error",
|
||||
"object-shorthand": "error",
|
||||
"no-array-constructor": "error",
|
||||
"array-callback-return": "error",
|
||||
"no-eval": "error",
|
||||
"no-new-func": "error",
|
||||
"prefer-rest-params": "error",
|
||||
"prefer-spread": "error",
|
||||
"no-useless-constructor": "error",
|
||||
"no-dupe-class-members": "off",
|
||||
"no-duplicate-imports": "error",
|
||||
"eqeqeq": "error",
|
||||
"no-unneeded-ternary": "error",
|
||||
"curly": "error",
|
||||
"prettier/prettier": "error",
|
||||
"no-empty": "off",
|
||||
"no-restricted-syntax": [
|
||||
"error",
|
||||
{
|
||||
"selector": "ExportDefaultDeclaration",
|
||||
"message": "Prefer named exports"
|
||||
}
|
||||
],
|
||||
"import/no-webpack-loader-syntax": "off",
|
||||
"import/no-relative-parent-imports": "error",
|
||||
"import/first": "error",
|
||||
"import/no-default-export": "error",
|
||||
"babel/no-unused-expressions": "error",
|
||||
"node/no-callback-literal": "off",
|
||||
"import/no-commonjs": "off",
|
||||
"prefer-template": "off",
|
||||
"no-else-return": "off",
|
||||
"jest/valid-expect": "off",
|
||||
"jest/expect-expect": "off",
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
"@typescript-eslint/no-explicit-any": "error",
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "error",
|
||||
"@typescript-eslint/no-non-null-assertion": "error",
|
||||
"@typescript-eslint/no-var-requires": "off",
|
||||
"@typescript-eslint/naming-convention": [
|
||||
"error",
|
||||
{
|
||||
"selector": ["objectLiteralProperty", "classProperty", "typeProperty"],
|
||||
"modifiers": ["private", "protected"],
|
||||
"format": ["camelCase"],
|
||||
"leadingUnderscore": "require",
|
||||
"trailingUnderscore": "forbid"
|
||||
},
|
||||
{
|
||||
"selector": ["classProperty", "typeProperty"],
|
||||
"modifiers": ["public"],
|
||||
"format": ["camelCase", "PascalCase", "UPPER_CASE", "snake_case"],
|
||||
"leadingUnderscore": "forbid",
|
||||
"trailingUnderscore": "forbid"
|
||||
},
|
||||
{
|
||||
"selector": ["method"],
|
||||
"format": ["camelCase"],
|
||||
"leadingUnderscore": "forbid",
|
||||
"trailingUnderscore": "forbid"
|
||||
},
|
||||
{
|
||||
"selector": ["class", "enum"],
|
||||
"format": ["PascalCase"],
|
||||
"leadingUnderscore": "forbid",
|
||||
"trailingUnderscore": "forbid"
|
||||
},
|
||||
{
|
||||
"selector": ["enumMember"],
|
||||
"format": ["PascalCase", "UPPER_CASE"],
|
||||
"leadingUnderscore": "forbid",
|
||||
"trailingUnderscore": "forbid"
|
||||
},
|
||||
{
|
||||
"selector": ["variable"],
|
||||
"format": ["camelCase", "PascalCase", "UPPER_CASE"],
|
||||
"leadingUnderscore": "forbid",
|
||||
"trailingUnderscore": "forbid"
|
||||
}
|
||||
]
|
||||
},
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
},
|
||||
"ecmaVersion": 11,
|
||||
"sourceType": "module",
|
||||
"project": [
|
||||
"tsconfig.json",
|
||||
"jsconfig.json"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"import/parsers": {
|
||||
"@typescript-eslint/parser": [
|
||||
".ts",
|
||||
".tsx"
|
||||
]
|
||||
}
|
||||
},
|
||||
"globals": {
|
||||
"_": true,
|
||||
"$": true
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true,
|
||||
"es2020": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at antelle.net@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
|
||||
|
||||
[homepage]: http://contributor-covenant.org
|
||||
[version]: http://contributor-covenant.org/version/1/4/
|
||||
@@ -0,0 +1,12 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [keeweb, antelle]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: keeweb
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: # for example PayPal links
|
||||
@@ -1,12 +0,0 @@
|
||||
Thanks for taking the time to contribute!
|
||||
|
||||
Please, read https://github.com/keeweb/keeweb/wiki/FAQ before submitting a new issue and fill in the following (if applicable):
|
||||
|
||||
- what were your actions?
|
||||
- what was wrong?
|
||||
- app version
|
||||
- your user-agent (from Settings/Help section)
|
||||
- does it happen on Demo or New database?
|
||||
- if you are using a test db without your personal data, please attach it
|
||||
- please, open dev tools in your browser and attach output log from Console tab
|
||||
(if you are using desktop app, devtools can be opened from Settings/General/Advanced)
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve KeeWeb
|
||||
title: ''
|
||||
labels: 'bug'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Environment**
|
||||
Please copy all version information from Settings/Help
|
||||
|
||||
**Kdbx File**
|
||||
Does it happen on Demo or New database?
|
||||
If you have a test db without your personal data, please attach it.
|
||||
|
||||
**Logs**
|
||||
Please open dev tools in your browser and attach output log from Console tab. If you are using a desktop app, devtools can be opened from Settings/General/Advanced.
|
||||
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: true
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: 'enhancement'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -5,11 +5,11 @@ Please follow these steps:
|
||||
1. work on the right branch: it must be `develop` for new features and `master` for hotfixes
|
||||
2. make sure the feature you add is not listed in [banned features](https://github.com/keeweb/keeweb/wiki/Unsupported-Features)
|
||||
3. check compatibility with [the project roadmap](https://github.com/keeweb/keeweb/wiki/TODO) and key features: you should not break anything
|
||||
4. if your feature is introducing a lot of new UI, or is changing any UX or design concept, please, open an issue to discuss it first
|
||||
5. please, respect existing style in code, styles and markup (hint: install and enable [editorconfig](http://editorconfig.org/) in your editor)
|
||||
4. if your feature is introducing a lot of new UI or changing any UX or design concept, please open an issue to discuss it first
|
||||
5. please respect existing style in code, styles, and markup (hint: install and enable [editorconfig](http://editorconfig.org/) in your editor)
|
||||
6. don't add any dependencies
|
||||
7. test your code: it must work in browser, Windows, OSX and Linux
|
||||
7. test your code: it must work in all browsers, Windows, OSX, and Linux
|
||||
8. if your code is platform-dependent, please add corresponding switches
|
||||
9. respect each platform: don't create behaviour which breaks interface guidelines or user expectations
|
||||
9. respect each platform: don't create a behaviour that breaks interface guidelines or user expectations
|
||||
10. run `grunt` and make sure it's passing
|
||||
11. replace this message with actual description
|
||||
11. replace this message with the actual description
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
FROM snapcore/snapcraft:stable
|
||||
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y build-essential git-core unzip curl pkg-config rpm
|
||||
RUN curl -sL https://deb.nodesource.com/setup_15.x | sudo -E bash -
|
||||
RUN apt-get install -y nodejs
|
||||
RUN npm i -g grunt-cli
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
@@ -0,0 +1,5 @@
|
||||
name: 'Linux Build'
|
||||
description: 'Build KeeWeb on Linux in Docker'
|
||||
runs:
|
||||
using: 'docker'
|
||||
image: 'Dockerfile'
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash -e
|
||||
|
||||
cd /github/workspace
|
||||
npm ci
|
||||
grunt desktop-linux
|
||||
@@ -0,0 +1,404 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# git-restore-mtime - Change mtime of files based on commit date of last change
|
||||
#
|
||||
# Copyright (C) 2012 Rodrigo Silva (MestreLion) <linux@rodrigosilva.com>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. See <http://www.gnu.org/licenses/gpl.html>
|
||||
#
|
||||
"""
|
||||
Change the modification time (mtime) of all files in work tree, based on the
|
||||
date of the most recent commit that modified the file.
|
||||
|
||||
Useful prior to generating release tarballs, so each file is archived with a
|
||||
date that is similar to the date when the file was actually last modified,
|
||||
assuming the actual modification date and its commit date are close.
|
||||
|
||||
By default ignores all ignored and untracked files, and also refuses to work
|
||||
on trees with uncommitted changes.
|
||||
"""
|
||||
|
||||
# TODO:
|
||||
# - Add -z on git whatchanged/ls-files so we don't deal with filename decode/OS normalization
|
||||
# - When Python is bumped to 3.7, use text instead of universal_newlines on subprocess
|
||||
# - Update "Statistics for some large projects" with modern hardware and repositories.
|
||||
# - Create a README.md for git-restore-mtime alone. It deserves extensive documentation
|
||||
# - Move Statistics there
|
||||
|
||||
# FIXME:
|
||||
# - When current dir is outside the worktree, e.g. using --work-tree, `git ls-files`
|
||||
# assume any relative pathspecs are to worktree root, not the current dir. As such,
|
||||
# relative pathspecs may not work.
|
||||
# - Renames and mode changes should not change file mtime:
|
||||
# - Must check on status 'R100' and mode changes with same blobs
|
||||
# - Should require status to be (A, C, M, R<100, T). D will never be processed as
|
||||
# filelist is a subset of lsfileslist.
|
||||
# - Check file (A, D) for directory mtime is not sufficient:
|
||||
# - Renames also change dir mtime, unless rename was on a parent dir
|
||||
# - If most recent change of all files in a dir was a [M]odification,
|
||||
# dir might not be touched at all.
|
||||
# - Dirs containing only subdirectories but no direct files will also
|
||||
# not be touched. They're files' [grand]parent dir, but never their dirname().
|
||||
# - Some solutions:
|
||||
# - After files done, perform some dir processing for missing dirs, finding latest
|
||||
# file (A, D, R)
|
||||
# - Simple approach: dir mtime is most recent child (dir or file) mtime
|
||||
# - Use a virtual concept of "created at most at" to fill missing info, bubble up
|
||||
# to parents and grandparents
|
||||
# - When handling [grand]parent dirs, stay inside <pathspec>
|
||||
# - Better handling of merge commits. `-m` is plain *wrong*. `-c/--cc` is perfect, but
|
||||
# painfully slow. First pass without merge commits is not accurate. Maybe add a new
|
||||
# `--accurate` mode for `--cc`?
|
||||
|
||||
if __name__ != "__main__":
|
||||
raise ImportError("{} should not be used as a module.".format(__name__))
|
||||
|
||||
import subprocess, shlex
|
||||
import sys, os.path
|
||||
import logging
|
||||
import argparse
|
||||
import time
|
||||
|
||||
|
||||
# Update symlinks only if the OS supports not following them
|
||||
UPDATE_SYMLINKS = bool(os.utime in getattr(os, 'supports_follow_symlinks', []))
|
||||
STEPMISSING = 100
|
||||
|
||||
|
||||
# Command-line interface ######################################################
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="""Restore original modification time of files based on the date of the
|
||||
most recent commit that modified them. Useful when generating release tarballs.""")
|
||||
|
||||
group = parser.add_mutually_exclusive_group()
|
||||
group.add_argument('--quiet', '-q', dest='loglevel',
|
||||
action="store_const", const=logging.WARNING, default=logging.INFO,
|
||||
help="Suppress informative messages and summary statistics.")
|
||||
group.add_argument('--verbose', '-v', action="count",
|
||||
help="Print additional information for each processed file.")
|
||||
|
||||
parser.add_argument('--force', '-f', action="store_true",
|
||||
help="Force execution on trees with uncommitted changes.")
|
||||
|
||||
parser.add_argument('--merge', '-m', action="store_true",
|
||||
help="""Include merge commits. Leads to more recent mtimes and more files per
|
||||
commit, thus with the same mtime (which may or may not be what you want). Including
|
||||
merge commits may lead to less commits being evaluated (all files are found sooner),
|
||||
which improves performance, sometimes substantially. But since merge commits are
|
||||
usually huge, processing them may also take longer, sometimes substantially.
|
||||
By default merge logs are only used for files missing from regular commit logs.""")
|
||||
|
||||
parser.add_argument('--first-parent', action="store_true",
|
||||
help="""Consider only the first parent, the "main branch", when parsing merge
|
||||
commit logs. Only effective when merge commits are included in the log, either
|
||||
by --merge or to find missing files after first log parse. See --skip-missing.""")
|
||||
|
||||
parser.add_argument('--skip-missing', '-s',
|
||||
action="store_false", default=True, dest="missing",
|
||||
help="""Do not try to find missing files. If some files were not found in regular
|
||||
commit logs, by default it re-tries using merge commit logs for these files (if
|
||||
--merge was not already used). This option disables this behavior, which may slightly
|
||||
improve performance, but files found only in merge commits will not be updated.""")
|
||||
|
||||
parser.add_argument('--no-directories', '-D',
|
||||
action="store_false", default=True, dest='dirs',
|
||||
help="""Do not update directory mtime for files created, renamed or deleted in it.
|
||||
Note: just modifying a file will not update its directory mtime.""")
|
||||
|
||||
parser.add_argument('--test', '-t', action="store_true", default=False,
|
||||
help="Test run: do not actually update any file")
|
||||
|
||||
parser.add_argument('--commit-time', '-c',
|
||||
action='store_true', default=False, dest='commit_time',
|
||||
help="Use commit time instead of author time")
|
||||
|
||||
parser.add_argument('pathspec', nargs='*', metavar='PATH',
|
||||
help="""Only modify paths matching PATH, directories or files, relative to current
|
||||
directory. Default is to modify all files handled by git, ignoring untracked files
|
||||
and submodules.""")
|
||||
|
||||
parser.add_argument('--work-tree', dest='workdir',
|
||||
help="Path to the work tree, if not current directory or one of its parents.")
|
||||
|
||||
parser.add_argument('--git-dir', dest='gitdir',
|
||||
help="Path to the git repository, if not the default <work-tree-root>/.git")
|
||||
|
||||
parser.add_argument('--skip-older-than', metavar='SECONDS', type=int,
|
||||
help="""Do not modify files that are older than %(metavar)s.
|
||||
It can significantly improve performance if fewer files are processed.
|
||||
Useful on CI builds, which can eventually switch workspace to different branch,
|
||||
but mostly performs builds on the same one (e.g. master).
|
||||
""")
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
# Helper functions ############################################################
|
||||
|
||||
def setup_logging(args):
|
||||
TRACE = logging.DEBUG // 2
|
||||
logging.Logger.trace = lambda _, m, *a, **k: _.log(TRACE, m, *a, **k)
|
||||
level = (args.verbose and max(TRACE, logging.DEBUG // args.verbose)) or args.loglevel
|
||||
logging.basicConfig(level=level, format='%(message)s')
|
||||
return logging.getLogger()
|
||||
|
||||
|
||||
def normalize(path):
|
||||
"""Normalize paths from git, handling non-ASCII characters.
|
||||
|
||||
Git for Windows, as of v1.7.10, stores paths as UTF-8 normalization form C. If path
|
||||
contains non-ASCII or non-printable chars it outputs the UTF-8 in octal-escaped
|
||||
notation, double-quoting the whole path. Double-quotes and backslashes are also escaped.
|
||||
|
||||
https://git-scm.com/docs/git-config#Documentation/git-config.txt-corequotePath
|
||||
https://github.com/msysgit/msysgit/wiki/Git-for-Windows-Unicode-Support
|
||||
https://github.com/git/git/blob/master/Documentation/i18n.txt
|
||||
|
||||
Example on git output, this function reverts this:
|
||||
r'back\slash_double"quote_açaí' -> r'"back\\slash_double\"quote_a\303\247a\303\255"'
|
||||
"""
|
||||
if path and path[0] == '"':
|
||||
# Python 2: path = path[1:-1].decode("string-escape")
|
||||
# Python 3: https://stackoverflow.com/a/46650050/624066
|
||||
path = (path[1:-1] # Remove enclosing double quotes
|
||||
.encode('latin1') # Convert to bytes, required 'unicode-escape'
|
||||
.decode('unicode-escape') # Perform the actual octal-escaping decode
|
||||
.encode('latin1') # 1:1 mapping to bytes, forming UTF-8 encoding
|
||||
.decode('utf8')) # Decode from UTF-8
|
||||
# Make sure the slash matches the OS; for Windows we need a backslash
|
||||
return os.path.normpath(path)
|
||||
|
||||
|
||||
if UPDATE_SYMLINKS:
|
||||
def touch(path, mtime, test=False):
|
||||
"""The actual mtime update"""
|
||||
if test: return
|
||||
os.utime(path, (mtime, mtime), follow_symlinks=False)
|
||||
else:
|
||||
def touch(path, mtime, test=False):
|
||||
"""The actual mtime update"""
|
||||
if test: return
|
||||
os.utime(path, (mtime, mtime))
|
||||
|
||||
|
||||
def isodate(secs):
|
||||
return time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(secs))
|
||||
|
||||
|
||||
# Git class and parselog(), the heart of the script ###########################
|
||||
|
||||
class Git():
|
||||
def __init__(self, workdir=None, gitdir=None):
|
||||
self.gitcmd = ['git']
|
||||
if workdir: self.gitcmd.extend(('--work-tree', workdir))
|
||||
if gitdir : self.gitcmd.extend(('--git-dir', gitdir))
|
||||
self.workdir, self.gitdir = self._repodirs()
|
||||
|
||||
def ls_files(self, pathlist=None):
|
||||
return (normalize(_) for _ in self._run('ls-files --full-name', pathlist))
|
||||
|
||||
def is_dirty(self):
|
||||
return bool(self._run('diff --no-ext-diff --quiet', output=False))
|
||||
|
||||
def log(self, merge=False, first_parent=False, commit_time=False, pathlist=None):
|
||||
cmd = 'whatchanged --pretty={}'.format('%ct' if commit_time else '%at')
|
||||
if merge: cmd += ' -m'
|
||||
if first_parent: cmd += ' --first-parent'
|
||||
return self._run(cmd, pathlist)
|
||||
|
||||
def _repodirs(self):
|
||||
return (os.path.normpath(_) for _ in
|
||||
self._run('rev-parse --show-toplevel --absolute-git-dir', check=True))
|
||||
|
||||
def _run(self, cmdstr, pathlist=None, output=True, check=False):
|
||||
cmdlist = self.gitcmd + shlex.split(cmdstr)
|
||||
if pathlist:
|
||||
cmdlist.append('--')
|
||||
cmdlist.extend(pathlist)
|
||||
log.trace("Executing: %s", ' '.join(cmdlist))
|
||||
if not output:
|
||||
return subprocess.call(cmdlist)
|
||||
if check:
|
||||
try:
|
||||
stdout = subprocess.check_output(cmdlist, universal_newlines=True)
|
||||
return stdout.splitlines()
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise self.Error(e.returncode, e.cmd, e.output, e.stderr)
|
||||
self.proc = subprocess.Popen(cmdlist, stdout=subprocess.PIPE, universal_newlines=True)
|
||||
return (_.strip() for _ in self.proc.stdout)
|
||||
|
||||
class Error(subprocess.CalledProcessError): pass
|
||||
|
||||
|
||||
def parselog(filelist, dirlist, stats, git, merge=False, filterlist=None):
|
||||
mtime = 0
|
||||
for line in git.log(merge, args.first_parent, args.commit_time, filterlist):
|
||||
stats['loglines'] += 1
|
||||
|
||||
# Blank line between Date and list of files
|
||||
if not line: continue
|
||||
|
||||
# File line
|
||||
if line[0] == ':': # Faster than line.startswith(':')
|
||||
# If line describes a rename, linetok has three tokens, otherwise two
|
||||
linetok = line.split('\t')
|
||||
status = linetok[0]
|
||||
file = linetok[-1]
|
||||
|
||||
# Handles non-ASCII chars and OS path separator
|
||||
file = normalize(file)
|
||||
|
||||
if file in filelist:
|
||||
stats['files'] -= 1
|
||||
log.debug("%d\t%d\t%d\t%s\t%s",
|
||||
stats['loglines'], stats['commits'], stats['files'],
|
||||
isodate(mtime), file)
|
||||
filelist.remove(file)
|
||||
try:
|
||||
touch(os.path.join(git.workdir, file), mtime, args.test)
|
||||
stats['touches'] += 1
|
||||
except Exception as e:
|
||||
log.error("ERROR: %s", e)
|
||||
stats['errors'] += 1
|
||||
|
||||
if args.dirs:
|
||||
dirname = os.path.dirname(file)
|
||||
if status[-1] in ('A', 'D') and dirname in dirlist:
|
||||
log.debug("%d\t%d\t-\t%s\t%s",
|
||||
stats['loglines'], stats['commits'],
|
||||
isodate(mtime), "{}/".format(dirname or '.'))
|
||||
dirlist.remove(dirname)
|
||||
try:
|
||||
touch(os.path.join(git.workdir, dirname), mtime, args.test)
|
||||
stats['dirtouches'] += 1
|
||||
except Exception as e:
|
||||
log.error("ERROR: %s", e)
|
||||
stats['direrrors'] += 1
|
||||
|
||||
# Date line
|
||||
else:
|
||||
stats['commits'] += 1
|
||||
mtime = int(line)
|
||||
|
||||
# All files done?
|
||||
if not stats['files']:
|
||||
git.proc.terminate() # hackish, but does the job. Not needed anyway
|
||||
return
|
||||
|
||||
|
||||
# Main Logic ##################################################################
|
||||
|
||||
def main():
|
||||
start = time.time() # yes, Wall time. CPU time is not realistic for users.
|
||||
stats = {_: 0 for _ in ('loglines', 'commits', 'touches', 'errors', 'dirtouches', 'direrrors')}
|
||||
|
||||
# First things first: Where and Who are we?
|
||||
try:
|
||||
git = Git(args.workdir, args.gitdir)
|
||||
except Git.Error as e:
|
||||
# Not in a git repository, and git already informed user on stderr. So we just...
|
||||
return e.returncode
|
||||
|
||||
# Do not work on dirty repositories, unless --force
|
||||
if not args.force and git.is_dirty():
|
||||
log.critical(
|
||||
"ERROR: There are local changes in the working directory.\n"
|
||||
"This could lead to undesirable results for modified files.\n"
|
||||
"Please, commit your changes (or use --force) and try again.\n"
|
||||
"Aborting")
|
||||
return 1
|
||||
|
||||
# Get the files managed by git and build file and dir list to be processed
|
||||
filelist = set()
|
||||
dirlist = set()
|
||||
if UPDATE_SYMLINKS and not args.skip_older_than:
|
||||
filelist = set(git.ls_files(args.pathspec))
|
||||
dirlist = set(os.path.dirname(_) for _ in filelist)
|
||||
else:
|
||||
for path in git.ls_files(args.pathspec):
|
||||
fullpath = os.path.join(git.workdir, path)
|
||||
|
||||
# Symlink (to file, to dir or broken - git handles the same way)
|
||||
if not UPDATE_SYMLINKS and os.path.islink(fullpath):
|
||||
log.warning("WARNING: Skipping symlink, OS does not support update: %s", path)
|
||||
continue
|
||||
|
||||
# skip files which are older than given threshold
|
||||
if args.skip_older_than and start - os.path.getmtime(fullpath) > args.skip_older_than:
|
||||
continue
|
||||
|
||||
# Always add them relative to worktree root
|
||||
filelist.add(path)
|
||||
dirlist.add(os.path.dirname(path))
|
||||
|
||||
stats['totalfiles'] = stats['files'] = len(filelist)
|
||||
log.info("{0:,} files to be processed in work dir".format(stats['totalfiles']))
|
||||
|
||||
if not filelist:
|
||||
# Nothing to do. Exit silently and without errors, just like git does
|
||||
return
|
||||
|
||||
# Process the log until all files are 'touched'
|
||||
log.debug("Line #\tLog #\tF.Left\tModification Time\tFile Name")
|
||||
parselog(filelist, dirlist, stats, git, args.merge, args.pathspec)
|
||||
|
||||
# Missing files
|
||||
if filelist:
|
||||
# Try to find them in merge logs, if not done already
|
||||
# (usually HUGE, thus MUCH slower!)
|
||||
if args.missing and not args.merge:
|
||||
filterlist = list(filelist)
|
||||
for i in range(0, len(filterlist), STEPMISSING):
|
||||
parselog(filelist, dirlist, stats, git,
|
||||
merge=True, filterlist=filterlist[i:i+STEPMISSING])
|
||||
|
||||
# Still missing some?
|
||||
for file in filelist:
|
||||
log.warning("WARNING: not found in log: %s", file)
|
||||
|
||||
# Final statistics
|
||||
# Suggestion: use git-log --before=mtime to brag about skipped log entries
|
||||
log.info(
|
||||
"Statistics:\n"
|
||||
"{:13,.2f} seconds\n"
|
||||
"{:13,} log lines processed\n"
|
||||
"{:13,} commits evaluated"
|
||||
"".format(time.time()-start, stats['loglines'], stats['commits']))
|
||||
|
||||
if args.dirs:
|
||||
if stats['direrrors']: log.info("{:13,} directory update errors".format(stats['direrrors']))
|
||||
log.info("{:13,} directories updated".format(stats['dirtouches']))
|
||||
|
||||
if stats['touches'] != stats['totalfiles']: log.info("{:13,} files".format(stats['totalfiles']))
|
||||
if stats['files']: log.info("{:13,} files missing".format(stats['files']))
|
||||
if stats['errors']: log.info("{:13,} file update errors".format(stats['errors']))
|
||||
|
||||
log.info("{:13,} files updated".format(stats['touches']))
|
||||
|
||||
if args.test:
|
||||
log.info("TEST RUN - No files modified!")
|
||||
|
||||
|
||||
args = parse_args()
|
||||
log = setup_logging(args)
|
||||
log.trace("Arguments: %s", args)
|
||||
|
||||
# UI done, it's show time!
|
||||
try:
|
||||
sys.exit(main())
|
||||
except KeyboardInterrupt:
|
||||
log.info("Aborting")
|
||||
sys.exit(-1)
|
||||
@@ -0,0 +1,424 @@
|
||||
name: Build
|
||||
on:
|
||||
push:
|
||||
tags: [ '*' ]
|
||||
jobs:
|
||||
|
||||
web:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get current git tag
|
||||
id: get_tag
|
||||
uses: keeweb/get-git-tag@v3.0.2
|
||||
with:
|
||||
tagRegex: "^v(\\d+\\.\\d+\\.\\d+)$"
|
||||
tagRegexGroup: 1
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: keeweb/keeweb
|
||||
ref: ${{ github.repository == 'keeweb/keeweb' && github.sha || 'develop' }}
|
||||
- name: Upgrade Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- name: Install npm modules
|
||||
run: npm ci
|
||||
- name: Lint
|
||||
run: npm lint
|
||||
- name: Test
|
||||
run: npm test
|
||||
- name: Build
|
||||
run: npm start
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.html
|
||||
path: dist
|
||||
- name: Write secrets
|
||||
env:
|
||||
VIRUS_TOTAL: ${{ secrets.VIRUS_TOTAL }}
|
||||
run: |
|
||||
mkdir keys
|
||||
echo "$VIRUS_TOTAL" > keys/virus-total.json
|
||||
- name: Check on VirusTotal
|
||||
run: npm start virustotal
|
||||
if: ${{ github.repository == 'keeweb/keeweb' }}
|
||||
|
||||
linux:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- web
|
||||
steps:
|
||||
- name: Get current git tag
|
||||
id: get_tag
|
||||
uses: keeweb/get-git-tag@v3.0.2
|
||||
with:
|
||||
tagRegex: "^v(\\d+\\.\\d+\\.\\d+)$"
|
||||
tagRegexGroup: 1
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: keeweb/keeweb
|
||||
ref: ${{ github.repository == 'keeweb/keeweb' && github.sha || 'develop' }}
|
||||
- name: Download artifact
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.html
|
||||
path: dist
|
||||
- name: Write secrets
|
||||
env:
|
||||
PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }}
|
||||
KEEWEB_SIGN: ${{ secrets.KEEWEB_SIGN }}
|
||||
run: |
|
||||
mkdir keys
|
||||
echo "$PRIVATE_KEY" > keys/private-key.pem
|
||||
echo "$KEEWEB_SIGN" > keys/keeweb-sign.json
|
||||
- name: Build in Docker
|
||||
uses: ./.github/actions/linux-build
|
||||
- name: Upload AppImage artifact
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.AppImage
|
||||
path: dist/desktop/KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.AppImage
|
||||
- name: Upload snap artifact
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.snap
|
||||
path: dist/desktop/KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.snap
|
||||
- name: Upload deb artifact
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x64.deb
|
||||
path: dist/desktop/KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x64.deb
|
||||
- name: Upload zip artifact
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x64.zip
|
||||
path: dist/desktop/KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x64.zip
|
||||
- name: Upload rpm artifact
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x86_64.rpm
|
||||
path: dist/desktop/KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x86_64.rpm
|
||||
|
||||
darwin:
|
||||
runs-on: macos-latest
|
||||
needs:
|
||||
- web
|
||||
steps:
|
||||
- name: Get current git tag
|
||||
id: get_tag
|
||||
uses: keeweb/get-git-tag@v3.0.2
|
||||
with:
|
||||
tagRegex: "^v(\\d+\\.\\d+\\.\\d+)$"
|
||||
tagRegexGroup: 1
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: keeweb/keeweb
|
||||
ref: ${{ github.repository == 'keeweb/keeweb' && github.sha || 'develop' }}
|
||||
- name: Download artifact
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.html
|
||||
path: dist
|
||||
- name: Upgrade Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- name: Install npm modules
|
||||
run: npm ci
|
||||
- name: Write secrets
|
||||
env:
|
||||
CODESIGN: ${{ secrets.CODESIGN }}
|
||||
APPLE_DEPLOY_PASSWORD: ${{ secrets.APPLE_DEPLOY_PASSWORD }}
|
||||
APPLE_ID_USERNAME: ${{ secrets.APPLE_ID_USERNAME }}
|
||||
APPLE_PROVISIONING_PROFILE: ${{ secrets.APPLE_PROVISIONING_PROFILE }}
|
||||
run: |
|
||||
mkdir keys
|
||||
echo "$CODESIGN" > keys/codesign.json
|
||||
xcrun altool --store-password-in-keychain-item "AC_PASSWORD" -u "$APPLE_ID_USERNAME" -p "$APPLE_DEPLOY_PASSWORD"
|
||||
echo "$APPLE_PROVISIONING_PROFILE" | base64 -d > keys/keeweb.provisionprofile
|
||||
- name: Import certificates
|
||||
uses: keeweb/import-codesign-certs@v1
|
||||
with:
|
||||
p12-file-base64: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
- name: Build
|
||||
run: npm start -- --max-old-space-size=4096 desktop-darwin ${{ github.repository != 'keeweb/keeweb' && '--skip-sign' || '' }}
|
||||
- name: Upload x64 dmg artifact
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.mac.x64.dmg
|
||||
path: dist/desktop/KeeWeb-${{ steps.get_tag.outputs.tag }}.mac.x64.dmg
|
||||
- name: Upload arm64 dmg artifact
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.mac.arm64.dmg
|
||||
path: dist/desktop/KeeWeb-${{ steps.get_tag.outputs.tag }}.mac.arm64.dmg
|
||||
|
||||
win32:
|
||||
runs-on: windows-latest
|
||||
needs:
|
||||
- web
|
||||
steps:
|
||||
- name: Get current git tag
|
||||
id: get_tag
|
||||
uses: keeweb/get-git-tag@v3.0.2
|
||||
with:
|
||||
tagRegex: "^v(\\d+\\.\\d+\\.\\d+)$"
|
||||
tagRegexGroup: 1
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: keeweb/keeweb
|
||||
ref: ${{ github.repository == 'keeweb/keeweb' && github.sha || 'develop' }}
|
||||
- name: Download artifact
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.html
|
||||
path: dist
|
||||
- name: Upgrade Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- name: Install npm modules
|
||||
run: npm ci
|
||||
- name: Write secrets
|
||||
env:
|
||||
CODESIGN: ${{ secrets.CODESIGN }}
|
||||
MS_CODESIGN_PRIVATE_KEY_CLIENT: ${{ secrets.MS_CODESIGN_PRIVATE_KEY_CLIENT }}
|
||||
MS_CODESIGN_PUBLIC_KEY_CLIENT: ${{ secrets.MS_CODESIGN_PUBLIC_KEY_CLIENT }}
|
||||
MS_CODESIGN_PUBLIC_KEY_SERVER: ${{ secrets.MS_CODESIGN_PUBLIC_KEY_SERVER }}
|
||||
run: |
|
||||
mkdir keys
|
||||
echo $Env:CODESIGN > keys/codesign.json
|
||||
mkdir keys/code-signing
|
||||
echo $Env:MS_CODESIGN_PRIVATE_KEY_CLIENT > keys/code-signing/private-key-client.pem
|
||||
echo $Env:MS_CODESIGN_PUBLIC_KEY_CLIENT > keys/code-signing/public-key-client.pem
|
||||
echo $Env:MS_CODESIGN_PUBLIC_KEY_SERVER > keys/code-signing/public-key-server.pem
|
||||
- name: Build
|
||||
run: npm start -- desktop-win32 ${{ github.repository != 'keeweb/keeweb' && '--skip-sign' || '' }}
|
||||
- name: Upload ia32 exe artifact
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.ia32.exe
|
||||
path: dist/desktop/KeeWeb-${{ steps.get_tag.outputs.tag }}.win.ia32.exe
|
||||
- name: Upload ia32 zip artifact
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.ia32.zip
|
||||
path: dist/desktop/KeeWeb-${{ steps.get_tag.outputs.tag }}.win.ia32.zip
|
||||
- name: Upload x64 exe artifact
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.x64.exe
|
||||
path: dist/desktop/KeeWeb-${{ steps.get_tag.outputs.tag }}.win.x64.exe
|
||||
- name: Upload x64 zip artifact
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.x64.zip
|
||||
path: dist/desktop/KeeWeb-${{ steps.get_tag.outputs.tag }}.win.x64.zip
|
||||
- name: Upload arm64 exe artifact
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.arm64.exe
|
||||
path: dist/desktop/KeeWeb-${{ steps.get_tag.outputs.tag }}.win.arm64.exe
|
||||
- name: Upload arm64 zip artifact
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.arm64.zip
|
||||
path: dist/desktop/KeeWeb-${{ steps.get_tag.outputs.tag }}.win.arm64.zip
|
||||
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- linux
|
||||
- darwin
|
||||
- win32
|
||||
steps:
|
||||
- name: Get current git tag
|
||||
id: get_tag
|
||||
uses: keeweb/get-git-tag@v3.0.2
|
||||
with:
|
||||
tagRegex: "^v(\\d+\\.\\d+\\.\\d+)$"
|
||||
tagRegexGroup: 1
|
||||
- name: Setup GCloud
|
||||
uses: google-github-actions/setup-gcloud@master
|
||||
with:
|
||||
version: '285.0.0'
|
||||
service_account_key: ${{ secrets.GCP_SA_KEY }}
|
||||
export_default_credentials: true
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: keeweb/keeweb
|
||||
path: keeweb
|
||||
ref: ${{ github.repository == 'keeweb/keeweb' && github.sha || 'develop' }}
|
||||
- name: Upgrade Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- name: Install npm modules
|
||||
working-directory: keeweb
|
||||
run: npm ci
|
||||
- name: Download html artifact
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.html
|
||||
path: html
|
||||
- name: Download linux.AppImage artifact
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.AppImage
|
||||
path: assets
|
||||
- name: Download linux.snap artifact
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.snap
|
||||
path: assets
|
||||
- name: Download linux.deb artifact
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x64.deb
|
||||
path: assets
|
||||
- name: Download linux.zip artifact
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x64.zip
|
||||
path: assets
|
||||
- name: Download linux.rpm artifact
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x86_64.rpm
|
||||
path: assets
|
||||
- name: Download darwin.x64.dmg artifact
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.mac.x64.dmg
|
||||
path: assets
|
||||
- name: Download darwin.arm64.dmg artifact
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.mac.arm64.dmg
|
||||
path: assets
|
||||
- name: Download win32.ia32.exe artifact
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.ia32.exe
|
||||
path: assets
|
||||
- name: Download win32.ia32.zip artifact
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.ia32.zip
|
||||
path: assets
|
||||
- name: Download win32.x64.exe artifact
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.x64.exe
|
||||
path: assets
|
||||
- name: Download win32.x64.zip artifact
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.x64.zip
|
||||
path: assets
|
||||
- name: Download win32.arm64.exe artifact
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.arm64.exe
|
||||
path: assets
|
||||
- name: Download win32.arm64.zip artifact
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.arm64.zip
|
||||
path: assets
|
||||
- name: Zip html
|
||||
working-directory: html
|
||||
run: zip -vr ../assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.html.zip .
|
||||
- name: Copy assets to dist
|
||||
run: mkdir -p keeweb/dist/desktop && cp assets/* keeweb/dist/desktop
|
||||
- name: Write secrets
|
||||
env:
|
||||
PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }}
|
||||
KEEWEB_SIGN: ${{ secrets.KEEWEB_SIGN }}
|
||||
working-directory: keeweb
|
||||
run: |
|
||||
mkdir keys
|
||||
echo "$PRIVATE_KEY" > keys/private-key.pem
|
||||
echo "$KEEWEB_SIGN" > keys/keeweb-sign.json
|
||||
- name: Build
|
||||
working-directory: keeweb
|
||||
run: npm start finish-release
|
||||
- name: Copy signatures to assets
|
||||
run: cp keeweb/dist/desktop/Verify.sign.sha256 assets
|
||||
- name: Copy checksums to assets
|
||||
run: cp keeweb/dist/desktop/Verify.sha256 assets
|
||||
- name: Login to DockerHub Registry
|
||||
env:
|
||||
DOCKERHUB_ACCESS_TOKEN: ${{ secrets.DOCKERHUB_ACCESS_TOKEN }}
|
||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
run: echo $DOCKERHUB_ACCESS_TOKEN | docker login -u $DOCKERHUB_USERNAME --password-stdin
|
||||
- name: Copy dist to the Docker context
|
||||
run: cp -r html keeweb/package/docker/dist
|
||||
- name: Build the Docker image
|
||||
working-directory: keeweb
|
||||
run: docker build -t antelle/keeweb:latest -t antelle/keeweb:${{ steps.get_tag.outputs.tag }} package/docker
|
||||
- name: Push the Docker image to dockerhub
|
||||
if: ${{ github.repository == 'keeweb/keeweb' }}
|
||||
run: docker push --all-tags antelle/keeweb
|
||||
- name: Extract release notes
|
||||
run: node util/extract-release-notes.js --version=${{ steps.get_tag.outputs.tag }} --output=tmp/release-body.md
|
||||
working-directory: keeweb
|
||||
- name: Publish GitHub release
|
||||
uses: softprops/action-gh-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: v${{ steps.get_tag.outputs.tag }}
|
||||
name: Desktop apps v${{ steps.get_tag.outputs.tag }}
|
||||
files: assets/*
|
||||
body_path: keeweb/tmp/release-body.md
|
||||
- name: Checkout gh-pages
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: gh-pages
|
||||
path: gh-pages
|
||||
fetch-depth: 0
|
||||
- name: Commit dist to gh-pages
|
||||
working-directory: gh-pages
|
||||
run: |
|
||||
git rm -r '*'
|
||||
cp -r ../html/* .
|
||||
mkdir -p .github/workflows
|
||||
echo $GITHUB_SHA > build.txt
|
||||
date >> build.txt
|
||||
git add .
|
||||
git config --local user.email "action@github.com"
|
||||
git config --local user.name "GitHub Action"
|
||||
git commit -am v${{ steps.get_tag.outputs.tag }}
|
||||
- name: Push gh-pages
|
||||
uses: keeweb/github-push-action@master
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
branch: gh-pages
|
||||
directory: gh-pages
|
||||
- name: Restore git mtime
|
||||
working-directory: gh-pages
|
||||
run: python3 ../keeweb/.github/actions/scripts/git-restore-mtime.py
|
||||
- name: Sync the website
|
||||
if: ${{ github.repository == 'keeweb/keeweb' }}
|
||||
run: gsutil -m rsync -r -d -x "^\." gh-pages gs://app.keeweb.info/
|
||||
- name: Purge CloudFlare cache
|
||||
if: ${{ github.repository == 'keeweb/keeweb' }}
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
run: |
|
||||
curl -sS -X POST "https://api.cloudflare.com/client/v4/zones/dbda8e7e454d23edb08d4bc3b3f6f872/purge_cache" \
|
||||
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data '{"purge_everything":true}'
|
||||
@@ -2,11 +2,9 @@ workspace.xml
|
||||
misc.xml
|
||||
.DS_Store
|
||||
node_modules/
|
||||
bower_components/
|
||||
*.log
|
||||
dist/
|
||||
/dist/
|
||||
tmp/
|
||||
build/
|
||||
coverage/
|
||||
keys
|
||||
*.user
|
||||
@@ -16,3 +14,6 @@ obj/
|
||||
xcuserdata/
|
||||
*.suo
|
||||
.idea/
|
||||
.vscode/
|
||||
*.iml
|
||||
test/dist/
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"tabWidth": 4,
|
||||
"singleQuote": true,
|
||||
"printWidth": 100,
|
||||
"trailingComma": "none",
|
||||
"quoteProps": "preserve",
|
||||
"endOfLine": "auto"
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
# Contributing to KeeWeb
|
||||
|
||||
Thanks for taking the time to contribute! :gift:
|
||||
|
||||
There are different ways to help the project, you will find a summary here.
|
||||
|
||||
## Code of conduct
|
||||
|
||||
This project and everyone participating in it is governed by the [KeeWeb Code of Conduct](https://github.com/keeweb/keeweb/blob/master/.github/CODE_OF_CONDUCT.md#readme).
|
||||
By participating, you are expected to uphold this code. Please report unacceptable behavior to antelle.net@gmail.com.
|
||||
|
||||
## Issues
|
||||
|
||||
First, let's check if it's [already there](https://github.com/keeweb/keeweb/issues).
|
||||
Perhaps, someone has already reported it?
|
||||
|
||||
[Click here](https://github.com/keeweb/keeweb/issues/new/choose) to create a new issue.
|
||||
When creating issues, please answer some questions asked on that page.
|
||||
They will help to understand, reproduce, and fix your issue.
|
||||
|
||||
## Vulnerabilities
|
||||
|
||||
We would appreciate [responsible disclosure](https://en.wikipedia.org/wiki/Responsible_disclosure).
|
||||
If you would like to report a vulnerability, the preferred way to do so is [contacting the developer directly](mailto:antelle.net@gmail.com).
|
||||
|
||||
## Questions, great ideas, etc...
|
||||
|
||||
You can [create an issue](https://github.com/keeweb/keeweb/issues/new/choose) to discuss any topic.
|
||||
Indeed, questions on that page are not applicable in this case.
|
||||
|
||||
## Development
|
||||
|
||||
We don't accept (and in foreseeable future, we won't accept) anyone as a project collaborator directly.
|
||||
Instead, the code should be contributed via pull requests. To get started with development, take a look at these pages:
|
||||
- [Architecture](https://github.com/keeweb/keeweb/wiki/Architecture)
|
||||
- [Engineering](https://github.com/keeweb/keeweb/wiki/Engineering)
|
||||
- [Unsupported Features](https://github.com/keeweb/keeweb/wiki/Unsupported%20Features)
|
||||
- [Pull Request Guidelines](https://github.com/keeweb/keeweb/blob/master/.github/PULL_REQUEST_TEMPLATE.md#readme)
|
||||
|
||||
## Pull requests
|
||||
|
||||
Please follow [this guide](https://github.com/keeweb/keeweb/blob/master/.github/PULL_REQUEST_TEMPLATE.md#readme).
|
||||
|
||||
## Docs (Wiki pages)
|
||||
|
||||
KeeWeb has [Wiki pages](https://github.com/keeweb/keeweb/wiki), however it's not possible to contribute with pull requests.
|
||||
If you would like to improve it, please [open an issue](https://github.com/keeweb/keeweb/issues/new/choose) and submit your changes in Markdown format.
|
||||
|
||||
## Donations
|
||||
|
||||
There are two ways of donating:
|
||||
- join as backer on [OpenCollective](https://opencollective.com/keeweb#support);
|
||||
- sponsor the developer directly on [GitHub](https://github.com/sponsors/antelle).
|
||||
|
||||
Please note: donation does not imply any type of service contract.
|
||||
|
||||
## Translations
|
||||
|
||||
All KeeWeb translations are contributed by the community. To improve a translation or add a missing one,
|
||||
join us on [OneSky](https://keeweb.oneskyapp.com).
|
||||
|
||||
## PR
|
||||
|
||||
We would be happy to support you if you decide to post, blog, or review the project,
|
||||
or provide any other kind of PR. Please [contact the developer](mailto:antelle.net@gmail.com)
|
||||
if you have anything to share. You can also poke us on [Twitter](https://twitter.com/kee_web).
|
||||
@@ -0,0 +1,24 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Antelle https://antelle.net
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
Licences of dependencies can be found here:
|
||||
https://raw.githubusercontent.com/keeweb/keeweb/master/DEPS-LICENSE
|
||||
@@ -1,24 +0,0 @@
|
||||
Copyright (C) 2017 Antelle
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
======================================================================
|
||||
|
||||
Dependencies licenses can be found in LICENSES-DEPS.txt
|
||||
@@ -3,25 +3,25 @@
|
||||
This webapp is a browser and desktop password manager compatible with KeePass databases. It doesn't require any server or additional resources.
|
||||
The app can run either in browser, or as a desktop app.
|
||||
|
||||

|
||||

|
||||
|
||||
# Quick Links
|
||||
## Quick Links
|
||||
|
||||
Apps: [Web](https://app.keeweb.info/), [Desktop](https://github.com/keeweb/keeweb/releases/latest)
|
||||
Timeline: [Release Notes](release-notes.md), [TODO](https://github.com/keeweb/keeweb/wiki/TODO)
|
||||
On one page: [Features](https://keeweb.info/#features), [FAQ](https://github.com/keeweb/keeweb/wiki/FAQ)
|
||||
Website: [keeweb.info](https://keeweb.info)
|
||||
Twitter: [kee_web](https://twitter.com/kee_web)
|
||||
Donate: [OpenCollective](https://opencollective.com/keeweb#support), [GitHub](https://github.com/sponsors/antelle)
|
||||
|
||||
# Status
|
||||
## Status
|
||||
|
||||
The app is already rather stable, so basic stuff should work.
|
||||
Project roadmap with planned features and approximate schedule is on [TODO](https://github.com/keeweb/keeweb/wiki/TODO) page.
|
||||
The app is quite stable now. Basic stuff, as well as more advanced operations, should be rather reliable.
|
||||
|
||||
# Self-hosting
|
||||
## Self-hosting
|
||||
|
||||
Everything you need to host this app on your server is any static file server. The app is a single HTML file + cache manifest (optionally; for offline access).
|
||||
You can download the latest distribution files from [gh-pages](https://github.com/keeweb/keeweb/tree/gh-pages) branch.
|
||||
Everything you need to host this app on your server is any static file server. The app is a single HTML file + a service worker (optionally; for offline access).
|
||||
You can download the latest distribution files from [gh-pages](https://github.com/keeweb/keeweb/archive/gh-pages.zip) branch.
|
||||
|
||||
If you are using Docker:
|
||||
|
||||
@@ -33,42 +33,68 @@ docker run --name keeweb -d -p 443:443 -p 80:80 -v $EXT_DIR:/etc/nginx/external/
|
||||
|
||||
To make Dropbox work in your self-hosted app, [go to this Wiki page](https://github.com/keeweb/keeweb/wiki/Dropbox-and-GDrive).
|
||||
|
||||
# Building
|
||||
## Building
|
||||
|
||||
The app can be built with grunt: `grunt` (html file will be in `dist/`).
|
||||
Desktop apps are built with `grunt desktop`. This works only in mac osx as it builds dmg; requires wine.
|
||||
To run Electron app without building installer, install electron package (`npm install electron -g`), build the app with `grunt` and start in this way:
|
||||
The easiest way to clone all KeeWeb repos is:
|
||||
```bash
|
||||
$ grunt dev
|
||||
$ npm run-script electron
|
||||
curl https://raw.githubusercontent.com/keeweb/keeweb/develop/dev-env.sh | bash -
|
||||
```
|
||||
|
||||
The app can be built with grunt: `grunt` (html files will be in `dist/`).
|
||||
Desktop apps are built with `grunt desktop`. This requires some magic and currently works only on CI,
|
||||
you can find more details in [the GitHub Actions workflow](.github/workflows/build.yaml).
|
||||
|
||||
To run the desktop (electron) app in development mode, start it this way:
|
||||
```bash
|
||||
npm run dev
|
||||
npm run electron
|
||||
```
|
||||
|
||||
For debug build:
|
||||
|
||||
1. run `grunt dev`
|
||||
2. open `http://localhost:8085/tmp`
|
||||
1. run `npm run dev`
|
||||
2. open `http://localhost:8085`
|
||||
|
||||
# Contributing
|
||||
To build desktop apps, use these goals, the result can be found in `tmp`:
|
||||
|
||||
Please, read contribution guidelines: [for issues](.github/ISSUE_TEMPLATE.md) and [for pull requests](.github/PULL_REQUEST_TEMPLATE.md).
|
||||
For pull requests: branch is important! `master` is only for hotfixes, `develop` is for new features.
|
||||
Here's a [list of issues](https://github.com/keeweb/keeweb/labels/need%20help) which need help.
|
||||
```
|
||||
npm run dev-desktop-macos
|
||||
npm run dev-desktop-windows
|
||||
npm run dev-desktop-linux
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Please read contribution guidelines [for pull requests](.github/PULL_REQUEST_TEMPLATE.md).
|
||||
Here's a [list of issues](https://github.com/keeweb/keeweb/labels/help%20wanted) where your help would be very welcome.
|
||||
Also you can help by [translating KeeWeb](https://keeweb.oneskyapp.com) to your language.
|
||||
|
||||
### Important notes for pull requests
|
||||
Other ways of contribution can be found [on this page](CONTRIBUTING.md).
|
||||
|
||||
#### Important notes for pull requests
|
||||
|
||||
- please branch from `develop`, not `master`
|
||||
- don't edit translation files except base.json, they will be replaced
|
||||
|
||||
# Donations
|
||||
## Donations
|
||||
|
||||
KeeWeb is not free to develop. It takes time, requires paid code signing certificates and domains.
|
||||
You can help the project or say "thank you" with this button:
|
||||
[](https://www.paypal.me/dvitkovsky)
|
||||
[<img src="https://opencollective.com/keeweb/tiers/backer.svg?avatarHeight=42&width=880" alt="OpenCollective">](https://opencollective.com/keeweb#support)
|
||||
|
||||
You can also sponsor the developer directly [on GitHub](https://github.com/sponsors/antelle).
|
||||
|
||||
Please note: donation does not imply any type of service contract.
|
||||
|
||||
## Thank you
|
||||
|
||||
# License
|
||||
Notable contributions to KeeWeb:
|
||||
|
||||
[MIT](https://github.com/keeweb/keeweb/blob/master/LICENSE.txt)
|
||||
- Florian Reuschel ([@Loilo](https://github.com/Loilo)): [German translation](https://keeweb.oneskyapp.com/collaboration/translate/project/project/173183/language/550)
|
||||
- Dennis Ploeger ([@dploeger](https://github.com/dploeger)): [auto-type improvements](https://github.com/keeweb/keeweb/pulls?q=is%3Apr+is%3Aclosed+author%3Adploeger)
|
||||
- Hackmanit ([hackmanit.de](https://www.hackmanit.de)): [penetration test](https://www.hackmanit.de/en/blog-en/104-pro-bono-penetration-test-keeweb)
|
||||
- Peter Bittner ([@bittner](https://github.com/bittner)): [Wikipedia article](https://en.wikipedia.org/wiki/KeeWeb)
|
||||
|
||||
## License
|
||||
|
||||
[MIT](https://github.com/keeweb/keeweb/blob/master/LICENSE)
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Only the latest version of the product is supported. This version can be accessed at https://app.keeweb.info
|
||||
and downloaded from https://keeweb.info or GitHub releases ([latest release link](https://github.com/keeweb/keeweb/releases/latest)).
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
We would appreciate [responsible disclosure](https://en.wikipedia.org/wiki/Responsible_disclosure).
|
||||
If you would like to report a vulnerability, the preferred way to do so is [contacting the developer directly](mailto:antelle.net@gmail.com).
|
||||
@@ -0,0 +1,28 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Not Found</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, 'BlinkMacSystemFont', 'Raleway', 'Helvetica Neue',
|
||||
'Helvetica', 'Arial', sans-serif;
|
||||
font-feature-settings: 'liga' 0;
|
||||
text-align: center;
|
||||
background: #f5f5f5;
|
||||
font-weight: 300;
|
||||
}
|
||||
h1 {
|
||||
font-size: 3em;
|
||||
margin-top: 10vh;
|
||||
}
|
||||
p {
|
||||
line-height: 1.5em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>404 Not Found</h1>
|
||||
<p>Sorry, the page you're looking for was not found.</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>KeeWeb</title>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
window.opener.postMessage(
|
||||
{ storage: 'dropbox', search: location.search },
|
||||
window.location.origin
|
||||
);
|
||||
window.close();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>KeeWeb</title>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
window.opener.postMessage(
|
||||
{ storage: 'gdrive', search: location.search },
|
||||
window.location.origin
|
||||
);
|
||||
window.close();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>KeeWeb</title>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
window.opener.postMessage(
|
||||
{ storage: 'onedrive', search: location.search },
|
||||
window.location.origin
|
||||
);
|
||||
window.close();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
Before Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 685 B |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-64 -64 640 640"><path d="M136.736 512c-4.892-.008-10.322-2.061-15.92-4.997-11.941-6.265-20.13-14.185-13.293-27.218l166.101-316.638c-1.972-1.022-3.619-1.88-4.743-2.469-59.892-31.418-86.463-77.87-64.952-118.877 24.726-47.135 101.304-55.69 171.042-19.107 69.738 36.584 106.227 104.451 81.501 151.586-21.264 40.536-76.784 44.151-132.61 14.866-1.538-.807-4.033-2.103-6.857-3.567L150.768 502.471c-3.632 6.924-8.488 9.538-14.032 9.529zm-30.582-67.835l-3.144-1.649-.7-.366-42.546-22.32v-.001l-10.256-5.38c-1.738-.911-2.407-3.057-1.496-4.794l13.465-25.667c.906-1.728 3.036-2.399 4.768-1.509l.028.013 20.513 10.761c1.736.911 3.883.241 4.794-1.496l13.466-25.667c.911-1.737.24-3.883-1.497-4.794l-10.256-5.382-10.256-5.38c-1.737-.911-2.406-3.057-1.495-4.794l13.464-25.669c.911-1.737 3.058-2.406 4.794-1.494l10.258 5.38 26.697 14.005 16.548 8.681c.007.003.014.007.021.011l3.125 1.639-14.142 26.957-.973 1.855c0 .001-.001.003-.002.004l-20.063 38.245-2.041 3.888-11.424 21.779c-.002.004-.005.009-.008.014zm67.232-128.162l-3.145-1.651-14.938-7.837-11.794-6.187c-1.737-.911-2.408-3.057-1.497-4.794l13.466-25.667c.911-1.737 3.058-2.408 4.794-1.497l11.795 6.188 14.939 7.837c.007.003.014.007.021.01l3.125 1.64-1.651 3.145-6.731 12.834-6.733 12.833c-.003.005-.006.008-.009.013zm219.778-151.914c8.857.016 15.784-3.552 20.386-12.326 14.027-26.737-11.92-67.987-57.953-92.135-46.032-24.148-94.719-22.049-108.746 4.688-14.026 26.737 24.987 44.293 71.02 68.441 30.928 16.224 57.156 31.296 75.293 31.332z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1013 KiB |
|
After Width: | Height: | Size: 960 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 322 KiB |
|
After Width: | Height: | Size: 408 KiB |
@@ -1,23 +1,84 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head lang="en">
|
||||
<meta charset="UTF-8">
|
||||
<title>KeeWeb</title>
|
||||
<meta name="kw-signature" content="">
|
||||
<meta name="kw-config" content="(no-config)">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="theme-color" content="#6386EC">
|
||||
<link rel="shortcut icon" href="favicon.png?__inline=true" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="touchicon.png?__inline=true">
|
||||
<link rel="stylesheet" href="css/main.css?__inline=true" />
|
||||
<script src="js/vendor.js?__inline=true"></script>
|
||||
<script src="js/app.js?__inline=true"></script>
|
||||
</head>
|
||||
<body class="th-d">
|
||||
<noscript>
|
||||
<h1>KeeWeb</h1>
|
||||
<p>KeeWeb is a password manager written in JavaScript. Please, enable JavaScript to run it.</p>
|
||||
</noscript>
|
||||
</body>
|
||||
<head lang="en">
|
||||
<meta charset="UTF-8" />
|
||||
<title>KeeWeb</title>
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="
|
||||
default-src 'self';
|
||||
font-src data:;
|
||||
script-src 'self' 'unsafe-eval';
|
||||
style-src 'self' blob:;
|
||||
connect-src 'self' ws: https:;
|
||||
child-src 'self' blob:;
|
||||
worker-src 'self' blob:;
|
||||
img-src 'self' data: blob: https://services.keeweb.info/;
|
||||
object-src 'none';
|
||||
form-action 'none';
|
||||
"
|
||||
/>
|
||||
<meta name="application-name" content="KeeWeb" />
|
||||
<meta name="kw-config" content="(no-config)" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-title" content="KeeWeb" />
|
||||
<meta name="theme-color" content="#6386ec" />
|
||||
<meta name="msapplication-config" content="browserconfig.xml" />
|
||||
<meta name="msapplication-TileColor" content="#6386ec" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="icons/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="icons/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="icons/favicon-16x16.png" />
|
||||
<link rel="mask-icon" href="icons/safari-pinned-tab.svg" color="#6386ec" />
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="icons/splash-640x1136.png"
|
||||
media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="icons/splash-750x1294.png"
|
||||
media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="icons/splash-1242x2148.png"
|
||||
media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="icons/splash-1125x2436.png"
|
||||
media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="icons/splash-1536x2048.png"
|
||||
media="(min-device-width: 768px) and (max-device-width: 1024px) and (-webkit-min-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="icons/splash-1668x2224.png"
|
||||
media="(min-device-width: 834px) and (max-device-width: 834px) and (-webkit-min-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-startup-image"
|
||||
href="icons/splash-2048x2732.png"
|
||||
media="(min-device-width: 1024px) and (max-device-width: 1024px) and (-webkit-min-device-pixel-ratio: 2) and (orientation: portrait)"
|
||||
/>
|
||||
<link rel="manifest" href="manifest.json" />
|
||||
<link rel="stylesheet" href="css/app.css?__inline=true" />
|
||||
<script src="js/app.js?__inline=true"></script>
|
||||
</head>
|
||||
<body class="th-dark">
|
||||
<div class="app">
|
||||
<noscript>
|
||||
<h1>KeeWeb</h1>
|
||||
<p>KeeWeb is a password manager written in JavaScript. Please enable JavaScript to run it.</p>
|
||||
</noscript>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
(function (global) {
|
||||
var babelHelpers = global.babelHelpers = {};
|
||||
babelHelpers.typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) {
|
||||
return typeof obj;
|
||||
} : function (obj) {
|
||||
return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
|
||||
};
|
||||
|
||||
babelHelpers.defineProperty = function (obj, key, value) {
|
||||
if (key in obj) {
|
||||
Object.defineProperty(obj, key, {
|
||||
value: value,
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true
|
||||
});
|
||||
} else {
|
||||
obj[key] = value;
|
||||
}
|
||||
|
||||
return obj;
|
||||
};
|
||||
|
||||
babelHelpers.slicedToArray = function () {
|
||||
function sliceIterator(arr, i) {
|
||||
var _arr = [];
|
||||
var _n = true;
|
||||
var _d = false;
|
||||
var _e = undefined;
|
||||
|
||||
try {
|
||||
for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) {
|
||||
_arr.push(_s.value);
|
||||
|
||||
if (i && _arr.length === i) break;
|
||||
}
|
||||
} catch (err) {
|
||||
_d = true;
|
||||
_e = err;
|
||||
} finally {
|
||||
try {
|
||||
if (!_n && _i["return"]) _i["return"]();
|
||||
} finally {
|
||||
if (_d) throw _e;
|
||||
}
|
||||
}
|
||||
|
||||
return _arr;
|
||||
}
|
||||
|
||||
return function (arr, i) {
|
||||
if (Array.isArray(arr)) {
|
||||
return arr;
|
||||
} else if (Symbol.iterator in Object(arr)) {
|
||||
return sliceIterator(arr, i);
|
||||
} else {
|
||||
throw new TypeError("Invalid attempt to destructure non-iterable instance");
|
||||
}
|
||||
};
|
||||
}();
|
||||
|
||||
babelHelpers.toConsumableArray = function (arr) {
|
||||
if (Array.isArray(arr)) {
|
||||
for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) arr2[i] = arr[i];
|
||||
|
||||
return arr2;
|
||||
} else {
|
||||
return Array.from(arr);
|
||||
}
|
||||
};
|
||||
})(typeof global === "undefined" ? self : global);
|
||||
@@ -0,0 +1,778 @@
|
||||
{
|
||||
"retToApp": "return to app",
|
||||
"name": "name",
|
||||
"icon": "icon",
|
||||
"title": "title",
|
||||
"password": "password",
|
||||
"user": "user",
|
||||
"website": "website",
|
||||
"tags": "tags",
|
||||
"notes": "notes",
|
||||
"entry": "entry",
|
||||
"group": "group",
|
||||
"noTitle": "no title",
|
||||
"or": "or",
|
||||
"history": "history",
|
||||
"template": "template",
|
||||
"templates": "templates",
|
||||
"notImplemented": "Not Implemented",
|
||||
"saveChanges": "Save changes",
|
||||
"discardChanges": "Discard changes",
|
||||
"advanced": "Advanced",
|
||||
"shortcuts": "Shortcuts",
|
||||
"help": "Help",
|
||||
"settings": "Settings",
|
||||
"plugins": "Plugins",
|
||||
"ctrlKey": "ctrl",
|
||||
"shiftKey": "shift",
|
||||
"altKey": "alt",
|
||||
"error": "error",
|
||||
"oneMinute": "one minute",
|
||||
"minutes": "{} minutes",
|
||||
"oneHour": "one hour",
|
||||
"hours": "{} hours",
|
||||
"oneDay": "one day",
|
||||
"days": "{} days",
|
||||
"oneWeek": "one week",
|
||||
"oneMonth": "one month",
|
||||
"oneYear": "one year",
|
||||
"cache": "cache",
|
||||
"file": "file",
|
||||
"device": "device",
|
||||
"webdav": "WebDAV",
|
||||
"dropbox": "Dropbox",
|
||||
"gdrive": "Google Drive",
|
||||
"onedrive": "OneDrive",
|
||||
"menuAllItems": "All Items",
|
||||
"menuColors": "Colors",
|
||||
"menuTrash": "Trash",
|
||||
"menuSetGeneral": "General",
|
||||
"menuSetBrowser": "Browser",
|
||||
"menuSetAbout": "About",
|
||||
"menuSetDevices": "Devices",
|
||||
"menuAlertNoTags": "No tags",
|
||||
"menuAlertNoTagsBody": "You can add new tags while editing fields, in the Tags section.",
|
||||
"menuEmptyTrash": "Empty Trash",
|
||||
"menuEmptyTrashAlert": "Empty Trash?",
|
||||
"menuEmptyTrashAlertBody": "You will not be able to put items back",
|
||||
"menuItemCollapsed": "Double-click to expand",
|
||||
"menuRestoreApp": "Open {}",
|
||||
"menuQuitApp": "Quit {}",
|
||||
"sysMenuAboutKeeWeb": "About {}",
|
||||
"sysMenuServices": "Services",
|
||||
"sysMenuHide": "Hide {}",
|
||||
"sysMenuHideOthers": "Hide Others",
|
||||
"sysMenuUnhide": "Show All",
|
||||
"sysMenuQuit": "Quit {}",
|
||||
"sysMenuEdit": "Edit",
|
||||
"sysMenuUndo": "Undo",
|
||||
"sysMenuRedo": "Redo",
|
||||
"sysMenuCut": "Cut",
|
||||
"sysMenuCopy": "Copy",
|
||||
"sysMenuPaste": "Paste",
|
||||
"sysMenuSelectAll": "Select All",
|
||||
"sysMenuWindow": "Window",
|
||||
"sysMenuMinimize": "Minimize",
|
||||
"sysMenuClose": "Close Window",
|
||||
"alertYes": "Yes",
|
||||
"alertNo": "No",
|
||||
"alertOk": "OK",
|
||||
"alertCancel": "Cancel",
|
||||
"alertSignIn": "Sign In",
|
||||
"alertCopy": "Copy",
|
||||
"alertClose": "Close",
|
||||
"alertDoNotAsk": "Don't ask me anymore",
|
||||
"alertAllow": "Allow",
|
||||
"alertDeny": "Deny",
|
||||
"appBeta": "WARNING: beta version, only for preview",
|
||||
"footerOpen": "Open / New",
|
||||
"footerSyncError": "Sync error",
|
||||
"footerTitleGen": "Generate",
|
||||
"footerTitleLock": "Lock",
|
||||
"genLen": "Length",
|
||||
"genNewPass": "New password",
|
||||
"genPresetDefault": "default preset",
|
||||
"genPresetDerived": "like old password",
|
||||
"genPresetPronounceable": "pronounceable",
|
||||
"genPresetMed": "medium length",
|
||||
"genPresetLong": "long",
|
||||
"genPresetPin4": "4-digit PIN",
|
||||
"genPresetMac": "MAC address",
|
||||
"genPresetHash128": "128-bit hash",
|
||||
"genPresetHash256": "256-bit hash",
|
||||
"genHidePass": "Hide password",
|
||||
"genShowPass": "Show password",
|
||||
"grpTitle": "Group",
|
||||
"grpSearch": "Enable searching entries in this group",
|
||||
"grpAutoType": "Enable auto-type",
|
||||
"grpAutoTypeSeq": "Auto-type sequence",
|
||||
"grpAutoTypeSeqDefault": "Use default auto-type sequence",
|
||||
"grpTrash": "Delete group with all entries",
|
||||
"tagTitle": "Tag",
|
||||
"tagTrash": "Remove tag from all entries",
|
||||
"tagRename": "Rename",
|
||||
"tagTrashQuestion": "Remove tag from all entries?",
|
||||
"tagTrashQuestionBody": "This tag will be removed from all entries. There will be no easy way to put it back.",
|
||||
"tagExists": "Tag already exists",
|
||||
"tagExistsBody": "Tag with this name already exists. Please choose another name.",
|
||||
"tagBadName": "Bad name",
|
||||
"tagBadNameBody": "Tag name cannot contain characters {}. Please remove them.",
|
||||
"genPsTitle": "Generator Presets",
|
||||
"genPsCreate": "New preset",
|
||||
"genPsDelete": "Delete preset",
|
||||
"genPsNew": "preset",
|
||||
"genPsEnabled": "Show in presets list",
|
||||
"genPsDefault": "Selected by default",
|
||||
"genPsDefaultLength": "Default length",
|
||||
"genPsUpper": "Uppercase latin letters",
|
||||
"genPsLower": "Lowercase latin letters",
|
||||
"genPsDigits": "Digits",
|
||||
"genPsSpecial": "Special symbols",
|
||||
"genPsBrackets": "Brackets",
|
||||
"genPsHigh": "High ASCII characters",
|
||||
"genPsAmbiguous": "Ambiguous symbols",
|
||||
"genPsInclude": "Additional symbols to include",
|
||||
"genPsExample": "Example of generated password",
|
||||
"genPsPattern": "Pattern",
|
||||
"genPsPatternHelp": "Patterns can be used to specify custom rules for selecting characters. For example, 1-AA will generate passwords starting with a digit, followed by a dash and two letters. You can use these symbols:",
|
||||
"genPsAllRanges": "All symbols",
|
||||
"genPsIncluded": "Additional symbols added above",
|
||||
"keyChangeTitleRemote": "Master Key Changed",
|
||||
"keyChangeMessageRemote": "Master key was changed for this database. Please enter a new key",
|
||||
"keyChangeTitleExpired": "Master Key Expired",
|
||||
"keyChangeMessageExpired": "Master key for this database is expired. Please enter a new key",
|
||||
"keyChangeRepeatPassword": "Password, once again",
|
||||
"keyEnter": "Enter",
|
||||
"keyEsc": "Esc",
|
||||
"iconFavTitle": "Download and use website favicon",
|
||||
"iconSelCustom": "Select a custom icon",
|
||||
"listEmptyTitle": "Empty",
|
||||
"listEmptyAdd": "add with {} button above",
|
||||
"listGroup": "Group",
|
||||
"listNoWebsite": "no website",
|
||||
"listNoUser": "no user",
|
||||
"listNoAttachments": "no attachments",
|
||||
"listAddTemplateHeader": "Templates",
|
||||
"listAddTemplateBody1": "Templates allow you to create entries with one click. Add something to template entry and then click {} again to use this template.",
|
||||
"listAddTemplateBody2": "You can always find your templates in {} group.",
|
||||
"searchAddNew": "Add New",
|
||||
"searchSort": "Sort",
|
||||
"searchCreated": "Created",
|
||||
"searchUpdated": "Updated",
|
||||
"searchAttachments": "Attachments",
|
||||
"searchAZ": "A {} Z",
|
||||
"searchZA": "Z {} A",
|
||||
"searchON": "Old {} New",
|
||||
"searchNO": "New {} Old",
|
||||
"searchShiftClickOr": "shift-click or",
|
||||
"searchAdvTitle": "Toggle advanced search",
|
||||
"searchSearchIn": "Search in",
|
||||
"searchOther": "Other fields",
|
||||
"searchProtect": "Secure fields",
|
||||
"searchOptions": "Options",
|
||||
"searchCase": "Match case",
|
||||
"searchRegex": "RegEx",
|
||||
"searchRank": "Auto",
|
||||
"searchExact": "Exact",
|
||||
"openOpen": "Open",
|
||||
"openNew": "New",
|
||||
"openNewFile": "New",
|
||||
"openMore": "More",
|
||||
"openDemo": "Demo",
|
||||
"openGenerate": "Generate",
|
||||
"openXml": "Import XML",
|
||||
"openCaps": "Caps Lock is on",
|
||||
"openClickToOpen": "Click to open a file",
|
||||
"openKeyFile": "key file",
|
||||
"openKeyFileDropbox": "(from dropbox)",
|
||||
"openDropHere": "drop files here",
|
||||
"openFailedRead": "Failed to read file",
|
||||
"openNothingFound": "Nothing found",
|
||||
"openNothingFoundBody": "We haven't found any suitable files.",
|
||||
"openSelectFile": "Select a file",
|
||||
"openSelectFileBody": "Select a file you would like to open",
|
||||
"openPassFor": "Password for",
|
||||
"openRemoveLastQuestion": "Delete local file?",
|
||||
"openRemoveLastQuestionBody": "The file you are deleting is stored only inside the app. Delete it permanently?",
|
||||
"openRemoveLastQuestionModBody": "The file you are deleting has local modifications. Delete it and discard these modifications?",
|
||||
"openLocalFile": "Local file",
|
||||
"openLocalFileBody": "You are going to open a file that will be stored inside the app. Changes you make will not be saved back to file system. To get the file with your data, export it from settings.",
|
||||
"openLocalFileDontShow": "Don't show this again",
|
||||
"openWrongFile": "Bad file",
|
||||
"openWrongFileBody": "This file format is not supported. This app works with KeePass database format files (kdbx).",
|
||||
"openKdbFileBody": "You are opening an old version format file (KDB). This app supports only new format (kdbx), please use KeePass v2 to convert between them.",
|
||||
"openConfigHeader": "{} Settings",
|
||||
"openUrl": "URL",
|
||||
"openUrlDesc": "https://server/path/file.kdbx, or just file.kdbx",
|
||||
"openUser": "User",
|
||||
"openUserDesc": "WebDAV server user (if required)",
|
||||
"openUserPlaceholder": "no user",
|
||||
"openPass": "Password",
|
||||
"openPassDesc": "WebDAV server password (this is not your file password)",
|
||||
"openPassPlaceholder": "no password",
|
||||
"openConfigError": "Error: {}",
|
||||
"openConfigErrorNotFound": "File not found",
|
||||
"openError": "Error",
|
||||
"openErrorDescription": "There was an error opening file",
|
||||
"openErrorDescriptionMaybeTouchIdChanged": "The error looks similar to what usually happens when Touch ID setup is changed, for example, you added or removed an additional finger. If it's the case, go to Settings, disable Touch ID, and re-enable it again.",
|
||||
"openErrorFileNotFound": "File not found",
|
||||
"openListErrorBody": "There was an error loading file list",
|
||||
"openShowAllFiles": "Show all files",
|
||||
"openFileNoCacheError": "File not found in the cache storage. This can happen because browser storage was cleaned up. To open the file, remove it from KeeWeb and add it again.",
|
||||
"openChalRespHeader": "Challenge-Response",
|
||||
"openChalRespLoading": "Loading the list of YubiKeys",
|
||||
"openChalRespSelectYubiKey": "Select a YubiKey that you would like to use",
|
||||
"openChalRespErrorEmpty": "No YubiKeys found.",
|
||||
"openChalRespErrorEmptyMac": "First time using this feature on macOS? KeeWeb must be added to the Input Monitoring section of security settings.",
|
||||
"detAttDownload": "Shift-click the attachment button to download it or",
|
||||
"detAttDelToRemove": "Delete to remove",
|
||||
"detAttSave": "Save as …",
|
||||
"detEmpty": "Your passwords will be displayed here",
|
||||
"detGroupRestore": "To restore this group, please drag it to any group outside trash",
|
||||
"detHistoryClickPoint": "Click entry history timeline point to view state",
|
||||
"detHistoryReturn": "return to entry",
|
||||
"detHistoryRevert": "Revert to state",
|
||||
"detHistoryDel": "Delete state",
|
||||
"detHistoryEmpty": "empty",
|
||||
"detHistoryModified": "modified",
|
||||
"detHistoryRec": "record",
|
||||
"detHistoryRecs": "records",
|
||||
"detHistoryVersion": "Version",
|
||||
"detHistorySaved": "Saved",
|
||||
"detHistoryNoTitle": "no title",
|
||||
"detHistoryCurState": "current state",
|
||||
"detHistoryCurUnsavedState": "current unsaved state",
|
||||
"detHistoryRevertAlert": "Revert to this history state?",
|
||||
"detHistoryRevertAlertBody": "Your current state will be saved to history.",
|
||||
"detHistoryDeleteAlert": "Delete this history state?",
|
||||
"detHistoryDeleteAlertBody": "You will not be able to restore it.",
|
||||
"detHistoryDiscardChangesAlert": "Discard changes made to entry?",
|
||||
"detHistoryDiscardChangesAlertBody": "Unsaved changes will be lost, there will be no way back.",
|
||||
"detBackToList": "back to list",
|
||||
"detSetIconColor": "Change color",
|
||||
"detSetIcon": "Change icon",
|
||||
"detDropAttachments": "drop attachments here",
|
||||
"detDelEntry": "Delete",
|
||||
"detDelEntryPerm": "Delete permanently",
|
||||
"detExpires": "Expires",
|
||||
"detExpired": "expired",
|
||||
"detGroup": "Group",
|
||||
"detCreated": "Created",
|
||||
"detUpdated": "Updated",
|
||||
"detNetField": "New Field",
|
||||
"detAttachments": "Attachments",
|
||||
"detDelFromTrash": "Delete from trash?",
|
||||
"detDelFromTrashBody": "You will not be able to put it back.",
|
||||
"detDelFromTrashBodyHint": "To quickly remove all items from trash, click Empty icon in Trash menu.",
|
||||
"detDelToTrash": "Delete the entry?",
|
||||
"detDelToTrashBody": "The entry will be moved to the trash folder.",
|
||||
"detFieldCopied": "Copied",
|
||||
"detFieldCopiedTime": "Copied for {} seconds",
|
||||
"detCopyHint": "You can copy field value by clicking its title",
|
||||
"detMore": "more",
|
||||
"detClickToAddField": "click to add a new field",
|
||||
"detMenuAddNewField": "Add a new field",
|
||||
"detMenuAddNewWebsite": "Add another website",
|
||||
"detMenuShowEmpty": "Show empty fields",
|
||||
"detMenuHideEmpty": "Hide empty fields",
|
||||
"detMenuAddField": "Add {}",
|
||||
"detMenuCopyPassword": "Copy password",
|
||||
"detMenuCopyUser": "Copy user",
|
||||
"detMenuCopyOtp": "Copy one-time code",
|
||||
"detSetupOtp": "One-time codes",
|
||||
"detClone": "Make a copy",
|
||||
"detCopyEntryToClipboard": "Copy all fields",
|
||||
"detClonedName": "Copy",
|
||||
"detAutoType": "Auto-type",
|
||||
"detAutoTypeSettings": "Auto-type settings",
|
||||
"detAutoTypeEnabled": "Enable auto-type for this entry",
|
||||
"detAutoTypeSequence": "Keystrokes",
|
||||
"detAutoTypeInput": "Input",
|
||||
"detAutoTypeShortcutsDesc": "{} or {} while the app is inactive",
|
||||
"detAutoTypeObfuscation": "Mix real keystrokes with random",
|
||||
"detAutoTypeWindow": "Window",
|
||||
"detAutoTypeInputWindow": "Window title",
|
||||
"detSetupOtpAlert": "Scan the QR code",
|
||||
"detSetupOtpAlertBody": "Please copy the QR code which is displayed on the authorization page.",
|
||||
"detSetupOtpAlertBody1": "1. go to the authorization page",
|
||||
"detSetupOtpAlertBody2": "2. make a screenshot of the QR code {}",
|
||||
"detSetupOtpAlertBody3": "3. paste it here {}",
|
||||
"detSetupOtpAlertBody3Mobile": "3. select it or scan with your camera using Select/Scan button below",
|
||||
"detSetupOtpAlertBody4": "If you can't scan the code, click Enter code manually",
|
||||
"detSetupOtpManualButton": "Enter code manually",
|
||||
"detSetupOtpScanButton": "Select/Scan",
|
||||
"detSetupOtpAlertBodyWith": "with {}",
|
||||
"detOtpImageError": "Error reading image",
|
||||
"detOtpImageErrorBody": "Sorry, we could not read the image format, please contact the app authors with error details.",
|
||||
"detOtpImageReading": "Reading QR code...",
|
||||
"detOtpQrError": "QR code scan error",
|
||||
"detOtpQrErrorBody": "Sorry, we could not read the QR code, please try once again or contact the app authors with error details.",
|
||||
"detOtpQrWrong": "Wrong QR code",
|
||||
"detOtpQrWrongBody": "Your QR code was successfully scanned but it doesn't contain one-time code data.",
|
||||
"detOtpField": "One-time code",
|
||||
"detOtpClickToTouch": "Click to generate",
|
||||
"detOtpGenerating": "Generating...",
|
||||
"detOtpTouch": "Touch your {}",
|
||||
"detLockField": "Lock this field, so its content isn't searchable and visible. Displaying the content requires explicitly clicking it.",
|
||||
"detUnlockField": "Unlock this field, making its content searchable and visible immediately",
|
||||
"detRevealField": "Reveal",
|
||||
"detHideField": "Hide",
|
||||
"detAutoTypeField": "Auto type",
|
||||
"detIssuesHideTooltip": "Hide this warning",
|
||||
"detIssueWeakPassword": "The password is weak, it's recommended to change it.",
|
||||
"detIssuePoorPassword": "The password is very weak, it's strongly recommended to change it.",
|
||||
"detIssuePwnedPassword": "This password has been exposed in a data breach according to {}, it's recommended to change it.",
|
||||
"detIssuePasswordCheckError": "There was an error checking password strength online.",
|
||||
"detIssueOldPassword": "The password is old.",
|
||||
"detIssueCloseAlertHeader": "Hide password issues",
|
||||
"detIssueCloseAlertBody": "There are different ways you can hide this warning:",
|
||||
"detIssueCloseAlertEntry": "Don't show for this entry",
|
||||
"detIssueCloseAlertSettings": "Adjust global settings",
|
||||
"autoTypeEntryFields": "Entry fields",
|
||||
"autoTypeModifiers": "Modifier keys",
|
||||
"autoTypeKeys": "Keys",
|
||||
"autoTypeLink": "more...",
|
||||
"autoTypeError": "Auto-type error",
|
||||
"autoTypeErrorGeneric": "There was an error performing auto-type: {}",
|
||||
"autoTypeErrorAccessibilityMacOS": "We tried to send keystrokes to the application, but it doesn't seem to work. This may happen because of missing permissions, click here to read more about it:",
|
||||
"autoTypeErrorGlobal": "To use a system-wide shortcut, please focus the app where you want to type your password",
|
||||
"autoTypeErrorNotInstalled": "{} is not installed",
|
||||
"autoTypeHeader": "Auto-Type: Select",
|
||||
"autoTypeMsgNoWindow": "We were unable to get active window title, start typing to search",
|
||||
"autoTypeMsgMatchedByWindow": "Select a password for {}",
|
||||
"autoTypeNoMatches": "no matches",
|
||||
"autoTypeSelectionHint": "Type the autotype sequence",
|
||||
"autoTypeSelectionHintAction": "Only type the password",
|
||||
"autoTypeSelectionHintOpt": "Only type the username",
|
||||
"autoTypeSelectionHintShift": "Other fields",
|
||||
"autoTypeSelectionOtp": "One-time code",
|
||||
"autoTypeUnlockMessage": "Unlock to auto-type",
|
||||
"appSecWarn": "Not Secure!",
|
||||
"appSecWarnBody1": "You have loaded this app with an insecure connection. Someone may be watching you and stealing your passwords. We strongly advise you to stop, unless you clearly understand what you're doing.",
|
||||
"appSecWarnBody2": "Yes, your database is encrypted but no one can guarantee that the app has not been modified on the way to you.",
|
||||
"appSecWarnBtn": "I understand the risks, continue",
|
||||
"appUnsavedWarn": "Unsaved changes!",
|
||||
"appUnsavedWarnBody": "You have unsaved files, if you close the app, changes will be lost.",
|
||||
"appDontExitBtn": "Don't exit",
|
||||
"appCannotLockAutoInit": "The app cannot be locked because autosave is disabled.",
|
||||
"appCannotLock": "You have unsaved changes that will be lost. Continue?",
|
||||
"appAutoSave": "Save changes automatically",
|
||||
"appSaveError": "Save Error",
|
||||
"appSaveErrorBody": "Failed to auto-save file",
|
||||
"appSaveErrorBodyMul": "Failed to auto-save files:",
|
||||
"appSaveErrorExitLoseChanges": "Quit and lose all changes",
|
||||
"appSaveErrorExitLoseChangesBody": "You can either quit the app and lose all changes, or go file settings to export data",
|
||||
"appSettingsError": "Error loading app",
|
||||
"appSettingsErrorBody": "There was an error loading app settings. Please double check the app URL or contact your administrator.",
|
||||
"appNotSupportedError": "Your browser doesn't support some important features we're using.",
|
||||
"appTabWarn": "Too many tabs",
|
||||
"appTabWarnBody": "KeeWeb cannot be used in two browser tabs simultaneously, please close this tab.",
|
||||
"appRightsAlert": "Locking KeeWeb app",
|
||||
"appRightsAlertBody1": "Your KeeWeb app is currently not guarded against unauthorized changes. We're asking you for permissions to make the app writable only for administrators.",
|
||||
"appRightsAlertBody2": "Don't want to give permissions? You can do it yourself in Terminal",
|
||||
"appBrowserAuthComplete": "Authentication is complete, you may close this tab now.",
|
||||
"setGenTitle": "General Settings",
|
||||
"setGenUpdate": "Update",
|
||||
"setGenNewVersion": "New app version was released and downloaded",
|
||||
"setGenReleaseNotes": "View release notes",
|
||||
"setGenReloadToUpdate": "Reload to update",
|
||||
"setGenUpdateManual": "A new version has been released. It will check for updates and install them automatically, but auto-upgrading from your version is not possible.",
|
||||
"setGenDownloadUpdate": "Download update",
|
||||
"setGenUpdateAuto": "Download and install automatically",
|
||||
"setGenUpdateCheck": "Check but don't install",
|
||||
"setGenNoUpdate": "Never check for updates",
|
||||
"setGenUpdateChecking": "Checking for updates",
|
||||
"setGenCheckUpdate": "Check for updates",
|
||||
"setGenErrorChecking": "Error checking for updates",
|
||||
"setGenLastCheckSuccess": "Last successful check was at {}",
|
||||
"setGenLastCheckVer": "the latest version was {}",
|
||||
"setGenCheckedAt": "Checked at",
|
||||
"setGenLatestVer": "you are using the latest version",
|
||||
"setGenNewVer": "new version {} available, released",
|
||||
"setGenDownloadingUpdate": "Downloading update...",
|
||||
"setGenExtractingUpdate": "Extracting update...",
|
||||
"setGenCheckErr": "There was an error downloading new version",
|
||||
"setGenNeverChecked": "Never checked for updates",
|
||||
"setGenRestartToUpdate": "Restart KeeWeb to update",
|
||||
"setGenDownloadAndRestart": "Download update and restart",
|
||||
"setGenAppearance": "Appearance",
|
||||
"setGenTheme": "Theme",
|
||||
"setGenThemeDefault": "Default",
|
||||
"setGenThemeDark": "Dark",
|
||||
"setGenThemeLight": "Light",
|
||||
"setGenThemeBlue": "Flat blue",
|
||||
"setGenThemeFb": "Dark blue",
|
||||
"setGenThemeBl": "Light blue",
|
||||
"setGenThemeBrown": "Brownie",
|
||||
"setGenThemeDb": "Dark brown",
|
||||
"setGenThemeLb": "Light brown",
|
||||
"setGenThemeTerminal": "Terminal",
|
||||
"setGenThemeTe": "Terminal",
|
||||
"setGenThemeLt": "Terminal light",
|
||||
"setGenThemeHighContrast": "High contrast",
|
||||
"setGenThemeHc": "High contrast",
|
||||
"setGenThemeDc": "Dark contrast",
|
||||
"setGenThemeSol": "Solarized",
|
||||
"setGenThemeSd": "Solarized dark",
|
||||
"setGenThemeSl": "Solarized light",
|
||||
"setGenMoreThemes": "More themes",
|
||||
"setGenAutoSwitchTheme": "Automatically switch between light and dark theme when possible",
|
||||
"setGenLocale": "Language",
|
||||
"setGenLocOther": "other languages are available as plugins",
|
||||
"setGenFontSize": "Font size",
|
||||
"setGenFontSizeNormal": "Normal",
|
||||
"setGenFontSizeLarge": "Large",
|
||||
"setGenFontSizeLargest": "Largest",
|
||||
"setGenTitlebarStyle": "Window style (requires restart)",
|
||||
"setGenTitlebarStyleDefault": "Default",
|
||||
"setGenTitlebarStyleHidden": "Custom title",
|
||||
"setGenTitlebarStyleHiddenInset": "Custom title, draggable window",
|
||||
"setGenShowSubgroups": "Show entries from all subgroups",
|
||||
"setGenTableView": "Entries list table view",
|
||||
"setGenColorfulIcons": "Colorful custom icons in the list",
|
||||
"setGenUseMarkdown": "Markdown in notes",
|
||||
"setGenUseGroupIconForEntries": "Automatically use group icon for new entries",
|
||||
"setGenDirectAutotype": "If only one matching entry is found, select that one automatically for auto-type",
|
||||
"setGenAutoTypeTitleFilterEnabled": "Filter entries by title in auto-type",
|
||||
"setGenFunction": "Function",
|
||||
"setGenAutoSyncOnClose": "Automatically save and sync on close",
|
||||
"setGenAutoSyncTimer": "Automatically save and sync periodically",
|
||||
"setGenAutoSyncTimerOff": "Disabled",
|
||||
"setGenAutoSyncTimerOnChange": "On every change",
|
||||
"setGenAutoSyncTimerInterval": "Every {} minute(s)",
|
||||
"setGenRememberKeyFiles": "Remember key files",
|
||||
"setGenNoRememberKeyFiles": "Don't remember",
|
||||
"setGenRememberKeyFilesData": "Store in the internal app storage",
|
||||
"setGenRememberKeyFilesPath": "Remember only path to key files",
|
||||
"setGenLockInactive": "If the app is inactive",
|
||||
"setGenNoAutoLock": "Don't auto-lock",
|
||||
"setGenLockMinutes": "In {} minutes",
|
||||
"setGenLockHour": "In an hour",
|
||||
"setGenLockHours": "In {} hours",
|
||||
"setGenLockDay": "In a day",
|
||||
"setGenClearClip": "Clear clipboard after copy",
|
||||
"setGenNoClear": "Don't clear",
|
||||
"setGenClearSeconds": "In {} seconds",
|
||||
"setGenClearMinute": "In a minute",
|
||||
"setGenMinInstead": "Minimize the app instead of close",
|
||||
"setGenMinOnFieldCopy": "Minimize on field copy",
|
||||
"setGenLock": "Auto lock",
|
||||
"setGenLockMinimize": "When the app is minimized",
|
||||
"setGenLockCopy": "On password copy",
|
||||
"setGenLockAutoType": "On auto-type",
|
||||
"setGenLockOrSleep": "When the computer is locked or put to sleep",
|
||||
"setGenStorage": "Storage",
|
||||
"setGenDisableOfflineStorage": "Don't cache loaded files in offline storage",
|
||||
"setGenShortLivedStorageToken": "Use short-lived sessions in cloud storage providers",
|
||||
"setGenStorageLogout": "Log out",
|
||||
"setGenShowAdvanced": "Show advanced settings",
|
||||
"setGenDevTools": "Show dev tools",
|
||||
"setGenTryBeta": "Try beta version until restart",
|
||||
"setGenTryBetaWarning": "Unsaved files",
|
||||
"setGenTryBetaWarningBody": "Please save all files and click this button again",
|
||||
"setGenShowAppLogs": "Show app logs",
|
||||
"setGenReloadApp": "Reload the app",
|
||||
"setGenFieldLabelDblClickAutoType": "Auto-type on double-clicking field labels",
|
||||
"setGenTouchId": "Touch ID",
|
||||
"setGenTouchIdDisabled": "Don't use Touch ID",
|
||||
"setGenTouchIdMemory": "Unlock with Touch ID only while KeeWeb is running",
|
||||
"setGenTouchIdFile": "Always use Touch ID instead of master password",
|
||||
"setGenTouchIdPass": "Require master password after",
|
||||
"setGenAudit": "Audit",
|
||||
"setGenAuditPasswords": "Show warnings about password strength",
|
||||
"setGenAuditPasswordEntropy": "Check password length and randomness",
|
||||
"setGenExcludePinsFromAudit": "Never check short numeric PIN codes, such as 123456",
|
||||
"setGenCheckPasswordsOnHIBP": "Check passwords using an online service {}",
|
||||
"setGenHelpHIBP": "KeeWeb can check if your passwords have been previously exposed in a data breach using an online service. Your password cannot be recovered based on data sent online, however, the number of passwords checked this way may be exposed. More about your privacy when using this service can be found {}. If this option is enabled, KeeWeb will automatically check your passwords there.",
|
||||
"setGenHelpHIBPLink": "here",
|
||||
"setGenAuditPasswordAge": "Old passwords",
|
||||
"setGenAuditPasswordAgeOff": "Don't show warnings about old passwords",
|
||||
"setGenAuditPasswordAgeOneYear": "Show warnings for passwords older than one year",
|
||||
"setGenAuditPasswordAgeYears": "Show warnings for passwords older than {} years",
|
||||
"setFilePath": "File path",
|
||||
"setFileStorage": "This file is loaded from {}.",
|
||||
"setFileIntl": "This file is stored in the internal app storage",
|
||||
"setFileLocalHint": "Want to work seamlessly with local files?",
|
||||
"setFileDownloadApp": "Download a desktop app",
|
||||
"setFileSave": "Save",
|
||||
"setFileSaveTo": "Save to …",
|
||||
"setFileClose": "Close",
|
||||
"setFileSync": "Sync",
|
||||
"setFileSyncVerb": "Sync",
|
||||
"setFileSaveToXml": "XML",
|
||||
"setFileSaveToHtml": "HTML",
|
||||
"setFileLastSync": "Last sync",
|
||||
"setFileLastSyncUnknown": "unknown",
|
||||
"setFileSyncInProgress": "sync in progress",
|
||||
"setFileSyncError": "Sync error",
|
||||
"setFilePass": "Master password",
|
||||
"setFileConfirmPass": "Confirm Master password",
|
||||
"setFilePassChange": "to change your password, please type it in the Confirm field",
|
||||
"setFilePassChanged": "your password was changed; leave the field blank to use the old password",
|
||||
"setFilePassNotMatch": "passwords don't match, please type it again",
|
||||
"setFileKeyFile": "Key file",
|
||||
"setFileSelKeyFile": "Select a key file",
|
||||
"setFileNames": "Names",
|
||||
"setFileDefUser": "Default user",
|
||||
"setFileEnableTrash": "Enable trash",
|
||||
"setFileHistMode": "Entry history",
|
||||
"setFileHistLimited": "Delete old history records automatically",
|
||||
"setFileHistDisabled": "Don't save history",
|
||||
"setFileHistUnlimited": "Keep history records forever",
|
||||
"setFileHistLen": "History length, keep last records per entry",
|
||||
"setFileHistSize": "History size, total MB per file",
|
||||
"setFileBackups": "Backups",
|
||||
"setFileBackupEnable": "Backup this file",
|
||||
"setFileBackupPath": "Backup path",
|
||||
"setFileBackupTime": "Make backups",
|
||||
"setFileBackupNow": "Backup now",
|
||||
"setFileBackupNowWorking": "Working...",
|
||||
"setFileBackupError": "Backup error",
|
||||
"setFileBackupErrorDescription": "Error writing backup file",
|
||||
"setFileBackupErrorIsDir": "Backup path invalid",
|
||||
"setFileBackupErrorIsDirDescription": "The backup path seems to point to a directory. Please specify a filename inside a directory instead.",
|
||||
"setFileBackupOnSave": "Every time I save the file",
|
||||
"setFileBackupDaily": "Daily",
|
||||
"setFileBackupWeekly": "Weekly",
|
||||
"setFileBackupMonthly": "Monthly",
|
||||
"setFileBackupManually": "Manually, no auto-backups",
|
||||
"setFileRounds": "Key encryption rounds",
|
||||
"setFileKdfParams": "Key derivation function",
|
||||
"setFileKdfParamsIter": "Iterations",
|
||||
"setFileKdfParamsMem": "Memory, KB",
|
||||
"setFileKdfParamsPar": "Parallelism",
|
||||
"setFileKeyChangeForce": "Ask to change key after (days)",
|
||||
"setFileUseKeyFile": "Use key file",
|
||||
"setFileUseGenKeyFile": "Use the generated key file",
|
||||
"setFileUseOldKeyFile": "Use the old key file",
|
||||
"setFileGenKeyFile": "Generate a new key file",
|
||||
"setFileDontUseKeyFile": "Don't use a key file",
|
||||
"setFileEmptyPass": "Empty password",
|
||||
"setFileEmptyPassBody": "Saving database with an empty password makes it completely unprotected. Do you really want to do it?",
|
||||
"setFileSaveError": "Save error",
|
||||
"setFileSaveErrorBody": "Error saving to file",
|
||||
"setFileAlreadyExists": "Already exists",
|
||||
"setFileAlreadyExistsBody": "File {} already exists. Overwrite it?",
|
||||
"setFileUnsaved": "Unsaved changes",
|
||||
"setFileUnsavedBody": "There are unsaved changes in this file",
|
||||
"setFileCloseNoSave": "Close and lose changes",
|
||||
"setFileDontClose": "Don't close",
|
||||
"setFileFormatVersion": "File format",
|
||||
"setFileExportRaw": "Exporting your passwords",
|
||||
"setFileExportRawBody": "The exported file will contain your passwords, they will not be encrypted there. Would you like to proceed?",
|
||||
"setFileDeviceIntro": "One-time codes from this {} will be displayed in the app.",
|
||||
"setFileDeviceSettings": "Settings",
|
||||
"setFileYubiKey": "YubiKey",
|
||||
"setFileDontUseYubiKey": "Don't use a YubiKey",
|
||||
"setFileRefreshYubiKeyList": "Refresh the list",
|
||||
"setFileYubiKeyHeader": "YubiKey",
|
||||
"setFileYubiKeyBody": "Using a YubiKey as a part of master key is dangerous and you may lose access to your passwords if something goes wrong. Have you made a backup of your file before changing this setting?",
|
||||
"setFileYubiKeyErrorEmptyMac": "Looks like there's a YubiKey plugged in, but it's not visible. KeeWeb must be added to the Input Monitoring section of security settings to access YubiKeys.",
|
||||
"setShTitle": "Shortcuts",
|
||||
"setShShowAll": "show all items",
|
||||
"setShColors": "show items with colors",
|
||||
"setShTrash": "go to Trash",
|
||||
"setShFind": "search, or just start typing",
|
||||
"setShClearSearch": "clear search",
|
||||
"setShCopyPass": "copy password or selected field",
|
||||
"setShCopyUser": "copy user",
|
||||
"setShCopyUrl": "copy website",
|
||||
"setShAutoType": "auto-type selected entry",
|
||||
"setShPrev": "go to previous item",
|
||||
"setShNext": "go to next item",
|
||||
"setShCreateEntry": "create entry",
|
||||
"setShOpen": "open / new",
|
||||
"setShSave": "save all files",
|
||||
"setShGen": "generate password",
|
||||
"setShSet": "app settings",
|
||||
"setShCopyPassGlobal": "copy password",
|
||||
"setShCopyOtp": "copy OTP",
|
||||
"setShAutoTypeGlobal": "auto-type",
|
||||
"setShCopyPassOnly": "copy password",
|
||||
"setShRestoreApp": "open {}",
|
||||
"setShGlobal": "These system-wide shortcuts work when the app is running in background. You can click them to select your own shortcut.",
|
||||
"setShLock": "lock database",
|
||||
"setShEdit": "Press a new key combination to set it as shortcut",
|
||||
"setPlInstallTitle": "Install new plugins",
|
||||
"setPlInstallDesc": "KeeWeb plugins add features, themes, and languages to KeeWeb. Plugins run with the same privileges as KeeWeb, they can access and manage all your passwords. Never install plugins you don't trust.",
|
||||
"setPlInstallLabel": "Plugin URL",
|
||||
"setPlInstallBtn": "Install",
|
||||
"setPlInstallBtnProgress": "Installing",
|
||||
"setPlUninstallBtn": "Uninstall",
|
||||
"setPlDisableBtn": "Disable",
|
||||
"setPlEnableBtn": "Enable",
|
||||
"setPlUpdateBtn": "Update",
|
||||
"setPlLocaleBtn": "Switch to this language",
|
||||
"setPlThemeBtn": "Switch to this theme",
|
||||
"setPlJs": "code",
|
||||
"setPlCss": "styles",
|
||||
"setPlLoc": "language",
|
||||
"setPlCreatedBy": "Created by {}",
|
||||
"setPlLoadTime": "took {} to load",
|
||||
"setPlLastUpdate": "Last check for updates",
|
||||
"setPlLoadError": "error loading plugin",
|
||||
"setPlGalleryLoading": "Loading plugins, please wait a bit",
|
||||
"setPlGalleryLoadError": "Error loading plugins",
|
||||
"setPlInstallUrlTitle": "Add plugin from URL",
|
||||
"setPlInstallUrlDesc": "If the plugin is not in the gallery, you can install it manually from URL",
|
||||
"setPlOfficial": "Official KeeWeb plugin",
|
||||
"setPlSearch": "Search for plugins",
|
||||
"setPlDevelop": "Want to develop your own plugins?",
|
||||
"setPlDevelopStart": "Start here",
|
||||
"setPlTranslate": "Or you can {}",
|
||||
"setPlTranslateLink": "translate the app to your language",
|
||||
"setPlAutoUpdate": "Update automatically",
|
||||
"setPlLoadGallery": "Load plugin gallery",
|
||||
"setBrowserTitle": "Browser",
|
||||
"setBrowserIntroDesktop": "KeeWeb can enter passwords using auto-type, however, a browser extension may be a more convenient way of doing it. KeeWeb supports two browser extensions:",
|
||||
"setBrowserIntroKeeWebConnect": "the official KeeWeb extension, it's built with features of KeeWeb in mind, but you may miss some advanced capabilities that can be found in the other extension.",
|
||||
"setBrowserIntroKeePassXcBrowser": "this extension was developed for KeePassXC, it's been on the market for a while and it is quite solid. The extension is not a part of KeeWeb and there may be compatibility issues.",
|
||||
"setBrowserIntroWeb": "Install our browser extension to auto-fill passwords from KeeWeb on different pages. The browser extension will connect to a KeeWeb tab in your browser to fetch passwords. Click here to download the extension:",
|
||||
"setBrowserNotEnabled": "Browser integration is not enabled, extensions won't be able to connect to KeeWeb. Use the checkboxes below to enable it:",
|
||||
"setBrowserEnablePerBrowser": "Enable browser integration using the checkboxes below:",
|
||||
"setBrowserFocusIfLocked": "Open KeeWeb if a browser extension tries to connect while there's no open file",
|
||||
"setBrowserFocusIfEmpty": "Show the entry selection screen if there's no match found by URL",
|
||||
"setBrowserOtherBrowsers": "Other browsers",
|
||||
"setBrowserExtensionFor": "for {}",
|
||||
"setBrowserExtensionNotSupported": "Not supported",
|
||||
"setBrowserExtensionHelp": "How to install?",
|
||||
"setBrowserExtensionInstall": "Install the extension",
|
||||
"setBrowserExtensionKPXCWarnHeader": "{} will stop working",
|
||||
"setBrowserExtensionKPXCWarnBody1": "Unfortunately it's not possible to connect one extension to several apps. If you connect the extension to KeeWeb, we'll overwrite its app association, which means that integration with {} will stop working. Even if you uncheck this checkbox, the association with {} won't be restored. To make it work again, configure browser integration in {} settings.",
|
||||
"setBrowserExtensionKPXCWarnBody2": "Configure the extension to use KeeWeb?",
|
||||
"setBrowserSessions": "Sessions",
|
||||
"setBrowserSessionsEmpty": "No connected sessions.",
|
||||
"setBrowserSessionsIntro": "These extensions are now connected to KeeWeb.",
|
||||
"setBrowserSessionsActiveTooltip": "Active session",
|
||||
"setBrowserSessionsActiveText": "This session is active. It can exchange data with KeeWeb based on the following permissions:",
|
||||
"setBrowserSessionsInactiveTooltip": "Inactive session",
|
||||
"setBrowserSessionsInactiveText": "This session is inactive. The extension is connected to KeeWeb, however, it hasn't tried to exchange data. When the extension requests anything, you will be able to choose what you want to share.",
|
||||
"setBrowserSessionsDeniedTooltip": "Access denied",
|
||||
"setBrowserSessionsDeniedText": "This session is inactive. The extension is connected to KeeWeb, however, you denied access to data.",
|
||||
"setBrowserSessionsConnectedDate": "Connected",
|
||||
"setBrowserSessionsTerminate": "Terminate this session",
|
||||
"setBrowserSessionsAccessToFiles": "Allow access to files",
|
||||
"setBrowserSessionsNoFileAccess": "The extension doesn't have access to any file, it won't be able to fetch passwords from KeeWeb.",
|
||||
"setBrowserSessionsPasswordsRead": "Passwords read",
|
||||
"setBrowserSessionsPasswordsWritten": "Passwords saved",
|
||||
"setDevicesTitle": "Devices",
|
||||
"setDevicesEnableUsb": "Enable interaction with USB devices",
|
||||
"setDevicesYubiKeyIntro": "It's recommended to read {} before using a YubiKey.",
|
||||
"setDevicesYubiKeyIntroLink": "this document",
|
||||
"setDevicesYubiKeyToolsDesc": "To be able to use YubiKey in this mode, you need to install a tool called {}.",
|
||||
"setDevicesYubiKeyToolsDesc2": "{} to get more information about this tool.",
|
||||
"setDevicesYubiKeyToolsDescLink": "Click here",
|
||||
"setDevicesYubiKeyToolsStatusChecking": "Checking if {} is installed",
|
||||
"setDevicesYubiKeyToolsStatusOk": "{} is installed",
|
||||
"setDevicesYubiKeyToolsStatusError": "{} is not installed or not working",
|
||||
"setDevicesYubiKeyOtpTitle": "One-time codes",
|
||||
"setDevicesYubiKeyOtpDesc": "YubiKey can be used to generate one-time codes for different services.",
|
||||
"setDevicesYubiKeyOtpShowIcon": "Show the icon on the start screen",
|
||||
"setDevicesYubiKeyOtpAutoOpen": "Automatically load one-time codes when there are open files",
|
||||
"setDevicesYubiKeyOtpMatchEntries": "Show matching one-time codes in entries",
|
||||
"setDevicesYubiKeyChalRespTitle": "Challenge-Response",
|
||||
"setDevicesYubiKeyChalRespDesc": "It's also possible to use a YubiKey in challenge-response mode, so that a piece of private key used to encrypt files resides on a YubiKey.",
|
||||
"setDevicesYubiKeyChalRespShow": "Show the option to use a YubiKey when opening files",
|
||||
"setDevicesYubiKeyStuckWorkaround": "Reconnect the YubiKey if it seems to be stuck during loading",
|
||||
"setDevicesYubiKeyRememberChalResp": "Remember challenge-response while the app is open",
|
||||
"setAboutTitle": "About",
|
||||
"setAboutBuilt": "This app is built with these awesome tools",
|
||||
"setAboutLic": "License",
|
||||
"setAboutLicComment": "The app itself and all included components are licensed under MIT license, unless otherwise specified",
|
||||
"setAboutFirst": "This is an open-source app created by {}",
|
||||
"setAboutSecond": "and licensed under {}.",
|
||||
"setAboutSource": "The source code and issues are on {}.",
|
||||
"setHelpFormat": "File Format",
|
||||
"setHelpFormatBody": "This is a port of {} app built with web technologies. It understands files in KeePass format (kdbx). You can create such files (password databases) either in KeePass or in this app. The file format is 100% compatible and should be understood by both apps.",
|
||||
"setHelpProblems": "Need some help?",
|
||||
"setHelpProblems1": "If something goes wrong, please {}",
|
||||
"setHelpProblems2": "or {}",
|
||||
"setHelpOpenIssue": "open an issue on GitHub",
|
||||
"setHelpContactLink": "contact the developer directly",
|
||||
"setHelpAppInfo": "App information",
|
||||
"setHelpOtherPlatforms": "Other platforms",
|
||||
"setHelpDesktopApps": "Desktop apps",
|
||||
"setHelpWebApp": "Web app",
|
||||
"setHelpUpdates": "Updates",
|
||||
"setHelpTwitter": "App twitter",
|
||||
"dropboxSetupDesc": "Some configuration is required to use Dropbox in a self-hosted app. Please create your own Dropbox app and fill in its key below.",
|
||||
"dropboxAppKey": "Dropbox app key",
|
||||
"dropboxAppKeyDesc": "Copy the key from your Dropbox app (Developer settings)",
|
||||
"dropboxAppKeyHint": "your app key",
|
||||
"dropboxAppSecret": "Dropbox app secret",
|
||||
"dropboxAppSecretDesc": "The secret can be found next to the app key",
|
||||
"dropboxFolder": "App folder",
|
||||
"dropboxFolderDesc": "If your app is linked to entire Dropbox (not app folder), set the folder with your kdbx files here",
|
||||
"dropboxFolderSettingsDesc": "Select any folder in your Dropbox where files will be stored (root folder by default)",
|
||||
"dropboxFolderPlaceholder": "default folder",
|
||||
"dropboxLink": "Link the app to",
|
||||
"dropboxLinkApp": "App folder (Apps/KeeWeb)",
|
||||
"dropboxLinkFull": "Full Dropbox or any folder",
|
||||
"dropboxLinkCustom": "Own Dropbox app",
|
||||
"gdriveSharedWithMe": "Shared with me",
|
||||
"gdriveSharedDrives": "Shared drives",
|
||||
"webdavSaveMethod": "Save method",
|
||||
"webdavSaveMove": "Upload a temporary file and move",
|
||||
"webdavSavePut": "Overwrite the kdbx file with PUT",
|
||||
"webdavNoLastModified": "Last-Modified HTTP header is absent",
|
||||
"webdavStatReload": "Always reload the file instead of relying on Last-Modified HTTP header",
|
||||
"launcherSave": "Save Passwords Database",
|
||||
"launcherFileFilter": "KeePass files",
|
||||
"authPopupRequired": "Pop-ups are blocked",
|
||||
"authPopupRequiredBody": "Please allow pop-ups in your browser or try again.",
|
||||
"exportFileInfo": "File information",
|
||||
"exportHtmlName": "Name",
|
||||
"exportHtmlDate": "Export date",
|
||||
"exportGenerator": "Software",
|
||||
"exportDescription": "This file is generated with {}.",
|
||||
"exportEntries": "Entries",
|
||||
"importCsvTitle": "Import from CSV",
|
||||
"importCsvRun": "Import",
|
||||
"importIgnoreField": "Ignore",
|
||||
"importTo": "Entries will be imported to",
|
||||
"importNewFile": "New file",
|
||||
"yubiKeyStuckError": "The YubiKey seems to be stuck, auto-repair can be enabled in app settings.",
|
||||
"yubiKeyNoKeyHeader": "YubiKey required",
|
||||
"yubiKeyNoKeyBody": "Please insert your YubiKey with serial number {}",
|
||||
"yubiKeySlot": "slot",
|
||||
"yubiKeyTouchRequestedHeader": "Touch your YubiKey",
|
||||
"yubiKeyTouchRequestedBody": "Please touch your YubiKey with serial number {}",
|
||||
"yubiKeyDisabledErrorHeader": "USB is disabled",
|
||||
"yubiKeyDisabledErrorBody": "YubiKey is required to open this file, please enable USB devices in settings.",
|
||||
"yubiKeyErrorWithCode": "YubiKey error, code {}.",
|
||||
"bioOpenAuthPrompt": "open \"{}\"",
|
||||
"extensionErrorNoOpenFiles": "No open files",
|
||||
"extensionErrorUserRejected": "The request was denied",
|
||||
"extensionErrorNoMatches": "No matching logins",
|
||||
"extensionErrorAlertDisplayed": "Cannot ask a question now because there's another dialog displayed, please try again",
|
||||
"extensionConnectHeader": "Extension data exchange",
|
||||
"extensionConnectIntro": "A browser extension that identifies itself as {} tries to exchange data with KeeWeb.",
|
||||
"extensionConnectUnknownActivity": "KeeWeb doesn't verify that the connected application is what it pretends to be. Approve the request only if you recognize this activity.",
|
||||
"extensionConnectFiles": "In this session, allow access to:",
|
||||
"extensionConnectAllOtherFiles": "All other files",
|
||||
"extensionConnectAllFiles": "All files",
|
||||
"extensionConnectAskGet": "Ask before returning passwords to the extension:",
|
||||
"extensionConnectAskGetMultiple": "if there's more than one match",
|
||||
"extensionConnectAskGetAlways": "always",
|
||||
"extensionConnectAskSave": "Ask before saving new passwords to KeeWeb:",
|
||||
"extensionConnectAskSaveAlways": "always",
|
||||
"extensionConnectAskSaveAuto": "when it's not possible to save automatically",
|
||||
"extensionConnectSettingsAreForSession": "Settings you select here are valid only for the active session. You can view and manage sessions in KeeWeb settings.",
|
||||
"extensionUnlockMessage": "Unlock to connect a browser extension",
|
||||
"extensionNewGroupHeader": "New group",
|
||||
"extensionNewGroupBody": "{} is trying to create a new group. Allow this?",
|
||||
"extensionNewGroupPath": "Group path",
|
||||
"extensionNewGroupFile": "This group will be created in:",
|
||||
"extensionSaveEntryHeader": "Save password",
|
||||
"extensionSaveEntryBody": "{} is trying to save a password. Allow this?",
|
||||
"extensionSaveEntryAuto": "Save other passwords automatically in this session",
|
||||
"extensionSaveEntryNewGroup": "new group",
|
||||
"extensionSelectPasswordFor": "Select a password for {}",
|
||||
"selectEntryHeader": "Select entry",
|
||||
"selectEntryEnterHint": "use the highlighted entry",
|
||||
"selectEntryTypingHint": "Start typing to filter",
|
||||
"selectEntryContains": "Contains text",
|
||||
"selectEntrySubdomains": "Subdomains",
|
||||
"selectEntryFieldHeader": "Select field",
|
||||
"selectEntryFieldTouch": "Press the button on your device to generate a one-time code."
|
||||
}
|
||||
@@ -1,50 +1,4 @@
|
||||
{
|
||||
"months": [
|
||||
"Januar",
|
||||
"Februar",
|
||||
"März",
|
||||
"April",
|
||||
"Mai",
|
||||
"Juni",
|
||||
"Juli",
|
||||
"August",
|
||||
"September",
|
||||
"Oktober",
|
||||
"November",
|
||||
"Dezember"
|
||||
],
|
||||
"monthsShort": [
|
||||
"Jan",
|
||||
"Feb",
|
||||
"Mär",
|
||||
"Apr",
|
||||
"Mai",
|
||||
"Jun",
|
||||
"Jul",
|
||||
"Aug",
|
||||
"Sep",
|
||||
"Okt",
|
||||
"Nov",
|
||||
"Dez"
|
||||
],
|
||||
"weekdays": [
|
||||
"Sonntag",
|
||||
"Montag",
|
||||
"Dienstag",
|
||||
"Mittwoch",
|
||||
"Donnerstag",
|
||||
"Freitag",
|
||||
"Samstag"
|
||||
],
|
||||
"weekdaysShort": [
|
||||
"So",
|
||||
"Mo",
|
||||
"Di",
|
||||
"Mi",
|
||||
"Do",
|
||||
"Fr",
|
||||
"Sa"
|
||||
],
|
||||
"retToApp": "zurück zur App",
|
||||
"name": "Name",
|
||||
"icon": "Icon",
|
||||
@@ -56,10 +10,11 @@
|
||||
"notes": "Notizen",
|
||||
"entry": "Eintrag",
|
||||
"group": "Gruppe",
|
||||
"noTitle": "Keinen Titel",
|
||||
"noTitle": "Kein Titel",
|
||||
"or": "oder",
|
||||
"history": "Verlauf",
|
||||
"template": "Template",
|
||||
"template": "Vorlage",
|
||||
"templates": "Vorlagen",
|
||||
"notImplemented": "Nicht implementiert",
|
||||
"saveChanges": "Änderungen speichern",
|
||||
"discardChanges": "Änderungen verwerfen",
|
||||
@@ -68,8 +23,22 @@
|
||||
"help": "Hilfe",
|
||||
"settings": "Einstellungen",
|
||||
"plugins": "Plugins",
|
||||
"ctrlKey": "Strg",
|
||||
"shiftKey": "Umschalt",
|
||||
"altKey": "Alt",
|
||||
"error": "Fehler",
|
||||
"oneMinute": "Eine Minute",
|
||||
"minutes": "{} Minuten",
|
||||
"oneHour": "Eine Stunde",
|
||||
"hours": "{} Stunden",
|
||||
"oneDay": "Ein Tag",
|
||||
"days": "{} Tage",
|
||||
"oneWeek": "Eine Woche",
|
||||
"oneMonth": "Ein Monat",
|
||||
"oneYear": "Ein Jahr",
|
||||
"cache": "Cache",
|
||||
"file": "Datei",
|
||||
"device": "Gerät",
|
||||
"webdav": "WebDAV",
|
||||
"dropbox": "Dropbox",
|
||||
"gdrive": "Google Drive",
|
||||
@@ -79,12 +48,31 @@
|
||||
"menuTrash": "Papierkorb",
|
||||
"menuSetGeneral": "Allgemein",
|
||||
"menuSetAbout": "Info",
|
||||
"menuSetDevices": "Geräte",
|
||||
"menuAlertNoTags": "Keine Tags",
|
||||
"menuAlertNoTagsBody": "Neue Tags können durch das Bearbeiten von Einträgen (Abschnitt \"Tags\") hinzugefügt werden.",
|
||||
"menuEmptyTrash": "Leere Papierkorb",
|
||||
"menuAlertNoTagsBody": "Neue Tags können durch das Bearbeiten von Einträgen (Abschnitt „Tags“) hinzugefügt werden.",
|
||||
"menuEmptyTrash": "Papierkorb leeren",
|
||||
"menuEmptyTrashAlert": "Papierkorb leeren?",
|
||||
"menuEmptyTrashAlertBody": "Die Einträge können nicht wiederhergestellt werden",
|
||||
"menuItemCollapsed": "Doppelklick zum Aufklappen",
|
||||
"menuRestoreApp": "{} öffnen",
|
||||
"menuQuitApp": "{} beenden",
|
||||
"sysMenuAboutKeeWeb": "Über {}",
|
||||
"sysMenuServices": "Dienste",
|
||||
"sysMenuHide": "{} ausblenden",
|
||||
"sysMenuHideOthers": "Andere ausblenden",
|
||||
"sysMenuUnhide": "Alle anzeigen",
|
||||
"sysMenuQuit": "{} beenden",
|
||||
"sysMenuEdit": "Bearbeiten",
|
||||
"sysMenuUndo": "Rückgängig",
|
||||
"sysMenuRedo": "Wiederherstellen",
|
||||
"sysMenuCut": "Ausschneiden",
|
||||
"sysMenuCopy": "Kopieren",
|
||||
"sysMenuPaste": "Einfügen",
|
||||
"sysMenuSelectAll": "Alles auswählen",
|
||||
"sysMenuWindow": "Fenster",
|
||||
"sysMenuMinimize": "Minimieren",
|
||||
"sysMenuClose": "Fenster schließen",
|
||||
"alertYes": "Ja",
|
||||
"alertNo": "Nein",
|
||||
"alertOk": "OK",
|
||||
@@ -92,6 +80,7 @@
|
||||
"alertSignIn": "Anmelden",
|
||||
"alertCopy": "Kopieren",
|
||||
"alertClose": "Schließen",
|
||||
"alertDoNotAsk": "Nicht mehr nachfragen",
|
||||
"appBeta": "ACHTUNG: Beta-Version, nur für Vorschau-Zwecke",
|
||||
"footerOpen": "Öffnen / Neu",
|
||||
"footerSyncError": "Synchronisierungsfehler",
|
||||
@@ -100,19 +89,21 @@
|
||||
"genLen": "Länge",
|
||||
"genNewPass": "Neues Passwort",
|
||||
"genPresetDefault": "Standardeinstellung",
|
||||
"genPresetDerived": "unverändert",
|
||||
"genPresetDerived": "Wie das alte Passwort",
|
||||
"genPresetPronounceable": "aussprechbar",
|
||||
"genPresetMed": "durchschnittlich lang",
|
||||
"genPresetMed": "Durchschnittliche Länge",
|
||||
"genPresetLong": "lang",
|
||||
"genPresetPin4": "4-stellige PIN",
|
||||
"genPresetMac": "MAC-Adresse",
|
||||
"genPresetHash128": "128-Bit Hash",
|
||||
"genPresetHash256": "256-Bit Hash",
|
||||
"genPresetHash128": "128-Bit-Hash",
|
||||
"genPresetHash256": "256-Bit-Hash",
|
||||
"genHidePass": "Passwort ausblenden",
|
||||
"genShowPass": "Passwort einblenden",
|
||||
"grpTitle": "Gruppe",
|
||||
"grpSearch": "Suche für Einträge in dieser Gruppe aktivieren",
|
||||
"grpAutoType": "Auto-Type aktivieren",
|
||||
"grpAutoTypeSeq": "Auto-Type Abfolge",
|
||||
"grpAutoTypeSeqDefault": "Standard Auto-Type Abfolge verwenden",
|
||||
"grpAutoTypeSeq": "Auto-Type-Sequenz",
|
||||
"grpAutoTypeSeqDefault": "Standard-Auto-Type-Sequenz verwenden",
|
||||
"grpTrash": "Gruppe und alle enthaltenen Einträge löschen",
|
||||
"tagTitle": "Tag",
|
||||
"tagTrash": "Diesen Tag von allen Einträgen entfernen",
|
||||
@@ -120,14 +111,14 @@
|
||||
"tagTrashQuestion": "Diesen Tag von allen Einträgen entfernen?",
|
||||
"tagTrashQuestionBody": "Der Tag wird von allen Einträgen entfernt. Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"tagExists": "Tag existiert bereits",
|
||||
"tagExistsBody": "Ein Tag mit diesem Namen ist bereits vorhanden. Bitte wählen Sie einen anderen Namen.",
|
||||
"tagExistsBody": "Ein Tag mit diesem Namen ist bereits vorhanden. Bitte einen anderen Namen wählen.",
|
||||
"tagBadName": "Ungültiger Name",
|
||||
"tagBadNameBody": "Der Tag-Name darf nicht die folgenden Zeichen enthalten: {}",
|
||||
"tagBadNameBody": "Der Tag-Name darf folgende Zeichen nicht enthalten: {}. Bitte diese entfernen.",
|
||||
"genPsTitle": "Generator-Voreinstellungen",
|
||||
"genPsCreate": "Neue Voreinstellung",
|
||||
"genPsDelete": "Voreinstellung löschen",
|
||||
"genPsNew": "Voreinstellung",
|
||||
"genPsEnabled": "In Voreinstellungs-Liste anzeigen",
|
||||
"genPsEnabled": "In Voreinstellungsliste anzeigen",
|
||||
"genPsDefault": "Standardmäßig ausgewählt",
|
||||
"genPsDefaultLength": "Standardlänge",
|
||||
"genPsUpper": "Lateinische Großbuchstaben",
|
||||
@@ -139,12 +130,17 @@
|
||||
"genPsAmbiguous": "Ähnliche Zeichen",
|
||||
"genPsInclude": "Zusätzliche Symbole angeben",
|
||||
"genPsExample": "Beispiel für ein generiertes Passwort",
|
||||
"genPsPattern": "Muster",
|
||||
"genPsPatternHelp": "Mit Mustern können eigene Regeln zur Auswahl von Zeichen festgelegt werden. Beispielsweise werden mit 1-AA Passwörter generiert, die mit einer Ziffer starten, gefolgt von einem Bindestrich und zwei Großbuchstaben. Folgende Symbole stehen zur Verfügung:",
|
||||
"genPsAllRanges": "Alle Zeichen",
|
||||
"genPsIncluded": "Zusätzliche oben hinzugefügte Zeichen",
|
||||
"keyChangeTitleRemote": "Hauptschlüssel geändert",
|
||||
"keyChangeMessageRemote": "Der Hauptschlüssel für diese Datenbank wurde geändert. Bitte geben Sie das neue Passwort ein.",
|
||||
"keyChangeMessageRemote": "Der Hauptschlüssel für diese Datenbank wurde geändert. Bitte das neue Passwort eingeben.",
|
||||
"keyChangeTitleExpired": "Hauptschlüssel abgelaufen",
|
||||
"keyChangeMessageExpired": "Der Hauptschlüssel für diese Datenkbank ist abgelaufen. Bitte geben Sie einen neuen Schlüssel ein.",
|
||||
"keyChangeRepeatPassword": "Passwort (Wiederholung)",
|
||||
"iconFavTitle": "Website-Icon herunterladen und verwenden",
|
||||
"keyEnter": "Enter",
|
||||
"iconFavTitle": "Website-Symbol herunterladen und verwenden",
|
||||
"iconSelCustom": "Eigenes Icon auswählen",
|
||||
"listEmptyTitle": "Leer",
|
||||
"listEmptyAdd": "Zum Anlegen neuer Einträge den obigen {} Button benutzen",
|
||||
@@ -152,9 +148,9 @@
|
||||
"listNoWebsite": "keine Website",
|
||||
"listNoUser": "kein Benutzer",
|
||||
"listNoAttachments": "keine Anhänge",
|
||||
"listAddTemplateHeader": "Templates",
|
||||
"listAddTemplateBody1": "Templates ermöglichen es, Einträge mit nur einem Klick zu erstellen. Fügen Sie etwas zum Template-Eintrag hinzu und klicken Sie auf {}, um dieses Template zu benutzen.",
|
||||
"listAddTemplateBody2": "Sie können Ihre Templates in der Gruppe \"{}\" finden.",
|
||||
"listAddTemplateHeader": "Vorlagen",
|
||||
"listAddTemplateBody1": "Vorlagen ermöglichen es, Einträge mit nur einem Klick zu erstellen. Fügen Sie etwas zum Vorlage-Eintrag hinzu und klicken Sie auf {}, um diese Vorlage zu verwenden.",
|
||||
"listAddTemplateBody2": "Sie können Ihre Vorlagen in der Gruppe “{}” finden.",
|
||||
"searchAddNew": "Hinzufügen",
|
||||
"searchSort": "Sortierung",
|
||||
"searchCreated": "Erstelldatum",
|
||||
@@ -172,27 +168,31 @@
|
||||
"searchOptions": "Suchoptionen",
|
||||
"searchCase": "Groß/klein",
|
||||
"searchRegex": "RegEx",
|
||||
"searchRank": "Automatisch",
|
||||
"searchExact": "Exakt",
|
||||
"openOpen": "Öffnen",
|
||||
"openNew": "Neu",
|
||||
"openNewFile": "Neu",
|
||||
"openMore": "Mehr",
|
||||
"openDemo": "Demo",
|
||||
"openGenerate": "Generieren",
|
||||
"openXml": "XML importieren",
|
||||
"openCaps": "Feststelltaste ist aktiviert",
|
||||
"openClickToOpen": "Hier klicken, um eine Datei zu öffnen",
|
||||
"openKeyFile": "Schlüsseldatei",
|
||||
"openKeyFileDropbox": "(von Dropbox)",
|
||||
"openDropHere": "Dateien hier ablegen",
|
||||
"openFailedRead": "Konnte die Datei nicht lesen",
|
||||
"openFailedRead": "Datei konnte nicht gelesen werden",
|
||||
"openNothingFound": "Nichts gefunden",
|
||||
"openNothingFoundBody": "Es konnten keine kompatiblen Dateien gefunden werden.",
|
||||
"openSelectFile": "Datei auswählen",
|
||||
"openSelectFileBody": "Wählen Sie eine Datei zum Öffnen aus",
|
||||
"openPassFor": "Passwort für",
|
||||
"openRemoveLastQuestion": "Lokale Datei löschen?",
|
||||
"openRemoveLastQuestionBody": "Die Datei, die Sie löschen möchten, ist nur innerhalb der App gespeichert. Möchten Sie sie unwiederbringlich löschen?",
|
||||
"openRemoveLastQuestionBody": "Die Datei, die Sie löschen möchten, ist nur innerhalb der App gespeichert. Möchten Sie diese unwiederbringlich löschen?",
|
||||
"openRemoveLastQuestionModBody": "Die Datei, die Sie löschen möchten, beinhaltet lokale Änderungen. Möchten Sie diese Änderungen verwerfen und die Datei löschen?",
|
||||
"openLocalFile": "Lokale Datei",
|
||||
"openLocalFileBody": "Sie sind dabei, eine Datei zu öffnen, die innerhalb der App gespeichert wird. Wenn Sie an dieser Datei Änderungen vornehmen, werden diese nicht im Dateisystem gespeichert. Um die Datei mit den Änderungen zu erhalten, können Sie sie aus den Einstellungen heraus exportieren.",
|
||||
"openLocalFileBody": "Sie sind dabei, eine Datei zu öffnen, die innerhalb der App gespeichert wird. Wenn Sie an dieser Datei Änderungen vornehmen, werden diese nicht im Dateisystem gespeichert. Um die Datei mit den Änderungen zu erhalten, können Sie diese aus den Einstellungen heraus exportieren.",
|
||||
"openLocalFileDontShow": "Nicht wieder anzeigen",
|
||||
"openWrongFile": "Inkompatibles Dateiformat",
|
||||
"openWrongFileBody": "Dieses Dateiformat ist nicht kompatibel. Die App unterstützt KeePass-Datenbanken im kdbx-Format.",
|
||||
@@ -201,21 +201,30 @@
|
||||
"openUrl": "URL",
|
||||
"openUrlDesc": "https://server/pfad/datei.kdbx, oder einfach datei.kdbx",
|
||||
"openUser": "Benutzername",
|
||||
"openUserDesc": "WebDAV Server Benutzername (falls erforderlich)",
|
||||
"openUserDesc": "WebDAV-Server-Benutzername (falls erforderlich)",
|
||||
"openUserPlaceholder": "kein Benutzername",
|
||||
"openPass": "Passwort",
|
||||
"openPassDesc": "WebDAV Server-Passwort (nicht das KDBX-Datei-Passwort)",
|
||||
"openPassDesc": "WebDAV-Server-Passwort (nicht das KDBX-Datei-Passwort)",
|
||||
"openPassPlaceholder": "kein Passwort",
|
||||
"openConfigError": "Fehler: {}",
|
||||
"openConfigErrorNotFound": "Datei nicht gefunden",
|
||||
"openError": "Fehler",
|
||||
"openErrorDescription": "Beim Öffnen der Datei ist ein Fehler aufgetreten",
|
||||
"openErrorFileNotFound": "Datei nicht gefunden",
|
||||
"openListErrorBody": "Dateien konnten nicht geladen werden",
|
||||
"openShowAllFiles": "Alle Dateien anzeigen",
|
||||
"openFileNoCacheError": "Die Datei wurde im Cache-Speicher nicht gefunden. Möglicherweise wurde Ihr Browser-Speicher geleert. Um die Datei zu öffnen, entfernen Sie sie aus KeeWeb und fügen Sie sie erneut hinzu",
|
||||
"openChalRespHeader": "Challenge-Response",
|
||||
"openChalRespLoading": "Liste mit YubiKeys wird geladen",
|
||||
"openChalRespSelectYubiKey": "Wählen Sie den YubiKey aus, den Sie benutzen wollen",
|
||||
"openChalRespErrorEmpty": "Keine YubiKeys gefunden.",
|
||||
"openChalRespErrorEmptyMac": "Verwenden Sie dieses Feature zum ersten Mal auf MacOS? KeeWeb muss in Ihren Sicherheitseinstellung im Input-Monitoring-Abschnitt hinzugefügt werden.",
|
||||
"detAttDownload": "Umschalttaste + Klick auf den Anhang-Button zum Herunterladen oder",
|
||||
"detAttDelToRemove": "Backspace zum entfernen",
|
||||
"detAttDelToRemove": "Backspace zum Entfernen",
|
||||
"detAttSave": "Speichern unter …",
|
||||
"detEmpty": "Ihre Passwörter werden hier angezeigt",
|
||||
"detGroupRestore": "Ziehen Sie diese Gruppe in eine andere Gruppe als den Papierkorb, um sie wiederherzustellen.",
|
||||
"detHistoryClickPoint": "Klicken Sie auf einen der Punkte im Verlauf um den entsprechenden Zustand des Eintrags anzuzeigen",
|
||||
"detHistoryClickPoint": "Klicken Sie auf einen der Punkte im Verlauf, um den entsprechenden Zustand des Eintrags anzuzeigen",
|
||||
"detHistoryReturn": "zurück zum Eintrag",
|
||||
"detHistoryRevert": "Auf diese Version zurücksetzen",
|
||||
"detHistoryDel": "Version löschen",
|
||||
@@ -228,9 +237,15 @@
|
||||
"detHistoryNoTitle": "keine Bezeichnung",
|
||||
"detHistoryCurState": "aktuelle Version",
|
||||
"detHistoryCurUnsavedState": "aktuelle ungespeicherte Version",
|
||||
"detHistoryRevertAlert": "Zu diesem Stand zurückkehren?",
|
||||
"detHistoryRevertAlertBody": "Ihr aktueller Stand wird in der Historie gesichert.",
|
||||
"detHistoryDeleteAlert": "Diesen Stand löschen?",
|
||||
"detHistoryDeleteAlertBody": "Sie können ihn nicht wiederherstellen.",
|
||||
"detHistoryDiscardChangesAlert": "Änderungen an diesem Eintrag verwerfen?",
|
||||
"detHistoryDiscardChangesAlertBody": "Ungespeicherte Änderungen gehen dabei unwiderruflich verloren.",
|
||||
"detBackToList": "zurück zur Liste",
|
||||
"detSetIconColor": "Farbe ändern",
|
||||
"detSetIcon": "Icon ändern",
|
||||
"detSetIcon": "Symbol ändern",
|
||||
"detDropAttachments": "Anhänge hier ablegen",
|
||||
"detDelEntry": "Löschen",
|
||||
"detDelEntryPerm": "Dauerhaft löschen",
|
||||
@@ -243,7 +258,9 @@
|
||||
"detAttachments": "Anhänge",
|
||||
"detDelFromTrash": "Aus dem Papierkorb löschen?",
|
||||
"detDelFromTrashBody": "Es wird keine Möglichkeit zur Wiederherstellung geben.",
|
||||
"detDelFromTrashBodyHint": "Um alle Dateien aus dem Papierkorb zu entfernen, klicken Sie auf das \"Papierkorb leeren\"-Icon im Papierkorb-Menüpunkt.",
|
||||
"detDelFromTrashBodyHint": "Um alle Dateien aus dem Papierkorb zu entfernen, klicken Sie auf das „Papierkorb leeren“-Symbol im Papierkorb-Menüpunkt.",
|
||||
"detDelToTrash": "Diesen Eintrag löschen?",
|
||||
"detDelToTrashBody": "Der Eintrag wird in den Papierkorb verschoben.",
|
||||
"detFieldCopied": "Kopiert",
|
||||
"detFieldCopiedTime": "Kopiert für {} Sekunden",
|
||||
"detCopyHint": "Sie können den Inhalt eines Feldes kopieren, indem Sie auf seine Beschriftung klicken",
|
||||
@@ -255,40 +272,61 @@
|
||||
"detMenuAddField": "{} hinzufügen",
|
||||
"detMenuCopyPassword": "Passwort kopieren",
|
||||
"detMenuCopyUser": "Benutzer kopieren",
|
||||
"detSetupOtp": "Einweg-Passwörter",
|
||||
"detMenuCopyOtp": "Einmal-Code kopieren",
|
||||
"detSetupOtp": "Einmal-Codes",
|
||||
"detClone": "Kopie erstellen",
|
||||
"detClonedName": "Kopieren",
|
||||
"detCopyEntryToClipboard": "Alle Felder kopieren",
|
||||
"detClonedName": "Kopie",
|
||||
"detAutoType": "Auto-Type",
|
||||
"detAutoTypeSettings": "Auto-Type Einstellungen",
|
||||
"detAutoTypeEnabled": "Auto-Type für diesen Eintrag aktivieren",
|
||||
"detAutoTypeSequence": "Tastensequenz",
|
||||
"detAutoTypeSequence": "Tastenfolge",
|
||||
"detAutoTypeInput": "Eingabe",
|
||||
"detAutoTypeShortcutsDesc": "{} oder {} während die App inaktiv ist",
|
||||
"detAutoTypeObfuscation": "Echte Tastenanschläge mit zufälligen mischen",
|
||||
"detAutoTypeWindow": "Fenster",
|
||||
"detAutoTypeInputWindow": "Fenster-Titel",
|
||||
"detAutoTypeInputWindow": "Fenstertitel",
|
||||
"detSetupOtpAlert": "QR-Code einscannen",
|
||||
"detSetupOtpAlertBody": "Bitte kopieren Sie den QR-Code, der auf der Autorisierungsseite angezeigt wird.",
|
||||
"detSetupOtpAlertBody1": "1. Gehen Sie zur Autorisierungsseite",
|
||||
"detSetupOtpAlertBody2": "2. Erstellen Sie einen Screenshot des QR-Codes {}",
|
||||
"detSetupOtpAlertBody3": "3. Fügen Sie ihn hier ein {}",
|
||||
"detSetupOtpAlertBody3Mobile": "3. Markieren Sie ihn oder scannen Sie ihn mit Ihrer Kamera mit dem untenstehenden Markieren/Scannen-Button",
|
||||
"detSetupOtpAlertBody4": "Wenn Sie den Code nicht einscannen können, klicken Sie bitte auf \"Code manuell eingeben\"",
|
||||
"detSetupOtpAlertBody4": "Wenn Sie den Code nicht einscannen können, klicken Sie bitte auf “Code manuell eingeben”",
|
||||
"detSetupOtpManualButton": "Code manuell eingeben",
|
||||
"detSetupOtpScanButton": "Markieren/Scannen",
|
||||
"detSetupOtpAlertBodyWith": "mit {}",
|
||||
"detOtpImageError": "Fehler beim Einlesen des Bildes",
|
||||
"detOtpImageErrorBody": "Entschuldigung, wir konnten das Bildformat nicht lesen. Bitte kontaktieren Sie den Hersteller mit Einzelheiten zu diesem Fehler.",
|
||||
"detOtpImageReading": "QR-Code wird eingelesen...",
|
||||
"detOtpImageReading": "QR-Code wird eingelesen …",
|
||||
"detOtpQrError": "QR-Code Lesefehler",
|
||||
"detOtpQrErrorBody": "Der QR-Code konnte leider nicht eingelesen werden. Bitte versuchen Sie es noch einmal oder kontaktieren Sie den Hersteller mit Einzelheiten zum aufgetretenen Fehler.",
|
||||
"detOtpQrWrong": "Fehlerhafter QR-Code",
|
||||
"detOtpQrWrongBody": "Ihr QR-Code wurde erfolgreich eingelesen, enthält jedoch kein Einweg-Passwort.",
|
||||
"detOtpQrWrongBody": "Ihr QR-Code wurde erfolgreich eingelesen, enthält jedoch keinen Einmal-Code.",
|
||||
"detOtpField": "Einmal-Code",
|
||||
"detOtpClickToTouch": "Zum Erzeugen hier klicken",
|
||||
"detOtpGenerating": "Wird erzeugt …",
|
||||
"detOtpTouch": "Berühren Sie Ihren {}",
|
||||
"detLockField": "Dieses Feld sperren, damit der Inhalt nicht durchsuchbar ist und angezeigt wird. Um ihn anzuzeigen, muss er angeklickt werden.",
|
||||
"detUnlockField": "Dieses Feld entsperren. Dies führt dazu, dass der Inhalt direkt angezeigt wird und durchsuchbar ist.",
|
||||
"detRevealField": "Anzeigen",
|
||||
"detHideField": "Verbergen",
|
||||
"detAutoTypeField": "Auto-Type",
|
||||
"detIssuesHideTooltip": "Diese Warnung verstecken",
|
||||
"detIssueWeakPassword": "Das Passwort ist schwach, es wird empfohlen es zu ändern.",
|
||||
"detIssuePoorPassword": "Das Passwort ist schwach, es wird dringend empfohlen es zu ändern.",
|
||||
"detIssuePwnedPassword": "Dieses Passwort wurde laut {} in einem Datenleck gefunden, es wird empfohlen das Passwort zu ändern.",
|
||||
"detIssuePasswordCheckError": "Es gab einen Fehler bei der online-überprüfung des Passwort.",
|
||||
"detIssueOldPassword": "Das Passwort ist alt.",
|
||||
"detIssueCloseAlertHeader": "Verstecke Passwortmeldungen",
|
||||
"detIssueCloseAlertBody": "Es gibt verschiedene Möglichkeiten diese Warnung zu verstecken:",
|
||||
"detIssueCloseAlertEntry": "Nicht für diesen Eintrag anzeigen.",
|
||||
"detIssueCloseAlertSettings": "Globale Einstellungen anpassen",
|
||||
"autoTypeEntryFields": "Feld-Platzhalter",
|
||||
"autoTypeModifiers": "Hilfstasten",
|
||||
"autoTypeKeys": "Tasten",
|
||||
"autoTypeLink": "mehr...",
|
||||
"autoTypeError": "Auto-Type Fehler",
|
||||
"autoTypeLink": "mehr …",
|
||||
"autoTypeError": "Auto-Type-Fehler",
|
||||
"autoTypeErrorGeneric": "Während der Ausführung von Auto-Type ist ein Fehler aufgetreten: {}",
|
||||
"autoTypeErrorGlobal": "Um globale Tastenkürzel zu nutzen, wechseln Sie bitte zu der Anwendung, in der Sie das Passwort eingeben wollen.",
|
||||
"autoTypeErrorNotInstalled": "{} ist nicht installiert",
|
||||
@@ -296,6 +334,11 @@
|
||||
"autoTypeMsgNoWindow": "Wir konnten den Titel des aktiven Fensters leider nicht auslesen. Fangen Sie einfach an zu tippen, um nach dem richtigen Eintrag zu suchen.",
|
||||
"autoTypeMsgMatchedByWindow": "Passwort auswählen für {}",
|
||||
"autoTypeNoMatches": "keine passenden Einträge",
|
||||
"autoTypeSelectionHint": "Die Auto-Type-Sequenz eintippen",
|
||||
"autoTypeSelectionHintAction": "Nur das Passwort eintippen",
|
||||
"autoTypeSelectionHintOpt": "Nur den Benutzernamen eintippen",
|
||||
"autoTypeSelectionHintShift": "Andere Felder",
|
||||
"autoTypeSelectionOtp": "Einmal-Code",
|
||||
"appSecWarn": "Unsichere Verbindung!",
|
||||
"appSecWarnBody1": "Sie haben diese App über eine ungesicherte Verbindung geöffnet, Sie könnten beobachtet und Ihre Passwörter könnten gestohlen werden. Wir empfehlen Ihnen dringend, die App hier nicht weiter zu nutzen, wenn Sie nicht genau wissen, was Sie tun.",
|
||||
"appSecWarnBody2": "Ihre Datenbank ist zwar verschlüsselt, aber es kann nicht sichergestellt werden, dass die App nicht auf dem Web vom Server zu Ihrem Browser manipuliert wurde.",
|
||||
@@ -309,22 +352,28 @@
|
||||
"appSaveError": "Fehler beim Speichern",
|
||||
"appSaveErrorBody": "Automatisches Speichern fehlgeschlagen",
|
||||
"appSaveErrorBodyMul": "Automatisches Speichern fehlgeschlagen:",
|
||||
"appSaveErrorExitLoseChanges": "Schließen und alle Änderungen verlieren",
|
||||
"appSaveErrorExitLoseChangesBody": "Du kannst entweder die App schließen und alle Änderungen verlieren, oder gehe in die Dateieinstellungen und exportiere die Daten",
|
||||
"appSettingsError": "Fehler beim Laden der App",
|
||||
"appSettingsErrorBody": "Es gab einen Fehler beim Laden der App-Einstellungen. Bitte überprüfen Sie die App URL oder kontaktieren Sie Ihren Systemadministrator.",
|
||||
"appNotSupportedError": "Ihr Browser wird nicht unterstützt.",
|
||||
"appSettingsErrorBody": "Es gab einen Fehler beim Laden der App-Einstellungen. Bitte überprüfen Sie die App-URL oder kontaktieren Sie Ihren Systemadministrator.",
|
||||
"appNotSupportedError": "Ihr Browser unterstützt einige wichtige Funktionen nicht, die für diese App benötigt werden.",
|
||||
"appTabWarn": "Zu viele Tabs",
|
||||
"appTabWarnBody": "KeeWeb kann nicht in mehreren Browser-Tabs gleichzeitig genutzt werden, bitte schließen Sie diesen Tab.",
|
||||
"appRightsAlert": "Schreibschutz für KeeWeb wird eingerichtet",
|
||||
"appRightsAlertBody1": "Ihre KeeWeb-Anwendung ist nicht gegen Änderungen von außen gesichert. Um diesen Schreibschutz einzurichten, benötigt die App Administratorrechte.",
|
||||
"appRightsAlertBody2": "Sie möchten der App keine Administratorrechte erteilen? Mit folgendem Befehl können Sie den Schreibschutz auch von Hand im Terminal aktivieren",
|
||||
"appBrowserAuthComplete": "Die Authentifizierung ist abgeschlossen, Sie können diesen Tab jetzt schließen.",
|
||||
"setGenTitle": "Allgemeine Einstellungen",
|
||||
"setGenUpdate": "Update",
|
||||
"setGenNewVersion": "Eine neue Version der App ist verfügbar und wurde heruntergeladen",
|
||||
"setGenReleaseNotes": "Versionshinweise anzeigen",
|
||||
"setGenReloadToUpdate": "Seite aktualisieren, um das Update durchzuführen",
|
||||
"setGenUpdateManual": "Eine neue Version ist verfügbar. Sie wird nach Updates suchen und sie automatisch installieren, ein automatisches Update von Ihrer Version ist aber nicht möglich.",
|
||||
"setGenUpdateManual": "Eine neue Version ist verfügbar. Sie wird nach Updates suchen und diese automatisch installieren, ein automatisches Update Ihrer Version ist aber nicht möglich.",
|
||||
"setGenDownloadUpdate": "Update herunterladen",
|
||||
"setGenUpdateAuto": "Automatisch herunterladen und installieren",
|
||||
"setGenUpdateCheck": "Auf Updates überprüfen aber nicht installieren",
|
||||
"setGenUpdateCheck": "Auf Updates überprüfen, aber nicht installieren",
|
||||
"setGenNoUpdate": "Nie nach Updates suchen",
|
||||
"setGenUpdateChecking": "Suche nach Updates",
|
||||
"setGenUpdateChecking": "Updates werden gesucht",
|
||||
"setGenCheckUpdate": "Nach Updates suchen",
|
||||
"setGenErrorChecking": "Updatesuche fehlgeschlagen",
|
||||
"setGenLastCheckSuccess": "Letzte erfolgreiche Überprüfung am {}",
|
||||
@@ -332,41 +381,60 @@
|
||||
"setGenCheckedAt": "Überprüft am",
|
||||
"setGenLatestVer": "Sie haben die aktuellste Version",
|
||||
"setGenNewVer": "Neue Version {} verfügbar, veröffentlicht am",
|
||||
"setGenDownloadingUpdate": "Update wird heruntergeladen...",
|
||||
"setGenExtractingUpdate": "Update wird entpackt...",
|
||||
"setGenDownloadingUpdate": "Update wird heruntergeladen …",
|
||||
"setGenExtractingUpdate": "Update wird entpackt …",
|
||||
"setGenCheckErr": "Beim Herunterladen der neuen Version ist ein Fehler aufgetreten",
|
||||
"setGenNeverChecked": "Nie nach Updates gesucht",
|
||||
"setGenRestartToUpdate": "App neu starten, um das Update durchzuführen",
|
||||
"setGenDownloadAndRestart": "Update herunterladen und App neu starten",
|
||||
"setGenAppearance": "Oberfläche",
|
||||
"setGenTheme": "Theme",
|
||||
"setGenThemeFb": "Flat blue",
|
||||
"setGenThemeDb": "Dark brown",
|
||||
"setGenThemeWh": "White",
|
||||
"setGenThemeDefault": "Standard",
|
||||
"setGenThemeDark": "Dunkel",
|
||||
"setGenThemeLight": "Hell",
|
||||
"setGenThemeBlue": "Flach blau",
|
||||
"setGenThemeFb": "Mattblau",
|
||||
"setGenThemeBl": "Hell blau",
|
||||
"setGenThemeBrown": "Brownie",
|
||||
"setGenThemeDb": "Dunkelbraun",
|
||||
"setGenThemeLb": "Hell braun",
|
||||
"setGenThemeTerminal": "Terminal",
|
||||
"setGenThemeTe": "Terminal",
|
||||
"setGenThemeHc": "High contrast",
|
||||
"setGenThemeSd": "Solarized dark",
|
||||
"setGenThemeSl": "Solarized light",
|
||||
"setGenThemeLt": "Hell Terminal",
|
||||
"setGenThemeHighContrast": "Hoher Kontrast",
|
||||
"setGenThemeHc": "Hoher Kontrast",
|
||||
"setGenThemeDc": "Dunkel Kontrast",
|
||||
"setGenThemeSol": "Solarized",
|
||||
"setGenThemeSd": "Solarisiert dunkel",
|
||||
"setGenThemeSl": "Solarisiert hell",
|
||||
"setGenMoreThemes": "Mehr Themes",
|
||||
"setGenAutoSwitchTheme": "Automatisch zwischen hellem und dunklem Thema umschalten, sofern möglich",
|
||||
"setGenLocale": "Sprache",
|
||||
"setGenLocOther": "Andere Sprachen sind als Plugins verfügbar.",
|
||||
"setGenFontSize": "Schriftgröße",
|
||||
"setGenFontSizeNormal": "Standard",
|
||||
"setGenFontSizeLarge": "Groß",
|
||||
"setGenFontSizeLargest": "Größte",
|
||||
"setGenTitlebarStyle": "Fenster-Stil (erfordert Neustart des Programms)",
|
||||
"setGenTitlebarStyle": "Fensterstil (erfordert Neustart des Programms)",
|
||||
"setGenTitlebarStyleDefault": "Standard",
|
||||
"setGenTitlebarStyleHidden": "Eigener Titel",
|
||||
"setGenTitlebarStyleHiddenInset": "Eigener Titel, verschiebbares Fenster",
|
||||
"setGenShowSubgroups": "Einträge aus allen Untergruppen anzeigen",
|
||||
"setGenTableView": "Einträge in Tabellenansicht anzeigen",
|
||||
"setGenColorfulIcons": "Eigene Icons in der Listenansicht farbig anzeigen",
|
||||
"setGenColorfulIcons": "Eigene Symbole in der Listenansicht farbig anzeigen",
|
||||
"setGenUseMarkdown": "Markdown in Notizen",
|
||||
"setGenUseGroupIconForEntries": "Für neue Einträge automatisch das Gruppensymbol verwenden",
|
||||
"setGenDirectAutotype": "Wenn nur ein passender Eintrag gefunden wird, diesen automatisch für Auto-Type auswählen",
|
||||
"setGenFunction": "Arbeitsweise",
|
||||
"setGenAutoSync": "Automatisch speichern und synchronisieren",
|
||||
"setGenAutoSyncOnClose": "Beim Schließen speichern und synchronisieren",
|
||||
"setGenAutoSyncTimer": "Regelmäßig speichern und synchronisieren",
|
||||
"setGenAutoSyncTimerOff": "Abgeschaltet",
|
||||
"setGenAutoSyncTimerInterval": "Alle {} Minuten",
|
||||
"setGenRememberKeyFiles": "Schlüsseldateien merken",
|
||||
"setGenNoRememberKeyFiles": "Nicht merken",
|
||||
"setGenRememberKeyFilesData": "Im internen App-Speicher ablegen",
|
||||
"setGenRememberKeyFilesPath": "Nur Speicherort der Schlüsseldateien merken",
|
||||
"setGenLockInactive": "Automatisch sperren, wenn die App inaktiv ist",
|
||||
"setGenLockInactive": "Wenn die App inaktiv ist",
|
||||
"setGenNoAutoLock": "Nicht automatisch sperren",
|
||||
"setGenLockMinutes": "Nach {} Minuten",
|
||||
"setGenLockHour": "In einer Stunde",
|
||||
@@ -377,37 +445,67 @@
|
||||
"setGenClearSeconds": "In {} Sekunden",
|
||||
"setGenClearMinute": "Nach einer Minute",
|
||||
"setGenMinInstead": "App beim Schließen stattdessen minimieren",
|
||||
"setGenLockMinimize": "Beim Minimieren automatisch sperren",
|
||||
"setGenLockCopy": "Nach dem Kopieren eines Passworts automatisch sperren",
|
||||
"setGenLockAutoType": "Automatisches Sperren bei Auto-Type",
|
||||
"setGenLock": "Automatisch sperren",
|
||||
"setGenLockMinimize": "Beim Minimieren der App",
|
||||
"setGenLockCopy": "Nach dem Kopieren eines Passworts",
|
||||
"setGenLockAutoType": "Bei Auto-Type",
|
||||
"setGenLockOrSleep": "Bei Aktivierung von Bildschirmsperre oder Ruhezustand",
|
||||
"setGenStorage": "Synchronisierung",
|
||||
"setGenDisableOfflineStorage": "Geladene Dateien nicht in Offline-Speicher zwischenspeichern",
|
||||
"setGenStorageLogout": "Abmelden",
|
||||
"setGenShowAdvanced": "Erweiterte Einstellungen anzeigen",
|
||||
"setGenDevTools": "Entwicklerwerkzeuge anzeigen",
|
||||
"setGenTryBeta": "Beta-Version bis zum Schließen der App ausprobieren",
|
||||
"setGenTryBeta": "Betaversion bis zum Schließen der App ausprobieren",
|
||||
"setGenTryBetaWarning": "Ungespeicherte Dateien",
|
||||
"setGenTryBetaWarningBody": "Bitte speichern Sie alle Dateien und Klicken Sie erneut auf diesen Button",
|
||||
"setGenTryBetaWarningBody": "Bitte speichern Sie alle Dateien und klicken erneut auf diese Schaltfläche",
|
||||
"setGenShowAppLogs": "App-Logs anzeigen",
|
||||
"setGenReloadApp": "App neu laden",
|
||||
"setGenFieldLabelDblClickAutoType": "Auto-Type durch Anklicken von Beschriftungen aktivieren",
|
||||
"setGenTouchId": "Fingerabdruck",
|
||||
"setGenTouchIdDisabled": "Fingerabdruck nicht benutzen",
|
||||
"setGenTouchIdMemory": "Nur mit Fingerabdruck entsperren, wenn KeeWeb aktiv ist",
|
||||
"setGenTouchIdFile": "Benutze immer den Fingerabdruck anstatt das Master-Passwort",
|
||||
"setGenTouchIdPass": "Benötige Master-Passwort nach",
|
||||
"setGenAudit": "Audit",
|
||||
"setGenAuditPasswords": "Warnungen für schwache Passwörter anzeigen",
|
||||
"setGenAuditPasswordEntropy": "Passwortlänge und Zufälligkeit prüfen",
|
||||
"setGenExcludePinsFromAudit": "Kurze Pin-Codes niemals prüfen (z. B. 12345)",
|
||||
"setGenHelpHIBP": "KeeWeb kann überprüfen ob deine Passwörter bei einem früheren Datenleck veröffentlicht wurden. Deine Passwörter können nicht beim übertragen ausgelesen werden, aber die Anzahl der überprüften Passwörter kann möglicherweise entdeckt werden. {} kannst du mehr über den Datenschutz beim Nutzen dieses Dienstes erfahren. Wenn diese Option aktiviert ist überprüft KeeWeb deine Passwörter automatisch.",
|
||||
"setGenHelpHIBPLink": "hier",
|
||||
"setGenAuditPasswordAge": "Alte Passwörter",
|
||||
"setGenAuditPasswordAgeOff": "Zeige keine Warnungen über alte Passwörter",
|
||||
"setGenAuditPasswordAgeOneYear": "Zeige Warnungen für Passwörter die über ein Jahr alt sind",
|
||||
"setGenAuditPasswordAgeYears": "Zeige Warnungen für Passwörter die über {} Jahre alt sind",
|
||||
"setFilePath": "Dateipfad",
|
||||
"setFileStorage": "Diese Datei wird von {} geladen.",
|
||||
"setFileIntl": "Diese Datei ist im internen App-Speicher abgelegt",
|
||||
"setFileLocalHint": "Sie wollen nahtlos mit lokalen Dateien arbeiten?",
|
||||
"setFileDownloadApp": "Laden Sie die Desktop-App herunter",
|
||||
"setFileSave": "Speichern",
|
||||
"setFileSaveTo": "Speichern unter...",
|
||||
"setFileSaveTo": "Speichern unter …",
|
||||
"setFileClose": "Sperren",
|
||||
"setFileSync": "Synchronisierung",
|
||||
"setFileSyncVerb": "Synchronisieren",
|
||||
"setFileSaveToXml": "XML",
|
||||
"setFileSaveToHtml": "HTML",
|
||||
"setFileLastSync": "Letzte Synchronisierung",
|
||||
"setFileLastSyncUnknown": "unbekannt",
|
||||
"setFileSyncInProgress": "Synchronisierung läuft",
|
||||
"setFileSyncError": "Synchronisierungsfehler",
|
||||
"setFilePass": "Master-Passwort",
|
||||
"setFilePass": "Hauptpasswort",
|
||||
"setFileConfirmPass": "Masterpasswort bestätigen",
|
||||
"setFilePassChange": "Um das Passwort zu ändern, geben Sie es im Bestätigungsfeld an",
|
||||
"setFilePassChanged": "Das Passwort wurde geändert. Lassen Sie das Feld leer, um das bisherige Passwort zu behalten.",
|
||||
"setFilePassNotMatch": "Die Passwörter stimmen nicht überein, bitte geben Sie diese erneut ein",
|
||||
"setFileKeyFile": "Schlüsseldatei",
|
||||
"setFileSelKeyFile": "Schlüsseldatei auswählen",
|
||||
"setFileNames": "Namen",
|
||||
"setFileDefUser": "Standard-Benutzername",
|
||||
"setFileEnableTrash": "Papierkorb aktivieren",
|
||||
"setFileHistMode": "Eintragsverlauf",
|
||||
"setFileHistLimited": "Alte Einträge im Verlauf automatisch löschen",
|
||||
"setFileHistDisabled": "Keinen Verlauf speichern",
|
||||
"setFileHistUnlimited": "Verlauf dauerhaft speichern",
|
||||
"setFileHistLen": "Anzahl der im Verlauf gespeicherten Versionen",
|
||||
"setFileHistSize": "Maximalgröße des Verlaufs pro Datei in MB",
|
||||
"setFileBackups": "Sicherungen",
|
||||
@@ -415,9 +513,11 @@
|
||||
"setFileBackupPath": "Sicherungspfad",
|
||||
"setFileBackupTime": "Sicherungen anlegen",
|
||||
"setFileBackupNow": "Jetzt sichern",
|
||||
"setFileBackupNowWorking": "Sichern...",
|
||||
"setFileBackupNowWorking": "Ausführung läuft …",
|
||||
"setFileBackupError": "Sicherungsfehler",
|
||||
"setFileBackupErrorDescription": "Fehler beim Schreiben der Sicherungsdatei",
|
||||
"setFileBackupErrorIsDir": "Der Sicherungspfad ist ungültig",
|
||||
"setFileBackupErrorIsDirDescription": "Der Sicherungspfad scheint ein Verzeichnis zu sein. Bitte geben Sie stattdessen einen Dateinamen ein.",
|
||||
"setFileBackupOnSave": "Immer, wenn die Datei gespeichert wird",
|
||||
"setFileBackupDaily": "Täglich",
|
||||
"setFileBackupWeekly": "Wöchentlich",
|
||||
@@ -433,9 +533,9 @@
|
||||
"setFileUseGenKeyFile": "Generierte Schlüsseldatei benutzen",
|
||||
"setFileUseOldKeyFile": "Alte Schlüsseldatei benutzen",
|
||||
"setFileGenKeyFile": "Neue Schlüsseldatei generieren",
|
||||
"setFileDontUseKeyFile": "Keine Schlüsseldatei benutzen",
|
||||
"setFileDontUseKeyFile": "Keine Schlüsseldatei verwenden",
|
||||
"setFileEmptyPass": "Leeres Passwort",
|
||||
"setFileEmptyPassBody": "Eine Datenbank ohne Passwort zu speichern macht sie vollkommen ungeschützt. Möchten Sie sie trotzdem speichern?",
|
||||
"setFileEmptyPassBody": "Eine Datenbank ohne Passwort zu speichern, macht diese vollkommen ungeschützt. Möchten Sie dies trotzdem tun?",
|
||||
"setFileSaveError": "Fehler beim Speichern",
|
||||
"setFileSaveErrorBody": "Schreibfehler beim Speichern",
|
||||
"setFileAlreadyExists": "Existiert bereits",
|
||||
@@ -444,13 +544,22 @@
|
||||
"setFileUnsavedBody": "Es gibt ungespeicherte Änderungen in dieser Datei",
|
||||
"setFileCloseNoSave": "Sperren und Änderungen verwerfen",
|
||||
"setFileDontClose": "Nicht sperren",
|
||||
"setFileNoWebDavUpload": "Speichern über WebDAV nicht möglich",
|
||||
"setFileNoWebDavUploadBody": "Neue Dateien zu WebDAV hinzuzufügen wird aktuell nicht unterstützt. Bitte laden Sie Ihre Datei manuell hoch und öffnen Sie sie anschließend mit der App.",
|
||||
"setFileFormatVersion": "Dateiformat",
|
||||
"setFileExportRaw": "Passwörter exportieren",
|
||||
"setFileExportRawBody": "Die Exportdatei wird Ihre Passwörter als unverschlüsselten Klartext enthalten. Möchten Sie fortfahren?",
|
||||
"setFileDeviceIntro": "Einmal-Codes aus diesem {} werden in der App angezeigt.",
|
||||
"setFileDeviceSettings": "Einstellungen",
|
||||
"setFileYubiKey": "YubiKey",
|
||||
"setFileDontUseYubiKey": "Keinen YubiKey verwenden",
|
||||
"setFileRefreshYubiKeyList": "Liste aktualisieren",
|
||||
"setFileYubiKeyHeader": "YubiKey",
|
||||
"setFileYubiKeyBody": "Es ist gefährlich, einen YubiKey als Teil des Hauptschlüssels zu verwenden. Falls etwas schief geht, besteht möglicherweise kein Zugriff mehr auf die Passwörter. Haben Sie ein Backup Ihrer Datei erstellt, bevor diese Einstellung geändert wurde?",
|
||||
"setFileYubiKeyErrorEmptyMac": "Scheinbar wurde ein YubiKey angeschlossen, er ist aber nicht sichtbar. KeeWeb muss in den Sicherheitseinstellungen im Abschnitt Input-Monitoring hinzugefügt werden, um auf YubiKeys zugreifen zu können.",
|
||||
"setShTitle": "Tastenkürzel",
|
||||
"setShShowAll": "alle Einträge anzeigen",
|
||||
"setShColors": "mit Farben markierte Einträge anzeigen",
|
||||
"setShTrash": "Papierkorb öffnen",
|
||||
"setShFind": "Suchfeld aktivieren; oder: einfach anfangen, den Suchbegriff zu tippen",
|
||||
"setShFind": "Suchen oder einfach beginnen zu tippen",
|
||||
"setShClearSearch": "Suchfeld leeren",
|
||||
"setShCopyPass": "Passwort bzw. ausgewähltes Feld kopieren",
|
||||
"setShCopyUser": "Benutzername kopieren",
|
||||
@@ -459,20 +568,23 @@
|
||||
"setShPrev": "zum vorherigen Eintrag gehen",
|
||||
"setShNext": "zum nächsten Eintrag gehen",
|
||||
"setShCreateEntry": "Neuer Eintrag",
|
||||
"setShOpen": "öffnen / neu",
|
||||
"setShSave": "alle Dateien speichern",
|
||||
"setShOpen": "Öffnen / Neu",
|
||||
"setShSave": "Alle Dateien speichern",
|
||||
"setShGen": "Passwort generieren",
|
||||
"setShSet": "App-Einstellungen",
|
||||
"setShCopyPassGlobal": "Passwort kopieren (wenn die App im Hintergrund ist)",
|
||||
"setShCopyUserGlobal": "Usernamen kopieren (wenn die App im Hintergrund ist)",
|
||||
"setShCopyUrlGlobal": "Website kopieren (wenn die App im Hintergrund ist)",
|
||||
"setShAutoTypeGlobal": "Auto-Type (wenn die App im Hintergrund ist)",
|
||||
"setShCopyPassGlobal": "Passwort kopieren",
|
||||
"setShCopyOtp": "Einmal-Passwort kopieren",
|
||||
"setShAutoTypeGlobal": "Auto-Type",
|
||||
"setShCopyPassOnly": "Passwort kopieren",
|
||||
"setShRestoreApp": "{} öffnen",
|
||||
"setShGlobal": "Diese systemweiten Tastenkürzel funktionieren auch, wenn die App im Hintergrund läuft. Klicken Sie auf einen Eintrag, um ein eigenes Kürzel festzulegen.",
|
||||
"setShLock": "Datenbank sperren",
|
||||
"setShEdit": "Drücken Sie eine Tastenkombination, um diese als Kürzel zu festzulegen",
|
||||
"setPlInstallTitle": "Neue Plugins installieren",
|
||||
"setPlInstallDesc": "KeeWeb-Plugins fügen Funktionen, Themes und Sprachen zu KeeWeb hinzu. Plugins werden mit denselben Rechten ausgeführt wie KeeWeb selbst, sie können auf all Ihre Passwörter zugreifen oder diese ändern. Installieren Sie niemals Plugins, denen Sie nicht vertrauen.",
|
||||
"setPlInstallLabel": "Plugin-URL",
|
||||
"setPlInstallBtn": "Installieren",
|
||||
"setPlInstallBtnProgress": "Installiere",
|
||||
"setPlInstallBtnProgress": "Installation läuft",
|
||||
"setPlUninstallBtn": "Deinstallieren",
|
||||
"setPlDisableBtn": "Deaktivieren",
|
||||
"setPlEnableBtn": "Aktivieren",
|
||||
@@ -486,7 +598,7 @@
|
||||
"setPlLoadTime": "geladen in {}",
|
||||
"setPlLastUpdate": "Letzte Prüfung auf Updates",
|
||||
"setPlLoadError": "Fehler beim Laden des Plugins",
|
||||
"setPlGalleryLoading": "Plugins werden geladen, bitte warten Sie einen Moment",
|
||||
"setPlGalleryLoading": "Plugins werden geladen, bitte einen Moment warten",
|
||||
"setPlGalleryLoadError": "Fehler beim Laden der Plugins",
|
||||
"setPlInstallUrlTitle": "Plugin von einer URL hinzufügen",
|
||||
"setPlInstallUrlDesc": "Wenn das Plugin nicht in der Galerie verfügbar ist, können Sie es manuell von einer URL installieren",
|
||||
@@ -497,53 +609,87 @@
|
||||
"setPlTranslate": "Oder {}",
|
||||
"setPlTranslateLink": "übersetzen Sie die App in Ihre Sprache",
|
||||
"setPlAutoUpdate": "Automatisch aktualisieren",
|
||||
"setPlLoadGallery": "Plugin-Galerie laden",
|
||||
"setDevicesTitle": "Geräte",
|
||||
"setDevicesEnableUsb": "Interaktion mit USB-Geräten aktivieren",
|
||||
"setDevicesYubiKeyIntro": "Es wird empfehlen {} zu lesen, bevor ein YubiKey verwendet wird.",
|
||||
"setDevicesYubiKeyIntroLink": "dieses Dokument",
|
||||
"setDevicesYubiKeyToolsDesc": "Um YubiKey in diesem Modus verwenden zu können, müssen Sie vorher das Tool {} installieren.",
|
||||
"setDevicesYubiKeyToolsDesc2": "{}, um mehr Informationen über dieses Tool zu erhalten.",
|
||||
"setDevicesYubiKeyToolsDescLink": "Hier klicken",
|
||||
"setDevicesYubiKeyToolsStatusChecking": "Prüfung, ob {} installiert ist",
|
||||
"setDevicesYubiKeyToolsStatusOk": "{} ist installiert",
|
||||
"setDevicesYubiKeyToolsStatusError": "{} ist nicht installiert oder funktioniert nicht korrekt",
|
||||
"setDevicesYubiKeyOtpTitle": "Einmal-Codes",
|
||||
"setDevicesYubiKeyOtpDesc": "Ein YubiKey kann genutzt werden, um Einmal-Codes für verschiedene Dienste zu erzeugen.",
|
||||
"setDevicesYubiKeyOtpShowIcon": "Das Symbol auf der Startseite anzeigen",
|
||||
"setDevicesYubiKeyOtpAutoOpen": "Einmal-Codes automatisch laden, wenn Dateien geöffnet sind",
|
||||
"setDevicesYubiKeyOtpMatchEntries": "Passende Einmal-Codes in den Einträgen anzeigen",
|
||||
"setDevicesYubiKeyChalRespTitle": "Challenge-Response",
|
||||
"setDevicesYubiKeyChalRespDesc": "Es ist auch möglich, einen YubiKey im Challenge-Response-Modus zu verwenden, um einen Teil des Private Schlüssels, der zum Verschlüsseln der Dateien verwendet wird, auf dem YubiKey zu speichern.",
|
||||
"setDevicesYubiKeyChalRespShow": "Optionen zum Verwenden eines YubiKeys beim Öffnen von Dateien anzeigen",
|
||||
"setDevicesYubiKeyStuckWorkaround": "YubiKey entfernen und neu verbinden, falls der Ladeprozess hängt",
|
||||
"setDevicesYubiKeyRememberChalResp": "Challenge-Response-Einstellungen merken, während die App geöffnet ist",
|
||||
"setAboutTitle": "Über",
|
||||
"setAboutBuilt": "Diese App wurde mit den folgenden Werkzeugen erstellt",
|
||||
"setAboutBuilt": "Diese App wurde mit folgenden Werkzeugen erstellt",
|
||||
"setAboutLic": "Lizenz",
|
||||
"setAboutLicComment": "Die App selbst und alle enthaltenen Komponenten, die nicht Public Domain sind, stehen unter der MIT-Lizenz.",
|
||||
"setAboutFirst": "Dies ist eine Open-Source App, erstellt von {}",
|
||||
"setAboutSecond": "und lizenziert unter {}.",
|
||||
"setAboutSource": "Quellcode und Tickets befinden sich auf {}.",
|
||||
"setHelpFormat": "Dateiformat",
|
||||
"setHelpFormatBody": "Dies ist eine Portierung der {} App, programmiert mit Webtechnologien. Sie ist kompatibel mit Dateien im KeePass-Format (kdbx). Sie können solche Dateien (Passwort-Datenbanken) entweder mit KeePass oder mit dieser App erstellen. Das Dateiformat ist zu 100% kompatibel und sollte in beiden Anwendungen nutzbar sein.",
|
||||
"setHelpFormatBody": "Dies ist eine Portierung der App {}, programmiert mit Webtechnologien. Sie ist kompatibel mit Dateien im KeePass-Format (kdbx). Sie können solche Dateien (Passwort-Datenbanken) entweder mit KeePass oder mit dieser App erstellen. Das Dateiformat ist zu 100 % kompatibel und sollte in beiden Anwendungen nutzbar sein.",
|
||||
"setHelpProblems": "Probleme?",
|
||||
"setHelpProblems1": "Falls etwas schief geht, {}",
|
||||
"setHelpProblems2": "oder {}",
|
||||
"setHelpOpenIssue": "eröffnen Sie bitte ein Ticket auf GitHub (englisch)",
|
||||
"setHelpContactLink": "kontaktieren Sie einen Entwickler direkt",
|
||||
"setHelpContactLink": "kontaktieren Sie den Entwickler direkt",
|
||||
"setHelpAppInfo": "App-Informationen",
|
||||
"setHelpOtherPlatforms": "Andere Plattformen",
|
||||
"setHelpDesktopApps": "Deskop-Apps",
|
||||
"setHelpWebApp": "Web-App",
|
||||
"setHelpUpdates": "Neuigkeiten",
|
||||
"setHelpTwitter": "Twitter",
|
||||
"dropboxLogin": "In Dropbox anmelden",
|
||||
"dropboxLoginBody": "Um fortzufahren müssen Sie sich bei Dropbox einloggen.",
|
||||
"dropboxSyncError": "Dropbox Synchronisierungsfehler",
|
||||
"dropboxNotFoundBody": "Die Datei wurde nicht gefunden. Wurde Sie vom anderen Computer entfernt?",
|
||||
"dropboxFull": "Dropbox voll",
|
||||
"dropboxFullBody": "Ihre Dropbox ist voll, es ist kein Speicherplatz mehr verfügbar.",
|
||||
"dropboxRateLimitedBody": "Von dieser App wurden zu viele Anfragen an Dropbox geschickt. Bitte veruschen Sie es später noch einmal.",
|
||||
"dropboxNetError": "Verbindungsfehler während der Dropbox-Synchronisierung",
|
||||
"dropboxNetErrorBody": "Ein Verbindungsfehler ist während der Dropbox-Synchronisierung aufgetreten. Bitte überprüfen Sie Ihre Internetverbindung und versuchen Sie es erneut.",
|
||||
"dropboxErrorBody": "Bei der Dropbox-Synchronisierung ist etwas schief gegangen. Bitte versuchen Sie es später noch einmal. Fehlercode:",
|
||||
"dropboxErrorRepeatBody": "Bei der Dropbox-Synchronisierung ist etwas schief gegangen. Bitte versuchen Sie es später noch einmal. Fehler:",
|
||||
"dropboxSetupDesc": "Etwas Konfiguration ist notwendig, um Dropbox in einer selbst gehosteten Web-App zu verwenden. Bitte erstellen Sie eine eigene Dropbox-App und tragen Sie ihren App-Schlüssel unten ein.",
|
||||
"dropboxAppKey": "Dropbox App-Schlüssel",
|
||||
"dropboxAppKey": "Dropbox-App-Schlüssel",
|
||||
"dropboxAppKeyDesc": "Kopieren Sie den Schlüssel aus Ihrer Dropbox-App (Entwicklereinstellungen)",
|
||||
"dropboxAppKeyHint": "Ihr App-Schlüssel",
|
||||
"dropboxAppSecret": "Dropbox-App-Secret",
|
||||
"dropboxAppSecretDesc": "Das App-Secret ist neben dem App-Schlüssel zu finden",
|
||||
"dropboxFolder": "App-Ordner",
|
||||
"dropboxFolderDesc": "Wenn Ihre App mit der gesamten Dropbox verlinkt ist (anstatt mit einem bestimmten Ordner), geben Sie hier den Pfad zum Ordner mit Ihren KDBX-Dateien an.",
|
||||
"dropboxFolderSettingsDesc": "Wählen Sie einen Ordner in Ihrer Dropbox, in dem die Dateien gespeichert werden (standardmäßig das Stammverzeichnis)",
|
||||
"dropboxFolderPlaceholder": "Standard-Ordner",
|
||||
"dropboxFolderPlaceholder": "Standardordner",
|
||||
"dropboxLink": "App verlinken mit",
|
||||
"dropboxLinkApp": "App-Ordner (Apps/KeeWeb)",
|
||||
"dropboxLinkFull": "Gesamte Dropbox oder beliebiger Ordner",
|
||||
"dropboxLinkCustom": "Eigene Dropbox-App",
|
||||
"gdriveSharedWithMe": "Mit mir geteilt",
|
||||
"webdavSaveMethod": "Speichermethode",
|
||||
"webdavSaveMove": "Eine temporäre Datei hochladen und verschieben",
|
||||
"webdavSavePut": "kdbx-Datei mittels PUT überschreiben",
|
||||
"launcherSave": "Passwort-Datenbank speichern",
|
||||
"launcherSave": "Passwortdatenbank speichern",
|
||||
"launcherFileFilter": "KeePass-Dateien",
|
||||
"authPopupRequired": "Pop-Ups blockiert",
|
||||
"authPopupRequiredBody": "Bitte erlauben Sie Pop-Ups für diese App in Ihrem Browser."
|
||||
"authPopupRequired": "Pop-ups blockiert",
|
||||
"authPopupRequiredBody": "Bitte erlauben Sie Pop-ups in Ihrem Browser.",
|
||||
"exportFileInfo": "Dateiinformationen",
|
||||
"exportHtmlName": "Name",
|
||||
"exportHtmlDate": "Exportdatum",
|
||||
"exportGenerator": "Software",
|
||||
"exportDescription": "Diese Datei wurde mit {} erstellt.",
|
||||
"importCsvTitle": "Aus CSV importieren",
|
||||
"importCsvRun": "Importieren",
|
||||
"importIgnoreField": "Ignorieren",
|
||||
"importTo": "Einträge werden importiert nach:",
|
||||
"importNewFile": "Neue Datei",
|
||||
"yubiKeyStuckError": "Der YubiKey scheint nicht mehr zu funktionieren. Auto-Repair kann in den App-Einstellungen aktiviert werden.",
|
||||
"yubiKeyNoKeyHeader": "YubiKey benötigt",
|
||||
"yubiKeyNoKeyBody": "Bitte den YubiKey mit der Seriennummer {} anschließen",
|
||||
"yubiKeySlot": "Anschluss",
|
||||
"yubiKeyTouchRequestedHeader": "Bitte den YubiKey berühren",
|
||||
"yubiKeyTouchRequestedBody": "Bitte den YubiKey mit der Seriennummer {} berühren",
|
||||
"yubiKeyDisabledErrorHeader": "USB ist deaktiviert",
|
||||
"yubiKeyDisabledErrorBody": "YubiKey ist zum Öffnen dieser Datei erforderlich. Bitte USB-Geräte in den Einstellungen aktivieren.",
|
||||
"yubiKeyErrorWithCode": "YubiKey-Fehler, Code {}.",
|
||||
"bioOpenAuthPrompt": "„{}“ öffnen"
|
||||
}
|
||||
@@ -0,0 +1,701 @@
|
||||
{
|
||||
"retToApp": "retour vers l'appli",
|
||||
"name": "nom",
|
||||
"icon": "icône",
|
||||
"title": "titre",
|
||||
"password": "mot de passe",
|
||||
"user": "utilisateur",
|
||||
"website": "site web",
|
||||
"tags": "tags",
|
||||
"notes": "notes",
|
||||
"entry": "entrée",
|
||||
"group": "groupe",
|
||||
"noTitle": "aucun titre",
|
||||
"or": "ou",
|
||||
"history": "historique",
|
||||
"template": "modèle",
|
||||
"notImplemented": "Non Implémenté",
|
||||
"saveChanges": "Sauvegarder les modifications",
|
||||
"discardChanges": "Annuler les modifications",
|
||||
"advanced": "Avancé",
|
||||
"shortcuts": "Raccourcis",
|
||||
"help": "Aide",
|
||||
"settings": "Paramètres",
|
||||
"plugins": "Modules (plugins)",
|
||||
"ctrlKey": "ctrl",
|
||||
"shiftKey": "shift",
|
||||
"altKey": "alt",
|
||||
"error": "erreur",
|
||||
"oneMinute": "une minute",
|
||||
"minutes": "{} minutes",
|
||||
"oneHour": "une heure",
|
||||
"hours": "{} heures",
|
||||
"oneDay": "un jour",
|
||||
"days": "{} jours",
|
||||
"oneWeek": "une semaine",
|
||||
"oneMonth": "un mois",
|
||||
"oneYear": "une année",
|
||||
"cache": "cache",
|
||||
"file": "fichier",
|
||||
"device": "appareil",
|
||||
"webdav": "WebDAV",
|
||||
"dropbox": "Dropbox",
|
||||
"gdrive": "Google Drive",
|
||||
"onedrive": "OneDrive",
|
||||
"menuAllItems": "Tout",
|
||||
"menuColors": "Couleurs",
|
||||
"menuTrash": "Corbeille",
|
||||
"menuSetGeneral": "Général",
|
||||
"menuSetAbout": "À propos",
|
||||
"menuSetDevices": "Appareils",
|
||||
"menuAlertNoTags": "Aucun tag",
|
||||
"menuAlertNoTagsBody": "Vous pouvez ajouter de nouveaux tags en éditant les champs, dans la section Tags.",
|
||||
"menuEmptyTrash": "Vider la Corbeille",
|
||||
"menuEmptyTrashAlert": "Vider la Corbeille ?",
|
||||
"menuEmptyTrashAlertBody": "Vous ne pourrez plus récupérer ces éléments",
|
||||
"menuItemCollapsed": "Double-cliquez pour déplier",
|
||||
"menuRestoreApp": "Ouvrir {}",
|
||||
"menuQuitApp": "Quitter {}",
|
||||
"sysMenuAboutKeeWeb": "A propos {}",
|
||||
"sysMenuServices": "Services",
|
||||
"sysMenuHide": "Cacher {}",
|
||||
"sysMenuHideOthers": "Cacher autres",
|
||||
"sysMenuUnhide": "Montrer tous",
|
||||
"sysMenuQuit": "Quitter {}",
|
||||
"sysMenuEdit": "Editer",
|
||||
"sysMenuUndo": "Annuler",
|
||||
"sysMenuRedo": "Répéter",
|
||||
"sysMenuCut": "Couper",
|
||||
"sysMenuCopy": "Copier",
|
||||
"sysMenuPaste": "Coller",
|
||||
"sysMenuSelectAll": "Tout sélectionner",
|
||||
"sysMenuWindow": "Fenêtre",
|
||||
"sysMenuMinimize": "Réduire",
|
||||
"sysMenuClose": "Fermer fenêtre",
|
||||
"alertYes": "Oui",
|
||||
"alertNo": "Non",
|
||||
"alertOk": "OK",
|
||||
"alertCancel": "Annuler",
|
||||
"alertSignIn": "Connexion",
|
||||
"alertCopy": "Copier",
|
||||
"alertClose": "Fermer",
|
||||
"alertDoNotAsk": "Ne plus me le redemander",
|
||||
"appBeta": "ATTENTION: version beta, aperçu seulement",
|
||||
"footerOpen": "Ouvrir/Nouveau",
|
||||
"footerSyncError": "Erreur de synchronisation",
|
||||
"footerTitleGen": "Générer",
|
||||
"footerTitleLock": "Verrouiller",
|
||||
"genLen": "Longueur",
|
||||
"genNewPass": "Nouveau mot de passe",
|
||||
"genPresetDefault": "réglages par défaut",
|
||||
"genPresetDerived": "similaire à l'ancien mot de passe",
|
||||
"genPresetPronounceable": "prononçable",
|
||||
"genPresetMed": "longueur moyenne",
|
||||
"genPresetLong": "long",
|
||||
"genPresetPin4": "code à 4 chiffres",
|
||||
"genPresetMac": "adresse MAC",
|
||||
"genPresetHash128": "hash 128 bits",
|
||||
"genPresetHash256": "hash 256 bits",
|
||||
"genHidePass": "Masquer le mot de passe",
|
||||
"genShowPass": "Afficher le mot de passe",
|
||||
"grpTitle": "Groupe",
|
||||
"grpSearch": "Activer la recherche dans les entrées de ce groupe",
|
||||
"grpAutoType": "Activer l'auto-complétion",
|
||||
"grpAutoTypeSeq": "Séquence d'auto-complétion",
|
||||
"grpAutoTypeSeqDefault": "Utiliser la séquence d'auto-complétion par défaut",
|
||||
"grpTrash": "Supprimer le groupe et toutes ses entrées",
|
||||
"tagTitle": "Tag",
|
||||
"tagTrash": "Supprimer le tag de toutes les entrées",
|
||||
"tagRename": "Renommer",
|
||||
"tagTrashQuestion": "Supprimer le tag de toutes les entrées ?",
|
||||
"tagTrashQuestionBody": "Ce tag va être supprimé de toutes les entrées. Il n'y aura pas de moyen facile pour le remettre.",
|
||||
"tagExists": "Ce tag existe déjà",
|
||||
"tagExistsBody": "Un tag existe déjà avec ce nom. Merci de choisir un autre nom.",
|
||||
"tagBadName": "Nom invalide",
|
||||
"tagBadNameBody": "Les noms de Tag ne peuvent contenir les caractères {}. Veuillez les supprimer.",
|
||||
"genPsTitle": "Préréglages du Générateur",
|
||||
"genPsCreate": "Nouveau préréglage",
|
||||
"genPsDelete": "Supprimer préréglage",
|
||||
"genPsNew": "préréglage",
|
||||
"genPsEnabled": "Afficher dans la liste des préréglages",
|
||||
"genPsDefault": "Sélectionné par défaut",
|
||||
"genPsDefaultLength": "Longueur par défaut",
|
||||
"genPsUpper": "Lettres latines majuscules",
|
||||
"genPsLower": "Lettres latines minuscules",
|
||||
"genPsDigits": "Chiffres",
|
||||
"genPsSpecial": "Caractères spéciaux",
|
||||
"genPsBrackets": "Parenthèses",
|
||||
"genPsHigh": "Caractères ASCII étendus",
|
||||
"genPsAmbiguous": "Caractères ambigus",
|
||||
"genPsInclude": "Caractères additionnels à inclure",
|
||||
"genPsExample": "Exemple de mot de passe généré",
|
||||
"genPsPattern": "Modèle",
|
||||
"genPsPatternHelp": "Des modèles peuvent être utilisés pour spécifier des règles de sélection de caractères. 1-AA générera un mot de passe commençant par un chiffre, suivi d'un tiret et de 2 lettres. Vous pouvez utiliser ces symboles :",
|
||||
"genPsAllRanges": "Tous les caractères",
|
||||
"genPsIncluded": "Caractères additionnels ajoutés ci-dessus",
|
||||
"keyChangeTitleRemote": "Clé Maître Modifiée",
|
||||
"keyChangeMessageRemote": "La clé maître a été modifiée pour cette base de données. Merci de saisir une nouvelle clé.",
|
||||
"keyChangeTitleExpired": "Clé Maître Expirée",
|
||||
"keyChangeMessageExpired": "La clé maître pour cette base de donnée est expirée. Merci de saisir une nouvelle clé.",
|
||||
"keyChangeRepeatPassword": "Mot de passe, encore une fois",
|
||||
"keyEnter": "Entrée",
|
||||
"iconFavTitle": "Télécharger et utiliser le favicon du site web",
|
||||
"iconSelCustom": "Sélectionner une icône personnalisée",
|
||||
"listEmptyTitle": "Vide",
|
||||
"listEmptyAdd": "ajouter avec le bouton {} au-dessus",
|
||||
"listGroup": "Groupe",
|
||||
"listNoWebsite": "aucun site web",
|
||||
"listNoUser": "aucun utilisateur",
|
||||
"listNoAttachments": "aucune pièce-jointe",
|
||||
"listAddTemplateHeader": "Modèles",
|
||||
"listAddTemplateBody1": "Les modèles (templates) vous permettent de créer des nouvelles entrées en un clic. Ajouter quelque chose à l'entrée du modèle et ensuite cliquer de nouveau sur {} pour utiliser ce modèle.",
|
||||
"listAddTemplateBody2": "Vous pouvez toujours retrouver vos modèles dans le groupe {}.",
|
||||
"searchAddNew": "Ajouter Nouveau",
|
||||
"searchSort": "Trier",
|
||||
"searchCreated": "Créé",
|
||||
"searchUpdated": "Mis à jour",
|
||||
"searchAttachments": "Pièce-jointes",
|
||||
"searchAZ": "A {} Z",
|
||||
"searchZA": "Z {} A",
|
||||
"searchON": "Ancien {} Nouveau",
|
||||
"searchNO": "Nouveau {} Ancien",
|
||||
"searchShiftClickOr": "maj-click ou",
|
||||
"searchAdvTitle": "Afficher/masquer recherche avancée",
|
||||
"searchSearchIn": "Rechercher dans",
|
||||
"searchOther": "Autres champs",
|
||||
"searchProtect": "Champs sécurisés",
|
||||
"searchOptions": "Options",
|
||||
"searchCase": "Respecte la casse",
|
||||
"searchRegex": "RegEx",
|
||||
"searchRank": "Auto",
|
||||
"searchExact": "Exact",
|
||||
"openOpen": "Ouvrir",
|
||||
"openNew": "Nouveau",
|
||||
"openNewFile": "Nouveau",
|
||||
"openMore": "Plus",
|
||||
"openDemo": "Démo",
|
||||
"openGenerate": "Générer",
|
||||
"openXml": "Importer XML",
|
||||
"openCaps": "Majuscules activées",
|
||||
"openClickToOpen": "Cliquer pour ouvrir un fichier",
|
||||
"openKeyFile": "fichier clé",
|
||||
"openKeyFileDropbox": "(depuis DropBox)",
|
||||
"openDropHere": "déposez les fichiers ici",
|
||||
"openFailedRead": "Échec lors de la lecture du fichier",
|
||||
"openNothingFound": "Aucun résultat",
|
||||
"openNothingFoundBody": "Nous n'avons trouvé aucun fichier convenable.",
|
||||
"openSelectFile": "Sélectionnez un fichier",
|
||||
"openSelectFileBody": "Sélectionnez un fichier que vous voudriez ouvrir",
|
||||
"openPassFor": "Mot de passe pour",
|
||||
"openRemoveLastQuestion": "Supprimer le fichier local ?",
|
||||
"openRemoveLastQuestionBody": "Le fichier que vous voulez supprimer est sauvegardé dans l'appli. Le supprimer de façon définitive ?",
|
||||
"openRemoveLastQuestionModBody": "Le fichier que vous voulez supprimer a des modifications locales. Le supprimer et annuler ces modifications ?",
|
||||
"openLocalFile": "Fichier local",
|
||||
"openLocalFileBody": "Vous allez ouvrir un fichier qui sera stocké dans l'application. Les modifications apportées ne seront pas sauvegardées vers le système de fichiers. Pour obtenir le fichier avec vos données, exportez-le via les paramètres.",
|
||||
"openLocalFileDontShow": "Ne plus afficher",
|
||||
"openWrongFile": "Fichier invalide",
|
||||
"openWrongFileBody": "Ce format de fichier n'est pas supporté. Cette application fonctionne avec des fichiers de base de donnée KeePass (kdbx).",
|
||||
"openKdbFileBody": "Vous essayez d'ouvrir un ancien fichier de base de donnée KeePass (KDB). Cette application supporte uniquement le nouveaux format (kdbx), merci d'utiliser KeePass 2 afin de convertir celui-ci.",
|
||||
"openConfigHeader": "Paramètres {}",
|
||||
"openUrl": "URL",
|
||||
"openUrlDesc": "https://serveur/chemin/fichier.kdbx, ou seulement fichier.kdbx",
|
||||
"openUser": "Nom d'utilisateur",
|
||||
"openUserDesc": "Nom d'utilisateur du serveur WebDAV (si requis)",
|
||||
"openUserPlaceholder": "aucun nom d'utilisateur",
|
||||
"openPass": "Mot de passe",
|
||||
"openPassDesc": "Mot de passe du serveur WebDAV (ce n'est pas votre mot de passe de fichier)",
|
||||
"openPassPlaceholder": "aucun mot de passe",
|
||||
"openConfigError": "Erreur: {}",
|
||||
"openConfigErrorNotFound": "Aucun fichier trouvé",
|
||||
"openError": "Erreur",
|
||||
"openErrorDescription": "Une erreur est survenue à l'ouverture du fichier",
|
||||
"openErrorDescriptionMaybeTouchIdChanged": "L'erreur ressemble à ce qu'y arrive habituellement quand la configuration de Touch ID a été changée. Par exemple, vous avez ajouté ou supprimé un doigt supplémentaire. Si c'est le cas, allez dans Paramètres, désactiver Touch ID et activer le de nouveau.",
|
||||
"openErrorFileNotFound": "Fichier non trouvé",
|
||||
"openListErrorBody": "Erreur au chargement de la liste des fichiers",
|
||||
"openShowAllFiles": "Tous les fichiers",
|
||||
"openFileNoCacheError": "Fichier non trouvé dans la mémoire cache. Cela peut arriver parce que le stockage du navigateur a été nettoyé. Pour ouvrir le fichier, supprimez-le de KeeWeb et ajoutez-le à nouveau.",
|
||||
"openChalRespHeader": "Challenge-Réponse",
|
||||
"openChalRespLoading": "Chargement de la liste des YubiKeys",
|
||||
"openChalRespSelectYubiKey": "Sélectionnez une YubiKey que vous voudriez utiliser",
|
||||
"openChalRespErrorEmpty": "Pas de clé YubiKey trouvée.",
|
||||
"openChalRespErrorEmptyMac": "Première fois que vous utilisez cette fonctionnalité sur MacOS ? KeeWeb doit avoir été autorisé dans la section \"Surveillance de l’entrée\" des paramètres de sécurité.",
|
||||
"detAttDownload": "Cliquez sur le bouton de pièce-jointe en pressant la touche Maj pour le télécharger ou",
|
||||
"detAttDelToRemove": "Supprimer pour enlever",
|
||||
"detAttSave": "Enregistrer sous",
|
||||
"detEmpty": "Vos mots de passe seront affichés ici",
|
||||
"detGroupRestore": "Pour restaurer ce groupe, veuillez le glisser-déposer dans un des groupes hors de la corbeille",
|
||||
"detHistoryClickPoint": "Cliquer sur une entrée de l'historique pour voir son état",
|
||||
"detHistoryReturn": "Retour vers l'entrée",
|
||||
"detHistoryRevert": "Revenir à cet état",
|
||||
"detHistoryDel": "Supprimer cet état",
|
||||
"detHistoryEmpty": "vide",
|
||||
"detHistoryModified": "modifié",
|
||||
"detHistoryRec": "archive",
|
||||
"detHistoryRecs": "archives",
|
||||
"detHistoryVersion": "Version",
|
||||
"detHistorySaved": "Sauvegardé",
|
||||
"detHistoryNoTitle": "sans titre",
|
||||
"detHistoryCurState": "état actuel",
|
||||
"detHistoryCurUnsavedState": "état actuel non sauvegardé",
|
||||
"detHistoryRevertAlert": "Revenir à cet état de l'historique ?",
|
||||
"detHistoryRevertAlertBody": "L'état courant sera sauvegardé dans l'historique.",
|
||||
"detHistoryDeleteAlert": "Supprimer cet état de l'historique ?",
|
||||
"detHistoryDeleteAlertBody": "Vous ne pourrez plus le restaurer.",
|
||||
"detHistoryDiscardChangesAlert": "Ne pas sauvegarder les modifications de cette entrée ?",
|
||||
"detHistoryDiscardChangesAlertBody": "Les changements non sauvegardés seront définitivement perdus, il n'y a pas de retour en arrière.",
|
||||
"detBackToList": "retour vers liste",
|
||||
"detSetIconColor": "Changer de couleur",
|
||||
"detSetIcon": "Changer d'icône",
|
||||
"detDropAttachments": "Déposer une pièce-jointe ici",
|
||||
"detDelEntry": "Supprimer",
|
||||
"detDelEntryPerm": "Supprimer définitivement",
|
||||
"detExpires": "Expire",
|
||||
"detExpired": "expirée",
|
||||
"detGroup": "Groupe",
|
||||
"detCreated": "Créé le",
|
||||
"detUpdated": "Mis à jour",
|
||||
"detNetField": "Nouveau champ",
|
||||
"detAttachments": "Pièces-jointes",
|
||||
"detDelFromTrash": "Supprimer de la Corbeille ?",
|
||||
"detDelFromTrashBody": "Vous ne pourrez plus le récupérer.",
|
||||
"detDelFromTrashBodyHint": "Pour supprimer tous les éléments de la Corbeille, cliquez l’icône \"Vider la corbeille\" du menu Corbeille.",
|
||||
"detDelToTrash": "Supprimer l'entrée ?",
|
||||
"detDelToTrashBody": "L'entrée sera déplacée dans la corbeille.",
|
||||
"detFieldCopied": "Copié",
|
||||
"detFieldCopiedTime": "Copié pendant {} secondes",
|
||||
"detCopyHint": "Vous pouvez copier la valeur du champ en cliquant sur ton titre",
|
||||
"detMore": "plus",
|
||||
"detClickToAddField": "cliquez pour ajouter un nouveau champ",
|
||||
"detMenuAddNewField": "Ajouter nouveau champ",
|
||||
"detMenuShowEmpty": "Montrer les champs vides",
|
||||
"detMenuHideEmpty": "Cacher les champs vides",
|
||||
"detMenuAddField": "Ajouter {}",
|
||||
"detMenuCopyPassword": "Copier mot de passe",
|
||||
"detMenuCopyUser": "Copier nom",
|
||||
"detMenuCopyOtp": "Copier le code à usage unique",
|
||||
"detSetupOtp": "Codes à usage unique",
|
||||
"detClone": "Dupliquer",
|
||||
"detCopyEntryToClipboard": "Copier tous les champs",
|
||||
"detClonedName": "Copier",
|
||||
"detAutoType": "Saisie auto",
|
||||
"detAutoTypeSettings": "Paramètres saisie auto",
|
||||
"detAutoTypeEnabled": "Activer la saisie automatique pour cette entrée",
|
||||
"detAutoTypeSequence": "Frappes",
|
||||
"detAutoTypeInput": "Entrée",
|
||||
"detAutoTypeShortcutsDesc": "{} ou {} tant que l'appli est inactive",
|
||||
"detAutoTypeObfuscation": "Mélanger touches réelles et aléatoire",
|
||||
"detAutoTypeWindow": "Fenêtre",
|
||||
"detAutoTypeInputWindow": "Titre de fenêtre",
|
||||
"detSetupOtpAlert": "Scanner le QR code",
|
||||
"detSetupOtpAlertBody": "Veuillez copier le QR code qui est affiché sur la page d'autorisation",
|
||||
"detSetupOtpAlertBody1": "1. aller sur la page d'autorisation",
|
||||
"detSetupOtpAlertBody2": "2. faire une capture d'écran du QR code avec {}",
|
||||
"detSetupOtpAlertBody3": "3. collez-la ici avec {}",
|
||||
"detSetupOtpAlertBody3Mobile": "3. Sélectionner ou scanner avec votre appareil photo en utilisant Sélectionner/Scan ci-dessous",
|
||||
"detSetupOtpAlertBody4": "Si vous ne pouvez pas scanner le code, cliquer sur Entrer le code manuellement",
|
||||
"detSetupOtpManualButton": "Entrer le code manuellement",
|
||||
"detSetupOtpScanButton": "Sélectionner/Scan",
|
||||
"detSetupOtpAlertBodyWith": "avec {}",
|
||||
"detOtpImageError": "Erreur de lecture de l'image",
|
||||
"detOtpImageErrorBody": "Désolé, le format de l'image ne peut pas être lu, merci de contacter les auteurs de l'application avec le détail de l'erreur.",
|
||||
"detOtpImageReading": "Lecture du QR code...",
|
||||
"detOtpQrError": "Erreur lecture du QR code",
|
||||
"detOtpQrErrorBody": "Désolé, le QR code n'a pas pu être lu, essayez à nouveau ou contacter les auteurs de l'application avec le détail de l'erreur.",
|
||||
"detOtpQrWrong": "QR code incorrect",
|
||||
"detOtpQrWrongBody": "Votre code QR code a été scanné avec succès mais il ne contient pas d'information pour un code à usage unique.",
|
||||
"detOtpField": "Code à usage unique",
|
||||
"detOtpClickToTouch": "Cliquer pour générer",
|
||||
"detOtpGenerating": "Création...",
|
||||
"detOtpTouch": "Touchez votre {}",
|
||||
"detLockField": "Verrouiller ce champ, ainsi son contenu ne sera pas visible ni ne pourra être cherché. Visualiser son contenu exigera de le cliquer explicitement.",
|
||||
"detUnlockField": "Déverrouiller ce champ, rendant son contenu immédiatement visible et pouvant être cherché",
|
||||
"detRevealField": "Révéler",
|
||||
"detHideField": "Cacher",
|
||||
"detAutoTypeField": "Saisie auto",
|
||||
"detIssuesHideTooltip": "Cacher cet avertissement",
|
||||
"detIssueWeakPassword": "Ce mot de passe est faible, nous vous recommandons de le changer",
|
||||
"detIssuePoorPassword": "Ce mot de passe est très faible, nous vous recommandons très fort de le changer",
|
||||
"detIssuePwnedPassword": "Ce mot de passe a été exposé à une faille selon {}, il est recommandé de le changer",
|
||||
"detIssuePasswordCheckError": "Une erreur est survenue en vérifiant la force du mot de passe en ligne",
|
||||
"detIssueOldPassword": "Ce mot de passe est vieux",
|
||||
"detIssueCloseAlertHeader": "Cacher les problèmes de mots de passe",
|
||||
"detIssueCloseAlertBody": "Il y a plusieurs façon de cacher ce message :",
|
||||
"detIssueCloseAlertEntry": "Ne plus montrer pour cette entrée",
|
||||
"detIssueCloseAlertSettings": "Modifier les paramètres globaux",
|
||||
"autoTypeEntryFields": "Champs",
|
||||
"autoTypeModifiers": "Touches modificatrices",
|
||||
"autoTypeKeys": "Clés",
|
||||
"autoTypeLink": "plus...",
|
||||
"autoTypeError": "Erreur saisie auto",
|
||||
"autoTypeErrorGeneric": "Il y a eu une erreur lors de la saisie automatique : {}",
|
||||
"autoTypeErrorAccessibilityMacOS": "Nous avons essayé d'envoyer les frappes de caractères à cette application mais cela ne semble pas fonctionner parce qu'il manque des permissions. Cliquer ici pour en lire plus.",
|
||||
"autoTypeErrorGlobal": "Pour utiliser un raccourci global, sélectionner l'application là où vous voulez saisir le mot de passe",
|
||||
"autoTypeErrorNotInstalled": "{} n'est pas installé",
|
||||
"autoTypeHeader": "Saisie auto : Selectionner",
|
||||
"autoTypeMsgNoWindow": "Nous n'avons pas pu obtenir le titre de la fenêtre active, commencez à taper pour rechercher",
|
||||
"autoTypeMsgMatchedByWindow": "Sélectionner un mot de passe pour {}",
|
||||
"autoTypeNoMatches": "aucun",
|
||||
"autoTypeSelectionHint": "Saisissez la séquence automatique",
|
||||
"autoTypeSelectionHintAction": "Entrez seulement le mot de passe",
|
||||
"autoTypeSelectionHintOpt": "Entrez seulement le compte",
|
||||
"autoTypeSelectionHintShift": "Autres champs",
|
||||
"autoTypeSelectionOtp": "code à usage unique",
|
||||
"appSecWarn": "Non sécurisé !",
|
||||
"appSecWarnBody1": "Vous avez chargé cette appli avec une connexion non sécurisée. Quelqu'un peut vous observer et voler vos mots de passe. Nous vous recommandons fortement d'arrêter cette connexion, à moins que vous ne compreniez exactement ce que vous faites.",
|
||||
"appSecWarnBody2": "Oui, votre base est chiffrée mais personne ne peut garantir que l'application n'a pas été modifiée avant d'arriver à vous.",
|
||||
"appSecWarnBtn": "Je comprends les risques, continuer",
|
||||
"appUnsavedWarn": "Modifications non sauvegardées",
|
||||
"appUnsavedWarnBody": "Vous avez des fichiers non sauvegardés, si vous fermez l'appli, les changements seront perdus.",
|
||||
"appDontExitBtn": "Ne pas quitter",
|
||||
"appCannotLockAutoInit": "L'appli ne peut pas être verrouillée car la sauvegarde automatique est désactivée.",
|
||||
"appCannotLock": "Vous avez des modifications non sauvegardées qui seront perdus. Continuer ?",
|
||||
"appAutoSave": "Sauver les modifications automatiquement",
|
||||
"appSaveError": "Sauver l'erreur",
|
||||
"appSaveErrorBody": "Échec sauvegarde auto du fichier",
|
||||
"appSaveErrorBodyMul": "Échec sauvegarde auto des fichiers",
|
||||
"appSaveErrorExitLoseChanges": "Afsluiten en alle aanpassingen kwijtraken",
|
||||
"appSaveErrorExitLoseChangesBody": "Je kunt de app afsluiten en alle wijzigingen kwijtraken, of je kunt de bestandsinstellingen gebruiken om gegevens te exporteren",
|
||||
"appSettingsError": "Erreur lancement appli",
|
||||
"appSettingsErrorBody": "Il y a eu une erreur lors du chargement des paramètres. Merci de revérifier l'URL de l'application ou contacter votre administrateur.",
|
||||
"appNotSupportedError": "Votre navigateur n'est pas compatible avec certaines fonctionnalités importantes que nous utilisons.",
|
||||
"appTabWarn": "Trop d'onglets",
|
||||
"appTabWarnBody": "KeeWeb ne peut être utilisé dans 2 onglets de navigateurs simultanément. Fermer cet onglet, s'il vous plait.",
|
||||
"appRightsAlert": "Protéger l'appli KeeWeb",
|
||||
"appRightsAlertBody1": "Votre application KeeWeb est accessible en écriture. Nous demandons les droits administrateurs pour le mettre en écriture uniquement pour les administrateurs.",
|
||||
"appRightsAlertBody2": "Vous ne voulez pas donner les permissions ? Vous pouvez le faire vous-même à partir d'un terminal",
|
||||
"appBrowserAuthComplete": "Authentification terminée, vous pouvez fermer cet onglet.",
|
||||
"setGenTitle": "Paramètres",
|
||||
"setGenUpdate": "Mettre à jour",
|
||||
"setGenNewVersion": "Une nouvelle version a été téléchargée",
|
||||
"setGenReleaseNotes": "Voir les notes de version",
|
||||
"setGenReloadToUpdate": "Recharger pour mettre à jour",
|
||||
"setGenUpdateManual": "Une nouvelle version est disponible. Elle vérifiera les mises à jour et les installera automatiquement, mais cette mise à jour automatique à partir de votre version n'est pas possible.",
|
||||
"setGenDownloadUpdate": "Télécharger mise à jour",
|
||||
"setGenUpdateAuto": "Télécharger et installer automatiquement",
|
||||
"setGenUpdateCheck": "Vérifier mais ne pas installer",
|
||||
"setGenNoUpdate": "Ne jamais vérifier les mises à jour",
|
||||
"setGenUpdateChecking": "Vérification des mises à jour",
|
||||
"setGenCheckUpdate": "Vérifier les mises à jour",
|
||||
"setGenErrorChecking": "Erreur vérification des mises à jour",
|
||||
"setGenLastCheckSuccess": "Dernière vérification réussie fut le {}",
|
||||
"setGenLastCheckVer": "la dernière version était {}",
|
||||
"setGenCheckedAt": "Vérifié le",
|
||||
"setGenLatestVer": "vous utilisez la dernière version",
|
||||
"setGenNewVer": "nouvelle version {} disponible",
|
||||
"setGenDownloadingUpdate": "Téléchargement de la mise à jour...",
|
||||
"setGenExtractingUpdate": "Décompression de la mise à jour...",
|
||||
"setGenCheckErr": "Une erreur est intervenue durant le téléchargement de la mise à jour",
|
||||
"setGenNeverChecked": "Ne jamais vérifier les mises à jour",
|
||||
"setGenRestartToUpdate": "Redémarrer KeeWeb pour mettre à jour",
|
||||
"setGenDownloadAndRestart": "Télécharger la mise à jour et redémarrer",
|
||||
"setGenAppearance": "Apparence",
|
||||
"setGenTheme": "Thème",
|
||||
"setGenThemeDefault": "Défaut",
|
||||
"setGenThemeDark": "Sombre",
|
||||
"setGenThemeLight": "Clair",
|
||||
"setGenThemeBlue": "Bleu",
|
||||
"setGenThemeFb": "Bleu foncé",
|
||||
"setGenThemeBl": "Bleu clair",
|
||||
"setGenThemeBrown": "Brownie",
|
||||
"setGenThemeDb": "Marron foncé",
|
||||
"setGenThemeLb": "Brun clair",
|
||||
"setGenThemeTerminal": "Terminal",
|
||||
"setGenThemeTe": "Terminal",
|
||||
"setGenThemeLt": "Terminal clair",
|
||||
"setGenThemeHighContrast": "Contraste élevé",
|
||||
"setGenThemeHc": "Contraste élevé",
|
||||
"setGenThemeDc": "Contraste foncé",
|
||||
"setGenThemeSol": "Solarisé",
|
||||
"setGenThemeSd": "Solarized dark",
|
||||
"setGenThemeSl": "Solarized light",
|
||||
"setGenMoreThemes": "Plus de thèmes",
|
||||
"setGenAutoSwitchTheme": "Basculer automatiquement entre les thèmes clairs et foncés lorsque possible",
|
||||
"setGenLocale": "Langue",
|
||||
"setGenLocOther": "d'autres langues sont disponibles en tant que modules",
|
||||
"setGenFontSize": "Taille de police",
|
||||
"setGenFontSizeNormal": "Normal",
|
||||
"setGenFontSizeLarge": "Large",
|
||||
"setGenFontSizeLargest": "Très large",
|
||||
"setGenTitlebarStyle": "Style de fenêtres (redémarrage requis)",
|
||||
"setGenTitlebarStyleDefault": "Par défaut",
|
||||
"setGenTitlebarStyleHidden": "Titre personnalisé",
|
||||
"setGenTitlebarStyleHiddenInset": "Titre personnalisé, fenêtre déplaçable",
|
||||
"setGenShowSubgroups": "Afficher les entrées de tous les sous groupes",
|
||||
"setGenTableView": "Voir les entrées en liste",
|
||||
"setGenColorfulIcons": "Icônes colorées dans la liste",
|
||||
"setGenUseMarkdown": "Markdown dans les notes",
|
||||
"setGenUseGroupIconForEntries": "Utilise automatiquement l'icône de groupe pour les nouvelles entrées",
|
||||
"setGenDirectAutotype": "Si une seule entrée correspondante est trouvée, sélectionner celle-ci automatiquement pour l'autotype.",
|
||||
"setGenFunction": "Opérations",
|
||||
"setGenAutoSyncOnClose": "Sauvegarde et synchro automatique à la fermeture",
|
||||
"setGenAutoSyncTimer": "Sauvegarde et synchro automatique régulièrement",
|
||||
"setGenAutoSyncTimerOff": "Désactivé",
|
||||
"setGenAutoSyncTimerInterval": "Toutes les {} minute(s)",
|
||||
"setGenRememberKeyFiles": "Se souvenir des fichiers clé",
|
||||
"setGenNoRememberKeyFiles": "Ne pas se souvenir",
|
||||
"setGenRememberKeyFilesData": "Sauvegarder dans le stockage interne de l'appli",
|
||||
"setGenRememberKeyFilesPath": "Se souvenir seulement des chemins des fichiers",
|
||||
"setGenLockInactive": "si l'application est inactive",
|
||||
"setGenNoAutoLock": "Ne pas verrouiller automatiquement",
|
||||
"setGenLockMinutes": "Dans {} minutes",
|
||||
"setGenLockHour": "Dans une heure",
|
||||
"setGenLockHours": "Dans {} heures",
|
||||
"setGenLockDay": "Dans 1 jour",
|
||||
"setGenClearClip": "Vider le presse-papier après une copie",
|
||||
"setGenNoClear": "Ne pas effacer",
|
||||
"setGenClearSeconds": "Dans {} secondes",
|
||||
"setGenClearMinute": "Dans une minute",
|
||||
"setGenMinInstead": "Réduire l'application au lieu de fermer",
|
||||
"setGenMinOnFieldCopy": "Réduire sur copie d'un champ",
|
||||
"setGenLock": "Verrou Auto",
|
||||
"setGenLockMinimize": "quand l'appli est en arrière-plan",
|
||||
"setGenLockCopy": "sur copie de m.d.p.",
|
||||
"setGenLockAutoType": "sur saisie auto",
|
||||
"setGenLockOrSleep": "Quand l'ordinateur est verrouillé ou mis en sommeil",
|
||||
"setGenStorage": "Stockage",
|
||||
"setGenDisableOfflineStorage": "Ne pas mettre en cache des fichiers d'un stockage offline",
|
||||
"setGenShortLivedStorageToken": "Utiliser des sessions de courte-durée avec les fournisseurs de stockage dans le cloud",
|
||||
"setGenStorageLogout": "Déconnexion",
|
||||
"setGenShowAdvanced": "Afficher les paramètres avancés",
|
||||
"setGenDevTools": "Afficher les outils de développement",
|
||||
"setGenTryBeta": "Essayer une fois la version beta ",
|
||||
"setGenTryBetaWarning": "Fichiers non sauvegardés",
|
||||
"setGenTryBetaWarningBody": "Sauvegarder tous les fichiers et re-cliquer ce bouton",
|
||||
"setGenShowAppLogs": "Voir les logs",
|
||||
"setGenReloadApp": "Recharger l'application",
|
||||
"setGenFieldLabelDblClickAutoType": "Remplissage auto par double clic sur les noms de champ",
|
||||
"setGenTouchId": "Touch ID",
|
||||
"setGenTouchIdDisabled": "Ne pas utiliser Touch ID",
|
||||
"setGenTouchIdMemory": "Dévérouiller avec Touch ID uniquement quand KeeWeb est lancé",
|
||||
"setGenTouchIdFile": "Toujours utiliser Touch ID à la place du mot de passe",
|
||||
"setGenTouchIdPass": "Exige le mot de passe après",
|
||||
"setGenAudit": "Audit",
|
||||
"setGenAuditPasswords": "Voir des avertissements sur la force du mot de passe",
|
||||
"setGenAuditPasswordEntropy": "Vérifier la longueur et l'aléas du mot de passe",
|
||||
"setGenExcludePinsFromAudit": "Ne jamais vérifier les codes PIN courts, comme 123456",
|
||||
"setGenCheckPasswordsOnHIBP": "Vérifier les mots de passe en utilisant un service en ligne {}",
|
||||
"setGenHelpHIBP": "KeeWeb peut vérifier si vos mots de passes ont été précédemment exposés à une faille de sécurité en utilisant un service en ligne. Votre mot de passe ne peut pas être récupéré à partir de données envoyées en ligne, cependant le nombre de mots de passes vérifiés peut être exposé. Plus d'informations sur votre sécurité en utilisant ce service peuvent être trouvées {}. Si cette option est activée, KeeWeb vérifiera automatiquement vos mots de passes là.",
|
||||
"setGenHelpHIBPLink": "ici",
|
||||
"setGenAuditPasswordAge": "Vieux mots de passe",
|
||||
"setGenAuditPasswordAgeOff": "Ne pas afficher d'avertissements à propos des vieux mots de passe",
|
||||
"setGenAuditPasswordAgeOneYear": "Afficher des avertissements pour les mots de passe plus vieux qu'un an",
|
||||
"setGenAuditPasswordAgeYears": "Afficher des avertissement pour les mots de passe plus vieux que {} ans",
|
||||
"setFilePath": "Chemin",
|
||||
"setFileStorage": "Le fichier est ouvert de {}.",
|
||||
"setFileIntl": "Le fichier est conservé dans le stockage interne de l'application",
|
||||
"setFileLocalHint": "Voulez-vous travailler avec des fichiers locaux ?",
|
||||
"setFileDownloadApp": "Télécharger l'application",
|
||||
"setFileSave": "Enregistrer",
|
||||
"setFileSaveTo": "Enregistrer sous...",
|
||||
"setFileClose": "Fermer",
|
||||
"setFileSync": "Sync",
|
||||
"setFileSyncVerb": "Synchroniser",
|
||||
"setFileSaveToXml": "XML",
|
||||
"setFileSaveToHtml": "HTML",
|
||||
"setFileLastSync": "Dernière synchro",
|
||||
"setFileLastSyncUnknown": "inconnu",
|
||||
"setFileSyncInProgress": "synchronisation en cours",
|
||||
"setFileSyncError": "Sync erreur",
|
||||
"setFilePass": "Mot de passe principal",
|
||||
"setFileConfirmPass": "Confirmer le mot de passe Maître",
|
||||
"setFilePassChange": "pour changer le mot de passe, merci de le saisir dans le champ Confirmation",
|
||||
"setFilePassChanged": "Le mot de passe a été changé ; laissez le champ vide pour utiliser l'ancien mot de passe",
|
||||
"setFilePassNotMatch": "les mots de passe ne correspondent pas, veuillez réessayer",
|
||||
"setFileKeyFile": "Fichier clé",
|
||||
"setFileSelKeyFile": "Sélectionner fichier clé",
|
||||
"setFileNames": "Noms",
|
||||
"setFileDefUser": "Utilisateur par défaut",
|
||||
"setFileEnableTrash": "Activer corbeille",
|
||||
"setFileHistMode": "Historique",
|
||||
"setFileHistLimited": "Supprimer les anciens enregistrements automatiquement",
|
||||
"setFileHistDisabled": "Ne pas sauvegarder l'historique",
|
||||
"setFileHistUnlimited": "Garder les enregistrements de l'historique pour toujours",
|
||||
"setFileHistLen": "Longueur de l'historique, conserve les derniers enregistrements",
|
||||
"setFileHistSize": "Taille de l'historique, Mo par fichier",
|
||||
"setFileBackups": "Sauvegardes",
|
||||
"setFileBackupEnable": "Sauvegarder ce fichier",
|
||||
"setFileBackupPath": "Chemin",
|
||||
"setFileBackupTime": "Lancer sauvegardes ",
|
||||
"setFileBackupNow": "Sauvegarder...",
|
||||
"setFileBackupNowWorking": "En cours...",
|
||||
"setFileBackupError": "Erreur sauvegarde",
|
||||
"setFileBackupErrorDescription": "Erreur à l'écriture du fichier",
|
||||
"setFileBackupErrorIsDir": "Chemin de sauvegarde invalide",
|
||||
"setFileBackupErrorIsDirDescription": "Le chemin de sauvegarde semble pointer sur un répertoire. Merci de spécifier un fichier dans un répertoire à la place.",
|
||||
"setFileBackupOnSave": "A chaque fois que je sauvegarde",
|
||||
"setFileBackupDaily": "Quotidien",
|
||||
"setFileBackupWeekly": "Chaque semaine",
|
||||
"setFileBackupMonthly": "Chaque mois",
|
||||
"setFileBackupManually": "Manuellement, pas de sauvegarde automatique",
|
||||
"setFileRounds": "Cycles de chiffrement de la clé",
|
||||
"setFileKdfParams": "Fonction de dérivation de clé",
|
||||
"setFileKdfParamsIter": "Itérations",
|
||||
"setFileKdfParamsMem": "Mémoire, Ko",
|
||||
"setFileKdfParamsPar": "Parallèlisme",
|
||||
"setFileKeyChangeForce": "Demander à changer la clé après (jours)",
|
||||
"setFileUseKeyFile": "Utiliser un fichier clé",
|
||||
"setFileUseGenKeyFile": "Utiliser le fichier clé généré",
|
||||
"setFileUseOldKeyFile": "Utiliser l'ancien fichier clé",
|
||||
"setFileGenKeyFile": "Générer un nouveau fichier clé",
|
||||
"setFileDontUseKeyFile": "Ne pas utiliser de fichier clé",
|
||||
"setFileEmptyPass": "Mot de passe vide",
|
||||
"setFileEmptyPassBody": "Sauvegarder la base de données avec un mot de passe vide la laisse sans aucune protection. Voulez-vous vraiment faire cela ?",
|
||||
"setFileSaveError": "Erreur sauvegarde",
|
||||
"setFileSaveErrorBody": "Erreur sauvegarde dans le fichier",
|
||||
"setFileAlreadyExists": "Existe déjà",
|
||||
"setFileAlreadyExistsBody": "Le fichier {} existe déjà. L'écraser ?",
|
||||
"setFileUnsaved": "Modif non sauvegardées",
|
||||
"setFileUnsavedBody": "Il y a des modifications non sauvegardées dans ce fichier",
|
||||
"setFileCloseNoSave": "Fermer et perdre les modifs",
|
||||
"setFileDontClose": "Ne pas fermer",
|
||||
"setFileFormatVersion": "Format de fichier",
|
||||
"setFileExportRaw": "Exporter vos mots de passe",
|
||||
"setFileExportRawBody": "Le fichier exporté contiendra vos mots de passe, il n'y seront plus cryptés. Voulez vous continuer ?",
|
||||
"setFileDeviceIntro": "Les codes à usage unique de cette {} vont être affichés dans l'application.",
|
||||
"setFileDeviceSettings": "Paramètres",
|
||||
"setFileYubiKey": "YubiKey",
|
||||
"setFileDontUseYubiKey": "Ne pas utiliser une YubiKey",
|
||||
"setFileRefreshYubiKeyList": "Rafraichir la liste",
|
||||
"setFileYubiKeyHeader": "YubiKey",
|
||||
"setFileYubiKeyBody": "Utiliser une YubiKey comme partie de la clé maitre est dangeureux, et vous risquez de perdre l'accès à vos mots de passe si quelque chose tourne mal. Avez vous réalisé une sauvegarde de votre fichier avant de modifier les paramètres ?",
|
||||
"setFileYubiKeyErrorEmptyMac": "Il semble qu'il y a une YubiKey connectée, mais elle est invisible. KeeWeb doit avoir été autorisé dans la section \"Surveillance de l’entrée\" des paramètres de sécurité pour se connecter à la YubiKey.",
|
||||
"setShTitle": "Raccourcis",
|
||||
"setShShowAll": "montrer tout",
|
||||
"setShColors": "montrer les champs en couleur",
|
||||
"setShTrash": "aller à Corbeille",
|
||||
"setShFind": "rechercher ou juste commencer à taper",
|
||||
"setShClearSearch": "effacer recherche",
|
||||
"setShCopyPass": "copier le mot de passe ou le champ sélectionné",
|
||||
"setShCopyUser": "copier le nom",
|
||||
"setShCopyUrl": "copier le site",
|
||||
"setShAutoType": "saisie automatique pour cette entrée",
|
||||
"setShPrev": "aller au précédent",
|
||||
"setShNext": "aller au prochain",
|
||||
"setShCreateEntry": "créer une entrée",
|
||||
"setShOpen": "ouvrir/nouveau",
|
||||
"setShSave": "sauver tous les fichiers",
|
||||
"setShGen": "générer un mot de passe",
|
||||
"setShSet": "paramètres",
|
||||
"setShCopyPassGlobal": "copier le mot de passe",
|
||||
"setShCopyOtp": "Copier l'OTP",
|
||||
"setShAutoTypeGlobal": "saisie automatique",
|
||||
"setShCopyPassOnly": "Copier mot de passe",
|
||||
"setShRestoreApp": "Ouvrir {}",
|
||||
"setShGlobal": "Ces raccourcis à l'échelle du système fonctionnent lorsque l'application s'exécute en arrière-plan. Vous pouvez cliquer dessus pour sélectionner votre propre raccourci.",
|
||||
"setShLock": "verrouiller la base",
|
||||
"setShEdit": "Appuyez sur une nouvelle combinaison de touches pour la définir comme raccourci.",
|
||||
"setPlInstallTitle": "Installer de nouveaux modules",
|
||||
"setPlInstallDesc": "Les modules (plugins) de KeeWeb ajoutent des fonctionnalités, thèmes et langues à KeeWeb. Les modules sont lancés avec les même privilèges que KeeWeb : ils peuvent accéder et gérer tous vos mots de passe. Ne jamais installer de modules auxquels vous ne faites pas confiance.",
|
||||
"setPlInstallLabel": "URL du module",
|
||||
"setPlInstallBtn": "Installer",
|
||||
"setPlInstallBtnProgress": "Installation",
|
||||
"setPlUninstallBtn": "Désintaller",
|
||||
"setPlDisableBtn": "Désactiver",
|
||||
"setPlEnableBtn": "Activer",
|
||||
"setPlUpdateBtn": "Mettre à jour",
|
||||
"setPlLocaleBtn": "Changer pour cette langue",
|
||||
"setPlThemeBtn": "Changer pour ce thème",
|
||||
"setPlJs": "code",
|
||||
"setPlCss": "styles",
|
||||
"setPlLoc": "langue",
|
||||
"setPlCreatedBy": "Créé par {}",
|
||||
"setPlLoadTime": "{} pour charger",
|
||||
"setPlLastUpdate": "Dernière vérification des mises à jour",
|
||||
"setPlLoadError": "erreur au chargement du module",
|
||||
"setPlGalleryLoading": "Chargement des modules, patientez un instant",
|
||||
"setPlGalleryLoadError": "erreur au chargement des modules",
|
||||
"setPlInstallUrlTitle": "Ajouter un module à partir d'une URL",
|
||||
"setPlInstallUrlDesc": "Si un module n'est pas dans le catalogue, vous pouvez l'installer manuellement à partir d'une URL.",
|
||||
"setPlOfficial": "Module officiel KeeWeb",
|
||||
"setPlSearch": "Rechercher des modules",
|
||||
"setPlDevelop": "Vous souhaitez développer vos propres modules ?",
|
||||
"setPlDevelopStart": "Commencez ici",
|
||||
"setPlTranslate": "Ou vous pouvez {}",
|
||||
"setPlTranslateLink": "traduire l'appli dans votre langue",
|
||||
"setPlAutoUpdate": "Mise à jour automatique",
|
||||
"setPlLoadGallery": "Charger le catalogue",
|
||||
"setDevicesTitle": "Appareils",
|
||||
"setDevicesEnableUsb": "Autoriser les interactions avec les appareils USB",
|
||||
"setDevicesYubiKeyIntro": "Il est recommandé de lire {} avant d'utiliser une YubiKey.",
|
||||
"setDevicesYubiKeyIntroLink": "ce document",
|
||||
"setDevicesYubiKeyToolsDesc": "Pour permettre l'utilisation de YubiKey dans ce mode, vous devez installer un outil appelé {}.",
|
||||
"setDevicesYubiKeyToolsDesc2": "{} pour avoir plus d'information sur cet outil.",
|
||||
"setDevicesYubiKeyToolsDescLink": "Cliquez ici",
|
||||
"setDevicesYubiKeyToolsStatusChecking": "Vérification si {} est installé",
|
||||
"setDevicesYubiKeyToolsStatusOk": "{} est installé",
|
||||
"setDevicesYubiKeyToolsStatusError": "{} n'est pas installé, ou ne fonctionne pas",
|
||||
"setDevicesYubiKeyOtpTitle": "Codes à usage unique",
|
||||
"setDevicesYubiKeyOtpDesc": "YubiKey peut être utilisé pour générer des codes à usage unique pour différents services.",
|
||||
"setDevicesYubiKeyOtpShowIcon": "Afficher l'icône sur l'écran de démarrage",
|
||||
"setDevicesYubiKeyOtpAutoOpen": "Charger automatiquement les codes à usage unique lorsqu'il y a des fichiers ouverts",
|
||||
"setDevicesYubiKeyOtpMatchEntries": "Afficher les codes à usage unique qui correspondent dans les entrées",
|
||||
"setDevicesYubiKeyChalRespTitle": "Challenge-Réponse",
|
||||
"setDevicesYubiKeyChalRespDesc": "Il est aussi possible d'utiliser une YubiKey en mode Challenge-Réponse, afin qu'une partie de la clé privée utilisée pour crypter les fichiers provienne d'une YubiKey.",
|
||||
"setDevicesYubiKeyChalRespShow": "Proposer l'option d'utiliser une YubiKey à l'ouverture des fichiers",
|
||||
"setDevicesYubiKeyStuckWorkaround": "Reconnecter la YubiKey si elle semble bloquée au moment du chargement",
|
||||
"setDevicesYubiKeyRememberChalResp": "Se souvenir du challenge-réponse le temps que l'application reste ouverte",
|
||||
"setAboutTitle": "À propos",
|
||||
"setAboutBuilt": "Cette appli est construite avec ces outils géniaux",
|
||||
"setAboutLic": "Licence",
|
||||
"setAboutLicComment": "Cette application et tous les composants qu'elle contient qui ne sont pas dans le domaine public sont sous licence MIT, sauf si autrement spécifié",
|
||||
"setAboutFirst": "Ceci est une application open-source créée par {}",
|
||||
"setAboutSecond": " et sous licence {}.",
|
||||
"setAboutSource": "Le code source et les problèmes sont sur {}.",
|
||||
"setHelpFormat": "Format de fichier",
|
||||
"setHelpFormatBody": "Ceci est un portage de l'appli {} développée avec les technologies web. Il interprète les fichiers au format KeePass (kdbx). Vous pouvez créer de tels fichiers (bases de données de mots de passe) soit avec KeePass, soit avec cette appli. Le format de fichier est 100% compatible et devrait être compris par les 2 applications.",
|
||||
"setHelpProblems": "Un problème ?",
|
||||
"setHelpProblems1": "Si quelque chose ne va pas, merci de {}",
|
||||
"setHelpProblems2": "ou {}",
|
||||
"setHelpOpenIssue": "faire connaître le problème sur GitHub",
|
||||
"setHelpContactLink": "contactez le développeur directement",
|
||||
"setHelpAppInfo": "Information",
|
||||
"setHelpOtherPlatforms": "Autres plateformes",
|
||||
"setHelpDesktopApps": "Applis Desktop",
|
||||
"setHelpWebApp": "Appli web",
|
||||
"setHelpUpdates": "Mises à jour",
|
||||
"setHelpTwitter": "Appli twitter",
|
||||
"dropboxSetupDesc": "Une configuration particulière est nécessaire pour utiliser Dropbox dans une application auto-hébergée. Merci de créer votre propre application Dropbox et d'inscrire sa clé ci-dessous.",
|
||||
"dropboxAppKey": "Clé Dropbox",
|
||||
"dropboxAppKeyDesc": "Copier la clé de votre appli Dropbox (Réglages développeur)",
|
||||
"dropboxAppKeyHint": "Votre \"Dropbox App Key\"",
|
||||
"dropboxAppSecret": "Votre \"Dropbox App Secret\"",
|
||||
"dropboxAppSecretDesc": "Votre \"Dropbox App Secret\" peut être trouvée à côté de votre \"Dropbox App Key\".",
|
||||
"dropboxFolder": "Dossier Appli",
|
||||
"dropboxFolderDesc": "Si votre appli est liée à l'espace Dropbox entier (et non un dossier), définir le dossier avec vos fichiers Kdbx ici",
|
||||
"dropboxFolderSettingsDesc": "Sélectionner un dossier dans votre Dropbox où les fichiers sont stockés (à la racine par défaut)",
|
||||
"dropboxFolderPlaceholder": "dossier par défaut",
|
||||
"dropboxLink": "Lier l'appli à",
|
||||
"dropboxLinkApp": "Dossier de l'appli (Apps/KeeWeb)",
|
||||
"dropboxLinkFull": "Tout Dropbox ou n'importe quel dossier",
|
||||
"dropboxLinkCustom": "Votre appli Dropbox",
|
||||
"gdriveSharedWithMe": "Partagé avec moi",
|
||||
"webdavSaveMethod": "Méthode de sauvegarde",
|
||||
"webdavSaveMove": "Envoyer un fichier temporaire et le déplacer",
|
||||
"webdavSavePut": "Écraser le fichier kdbx avec PUT",
|
||||
"webdavNoLastModified": "L'entête HTTP \"Last-Modified\" est absent",
|
||||
"webdavStatReload": "Toujours recharger le fichier au lieu de se fier à l'entête HTTP \"Last-Modified\"",
|
||||
"launcherSave": "Sauvegarder base des mots de passe",
|
||||
"launcherFileFilter": "Fichiers Keepass",
|
||||
"authPopupRequired": "Les pop-up sont bloquées",
|
||||
"authPopupRequiredBody": "Merci d'autoriser les pop-ups pour cette appli dans votre navigateur ou essayez à nouveau.",
|
||||
"exportFileInfo": "Propriétés du fichier",
|
||||
"exportHtmlName": "Nom",
|
||||
"exportHtmlDate": "Date d'exportation",
|
||||
"exportGenerator": "Logiciel",
|
||||
"exportDescription": "Ce fichier est généré par {}.",
|
||||
"importCsvTitle": "Import depuis un CSV",
|
||||
"importCsvRun": "Importer",
|
||||
"importIgnoreField": "Ignorer",
|
||||
"importTo": "Les entrées seront importées dans",
|
||||
"importNewFile": "Nouveau fichier",
|
||||
"yubiKeyStuckError": "La YubiKey semble coincée, la réparation automatique est activable dans les paramètres de l'application.",
|
||||
"yubiKeyNoKeyHeader": "Une YubiKey est requise",
|
||||
"yubiKeyNoKeyBody": "Merci d'insérer votre YubiKey avec le numéro de série {}",
|
||||
"yubiKeySlot": "port",
|
||||
"yubiKeyTouchRequestedHeader": "Touchez votre YubiKey",
|
||||
"yubiKeyTouchRequestedBody": "Merci de toucher votre YubiKey avec le numéro de série {}",
|
||||
"yubiKeyDisabledErrorHeader": "L'USB est désactivé",
|
||||
"yubiKeyDisabledErrorBody": "Yubikey est nécessaire pour ouvrir ce fichier, merci d'activer les appareils USB dans les paramètres",
|
||||
"yubiKeyErrorWithCode": "Erreur Yubikey code {}.",
|
||||
"bioOpenAuthPrompt": "ouvrir \"{}\""
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
CACHE MANIFEST
|
||||
|
||||
# YYYY-MM-DD:v0.0.0
|
||||
# updmin:v0.0.0
|
||||
|
||||
CACHE:
|
||||
index.html
|
||||
|
||||
NETWORK:
|
||||
*
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"name": "KeeWeb",
|
||||
"short_name": "KeeWeb",
|
||||
"description": "Free cross-platform password manager compatible with KeePass",
|
||||
"display": "standalone",
|
||||
"orientation": "any",
|
||||
"theme_color": "#6386EC",
|
||||
"background_color": "#6386EC"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square70x70logo src="/icons/mstile-70x70.png"/>
|
||||
<square150x150logo src="/icons/mstile-150x150.png"/>
|
||||
<square310x310logo src="/icons/mstile-310x310.png"/>
|
||||
<wide310x150logo src="/icons/mstile-310x150.png"/>
|
||||
<TileColor>#6386ec</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "KeeWeb",
|
||||
"short_name": "KeeWeb",
|
||||
"description": "Free cross-platform password manager compatible with KeePass",
|
||||
"start_url": "./",
|
||||
"display": "standalone",
|
||||
"theme_color": "#6386ec",
|
||||
"background_color": "#6386ec",
|
||||
"icons": [
|
||||
{
|
||||
"src": "icons/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "icons/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyZAmWXMpKsh9WCyUO5TR
|
||||
2/QIJsgyaViWRAgY5nHZhTR/sjhfG97OeurW7NYC4XPsnyJDepEjDsj9LNzquMES
|
||||
xsLRSmxo+m1lVWn8Q59LwqOvUK+BKH4eZYF89Nwv2HZjZnr/w7xYcxJbhOuAzpbq
|
||||
FD/gAvlozrsHG9uI95h1FHPd6mfolEHLK30LXPQ5FdJ/AWb29ILKYbVEdM8qsjoN
|
||||
fSICw7XTbd7wB0TMcgRpjWllzKaftxCO38svhBjAz4cUvtZ05yi4dzK0VsJHfW33
|
||||
uno3YBn0/bwLkqMzD1/Hv05p6CVzuDTOCBsXkEobra8OBviatoSukot6cS4Famki
|
||||
9QIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
@@ -1,9 +1,9 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0oZB2Kt7AzRFNqf8FuO3
|
||||
C3kepHPAIQYiDPYdQxHcsiaFCwyKVx6K1cE/3vBhb8/2rj+QIIWNfAAuu1Y+2VK9
|
||||
0ZBeq6HciukWzQRO/HWhfdy0c7JwDAslmyGI5olj0ZQkNLhkde1MiMxjDPpRhZtd
|
||||
JaryVO5cFJaJESpv3dV6m0qXsaQCluWYOSNfSjP9C8o2zRVjSi3ZQZnZIV5pnk9K
|
||||
2MtlZIPXrN9iJiM5zZ9DTSnqApI6dC9mX4R3LvGN+GTovm9C8Crl+qb106nGRR3L
|
||||
cweicDnPyMtZLa/E0DBpWYxUVLDp6WeLhxoUBr+6+t3Xp9IDnPoANDQXJXD0f1vQ
|
||||
xQIDAQAB
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnRq2k3TTx0ewTe6wDr6Q
|
||||
VeB5diwiIWzsJD+ApfZu1KNPedcAgslAfjpNsYF1if6cYsPMJH70xJ2np6RQBl1V
|
||||
PdwShOuxkD7m0BD5Hw/Aar8Hdp5cvAdOOMdBO+0DbGeUMy+z66s+oUCJmqVp19T6
|
||||
PkkxbhN08rgtT7v+aFvrbqbO/vlsskbJpH2K2io+e1XmRGPnSr9q4KSqfGbTfe5g
|
||||
LwDIOFd66Z4mb5Utb5wWpsy6Gjh06Yf257AccGD3A1bkTNOyeeX0tqciYBePWMk0
|
||||
icP/aZ6hnErfhnUKf3tOgPLppSHiGcaSKekhChZ2xLUs3U64JwrXSmwHj+TzdO3S
|
||||
0QIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
const AppModel = require('./models/app-model');
|
||||
const AppView = require('./views/app-view');
|
||||
const AppSettingsModel = require('./models/app-settings-model');
|
||||
const UpdateModel = require('./models/update-model');
|
||||
const RuntimeDataModel = require('./models/runtime-data-model');
|
||||
const FileInfoCollection = require('./collections/file-info-collection');
|
||||
const KeyHandler = require('./comp/key-handler');
|
||||
const IdleTracker = require('./comp/idle-tracker');
|
||||
const PopupNotifier = require('./comp/popup-notifier');
|
||||
const SingleInstanceChecker = require('./comp/single-instance-checker');
|
||||
const Alerts = require('./comp/alerts');
|
||||
const Updater = require('./comp/updater');
|
||||
const AuthReceiver = require('./comp/auth-receiver');
|
||||
const ExportApi = require('./comp/export-api');
|
||||
const SettingsManager = require('./comp/settings-manager');
|
||||
const PluginManager = require('./plugins/plugin-manager');
|
||||
const Launcher = require('./comp/launcher');
|
||||
const Timeouts = require('./const/timeouts');
|
||||
const FeatureDetector = require('./util/feature-detector');
|
||||
const KdbxwebInit = require('./util/kdbxweb-init');
|
||||
const Locale = require('./util/locale');
|
||||
|
||||
const ready = Launcher && Launcher.ready || $;
|
||||
|
||||
ready(() => {
|
||||
if (FeatureDetector.isPopup && AuthReceiver.receive()) {
|
||||
return;
|
||||
}
|
||||
loadMixins();
|
||||
|
||||
const appModel = new AppModel();
|
||||
|
||||
Promise.resolve()
|
||||
.then(loadConfigs)
|
||||
.then(initModules)
|
||||
.then(loadRemoteConfig)
|
||||
.then(ensureCanRun)
|
||||
.then(showApp)
|
||||
.then(autoUpdatePlugins)
|
||||
.catch(e => {
|
||||
appModel.appLogger.error('Error starting app', e);
|
||||
});
|
||||
|
||||
function loadMixins() {
|
||||
require('./mixins/view');
|
||||
require('./helpers');
|
||||
}
|
||||
|
||||
function ensureCanRun() {
|
||||
return Promise.resolve()
|
||||
.then(() => FeatureDetector.ensureCanRun())
|
||||
.catch(e => {
|
||||
Alerts.error({
|
||||
header: Locale.appSettingsError,
|
||||
body: Locale.appNotSupportedError,
|
||||
buttons: [],
|
||||
esc: false, enter: false, click: false
|
||||
});
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
function loadConfigs() {
|
||||
return Promise.all([
|
||||
AppSettingsModel.instance.load(),
|
||||
UpdateModel.instance.load(),
|
||||
RuntimeDataModel.instance.load(),
|
||||
FileInfoCollection.instance.load()
|
||||
]);
|
||||
}
|
||||
|
||||
function initModules() {
|
||||
KeyHandler.init();
|
||||
IdleTracker.init();
|
||||
PopupNotifier.init();
|
||||
KdbxwebInit.init();
|
||||
window.kw = ExportApi;
|
||||
return PluginManager.init();
|
||||
}
|
||||
|
||||
function showSettingsLoadError() {
|
||||
Alerts.error({
|
||||
header: Locale.appSettingsError,
|
||||
body: Locale.appSettingsErrorBody,
|
||||
buttons: [],
|
||||
esc: false, enter: false, click: false
|
||||
});
|
||||
}
|
||||
|
||||
function loadRemoteConfig() {
|
||||
return Promise.resolve().then(() => {
|
||||
SettingsManager.setBySettings(appModel.settings);
|
||||
const configParam = getConfigParam();
|
||||
if (configParam) {
|
||||
return appModel.loadConfig(configParam).then(() => {
|
||||
SettingsManager.setBySettings(appModel.settings);
|
||||
}).catch(e => {
|
||||
if (!appModel.settings.get('cacheConfigSettings')) {
|
||||
showSettingsLoadError();
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showApp() {
|
||||
const skipHttpsWarning = localStorage.skipHttpsWarning || appModel.settings.get('skipHttpsWarning');
|
||||
const protocolIsInsecure = ['https:', 'file:', 'app:'].indexOf(location.protocol) < 0;
|
||||
const hostIsInsecure = location.hostname !== 'localhost';
|
||||
if (protocolIsInsecure && hostIsInsecure && !skipHttpsWarning) {
|
||||
Alerts.error({ header: Locale.appSecWarn, icon: 'user-secret', esc: false, enter: false, click: false,
|
||||
body: Locale.appSecWarnBody1 + '<br/><br/>' + Locale.appSecWarnBody2,
|
||||
buttons: [
|
||||
{ result: '', title: Locale.appSecWarnBtn, error: true }
|
||||
],
|
||||
complete: showView
|
||||
});
|
||||
} else {
|
||||
showView();
|
||||
}
|
||||
}
|
||||
|
||||
function autoUpdatePlugins() {
|
||||
setTimeout(() => PluginManager.runAutoUpdate(), Timeouts.AutoUpdatePluginsAfterStart);
|
||||
}
|
||||
|
||||
function showView() {
|
||||
appModel.prepare();
|
||||
new AppView({ model: appModel }).render();
|
||||
Updater.init();
|
||||
SingleInstanceChecker.init();
|
||||
const time = Math.round(performance.now());
|
||||
appModel.appLogger.info(`Started in ${time}ms ¯\\_(ツ)_/¯`);
|
||||
}
|
||||
|
||||
function getConfigParam() {
|
||||
const metaConfig = document.head.querySelector('meta[name=kw-config]');
|
||||
if (metaConfig && metaConfig.content && metaConfig.content[0] !== '(') {
|
||||
return metaConfig.content;
|
||||
}
|
||||
const match = location.search.match(/[?&]config=([^&]+)/i);
|
||||
if (match && match[1]) {
|
||||
return match[1];
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,13 +0,0 @@
|
||||
const Launcher = require('../comp/launcher');
|
||||
|
||||
const AutoTypeEmitterFactory = {
|
||||
create: function(callback) {
|
||||
if (Launcher && Launcher.autoTypeSupported) {
|
||||
const AutoTypeEmitter = require('./emitter/auto-type-emitter-' + Launcher.platform());
|
||||
return new AutoTypeEmitter(callback);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = AutoTypeEmitterFactory;
|
||||
@@ -0,0 +1,160 @@
|
||||
import { Features } from 'util/features';
|
||||
import { NativeModules } from 'comp/launcher/native-modules';
|
||||
import { Logger } from 'util/logger';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
import { Timeouts } from 'const/timeouts';
|
||||
|
||||
const logger = new Logger('auto-type-emitter');
|
||||
|
||||
const KeyMap = {
|
||||
tab: 'Tab',
|
||||
enter: 'Return',
|
||||
space: 'Space',
|
||||
up: 'UpArrow',
|
||||
down: 'DownArrow',
|
||||
left: 'LeftArrow',
|
||||
right: 'RightArrow',
|
||||
home: 'Home',
|
||||
end: 'End',
|
||||
pgup: 'PageUp',
|
||||
pgdn: 'PageDown',
|
||||
ins: 'Insert',
|
||||
del: 'ForwardDelete',
|
||||
bs: 'BackwardDelete',
|
||||
esc: 'Escape',
|
||||
win: 'Meta',
|
||||
rwin: 'RightMeta',
|
||||
f1: 'F1',
|
||||
f2: 'F2',
|
||||
f3: 'F3',
|
||||
f4: 'F4',
|
||||
f5: 'F5',
|
||||
f6: 'F6',
|
||||
f7: 'F7',
|
||||
f8: 'F8',
|
||||
f9: 'F9',
|
||||
f10: 'F10',
|
||||
f11: 'F11',
|
||||
f12: 'F12',
|
||||
f13: 'F13',
|
||||
f14: 'F14',
|
||||
f15: 'F15',
|
||||
f16: 'F16',
|
||||
add: 'KeypadPlus',
|
||||
subtract: 'KeypadMinus',
|
||||
multiply: 'KeypadMultiply',
|
||||
divide: 'KeypadDivide',
|
||||
n0: 'D0',
|
||||
n1: 'D1',
|
||||
n2: 'D2',
|
||||
n3: 'D3',
|
||||
n4: 'D4',
|
||||
n5: 'D5',
|
||||
n6: 'D6',
|
||||
n7: 'D7',
|
||||
n8: 'D8',
|
||||
n9: 'D9'
|
||||
};
|
||||
|
||||
const ModMap = {
|
||||
'^': Features.isMac ? 'Command' : 'Ctrl',
|
||||
'+': 'Shift',
|
||||
'%': 'Alt',
|
||||
'^^': 'Ctrl'
|
||||
};
|
||||
|
||||
class AutoTypeEmitter {
|
||||
constructor(callback) {
|
||||
this.callback = callback;
|
||||
this.mod = {};
|
||||
}
|
||||
|
||||
begin() {
|
||||
this.withCallback(NativeModules.kbdEnsureModifierNotPressed());
|
||||
}
|
||||
|
||||
setMod(mod, enabled) {
|
||||
const nativeMod = ModMap[mod];
|
||||
if (!nativeMod) {
|
||||
return this.callback(`Bad modifier: ${mod}`);
|
||||
}
|
||||
NativeModules.kbdKeyMoveWithModifier(!!enabled, [nativeMod]).catch((e) => {
|
||||
logger.error('Error moving modifier', mod, enabled ? 'down' : 'up', e);
|
||||
});
|
||||
if (enabled) {
|
||||
this.mod[nativeMod] = true;
|
||||
} else {
|
||||
delete this.mod[nativeMod];
|
||||
}
|
||||
}
|
||||
|
||||
text(str) {
|
||||
if (!str) {
|
||||
return this.withCallback(Promise.resolve());
|
||||
}
|
||||
const mods = Object.keys(this.mod);
|
||||
if (mods.length) {
|
||||
this.withCallback(NativeModules.kbdTextAsKeys(str, mods));
|
||||
} else {
|
||||
this.withCallback(NativeModules.kbdText(str));
|
||||
}
|
||||
}
|
||||
|
||||
key(key) {
|
||||
const mods = Object.keys(this.mod);
|
||||
if (typeof key === 'number') {
|
||||
this.withCallback(NativeModules.kbdKeyPressWithCharacter(0, key, mods));
|
||||
} else {
|
||||
if (!KeyMap[key]) {
|
||||
return this.callback('Bad key: ' + key);
|
||||
}
|
||||
const code = KeyMap[key];
|
||||
this.withCallback(NativeModules.kbdKeyPress(code, mods));
|
||||
}
|
||||
}
|
||||
|
||||
copyPaste(text) {
|
||||
setTimeout(() => {
|
||||
Launcher.setClipboardText(text);
|
||||
setTimeout(() => {
|
||||
this.withCallback(NativeModules.kbdShortcut('V'));
|
||||
}, Timeouts.AutoTypeCopyPaste);
|
||||
}, Timeouts.AutoTypeCopyPaste);
|
||||
}
|
||||
|
||||
wait(time) {
|
||||
setTimeout(() => this.withCallback(Promise.resolve()), time);
|
||||
}
|
||||
|
||||
waitComplete() {
|
||||
this.withCallback(Promise.resolve());
|
||||
}
|
||||
|
||||
setDelay() {
|
||||
this.callback('Not implemented');
|
||||
}
|
||||
|
||||
withCallback(promise) {
|
||||
promise
|
||||
.then(() => {
|
||||
try {
|
||||
this.callback();
|
||||
} catch (err) {
|
||||
logger.error('Callback error', err);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
const keyPressFailed = err.message === 'Key press failed';
|
||||
if (keyPressFailed) {
|
||||
err.keyPressFailed = true;
|
||||
}
|
||||
try {
|
||||
this.callback(err);
|
||||
} catch (err) {
|
||||
logger.error('Callback error', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export { AutoTypeEmitter };
|
||||
@@ -1,95 +0,0 @@
|
||||
const EntryCollection = require('../collections/entry-collection');
|
||||
|
||||
const urlPartsRegex = /^(\w+:\/\/)?(?:(?:www|wwws|secure)\.)?([^\/]+)\/?(.*)/;
|
||||
|
||||
const AutoTypeFilter = function(windowInfo, appModel) {
|
||||
this.title = windowInfo.title;
|
||||
this.url = windowInfo.url;
|
||||
this.text = '';
|
||||
this.ignoreWindowInfo = false;
|
||||
this.appModel = appModel;
|
||||
};
|
||||
|
||||
AutoTypeFilter.prototype.getEntries = function() {
|
||||
const filter = {
|
||||
text: this.text,
|
||||
autoType: true
|
||||
};
|
||||
this.prepareFilter();
|
||||
let entries = this.appModel.getEntriesByFilter(filter)
|
||||
.map(e => [e, this.getEntryRank(e)]);
|
||||
if (!this.ignoreWindowInfo) {
|
||||
entries = entries.filter(e => e[1]);
|
||||
}
|
||||
entries = entries.sort((x, y) => x[1] === y[1] ? x[0].title.localeCompare(y[0].title) : y[1] - x[1]);
|
||||
entries = entries.map(p => p[0]);
|
||||
return new EntryCollection(entries, {comparator: 'none'});
|
||||
};
|
||||
|
||||
AutoTypeFilter.prototype.hasWindowInfo = function() {
|
||||
return this.title || this.url;
|
||||
};
|
||||
|
||||
AutoTypeFilter.prototype.prepareFilter = function() {
|
||||
this.titleLower = this.title ? this.title.toLowerCase() : null;
|
||||
this.urlLower = this.url ? this.url.toLowerCase() : null;
|
||||
this.urlParts = this.url ? urlPartsRegex.exec(this.urlLower) : null;
|
||||
};
|
||||
|
||||
AutoTypeFilter.prototype.getEntryRank = function(entry) {
|
||||
let rank = 0;
|
||||
if (this.titleLower && entry.title) {
|
||||
rank += this.getStringRank(entry.title.toLowerCase(), this.titleLower);
|
||||
}
|
||||
if (this.urlParts && entry.url) {
|
||||
const entryUrlParts = urlPartsRegex.exec(entry.url.toLowerCase());
|
||||
if (entryUrlParts) {
|
||||
const [, scheme, domain, path] = entryUrlParts;
|
||||
const [, thisScheme, thisDomain, thisPath] = this.urlParts;
|
||||
if (domain === thisDomain) {
|
||||
rank += 10;
|
||||
if (path === thisPath) {
|
||||
rank += 10;
|
||||
} else if (path && thisPath) {
|
||||
if (path.lastIndexOf(thisPath, 0) === 0) {
|
||||
rank += 5;
|
||||
} else if (thisPath.lastIndexOf(path, 0) === 0) {
|
||||
rank += 3;
|
||||
}
|
||||
}
|
||||
if (scheme === thisScheme) {
|
||||
rank += 1;
|
||||
}
|
||||
} else {
|
||||
if (entry.searchText.indexOf(this.urlLower) >= 0) {
|
||||
// the url is in some field; include it
|
||||
rank += 5;
|
||||
} else {
|
||||
// another domain; don't show this record at all, ignore title match
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return rank;
|
||||
};
|
||||
|
||||
AutoTypeFilter.prototype.getStringRank = function(s1, s2) {
|
||||
let ix = s1.indexOf(s2);
|
||||
if (ix === 0 && s1.length === s2.length) {
|
||||
return 10;
|
||||
} else if (ix === 0) {
|
||||
return 5;
|
||||
} else if (ix > 0) {
|
||||
return 3;
|
||||
}
|
||||
ix = s2.indexOf(s1);
|
||||
if (ix === 0) {
|
||||
return 5;
|
||||
} else if (ix > 0) {
|
||||
return 3;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
module.exports = AutoTypeFilter;
|
||||
@@ -1,13 +0,0 @@
|
||||
const Launcher = require('../comp/launcher');
|
||||
|
||||
const AutoTypeHelperFactory = {
|
||||
create: function() {
|
||||
if (Launcher && Launcher.autoTypeSupported) {
|
||||
const AutoTypeHelper = require('./helper/auto-type-helper-' + Launcher.platform());
|
||||
return new AutoTypeHelper();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = AutoTypeHelperFactory;
|
||||
@@ -0,0 +1,16 @@
|
||||
import { NativeModules } from 'comp/launcher/native-modules';
|
||||
|
||||
class AutoTypeHelper {
|
||||
getActiveWindowInfo(callback) {
|
||||
NativeModules.kbdGetActiveWindow({
|
||||
getWindowTitle: true,
|
||||
getBrowserUrl: true
|
||||
})
|
||||
.then((win) => {
|
||||
callback(undefined, win);
|
||||
})
|
||||
.catch((err) => callback(err));
|
||||
}
|
||||
}
|
||||
|
||||
export { AutoTypeHelper };
|
||||
@@ -1,14 +1,13 @@
|
||||
const Logger = require('../util/logger');
|
||||
import { Logger } from 'util/logger';
|
||||
|
||||
const logger = new Logger('auto-type-obfuscator');
|
||||
logger.setLevel(localStorage.autoTypeDebug ? Logger.Level.All : Logger.Level.Warn);
|
||||
|
||||
const MaxFakeOps = 30;
|
||||
const MaxSteps = 1000;
|
||||
const MaxCopy = 2;
|
||||
const FakeCharAlphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz123456789O0oIl';
|
||||
|
||||
const AutoTypeObfuscator = function(chars) {
|
||||
const AutoTypeObfuscator = function (chars) {
|
||||
this.chars = chars;
|
||||
this.inputChars = [];
|
||||
this.inputCursor = 0;
|
||||
@@ -18,7 +17,7 @@ const AutoTypeObfuscator = function(chars) {
|
||||
this.copyCount = 0;
|
||||
};
|
||||
|
||||
AutoTypeObfuscator.prototype.obfuscate = function() {
|
||||
AutoTypeObfuscator.prototype.obfuscate = function () {
|
||||
while (!this.finished()) {
|
||||
this.step();
|
||||
if (this.stepCount++ > MaxSteps) {
|
||||
@@ -32,12 +31,16 @@ AutoTypeObfuscator.prototype.obfuscate = function() {
|
||||
return this.ops;
|
||||
};
|
||||
|
||||
AutoTypeObfuscator.prototype.finished = function() {
|
||||
return this.chars.length === this.inputChars.length &&
|
||||
this.chars.every(function(ch, ix) { return this.inputChars[ix].ch === ch; }, this);
|
||||
AutoTypeObfuscator.prototype.finished = function () {
|
||||
return (
|
||||
this.chars.length === this.inputChars.length &&
|
||||
this.chars.every(function (ch, ix) {
|
||||
return this.inputChars[ix].ch === ch;
|
||||
}, this)
|
||||
);
|
||||
};
|
||||
|
||||
AutoTypeObfuscator.prototype.step = function() {
|
||||
AutoTypeObfuscator.prototype.step = function () {
|
||||
const isFake = this.stepCount < MaxFakeOps && Math.random() > this.stepCount / MaxFakeOps;
|
||||
if (isFake) {
|
||||
this.stepFake();
|
||||
@@ -45,11 +48,11 @@ AutoTypeObfuscator.prototype.step = function() {
|
||||
this.stepReal();
|
||||
}
|
||||
if (logger.getLevel() >= Logger.Level.Debug) {
|
||||
logger.debug('value', this.inputChars.map(ic => ic.ch).join(''));
|
||||
logger.debug('value', this.inputChars.map((ic) => ic.ch).join(''));
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeObfuscator.prototype.stepFake = function() {
|
||||
AutoTypeObfuscator.prototype.stepFake = function () {
|
||||
const pos = Math.floor(Math.random() * (this.inputChars.length + 1));
|
||||
const ch = FakeCharAlphabet[Math.floor(Math.random() * FakeCharAlphabet.length)];
|
||||
logger.info('step.fake', pos, ch);
|
||||
@@ -78,7 +81,7 @@ AutoTypeObfuscator.prototype.stepFake = function() {
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeObfuscator.prototype.stepReal = function() {
|
||||
AutoTypeObfuscator.prototype.stepReal = function () {
|
||||
const possibleActions = [];
|
||||
const inputRealPositions = [];
|
||||
let i;
|
||||
@@ -106,7 +109,7 @@ AutoTypeObfuscator.prototype.stepReal = function() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
possibleActions.push({ ins: true, ch: this.chars[i], ix: i, from: from, to: to });
|
||||
possibleActions.push({ ins: true, ch: this.chars[i], ix: i, from, to });
|
||||
}
|
||||
}
|
||||
const action = possibleActions[Math.floor(Math.random() * possibleActions.length)];
|
||||
@@ -127,7 +130,7 @@ AutoTypeObfuscator.prototype.stepReal = function() {
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeObfuscator.prototype.moveToPos = function(pos) {
|
||||
AutoTypeObfuscator.prototype.moveToPos = function (pos) {
|
||||
logger.debug('moveToPos', pos);
|
||||
while (this.inputCursor > pos) {
|
||||
this.moveLeft();
|
||||
@@ -137,47 +140,47 @@ AutoTypeObfuscator.prototype.moveToPos = function(pos) {
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeObfuscator.prototype.moveLeft = function() {
|
||||
AutoTypeObfuscator.prototype.moveLeft = function () {
|
||||
logger.debug('moveLeft');
|
||||
this.ops.push({ type: 'key', value: 'left' });
|
||||
this.inputCursor--;
|
||||
this.inputSel = 0;
|
||||
};
|
||||
|
||||
AutoTypeObfuscator.prototype.moveRight = function() {
|
||||
AutoTypeObfuscator.prototype.moveRight = function () {
|
||||
logger.debug('moveRight');
|
||||
this.ops.push({ type: 'key', value: 'right' });
|
||||
this.inputCursor++;
|
||||
this.inputSel = 0;
|
||||
};
|
||||
|
||||
AutoTypeObfuscator.prototype.inputChar = function(ch) {
|
||||
AutoTypeObfuscator.prototype.inputChar = function (ch) {
|
||||
logger.debug('inputChar', ch);
|
||||
this.ops.push({ type: 'text', value: ch });
|
||||
this.inputChars.splice(this.inputCursor, this.inputSel, { ch: ch });
|
||||
this.inputChars.splice(this.inputCursor, this.inputSel, { ch });
|
||||
this.inputCursor++;
|
||||
this.inputSel = 0;
|
||||
};
|
||||
|
||||
AutoTypeObfuscator.prototype.copyPaste = function(ch) {
|
||||
AutoTypeObfuscator.prototype.copyPaste = function (ch) {
|
||||
logger.debug('copyPaste', ch);
|
||||
this.ops.push({type: 'cmd', value: 'copyPaste', arg: ch});
|
||||
this.inputChars.splice(this.inputCursor, this.inputSel, { ch: ch });
|
||||
this.ops.push({ type: 'cmd', value: 'copyPaste', arg: ch });
|
||||
this.inputChars.splice(this.inputCursor, this.inputSel, { ch });
|
||||
this.inputCursor++;
|
||||
this.inputSel = 0;
|
||||
};
|
||||
|
||||
AutoTypeObfuscator.prototype.selectText = function(backward, count) {
|
||||
AutoTypeObfuscator.prototype.selectText = function (backward, count) {
|
||||
logger.debug('selectText', backward ? 'left' : 'right', count);
|
||||
const ops = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
ops.push({ type: 'key', value: backward ? 'left' : 'right' });
|
||||
}
|
||||
if (ops.length === 1) {
|
||||
ops[0].mod = {'+': true};
|
||||
ops[0].mod = { '+': true };
|
||||
this.ops.push(ops[0]);
|
||||
} else {
|
||||
this.ops.push({type: 'group', value: ops, mod: {'+': true}});
|
||||
this.ops.push({ type: 'group', value: ops, mod: { '+': true } });
|
||||
}
|
||||
if (backward) {
|
||||
this.inputCursor -= count;
|
||||
@@ -185,7 +188,7 @@ AutoTypeObfuscator.prototype.selectText = function(backward, count) {
|
||||
this.inputSel = count;
|
||||
};
|
||||
|
||||
AutoTypeObfuscator.prototype.deleteText = function(backward) {
|
||||
AutoTypeObfuscator.prototype.deleteText = function (backward) {
|
||||
logger.debug('deleteText', backward ? 'left' : 'right');
|
||||
this.ops.push({ type: 'key', value: backward ? 'bs' : 'del' });
|
||||
if (this.inputSel) {
|
||||
@@ -199,4 +202,4 @@ AutoTypeObfuscator.prototype.deleteText = function(backward) {
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = AutoTypeObfuscator;
|
||||
export { AutoTypeObfuscator };
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const AutoTypeRunner = require('./auto-type-runner');
|
||||
import { AutoTypeRunner } from 'auto-type/auto-type-runner';
|
||||
|
||||
const AutoTypeParser = function(sequence) {
|
||||
const AutoTypeParser = function (sequence) {
|
||||
this.sequence = sequence;
|
||||
this.ix = 0;
|
||||
this.states = [];
|
||||
@@ -8,7 +8,7 @@ const AutoTypeParser = function(sequence) {
|
||||
|
||||
AutoTypeParser.opRegex = /^(.*?)(?:([\s:=])[\s:=]*(.*))?$/;
|
||||
|
||||
AutoTypeParser.prototype.parse = function() {
|
||||
AutoTypeParser.prototype.parse = function () {
|
||||
const len = this.sequence.length;
|
||||
this.pushState();
|
||||
while (this.ix < len) {
|
||||
@@ -45,14 +45,14 @@ AutoTypeParser.prototype.parse = function() {
|
||||
return new AutoTypeRunner(this.state().ops);
|
||||
};
|
||||
|
||||
AutoTypeParser.prototype.pushState = function() {
|
||||
AutoTypeParser.prototype.pushState = function () {
|
||||
this.states.unshift({
|
||||
modifiers: null,
|
||||
ops: []
|
||||
});
|
||||
};
|
||||
|
||||
AutoTypeParser.prototype.popState = function() {
|
||||
AutoTypeParser.prototype.popState = function () {
|
||||
if (this.states.length <= 1) {
|
||||
throw 'Unexpected ")" at index ' + this.ix;
|
||||
}
|
||||
@@ -60,11 +60,11 @@ AutoTypeParser.prototype.popState = function() {
|
||||
this.addState(state);
|
||||
};
|
||||
|
||||
AutoTypeParser.prototype.state = function() {
|
||||
AutoTypeParser.prototype.state = function () {
|
||||
return this.states[0];
|
||||
};
|
||||
|
||||
AutoTypeParser.prototype.readOp = function() {
|
||||
AutoTypeParser.prototype.readOp = function () {
|
||||
const toIx = this.sequence.indexOf('}', this.ix + 2);
|
||||
if (toIx < 0) {
|
||||
throw 'Mismatched "{" at index ' + this.ix;
|
||||
@@ -79,7 +79,7 @@ AutoTypeParser.prototype.readOp = function() {
|
||||
this.addOp(op, sep, arg);
|
||||
};
|
||||
|
||||
AutoTypeParser.prototype.readModifier = function(modifier) {
|
||||
AutoTypeParser.prototype.readModifier = function (modifier) {
|
||||
const state = this.state();
|
||||
if (!state.modifiers) {
|
||||
state.modifiers = {};
|
||||
@@ -91,14 +91,14 @@ AutoTypeParser.prototype.readModifier = function(modifier) {
|
||||
state.modifiers[modifier] = true;
|
||||
};
|
||||
|
||||
AutoTypeParser.prototype.resetModifiers = function() {
|
||||
AutoTypeParser.prototype.resetModifiers = function () {
|
||||
const state = this.state();
|
||||
const modifiers = state.modifiers;
|
||||
state.modifiers = null;
|
||||
return modifiers;
|
||||
};
|
||||
|
||||
AutoTypeParser.prototype.addState = function(state) {
|
||||
AutoTypeParser.prototype.addState = function (state) {
|
||||
this.state().ops.push({
|
||||
type: 'group',
|
||||
value: state.ops,
|
||||
@@ -106,7 +106,7 @@ AutoTypeParser.prototype.addState = function(state) {
|
||||
});
|
||||
};
|
||||
|
||||
AutoTypeParser.prototype.addChar = function(ch) {
|
||||
AutoTypeParser.prototype.addChar = function (ch) {
|
||||
this.state().ops.push({
|
||||
type: 'text',
|
||||
value: ch,
|
||||
@@ -114,14 +114,14 @@ AutoTypeParser.prototype.addChar = function(ch) {
|
||||
});
|
||||
};
|
||||
|
||||
AutoTypeParser.prototype.addOp = function(op, sep, arg) {
|
||||
AutoTypeParser.prototype.addOp = function (op, sep, arg) {
|
||||
this.state().ops.push({
|
||||
type: 'op',
|
||||
value: op,
|
||||
mod: this.resetModifiers(),
|
||||
sep: sep,
|
||||
arg: arg
|
||||
sep,
|
||||
arg
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = AutoTypeParser;
|
||||
export { AutoTypeParser };
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
const AutoTypeObfuscator = require('./auto-type-obfuscator');
|
||||
const AutoTypeEmitterFactory = require('./auto-type-emitter-factory');
|
||||
const Format = require('../util/format');
|
||||
const Logger = require('../util/logger');
|
||||
import { AutoTypeEmitter } from 'auto-type/auto-type-emitter';
|
||||
import { AutoTypeObfuscator } from 'auto-type/auto-type-obfuscator';
|
||||
import { StringFormat } from 'util/formatting/string-format';
|
||||
import { Logger } from 'util/logger';
|
||||
|
||||
const emitterLogger = new Logger('auto-type-emitter');
|
||||
emitterLogger.setLevel(localStorage.autoTypeDebug ? Logger.Level.All : Logger.Level.Warn);
|
||||
|
||||
const AutoTypeRunner = function(ops) {
|
||||
const AutoTypeRunner = function (ops) {
|
||||
this.ops = ops;
|
||||
this.pendingResolvesCount = 0;
|
||||
this.entry = null;
|
||||
@@ -16,43 +15,132 @@ const AutoTypeRunner = function(ops) {
|
||||
AutoTypeRunner.PendingResolve = { pending: true };
|
||||
|
||||
AutoTypeRunner.Keys = {
|
||||
tab: 'tab', enter: 'enter', space: 'space',
|
||||
up: 'up', down: 'down', left: 'left', right: 'right', home: 'home', end: 'end', pgup: 'pgup', pgdn: 'pgdn',
|
||||
insert: 'ins', ins: 'ins', delete: 'del', del: 'del', backspace: 'bs', bs: 'bs', bksp: 'bs', esc: 'esc',
|
||||
win: 'win', lwin: 'win', rwin: 'rwin', f1: 'f1', f2: 'f2', f3: 'f3', f4: 'f4', f5: 'f5', f6: 'f6',
|
||||
f7: 'f7', f8: 'f8', f9: 'f9', f10: 'f10', f11: 'f11', f12: 'f12', f13: 'f13', f14: 'f14', f15: 'f15', f16: 'f16',
|
||||
add: 'add', subtract: 'subtract', multiply: 'multiply', divide: 'divide',
|
||||
numpad0: 'n0', numpad1: 'n1', numpad2: 'n2', numpad3: 'n3', numpad4: 'n4',
|
||||
numpad5: 'n5', numpad6: 'n6', numpad7: 'n7', numpad8: 'n8', numpad9: 'n9'
|
||||
tab: 'tab',
|
||||
enter: 'enter',
|
||||
space: 'space',
|
||||
up: 'up',
|
||||
down: 'down',
|
||||
left: 'left',
|
||||
right: 'right',
|
||||
home: 'home',
|
||||
end: 'end',
|
||||
pgup: 'pgup',
|
||||
pgdn: 'pgdn',
|
||||
insert: 'ins',
|
||||
ins: 'ins',
|
||||
delete: 'del',
|
||||
del: 'del',
|
||||
backspace: 'bs',
|
||||
bs: 'bs',
|
||||
bksp: 'bs',
|
||||
esc: 'esc',
|
||||
win: 'win',
|
||||
lwin: 'win',
|
||||
rwin: 'rwin',
|
||||
f1: 'f1',
|
||||
f2: 'f2',
|
||||
f3: 'f3',
|
||||
f4: 'f4',
|
||||
f5: 'f5',
|
||||
f6: 'f6',
|
||||
f7: 'f7',
|
||||
f8: 'f8',
|
||||
f9: 'f9',
|
||||
f10: 'f10',
|
||||
f11: 'f11',
|
||||
f12: 'f12',
|
||||
f13: 'f13',
|
||||
f14: 'f14',
|
||||
f15: 'f15',
|
||||
f16: 'f16',
|
||||
add: 'add',
|
||||
subtract: 'subtract',
|
||||
multiply: 'multiply',
|
||||
divide: 'divide',
|
||||
numpad0: 'n0',
|
||||
numpad1: 'n1',
|
||||
numpad2: 'n2',
|
||||
numpad3: 'n3',
|
||||
numpad4: 'n4',
|
||||
numpad5: 'n5',
|
||||
numpad6: 'n6',
|
||||
numpad7: 'n7',
|
||||
numpad8: 'n8',
|
||||
numpad9: 'n9'
|
||||
};
|
||||
|
||||
AutoTypeRunner.Substitutions = {
|
||||
title: function(runner, op) { return runner.getEntryFieldKeys('Title', op); },
|
||||
username: function(runner, op) { return runner.getEntryFieldKeys('UserName', op); },
|
||||
url: function(runner, op) { return runner.getEntryFieldKeys('URL', op); },
|
||||
password: function(runner, op) { return runner.getEntryFieldKeys('Password', op); },
|
||||
notes: function(runner, op) { return runner.getEntryFieldKeys('Notes', op); },
|
||||
group: function(runner) { return runner.getEntryGroupName(); },
|
||||
totp: function(runner, op) { return runner.getOtp(op); },
|
||||
s: function(runner, op) { return runner.getEntryFieldKeys(op.arg, op); },
|
||||
'dt_simple': function(runner) { return runner.dt('simple'); },
|
||||
'dt_year': function(runner) { return runner.dt('Y'); },
|
||||
'dt_month': function(runner) { return runner.dt('M'); },
|
||||
'dt_day': function(runner) { return runner.dt('D'); },
|
||||
'dt_hour': function(runner) { return runner.dt('h'); },
|
||||
'dt_minute': function(runner) { return runner.dt('m'); },
|
||||
'dt_second': function(runner) { return runner.dt('s'); },
|
||||
'dt_utc_simple': function(runner) { return runner.udt('simple'); },
|
||||
'dt_utc_year': function(runner) { return runner.udt('Y'); },
|
||||
'dt_utc_month': function(runner) { return runner.udt('M'); },
|
||||
'dt_utc_day': function(runner) { return runner.udt('D'); },
|
||||
'dt_utc_hour': function(runner) { return runner.udt('h'); },
|
||||
'dt_utc_minute': function(runner) { return runner.udt('m'); },
|
||||
'dt_utc_second': function(runner) { return runner.udt('s'); }
|
||||
title(runner, op) {
|
||||
return runner.getEntryFieldKeys('Title', op);
|
||||
},
|
||||
username(runner, op) {
|
||||
return runner.getEntryFieldKeys('UserName', op);
|
||||
},
|
||||
url(runner, op) {
|
||||
return runner.getEntryFieldKeys('URL', op);
|
||||
},
|
||||
password(runner, op) {
|
||||
return runner.getEntryFieldKeys('Password', op);
|
||||
},
|
||||
notes(runner, op) {
|
||||
return runner.getEntryFieldKeys('Notes', op);
|
||||
},
|
||||
group(runner) {
|
||||
return runner.getEntryGroupName();
|
||||
},
|
||||
totp(runner, op) {
|
||||
return runner.getOtp(op);
|
||||
},
|
||||
s(runner, op) {
|
||||
return runner.getEntryFieldKeys(op.arg, op);
|
||||
},
|
||||
'dt_simple'(runner) {
|
||||
return runner.dt('simple');
|
||||
},
|
||||
'dt_year'(runner) {
|
||||
return runner.dt('Y');
|
||||
},
|
||||
'dt_month'(runner) {
|
||||
return runner.dt('M');
|
||||
},
|
||||
'dt_day'(runner) {
|
||||
return runner.dt('D');
|
||||
},
|
||||
'dt_hour'(runner) {
|
||||
return runner.dt('h');
|
||||
},
|
||||
'dt_minute'(runner) {
|
||||
return runner.dt('m');
|
||||
},
|
||||
'dt_second'(runner) {
|
||||
return runner.dt('s');
|
||||
},
|
||||
'dt_utc_simple'(runner) {
|
||||
return runner.udt('simple');
|
||||
},
|
||||
'dt_utc_year'(runner) {
|
||||
return runner.udt('Y');
|
||||
},
|
||||
'dt_utc_month'(runner) {
|
||||
return runner.udt('M');
|
||||
},
|
||||
'dt_utc_day'(runner) {
|
||||
return runner.udt('D');
|
||||
},
|
||||
'dt_utc_hour'(runner) {
|
||||
return runner.udt('h');
|
||||
},
|
||||
'dt_utc_minute'(runner) {
|
||||
return runner.udt('m');
|
||||
},
|
||||
'dt_utc_second'(runner) {
|
||||
return runner.udt('s');
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.resolve = function(entry, callback) {
|
||||
AutoTypeRunner.prototype.resolve = function (entry, context, callback) {
|
||||
this.entry = entry;
|
||||
this.context = context;
|
||||
try {
|
||||
this.resolveOps(this.ops);
|
||||
if (!this.pendingResolvesCount) {
|
||||
@@ -65,7 +153,7 @@ AutoTypeRunner.prototype.resolve = function(entry, callback) {
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.resolveOps = function(ops) {
|
||||
AutoTypeRunner.prototype.resolveOps = function (ops) {
|
||||
for (let i = 0, len = ops.length; i < len; i++) {
|
||||
const op = ops[i];
|
||||
if (op.type === 'group') {
|
||||
@@ -76,7 +164,7 @@ AutoTypeRunner.prototype.resolveOps = function(ops) {
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.resolveOp = function(op) {
|
||||
AutoTypeRunner.prototype.resolveOp = function (op) {
|
||||
if (op.value.length === 1 && !op.sep) {
|
||||
// {x}
|
||||
op.type = 'text';
|
||||
@@ -103,7 +191,7 @@ AutoTypeRunner.prototype.resolveOp = function(op) {
|
||||
op.value = [];
|
||||
const count = +op.arg;
|
||||
for (let i = 0; i < count; i++) {
|
||||
op.value.push({type: 'key', value: key});
|
||||
op.value.push({ type: 'key', value: key });
|
||||
}
|
||||
} else {
|
||||
// {TAB}
|
||||
@@ -112,6 +200,11 @@ AutoTypeRunner.prototype.resolveOp = function(op) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (this.context?.resolved?.[lowerValue]) {
|
||||
op.type = 'text';
|
||||
op.value = this.context.resolved[lowerValue];
|
||||
return;
|
||||
}
|
||||
const substitution = AutoTypeRunner.Substitutions[lowerValue];
|
||||
if (substitution) {
|
||||
// {title}
|
||||
@@ -127,7 +220,7 @@ AutoTypeRunner.prototype.resolveOp = function(op) {
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.tryParseCommand = function(op) {
|
||||
AutoTypeRunner.prototype.tryParseCommand = function (op) {
|
||||
switch (op.value.toLowerCase()) {
|
||||
case 'clearfield':
|
||||
// {CLEARFIELD}
|
||||
@@ -142,7 +235,7 @@ AutoTypeRunner.prototype.tryParseCommand = function(op) {
|
||||
// {VKEY 10} {VKEY 0x1F}
|
||||
op.type = 'key';
|
||||
op.value = parseInt(op.arg);
|
||||
if (isNaN(op.value) || op.value <= 0) {
|
||||
if (isNaN(op.value) || op.value < 0) {
|
||||
throw 'Bad vkey: ' + op.arg;
|
||||
}
|
||||
return true;
|
||||
@@ -166,7 +259,7 @@ AutoTypeRunner.prototype.tryParseCommand = function(op) {
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.getEntryFieldKeys = function(field, op) {
|
||||
AutoTypeRunner.prototype.getEntryFieldKeys = function (field, op) {
|
||||
if (!field || !this.entry) {
|
||||
return '';
|
||||
}
|
||||
@@ -177,11 +270,11 @@ AutoTypeRunner.prototype.getEntryFieldKeys = function(field, op) {
|
||||
if (value.isProtected) {
|
||||
op.type = 'group';
|
||||
const ops = [];
|
||||
value.forEachChar(ch => {
|
||||
value.forEachChar((ch) => {
|
||||
if (ch === 10 || ch === 13) {
|
||||
ops.push({type: 'key', value: 'enter'});
|
||||
ops.push({ type: 'key', value: 'enter' });
|
||||
} else {
|
||||
ops.push({type: 'text', value: String.fromCharCode(ch)});
|
||||
ops.push({ type: 'text', value: String.fromCharCode(ch) });
|
||||
}
|
||||
});
|
||||
return ops;
|
||||
@@ -192,65 +285,79 @@ AutoTypeRunner.prototype.getEntryFieldKeys = function(field, op) {
|
||||
}
|
||||
op.type = 'group';
|
||||
const partsOps = [];
|
||||
parts.forEach(part => {
|
||||
parts.forEach((part) => {
|
||||
if (partsOps.length) {
|
||||
partsOps.push({type: 'key', value: 'enter'});
|
||||
partsOps.push({ type: 'key', value: 'enter' });
|
||||
}
|
||||
if (part) {
|
||||
partsOps.push({type: 'text', value: part});
|
||||
partsOps.push({ type: 'text', value: part });
|
||||
}
|
||||
});
|
||||
return partsOps;
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.getEntryGroupName = function() {
|
||||
return this.entry && this.entry.group.get('title');
|
||||
AutoTypeRunner.prototype.getEntryGroupName = function () {
|
||||
return this.entry && this.entry.group.title;
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.dt = function(part) {
|
||||
AutoTypeRunner.prototype.dt = function (part) {
|
||||
switch (part) {
|
||||
case 'simple':
|
||||
return this.dt('Y') + this.dt('M') + this.dt('D') + this.dt('h') + this.dt('m') + this.dt('s');
|
||||
return (
|
||||
this.dt('Y') +
|
||||
this.dt('M') +
|
||||
this.dt('D') +
|
||||
this.dt('h') +
|
||||
this.dt('m') +
|
||||
this.dt('s')
|
||||
);
|
||||
case 'Y':
|
||||
return this.now.getFullYear().toString();
|
||||
case 'M':
|
||||
return Format.pad(this.now.getMonth() + 1, 2);
|
||||
return StringFormat.pad(this.now.getMonth() + 1, 2);
|
||||
case 'D':
|
||||
return Format.pad(this.now.getDate(), 2);
|
||||
return StringFormat.pad(this.now.getDate(), 2);
|
||||
case 'h':
|
||||
return Format.pad(this.now.getHours(), 2);
|
||||
return StringFormat.pad(this.now.getHours(), 2);
|
||||
case 'm':
|
||||
return Format.pad(this.now.getMinutes(), 2);
|
||||
return StringFormat.pad(this.now.getMinutes(), 2);
|
||||
case 's':
|
||||
return Format.pad(this.now.getSeconds(), 2);
|
||||
return StringFormat.pad(this.now.getSeconds(), 2);
|
||||
default:
|
||||
throw 'Bad part: ' + part;
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.udt = function(part) {
|
||||
AutoTypeRunner.prototype.udt = function (part) {
|
||||
switch (part) {
|
||||
case 'simple':
|
||||
return this.udt('Y') + this.udt('M') + this.udt('D') + this.udt('h') + this.udt('m') + this.udt('s');
|
||||
return (
|
||||
this.udt('Y') +
|
||||
this.udt('M') +
|
||||
this.udt('D') +
|
||||
this.udt('h') +
|
||||
this.udt('m') +
|
||||
this.udt('s')
|
||||
);
|
||||
case 'Y':
|
||||
return this.now.getUTCFullYear().toString();
|
||||
case 'M':
|
||||
return Format.pad(this.now.getUTCMonth() + 1, 2);
|
||||
return StringFormat.pad(this.now.getUTCMonth() + 1, 2);
|
||||
case 'D':
|
||||
return Format.pad(this.now.getUTCDate(), 2);
|
||||
return StringFormat.pad(this.now.getUTCDate(), 2);
|
||||
case 'h':
|
||||
return Format.pad(this.now.getUTCHours(), 2);
|
||||
return StringFormat.pad(this.now.getUTCHours(), 2);
|
||||
case 'm':
|
||||
return Format.pad(this.now.getUTCMinutes(), 2);
|
||||
return StringFormat.pad(this.now.getUTCMinutes(), 2);
|
||||
case 's':
|
||||
return Format.pad(this.now.getUTCSeconds(), 2);
|
||||
return StringFormat.pad(this.now.getUTCSeconds(), 2);
|
||||
default:
|
||||
throw 'Bad part: ' + part;
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.getOtp = function(op) {
|
||||
AutoTypeRunner.prototype.getOtp = function (op) {
|
||||
if (!this.entry) {
|
||||
return '';
|
||||
}
|
||||
@@ -258,13 +365,13 @@ AutoTypeRunner.prototype.getOtp = function(op) {
|
||||
if (!this.entry.otpGenerator) {
|
||||
return '';
|
||||
}
|
||||
this.entry.otpGenerator.next(otp => {
|
||||
this.pendingResolved(op, otp, otp ? undefined : 'OTP error');
|
||||
this.entry.otpGenerator.next((err, otp) => {
|
||||
this.pendingResolved(op, otp, err);
|
||||
});
|
||||
return AutoTypeRunner.PendingResolve;
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.pendingResolved = function(op, value, error) {
|
||||
AutoTypeRunner.prototype.pendingResolved = function (op, value, error) {
|
||||
const wasPending = op.value === AutoTypeRunner.PendingResolve;
|
||||
if (value) {
|
||||
op.value = value;
|
||||
@@ -279,11 +386,11 @@ AutoTypeRunner.prototype.pendingResolved = function(op, value, error) {
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.obfuscate = function() {
|
||||
AutoTypeRunner.prototype.obfuscate = function () {
|
||||
this.obfuscateOps(this.ops);
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.obfuscateOps = function(ops) {
|
||||
AutoTypeRunner.prototype.obfuscateOps = function (ops) {
|
||||
for (let i = 0, len = ops.length; i < len; i++) {
|
||||
const op = ops[i];
|
||||
if (op.mod) {
|
||||
@@ -292,7 +399,7 @@ AutoTypeRunner.prototype.obfuscateOps = function(ops) {
|
||||
if (op.type === 'text') {
|
||||
this.obfuscateOp(op);
|
||||
} else if (op.type === 'group') {
|
||||
const onlyText = op.value.every(grOp => grOp.type === 'text' && !grOp.mod);
|
||||
const onlyText = op.value.every((grOp) => grOp.type === 'text' && !grOp.mod);
|
||||
if (onlyText) {
|
||||
this.obfuscateOp(op);
|
||||
} else {
|
||||
@@ -302,7 +409,7 @@ AutoTypeRunner.prototype.obfuscateOps = function(ops) {
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.obfuscateOp = function(op) {
|
||||
AutoTypeRunner.prototype.obfuscateOp = function (op) {
|
||||
let letters = [];
|
||||
if (op.type === 'text') {
|
||||
if (!op.value || op.value.length <= 1) {
|
||||
@@ -310,7 +417,7 @@ AutoTypeRunner.prototype.obfuscateOp = function(op) {
|
||||
}
|
||||
letters = op.value.split('');
|
||||
} else {
|
||||
op.value.forEach(grOp => letters.push.apply(letters, grOp.value.split('')));
|
||||
op.value.forEach((grOp) => letters.push(...grOp.value.split('')));
|
||||
}
|
||||
if (letters.length <= 1) {
|
||||
return;
|
||||
@@ -320,10 +427,10 @@ AutoTypeRunner.prototype.obfuscateOp = function(op) {
|
||||
op.type = 'group';
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.run = function(callback) {
|
||||
this.emitter = AutoTypeEmitterFactory.create(this.emitNext.bind(this));
|
||||
AutoTypeRunner.prototype.run = function (callback, windowId) {
|
||||
this.emitter = new AutoTypeEmitter(this.emitNext.bind(this), windowId);
|
||||
this.emitterState = {
|
||||
callback: callback,
|
||||
callback,
|
||||
stack: [],
|
||||
ops: this.ops,
|
||||
opIx: 0,
|
||||
@@ -331,10 +438,10 @@ AutoTypeRunner.prototype.run = function(callback) {
|
||||
activeMod: {},
|
||||
finished: null
|
||||
};
|
||||
this.emitNext();
|
||||
this.emitter.begin();
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.emitNext = function(err) {
|
||||
AutoTypeRunner.prototype.emitNext = function (err) {
|
||||
if (err) {
|
||||
this.emitterState.finished = true;
|
||||
this.emitterState.callback(err);
|
||||
@@ -348,7 +455,7 @@ AutoTypeRunner.prototype.emitNext = function(err) {
|
||||
if (this.emitterState.opIx >= this.emitterState.ops.length) {
|
||||
const state = this.emitterState.stack.pop();
|
||||
if (state) {
|
||||
_.extend(this.emitterState, { ops: state.ops, opIx: state.opIx, mod: state.mod });
|
||||
Object.assign(this.emitterState, { ops: state.ops, opIx: state.opIx, mod: state.mod });
|
||||
this.emitNext();
|
||||
} else {
|
||||
this.resetEmitterMod({});
|
||||
@@ -366,12 +473,12 @@ AutoTypeRunner.prototype.emitNext = function(err) {
|
||||
this.emitterState.stack.push({
|
||||
ops: this.emitterState.ops,
|
||||
opIx: this.emitterState.opIx + 1,
|
||||
mod: _.clone(this.emitterState.mod)
|
||||
mod: { ...this.emitterState.mod }
|
||||
});
|
||||
_.extend(this.emitterState, {
|
||||
Object.assign(this.emitterState, {
|
||||
ops: op.value,
|
||||
opIx: 0,
|
||||
mod: _.clone(this.emitterState.activeMod)
|
||||
mod: { ...this.emitterState.activeMod }
|
||||
});
|
||||
this.emitNext();
|
||||
return;
|
||||
@@ -393,7 +500,7 @@ AutoTypeRunner.prototype.emitNext = function(err) {
|
||||
emitterLogger.debug('key', op.value);
|
||||
this.emitter.key(op.value);
|
||||
break;
|
||||
case 'cmd':
|
||||
case 'cmd': {
|
||||
const method = this.emitter[op.value];
|
||||
if (!method) {
|
||||
throw 'Bad cmd: ' + op.value;
|
||||
@@ -401,13 +508,14 @@ AutoTypeRunner.prototype.emitNext = function(err) {
|
||||
emitterLogger.debug(op.value, op.arg);
|
||||
method.call(this.emitter, op.arg);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw 'Bad op: ' + op.type;
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.setEmitterMod = function(addedMod) {
|
||||
Object.keys(addedMod).forEach(function(mod) {
|
||||
AutoTypeRunner.prototype.setEmitterMod = function (addedMod) {
|
||||
Object.keys(addedMod).forEach(function (mod) {
|
||||
if (addedMod[mod] && !this.emitterState.activeMod[mod]) {
|
||||
emitterLogger.debug('mod', mod, true);
|
||||
this.emitter.setMod(mod, true);
|
||||
@@ -416,8 +524,8 @@ AutoTypeRunner.prototype.setEmitterMod = function(addedMod) {
|
||||
}, this);
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.resetEmitterMod = function(targetState) {
|
||||
Object.keys(this.emitterState.activeMod).forEach(function(mod) {
|
||||
AutoTypeRunner.prototype.resetEmitterMod = function (targetState) {
|
||||
Object.keys(this.emitterState.activeMod).forEach(function (mod) {
|
||||
if (this.emitterState.activeMod[mod] && !targetState[mod]) {
|
||||
emitterLogger.debug('mod', mod, false);
|
||||
this.emitter.setMod(mod, false);
|
||||
@@ -426,4 +534,4 @@ AutoTypeRunner.prototype.resetEmitterMod = function(targetState) {
|
||||
}, this);
|
||||
};
|
||||
|
||||
module.exports = AutoTypeRunner;
|
||||
export { AutoTypeRunner };
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
const Launcher = require('../../comp/launcher');
|
||||
const AutoTypeNativeHelper = require('../helper/auto-type-native-helper');
|
||||
|
||||
// http://eastmanreference.com/complete-list-of-applescript-key-codes/
|
||||
const KeyMap = {
|
||||
tab: 48, enter: 36, space: 49,
|
||||
up: 126, down: 125, left: 123, right: 124, home: 115, end: 119, pgup: 116, pgdn: 121,
|
||||
ins: 114, del: 117, bs: 51, esc: 53,
|
||||
win: 55, rwin: 55,
|
||||
f1: 122, f2: 120, f3: 99, f4: 118, f5: 96, f6: 97, f7: 98, f8: 100, f9: 101,
|
||||
f10: 109, f11: 103, f12: 111, f13: 105, f14: 107, f15: 113, f16: 106,
|
||||
add: 69, subtract: 78, multiply: 67, divide: 75,
|
||||
n0: 82, n1: 83, n2: 84, n3: 85, n4: 86,
|
||||
n5: 87, n6: 88, n7: 89, n8: 91, n9: 92
|
||||
};
|
||||
|
||||
const ModMap = {
|
||||
'^': '@',
|
||||
'+': '+',
|
||||
'%': '%',
|
||||
'^^': '^'
|
||||
};
|
||||
|
||||
const AutoTypeEmitter = function(callback) {
|
||||
this.callback = callback;
|
||||
this.mod = {};
|
||||
this.pendingScript = [];
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.setMod = function(mod, enabled) {
|
||||
if (enabled) {
|
||||
this.mod[ModMap[mod]] = true;
|
||||
} else {
|
||||
delete this.mod[ModMap[mod]];
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.text = function(text) {
|
||||
this.pendingScript.push('text ' + this.modString() + ' ' + text);
|
||||
this.callback();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.key = function(key) {
|
||||
if (typeof key !== 'number') {
|
||||
if (!KeyMap[key]) {
|
||||
return this.callback('Bad key: ' + key);
|
||||
}
|
||||
key = KeyMap[key];
|
||||
}
|
||||
this.pendingScript.push('key ' + this.modString() + key);
|
||||
this.callback();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.copyPaste = function(text) {
|
||||
this.pendingScript.push('copypaste ' + text);
|
||||
this.callback();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.wait = function(time) {
|
||||
this.pendingScript.push('wait ' + time);
|
||||
this.callback();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.waitComplete = function() {
|
||||
if (this.pendingScript.length) {
|
||||
const script = this.pendingScript.join('\n');
|
||||
this.pendingScript.length = 0;
|
||||
this.runScript(script);
|
||||
} else {
|
||||
this.callback();
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.setDelay = function(delay) {
|
||||
this.delay = delay || 0;
|
||||
this.callback('Not implemented');
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.modString = function() {
|
||||
return Object.keys(this.mod).join('');
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.runScript = function(script) {
|
||||
Launcher.spawn({
|
||||
cmd: AutoTypeNativeHelper.getHelperPath(),
|
||||
data: script,
|
||||
complete: this.callback
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = AutoTypeEmitter;
|
||||
@@ -1,109 +0,0 @@
|
||||
const Launcher = require('../../comp/launcher');
|
||||
const Locale = require('../../util/locale');
|
||||
|
||||
// https://cgit.freedesktop.org/xorg/proto/x11proto/plain/keysymdef.h
|
||||
const KeyMap = {
|
||||
tab: 'Tab', enter: 'KP_Enter', space: 'KP_Space',
|
||||
up: 'Up', down: 'Down', left: 'Left', right: 'Right', home: 'Home', end: 'End', pgup: 'Page_Up', pgdn: 'Page_Down',
|
||||
ins: 'Insert', del: 'Delete', bs: 'BackSpace', esc: 'Escape',
|
||||
win: 'Meta_L', rwin: 'Meta_R',
|
||||
f1: 'F1', f2: 'F2', f3: 'F3', f4: 'F4', f5: 'F5', f6: 'F6', f7: 'F7', f8: 'F8', f9: 'F9',
|
||||
f10: 'F10', f11: 'F11', f12: 'F12', f13: 'F13', f14: 'F14', f15: 'F15', f16: 'F16',
|
||||
add: 'KP_Add', subtract: 'KP_Subtract', multiply: 'KP_Multiply', divide: 'KP_Divide',
|
||||
n0: 'KP_0', n1: 'KP_1', n2: 'KP_2', n3: 'KP_3', n4: 'KP_4',
|
||||
n5: 'KP_5', n6: 'KP_6', n7: 'KP_7', n8: 'KP_8', n9: 'KP_9'
|
||||
};
|
||||
|
||||
const ModMap = {
|
||||
'^': 'ctrl',
|
||||
'+': 'shift',
|
||||
'%': 'alt',
|
||||
'^^': 'ctrl'
|
||||
};
|
||||
|
||||
const AutoTypeEmitter = function(callback) {
|
||||
this.callback = callback;
|
||||
this.mod = {};
|
||||
this.pendingScript = [];
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.setMod = function(mod, enabled) {
|
||||
if (enabled) {
|
||||
this.mod[ModMap[mod]] = true;
|
||||
} else {
|
||||
delete this.mod[ModMap[mod]];
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.text = function(text) {
|
||||
this.pendingScript.push('keyup ctrl alt shift t');
|
||||
Object.keys(this.mod).forEach(mod => {
|
||||
this.pendingScript.push('keydown ' + ModMap[mod]);
|
||||
});
|
||||
text.split('').map(char => {
|
||||
this.pendingScript.push('key U' + char.charCodeAt(0).toString(16));
|
||||
});
|
||||
Object.keys(this.mod).forEach(mod => {
|
||||
this.pendingScript.push('keyup ' + ModMap[mod]);
|
||||
});
|
||||
this.waitComplete();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.key = function(key) {
|
||||
if (typeof key !== 'number') {
|
||||
if (!KeyMap[key]) {
|
||||
return this.callback('Bad key: ' + key);
|
||||
}
|
||||
key = KeyMap[key].toString(16);
|
||||
}
|
||||
this.pendingScript.push('key --clearmodifiers ' + this.modString() + key);
|
||||
this.callback();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.copyPaste = function(text) {
|
||||
this.pendingScript.push('sleep 0.5');
|
||||
Launcher.setClipboardText(text);
|
||||
this.pendingScript.push('key --clearmodifiers shift+Insert');
|
||||
this.pendingScript.push('sleep 0.5');
|
||||
this.waitComplete();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.wait = function(time) {
|
||||
this.pendingScript.push('sleep ' + (time / 1000));
|
||||
this.callback();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.waitComplete = function(callback) {
|
||||
if (this.pendingScript.length) {
|
||||
const script = this.pendingScript.join(' ');
|
||||
this.pendingScript.length = 0;
|
||||
this.runScript(script, callback);
|
||||
} else {
|
||||
this.callback();
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.modString = function() {
|
||||
let mod = '';
|
||||
Object.keys(this.mod).forEach(key => {
|
||||
mod += key + '+';
|
||||
});
|
||||
return mod;
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.runScript = function(script, callback) {
|
||||
Launcher.spawn({
|
||||
cmd: 'xdotool',
|
||||
args: ['-'],
|
||||
data: script,
|
||||
complete: (err, stdout, code) => {
|
||||
if (err && err.code === 'ENOENT') {
|
||||
err = Locale.autoTypeErrorNotInstalled.replace('{}', 'xdotool');
|
||||
}
|
||||
const cb = callback || this.callback;
|
||||
cb(err, stdout, code);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = AutoTypeEmitter;
|
||||
@@ -1,93 +0,0 @@
|
||||
const Launcher = require('../../comp/launcher');
|
||||
const AutoTypeNativeHelper = require('../helper/auto-type-native-helper');
|
||||
|
||||
// https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx
|
||||
const KeyMap = {
|
||||
tab: 0x09, enter: 0x0D, space: 0x20,
|
||||
up: 0x26, down: 0x28, left: 0x25, right: 0x27, home: 0x24, end: 0x23, pgup: 0x21, pgdn: 0x22,
|
||||
ins: 0x2D, del: 0x2E, bs: 0x08, esc: 0x1B,
|
||||
win: 0x5B, rwin: 0x5C,
|
||||
f1: 0x70, f2: 0x71, f3: 0x72, f4: 0x73, f5: 0x74, f6: 0x75, f7: 0x76, f8: 0x77, f9: 0x78,
|
||||
f10: 0x79, f11: 0x7A, f12: 0x7B, f13: 0x7C, f14: 0x7D, f15: 0x7E, f16: 0x7F,
|
||||
add: 0x6B, subtract: 0x6D, multiply: 0x6A, divide: 0x6F,
|
||||
n0: 0x30, n1: 0x31, n2: 0x32, n3: 0x33, n4: 0x34,
|
||||
n5: 0x35, n6: 0x36, n7: 0x37, n8: 0x38, n9: 0x39
|
||||
};
|
||||
|
||||
const ModMap = {
|
||||
'^': '^',
|
||||
'+': '+',
|
||||
'%': '%',
|
||||
'^^': '^'
|
||||
};
|
||||
|
||||
const AutoTypeEmitter = function(callback) {
|
||||
this.callback = callback;
|
||||
this.mod = {};
|
||||
this.pendingScript = [];
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.setMod = function(mod, enabled) {
|
||||
if (enabled) {
|
||||
this.mod[ModMap[mod]] = true;
|
||||
} else {
|
||||
delete this.mod[ModMap[mod]];
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.text = function(text) {
|
||||
if (text) {
|
||||
this.pendingScript.push('text ' + this.modStr() + ' ' + text);
|
||||
}
|
||||
this.callback();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.key = function(key) {
|
||||
if (typeof key !== 'number') {
|
||||
if (!KeyMap[key]) {
|
||||
return this.callback('Bad key: ' + key);
|
||||
}
|
||||
key = KeyMap[key];
|
||||
}
|
||||
this.pendingScript.push('key ' + this.modStr() + key);
|
||||
this.callback();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.copyPaste = function(text) {
|
||||
this.pendingScript.push('copypaste ' + text);
|
||||
this.callback();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.wait = function(time) {
|
||||
this.pendingScript.push('wait ' + time);
|
||||
this.callback();
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.waitComplete = function() {
|
||||
if (this.pendingScript.length) {
|
||||
const script = this.pendingScript.join('\n');
|
||||
this.pendingScript.length = 0;
|
||||
this.runScript(script);
|
||||
} else {
|
||||
this.callback();
|
||||
}
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.setDelay = function(delay) {
|
||||
this.delay = delay || 0;
|
||||
this.callback('Not implemented');
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.modStr = function() {
|
||||
return Object.keys(this.mod).join('');
|
||||
};
|
||||
|
||||
AutoTypeEmitter.prototype.runScript = function(script) {
|
||||
Launcher.spawn({
|
||||
cmd: AutoTypeNativeHelper.getHelperPath(),
|
||||
data: script,
|
||||
complete: this.callback
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = AutoTypeEmitter;
|
||||
@@ -1,59 +0,0 @@
|
||||
const Launcher = require('../../comp/launcher');
|
||||
|
||||
const ForeMostAppScript = 'tell application "System Events" to set frontApp to name of first process whose frontmost is true';
|
||||
const ChromeScript = 'tell application "{}" to set appUrl to URL of active tab of front window\n' +
|
||||
'tell application "{}" to set appTitle to title of active tab of front window\n' +
|
||||
'return appUrl & "\n" & appTitle';
|
||||
const SafariScript = 'tell application "{}" to set appUrl to URL of front document\n' +
|
||||
'tell application "{}" to set appTitle to name of front document\n' +
|
||||
'return appUrl & "\n" & appTitle';
|
||||
const OtherAppsScript = 'tell application "System Events"\n' +
|
||||
' tell process "{}"\n' +
|
||||
' tell (1st window whose value of attribute "AXMain" is true)\n' +
|
||||
' set windowTitle to value of attribute "AXTitle"\n' +
|
||||
' end tell\n' +
|
||||
' end tell\n' +
|
||||
'end tell';
|
||||
|
||||
const AutoTypeHelper = function() {
|
||||
};
|
||||
|
||||
AutoTypeHelper.prototype.getActiveWindowTitle = function(callback) {
|
||||
AutoTypeHelper.exec(ForeMostAppScript, (err, out) => {
|
||||
if (err) { return callback(err); }
|
||||
const appName = out.trim();
|
||||
// getting urls and titles from Chrome or Safari:
|
||||
// - will suit in 90% cases
|
||||
// - does not require assistive access
|
||||
// - allows to get url
|
||||
if (['Google Chrome', 'Chromium', 'Google Chrome Canary'].indexOf(appName) >= 0) {
|
||||
AutoTypeHelper.exec(ChromeScript.replace(/\{}/g, appName), (err, out) => {
|
||||
if (err) { return callback(err); }
|
||||
const parts = out.split('\n');
|
||||
return callback(null, (parts[1] || '').trim(), parts[0].trim());
|
||||
});
|
||||
} else if (['Safari', 'Webkit'].indexOf(appName) >= 0) {
|
||||
AutoTypeHelper.exec(SafariScript.replace(/\{}/g, appName), (err, out) => {
|
||||
if (err) { return callback(err); }
|
||||
const parts = out.split('\n');
|
||||
return callback(null, (parts[1] || '').trim(), parts[0].trim());
|
||||
});
|
||||
} else {
|
||||
// special cases are not available. this method may ask the user about assistive access
|
||||
AutoTypeHelper.exec(OtherAppsScript.replace(/\{}/g, appName), (err, out) => {
|
||||
if (err) { return callback(err); }
|
||||
return callback(null, out.trim());
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
AutoTypeHelper.exec = function(script, callback) {
|
||||
Launcher.spawn({
|
||||
cmd: 'osascript',
|
||||
args: ['-e', script],
|
||||
complete: callback
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = AutoTypeHelper;
|
||||
@@ -1,16 +0,0 @@
|
||||
const Launcher = require('../../comp/launcher');
|
||||
|
||||
const AutoTypeHelper = function() {
|
||||
};
|
||||
|
||||
AutoTypeHelper.prototype.getActiveWindowTitle = function(callback) {
|
||||
Launcher.spawn({
|
||||
cmd: 'xdotool',
|
||||
args: ['getactivewindow', 'getwindowname'],
|
||||
complete: function(err, res) {
|
||||
return callback(err, res ? res.trim() : undefined);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = AutoTypeHelper;
|
||||
@@ -1,20 +0,0 @@
|
||||
const Launcher = require('../../comp/launcher');
|
||||
const AutoTypeNativeHelper = require('./auto-type-native-helper');
|
||||
|
||||
const AutoTypeHelper = function() {
|
||||
};
|
||||
|
||||
AutoTypeHelper.prototype.getActiveWindowTitle = function(callback) {
|
||||
Launcher.spawn({
|
||||
cmd: AutoTypeNativeHelper.getHelperPath(),
|
||||
args: ['--window-info'],
|
||||
complete: function(err, out) {
|
||||
if (err) { return callback(err); }
|
||||
const parts = out.split('\n');
|
||||
return callback(null, (parts[0] || '').trim(),
|
||||
parts[1] ? parts[1].trim() : undefined);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = AutoTypeHelper;
|
||||
@@ -1,39 +0,0 @@
|
||||
const Launcher = require('../../comp/launcher');
|
||||
const Logger = require('../../util/logger');
|
||||
|
||||
const logger = new Logger('auto-type');
|
||||
|
||||
const AutoTypeNativeHelper = {
|
||||
getHelperPath() {
|
||||
if (this._helperPath) {
|
||||
return this._helperPath;
|
||||
}
|
||||
const ext = process.platform === 'win32' ? '.exe' : '';
|
||||
const part = `helper/${process.platform}/KeeWebHelper${ext}`;
|
||||
const possiblePaths = [
|
||||
Launcher.getAppPath(part),
|
||||
Launcher.getUserDataPath(part),
|
||||
Launcher.getWorkDirPath(part)
|
||||
];
|
||||
let helperPath;
|
||||
const helperCTime = -1;
|
||||
possiblePaths.forEach(possiblePath => {
|
||||
try {
|
||||
const ctime = Launcher.statFileSync(possiblePath).ctime;
|
||||
if (ctime > helperCTime) {
|
||||
helperPath = possiblePath;
|
||||
}
|
||||
} catch (e) {}
|
||||
});
|
||||
if (!helperPath) {
|
||||
logger.error('Helper not found. Searched paths:', possiblePaths.join(', '));
|
||||
throw 'Helper not found';
|
||||
}
|
||||
Launcher.ensureRunnable(helperPath);
|
||||
logger.debug('Using auto-type helper', helperPath);
|
||||
this._helperPath = helperPath;
|
||||
return helperPath;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = AutoTypeNativeHelper;
|
||||
@@ -1,43 +1,47 @@
|
||||
const Backbone = require('backbone');
|
||||
const AutoTypeParser = require('./auto-type-parser');
|
||||
const AutoTypeFilter = require('./auto-type-filter');
|
||||
const AutoTypeHelperFactory = require('./auto-type-helper-factory');
|
||||
const Launcher = require('../comp/launcher');
|
||||
const Alerts = require('../comp/alerts');
|
||||
const AutoTypeSelectView = require('../views/auto-type/auto-type-select-view');
|
||||
const Logger = require('../util/logger');
|
||||
const Locale = require('../util/locale');
|
||||
const Timeouts = require('../const/timeouts');
|
||||
const AppSettingsModel = require('../models/app-settings-model');
|
||||
import { Events } from 'framework/events';
|
||||
import { AutoTypeHelper } from 'auto-type/auto-type-helper';
|
||||
import { AutoTypeParser } from 'auto-type/auto-type-parser';
|
||||
import { SelectEntryFilter } from 'comp/app/select-entry-filter';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
import { Features } from 'util/features';
|
||||
import { Alerts } from 'comp/ui/alerts';
|
||||
import { Timeouts } from 'const/timeouts';
|
||||
import { AppSettingsModel } from 'models/app-settings-model';
|
||||
import { AppModel } from 'models/app-model';
|
||||
import { Locale } from 'util/locale';
|
||||
import { Logger } from 'util/logger';
|
||||
import { Links } from 'const/links';
|
||||
import { SelectEntryView } from 'views/select/select-entry-view';
|
||||
|
||||
const logger = new Logger('auto-type');
|
||||
const clearTextAutoTypeLog = localStorage.autoTypeDebug;
|
||||
const clearTextAutoTypeLog = !!localStorage.debugAutoType;
|
||||
|
||||
const AutoType = {
|
||||
helper: AutoTypeHelperFactory.create(),
|
||||
enabled: !!(Launcher && Launcher.autoTypeSupported),
|
||||
supportsEventsWithWindowId: !!(Launcher && Launcher.platform() === 'linux'),
|
||||
selectEntryView: false,
|
||||
pendingEvent: null,
|
||||
running: false,
|
||||
|
||||
init(appModel) {
|
||||
init() {
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
this.appModel = appModel;
|
||||
Backbone.on('auto-type', this.handleEvent, this);
|
||||
Backbone.on('main-window-blur main-window-will-close', this.resetPendingEvent, this);
|
||||
Events.on('auto-type', (e) => this.handleEvent(e));
|
||||
},
|
||||
|
||||
handleEvent(e) {
|
||||
const entry = e && e.entry || null;
|
||||
logger.debug('Auto type event', entry);
|
||||
const entry = (e && e.entry) || null;
|
||||
const sequence = (e && e.sequence) || null;
|
||||
const context = (e && e.context) || null;
|
||||
logger.info('Auto type event', entry);
|
||||
if (this.running) {
|
||||
logger.debug('Already running, skipping event');
|
||||
logger.info('Already running, skipping event');
|
||||
return;
|
||||
}
|
||||
if (entry) {
|
||||
this.hideWindow(() => { this.runAndHandleResult(entry); });
|
||||
this.hideWindow(() => {
|
||||
this.runAndHandleResult({ entry, sequence, context });
|
||||
});
|
||||
} else {
|
||||
if (this.selectEntryView) {
|
||||
return;
|
||||
@@ -53,38 +57,46 @@ const AutoType = {
|
||||
}
|
||||
},
|
||||
|
||||
runAndHandleResult(entry) {
|
||||
this.run(entry, err => {
|
||||
runAndHandleResult(result, windowId) {
|
||||
this.run(result, windowId, (err) => {
|
||||
if (err) {
|
||||
let body = Locale.autoTypeErrorGeneric.replace('{}', err.message || err.toString());
|
||||
let link;
|
||||
if (err.keyPressFailed && Features.isMac) {
|
||||
body = Locale.autoTypeErrorAccessibilityMacOS;
|
||||
link = Links.AutoTypeMacOS;
|
||||
}
|
||||
Alerts.error({
|
||||
header: Locale.autoTypeError,
|
||||
body: Locale.autoTypeErrorGeneric.replace('{}', err.toString())
|
||||
body,
|
||||
link
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (AppSettingsModel.instance.get('lockOnAutoType')) {
|
||||
Backbone.trigger('lock-workspace');
|
||||
if (AppSettingsModel.lockOnAutoType) {
|
||||
Events.emit('lock-workspace');
|
||||
}
|
||||
},
|
||||
|
||||
run(entry, callback) {
|
||||
run(result, windowId, callback) {
|
||||
this.running = true;
|
||||
const sequence = entry.getEffectiveAutoTypeSeq();
|
||||
logger.debug('Start', sequence);
|
||||
const sequence = result.sequence || result.entry.getEffectiveAutoTypeSeq();
|
||||
const context = result.context;
|
||||
logger.info('Start', sequence);
|
||||
const ts = logger.ts();
|
||||
try {
|
||||
const parser = new AutoTypeParser(sequence);
|
||||
const runner = parser.parse();
|
||||
logger.debug('Parsed', this.printOps(runner.ops));
|
||||
runner.resolve(entry, err => {
|
||||
logger.info('Parsed', this.printOps(runner.ops));
|
||||
runner.resolve(result.entry, context, (err) => {
|
||||
if (err) {
|
||||
this.running = false;
|
||||
logger.error('Resolve error', err);
|
||||
return callback && callback(err);
|
||||
}
|
||||
logger.debug('Resolved', this.printOps(runner.ops));
|
||||
if (entry.autoTypeObfuscation) {
|
||||
logger.info('Resolved', this.printOps(runner.ops));
|
||||
if (result.entry.autoTypeObfuscation) {
|
||||
try {
|
||||
runner.obfuscate();
|
||||
} catch (e) {
|
||||
@@ -92,17 +104,17 @@ const AutoType = {
|
||||
logger.error('Obfuscate error', e);
|
||||
return callback && callback(e);
|
||||
}
|
||||
logger.debug('Obfuscated');
|
||||
logger.info('Obfuscated');
|
||||
}
|
||||
runner.run(err => {
|
||||
runner.run((err) => {
|
||||
this.running = false;
|
||||
if (err) {
|
||||
logger.error('Run error', err);
|
||||
return callback && callback(err);
|
||||
}
|
||||
logger.debug('Complete', logger.ts(ts));
|
||||
logger.info('Complete', logger.ts(ts));
|
||||
return callback && callback();
|
||||
});
|
||||
}, windowId);
|
||||
});
|
||||
} catch (ex) {
|
||||
this.running = false;
|
||||
@@ -115,7 +127,7 @@ const AutoType = {
|
||||
try {
|
||||
const parser = new AutoTypeParser(sequence);
|
||||
const runner = parser.parse();
|
||||
runner.resolve(entry, callback);
|
||||
runner.resolve(entry, null, callback);
|
||||
} catch (ex) {
|
||||
return callback(ex);
|
||||
}
|
||||
@@ -141,7 +153,7 @@ const AutoType = {
|
||||
},
|
||||
|
||||
hideWindow(callback) {
|
||||
logger.debug('Hide window');
|
||||
logger.info('Hide window');
|
||||
if (Launcher.isAppFocused()) {
|
||||
Launcher.hideApp();
|
||||
setTimeout(callback, Timeouts.AutoTypeAfterHide);
|
||||
@@ -150,81 +162,152 @@ const AutoType = {
|
||||
}
|
||||
},
|
||||
|
||||
getActiveWindowTitle(callback) {
|
||||
logger.debug('Get window title');
|
||||
return this.helper.getActiveWindowTitle((err, title, url) => {
|
||||
getActiveWindowInfo(callback) {
|
||||
logger.info('Getting window info');
|
||||
const helper = new AutoTypeHelper();
|
||||
return helper.getActiveWindowInfo((err, windowInfo) => {
|
||||
if (err) {
|
||||
logger.error('Error get window title', err);
|
||||
logger.error('Error getting window info', err);
|
||||
} else {
|
||||
logger.debug('Window title', title, url);
|
||||
if (!windowInfo.url) {
|
||||
// try to find a URL in the title
|
||||
const urlMatcher =
|
||||
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\\+~#=]{2,256}\.[a-z]{2,4}\b([-a-zA-Z0-9@:%_\\+.~#?&\/=]*)/;
|
||||
const urlMatches = urlMatcher.exec(windowInfo.title);
|
||||
windowInfo.url = urlMatches && urlMatches.length > 0 ? urlMatches[0] : null;
|
||||
}
|
||||
logger.info('Window info', windowInfo.id, windowInfo.title, windowInfo.url);
|
||||
}
|
||||
return callback(err, title, url);
|
||||
return callback(err, windowInfo);
|
||||
});
|
||||
},
|
||||
|
||||
activeWindowMatches(windowInfo, callback) {
|
||||
if (!windowInfo || !windowInfo.id) {
|
||||
logger.info('Skipped active window check because window id is unknown');
|
||||
return callback(true);
|
||||
}
|
||||
this.getActiveWindowInfo((err, activeWindowInfo) => {
|
||||
if (!activeWindowInfo) {
|
||||
logger.info('Error during active window check, something is wrong', err);
|
||||
return callback(false);
|
||||
}
|
||||
if (activeWindowInfo.id !== windowInfo.id && !this.supportsEventsWithWindowId) {
|
||||
logger.info(
|
||||
`Active window doesn't match: ID is different. ` +
|
||||
`Expected ${windowInfo.id}, got ${activeWindowInfo.id}`
|
||||
);
|
||||
return callback(false, activeWindowInfo);
|
||||
}
|
||||
if (activeWindowInfo.url !== windowInfo.url && !this.supportsEventsWithWindowId) {
|
||||
logger.info(
|
||||
`Active window doesn't match: url is different. ` +
|
||||
`Expected "${windowInfo.url}", got "${activeWindowInfo.url}"`
|
||||
);
|
||||
return callback(false, activeWindowInfo);
|
||||
}
|
||||
logger.info('Active window matches');
|
||||
callback(true, activeWindowInfo);
|
||||
});
|
||||
},
|
||||
|
||||
selectEntryAndRun() {
|
||||
this.getActiveWindowTitle((e, title, url) => {
|
||||
const filter = new AutoTypeFilter({title, url}, this.appModel);
|
||||
const evt = { filter };
|
||||
if (!this.appModel.files.hasOpenFiles()) {
|
||||
this.pendingEvent = evt;
|
||||
this.appModel.files.once('update', this.processPendingEvent, this);
|
||||
logger.debug('auto-type event delayed');
|
||||
this.getActiveWindowInfo(async (e, windowInfo) => {
|
||||
const filter = new SelectEntryFilter(
|
||||
windowInfo,
|
||||
AppModel.instance,
|
||||
AppModel.instance.files,
|
||||
{
|
||||
autoType: true
|
||||
}
|
||||
);
|
||||
const evt = { filter, windowInfo };
|
||||
if (!AppModel.instance.files.hasOpenFiles()) {
|
||||
logger.info('auto-type event delayed');
|
||||
this.focusMainWindow();
|
||||
} else {
|
||||
this.processEventWithFilter(evt);
|
||||
try {
|
||||
await AppModel.instance.unlockAnyFile('autoTypeUnlockMessage');
|
||||
} catch {
|
||||
logger.info('auto-type event canceled');
|
||||
return;
|
||||
}
|
||||
if (this.selectEntryView) {
|
||||
this.selectEntryView.show();
|
||||
}
|
||||
}
|
||||
logger.info('processing auto-type event');
|
||||
this.processEventWithFilter(evt);
|
||||
});
|
||||
},
|
||||
|
||||
focusMainWindow() {
|
||||
setTimeout(() => Launcher.showMainWindow(), Timeouts.RedrawInactiveWindow);
|
||||
if (!Launcher.isAppFocused()) {
|
||||
setTimeout(() => Launcher.showMainWindow(), Timeouts.RedrawInactiveWindow);
|
||||
}
|
||||
},
|
||||
|
||||
processEventWithFilter(evt) {
|
||||
const entries = evt.filter.getEntries();
|
||||
if (entries.length === 1) {
|
||||
let entries = evt.filter.getEntries();
|
||||
if (entries.length === 1 && AppSettingsModel.directAutotype) {
|
||||
this.hideWindow(() => {
|
||||
this.runAndHandleResult(entries.at(0));
|
||||
this.runAndHandleResult({ entry: entries[0] }, evt.windowInfo.id);
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.focusMainWindow();
|
||||
evt.filter.ignoreWindowInfo = true;
|
||||
this.selectEntryView = new AutoTypeSelectView({
|
||||
model: { filter: evt.filter }
|
||||
}).render();
|
||||
this.selectEntryView.on('result', result => {
|
||||
logger.debug('Entry selected', result);
|
||||
|
||||
if (entries.length === 0) {
|
||||
if (evt.filter.useUrl) {
|
||||
evt.filter.useUrl = false;
|
||||
if (evt.filter.title && AppSettingsModel.autoTypeTitleFilterEnabled) {
|
||||
evt.filter.useTitle = true;
|
||||
}
|
||||
}
|
||||
entries = evt.filter.getEntries();
|
||||
if (entries.length === 0 && evt.filter.useTitle) {
|
||||
evt.filter.useTitle = false;
|
||||
}
|
||||
}
|
||||
|
||||
const humanReadableTarget = evt.filter.title || evt.filter.url;
|
||||
const topMessage = humanReadableTarget
|
||||
? Locale.autoTypeMsgMatchedByWindow.replace('{}', humanReadableTarget)
|
||||
: Locale.autoTypeMsgNoWindow;
|
||||
|
||||
this.selectEntryView = new SelectEntryView({
|
||||
isAutoType: true,
|
||||
itemOptions: true,
|
||||
filter: evt.filter,
|
||||
topMessage
|
||||
});
|
||||
this.selectEntryView.on('result', (result) => {
|
||||
logger.info('Entry selected', result);
|
||||
this.selectEntryView.off('result');
|
||||
this.selectEntryView.remove();
|
||||
this.selectEntryView = null;
|
||||
this.hideWindow(() => {
|
||||
if (result) {
|
||||
this.runAndHandleResult(result);
|
||||
if (result?.entry) {
|
||||
this.activeWindowMatches(evt.windowInfo, (matches, activeWindowInfo) => {
|
||||
if (matches) {
|
||||
this.runAndHandleResult(result, evt.windowInfo.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
resetPendingEvent() {
|
||||
if (this.pendingEvent) {
|
||||
this.pendingEvent = null;
|
||||
this.appModel.files.off('update', this.processPendingEvent, this);
|
||||
logger.debug('auto-type event cancelled');
|
||||
}
|
||||
},
|
||||
|
||||
processPendingEvent() {
|
||||
if (!this.pendingEvent) {
|
||||
return;
|
||||
}
|
||||
logger.debug('processing pending auto-type event');
|
||||
const evt = this.pendingEvent;
|
||||
this.appModel.files.off('update', this.processPendingEvent, this);
|
||||
this.pendingEvent = null;
|
||||
this.processEventWithFilter(evt);
|
||||
this.selectEntryView.render();
|
||||
this.selectEntryView.on('show-open-files', async () => {
|
||||
this.selectEntryView.hide();
|
||||
try {
|
||||
await AppModel.instance.unlockAnyFile('autoTypeUnlockMessage');
|
||||
} catch {
|
||||
this.selectEntryView.emit('result', undefined);
|
||||
return;
|
||||
}
|
||||
this.selectEntryView.show();
|
||||
this.selectEntryView.render();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = AutoType;
|
||||
export { AutoType };
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
import 'util/kdbxweb/protected-value';
|
||||
import { h, render } from 'preact';
|
||||
import { App } from 'ui/app';
|
||||
import { Events } from 'util/events';
|
||||
import { StartProfiler } from 'util/logging/start-profiler';
|
||||
import { AppRightsChecker } from 'comp/launcher/app-rights-checker';
|
||||
import { ExportApi } from 'comp/app/export-api';
|
||||
import { SingleInstanceChecker } from 'comp/browser/single-instance-checker';
|
||||
import { FocusDetector } from 'comp/browser/focus-detector';
|
||||
import { IdleTracker } from 'comp/browser/idle-tracker';
|
||||
import { ThemeWatcher } from 'comp/browser/theme-watcher';
|
||||
import { KeyHandler } from 'comp/browser/key-handler';
|
||||
import { PopupNotifier } from 'comp/browser/popup-notifier';
|
||||
import { SettingsManager } from 'comp/settings/settings-manager';
|
||||
import { Alerts } from 'comp/ui/alerts';
|
||||
import { AppSettings } from 'models/app-settings';
|
||||
import { RuntimeData } from 'models/runtime-data';
|
||||
import { Features } from 'util/features';
|
||||
import { KdbxwebInit } from 'comp/kdbxweb/kdbxweb-init';
|
||||
import { Locale } from 'util/locale';
|
||||
import { Logger } from 'util/logger';
|
||||
import { FeatureTester } from 'util/browser/feature-tester';
|
||||
import { UsbListener } from 'comp/devices/usb-listener';
|
||||
import { noop } from 'util/fn';
|
||||
import { ConfigLoader } from 'comp/settings/config-loader';
|
||||
import { WindowClass } from 'comp/browser/window-class';
|
||||
import { FileManager } from 'models/file-manager';
|
||||
import { Updater } from './comp/app/updater';
|
||||
import { Timeouts } from 'const/timeouts';
|
||||
import { PluginManager } from 'plugins/plugin-manager';
|
||||
import { Workspace } from 'models/workspace';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
kw: typeof ExportApi;
|
||||
}
|
||||
}
|
||||
|
||||
StartProfiler.milestone('loading modules');
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
bootstrap().catch(noop);
|
||||
});
|
||||
|
||||
export async function bootstrap(): Promise<void> {
|
||||
StartProfiler.milestone('document ready');
|
||||
|
||||
const logger = new Logger('bootstrap');
|
||||
|
||||
try {
|
||||
await loadConfigs();
|
||||
await initModules();
|
||||
await loadRemoteConfig();
|
||||
await ensureCanRun();
|
||||
addWindowClasses();
|
||||
initUsbListener();
|
||||
await showApp();
|
||||
postInit();
|
||||
} catch (e) {
|
||||
logger.error('Error starting app', e);
|
||||
}
|
||||
|
||||
async function ensureCanRun() {
|
||||
if (Features.isFrame && !AppSettings.allowIframes) {
|
||||
return Promise.reject(
|
||||
'Running in iframe is not allowed (this can be changed in the app config).'
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
await FeatureTester.test();
|
||||
StartProfiler.milestone('checking features');
|
||||
} catch (e) {
|
||||
Alerts.error({
|
||||
header: Locale.appSettingsError,
|
||||
body: Locale.appNotSupportedError,
|
||||
pre: String(e),
|
||||
buttons: [],
|
||||
esc: undefined,
|
||||
enter: undefined,
|
||||
click: undefined
|
||||
});
|
||||
logger.error('Feature test failed', e);
|
||||
throw new Error(`Feature test failed`);
|
||||
}
|
||||
}
|
||||
|
||||
function loadConfigs() {
|
||||
return Promise.all([AppSettings.init(), RuntimeData.init(), FileManager.init()]).then(
|
||||
() => {
|
||||
StartProfiler.milestone('loading configs');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function initModules() {
|
||||
KeyHandler.init();
|
||||
PopupNotifier.init();
|
||||
KdbxwebInit.init();
|
||||
FocusDetector.init();
|
||||
// AutoType.init(); // TODO
|
||||
ThemeWatcher.init();
|
||||
SettingsManager.init();
|
||||
await PluginManager.init();
|
||||
window.kw = ExportApi;
|
||||
|
||||
StartProfiler.milestone('initializing modules');
|
||||
}
|
||||
|
||||
async function loadRemoteConfig() {
|
||||
SettingsManager.setBySettings();
|
||||
|
||||
const loaded = await ConfigLoader.loadConfig();
|
||||
if (loaded) {
|
||||
SettingsManager.setBySettings();
|
||||
}
|
||||
|
||||
StartProfiler.milestone('loading remote config');
|
||||
}
|
||||
|
||||
function addWindowClasses() {
|
||||
WindowClass.init();
|
||||
StartProfiler.milestone('adding window classes');
|
||||
}
|
||||
|
||||
function initUsbListener() {
|
||||
UsbListener.init();
|
||||
StartProfiler.milestone('starting usb');
|
||||
}
|
||||
|
||||
function showApp() {
|
||||
return Promise.resolve().then(() => {
|
||||
const skipHttpsWarning = AppSettings.skipHttpsWarning;
|
||||
const protocolIsInsecure = ['https:', 'file:', 'app:'].indexOf(location.protocol) < 0;
|
||||
const hostIsInsecure = location.hostname !== 'localhost';
|
||||
if (protocolIsInsecure && hostIsInsecure && !skipHttpsWarning) {
|
||||
return new Promise<void>((resolve) => {
|
||||
Alerts.error({
|
||||
header: Locale.appSecWarn,
|
||||
icon: 'user-secret',
|
||||
esc: undefined,
|
||||
enter: undefined,
|
||||
click: undefined,
|
||||
body: Locale.appSecWarnBody1 + '\n\n' + Locale.appSecWarnBody2,
|
||||
buttons: [{ result: '', title: Locale.appSecWarnBtn, error: true }],
|
||||
complete: () => {
|
||||
resolve(showView());
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
return showView().then(
|
||||
() => new Promise<void>((resolve) => requestAnimationFrame(() => resolve()))
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function postInit() {
|
||||
Updater.init();
|
||||
SingleInstanceChecker.init();
|
||||
AppRightsChecker.init().catch(noop);
|
||||
IdleTracker.init();
|
||||
// BrowserExtensionConnector.init(appModel); // TODO
|
||||
|
||||
setTimeout(() => {
|
||||
PluginManager.runAutoUpdate().catch(noop);
|
||||
}, Timeouts.PluginsAutoUpdateAfterStart);
|
||||
}
|
||||
|
||||
function showView() {
|
||||
Workspace.showOpen();
|
||||
|
||||
const root = document.querySelector('.app');
|
||||
if (root) {
|
||||
const app = h(App, null);
|
||||
render(app, document.body, root);
|
||||
}
|
||||
StartProfiler.milestone('first view rendering');
|
||||
|
||||
Events.emit('app-ready');
|
||||
StartProfiler.milestone('app ready event');
|
||||
|
||||
StartProfiler.report();
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
const Backbone = require('backbone');
|
||||
const EntryModel = require('../models/entry-model');
|
||||
const Comparators = require('../util/comparators');
|
||||
|
||||
const EntryCollection = Backbone.Collection.extend({
|
||||
model: EntryModel,
|
||||
|
||||
comparator: null,
|
||||
|
||||
comparators: {
|
||||
'none': null,
|
||||
'title': Comparators.stringComparator('title', true),
|
||||
'-title': Comparators.stringComparator('title', false),
|
||||
'website': Comparators.stringComparator('url', true),
|
||||
'-website': Comparators.stringComparator('url', false),
|
||||
'user': Comparators.stringComparator('user', true),
|
||||
'-user': Comparators.stringComparator('user', false),
|
||||
'created': Comparators.dateComparator('created', true),
|
||||
'-created': Comparators.dateComparator('created', false),
|
||||
'updated': Comparators.dateComparator('updated', true),
|
||||
'-updated': Comparators.dateComparator('updated', false),
|
||||
'-attachments': function(x, y) { return this.attachmentSortVal(x).localeCompare(this.attachmentSortVal(y)); }
|
||||
},
|
||||
|
||||
defaultComparator: 'title',
|
||||
|
||||
initialize: function(models, options) {
|
||||
const comparatorName = options && options.comparator || this.defaultComparator;
|
||||
this.comparator = this.comparators[comparatorName];
|
||||
},
|
||||
|
||||
sortEntries: function(comparator) {
|
||||
this.comparator = this.comparators[comparator] || this.comparators[this.defaultComparator];
|
||||
this.sort();
|
||||
},
|
||||
|
||||
attachmentSortVal: function(entry) {
|
||||
const att = entry.attachments;
|
||||
let str = att.length ? String.fromCharCode(64 + att.length) : 'Z';
|
||||
if (att[0]) {
|
||||
str += att[0].title;
|
||||
}
|
||||
return str;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = EntryCollection;
|
||||
@@ -1,24 +0,0 @@
|
||||
const Backbone = require('backbone');
|
||||
const FileModel = require('../models/file-model');
|
||||
|
||||
const FileCollection = Backbone.Collection.extend({
|
||||
model: FileModel,
|
||||
|
||||
hasOpenFiles: function() {
|
||||
return this.some(file => file.get('open'));
|
||||
},
|
||||
|
||||
hasUnsavedFiles: function() {
|
||||
return this.some(file => file.get('modified'));
|
||||
},
|
||||
|
||||
hasDirtyFiles: function() {
|
||||
return this.some(file => file.get('dirty'));
|
||||
},
|
||||
|
||||
getByName: function(name) {
|
||||
return this.find(file => file.get('name').toLowerCase() === name.toLowerCase());
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = FileCollection;
|
||||
@@ -1,42 +0,0 @@
|
||||
const Backbone = require('backbone');
|
||||
const FileInfoModel = require('../models/file-info-model');
|
||||
const SettingsStore = require('../comp/settings-store');
|
||||
|
||||
const FileInfoCollection = Backbone.Collection.extend({
|
||||
model: FileInfoModel,
|
||||
|
||||
initialize: function () {
|
||||
},
|
||||
|
||||
load: function () {
|
||||
return SettingsStore.load('file-info').then(data => {
|
||||
if (data) {
|
||||
this.reset(data, {silent: true});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
save: function () {
|
||||
SettingsStore.save('file-info', this.toJSON());
|
||||
},
|
||||
|
||||
getLast: function () {
|
||||
return this.first();
|
||||
},
|
||||
|
||||
getMatch: function (storage, name, path) {
|
||||
return this.find(fi => {
|
||||
return (fi.get('storage') || '') === (storage || '') &&
|
||||
(fi.get('name') || '') === (name || '') &&
|
||||
(fi.get('path') || '') === (path || '');
|
||||
});
|
||||
},
|
||||
|
||||
getByName: function(name) {
|
||||
return this.find(file => file.get('name').toLowerCase() === name.toLowerCase());
|
||||
}
|
||||
});
|
||||
|
||||
FileInfoCollection.instance = new FileInfoCollection();
|
||||
|
||||
module.exports = FileInfoCollection;
|
||||
@@ -1,8 +0,0 @@
|
||||
const Backbone = require('backbone');
|
||||
const GroupModel = require('../models/group-model');
|
||||
|
||||
const GroupCollection = Backbone.Collection.extend({
|
||||
model: GroupModel
|
||||
});
|
||||
|
||||
module.exports = GroupCollection;
|
||||
@@ -1,8 +0,0 @@
|
||||
const Backbone = require('backbone');
|
||||
const MenuItemModel = require('../../models/menu/menu-item-model');
|
||||
|
||||
const MenuItemCollection = Backbone.Collection.extend({
|
||||
model: MenuItemModel
|
||||
});
|
||||
|
||||
module.exports = MenuItemCollection;
|
||||
@@ -1,8 +0,0 @@
|
||||
const Backbone = require('backbone');
|
||||
const MenuOptionModel = require('../../models/menu/menu-option-model');
|
||||
|
||||
const MenuOptionCollection = Backbone.Collection.extend({
|
||||
model: MenuOptionModel
|
||||
});
|
||||
|
||||
module.exports = MenuOptionCollection;
|
||||
@@ -1,8 +0,0 @@
|
||||
const Backbone = require('backbone');
|
||||
const MenuSectionModel = require('../../models/menu/menu-section-model');
|
||||
|
||||
const MenuSectionCollection = Backbone.Collection.extend({
|
||||
model: MenuSectionModel
|
||||
});
|
||||
|
||||
module.exports = MenuSectionCollection;
|
||||
@@ -1,85 +0,0 @@
|
||||
const ModalView = require('../views/modal-view');
|
||||
const Locale = require('../util/locale');
|
||||
|
||||
const Alerts = {
|
||||
alertDisplayed: false,
|
||||
|
||||
buttons: {
|
||||
ok: {result: 'yes', get title() { return Locale.alertOk; }},
|
||||
yes: {result: 'yes', get title() { return Locale.alertYes; }},
|
||||
no: {result: '', get title() { return Locale.alertNo; }},
|
||||
cancel: {result: '', get title() { return Locale.alertCancel; }}
|
||||
},
|
||||
|
||||
alert: function(config) {
|
||||
if (config.skipIfAlertDisplayed && Alerts.alertDisplayed) {
|
||||
return null;
|
||||
}
|
||||
Alerts.alertDisplayed = true;
|
||||
const view = new ModalView({ model: config });
|
||||
view.render();
|
||||
view.on('result', (res, check) => {
|
||||
Alerts.alertDisplayed = false;
|
||||
if (res && config.success) {
|
||||
config.success(res, check);
|
||||
}
|
||||
if (!res && config.cancel) {
|
||||
config.cancel();
|
||||
}
|
||||
if (config.complete) {
|
||||
config.complete(res, check);
|
||||
}
|
||||
});
|
||||
return view;
|
||||
},
|
||||
|
||||
notImplemented: function() {
|
||||
this.alert({
|
||||
header: Locale.notImplemented,
|
||||
body: '',
|
||||
icon: 'exclamation-triangle',
|
||||
buttons: [this.buttons.ok],
|
||||
esc: '',
|
||||
click: '',
|
||||
enter: ''
|
||||
});
|
||||
},
|
||||
|
||||
info: function(config) {
|
||||
this.alert(_.extend({
|
||||
header: '',
|
||||
body: '',
|
||||
icon: 'info',
|
||||
buttons: [this.buttons.ok],
|
||||
esc: '',
|
||||
click: '',
|
||||
enter: ''
|
||||
}, config));
|
||||
},
|
||||
|
||||
error: function(config) {
|
||||
this.alert(_.extend({
|
||||
header: '',
|
||||
body: '',
|
||||
icon: 'exclamation-circle',
|
||||
buttons: [this.buttons.ok],
|
||||
esc: '',
|
||||
click: '',
|
||||
enter: ''
|
||||
}, config));
|
||||
},
|
||||
|
||||
yesno: function(config) {
|
||||
this.alert(_.extend({
|
||||
header: '',
|
||||
body: '',
|
||||
icon: 'question',
|
||||
buttons: [this.buttons.yes, this.buttons.no],
|
||||
esc: '',
|
||||
click: '',
|
||||
enter: 'yes'
|
||||
}, config));
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = Alerts;
|
||||
@@ -0,0 +1,5 @@
|
||||
const DragDropInfo = {
|
||||
dragObject: null as unknown
|
||||
};
|
||||
|
||||
export { DragDropInfo };
|
||||
@@ -0,0 +1,29 @@
|
||||
import { AppSettingsFieldName, AppSettings } from 'models/app-settings';
|
||||
import { RuntimeData } from 'models/runtime-data';
|
||||
|
||||
const ExportApi = {
|
||||
settings: {
|
||||
get(key?: AppSettingsFieldName): unknown {
|
||||
return key ? AppSettings[key] : { ...AppSettings };
|
||||
},
|
||||
set(key: AppSettingsFieldName, value: unknown): boolean {
|
||||
return AppSettings.set(key, value);
|
||||
},
|
||||
del(key: AppSettingsFieldName): void {
|
||||
AppSettings.delete(key);
|
||||
}
|
||||
},
|
||||
runtimeData: {
|
||||
get(key?: string): unknown {
|
||||
return key ? RuntimeData.get(key) : { ...RuntimeData };
|
||||
},
|
||||
set(key: string, value: unknown): boolean {
|
||||
return RuntimeData.set(key, value);
|
||||
},
|
||||
del(key: string): void {
|
||||
RuntimeData.delete(key);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export { ExportApi };
|
||||
@@ -0,0 +1,689 @@
|
||||
import * as kdbxweb from 'kdbxweb';
|
||||
import { Logger } from 'util/logger';
|
||||
import { Storage } from 'storage';
|
||||
import { Locale } from 'util/locale';
|
||||
import { IdGenerator } from 'util/generators/id-generator';
|
||||
import { OpenParams } from 'models/ui/open-state';
|
||||
import { FileManager } from 'models/file-manager';
|
||||
import { File } from 'models/file';
|
||||
import { FileInfo } from 'models/file-info';
|
||||
import {
|
||||
StorageFileData,
|
||||
StorageFileNotFoundError,
|
||||
StorageFileOptions,
|
||||
StorageFileStat,
|
||||
StorageRevConflictError,
|
||||
StorageSaveResult
|
||||
} from 'storage/types';
|
||||
import { AppSettings } from 'models/app-settings';
|
||||
import { errorToString, noop } from 'util/fn';
|
||||
import { StorageBase } from 'storage/storage-base';
|
||||
import { UrlFormat } from 'util/formatting/url-format';
|
||||
import { DateFormat } from 'util/formatting/date-format';
|
||||
|
||||
const MaxLoadLoops = 3;
|
||||
|
||||
interface FileSyncOptions {
|
||||
storage?: string;
|
||||
path?: string;
|
||||
opts: StorageFileOptions;
|
||||
}
|
||||
|
||||
interface SyncContext {
|
||||
logger: Logger;
|
||||
file: File;
|
||||
fileInfo: FileInfo;
|
||||
storage: StorageBase;
|
||||
path: string;
|
||||
stat?: StorageFileStat;
|
||||
opts?: StorageFileOptions;
|
||||
retryCount: number;
|
||||
}
|
||||
|
||||
class FileController {
|
||||
async open(params: OpenParams): Promise<File> {
|
||||
const logger = new Logger('open', params.name);
|
||||
logger.info('File open request');
|
||||
|
||||
if (!params.name) {
|
||||
throw new Error('Empty name');
|
||||
}
|
||||
|
||||
const fileInfo = params.id
|
||||
? FileManager.getFileInfoById(params.id)
|
||||
: FileManager.getFileInfo(params.name, params.storage, params.path);
|
||||
|
||||
if (!params.opts && fileInfo && fileInfo.opts) {
|
||||
params.opts = fileInfo.opts;
|
||||
}
|
||||
|
||||
if (fileInfo && fileInfo.modified) {
|
||||
logger.info('Open file from cache because it is modified');
|
||||
const file = await this.openFileFromCache(params, fileInfo, logger);
|
||||
logger.info('Sync just opened modified file');
|
||||
setTimeout(() => {
|
||||
this.syncFile(file).catch(noop);
|
||||
}, 0);
|
||||
return file;
|
||||
} else if (params.fileData) {
|
||||
logger.info('Open file from supplied content');
|
||||
if (params.storage === 'file' && params.path) {
|
||||
const stat = await Storage.file.stat(params.path);
|
||||
params.rev = stat.rev;
|
||||
return this.openFileWithData(params, fileInfo, params.fileData, false);
|
||||
} else {
|
||||
return this.openFileWithData(params, fileInfo, params.fileData, true);
|
||||
}
|
||||
} else if (!params.storage && fileInfo) {
|
||||
logger.info('Open file from cache as main storage');
|
||||
return this.openFileFromCache(params, fileInfo, logger);
|
||||
} else if (
|
||||
fileInfo?.openDate &&
|
||||
fileInfo.rev === params.rev &&
|
||||
fileInfo.storage !== 'file' &&
|
||||
!AppSettings.disableOfflineStorage
|
||||
) {
|
||||
logger.info('Open file from cache because it is latest');
|
||||
try {
|
||||
return this.openFileFromCache(params, fileInfo, logger);
|
||||
} catch (err) {
|
||||
if (err instanceof kdbxweb.KdbxError) {
|
||||
// TODO: || err.ykError
|
||||
throw err;
|
||||
}
|
||||
logger.info('Error loading file from cache, trying to open from storage', err);
|
||||
return this.openFileFromStorage(params, fileInfo, logger, true);
|
||||
}
|
||||
} else if (
|
||||
!fileInfo ||
|
||||
!fileInfo.openDate ||
|
||||
params.storage === 'file' ||
|
||||
AppSettings.disableOfflineStorage
|
||||
) {
|
||||
return this.openFileFromStorage(params, fileInfo, logger, false);
|
||||
} else {
|
||||
logger.info('Open file from cache, will sync after load', params.storage);
|
||||
let file: File;
|
||||
try {
|
||||
file = await this.openFileFromCache(params, fileInfo, logger);
|
||||
} catch (err) {
|
||||
if (err instanceof kdbxweb.KdbxError) {
|
||||
// TODO: || err.ykError
|
||||
throw err;
|
||||
}
|
||||
logger.info('Error loading file from cache, trying to open from storage', err);
|
||||
return this.openFileFromStorage(params, fileInfo, logger, true);
|
||||
}
|
||||
|
||||
logger.info('Sync just opened file');
|
||||
setTimeout(() => {
|
||||
this.syncFile(file).catch(noop);
|
||||
}, 0);
|
||||
return file;
|
||||
}
|
||||
}
|
||||
|
||||
private async openFileFromCache(
|
||||
params: OpenParams,
|
||||
fileInfo: FileInfo,
|
||||
logger: Logger
|
||||
): Promise<File> {
|
||||
let data: ArrayBuffer;
|
||||
try {
|
||||
const res = await Storage.cache.load(fileInfo.id);
|
||||
if (!res?.data) {
|
||||
throw new Error(Locale.openFileNoCacheError);
|
||||
}
|
||||
data = res.data;
|
||||
logger.info('Loaded from cache');
|
||||
} catch (err) {
|
||||
logger.info('Loaded error from cache', err);
|
||||
throw err;
|
||||
}
|
||||
return this.openFileWithData(params, fileInfo, data, false);
|
||||
}
|
||||
|
||||
private async openFileFromStorage(
|
||||
params: OpenParams,
|
||||
fileInfo: FileInfo | undefined,
|
||||
logger: Logger,
|
||||
noCache: boolean
|
||||
): Promise<File> {
|
||||
if (!params.storage) {
|
||||
throw new Error('Storage open error: empty storage');
|
||||
}
|
||||
if (!params.path) {
|
||||
throw new Error('Storage open error: empty path');
|
||||
}
|
||||
|
||||
logger.info('Open file from storage', params.storage);
|
||||
|
||||
const storage = Storage.get(params.storage ?? '');
|
||||
if (!storage) {
|
||||
logger.error('Storage not found', storage);
|
||||
throw new Error(`Storage not found: ${storage ?? ''}`);
|
||||
}
|
||||
const cacheRev = (fileInfo && fileInfo.rev) || null;
|
||||
if (cacheRev && storage.stat) {
|
||||
logger.info('Stat file');
|
||||
let stat: StorageFileStat;
|
||||
try {
|
||||
stat = await storage.stat(params.path, params.opts);
|
||||
} catch (err) {
|
||||
if (
|
||||
!noCache &&
|
||||
fileInfo &&
|
||||
storage.name !== 'file' &&
|
||||
!AppSettings.disableOfflineStorage
|
||||
) {
|
||||
logger.info('Open file from cache because of stat error', err);
|
||||
return this.openFileFromCache(params, fileInfo, logger);
|
||||
} else {
|
||||
logger.info('Stat error', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
if (
|
||||
!noCache &&
|
||||
fileInfo &&
|
||||
storage.name !== 'file' &&
|
||||
stat.rev === cacheRev &&
|
||||
!AppSettings.disableOfflineStorage
|
||||
) {
|
||||
logger.info('Open file from cache because it is latest');
|
||||
return this.openFileFromCache(params, fileInfo, logger);
|
||||
}
|
||||
logger.info(`Open file from storage (${stat.rev ?? '(empty)'}, local ${cacheRev})`);
|
||||
}
|
||||
|
||||
logger.info('Load from storage');
|
||||
let res: StorageFileData;
|
||||
try {
|
||||
res = await storage.load(params.path, params.opts);
|
||||
} catch (err) {
|
||||
if (fileInfo && fileInfo.openDate && !AppSettings.disableOfflineStorage) {
|
||||
logger.info('Open file from cache because of storage load error', err);
|
||||
return this.openFileFromCache(params, fileInfo, logger);
|
||||
} else {
|
||||
logger.info('Storage load error', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('Open file from content loaded from storage');
|
||||
params.fileData = res.data;
|
||||
params.rev = res.rev;
|
||||
|
||||
const needSaveToCache = storage.name !== 'file';
|
||||
return this.openFileWithData(params, fileInfo, res.data, needSaveToCache);
|
||||
}
|
||||
|
||||
private async openFileWithData(
|
||||
params: OpenParams,
|
||||
fileInfo: FileInfo | undefined,
|
||||
data: ArrayBuffer,
|
||||
updateCacheOnSuccess: boolean
|
||||
): Promise<File> {
|
||||
if (!params.name) {
|
||||
throw new Error('Open file without name');
|
||||
}
|
||||
if (!params.password) {
|
||||
throw new Error('Open file without password');
|
||||
}
|
||||
const logger = new Logger('open', params.name);
|
||||
|
||||
let needLoadKeyFile = false;
|
||||
if (!params.keyFileData && fileInfo && fileInfo.keyFileName) {
|
||||
params.keyFileName = fileInfo.keyFileName;
|
||||
if (AppSettings.rememberKeyFiles === 'data' && fileInfo.keyFileHash) {
|
||||
params.keyFileData = File.createKeyFileWithHash(fileInfo.keyFileHash);
|
||||
} else if (AppSettings.rememberKeyFiles === 'path' && fileInfo.keyFilePath) {
|
||||
params.keyFilePath = fileInfo.keyFilePath;
|
||||
if (Storage.file.enabled) {
|
||||
needLoadKeyFile = true;
|
||||
}
|
||||
}
|
||||
} else if (params.keyFilePath && !params.keyFileData && !fileInfo) {
|
||||
needLoadKeyFile = true;
|
||||
}
|
||||
|
||||
if (needLoadKeyFile && params.keyFilePath) {
|
||||
let data: ArrayBuffer;
|
||||
try {
|
||||
const res = await Storage.file.load(params.keyFilePath);
|
||||
data = res.data;
|
||||
} catch (err) {
|
||||
logger.info('Storage load error', err);
|
||||
throw err;
|
||||
}
|
||||
params.keyFileData = data;
|
||||
}
|
||||
|
||||
const id = fileInfo ? fileInfo.id : IdGenerator.uuid();
|
||||
|
||||
if (FileManager.getFileById(id)) {
|
||||
throw new Error('Duplicate file id');
|
||||
}
|
||||
|
||||
const file = await File.open(id, params.name, params.password, data, params.keyFileData);
|
||||
file.batchSet(() => {
|
||||
file.storage = params.storage;
|
||||
file.path = params.path;
|
||||
file.keyFileName = params.keyFileName;
|
||||
file.keyFilePath = params.keyFilePath;
|
||||
file.backup = fileInfo?.backup;
|
||||
file.chalResp = params.chalResp;
|
||||
file.syncDate = fileInfo?.syncDate;
|
||||
// if (params.encryptedPassword) { // TODO: touch id
|
||||
// file.encryptedPassword = fileInfo?.encryptedPassword;
|
||||
// file.encryptedPasswordDate = fileInfo?.encryptedPasswordDate || new Date();
|
||||
// }
|
||||
});
|
||||
|
||||
if (fileInfo?.modified) {
|
||||
if (fileInfo.editState) {
|
||||
logger.info('Loaded local edit state');
|
||||
file.setLocalEditState(fileInfo.editState);
|
||||
}
|
||||
logger.info('Mark file as modified');
|
||||
file.modified = true;
|
||||
}
|
||||
if (updateCacheOnSuccess && !AppSettings.disableOfflineStorage && params.fileData) {
|
||||
logger.info('Save loaded file to cache');
|
||||
await Storage.cache.save(file.id, params.fileData);
|
||||
}
|
||||
|
||||
const rev = params.rev || (fileInfo && fileInfo.rev);
|
||||
// this.setFileOpts(file, params.opts); // TODO: file opts
|
||||
|
||||
logger.info('Add last open file', file.id, file.name, file.storage, file.path, rev);
|
||||
FileManager.addLastOpenFileInfo(file, rev);
|
||||
FileManager.addFile(file);
|
||||
// this.fileOpened(file, data, params); // TODO: fileOpened
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
async syncFile(file: File, options?: FileSyncOptions): Promise<void> {
|
||||
if (file.demo) {
|
||||
return;
|
||||
}
|
||||
const logger = new Logger('sync', file.name);
|
||||
|
||||
const storageName = options?.storage || file.storage;
|
||||
const storage = storageName ? Storage.get(storageName) : undefined;
|
||||
const opts = options?.opts || file.opts;
|
||||
let path = options?.path || file.path;
|
||||
if (storage?.getPathForName && (!path || storage.name !== file.storage)) {
|
||||
path = storage.getPathForName(file.name);
|
||||
}
|
||||
|
||||
const optionsForLogging = { ...options };
|
||||
if (optionsForLogging.opts && optionsForLogging.opts.password) {
|
||||
optionsForLogging.opts = { ...optionsForLogging.opts };
|
||||
optionsForLogging.opts.password = '***';
|
||||
}
|
||||
logger.info('Sync started', storage, path, optionsForLogging);
|
||||
const fileInfo = this.getOrCreateFileInfo(file, logger);
|
||||
|
||||
let syncError: unknown;
|
||||
try {
|
||||
if (file.syncing) {
|
||||
throw new Error('Sync in progress');
|
||||
}
|
||||
file.setSyncProgress();
|
||||
if (!storage) {
|
||||
if (!file.modified && fileInfo.id === file.id) {
|
||||
logger.info('Local, not modified');
|
||||
} else {
|
||||
logger.info('Local, save to cache');
|
||||
const data = await file.getData();
|
||||
try {
|
||||
await Storage.cache.save(fileInfo.id, data);
|
||||
logger.info('Saved to cache');
|
||||
} catch (err) {
|
||||
logger.info('Cache save error', err);
|
||||
throw err;
|
||||
}
|
||||
// this.scheduleBackupFile(file, data); // TODO: backups
|
||||
}
|
||||
} else {
|
||||
if (!path) {
|
||||
throw new Error('Cannot sync file to storage but without path');
|
||||
}
|
||||
const ctx: SyncContext = {
|
||||
logger,
|
||||
file,
|
||||
fileInfo,
|
||||
storage,
|
||||
path,
|
||||
opts,
|
||||
retryCount: 0
|
||||
};
|
||||
|
||||
logger.info('Stat file');
|
||||
let stat: StorageFileStat | undefined;
|
||||
try {
|
||||
stat = await storage.stat(path, opts);
|
||||
ctx.stat = stat;
|
||||
} catch (err) {
|
||||
if (err instanceof StorageFileNotFoundError) {
|
||||
logger.info('File does not exist in storage, creating');
|
||||
await this.saveToCacheAndStorage(ctx);
|
||||
} else if (file.dirty) {
|
||||
if (AppSettings.disableOfflineStorage) {
|
||||
logger.info('Stat error, dirty, cache is disabled', err);
|
||||
throw err;
|
||||
}
|
||||
logger.info('Stat error, dirty, save to cache');
|
||||
const data = await file.getData();
|
||||
try {
|
||||
await Storage.cache.save(fileInfo.id, data);
|
||||
file.dirty = false;
|
||||
logger.info('Saved to cache, exit with error', err);
|
||||
} catch (e) {
|
||||
logger.error('Error saving to cache', e);
|
||||
}
|
||||
throw err;
|
||||
} else {
|
||||
logger.info('Stat error, not dirty', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
if (stat) {
|
||||
if (stat.rev === fileInfo.rev) {
|
||||
if (file.modified) {
|
||||
logger.info('Stat found same version, modified, saving');
|
||||
await this.saveToCacheAndStorage(ctx);
|
||||
} else {
|
||||
logger.info('Stat found same version, not modified');
|
||||
}
|
||||
} else {
|
||||
logger.info('Found new version, loading from storage');
|
||||
await this.loadFromStorageAndMerge(ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('Sync finished OK');
|
||||
} catch (e) {
|
||||
syncError = e;
|
||||
logger.error('Sync finished with error', syncError);
|
||||
}
|
||||
|
||||
file.setSyncComplete(path, storageName, syncError ? errorToString(syncError) : undefined);
|
||||
fileInfo.batchSet(() => {
|
||||
fileInfo.name = file.name;
|
||||
fileInfo.storage = storageName;
|
||||
fileInfo.path = path;
|
||||
// fileInfo.opts = this.getStoreOpts(file); // TODO: storage opts
|
||||
fileInfo.modified = file.dirty ? fileInfo.modified : file.modified;
|
||||
fileInfo.editState = file.dirty ? fileInfo.editState : file.getLocalEditState();
|
||||
fileInfo.syncDate = file.syncDate;
|
||||
fileInfo.chalResp = file.chalResp;
|
||||
|
||||
if (AppSettings.rememberKeyFiles === 'data') {
|
||||
fileInfo.keyFileName = file.keyFileName;
|
||||
fileInfo.keyFileHash = file.getKeyFileHash();
|
||||
}
|
||||
});
|
||||
|
||||
if (!FileManager.getFileById(fileInfo.id)) {
|
||||
FileManager.addFileInfo(fileInfo, true);
|
||||
}
|
||||
|
||||
if (syncError) {
|
||||
throw syncError;
|
||||
}
|
||||
}
|
||||
|
||||
private getOrCreateFileInfo(file: File, logger: Logger): FileInfo {
|
||||
let fileInfo = FileManager.getFileInfoById(file.id);
|
||||
if (!fileInfo) {
|
||||
logger.info('Create new file info');
|
||||
const dt = new Date();
|
||||
fileInfo = new FileInfo({
|
||||
id: file.id,
|
||||
name: file.name,
|
||||
storage: file.storage,
|
||||
path: file.path,
|
||||
// opts: this.getStoreOpts(file), // TODO: store opts
|
||||
modified: file.modified,
|
||||
syncDate: dt,
|
||||
openDate: dt,
|
||||
backup: file.backup
|
||||
});
|
||||
}
|
||||
return fileInfo;
|
||||
}
|
||||
|
||||
private async saveToStorage(ctx: SyncContext, data: ArrayBuffer) {
|
||||
ctx.logger.info('Save data to storage');
|
||||
const storageRev = ctx.fileInfo.storage === ctx.storage.name ? ctx.fileInfo.rev : undefined;
|
||||
let saveResult: StorageSaveResult;
|
||||
try {
|
||||
saveResult = await ctx.storage.save(ctx.path, data, ctx.opts, storageRev);
|
||||
} catch (err) {
|
||||
if (err instanceof StorageRevConflictError) {
|
||||
ctx.logger.info('Save rev conflict, reloading from storage');
|
||||
await this.loadFromStorageAndMerge(ctx);
|
||||
return;
|
||||
} else {
|
||||
ctx.logger.info('Error saving data to storage');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
if (saveResult.rev) {
|
||||
ctx.logger.info('Update rev in file info');
|
||||
ctx.fileInfo.rev = saveResult.rev;
|
||||
}
|
||||
if (saveResult.path) {
|
||||
ctx.logger.info('Update path in file info', saveResult.path);
|
||||
ctx.file.path = saveResult.path;
|
||||
ctx.fileInfo.path = saveResult.path;
|
||||
ctx.path = saveResult.path;
|
||||
}
|
||||
ctx.file.syncDate = new Date();
|
||||
ctx.logger.info('Save to storage complete, updated sync date');
|
||||
// this.scheduleBackupFile(file, data); // TODO: backups
|
||||
}
|
||||
|
||||
private async saveToCacheAndStorage(ctx: SyncContext) {
|
||||
ctx.logger.info('Getting file data for saving');
|
||||
const data = await ctx.file.getData();
|
||||
if (ctx.storage.name === 'file') {
|
||||
ctx.logger.info('Saving to file storage');
|
||||
} else if (!ctx.file.dirty) {
|
||||
ctx.logger.info('Saving to storage, skip cache because not dirty');
|
||||
} else if (AppSettings.disableOfflineStorage) {
|
||||
ctx.logger.info('Saving to storage, cache is disabled');
|
||||
} else {
|
||||
ctx.logger.info('Saving to cache');
|
||||
await Storage.cache.save(ctx.file.id, data);
|
||||
ctx.file.dirty = false;
|
||||
ctx.logger.info('Saved to cache, saving to storage');
|
||||
}
|
||||
await this.saveToStorage(ctx, data);
|
||||
}
|
||||
|
||||
private async loadFromStorageAndMerge(ctx: SyncContext) {
|
||||
if (++ctx.retryCount === MaxLoadLoops) {
|
||||
throw new Error('Too many load attempts');
|
||||
}
|
||||
ctx.logger.info('Load from storage, attempt', ctx.retryCount);
|
||||
const storageLoadRes = await ctx.storage.load(ctx.path, ctx.opts);
|
||||
ctx.logger.info('Load from storage', storageLoadRes.rev);
|
||||
|
||||
try {
|
||||
await ctx.file.mergeOrUpdate(storageLoadRes.data);
|
||||
ctx.logger.info('Merge complete OK');
|
||||
} catch (err) {
|
||||
ctx.logger.info('Merge error', err);
|
||||
// this.refresh(); // TODO: trigger refresh
|
||||
if (
|
||||
err instanceof kdbxweb.KdbxError &&
|
||||
err.code === kdbxweb.Consts.ErrorCodes.InvalidKey
|
||||
) {
|
||||
ctx.logger.info('Remote key changed, request to enter new key');
|
||||
// Events.emit('remote-key-changed', { file }); // TODO: show the key change screen
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
if (ctx.stat?.rev) {
|
||||
ctx.logger.info('Update rev in file info');
|
||||
ctx.fileInfo.rev = ctx.stat.rev;
|
||||
}
|
||||
ctx.file.syncDate = new Date();
|
||||
if (ctx.file.modified) {
|
||||
ctx.logger.info('Updated sync date, saving modified file');
|
||||
await this.saveToCacheAndStorage(ctx);
|
||||
} else if (ctx.file.dirty) {
|
||||
if (AppSettings.disableOfflineStorage) {
|
||||
ctx.logger.info('File is dirty and cache is disabled');
|
||||
throw new Error('File is dirty and cache is disabled');
|
||||
}
|
||||
ctx.logger.info('Saving not modified dirty file to cache');
|
||||
await Storage.cache.save(ctx.fileInfo.id, storageLoadRes.data);
|
||||
ctx.file.dirty = false;
|
||||
ctx.logger.info('Complete, remove dirty flag');
|
||||
} else {
|
||||
ctx.logger.info('Complete, no changes');
|
||||
}
|
||||
}
|
||||
|
||||
private async backupFile(file: File, data: ArrayBuffer): Promise<void> {
|
||||
if (file.backupInProgress) {
|
||||
return;
|
||||
}
|
||||
file.backupInProgress = true;
|
||||
try {
|
||||
const opts = file.opts;
|
||||
const backup = file.backup;
|
||||
const logger = new Logger('backup', file.name);
|
||||
if (!backup || !backup.storage || !backup.path) {
|
||||
throw new Error('Invalid backup settings');
|
||||
}
|
||||
const storage = Storage.get(backup.storage);
|
||||
if (!storage) {
|
||||
throw new Error(`Invalid backup storage: ${backup.storage}`);
|
||||
}
|
||||
|
||||
let path = backup.path.replace('{date}', DateFormat.dtStrFs(new Date()));
|
||||
logger.info('Backup file to', backup.storage, path);
|
||||
|
||||
let folderPath = UrlFormat.fileToDir(path);
|
||||
if (storage.getPathForName) {
|
||||
folderPath = storage.getPathForName(folderPath).replace('.kdbx', '');
|
||||
}
|
||||
try {
|
||||
await storage.stat(folderPath, opts);
|
||||
logger.info('Backup folder exists, saving');
|
||||
} catch (err) {
|
||||
if (err instanceof StorageFileNotFoundError) {
|
||||
logger.info('Backup folder does not exist');
|
||||
if (!storage.mkdir) {
|
||||
throw new Error('Mkdir not supported by ' + backup.storage);
|
||||
}
|
||||
try {
|
||||
await storage.mkdir(folderPath);
|
||||
logger.info('Backup folder created');
|
||||
} catch (err) {
|
||||
logger.error('Error creating backup folder', err);
|
||||
throw new Error('Error creating backup folder');
|
||||
}
|
||||
} else {
|
||||
logger.error('Stat folder error', err);
|
||||
throw new Error('Cannot stat backup folder');
|
||||
}
|
||||
}
|
||||
|
||||
if (storage.getPathForName) {
|
||||
path = storage.getPathForName(path);
|
||||
}
|
||||
try {
|
||||
await storage.save(path, data, opts);
|
||||
} catch (err) {
|
||||
logger.error('Backup error', err);
|
||||
throw err;
|
||||
}
|
||||
|
||||
logger.info('Backup complete');
|
||||
backup.lastTime = Date.now();
|
||||
file.backup = backup;
|
||||
|
||||
const fileInfo = FileManager.getFileInfoById(file.id);
|
||||
if (fileInfo) {
|
||||
fileInfo.backup = backup;
|
||||
}
|
||||
} finally {
|
||||
file.backupInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
private scheduleBackupFile(file: File, data: ArrayBuffer): void {
|
||||
const backup = file.backup;
|
||||
if (!backup || !backup.enabled || file.backupInProgress) {
|
||||
return;
|
||||
}
|
||||
const logger = new Logger('backup', file.name);
|
||||
let needBackup = false;
|
||||
if (!backup.lastTime) {
|
||||
needBackup = true;
|
||||
logger.info('No last backup time, backup now');
|
||||
} else {
|
||||
const dt = new Date(backup.lastTime);
|
||||
switch (backup.schedule) {
|
||||
case '0':
|
||||
break;
|
||||
case '1d':
|
||||
dt.setDate(dt.getDate() + 1);
|
||||
break;
|
||||
case '1w':
|
||||
dt.setDate(dt.getDate() + 7);
|
||||
break;
|
||||
case '1m':
|
||||
dt.setMonth(dt.getMonth() + 1);
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
if (dt.getTime() <= Date.now()) {
|
||||
needBackup = true;
|
||||
}
|
||||
|
||||
const lastTimeStr = DateFormat.dtStrFs(backup.lastTime);
|
||||
const dtStr = DateFormat.dtStrFs(dt);
|
||||
logger.info(
|
||||
`Last backup time: ${lastTimeStr}, schedule: ${backup.schedule}, ` +
|
||||
`next time: ${dtStr}, ${needBackup ? 'backup now' : 'skip backup'}`
|
||||
);
|
||||
}
|
||||
|
||||
const fileInfo = FileManager.getFileInfoById(file.id);
|
||||
if (fileInfo) {
|
||||
fileInfo.backup = backup;
|
||||
}
|
||||
if (needBackup) {
|
||||
this.backupFile(file, data).catch(noop);
|
||||
}
|
||||
}
|
||||
|
||||
deleteAllCachedFiles(): void {
|
||||
for (const fileInfo of FileManager.fileInfos) {
|
||||
if (fileInfo.storage && !fileInfo.modified) {
|
||||
Storage.cache.remove(fileInfo.id).catch(noop);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deleteAllStoredTokens(): void {
|
||||
for (const storage of Object.values(Storage.getAll())) {
|
||||
storage.deleteStoredToken();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const instance = new FileController();
|
||||
|
||||
export { instance as FileController };
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Logger } from 'util/logger';
|
||||
|
||||
const logger = new Logger('focus-manager');
|
||||
|
||||
class FocusManager {
|
||||
private readonly _modalStack: string[] = [];
|
||||
|
||||
get modal(): string | undefined {
|
||||
return this._modalStack[this._modalStack.length - 1];
|
||||
}
|
||||
|
||||
pushModal(value: string): void {
|
||||
this._modalStack.push(value);
|
||||
logger.debug('Push modal', value);
|
||||
}
|
||||
|
||||
popModal(): string | undefined {
|
||||
const lastModal = this._modalStack.pop();
|
||||
logger.debug('Pop modal', lastModal, '=>', this._modalStack[this._modalStack.length - 1]);
|
||||
return lastModal;
|
||||
}
|
||||
}
|
||||
|
||||
const instance = new FocusManager();
|
||||
|
||||
export { instance as FocusManager };
|
||||
@@ -0,0 +1,182 @@
|
||||
import { AppSettings } from 'models/app-settings';
|
||||
import { Locale } from 'util/locale';
|
||||
import {
|
||||
PasswordGeneratorAppSetting,
|
||||
PasswordGeneratorOptions,
|
||||
PasswordGeneratorPreset,
|
||||
PasswordGeneratorCustomPreset
|
||||
} from 'util/generators/password-generator';
|
||||
|
||||
export const GeneratorPresets = {
|
||||
get defaultPreset(): PasswordGeneratorPreset {
|
||||
return {
|
||||
name: 'Default',
|
||||
title: Locale.genPresetDefault,
|
||||
builtIn: true,
|
||||
length: 16,
|
||||
upper: true,
|
||||
lower: true,
|
||||
digits: true
|
||||
};
|
||||
},
|
||||
|
||||
get browserExtensionPreset(): PasswordGeneratorOptions {
|
||||
return {
|
||||
name: 'BrowserExtension',
|
||||
length: 20,
|
||||
upper: true,
|
||||
lower: true,
|
||||
special: true,
|
||||
brackets: true,
|
||||
ambiguous: true
|
||||
};
|
||||
},
|
||||
|
||||
get builtIn(): PasswordGeneratorPreset[] {
|
||||
return [
|
||||
this.defaultPreset,
|
||||
{
|
||||
name: 'Pronounceable',
|
||||
title: Locale.genPresetPronounceable,
|
||||
builtIn: true,
|
||||
length: 10,
|
||||
lower: true,
|
||||
upper: true
|
||||
},
|
||||
{
|
||||
name: 'Med',
|
||||
title: Locale.genPresetMed,
|
||||
builtIn: true,
|
||||
length: 16,
|
||||
upper: true,
|
||||
lower: true,
|
||||
digits: true,
|
||||
special: true,
|
||||
brackets: true,
|
||||
ambiguous: true
|
||||
},
|
||||
{
|
||||
name: 'Long',
|
||||
title: Locale.genPresetLong,
|
||||
builtIn: true,
|
||||
length: 32,
|
||||
upper: true,
|
||||
lower: true,
|
||||
digits: true
|
||||
},
|
||||
{ name: 'Pin4', title: Locale.genPresetPin4, builtIn: true, length: 4, digits: true },
|
||||
{
|
||||
name: 'Mac',
|
||||
title: Locale.genPresetMac,
|
||||
builtIn: true,
|
||||
length: 17,
|
||||
include: '0123456789ABCDEF',
|
||||
pattern: 'XX-'
|
||||
},
|
||||
{
|
||||
name: 'Hash128',
|
||||
title: Locale.genPresetHash128,
|
||||
builtIn: true,
|
||||
length: 32,
|
||||
include: '0123456789abcdef'
|
||||
},
|
||||
{
|
||||
name: 'Hash256',
|
||||
title: Locale.genPresetHash256,
|
||||
builtIn: true,
|
||||
length: 64,
|
||||
include: '0123456789abcdef'
|
||||
}
|
||||
];
|
||||
},
|
||||
|
||||
get all(): PasswordGeneratorPreset[] {
|
||||
let presets = this.builtIn;
|
||||
const setting = AppSettings.generatorPresets;
|
||||
if (setting) {
|
||||
if (setting.user) {
|
||||
presets = presets.concat(setting.user.map((item) => ({ ...item })));
|
||||
}
|
||||
let hasDefault = false;
|
||||
presets.forEach((preset) => {
|
||||
if (setting.disabled && setting.disabled[preset.name]) {
|
||||
preset.disabled = true;
|
||||
}
|
||||
if (setting.default === preset.name) {
|
||||
hasDefault = true;
|
||||
preset.default = true;
|
||||
}
|
||||
});
|
||||
if (!hasDefault) {
|
||||
presets[0].default = true;
|
||||
}
|
||||
}
|
||||
return presets;
|
||||
},
|
||||
|
||||
get enabled(): PasswordGeneratorPreset[] {
|
||||
const allPresets = this.all.filter((preset) => !preset.disabled);
|
||||
if (!allPresets.length) {
|
||||
allPresets.push(this.defaultPreset);
|
||||
}
|
||||
return allPresets;
|
||||
},
|
||||
|
||||
getOrCreateSetting(): PasswordGeneratorAppSetting {
|
||||
let setting = AppSettings.generatorPresets;
|
||||
if (!setting) {
|
||||
setting = { user: [], disabled: {} };
|
||||
}
|
||||
return setting;
|
||||
},
|
||||
|
||||
add(preset: PasswordGeneratorCustomPreset): void {
|
||||
const setting = this.getOrCreateSetting();
|
||||
if (preset.name && !setting.user.filter((p) => p.name === preset.name).length) {
|
||||
setting.user.push(preset);
|
||||
this.save(setting);
|
||||
}
|
||||
},
|
||||
|
||||
remove(name: string): void {
|
||||
const setting = this.getOrCreateSetting();
|
||||
setting.user = setting.user.filter((p) => p.name !== name);
|
||||
this.save(setting);
|
||||
},
|
||||
|
||||
setPreset(name: string, props: Partial<PasswordGeneratorCustomPreset>): void {
|
||||
const setting = this.getOrCreateSetting();
|
||||
const preset = setting.user.filter((p) => p.name === name)[0];
|
||||
if (preset) {
|
||||
Object.assign(preset, props);
|
||||
this.save(setting);
|
||||
}
|
||||
},
|
||||
|
||||
setDisabled(name: string, disabled: boolean): void {
|
||||
const setting = this.getOrCreateSetting();
|
||||
if (disabled) {
|
||||
if (!setting.disabled) {
|
||||
setting.disabled = {};
|
||||
}
|
||||
setting.disabled[name] = true;
|
||||
} else if (setting.disabled) {
|
||||
delete setting.disabled[name];
|
||||
}
|
||||
this.save(setting);
|
||||
},
|
||||
|
||||
setDefault(name: string | null): void {
|
||||
const setting = this.getOrCreateSetting();
|
||||
if (name) {
|
||||
setting.default = name;
|
||||
} else {
|
||||
delete setting.default;
|
||||
}
|
||||
this.save(setting);
|
||||
},
|
||||
|
||||
save(setting: PasswordGeneratorAppSetting): void {
|
||||
AppSettings.generatorPresets = { ...setting };
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
import { FileOpener } from 'util/browser/file-opener';
|
||||
import { IconSelectState } from 'models/ui/icon-select-state';
|
||||
import { Logger } from 'util/logger';
|
||||
import { Events } from 'util/events';
|
||||
import { IconUrlFormat } from 'util/formatting/icon-url-format';
|
||||
|
||||
const logger = new Logger('icon-select');
|
||||
|
||||
class IconSelectController {
|
||||
openFile() {
|
||||
FileOpener.open((file) => {
|
||||
FileOpener.readBinary(file)
|
||||
.then((fileData) => {
|
||||
IconSelectState.selectedIconDataUrl =
|
||||
IconUrlFormat.toDataUrl(fileData) ?? undefined;
|
||||
})
|
||||
.catch((ex) => logger.error('Error reading file', ex));
|
||||
}, 'image/*');
|
||||
}
|
||||
|
||||
downloadFavicon(websiteUrl: string): void {
|
||||
if (IconSelectState.downloading) {
|
||||
return;
|
||||
}
|
||||
IconSelectState.setDownloading();
|
||||
const iconUrl = this.getIconUrl(websiteUrl);
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.crossOrigin = 'Anonymous';
|
||||
img.src = iconUrl;
|
||||
img.onload = () => {
|
||||
IconSelectState.downloading = false;
|
||||
logger.debug('Favicon downloaded', iconUrl);
|
||||
const data = this.getIconData(img);
|
||||
logger.debug(`Favicon data extracted: ${data.length} chars`);
|
||||
Events.emit('custom-icon-downloaded', data);
|
||||
};
|
||||
img.onerror = (e) => {
|
||||
logger.error('Favicon download error', iconUrl, e);
|
||||
IconSelectState.setDownloadError();
|
||||
};
|
||||
}
|
||||
|
||||
private getIconUrl(websiteUrl: string): string {
|
||||
const domain = websiteUrl.replace(/^.*:\/+/, '').replace(/\/.*/, '');
|
||||
return 'https://services.keeweb.info/favicon/' + domain;
|
||||
}
|
||||
|
||||
private getIconData(img: HTMLImageElement): string {
|
||||
const size = Math.min(img.width, 32);
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
ctx?.drawImage(img, 0, 0, size, size);
|
||||
|
||||
return canvas.toDataURL().replace(/^.*,/, '');
|
||||
}
|
||||
}
|
||||
|
||||
const instance = new IconSelectController();
|
||||
|
||||
export { instance as IconSelectController };
|
||||
@@ -0,0 +1,314 @@
|
||||
import * as kdbxweb from 'kdbxweb';
|
||||
import { FileManager } from 'models/file-manager';
|
||||
import { Workspace } from 'models/workspace';
|
||||
import { AppSettings } from 'models/app-settings';
|
||||
import { FileOpener } from 'util/browser/file-opener';
|
||||
import { Alerts } from 'comp/ui/alerts';
|
||||
import { Locale } from 'util/locale';
|
||||
import { errorToString } from 'util/fn';
|
||||
import { DropboxChooser } from 'storage/dropbox-chooser';
|
||||
import { Logger } from 'util/logger';
|
||||
import { FileController } from 'comp/app/file-controller';
|
||||
import { OpenState } from 'models/ui/open-state';
|
||||
import { StorageBase } from 'storage/storage-base';
|
||||
import { BrowserAuthStartedError, OAuthRejectedError, StorageListItem } from 'storage/types';
|
||||
import { FunctionComponent, h } from 'preact';
|
||||
import { OpenStorageFileList } from 'ui/open/open-storage-file-list';
|
||||
import { UrlFormat } from 'util/formatting/url-format';
|
||||
|
||||
const logger = new Logger('open');
|
||||
|
||||
class OpenController {
|
||||
open(): void {
|
||||
if (OpenState.busy) {
|
||||
return;
|
||||
}
|
||||
if (OpenState.id && FileManager.getFileById(OpenState.id)) {
|
||||
Workspace.showList();
|
||||
return;
|
||||
}
|
||||
|
||||
OpenState.beginOpen();
|
||||
|
||||
FileController.open(OpenState)
|
||||
.then(() => {
|
||||
Workspace.showList();
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.error('Open error', err);
|
||||
const invalidKey =
|
||||
err instanceof kdbxweb.KdbxError &&
|
||||
err.code === kdbxweb.Consts.ErrorCodes.InvalidKey;
|
||||
OpenState.setOpenError(invalidKey);
|
||||
if (!invalidKey) {
|
||||
// } else if (err.userCanceled) { // TODO: yubikey cancellation
|
||||
// if (err.notFound) { // TODO: handle file not found
|
||||
// err = Locale.openErrorFileNotFound;
|
||||
// }
|
||||
const alertBody = Locale.openErrorDescription;
|
||||
// if (err.maybeTouchIdChanged) { // TODO: handle touch ID
|
||||
// alertBody += '\n' + Locale.openErrorDescriptionMaybeTouchIdChanged;
|
||||
// }
|
||||
Alerts.error({
|
||||
header: Locale.openError,
|
||||
body: alertBody,
|
||||
pre: errorToString(err)
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
selectKeyFileFromDropbox(): void {
|
||||
if (OpenState.busy || !AppSettings.canOpen) {
|
||||
return;
|
||||
}
|
||||
const dropboxChooser = new DropboxChooser((err, res) => {
|
||||
if (!err && res) {
|
||||
OpenState.setKeyFile(res.name, res.data);
|
||||
}
|
||||
});
|
||||
dropboxChooser.choose();
|
||||
}
|
||||
|
||||
chooseFile(): void {
|
||||
if (OpenState.busy || !AppSettings.canOpen) {
|
||||
return;
|
||||
}
|
||||
FileOpener.open((file) => {
|
||||
this.readFile(file).catch((e) => logger.error('Error selecting file', e));
|
||||
});
|
||||
}
|
||||
|
||||
async readFile(file: File): Promise<void> {
|
||||
const fileData = await FileOpener.readBinary(file);
|
||||
const format = OpenController.getOpenFileFormat(file, fileData);
|
||||
switch (format) {
|
||||
case 'kdbx': {
|
||||
const name = file.name.replace(/\.kdbx$/i, '');
|
||||
OpenState.setFile(
|
||||
name,
|
||||
fileData,
|
||||
file.path || undefined,
|
||||
file.path ? 'file' : undefined
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'xml': {
|
||||
const name = file.name.replace(/\.\w+$/i, '');
|
||||
const xml = kdbxweb.ByteUtils.bytesToString(fileData);
|
||||
try {
|
||||
await Workspace.importFileFromXml(name, xml);
|
||||
} catch (e) {
|
||||
logger.error('Error importing XML', e);
|
||||
Alerts.error({
|
||||
header: Locale.openXml,
|
||||
body: Locale.openError,
|
||||
pre: errorToString(e)
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'csv':
|
||||
// TODO: load CSV
|
||||
break;
|
||||
case 'kdb':
|
||||
Alerts.error({
|
||||
header: Locale.openWrongFile,
|
||||
body: Locale.openKdbFileBody
|
||||
});
|
||||
break;
|
||||
default:
|
||||
Alerts.error({
|
||||
header: Locale.openWrongFile,
|
||||
body: Locale.openWrongFileBody
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
chooseKeyFile(): void {
|
||||
if (OpenState.busy || !AppSettings.canOpen) {
|
||||
return;
|
||||
}
|
||||
FileOpener.open((file) => {
|
||||
this.readKeyFile(file).catch((e) => logger.error('Error selecting keyfile', e));
|
||||
});
|
||||
}
|
||||
|
||||
async readKeyFile(file: File): Promise<void> {
|
||||
const keyFileData = await FileOpener.readBinary(file);
|
||||
const path = AppSettings.rememberKeyFiles === 'path' && file.path ? file.path : undefined;
|
||||
OpenState.setKeyFile(file.name, keyFileData, path);
|
||||
}
|
||||
|
||||
readFileAndKeyFile(file: File, keyFile?: File): void {
|
||||
this.readFile(file)
|
||||
.then(() => {
|
||||
if (keyFile) {
|
||||
return this.readKeyFile(keyFile);
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
logger.error('Error reading file and keyfile', e);
|
||||
});
|
||||
}
|
||||
|
||||
readDroppedFiles(files: File[]): void {
|
||||
if (!files.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dataFile = files.find((file) => /\.kdbx$/i.test(file.name));
|
||||
const keyFile = files.find((file) => /\.keyx?$/i.test(file.name));
|
||||
|
||||
if (dataFile) {
|
||||
this.readFileAndKeyFile(dataFile, keyFile);
|
||||
return;
|
||||
}
|
||||
|
||||
this.readFile(files[0]).catch((e) => logger.error('Error reading dropped files', e));
|
||||
}
|
||||
|
||||
listStorage(storage: StorageBase, dir?: string, prevDir?: string): void {
|
||||
if (OpenState.busy || !storage.list) {
|
||||
return;
|
||||
}
|
||||
OpenState.setStorageProgress(storage.name);
|
||||
storage
|
||||
.list(dir)
|
||||
.then((files) => {
|
||||
OpenState.resetStorageProgress();
|
||||
|
||||
if (!files.length) {
|
||||
Alerts.error({
|
||||
header: Locale.openNothingFound,
|
||||
body: Locale.openNothingFoundBody
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const collator = new Intl.Collator(undefined, { sensitivity: 'base' });
|
||||
files.sort((x, y) => {
|
||||
if (x.dir !== y.dir) {
|
||||
return (y.dir ? 1 : 0) - (x.dir ? 1 : 0);
|
||||
}
|
||||
return collator.compare(x.path, y.path);
|
||||
});
|
||||
if (dir) {
|
||||
files.unshift({
|
||||
path: prevDir ?? '',
|
||||
name: '..',
|
||||
dir: true
|
||||
});
|
||||
}
|
||||
|
||||
this.showStorageFileListAlert(storage, files, dir);
|
||||
})
|
||||
.catch((e) => {
|
||||
OpenState.resetStorageProgress();
|
||||
if (e instanceof BrowserAuthStartedError || e instanceof OAuthRejectedError) {
|
||||
return;
|
||||
}
|
||||
if (!Alerts.alertDisplayed) {
|
||||
Alerts.error({
|
||||
header: Locale.openError,
|
||||
body: Locale.openListErrorBody,
|
||||
pre: errorToString(e)
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private showStorageFileListAlert(
|
||||
storage: StorageBase,
|
||||
files: StorageListItem[],
|
||||
currentDir?: string
|
||||
) {
|
||||
const listView: FunctionComponent = () => {
|
||||
const fileSelected = (file: StorageListItem) => {
|
||||
if (file.dir) {
|
||||
this.listStorage(storage, file.path, currentDir);
|
||||
} else {
|
||||
alert.closeWithResult('');
|
||||
OpenState.setStorageFile(storage.name, {
|
||||
path: file.path,
|
||||
name: UrlFormat.getDataFileName(file.name),
|
||||
rev: file.rev
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return h(OpenStorageFileList, {
|
||||
files,
|
||||
fileSelected
|
||||
});
|
||||
};
|
||||
|
||||
const alert = Alerts.alert({
|
||||
header: Locale.openSelectFile,
|
||||
body: Locale.openSelectFileBody,
|
||||
icon: storage.icon || 'file-alt',
|
||||
buttons: [{ result: '', title: Locale.alertCancel }],
|
||||
esc: '',
|
||||
click: '',
|
||||
view: listView
|
||||
});
|
||||
}
|
||||
|
||||
private static getOpenFileFormat(
|
||||
file: File,
|
||||
fileData: ArrayBuffer
|
||||
): 'kdbx' | 'kdb' | 'xml' | 'csv' | undefined {
|
||||
if (fileData.byteLength < 8) {
|
||||
return undefined;
|
||||
}
|
||||
const fileSig = new Uint32Array(fileData, 0, 2);
|
||||
if (fileSig[0] === kdbxweb.Consts.Signatures.FileMagic) {
|
||||
if (fileSig[1] === kdbxweb.Consts.Signatures.Sig2Kdb) {
|
||||
return 'kdb';
|
||||
} else if (fileSig[1] === kdbxweb.Consts.Signatures.Sig2Kdbx) {
|
||||
return 'kdbx';
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
} else if (AppSettings.canImportXml) {
|
||||
try {
|
||||
const str = kdbxweb.ByteUtils.bytesToString(fileSig).trim();
|
||||
if (str.startsWith('<?xml')) {
|
||||
return 'xml';
|
||||
}
|
||||
} catch (e) {}
|
||||
return undefined;
|
||||
} else if (AppSettings.canImportCsv && /\.csv$/i.test(file.name)) {
|
||||
return 'csv';
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private static async showLocalFileAlert(): Promise<void> {
|
||||
if (AppSettings.skipOpenLocalWarn) {
|
||||
return;
|
||||
}
|
||||
const res = await Alerts.alert({
|
||||
header: Locale.openLocalFile,
|
||||
body: Locale.openLocalFileBody,
|
||||
icon: 'file-alt',
|
||||
buttons: [
|
||||
{ result: 'skip', title: Locale.openLocalFileDontShow, error: true },
|
||||
{ result: 'ok', title: Locale.alertOk }
|
||||
],
|
||||
click: '',
|
||||
esc: '',
|
||||
enter: ''
|
||||
}).wait();
|
||||
|
||||
if (res === 'skip') {
|
||||
AppSettings.skipOpenLocalWarn = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const instance = new OpenController();
|
||||
|
||||
export { instance as OpenController };
|
||||
@@ -0,0 +1,189 @@
|
||||
import { Events } from 'util/events';
|
||||
import { Shortcuts } from 'comp/browser/shortcuts';
|
||||
import { Alert, Alerts } from 'comp/ui/alerts';
|
||||
import { Features } from 'util/features';
|
||||
import { Locale } from 'util/locale';
|
||||
import { Logger } from 'util/logger';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const QrCode = require('jsqrcode');
|
||||
|
||||
const logger = new Logger('otp-qr-reader');
|
||||
|
||||
class OtpQrReader {
|
||||
private _alert?: Alert;
|
||||
private _fileInput?: HTMLInputElement;
|
||||
|
||||
constructor() {
|
||||
this.pasteEvent = this.pasteEvent.bind(this);
|
||||
}
|
||||
|
||||
read() {
|
||||
let screenshotKey = Shortcuts.screenshotToClipboardShortcut();
|
||||
if (screenshotKey) {
|
||||
screenshotKey = Locale.detSetupOtpAlertBodyWith.with(screenshotKey);
|
||||
}
|
||||
const pasteKey = Features.isMobile
|
||||
? ''
|
||||
: Locale.detSetupOtpAlertBodyWith.with(Shortcuts.actionShortcutSymbol() + 'V');
|
||||
this.startListenClipboard();
|
||||
const buttons = [
|
||||
{ result: 'manually', title: Locale.detSetupOtpManualButton, silent: true },
|
||||
Alerts.buttons.cancel
|
||||
];
|
||||
if (Features.isMobile) {
|
||||
buttons.unshift({ result: 'select', title: Locale.detSetupOtpScanButton });
|
||||
}
|
||||
const line3 = Features.isMobile
|
||||
? Locale.detSetupOtpAlertBody3Mobile
|
||||
: Locale.detSetupOtpAlertBody3.with(pasteKey || '');
|
||||
this._alert = Alerts.alert({
|
||||
icon: 'qrcode',
|
||||
header: Locale.detSetupOtpAlert,
|
||||
body: [
|
||||
Locale.detSetupOtpAlertBody,
|
||||
Locale.detSetupOtpAlertBody1,
|
||||
Locale.detSetupOtpAlertBody2.with(screenshotKey || ''),
|
||||
line3,
|
||||
Locale.detSetupOtpAlertBody4
|
||||
].join('\n'),
|
||||
esc: '',
|
||||
click: '',
|
||||
enter: '',
|
||||
buttons,
|
||||
complete: (res) => {
|
||||
this._alert = undefined;
|
||||
this.stopListenClipboard();
|
||||
if (res === 'select') {
|
||||
this.selectFile();
|
||||
} else if (res === 'manually') {
|
||||
this.enterManually();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
selectFile() {
|
||||
if (!this._fileInput) {
|
||||
const input = document.createElement('input');
|
||||
input.setAttribute('type', 'file');
|
||||
input.setAttribute('capture', 'camera');
|
||||
input.setAttribute('accept', 'image/*');
|
||||
input.setAttribute('class', 'hide-by-pos');
|
||||
this._fileInput = input;
|
||||
this._fileInput.onchange = () => this.fileSelected();
|
||||
}
|
||||
this._fileInput.click();
|
||||
}
|
||||
|
||||
private fileSelected() {
|
||||
const file = this._fileInput?.files?.[0];
|
||||
if (!file || file.type.indexOf('image') < 0) {
|
||||
return;
|
||||
}
|
||||
this.readFile(file);
|
||||
}
|
||||
|
||||
private startListenClipboard() {
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
document.addEventListener('paste', this.pasteEvent);
|
||||
}
|
||||
|
||||
private stopListenClipboard() {
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
document.removeEventListener('paste', this.pasteEvent);
|
||||
}
|
||||
|
||||
private pasteEvent(e: ClipboardEvent) {
|
||||
const items = e.clipboardData?.items;
|
||||
if (!items) {
|
||||
logger.info('Empty clipboard data');
|
||||
return;
|
||||
}
|
||||
const item = [...items].find(
|
||||
(item) => item.kind === 'file' && item.type.indexOf('image') !== -1
|
||||
);
|
||||
if (!item) {
|
||||
logger.info('Paste without file');
|
||||
return;
|
||||
}
|
||||
logger.info('Reading pasted image', item.type);
|
||||
if (this._alert) {
|
||||
this._alert.change({
|
||||
header: Locale.detOtpImageReading
|
||||
});
|
||||
}
|
||||
const file = item.getAsFile();
|
||||
if (file) {
|
||||
this.readFile(file);
|
||||
} else {
|
||||
logger.info('Empty file');
|
||||
}
|
||||
}
|
||||
|
||||
private readFile(file: File) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
if (typeof reader.result !== 'string') {
|
||||
logger.info('Read not a string');
|
||||
return;
|
||||
}
|
||||
logger.info('Image data loaded');
|
||||
this.readQr(reader.result);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
private readQr(imageData: string) {
|
||||
const image = new Image();
|
||||
image.onload = () => {
|
||||
logger.info('Image format loaded');
|
||||
try {
|
||||
const ts = logger.ts();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment
|
||||
const url = new QrCode(image).decode();
|
||||
logger.info('QR code read', logger.ts(ts));
|
||||
this.removeAlert();
|
||||
if (typeof url === 'string' && url) {
|
||||
Events.emit('qr-url-read', url);
|
||||
} else {
|
||||
logger.error('Error reading QR code');
|
||||
Alerts.error({
|
||||
header: Locale.detOtpQrWrong,
|
||||
body: Locale.detOtpQrWrongBody
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error('Error reading QR code', e);
|
||||
this.removeAlert();
|
||||
Alerts.error({
|
||||
header: Locale.detOtpQrError,
|
||||
body: Locale.detOtpQrErrorBody
|
||||
});
|
||||
}
|
||||
};
|
||||
image.onerror = () => {
|
||||
logger.info('Image load error');
|
||||
this.removeAlert();
|
||||
Alerts.error({
|
||||
header: Locale.detOtpImageError,
|
||||
body: Locale.detOtpImageErrorBody
|
||||
});
|
||||
};
|
||||
image.src = imageData;
|
||||
}
|
||||
|
||||
private enterManually() {
|
||||
Events.emit('qr-enter-manually');
|
||||
}
|
||||
|
||||
private removeAlert() {
|
||||
if (this._alert) {
|
||||
this._alert.closeImmediate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const instance = new OtpQrReader();
|
||||
|
||||
export { instance as OtpQrReader };
|
||||
@@ -0,0 +1,472 @@
|
||||
import * as kdbxweb from 'kdbxweb';
|
||||
import { RuntimeInfo } from 'const/runtime-info';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
import { Links } from 'const/links';
|
||||
import { AppSettings } from 'models/app-settings';
|
||||
import { SemVer } from 'util/data/semver';
|
||||
import { Logger } from 'util/logger';
|
||||
import { SignatureVerifier } from 'util/data/signature-verifier';
|
||||
import { Model } from 'util/model';
|
||||
import { Timeouts } from 'const/timeouts';
|
||||
import { RuntimeData } from 'models/runtime-data';
|
||||
import { noop } from 'util/fn';
|
||||
import { StringFormat } from 'util/formatting/string-format';
|
||||
import { ClientRequestArgs } from 'http';
|
||||
import * as fs from 'fs';
|
||||
import path from 'path';
|
||||
import https from 'https';
|
||||
|
||||
const logger = new Logger('updater');
|
||||
|
||||
class Updater extends Model {
|
||||
readonly enabled = Launcher?.updaterEnabled() ?? false;
|
||||
|
||||
status: 'unknown' | 'ready' | 'checking' | 'error' | 'ok' = 'unknown';
|
||||
|
||||
updateStatus:
|
||||
| 'unknown'
|
||||
| 'found'
|
||||
| 'ready'
|
||||
| 'downloading'
|
||||
| 'extracting'
|
||||
| 'updating'
|
||||
| 'error' = 'unknown';
|
||||
|
||||
updateError?: string;
|
||||
|
||||
private _nextCheckTimeout?: number;
|
||||
private _updateCheckDate = new Date(0);
|
||||
|
||||
init(): void {
|
||||
this.scheduleNextCheck();
|
||||
if (!Launcher && navigator.serviceWorker && !RuntimeInfo.beta && !RuntimeInfo.devMode) {
|
||||
navigator.serviceWorker
|
||||
.register('service-worker.js')
|
||||
.then((reg) => {
|
||||
logger.info('Service worker registered');
|
||||
reg.addEventListener('updatefound', () => {
|
||||
if (reg.active) {
|
||||
logger.info('Service worker found an update');
|
||||
this.updateStatus = 'ready';
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
logger.error('Failed to register a service worker', e);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get updateInProgress(): boolean {
|
||||
return (
|
||||
this.status === 'checking' ||
|
||||
['downloading', 'extracting', 'updating'].includes(this.updateStatus)
|
||||
);
|
||||
}
|
||||
|
||||
scheduleNextCheck(): void {
|
||||
if (this._nextCheckTimeout) {
|
||||
clearTimeout(this._nextCheckTimeout);
|
||||
this._nextCheckTimeout = undefined;
|
||||
}
|
||||
if (!this.enabled || !AppSettings.autoUpdate) {
|
||||
return;
|
||||
}
|
||||
let timeDiff = Timeouts.MinUpdateTimeout;
|
||||
const lastCheckDate = RuntimeData.lastUpdateCheckDate;
|
||||
if (lastCheckDate) {
|
||||
timeDiff = Math.min(
|
||||
Math.max(
|
||||
Timeouts.UpdateInterval + (lastCheckDate.getTime() - Date.now()),
|
||||
Timeouts.MinUpdateTimeout
|
||||
),
|
||||
Timeouts.UpdateInterval
|
||||
);
|
||||
}
|
||||
this._nextCheckTimeout = window.setTimeout(() => {
|
||||
this.check(false).catch(noop);
|
||||
}, timeDiff);
|
||||
logger.info(`Next update check will happen in ${Math.round(timeDiff / 1000)}s`);
|
||||
}
|
||||
|
||||
async check(startedByUser: boolean) {
|
||||
if (!this.enabled || this.updateInProgress) {
|
||||
return;
|
||||
}
|
||||
this.status = 'checking';
|
||||
if (!startedByUser) {
|
||||
// additional protection from broken program logic, to ensure that auto-checks are not performed more than once an hour
|
||||
const diffMs = Date.now() - this._updateCheckDate.getTime();
|
||||
if (isNaN(diffMs) || diffMs < 1000 * 60 * 60) {
|
||||
logger.error(
|
||||
'Prevented update check; last check was performed at',
|
||||
this._updateCheckDate
|
||||
);
|
||||
this.scheduleNextCheck();
|
||||
return;
|
||||
}
|
||||
this._updateCheckDate = new Date();
|
||||
}
|
||||
logger.info('Checking for update...');
|
||||
|
||||
let data: Buffer | undefined;
|
||||
try {
|
||||
const httpRes = await this.httpGet({
|
||||
url: Links.UpdateJson
|
||||
});
|
||||
data = httpRes.data;
|
||||
} catch (e) {
|
||||
logger.error('Update check error', e);
|
||||
RuntimeData.batchSet(() => {
|
||||
RuntimeData.lastUpdateCheckDate = new Date();
|
||||
RuntimeData.lastUpdateCheckError = 'Error checking last version';
|
||||
});
|
||||
this.status = 'error';
|
||||
this.scheduleNextCheck();
|
||||
return;
|
||||
}
|
||||
|
||||
let updateJson: Record<string, unknown>;
|
||||
try {
|
||||
const parsed = JSON.parse(data?.toString() || '') as unknown;
|
||||
if (!parsed || typeof parsed !== 'object') {
|
||||
throw new Error('Empty data');
|
||||
}
|
||||
updateJson = parsed as Record<string, unknown>;
|
||||
} catch (e) {
|
||||
logger.error('Failed to parse JSON', data);
|
||||
return;
|
||||
}
|
||||
|
||||
const updateVersion = updateJson.version;
|
||||
const updateDate = new Date(String(updateJson.date));
|
||||
|
||||
const dt = new Date();
|
||||
logger.info('Update check', updateVersion || 'no version', updateDate);
|
||||
|
||||
if (typeof updateVersion !== 'string' || !updateDate.getTime()) {
|
||||
RuntimeData.batchSet(() => {
|
||||
RuntimeData.lastUpdateCheckDate = dt;
|
||||
RuntimeData.lastUpdateCheckError = 'Failed to parse update information';
|
||||
});
|
||||
this.status = 'error';
|
||||
this.scheduleNextCheck();
|
||||
return;
|
||||
}
|
||||
|
||||
const prevLastVersion = RuntimeData.lastUpdateVersion;
|
||||
RuntimeData.batchSet(() => {
|
||||
RuntimeData.lastUpdateCheckDate = dt;
|
||||
RuntimeData.lastSuccessUpdateCheckDate = dt;
|
||||
RuntimeData.lastUpdateVersionReleaseDate = updateDate;
|
||||
RuntimeData.lastUpdateVersion = updateVersion;
|
||||
RuntimeData.lastUpdateCheckError = undefined;
|
||||
});
|
||||
this.status = 'ok';
|
||||
this.scheduleNextCheck();
|
||||
|
||||
if (prevLastVersion === RuntimeData.lastUpdateVersion && this.updateStatus === 'ready') {
|
||||
logger.info('Waiting for the user to apply downloaded update');
|
||||
return;
|
||||
}
|
||||
if (!startedByUser && AppSettings.autoUpdate === 'install') {
|
||||
await this.update();
|
||||
} else if (SemVer.compareVersions(updateVersion, RuntimeInfo.version) > 0) {
|
||||
this.updateStatus = 'found';
|
||||
}
|
||||
}
|
||||
|
||||
async updateAndRestart() {
|
||||
await this.update();
|
||||
await this.installAndRestart();
|
||||
}
|
||||
|
||||
private async update() {
|
||||
if (!this.enabled) {
|
||||
logger.info('Updater is disabled');
|
||||
return;
|
||||
}
|
||||
const ver = RuntimeData.lastUpdateVersion;
|
||||
if (!ver) {
|
||||
logger.info('No last update version');
|
||||
return;
|
||||
}
|
||||
if (SemVer.compareVersions(RuntimeInfo.version, ver) >= 0) {
|
||||
logger.info('You are using the latest version');
|
||||
return;
|
||||
}
|
||||
|
||||
this.batchSet(() => {
|
||||
this.updateStatus = 'downloading';
|
||||
this.updateError = undefined;
|
||||
});
|
||||
|
||||
logger.info('Downloading update', ver);
|
||||
const updateAssetName = this.getUpdateAssetName(ver);
|
||||
if (!updateAssetName) {
|
||||
logger.error('Empty updater asset name for', process.platform, process.arch);
|
||||
return;
|
||||
}
|
||||
|
||||
const updateUrlBasePath = Links.UpdateBasePath.replace('{ver}', ver);
|
||||
const updateAssetUrl = updateUrlBasePath + updateAssetName;
|
||||
|
||||
let assetFilePath: string;
|
||||
try {
|
||||
const httpRes = await this.httpGet({
|
||||
url: updateAssetUrl,
|
||||
file: updateAssetName,
|
||||
cleanupOldFiles: true,
|
||||
cache: true
|
||||
});
|
||||
if (!httpRes.fileName) {
|
||||
throw new Error('No fileName');
|
||||
}
|
||||
assetFilePath = httpRes.fileName;
|
||||
} catch (e) {
|
||||
logger.error('Error downloading update', e);
|
||||
this.batchSet(() => {
|
||||
this.updateStatus = 'error';
|
||||
this.updateError = 'Error downloading update';
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info('Downloading update signatures');
|
||||
|
||||
let assetFileSignaturePath: string;
|
||||
try {
|
||||
const httpRes = await this.httpGet({
|
||||
url: updateUrlBasePath + 'Verify.sign.sha256',
|
||||
file: updateAssetName + '.sign',
|
||||
cleanupOldFiles: true,
|
||||
cache: true
|
||||
});
|
||||
if (!httpRes.fileName) {
|
||||
throw new Error('No fileName');
|
||||
}
|
||||
assetFileSignaturePath = httpRes.fileName;
|
||||
} catch (e) {
|
||||
logger.error('Error downloading update signatures', e);
|
||||
this.batchSet(() => {
|
||||
this.updateStatus = 'error';
|
||||
this.updateError = 'Error downloading update signatures';
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let valid: boolean;
|
||||
try {
|
||||
valid = await this.verifySignature(assetFilePath, updateAssetName);
|
||||
} catch (err) {
|
||||
this.batchSet(() => {
|
||||
this.updateStatus = 'error';
|
||||
this.updateError = 'Error verifying update signature';
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!valid) {
|
||||
this.batchSet(() => {
|
||||
this.updateStatus = 'error';
|
||||
this.updateError = 'Invalid update signature';
|
||||
});
|
||||
fs.unlink(assetFilePath, noop);
|
||||
fs.unlink(assetFileSignaturePath, noop);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info('Update is ready', assetFilePath);
|
||||
this.batchSet(() => {
|
||||
this.updateStatus = 'ready';
|
||||
this.updateError = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
private async verifySignature(assetFilePath: string, assetName: string): Promise<boolean> {
|
||||
logger.info('Verifying update signature', assetName);
|
||||
const signaturesTxt = await fs.promises.readFile(assetFilePath + '.sign', 'utf8');
|
||||
const assetSignatureLine = signaturesTxt
|
||||
.split('\n')
|
||||
.find((line) => line.endsWith(assetName));
|
||||
if (!assetSignatureLine) {
|
||||
logger.error('Signature not found for asset', assetName);
|
||||
throw new Error('Asset signature not found');
|
||||
}
|
||||
const signature = kdbxweb.ByteUtils.hexToBytes(assetSignatureLine.split(' ')[0]);
|
||||
const fileBytes = fs.readFileSync(assetFilePath);
|
||||
try {
|
||||
const valid = await SignatureVerifier.verify(fileBytes, signature);
|
||||
logger.info(`Update asset signature is ${valid ? 'valid' : 'invalid'}`);
|
||||
return valid;
|
||||
} catch (e) {
|
||||
logger.error('Error verifying signature', e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private getUpdateAssetName(ver: string): string | undefined {
|
||||
const platform = process.platform;
|
||||
const arch = process.arch;
|
||||
switch (platform) {
|
||||
case 'win32':
|
||||
switch (arch) {
|
||||
case 'x64':
|
||||
return `KeeWeb-${ver}.win.x64.exe`;
|
||||
case 'ia32':
|
||||
return `KeeWeb-${ver}.win.ia32.exe`;
|
||||
case 'arm64':
|
||||
return `KeeWeb-${ver}.win.arm64.exe`;
|
||||
}
|
||||
break;
|
||||
case 'darwin':
|
||||
switch (arch) {
|
||||
case 'x64':
|
||||
return `KeeWeb-${ver}.mac.x64.dmg`;
|
||||
case 'arm64':
|
||||
return `KeeWeb-${ver}.mac.arm64.dmg`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async installAndRestart(): Promise<void> {
|
||||
if (!Launcher || !RuntimeData.lastUpdateVersion) {
|
||||
return;
|
||||
}
|
||||
const updateAssetName = this.getUpdateAssetName(RuntimeData.lastUpdateVersion);
|
||||
const updateFilePath = await this.cacheFilePath(updateAssetName);
|
||||
Launcher.requestRestartAndUpdate(updateFilePath);
|
||||
}
|
||||
|
||||
async cacheFilePath(fileName?: string): Promise<string> {
|
||||
const tempPath = await Launcher?.ipcRenderer.invoke('get-temp-path');
|
||||
if (!tempPath) {
|
||||
throw new Error('Failed to get temp path');
|
||||
}
|
||||
return fileName ? path.join(tempPath, fileName) : tempPath;
|
||||
}
|
||||
|
||||
async httpGet(config: {
|
||||
url: string;
|
||||
file?: string;
|
||||
cache?: boolean;
|
||||
cleanupOldFiles?: boolean;
|
||||
noRedirect?: boolean;
|
||||
}): Promise<{ fileName?: string; data?: Buffer }> {
|
||||
let tmpFile: string;
|
||||
if (config.file) {
|
||||
const baseTempPath = await this.cacheFilePath();
|
||||
if (config.cleanupOldFiles) {
|
||||
const allFiles = await fs.promises.readdir(baseTempPath);
|
||||
for (const file of allFiles) {
|
||||
if (
|
||||
file !== config.file &&
|
||||
StringFormat.replaceVersion(file, '0') ===
|
||||
StringFormat.replaceVersion(config.file, '0')
|
||||
) {
|
||||
await fs.promises.unlink(path.join(baseTempPath, file));
|
||||
}
|
||||
}
|
||||
}
|
||||
tmpFile = path.join(baseTempPath, config.file);
|
||||
let tmpFileExists = true;
|
||||
try {
|
||||
await fs.promises.access(tmpFile);
|
||||
} catch {
|
||||
tmpFileExists = false;
|
||||
}
|
||||
if (tmpFileExists) {
|
||||
try {
|
||||
if (config.cache && (await fs.promises.stat(tmpFile)).size > 0) {
|
||||
logger.info('File already downloaded', config.url);
|
||||
return Promise.resolve({ fileName: tmpFile });
|
||||
} else {
|
||||
await fs.promises.unlink(tmpFile);
|
||||
}
|
||||
} catch (e) {
|
||||
fs.promises.unlink(tmpFile).catch(noop);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('GET ' + config.url);
|
||||
|
||||
const proxy = await Launcher?.ipcRenderer.invoke('resolve-proxy', config.url);
|
||||
|
||||
const url = new URL(config.url);
|
||||
const opts: ClientRequestArgs = {
|
||||
host: url.host,
|
||||
port: url.port,
|
||||
path: url.pathname + url.search
|
||||
};
|
||||
opts.headers = { 'User-Agent': navigator.userAgent };
|
||||
|
||||
logger.info(
|
||||
'Request to ' +
|
||||
config.url +
|
||||
' ' +
|
||||
(proxy ? `using proxy ${proxy.host}:${proxy.port}` : 'without proxy')
|
||||
);
|
||||
if (proxy) {
|
||||
opts.headers.Host = url.host;
|
||||
opts.host = proxy.host;
|
||||
opts.port = proxy.port;
|
||||
opts.path = config.url;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
https
|
||||
.get(opts, (res) => {
|
||||
logger.info(`Response from ${config.url}: `, res.statusCode);
|
||||
if (res.statusCode === 200) {
|
||||
if (config.file) {
|
||||
const file = fs.createWriteStream(tmpFile);
|
||||
res.pipe(file);
|
||||
file.on('finish', () => {
|
||||
file.on('close', () => {
|
||||
resolve({ fileName: tmpFile });
|
||||
});
|
||||
file.close();
|
||||
});
|
||||
file.on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
} else {
|
||||
const chunks: Buffer[] = [];
|
||||
res.on('data', (chunk) => {
|
||||
chunks.push(chunk);
|
||||
});
|
||||
res.on('end', () => {
|
||||
resolve({ data: Buffer.concat(chunks) });
|
||||
});
|
||||
}
|
||||
} else if (
|
||||
res.headers.location &&
|
||||
(res.statusCode === 301 || res.statusCode === 302)
|
||||
) {
|
||||
if (config.noRedirect) {
|
||||
return reject(new Error('Too many redirects'));
|
||||
}
|
||||
config.url = res.headers.location;
|
||||
config.noRedirect = true;
|
||||
resolve(this.httpGet(config));
|
||||
} else {
|
||||
reject(new Error(`HTTP status ${res.statusCode ?? 0}`));
|
||||
}
|
||||
})
|
||||
.on('error', (e) => {
|
||||
logger.error('Cannot GET ' + config.url, e);
|
||||
if (tmpFile) {
|
||||
fs.unlink(tmpFile, noop);
|
||||
}
|
||||
reject(e);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const instance = new Updater();
|
||||
|
||||
export { instance as Updater };
|
||||
@@ -1,30 +0,0 @@
|
||||
const FeatureDetector = require('../util/feature-detector');
|
||||
const Storage = require('../storage');
|
||||
|
||||
const AuthReceiver = {
|
||||
receive: function() {
|
||||
const opener = window.opener || window.parent;
|
||||
const message = this.urlArgsToMessage(window.location.href);
|
||||
if (FeatureDetector.isStandalone) {
|
||||
Storage[sessionStorage.authStorage].handleOAuthReturnMessage(message);
|
||||
return false;
|
||||
} else {
|
||||
opener.postMessage(message, window.location.origin);
|
||||
window.close();
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
urlArgsToMessage: function(url) {
|
||||
const message = {};
|
||||
url.split(/[?#&]/g).forEach(part => {
|
||||
const parts = part.split('=');
|
||||
if (parts.length === 2) {
|
||||
message[parts[0]] = parts[1];
|
||||
}
|
||||
});
|
||||
return message;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = AuthReceiver;
|
||||
@@ -0,0 +1,79 @@
|
||||
import { Events } from 'util/events';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
import { AppSettings } from 'models/app-settings';
|
||||
|
||||
interface CopyPasteResult {
|
||||
success: boolean;
|
||||
seconds?: number;
|
||||
}
|
||||
|
||||
const CopyPaste = {
|
||||
copy(text: string): CopyPasteResult {
|
||||
if (Launcher) {
|
||||
Launcher.setClipboardText(text);
|
||||
const clipboardSeconds = AppSettings.clipboardSeconds;
|
||||
if (clipboardSeconds > 0) {
|
||||
const clearClipboard = () => {
|
||||
if (Launcher?.getClipboardText() === text) {
|
||||
Launcher.clearClipboardText();
|
||||
}
|
||||
};
|
||||
Events.on('main-window-will-close', clearClipboard);
|
||||
setTimeout(() => {
|
||||
clearClipboard();
|
||||
Events.off('main-window-will-close', clearClipboard);
|
||||
}, clipboardSeconds * 1000);
|
||||
}
|
||||
return { success: true, seconds: clipboardSeconds };
|
||||
} else {
|
||||
try {
|
||||
if (document.execCommand('copy')) {
|
||||
return { success: true };
|
||||
}
|
||||
} catch (e) {}
|
||||
return { success: false };
|
||||
}
|
||||
},
|
||||
|
||||
createHiddenInput(text: string): void {
|
||||
const hiddenInput = document.createElement('input');
|
||||
hiddenInput.value = text;
|
||||
hiddenInput.classList.add('hide-by-pos');
|
||||
document.body.appendChild(hiddenInput);
|
||||
hiddenInput.selectionStart = 0;
|
||||
hiddenInput.selectionEnd = text.length;
|
||||
hiddenInput.focus();
|
||||
|
||||
const onChange = () => {
|
||||
setTimeout(() => hiddenInput.blur(), 0);
|
||||
};
|
||||
hiddenInput.addEventListener('cut', onChange);
|
||||
hiddenInput.addEventListener('copy', onChange);
|
||||
hiddenInput.addEventListener('paste', onChange);
|
||||
hiddenInput.addEventListener('blur', () => hiddenInput.remove());
|
||||
},
|
||||
|
||||
copyHtml(html: string): boolean {
|
||||
const el = document.createElement('div');
|
||||
el.style.userSelect = 'auto';
|
||||
el.innerHTML = html;
|
||||
document.body.appendChild(el);
|
||||
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(el);
|
||||
const sel = window.getSelection();
|
||||
if (!sel) {
|
||||
el.remove();
|
||||
return false;
|
||||
}
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
|
||||
const result = document.execCommand('copy');
|
||||
|
||||
el.remove();
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
export { CopyPaste };
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Events } from 'util/events';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
import { Features } from 'util/features';
|
||||
|
||||
const FocusDetector = {
|
||||
isFocused: false,
|
||||
detectsFocusWithEvents: false,
|
||||
|
||||
init(): void {
|
||||
this.isFocused = true;
|
||||
this.detectsFocusWithEvents = !Features.isDesktop && !Features.isMobile;
|
||||
if (this.detectsFocusWithEvents) {
|
||||
window.addEventListener('focus', () => {
|
||||
if (!FocusDetector.isFocused) {
|
||||
FocusDetector.isFocused = true;
|
||||
Events.emit('main-window-focus');
|
||||
}
|
||||
});
|
||||
window.addEventListener('blur', () => {
|
||||
if (FocusDetector.isFocused) {
|
||||
FocusDetector.isFocused = false;
|
||||
Events.emit('main-window-blur');
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
hasFocus(): Promise<boolean> {
|
||||
if (this.detectsFocusWithEvents) {
|
||||
return Promise.resolve(this.isFocused);
|
||||
} else if (Launcher) {
|
||||
return Launcher.ipcRenderer.invoke('is-app-focused');
|
||||
} else {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export { FocusDetector };
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Events } from 'util/events';
|
||||
import { AppSettings } from 'models/app-settings';
|
||||
|
||||
const IdleTracker = {
|
||||
actionTime: Date.now(),
|
||||
init(): void {
|
||||
setInterval(this.checkIdle.bind(this), 1000 * 60);
|
||||
},
|
||||
checkIdle(): void {
|
||||
const idleMinutes = (Date.now() - this.actionTime) / 1000 / 60;
|
||||
const maxIdleMinutes = AppSettings.idleMinutes;
|
||||
if (maxIdleMinutes && idleMinutes > maxIdleMinutes) {
|
||||
Events.emit('before-user-idle');
|
||||
Events.emit('user-idle');
|
||||
}
|
||||
},
|
||||
regUserAction(): void {
|
||||
this.actionTime = Date.now();
|
||||
}
|
||||
};
|
||||
|
||||
Events.on('power-monitor-resume', () => IdleTracker.checkIdle());
|
||||
|
||||
export { IdleTracker };
|
||||
@@ -0,0 +1,156 @@
|
||||
import { Events } from 'util/events';
|
||||
import { IdleTracker } from 'comp/browser/idle-tracker';
|
||||
import { Keys } from 'const/keys';
|
||||
import { FocusManager } from 'comp/app/focus-manager';
|
||||
|
||||
const shortcutKeyProp = navigator.platform.indexOf('Mac') >= 0 ? 'metaKey' : 'ctrlKey';
|
||||
|
||||
interface ShortcutDef {
|
||||
handler: (e: KeyboardEvent, code: number) => void;
|
||||
shortcut?: number;
|
||||
modal?: string;
|
||||
noPrevent?: boolean;
|
||||
}
|
||||
|
||||
class KeyHandler {
|
||||
SHORTCUT_ACTION = 1;
|
||||
SHORTCUT_OPT = 2;
|
||||
SHORTCUT_SHIFT = 4;
|
||||
|
||||
shortcuts = new Map<Keys, ShortcutDef[]>();
|
||||
|
||||
init() {
|
||||
document.addEventListener('keypress', (e) => this.keypress(e));
|
||||
document.addEventListener('keydown', (e) => this.keydown(e));
|
||||
|
||||
this.shortcuts.set(Keys.DOM_VK_A, [
|
||||
{
|
||||
handler: (e) => this.handleAKey(e),
|
||||
shortcut: this.SHORTCUT_ACTION,
|
||||
modal: '*',
|
||||
noPrevent: true
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
onKey(
|
||||
key: Keys,
|
||||
handler: (e: KeyboardEvent) => void,
|
||||
shortcut?: number,
|
||||
modal?: string,
|
||||
noPrevent?: boolean
|
||||
): () => void {
|
||||
let keyShortcuts = this.shortcuts.get(key);
|
||||
if (!keyShortcuts) {
|
||||
keyShortcuts = [];
|
||||
this.shortcuts.set(key, keyShortcuts);
|
||||
}
|
||||
const def: ShortcutDef = {
|
||||
handler,
|
||||
shortcut,
|
||||
modal,
|
||||
noPrevent
|
||||
};
|
||||
keyShortcuts.push(def);
|
||||
|
||||
return () => {
|
||||
const keyShortcuts = this.shortcuts.get(key);
|
||||
if (keyShortcuts) {
|
||||
const ix = keyShortcuts.indexOf(def);
|
||||
if (ix >= 0) {
|
||||
keyShortcuts.splice(ix, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
isActionKey(e: KeyboardEvent): boolean {
|
||||
return e[shortcutKeyProp];
|
||||
}
|
||||
|
||||
keydown(e: KeyboardEvent): void {
|
||||
IdleTracker.regUserAction();
|
||||
const code = e.keyCode || e.which;
|
||||
const keyShortcuts = this.shortcuts.get(code);
|
||||
if (keyShortcuts && keyShortcuts.length) {
|
||||
for (const sh of keyShortcuts) {
|
||||
if (FocusManager.modal && sh.modal !== FocusManager.modal && sh.modal !== '*') {
|
||||
e.stopPropagation();
|
||||
continue;
|
||||
}
|
||||
const isActionKey = this.isActionKey(e);
|
||||
switch (sh.shortcut) {
|
||||
case this.SHORTCUT_ACTION:
|
||||
if (!isActionKey) {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
case this.SHORTCUT_OPT:
|
||||
if (!e.altKey) {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
case this.SHORTCUT_SHIFT:
|
||||
if (!e.shiftKey) {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
case this.SHORTCUT_ACTION + this.SHORTCUT_OPT:
|
||||
if (!e.altKey || !isActionKey) {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (e.metaKey || e.ctrlKey || e.altKey || e.shiftKey) {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
let immediatePropagationStopped = false;
|
||||
e.stopImmediatePropagation = () => {
|
||||
immediatePropagationStopped = true;
|
||||
KeyboardEvent.prototype.stopImmediatePropagation.call(e);
|
||||
};
|
||||
sh.handler(e, code);
|
||||
if (isActionKey && !sh.noPrevent) {
|
||||
e.preventDefault();
|
||||
}
|
||||
if (immediatePropagationStopped) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
keypress(e: KeyboardEvent): void {
|
||||
if (
|
||||
!FocusManager.modal &&
|
||||
e.which !== Keys.DOM_VK_RETURN &&
|
||||
e.which !== Keys.DOM_VK_ESCAPE &&
|
||||
e.which !== Keys.DOM_VK_TAB &&
|
||||
!e.altKey &&
|
||||
!e.ctrlKey &&
|
||||
!e.metaKey
|
||||
) {
|
||||
Events.emit('keypress', e);
|
||||
} else if (FocusManager.modal) {
|
||||
Events.emit('keypress-modal', e, FocusManager.modal);
|
||||
}
|
||||
}
|
||||
|
||||
reg() {
|
||||
IdleTracker.regUserAction();
|
||||
}
|
||||
|
||||
handleAKey(e: KeyboardEvent): void {
|
||||
if (e.target instanceof HTMLInputElement && ['password', 'text'].includes(e.target.type)) {
|
||||
e.stopImmediatePropagation();
|
||||
} else {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const instance = new KeyHandler();
|
||||
|
||||
export { instance as KeyHandler };
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Events } from 'util/events';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
import { Alerts } from 'comp/ui/alerts';
|
||||
import { Timeouts } from 'const/timeouts';
|
||||
import { Locale } from 'util/locale';
|
||||
|
||||
const PopupNotifier = {
|
||||
init(): void {
|
||||
if (Launcher) {
|
||||
window.open = () => null;
|
||||
} else {
|
||||
const windowOpen = window.open;
|
||||
window.open = function (...args) {
|
||||
const win = windowOpen.apply(window, args);
|
||||
if (win) {
|
||||
PopupNotifier.deferCheckClosed(win);
|
||||
Events.emit('popup-opened', win);
|
||||
} else if (!Alerts.alertDisplayed) {
|
||||
Alerts.error({
|
||||
header: Locale.authPopupRequired,
|
||||
body: Locale.authPopupRequiredBody
|
||||
});
|
||||
}
|
||||
return win;
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
deferCheckClosed(win: Window): void {
|
||||
setTimeout(PopupNotifier.checkClosed.bind(PopupNotifier, win), Timeouts.CheckWindowClosed);
|
||||
},
|
||||
|
||||
checkClosed(win: Window): void {
|
||||
if (win.closed) {
|
||||
setTimeout(
|
||||
PopupNotifier.triggerClosed.bind(PopupNotifier, win),
|
||||
Timeouts.CheckWindowClosed
|
||||
);
|
||||
} else {
|
||||
const loc = PopupNotifier.tryGetLocationSearch(win);
|
||||
if (loc) {
|
||||
try {
|
||||
win.close();
|
||||
} catch {}
|
||||
PopupNotifier.triggerClosed(win, loc);
|
||||
return;
|
||||
}
|
||||
PopupNotifier.deferCheckClosed(win);
|
||||
}
|
||||
},
|
||||
|
||||
tryGetLocationSearch(win: Window): string | undefined {
|
||||
try {
|
||||
if (win.location.host === location.host) {
|
||||
return win.location.search;
|
||||
}
|
||||
} catch {}
|
||||
},
|
||||
|
||||
triggerClosed(window: Window, locationSearch?: string): void {
|
||||
Events.emit('popup-closed', window, locationSearch);
|
||||
}
|
||||
};
|
||||
|
||||
export { PopupNotifier };
|
||||