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 | ||
|
|
1b896f3218 |
@@ -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,39 +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"],
|
||||
"no-mixed-operators": "off",
|
||||
"prefer-promise-reject-errors": "off",
|
||||
"standard/no-callback-literal": "off",
|
||||
"import/no-webpack-loader-syntax": "off",
|
||||
"object-curly-spacing": "off"
|
||||
},
|
||||
"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,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
|
||||
@@ -2,7 +2,7 @@
|
||||
name: Bug report
|
||||
about: Create a report to help us improve KeeWeb
|
||||
title: ''
|
||||
labels: ''
|
||||
labels: 'bug'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
@@ -24,7 +24,7 @@ A clear and concise description of what you expected to happen.
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Environment**
|
||||
Your user-agent (from Settings/Help section)
|
||||
Please copy all version information from Settings/Help
|
||||
|
||||
**Kdbx File**
|
||||
Does it happen on Demo or New database?
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: true
|
||||
@@ -2,7 +2,7 @@
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
labels: 'enhancement'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
@@ -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}'
|
||||
@@ -3,7 +3,7 @@ misc.xml
|
||||
.DS_Store
|
||||
node_modules/
|
||||
*.log
|
||||
dist/
|
||||
/dist/
|
||||
tmp/
|
||||
coverage/
|
||||
keys
|
||||
@@ -14,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"
|
||||
}
|
||||
@@ -2,24 +2,41 @@
|
||||
|
||||
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.
|
||||
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) to create a new issue.
|
||||
[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.
|
||||
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) to discuss any question.
|
||||
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).
|
||||
@@ -27,4 +44,23 @@ Please follow [this guide](https://github.com/keeweb/keeweb/blob/master/.github/
|
||||
## 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) and submit your changes in Markdown format.
|
||||
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).
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
/* eslint-env node */
|
||||
|
||||
const fs = require('fs');
|
||||
const fs = require('fs-extra');
|
||||
const { execSync } = require('child_process');
|
||||
const debug = require('debug');
|
||||
|
||||
const sass = require('node-sass');
|
||||
|
||||
const webpackConfig = require('./webpack.config');
|
||||
const postCssReplaceFont = require('./build/util/postcss-replace-font');
|
||||
const webpackConfig = require('./build/webpack.config');
|
||||
const pkg = require('./package.json');
|
||||
|
||||
module.exports = function(grunt) {
|
||||
debug.enable('electron-notarize');
|
||||
|
||||
module.exports = function (grunt) {
|
||||
require('time-grunt')(grunt);
|
||||
require('load-grunt-tasks')(grunt);
|
||||
|
||||
@@ -22,42 +23,78 @@ module.exports = function(grunt) {
|
||||
|
||||
const dt = date.toISOString().replace(/T.*/, '');
|
||||
const year = date.getFullYear();
|
||||
const minElectronVersionForUpdate = '4.0.1';
|
||||
const zipCommentPlaceholderPart = 'zip_comment_placeholder_that_will_be_replaced_with_hash';
|
||||
const zipCommentPlaceholder = zipCommentPlaceholderPart + '.'.repeat(512 - zipCommentPlaceholderPart.length);
|
||||
const electronVersion = pkg.dependencies.electron.replace(/^\D/, '');
|
||||
const skipSign = grunt.option('skip-sign');
|
||||
const getCodeSignConfig = () =>
|
||||
skipSign ? { identities: {} } : require('./keys/codesign.json');
|
||||
|
||||
let sha = grunt.option('commit-sha');
|
||||
if (!sha) {
|
||||
try {
|
||||
sha = execSync('git rev-parse --short HEAD').toString('utf8').trim();
|
||||
} catch (e) {
|
||||
grunt.warn(
|
||||
"Cannot get commit sha from git. It's recommended to build KeeWeb from a git repo " +
|
||||
'because commit sha is displayed in the UI, however if you would like to build from a folder, ' +
|
||||
'you can override what will be displayed in the UI with --commit-sha=xxx.'
|
||||
);
|
||||
}
|
||||
}
|
||||
grunt.log.writeln(`Building KeeWeb v${pkg.version} (${sha})`);
|
||||
|
||||
const webpackOptions = {
|
||||
date,
|
||||
beta: !!grunt.option('beta'),
|
||||
sha,
|
||||
appleTeamId: '3LE7JZ657W'
|
||||
};
|
||||
|
||||
const windowsAppVersionString = {
|
||||
CompanyName: 'KeeWeb',
|
||||
FileDescription: pkg.description,
|
||||
OriginalFilename: 'KeeWeb.exe',
|
||||
ProductName: 'KeeWeb',
|
||||
InternalName: 'KeeWeb'
|
||||
};
|
||||
|
||||
const appdmgOptions = (arch) => ({
|
||||
title: 'KeeWeb',
|
||||
icon: 'graphics/icon.icns',
|
||||
background: 'graphics/dmg-background.png',
|
||||
'background-color': '#E0E6F9',
|
||||
'icon-size': 80,
|
||||
window: { size: { width: 658, height: 498 } },
|
||||
contents: [
|
||||
{ x: 438, y: 344, type: 'link', path: '/Applications' },
|
||||
{
|
||||
x: 192,
|
||||
y: 344,
|
||||
type: 'file',
|
||||
path: `tmp/desktop/KeeWeb-darwin-${arch}/KeeWeb.app`
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const linuxDependencies = [
|
||||
'libappindicator1',
|
||||
'libgconf-2-4',
|
||||
'gnome-keyring',
|
||||
'libxtst6',
|
||||
'libx11-6',
|
||||
'libatspi2.0-0'
|
||||
];
|
||||
|
||||
grunt.initConfig({
|
||||
gitinfo: {
|
||||
branch: {
|
||||
current: {
|
||||
SHA: 'Current HEAD SHA',
|
||||
shortSHA: 'Current HEAD short SHA',
|
||||
name: 'Current branch name',
|
||||
lastCommitTime: 'Last commit time'
|
||||
}
|
||||
}
|
||||
},
|
||||
noop: { noop: {} },
|
||||
clean: {
|
||||
dist: ['dist', 'tmp'],
|
||||
desktop: ['tmp/desktop', 'dist/desktop'],
|
||||
cordova: ['tmp/cordova', 'dist/cordova']
|
||||
desktop: ['tmp/desktop', 'dist/desktop']
|
||||
},
|
||||
copy: {
|
||||
html: {
|
||||
src: 'app/index.html',
|
||||
dest: 'tmp/index.html',
|
||||
nonull: true
|
||||
},
|
||||
favicon: {
|
||||
src: 'app/favicon.png',
|
||||
dest: 'tmp/favicon.png',
|
||||
nonull: true
|
||||
},
|
||||
icons: {
|
||||
cwd: 'app/icons/',
|
||||
src: ['*.png', '*.svg'],
|
||||
dest: 'tmp/icons/',
|
||||
'content-dist': {
|
||||
cwd: 'app/content/',
|
||||
src: '**',
|
||||
dest: 'dist/',
|
||||
expand: true,
|
||||
nonull: true
|
||||
},
|
||||
@@ -68,13 +105,6 @@ module.exports = function(grunt) {
|
||||
expand: true,
|
||||
nonull: true
|
||||
},
|
||||
manifest: {
|
||||
cwd: 'app/manifest/',
|
||||
src: ['*.json', '*.xml'],
|
||||
dest: 'tmp/',
|
||||
expand: true,
|
||||
nonull: true
|
||||
},
|
||||
'dist-manifest': {
|
||||
cwd: 'app/manifest/',
|
||||
src: ['*.json', '*.xml'],
|
||||
@@ -82,12 +112,10 @@ module.exports = function(grunt) {
|
||||
expand: true,
|
||||
nonull: true
|
||||
},
|
||||
fonts: {
|
||||
src: 'node_modules/font-awesome/fonts/fontawesome-webfont.*',
|
||||
dest: 'tmp/fonts/',
|
||||
nonull: true,
|
||||
expand: true,
|
||||
flatten: true
|
||||
'desktop-html': {
|
||||
src: 'dist/index.html',
|
||||
dest: 'tmp/desktop/app/index.html',
|
||||
nonull: true
|
||||
},
|
||||
'desktop-app-content': {
|
||||
cwd: 'desktop/',
|
||||
@@ -96,38 +124,18 @@ module.exports = function(grunt) {
|
||||
expand: true,
|
||||
nonull: true
|
||||
},
|
||||
'desktop-update': {
|
||||
cwd: 'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app/Contents/Resources/',
|
||||
src: 'app.asar',
|
||||
dest: 'tmp/desktop/update/',
|
||||
expand: true,
|
||||
nonull: true
|
||||
},
|
||||
'desktop-update-helper': {
|
||||
src: ['helper/darwin/KeeWebHelper', 'helper/win32/KeeWebHelper.exe'],
|
||||
dest: 'tmp/desktop/update/',
|
||||
nonull: true
|
||||
},
|
||||
'desktop-windows-helper-ia32': {
|
||||
src: 'helper/win32/KeeWebHelper.exe',
|
||||
dest: 'tmp/desktop/KeeWeb-win32-ia32/Resources/',
|
||||
nonull: true
|
||||
},
|
||||
'desktop-windows-helper-x64': {
|
||||
src: 'helper/win32/KeeWebHelper.exe',
|
||||
dest: 'tmp/desktop/KeeWeb-win32-x64/Resources/',
|
||||
nonull: true
|
||||
},
|
||||
'desktop-darwin-helper-x64': {
|
||||
src: 'helper/darwin/KeeWebHelper',
|
||||
dest: 'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app/Contents/Resources/',
|
||||
nonull: true,
|
||||
options: { mode: '0755' }
|
||||
},
|
||||
'desktop-darwin-installer': {
|
||||
cwd: 'package/osx/KeeWeb Installer.app',
|
||||
dest: 'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app/Contents/Installer/KeeWeb Installer.app',
|
||||
'desktop-darwin-installer-helper-x64': {
|
||||
cwd: 'tmp/desktop/KeeWeb Installer.app',
|
||||
src: '**',
|
||||
dest: 'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app/Contents/Installer/KeeWeb Installer.app',
|
||||
expand: true,
|
||||
nonull: true,
|
||||
options: { mode: true }
|
||||
},
|
||||
'desktop-darwin-installer-helper-arm64': {
|
||||
cwd: 'tmp/desktop/KeeWeb Installer.app',
|
||||
src: '**',
|
||||
dest: 'tmp/desktop/KeeWeb-darwin-arm64/KeeWeb.app/Contents/Installer/KeeWeb Installer.app',
|
||||
expand: true,
|
||||
nonull: true,
|
||||
options: { mode: true }
|
||||
@@ -141,36 +149,104 @@ module.exports = function(grunt) {
|
||||
src: 'tmp/desktop/KeeWeb.win.ia32.exe',
|
||||
dest: `dist/desktop/KeeWeb-${pkg.version}.win.ia32.exe`,
|
||||
nonull: true
|
||||
},
|
||||
'desktop-win32-dist-arm64': {
|
||||
src: 'tmp/desktop/KeeWeb.win.arm64.exe',
|
||||
dest: `dist/desktop/KeeWeb-${pkg.version}.win.arm64.exe`,
|
||||
nonull: true
|
||||
},
|
||||
'native-modules-darwin-x64': {
|
||||
src: 'node_modules/@keeweb/keeweb-native-modules/*-darwin-x64.node',
|
||||
dest: 'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app/Contents/Resources/',
|
||||
nonull: true
|
||||
},
|
||||
'native-modules-darwin-arm64': {
|
||||
src: 'node_modules/@keeweb/keeweb-native-modules/*-darwin-arm64.node',
|
||||
dest: 'tmp/desktop/KeeWeb-darwin-arm64/KeeWeb.app/Contents/Resources/',
|
||||
nonull: true
|
||||
},
|
||||
'native-modules-win32-x64': {
|
||||
src: 'node_modules/@keeweb/keeweb-native-modules/*-win32-x64.node',
|
||||
dest: 'tmp/desktop/KeeWeb-win32-x64/resources/',
|
||||
nonull: true
|
||||
},
|
||||
'native-modules-win32-ia32': {
|
||||
src: 'node_modules/@keeweb/keeweb-native-modules/*-win32-ia32.node',
|
||||
dest: 'tmp/desktop/KeeWeb-win32-ia32/resources/',
|
||||
nonull: true
|
||||
},
|
||||
'native-modules-win32-arm64': {
|
||||
src: 'node_modules/@keeweb/keeweb-native-modules/*-win32-arm64.node',
|
||||
dest: 'tmp/desktop/KeeWeb-win32-arm64/resources/',
|
||||
nonull: true
|
||||
},
|
||||
'native-modules-linux-x64': {
|
||||
src: 'node_modules/@keeweb/keeweb-native-modules/*-linux-x64.node',
|
||||
dest: 'tmp/desktop/keeweb-linux-x64/resources/',
|
||||
nonull: true
|
||||
},
|
||||
'electron-builder-dist-linux-rpm': {
|
||||
src: `tmp/desktop/electron-builder/KeeWeb-${pkg.version}.x86_64.rpm`,
|
||||
dest: `dist/desktop/KeeWeb-${pkg.version}.linux.x86_64.rpm`,
|
||||
nonull: true
|
||||
},
|
||||
'electron-builder-dist-linux-snap': {
|
||||
src: `tmp/desktop/electron-builder/KeeWeb_${pkg.version}_amd64.snap`,
|
||||
dest: `dist/desktop/KeeWeb-${pkg.version}.linux.snap`,
|
||||
nonull: true
|
||||
},
|
||||
'electron-builder-dist-linux-appimage': {
|
||||
src: `tmp/desktop/electron-builder/keeweb-${pkg.version}.AppImage`,
|
||||
dest: `dist/desktop/KeeWeb-${pkg.version}.linux.AppImage`,
|
||||
nonull: true
|
||||
},
|
||||
'darwin-installer-icon': {
|
||||
src: 'graphics/icon.icns',
|
||||
dest: 'tmp/desktop/KeeWeb Installer.app/Contents/Resources/applet.icns',
|
||||
nonull: true
|
||||
},
|
||||
'native-messaging-host-darwin-x64': {
|
||||
src: 'node_modules/@keeweb/keeweb-native-messaging-host/darwin-x64/keeweb-native-messaging-host',
|
||||
dest: 'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app/Contents/MacOS/util/keeweb-native-messaging-host',
|
||||
nonull: true,
|
||||
options: { mode: '0755' }
|
||||
},
|
||||
'native-messaging-host-darwin-arm64': {
|
||||
src: 'node_modules/@keeweb/keeweb-native-messaging-host/darwin-arm64/keeweb-native-messaging-host',
|
||||
dest: 'tmp/desktop/KeeWeb-darwin-arm64/KeeWeb.app/Contents/MacOS/util/keeweb-native-messaging-host',
|
||||
nonull: true,
|
||||
options: { mode: '0755' }
|
||||
},
|
||||
'native-messaging-host-linux-x64': {
|
||||
src: 'node_modules/@keeweb/keeweb-native-messaging-host/linux-x64/keeweb-native-messaging-host',
|
||||
dest: 'tmp/desktop/keeweb-linux-x64/keeweb-native-messaging-host',
|
||||
nonull: true,
|
||||
options: { mode: '0755' }
|
||||
},
|
||||
'native-messaging-host-win32-x64': {
|
||||
src: 'node_modules/@keeweb/keeweb-native-messaging-host/win32-x64/keeweb-native-messaging-host.exe',
|
||||
dest: 'tmp/desktop/KeeWeb-win32-x64/keeweb-native-messaging-host.exe',
|
||||
nonull: true
|
||||
},
|
||||
'native-messaging-host-win32-ia32': {
|
||||
src: 'node_modules/@keeweb/keeweb-native-messaging-host/win32-ia32/keeweb-native-messaging-host.exe',
|
||||
dest: 'tmp/desktop/KeeWeb-win32-ia32/keeweb-native-messaging-host.exe',
|
||||
nonull: true
|
||||
},
|
||||
'native-messaging-host-win32-arm64': {
|
||||
src: 'node_modules/@keeweb/keeweb-native-messaging-host/win32-arm64/keeweb-native-messaging-host.exe',
|
||||
dest: 'tmp/desktop/KeeWeb-win32-arm64/keeweb-native-messaging-host.exe',
|
||||
nonull: true
|
||||
}
|
||||
},
|
||||
eslint: {
|
||||
app: ['app/scripts/**/*.js'],
|
||||
desktop: ['desktop/**/*.js', '!desktop/node_modules/**'],
|
||||
grunt: ['Gruntfile.js', 'grunt/**/*.js']
|
||||
},
|
||||
sass: {
|
||||
options: {
|
||||
sourceMap: false,
|
||||
includePaths: ['./node_modules'],
|
||||
implementation: sass
|
||||
},
|
||||
dist: {
|
||||
files: {
|
||||
'tmp/css/main.css': 'app/styles/main.scss'
|
||||
}
|
||||
}
|
||||
},
|
||||
postcss: {
|
||||
options: {
|
||||
processors: [
|
||||
postCssReplaceFont,
|
||||
require('cssnano')({discardComments: {removeAll: true}})
|
||||
]
|
||||
},
|
||||
dist: {
|
||||
src: 'tmp/css/main.css',
|
||||
dest: 'tmp/css/main.css'
|
||||
}
|
||||
app: ['app/scripts/**/*.js', 'app/scripts/**/*.ts'],
|
||||
test: ['test/**/*.ts'],
|
||||
desktop: ['desktop/**/*.js'],
|
||||
build: ['Gruntfile.js', 'grunt.*.js', 'build/**/*.js', 'webpack.config.js'],
|
||||
plugins: ['plugins/**/*.js'],
|
||||
util: ['util/**/*.js'],
|
||||
installer: ['package/osx/installer.js']
|
||||
},
|
||||
inline: {
|
||||
app: {
|
||||
@@ -178,6 +254,19 @@ module.exports = function(grunt) {
|
||||
dest: 'tmp/app.html'
|
||||
}
|
||||
},
|
||||
'csp-hashes': {
|
||||
options: {
|
||||
algo: 'sha512',
|
||||
expected: {
|
||||
style: 1,
|
||||
script: 1
|
||||
}
|
||||
},
|
||||
app: {
|
||||
src: 'tmp/app.html',
|
||||
dest: 'dist/index.html'
|
||||
}
|
||||
},
|
||||
htmlmin: {
|
||||
options: {
|
||||
removeComments: true,
|
||||
@@ -185,154 +274,176 @@ module.exports = function(grunt) {
|
||||
},
|
||||
app: {
|
||||
files: {
|
||||
'dist/index.html': 'tmp/app.html'
|
||||
'tmp/app.html': 'tmp/app.html'
|
||||
}
|
||||
}
|
||||
},
|
||||
'string-replace': {
|
||||
manifest: {
|
||||
'update-manifest': {
|
||||
options: {
|
||||
replacements: [
|
||||
{ pattern: '# YYYY-MM-DD:v0.0.0', replacement: '# ' + dt + ':v' + pkg.version },
|
||||
{ pattern: '# updmin:v0.0.0', replacement: '# updmin:v' + minElectronVersionForUpdate }
|
||||
{
|
||||
pattern: /"version":\s*".*?"/,
|
||||
replacement: `"version": "${pkg.version}"`
|
||||
},
|
||||
{
|
||||
pattern: /"date":\s*".*?"/,
|
||||
replacement: `"date": "${dt}"`
|
||||
}
|
||||
]
|
||||
},
|
||||
files: { 'dist/manifest.appcache': 'app/manifest.appcache' }
|
||||
files: { 'dist/update.json': 'app/update.json' }
|
||||
},
|
||||
'manifest-html': {
|
||||
options: { replacements: [{ pattern: '<html', replacement: '<html manifest="manifest.appcache"' }] },
|
||||
files: { 'dist/index.html': 'dist/index.html' }
|
||||
},
|
||||
'desktop-html': {
|
||||
options: { replacements: [{ pattern: ' manifest="manifest.appcache"', replacement: '' }] },
|
||||
files: { 'tmp/desktop/app/index.html': 'dist/index.html' }
|
||||
'service-worker': {
|
||||
options: { replacements: [{ pattern: '0.0.0', replacement: pkg.version }] },
|
||||
files: { 'dist/service-worker.js': 'app/service-worker.js' }
|
||||
},
|
||||
'desktop-public-key': {
|
||||
options: { replacements: [{ pattern: '\'@@PUBLIC_KEY_CONTENT\'', replacement:
|
||||
'`' + fs.readFileSync('app/resources/public-key.pem', {encoding: 'utf8'}).trim() + '`' }] },
|
||||
options: {
|
||||
replacements: [
|
||||
{
|
||||
pattern: "'@@PUBLIC_KEY_CONTENT'",
|
||||
replacement:
|
||||
'`' +
|
||||
fs
|
||||
.readFileSync('app/resources/public-key.pem', {
|
||||
encoding: 'utf8'
|
||||
})
|
||||
.trim() +
|
||||
'`'
|
||||
}
|
||||
]
|
||||
},
|
||||
files: { 'tmp/desktop/app/main.js': 'desktop/main.js' }
|
||||
},
|
||||
'cordova-html': {
|
||||
options: { replacements: [{ pattern: '<script', replacement: '<script src="cordova.js"></script><script' }] },
|
||||
files: { 'tmp/cordova/app/index.html': 'dist/index.html' }
|
||||
}
|
||||
},
|
||||
webpack: {
|
||||
js: webpackConfig.config(grunt)
|
||||
},
|
||||
'webpack-dev-server': {
|
||||
options: {
|
||||
webpack: webpackConfig.devServerConfig(grunt),
|
||||
publicPath: '/tmp/js',
|
||||
progress: false
|
||||
},
|
||||
js: {
|
||||
keepalive: true,
|
||||
webpack: {
|
||||
devtool: 'source-map'
|
||||
},
|
||||
port: 8085
|
||||
}
|
||||
},
|
||||
uglify: {
|
||||
options: {
|
||||
preserveComments: false
|
||||
},
|
||||
app: {
|
||||
files: { 'tmp/js/app.js': ['tmp/js/app.js'] }
|
||||
},
|
||||
vendor: {
|
||||
options: {
|
||||
mangle: false,
|
||||
compress: false
|
||||
},
|
||||
files: { 'tmp/js/vendor.js': ['tmp/js/vendor.js'] }
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
options: {
|
||||
interrupt: true,
|
||||
debounceDelay: 500
|
||||
},
|
||||
styles: {
|
||||
files: 'app/styles/**/*.scss',
|
||||
tasks: ['sass']
|
||||
},
|
||||
indexhtml: {
|
||||
files: 'app/index.html',
|
||||
tasks: ['copy:html']
|
||||
}
|
||||
app: webpackConfig.config(webpackOptions)
|
||||
},
|
||||
electron: {
|
||||
options: {
|
||||
name: 'KeeWeb',
|
||||
dir: 'tmp/desktop/app',
|
||||
out: 'tmp/desktop',
|
||||
electronVersion: electronVersion,
|
||||
electronVersion,
|
||||
overwrite: true,
|
||||
asar: true,
|
||||
'appCopyright': `Copyright © ${year} Antelle`,
|
||||
'appVersion': pkg.version,
|
||||
'buildVersion': '<%= gitinfo.local.branch.current.shortSHA %>'
|
||||
appCopyright: `Copyright © ${year} Antelle`,
|
||||
appVersion: pkg.version,
|
||||
buildVersion: sha
|
||||
},
|
||||
linux: {
|
||||
options: {
|
||||
name: 'keeweb',
|
||||
platform: 'linux',
|
||||
arch: ['x64', 'ia32'],
|
||||
arch: 'x64',
|
||||
icon: 'graphics/icon.ico'
|
||||
}
|
||||
},
|
||||
darwin: {
|
||||
'darwin-x64': {
|
||||
options: {
|
||||
platform: 'darwin',
|
||||
arch: ['x64'],
|
||||
arch: 'x64',
|
||||
icon: 'graphics/icon.icns',
|
||||
'appBundleId': 'net.antelle.keeweb',
|
||||
'appCategoryType': 'public.app-category.productivity',
|
||||
'extendInfo': 'package/osx/extend.plist'
|
||||
appBundleId: 'net.antelle.keeweb',
|
||||
appCategoryType: 'public.app-category.productivity',
|
||||
extendInfo: 'package/osx/extend.plist'
|
||||
}
|
||||
},
|
||||
win32: {
|
||||
'darwin-arm64': {
|
||||
options: {
|
||||
platform: 'darwin',
|
||||
arch: 'arm64',
|
||||
icon: 'graphics/icon.icns',
|
||||
appBundleId: 'net.antelle.keeweb',
|
||||
appCategoryType: 'public.app-category.productivity',
|
||||
extendInfo: 'package/osx/extend.plist'
|
||||
}
|
||||
},
|
||||
'win32-x64': {
|
||||
options: {
|
||||
platform: 'win32',
|
||||
arch: ['ia32', 'x64'],
|
||||
arch: 'x64',
|
||||
icon: 'graphics/icon.ico',
|
||||
'buildVersion': pkg.version,
|
||||
'version-string': {
|
||||
'CompanyName': 'KeeWeb',
|
||||
'FileDescription': pkg.description,
|
||||
'OriginalFilename': 'KeeWeb.exe',
|
||||
'ProductName': 'KeeWeb',
|
||||
'InternalName': 'KeeWeb'
|
||||
buildVersion: pkg.version,
|
||||
'version-string': windowsAppVersionString
|
||||
}
|
||||
},
|
||||
'win32-ia32': {
|
||||
options: {
|
||||
platform: 'win32',
|
||||
arch: 'ia32',
|
||||
icon: 'graphics/icon.ico',
|
||||
buildVersion: pkg.version,
|
||||
'version-string': windowsAppVersionString
|
||||
}
|
||||
},
|
||||
'win32-arm64': {
|
||||
options: {
|
||||
platform: 'win32',
|
||||
arch: 'arm64',
|
||||
icon: 'graphics/icon.ico',
|
||||
buildVersion: pkg.version,
|
||||
'version-string': windowsAppVersionString
|
||||
}
|
||||
}
|
||||
},
|
||||
'electron-builder': {
|
||||
linux: {
|
||||
options: {
|
||||
publish: 'never',
|
||||
targets: 'linux',
|
||||
prepackaged: 'tmp/desktop/keeweb-linux-x64',
|
||||
config: {
|
||||
appId: 'net.antelle.keeweb',
|
||||
productName: 'keeweb',
|
||||
copyright: `Copyright © ${year} Antelle`,
|
||||
directories: {
|
||||
output: 'tmp/desktop/electron-builder',
|
||||
app: 'desktop',
|
||||
buildResources: 'graphics'
|
||||
},
|
||||
fileAssociations: {
|
||||
ext: 'kdbx',
|
||||
name: 'KeePass 2 database',
|
||||
mimeType: 'application/x-keepass2'
|
||||
},
|
||||
linux: {
|
||||
target: ['AppImage', 'snap', 'rpm'],
|
||||
category: 'Utility'
|
||||
},
|
||||
rpm: {
|
||||
// depends: linuxDependencies
|
||||
},
|
||||
snap: {
|
||||
stagePackages: linuxDependencies
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
codesign: {
|
||||
app: {
|
||||
options: {
|
||||
identity: 'app',
|
||||
deep: true
|
||||
},
|
||||
src: ['tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app']
|
||||
'electron-patch': {
|
||||
'win32-x64': 'tmp/desktop/KeeWeb-win32-x64/KeeWeb.exe',
|
||||
'win32-ia32': 'tmp/desktop/KeeWeb-win32-ia32/KeeWeb.exe',
|
||||
'win32-arm64': 'tmp/desktop/KeeWeb-win32-arm64/KeeWeb.exe',
|
||||
'darwin-x64': 'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app',
|
||||
'darwin-arm64': 'tmp/desktop/KeeWeb-darwin-arm64/KeeWeb.app',
|
||||
'linux': 'tmp/desktop/KeeWeb-linux-x64/keeweb'
|
||||
},
|
||||
osacompile: {
|
||||
options: {
|
||||
language: 'JavaScript'
|
||||
},
|
||||
dmg: {
|
||||
options: {
|
||||
identity: 'app'
|
||||
},
|
||||
src: [`dist/desktop/KeeWeb-${pkg.version}.mac.dmg`]
|
||||
installer: {
|
||||
files: {
|
||||
'tmp/desktop/KeeWeb Installer.app': 'package/osx/installer.js'
|
||||
}
|
||||
}
|
||||
},
|
||||
compress: {
|
||||
options: {
|
||||
level: 6
|
||||
},
|
||||
'desktop-update': {
|
||||
options: { archive: 'dist/desktop/UpdateDesktop.zip', comment: zipCommentPlaceholder },
|
||||
files: [
|
||||
{ cwd: 'tmp/desktop/update', src: '**', expand: true, nonull: true }
|
||||
]
|
||||
},
|
||||
'win32-x64': {
|
||||
options: { archive: `dist/desktop/KeeWeb-${pkg.version}.win.x64.zip` },
|
||||
files: [{ cwd: 'tmp/desktop/KeeWeb-win32-x64', src: '**', expand: true }]
|
||||
@@ -341,39 +452,33 @@ module.exports = function(grunt) {
|
||||
options: { archive: `dist/desktop/KeeWeb-${pkg.version}.win.ia32.zip` },
|
||||
files: [{ cwd: 'tmp/desktop/KeeWeb-win32-ia32', src: '**', expand: true }]
|
||||
},
|
||||
'win32-arm64': {
|
||||
options: { archive: `dist/desktop/KeeWeb-${pkg.version}.win.arm64.zip` },
|
||||
files: [{ cwd: 'tmp/desktop/KeeWeb-win32-arm64', src: '**', expand: true }]
|
||||
},
|
||||
'linux-x64': {
|
||||
options: { archive: `dist/desktop/KeeWeb-${pkg.version}.linux.x64.zip` },
|
||||
files: [{ cwd: 'tmp/desktop/KeeWeb-linux-x64', src: '**', expand: true },
|
||||
{ cwd: 'graphics', src: '128x128.png', nonull: true, expand: true }]
|
||||
},
|
||||
'linux-ia32': {
|
||||
options: { archive: `dist/desktop/KeeWeb-${pkg.version}.linux.ia32.zip` },
|
||||
files: [{ cwd: 'tmp/desktop/KeeWeb-linux-ia32', src: '**', expand: true },
|
||||
{ cwd: 'graphics', src: '128x128.png', nonull: true, expand: true }]
|
||||
files: [
|
||||
{ cwd: 'tmp/desktop/keeweb-linux-x64', src: '**', expand: true },
|
||||
{ cwd: 'graphics', src: '128x128.png', nonull: true, expand: true }
|
||||
]
|
||||
}
|
||||
},
|
||||
appdmg: {
|
||||
options: {
|
||||
title: 'KeeWeb',
|
||||
icon: 'graphics/icon.icns',
|
||||
background: 'graphics/background.png',
|
||||
'background-color': '#E0E6F9',
|
||||
'icon-size': 80,
|
||||
window: { size: { width: 658, height: 498 } },
|
||||
contents: [
|
||||
{ x: 438, y: 344, type: 'link', path: '/Applications' },
|
||||
{ x: 192, y: 344, type: 'file', path: 'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app' }
|
||||
]
|
||||
x64: {
|
||||
options: appdmgOptions('x64'),
|
||||
dest: `dist/desktop/KeeWeb-${pkg.version}.mac.x64.dmg`
|
||||
},
|
||||
app: {
|
||||
dest: `dist/desktop/KeeWeb-${pkg.version}.mac.dmg`
|
||||
arm64: {
|
||||
options: appdmgOptions('arm64'),
|
||||
dest: `dist/desktop/KeeWeb-${pkg.version}.mac.arm64.dmg`
|
||||
}
|
||||
},
|
||||
nsis: {
|
||||
options: {
|
||||
vars: {
|
||||
version: pkg.version,
|
||||
rev: function() { return grunt.config.get('gitinfo.local.branch.current.shortSHA'); },
|
||||
rev: sha,
|
||||
homepage: pkg.homepage
|
||||
}
|
||||
},
|
||||
@@ -404,6 +509,28 @@ module.exports = function(grunt) {
|
||||
arch: 'ia32',
|
||||
output: 'tmp/desktop/KeeWeb-win32-ia32/uninst.exe'
|
||||
}
|
||||
},
|
||||
'win32-arm64': {
|
||||
options: {
|
||||
installScript: 'package/nsis/main.nsi',
|
||||
arch: 'arm64',
|
||||
output: 'tmp/desktop/KeeWeb.win.arm64.exe'
|
||||
}
|
||||
},
|
||||
'win32-un-arm64': {
|
||||
options: {
|
||||
installScript: 'package/nsis/main-un.nsi',
|
||||
arch: 'arm64',
|
||||
output: 'tmp/desktop/KeeWeb-win32-arm64/uninst.exe'
|
||||
}
|
||||
}
|
||||
},
|
||||
chmod: {
|
||||
'linux-desktop-x64': {
|
||||
options: {
|
||||
mode: '4755'
|
||||
},
|
||||
src: ['tmp/desktop/keeweb-linux-x64/chrome-sandbox']
|
||||
}
|
||||
},
|
||||
deb: {
|
||||
@@ -415,7 +542,7 @@ module.exports = function(grunt) {
|
||||
description: pkg.description,
|
||||
author: pkg.author,
|
||||
homepage: pkg.homepage,
|
||||
rev: function() { return grunt.config.get('gitinfo.local.branch.current.shortSHA'); }
|
||||
rev: sha
|
||||
}
|
||||
},
|
||||
'linux-x64': {
|
||||
@@ -425,7 +552,7 @@ module.exports = function(grunt) {
|
||||
pkgName: `KeeWeb-${pkg.version}.linux.x64.deb`,
|
||||
targetDir: 'dist/desktop',
|
||||
appName: 'KeeWeb',
|
||||
depends: 'libappindicator1, libgconf2-4',
|
||||
depends: linuxDependencies.join(', '),
|
||||
scripts: {
|
||||
postinst: 'package/deb/scripts/postinst'
|
||||
}
|
||||
@@ -433,82 +560,86 @@ module.exports = function(grunt) {
|
||||
},
|
||||
files: [
|
||||
{ cwd: 'package/deb/usr', src: '**', dest: '/usr', expand: true, nonull: true },
|
||||
{ cwd: 'tmp/desktop/KeeWeb-linux-x64/', src: '**', dest: '/opt/keeweb-desktop', expand: true, nonull: true },
|
||||
{ src: 'graphics/128x128.png', dest: '/usr/share/icons/hicolor/128x128/apps/keeweb.png', nonull: true }
|
||||
{
|
||||
cwd: 'tmp/desktop/keeweb-linux-x64/',
|
||||
src: '**',
|
||||
dest: '/usr/share/keeweb-desktop',
|
||||
expand: true,
|
||||
nonull: true
|
||||
},
|
||||
{
|
||||
src: 'graphics/128x128.png',
|
||||
dest: '/usr/share/icons/hicolor/128x128/apps/keeweb.png',
|
||||
nonull: true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
'osx-sign': {
|
||||
options: {
|
||||
get identity() {
|
||||
return getCodeSignConfig().identities.app;
|
||||
},
|
||||
hardenedRuntime: true,
|
||||
entitlements: 'package/osx/entitlements.plist',
|
||||
'entitlements-inherit': 'package/osx/entitlements-inherit.plist',
|
||||
'gatekeeper-assess': false
|
||||
},
|
||||
'linux-ia32': {
|
||||
'desktop-x64': {
|
||||
options: {
|
||||
info: {
|
||||
arch: 'i386',
|
||||
pkgName: `KeeWeb-${pkg.version}.linux.ia32.deb`,
|
||||
targetDir: 'dist/desktop',
|
||||
appName: 'KeeWeb',
|
||||
depends: 'libappindicator1, libgconf2-4',
|
||||
scripts: {
|
||||
postinst: 'package/deb/scripts/postinst'
|
||||
}
|
||||
}
|
||||
'provisioning-profile': './keys/keeweb.provisionprofile'
|
||||
},
|
||||
files: [
|
||||
{ cwd: 'package/deb/usr', src: '**', dest: '/usr', expand: true, nonull: true },
|
||||
{ cwd: 'tmp/desktop/KeeWeb-linux-ia32/', src: '**', dest: '/opt/keeweb-desktop', expand: true, nonull: true },
|
||||
{ src: 'graphics/128x128.png', dest: '/usr/share/icons/hicolor/128x128/apps/keeweb.png', nonull: true }
|
||||
]
|
||||
src: 'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app'
|
||||
},
|
||||
'desktop-arm64': {
|
||||
options: {
|
||||
'provisioning-profile': './keys/keeweb.provisionprofile'
|
||||
},
|
||||
src: 'tmp/desktop/KeeWeb-darwin-arm64/KeeWeb.app'
|
||||
},
|
||||
'installer': {
|
||||
src: 'tmp/desktop/KeeWeb Installer.app'
|
||||
}
|
||||
},
|
||||
'sign-archive': {
|
||||
'desktop-update': {
|
||||
options: {
|
||||
file: 'dist/desktop/UpdateDesktop.zip',
|
||||
signature: zipCommentPlaceholder
|
||||
}
|
||||
}
|
||||
},
|
||||
'sign-desktop-files': {
|
||||
'desktop-update': {
|
||||
options: {
|
||||
path: 'tmp/desktop/update'
|
||||
}
|
||||
}
|
||||
},
|
||||
'validate-desktop-update': {
|
||||
desktop: {
|
||||
options: {
|
||||
file: 'dist/desktop/UpdateDesktop.zip',
|
||||
expected: [
|
||||
'app.asar',
|
||||
'helper/darwin/KeeWebHelper',
|
||||
'helper/win32/KeeWebHelper.exe'
|
||||
],
|
||||
expectedCount: 7,
|
||||
publicKey: 'app/resources/public-key.pem'
|
||||
}
|
||||
}
|
||||
},
|
||||
'sign-html': {
|
||||
'app': {
|
||||
options: {
|
||||
file: 'dist/index.html',
|
||||
skip: grunt.option('skip-sign')
|
||||
notarize: {
|
||||
options: {
|
||||
appBundleId: 'net.antelle.keeweb',
|
||||
get appleId() {
|
||||
return getCodeSignConfig().appleId;
|
||||
},
|
||||
appleIdPassword: '@keychain:AC_PASSWORD',
|
||||
get ascProvider() {
|
||||
return getCodeSignConfig().teamId;
|
||||
}
|
||||
},
|
||||
'desktop-x64': {
|
||||
src: 'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app'
|
||||
},
|
||||
'desktop-arm64': {
|
||||
src: 'tmp/desktop/KeeWeb-darwin-arm64/KeeWeb.app'
|
||||
}
|
||||
},
|
||||
'sign-exe': {
|
||||
options: {
|
||||
spc: 'keys/keeweb.spc',
|
||||
key: '01',
|
||||
algo: 'sha256',
|
||||
url: pkg.homepage
|
||||
url: pkg.homepage,
|
||||
get windows() {
|
||||
return getCodeSignConfig().windows;
|
||||
},
|
||||
get certHash() {
|
||||
return getCodeSignConfig().microsoftCertHash;
|
||||
}
|
||||
},
|
||||
'win32-build-x64': {
|
||||
options: {
|
||||
files: {
|
||||
'tmp/desktop/KeeWeb-win32-x64/KeeWeb.exe': 'KeeWeb',
|
||||
'tmp/desktop/KeeWeb-win32-x64/keeweb-native-messaging-host.exe':
|
||||
'KeeWeb Native Messaging Host',
|
||||
'tmp/desktop/KeeWeb-win32-x64/ffmpeg.dll': '',
|
||||
'tmp/desktop/KeeWeb-win32-x64/libEGL.dll': 'ANGLE libEGL Dynamic Link Library',
|
||||
'tmp/desktop/KeeWeb-win32-x64/libGLESv2.dll': 'ANGLE libGLESv2 Dynamic Link Library',
|
||||
'tmp/desktop/KeeWeb-win32-x64/osmesa.dll': ''
|
||||
'tmp/desktop/KeeWeb-win32-x64/libEGL.dll':
|
||||
'ANGLE libEGL Dynamic Link Library',
|
||||
'tmp/desktop/KeeWeb-win32-x64/libGLESv2.dll':
|
||||
'ANGLE libGLESv2 Dynamic Link Library'
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -516,10 +647,27 @@ module.exports = function(grunt) {
|
||||
options: {
|
||||
files: {
|
||||
'tmp/desktop/KeeWeb-win32-ia32/KeeWeb.exe': 'KeeWeb',
|
||||
'tmp/desktop/KeeWeb-win32-ia32/keeweb-native-messaging-host.exe':
|
||||
'KeeWeb Native Messaging Host',
|
||||
'tmp/desktop/KeeWeb-win32-ia32/ffmpeg.dll': '',
|
||||
'tmp/desktop/KeeWeb-win32-ia32/libEGL.dll': 'ANGLE libEGL Dynamic Link Library',
|
||||
'tmp/desktop/KeeWeb-win32-ia32/libGLESv2.dll': 'ANGLE libGLESv2 Dynamic Link Library',
|
||||
'tmp/desktop/KeeWeb-win32-ia32/osmesa.dll': ''
|
||||
'tmp/desktop/KeeWeb-win32-ia32/libEGL.dll':
|
||||
'ANGLE libEGL Dynamic Link Library',
|
||||
'tmp/desktop/KeeWeb-win32-ia32/libGLESv2.dll':
|
||||
'ANGLE libGLESv2 Dynamic Link Library'
|
||||
}
|
||||
}
|
||||
},
|
||||
'win32-build-arm64': {
|
||||
options: {
|
||||
files: {
|
||||
'tmp/desktop/KeeWeb-win32-arm64/KeeWeb.exe': 'KeeWeb',
|
||||
'tmp/desktop/KeeWeb-win32-arm64/keeweb-native-messaging-host.exe':
|
||||
'KeeWeb Native Messaging Host',
|
||||
'tmp/desktop/KeeWeb-win32-arm64/ffmpeg.dll': '',
|
||||
'tmp/desktop/KeeWeb-win32-arm64/libEGL.dll':
|
||||
'ANGLE libEGL Dynamic Link Library',
|
||||
'tmp/desktop/KeeWeb-win32-arm64/libGLESv2.dll':
|
||||
'ANGLE libGLESv2 Dynamic Link Library'
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -537,6 +685,13 @@ module.exports = function(grunt) {
|
||||
}
|
||||
}
|
||||
},
|
||||
'win32-uninst-arm64': {
|
||||
options: {
|
||||
files: {
|
||||
'tmp/desktop/KeeWeb-win32-arm64/uninst.exe': 'KeeWeb Uninstaller'
|
||||
}
|
||||
}
|
||||
},
|
||||
'win32-installer-x64': {
|
||||
options: {
|
||||
files: {
|
||||
@@ -550,26 +705,34 @@ module.exports = function(grunt) {
|
||||
'tmp/desktop/KeeWeb.win.ia32.exe': 'KeeWeb Setup'
|
||||
}
|
||||
}
|
||||
},
|
||||
'win32-installer-arm64': {
|
||||
options: {
|
||||
files: {
|
||||
'tmp/desktop/KeeWeb.win.arm64.exe': 'KeeWeb Setup'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'concurrent': {
|
||||
options: {
|
||||
logConcurrentOutput: true
|
||||
},
|
||||
'dev-server': [
|
||||
'watch:styles',
|
||||
'webpack-dev-server'
|
||||
]
|
||||
},
|
||||
'sign-dist': {
|
||||
'dist': {
|
||||
dist: {
|
||||
options: {
|
||||
sign: 'dist/desktop/Verify.sign.sha256'
|
||||
},
|
||||
files: {
|
||||
'dist/desktop/Verify.sha256': ['dist/desktop/KeeWeb-*', 'dist/desktop/UpdateDesktop.zip']
|
||||
'dist/desktop/Verify.sha256': ['dist/desktop/KeeWeb-*']
|
||||
}
|
||||
}
|
||||
},
|
||||
virustotal: {
|
||||
options: {
|
||||
prefix: `keeweb.v${pkg.version}-${sha}.`,
|
||||
timeout: 10 * 60 * 1000,
|
||||
get apiKey() {
|
||||
return require('./keys/virus-total.json').apiKey;
|
||||
}
|
||||
},
|
||||
html: 'dist/index.html'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Antelle
|
||||
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
|
||||
|
||||
@@ -5,22 +5,22 @@ 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).
|
||||
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,47 +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 easiest way to clone all KeeWeb repos is:
|
||||
```bash
|
||||
curl https://raw.githubusercontent.com/keeweb/keeweb/develop/dev-env.sh | bash -
|
||||
```
|
||||
|
||||
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 macOS as it builds dmg; requires wine.
|
||||
Also, a hardware token is required.
|
||||
To run Electron app without building an installer, build the app with `grunt` and start it this way:
|
||||
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
|
||||
grunt dev
|
||||
npm run-script electron
|
||||
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`:
|
||||
|
||||
```
|
||||
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:
|
||||
[<img src="img/paypal-donate.png" alt="Donate with PayPal" width="100">](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:
|
||||
|
||||
- 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 |
|
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 685 B |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 1.7 KiB |
@@ -1 +1 @@
|
||||
<svg version="1" xmlns="http://www.w3.org/2000/svg" width="922.667" height="922.667" viewBox="0 0 692.000000 692.000000"><path d="M266.9 57.4c-.2.2-4 .6-8.3 1-4.4.4-8.2.8-8.5 1-.3.2-2.7.7-5.3 1-5.5.7-21.6 4.8-29.8 7.5-46.3 15.4-93.3 48.9-126.5 90.1-25.7 31.8-46.8 74.8-52.9 107.4-.5 2.8-1.5 8.3-2.1 12.1-1.9 11.6-2.6 35.9-1.3 45 .3 2.7.9 7 1.2 9.5 3.3 27.4 16.9 55.4 36.1 74.5 12.1 12.1 27.3 22.7 40 27.8 3.9 1.6 7.9 3.3 9 3.7 1.1.5 4.3 1.5 7 2.4 4.4 1.3 7.1 1.9 18.5 4.1 12.1 2.4 42.4 2.4 56.5 0 1.7-.3 4.4-.7 6-1 1.7-.3 4.1-.7 5.5-1 1.4-.3 3.7-.7 5.1-1 4.5-.8 19.1-5.2 28.4-8.6 16.8-6.1 42.9-20.2 59.4-32.2l5.3-3.9 130.7 130.5C567.7 654 571.7 657.8 577.5 660c6.2 2.2 15.3 2.9 21.4 1.6 12.8-2.7 27.8-15.7 34.7-30.1 2.5-5.3 2.9-7.1 2.9-15-.1-15.8 1.1-14.3-49.6-65.1l-44-44 18.1-18.2 18.1-18.1 6.7 7.1c20.7 21.9 32.6 32.8 38.6 35.3 5.6 2.4 11.5-1.2 26.5-15.8 16.1-15.8 21.8-23.8 20-28.6-.9-2.3-4.6-7.8-7.4-10.7-.5-.6-4.1-4.8-8-9.2-7.2-8.3-64.7-65.9-91-91.1-8.2-7.9-16-14.7-17.3-15.2-6.8-2.6-12.6.8-28.4 16.3-13.1 12.9-19.8 21.8-19.8 26.3 0 5.6 13.3 21 37.4 43.4l5.8 5.4-18.1 18.1-18.1 18.1-67-67c-36.9-36.8-67-67.3-67-67.7 0-.4 3-5.1 6.7-10.4 16.1-23.5 30.3-53.8 35.8-76.4.7-2.8 4.3-22.5 5.3-28.5 1.1-6.5.8-41.1-.3-48.5-1.4-9.2-6.4-26.8-10-35.3-9.9-23.3-28.4-44.9-50.5-58.8-21.8-13.8-47.4-20.5-78.6-20.7-7.3-.1-13.3 0-13.5.2zm22 74c12.1 2.8 22.6 7.6 31.1 14.4 13.3 10.7 23.7 27.4 26.3 42.7 6 34.4-9.5 65.3-40.5 80.7-16.1 7.9-40.9 8.5-58.1 1.3-4.4-1.8-5.2-1.9-4-.3 1.1 1.4 4 9.8 4.8 13.9 5.3 25.7-.6 47.7-17.7 66-8.9 9.6-19.7 16.4-31.3 19.8-9.7 2.9-11.7 3.1-22.2 3.2-24.8 0-47.2-12.3-61-33.3-15.4-23.7-15.3-56.4.4-79.3 11.1-16.2 26.6-27.1 44.3-31.1 14.4-3.2 32.9-1.7 43.9 3.7 2 .9 3.7 1.6 3.8 1.5.2-.2-.8-3.4-2.2-7.3-3.4-9.7-3.7-11.4-4-23.3-.4-17.1 3.8-30.7 13.8-44 11.4-15.2 26.5-24.8 44.7-28.4 6.2-1.2 22.9-1.4 27.9-.2z"/></svg>
|
||||
<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>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.8 KiB 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,30 +1,84 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head lang="en">
|
||||
<meta charset="UTF-8">
|
||||
<title>KeeWeb</title>
|
||||
<meta name="application-name" content="KeeWeb">
|
||||
<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">
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<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">
|
||||
<meta name="msapplication-config" content="browserconfig.xml">
|
||||
<meta name="msapplication-TileColor" content="#6386ec">
|
||||
<meta name="theme-color" content="#6386ec">
|
||||
<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>
|
||||
<script src="js/runtime.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);
|
||||
@@ -1,9 +1,4 @@
|
||||
{
|
||||
"months": ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
|
||||
"monthsShort": ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
|
||||
"weekdays": ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
|
||||
"weekdaysShort": ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
|
||||
|
||||
"retToApp": "return to app",
|
||||
"name": "name",
|
||||
"icon": "icon",
|
||||
@@ -19,6 +14,7 @@
|
||||
"or": "or",
|
||||
"history": "history",
|
||||
"template": "template",
|
||||
"templates": "templates",
|
||||
"notImplemented": "Not Implemented",
|
||||
"saveChanges": "Save changes",
|
||||
"discardChanges": "Discard changes",
|
||||
@@ -27,26 +23,57 @@
|
||||
"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 tags section.",
|
||||
"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",
|
||||
@@ -55,14 +82,13 @@
|
||||
"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",
|
||||
@@ -74,14 +100,14 @@
|
||||
"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",
|
||||
@@ -91,7 +117,6 @@
|
||||
"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",
|
||||
@@ -108,17 +133,19 @@
|
||||
"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 custom icon",
|
||||
|
||||
"iconSelCustom": "Select a custom icon",
|
||||
"listEmptyTitle": "Empty",
|
||||
"listEmptyAdd": "add with {} button above",
|
||||
"listGroup": "Group",
|
||||
@@ -128,7 +155,6 @@
|
||||
"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",
|
||||
@@ -146,11 +172,14 @@
|
||||
"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",
|
||||
@@ -185,12 +214,19 @@
|
||||
"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",
|
||||
|
||||
"detAttDownload": "Shift-click attachment button to download or",
|
||||
"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",
|
||||
@@ -210,8 +246,8 @@
|
||||
"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 changed made to entry?",
|
||||
"detHistoryDiscardChangesAlertBody": "Unsaved changed will be lost, there will be no way back.",
|
||||
"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",
|
||||
@@ -228,19 +264,24 @@
|
||||
"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 with click on its title",
|
||||
"detCopyHint": "You can copy field value by clicking its title",
|
||||
"detMore": "more",
|
||||
"detClickToAddField": "click to add a new field",
|
||||
"detMenuAddNewField": "Add 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",
|
||||
"detSetupOtp": "One-time passwords",
|
||||
"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",
|
||||
@@ -267,17 +308,34 @@
|
||||
"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 password data.",
|
||||
"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: {}",
|
||||
"autoTypeErrorGlobal": "To use system-wide shortcut, please focus the app where you want to type your password",
|
||||
"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",
|
||||
@@ -286,7 +344,9 @@
|
||||
"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.",
|
||||
@@ -300,6 +360,8 @@
|
||||
"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.",
|
||||
@@ -308,7 +370,7 @@
|
||||
"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",
|
||||
@@ -331,17 +393,30 @@
|
||||
"setGenExtractingUpdate": "Extracting update...",
|
||||
"setGenCheckErr": "There was an error downloading new version",
|
||||
"setGenNeverChecked": "Never checked for updates",
|
||||
"setGenRestartToUpdate": "Restart the app to update",
|
||||
"setGenRestartToUpdate": "Restart KeeWeb to update",
|
||||
"setGenDownloadAndRestart": "Download update and restart",
|
||||
"setGenAppearance": "Appearance",
|
||||
"setGenTheme": "Theme",
|
||||
"setGenThemeFb": "Flat blue",
|
||||
"setGenThemeDefault": "Default",
|
||||
"setGenThemeDark": "Dark",
|
||||
"setGenThemeLight": "Light",
|
||||
"setGenThemeBlue": "Flat blue",
|
||||
"setGenThemeFb": "Dark blue",
|
||||
"setGenThemeBl": "Light blue",
|
||||
"setGenThemeBrown": "Brownie",
|
||||
"setGenThemeDb": "Dark brown",
|
||||
"setGenThemeWh": "White",
|
||||
"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",
|
||||
@@ -354,11 +429,16 @@
|
||||
"setGenTitlebarStyleHiddenInset": "Custom title, draggable window",
|
||||
"setGenShowSubgroups": "Show entries from all subgroups",
|
||||
"setGenTableView": "Entries list table view",
|
||||
"setGenColorfulIcons": "Colorful custom icons in list",
|
||||
"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",
|
||||
@@ -374,31 +454,53 @@
|
||||
"setGenNoClear": "Don't clear",
|
||||
"setGenClearSeconds": "In {} seconds",
|
||||
"setGenClearMinute": "In a minute",
|
||||
"setGenMinInstead": "Minimize app instead of close",
|
||||
"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 ...",
|
||||
"setFileSaveTo": "Save to …",
|
||||
"setFileClose": "Close",
|
||||
"setFileSync": "Sync",
|
||||
"setFileSyncVerb": "Sync",
|
||||
"setFileSaveToXml": "XML",
|
||||
"setFileSaveToHtml": "HTML",
|
||||
"setFileLastSync": "Last sync",
|
||||
"setFileLastSyncUnknown": "unknown",
|
||||
"setFileSyncInProgress": "sync in progress",
|
||||
@@ -413,6 +515,10 @@
|
||||
"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",
|
||||
@@ -437,10 +543,10 @@
|
||||
"setFileKdfParamsPar": "Parallelism",
|
||||
"setFileKeyChangeForce": "Ask to change key after (days)",
|
||||
"setFileUseKeyFile": "Use key file",
|
||||
"setFileUseGenKeyFile": "Use generated key file",
|
||||
"setFileUseOldKeyFile": "Use old key file",
|
||||
"setFileGenKeyFile": "Generate new key file",
|
||||
"setFileDontUseKeyFile": "Don't 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",
|
||||
@@ -451,7 +557,17 @@
|
||||
"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",
|
||||
@@ -469,12 +585,14 @@
|
||||
"setShSave": "save all files",
|
||||
"setShGen": "generate password",
|
||||
"setShSet": "app settings",
|
||||
"setShCopyPassGlobal": "copy password (when the app is in background)",
|
||||
"setShCopyUserGlobal": "copy user (when the app is in background)",
|
||||
"setShCopyUrlGlobal": "copy website (when the app is in background)",
|
||||
"setShAutoTypeGlobal": "auto-type (when the app is in background)",
|
||||
"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",
|
||||
@@ -505,32 +623,84 @@
|
||||
"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 which are not in public domain are licensed under MIT 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 a developer directly",
|
||||
"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)",
|
||||
@@ -539,16 +709,70 @@
|
||||
"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 kdbx file with PUT",
|
||||
|
||||
"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."
|
||||
"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",
|
||||
@@ -101,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",
|
||||
@@ -121,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: {}. Bitte entfernen Sie sie.",
|
||||
"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",
|
||||
@@ -140,13 +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)",
|
||||
"keyEnter": "Enter",
|
||||
"iconFavTitle": "Website-Icon herunterladen und verwenden",
|
||||
"iconFavTitle": "Website-Symbol herunterladen und verwenden",
|
||||
"iconSelCustom": "Eigenes Icon auswählen",
|
||||
"listEmptyTitle": "Leer",
|
||||
"listEmptyAdd": "Zum Anlegen neuer Einträge den obigen {} Button benutzen",
|
||||
@@ -154,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",
|
||||
@@ -174,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.",
|
||||
@@ -203,10 +201,10 @@
|
||||
"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",
|
||||
@@ -215,11 +213,18 @@
|
||||
"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",
|
||||
@@ -236,11 +241,11 @@
|
||||
"detHistoryRevertAlertBody": "Ihr aktueller Stand wird in der Historie gesichert.",
|
||||
"detHistoryDeleteAlert": "Diesen Stand löschen?",
|
||||
"detHistoryDeleteAlertBody": "Sie können ihn nicht wiederherstellen.",
|
||||
"detHistoryDiscardChangesAlert": "Änderung verwerfen?",
|
||||
"detHistoryDiscardChangesAlertBody": "Ungesicherte Änderungen sind unwiderruflich verloren.",
|
||||
"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",
|
||||
@@ -253,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",
|
||||
@@ -265,18 +272,20 @@
|
||||
"detMenuAddField": "{} hinzufügen",
|
||||
"detMenuCopyPassword": "Passwort kopieren",
|
||||
"detMenuCopyUser": "Benutzer kopieren",
|
||||
"detSetupOtp": "Einweg-Passwörter",
|
||||
"detMenuCopyOtp": "Einmal-Code kopieren",
|
||||
"detSetupOtp": "Einmal-Codes",
|
||||
"detClone": "Kopie erstellen",
|
||||
"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",
|
||||
@@ -289,18 +298,35 @@
|
||||
"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",
|
||||
@@ -308,9 +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 Abfolge eintippen",
|
||||
"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.",
|
||||
@@ -324,25 +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 {}",
|
||||
@@ -350,34 +381,50 @@
|
||||
"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",
|
||||
"setGenAutoSyncOnClose": "Beim Schließen speichern und synchronisieren",
|
||||
"setGenAutoSyncTimer": "Regelmäßig speichern und synchronisieren",
|
||||
@@ -387,7 +434,7 @@
|
||||
"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",
|
||||
@@ -399,42 +446,66 @@
|
||||
"setGenClearMinute": "Nach einer Minute",
|
||||
"setGenMinInstead": "App beim Schließen stattdessen minimieren",
|
||||
"setGenLock": "Automatisch sperren",
|
||||
"setGenLockMinimize": "Beim Minimieren 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",
|
||||
"setFileConfirmPass": "Master-Passwort bestätigen",
|
||||
"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 sie erneut ein",
|
||||
"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",
|
||||
@@ -442,7 +513,7 @@
|
||||
"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",
|
||||
@@ -462,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",
|
||||
@@ -473,11 +544,22 @@
|
||||
"setFileUnsavedBody": "Es gibt ungespeicherte Änderungen in dieser Datei",
|
||||
"setFileCloseNoSave": "Sperren und Änderungen verwerfen",
|
||||
"setFileDontClose": "Nicht sperren",
|
||||
"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",
|
||||
@@ -486,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",
|
||||
@@ -513,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",
|
||||
@@ -525,20 +610,40 @@
|
||||
"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",
|
||||
@@ -546,12 +651,15 @@
|
||||
"setHelpUpdates": "Neuigkeiten",
|
||||
"setHelpTwitter": "Twitter",
|
||||
"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",
|
||||
@@ -560,8 +668,28 @@
|
||||
"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"
|
||||
}
|
||||
@@ -1,50 +1,4 @@
|
||||
{
|
||||
"months": [
|
||||
"Janvier",
|
||||
"Février",
|
||||
"Mars",
|
||||
"Avril",
|
||||
"Mai",
|
||||
"Juin",
|
||||
"Juillet",
|
||||
"Août",
|
||||
"Septembre",
|
||||
"Octobre",
|
||||
"Novembre",
|
||||
"Décembre"
|
||||
],
|
||||
"monthsShort": [
|
||||
"Janv",
|
||||
"Févr",
|
||||
"Mars",
|
||||
"Avr",
|
||||
"Mai",
|
||||
"Juin",
|
||||
"Juil",
|
||||
"Août",
|
||||
"Sept",
|
||||
"Oct",
|
||||
"Nov",
|
||||
"Déc"
|
||||
],
|
||||
"weekdays": [
|
||||
"Dimanche",
|
||||
"Lundi",
|
||||
"Mardi",
|
||||
"Mercredi",
|
||||
"Jeudi",
|
||||
"Vendredi",
|
||||
"Samedi"
|
||||
],
|
||||
"weekdaysShort": [
|
||||
"Dim",
|
||||
"Lun",
|
||||
"Mar",
|
||||
"Mer",
|
||||
"Jeu",
|
||||
"Ven",
|
||||
"Sam"
|
||||
],
|
||||
"retToApp": "retour vers l'appli",
|
||||
"name": "nom",
|
||||
"icon": "icône",
|
||||
@@ -61,30 +15,63 @@
|
||||
"history": "historique",
|
||||
"template": "modèle",
|
||||
"notImplemented": "Non Implémenté",
|
||||
"saveChanges": "Sauver les modifs",
|
||||
"discardChanges": "Annuler les modifs",
|
||||
"saveChanges": "Sauvegarder les modifications",
|
||||
"discardChanges": "Annuler les modifications",
|
||||
"advanced": "Avancé",
|
||||
"shortcuts": "Raccourcis",
|
||||
"help": "Aide",
|
||||
"settings": "Paramètres",
|
||||
"plugins": "Modules",
|
||||
"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": "Tous les items",
|
||||
"menuAllItems": "Tout",
|
||||
"menuColors": "Couleurs",
|
||||
"menuTrash": "Corbeille",
|
||||
"menuSetGeneral": "Général",
|
||||
"menuSetAbout": "A propos",
|
||||
"menuSetAbout": "À propos",
|
||||
"menuSetDevices": "Appareils",
|
||||
"menuAlertNoTags": "Aucun tag",
|
||||
"menuAlertNoTagsBody": "Vous pouvez ajouter de nouveaux tags en éditant les champs, dans la section tags.",
|
||||
"menuAlertNoTagsBody": "Vous pouvez ajouter de nouveaux tags en éditant les champs, dans la section Tags.",
|
||||
"menuEmptyTrash": "Vider la Corbeille",
|
||||
"menuEmptyTrashAlert": "Vider la Corbeille?",
|
||||
"menuEmptyTrashAlert": "Vider la Corbeille ?",
|
||||
"menuEmptyTrashAlertBody": "Vous ne pourrez plus récupérer ces éléments",
|
||||
"menuItemCollapsed": "Double-cliquer pour déplier",
|
||||
"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",
|
||||
@@ -95,24 +82,26 @@
|
||||
"alertDoNotAsk": "Ne plus me le redemander",
|
||||
"appBeta": "ATTENTION: version beta, aperçu seulement",
|
||||
"footerOpen": "Ouvrir/Nouveau",
|
||||
"footerSyncError": "Erreur de synchro",
|
||||
"footerSyncError": "Erreur de synchronisation",
|
||||
"footerTitleGen": "Générer",
|
||||
"footerTitleLock": "Verrouiller",
|
||||
"genLen": "Longueur",
|
||||
"genNewPass": "Nouveau mot de passe",
|
||||
"genPresetDefault": "réglages par défaut",
|
||||
"genPresetDerived": "comme ancien mot de passe",
|
||||
"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-bit",
|
||||
"genPresetHash256": "hash 256-bit",
|
||||
"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 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",
|
||||
@@ -123,8 +112,8 @@
|
||||
"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": "Un nom de tag ne peut pas contenir les caractères {}. Merci de les supprimer.",
|
||||
"genPsTitle": "Générateur de préréglage",
|
||||
"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",
|
||||
@@ -136,25 +125,29 @@
|
||||
"genPsDigits": "Chiffres",
|
||||
"genPsSpecial": "Caractères spéciaux",
|
||||
"genPsBrackets": "Parenthèses",
|
||||
"genPsHigh": "Caractères ASCII hauts",
|
||||
"genPsHigh": "Caractères ASCII étendus",
|
||||
"genPsAmbiguous": "Caractères ambigus",
|
||||
"genPsInclude": "Caractères additionnels à inclure",
|
||||
"genPsExample": "Exemple de mot de passe généré",
|
||||
"keyChangeTitleRemote": "Clé maître changée",
|
||||
"keyChangeMessageRemote": "La clé maître a été changée pour cette base de donnée. Merci d'entrer une nouvelle clé",
|
||||
"keyChangeTitleExpired": "Clé maître expirée",
|
||||
"keyChangeMessageExpired": "La clé maître pour cette base de donnée est expirée. Merci d'entrer une nouvelle clé",
|
||||
"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 un icône personnalisé",
|
||||
"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": "Templates",
|
||||
"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",
|
||||
@@ -174,32 +167,36 @@
|
||||
"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és",
|
||||
"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": "Aucun fichier ne peut être ouvert.",
|
||||
"openNothingFoundBody": "Nous n'avons trouvé aucun fichier convenable.",
|
||||
"openSelectFile": "Sélectionnez un fichier",
|
||||
"openSelectFileBody": "Sélectionnez le fichier que vous voudriez ouvrir",
|
||||
"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é à l'intérieur de l'app. Les modifications apportées ne seront pas sauvegardés vers le système de fichiers. Pour obtenir le fichier avec vos données, exporter à partir des paramètres.",
|
||||
"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",
|
||||
"openConfigHeader": "Paramètres {}",
|
||||
"openUrl": "URL",
|
||||
"openUrlDesc": "https://serveur/chemin/fichier.kdbx, ou seulement fichier.kdbx",
|
||||
"openUser": "Nom d'utilisateur",
|
||||
@@ -212,12 +209,20 @@
|
||||
"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",
|
||||
"detAttDownload": "Cliquer sur le bouton de pièce-jointe en pressant la touche Maj ou",
|
||||
"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",
|
||||
"detEmpty": "Vos mot de passe seront affichés ici",
|
||||
"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",
|
||||
@@ -236,8 +241,8 @@
|
||||
"detHistoryRevertAlertBody": "L'état courant sera sauvegardé dans l'historique.",
|
||||
"detHistoryDeleteAlert": "Supprimer cet état de l'historique ?",
|
||||
"detHistoryDeleteAlertBody": "Vous ne pourrez plus le restaurer.",
|
||||
"detHistoryDiscardChangesAlert": "Oublier les changements fait à cette entrée ?",
|
||||
"detHistoryDiscardChangesAlertBody": "Les changements non sauvegardés seront définitivement perdus.",
|
||||
"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",
|
||||
@@ -254,9 +259,11 @@
|
||||
"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 son titre",
|
||||
"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",
|
||||
@@ -265,8 +272,10 @@
|
||||
"detMenuAddField": "Ajouter {}",
|
||||
"detMenuCopyPassword": "Copier mot de passe",
|
||||
"detMenuCopyUser": "Copier nom",
|
||||
"detSetupOtp": "Mots de passe à usage unique",
|
||||
"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",
|
||||
@@ -277,32 +286,50 @@
|
||||
"detAutoTypeObfuscation": "Mélanger touches réelles et aléatoire",
|
||||
"detAutoTypeWindow": "Fenêtre",
|
||||
"detAutoTypeInputWindow": "Titre de fenêtre",
|
||||
"detSetupOtpAlert": "Scanner le code QR",
|
||||
"detSetupOtpAlertBody": "Veuillez copier le code QR qui est affiché sur la page d'autorisation",
|
||||
"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 code QR {}",
|
||||
"detSetupOtpAlertBody3": "3. coller la ici {}",
|
||||
"detSetupOtpAlertBody3Mobile": "3. Selectionner ou scanner avec votre appareil photo en utilisant Selectionner/Scan ci-dessous",
|
||||
"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": "Selectionner/Scan",
|
||||
"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 code QR...",
|
||||
"detOtpQrError": "Erreur lecture du code QR",
|
||||
"detOtpQrErrorBody": "Désolé le QR code ne peut pas être lu, essayez à nouveau ou contacter les auteurs de l'application avec le détail de l'erreur.",
|
||||
"detOtpQrWrong": "Code QR incorrect",
|
||||
"detOtpQrWrongBody": "Votre QR code a été scanné avec succès mais il ne contient pas d'information pour un mot de passe à usage unique.",
|
||||
"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 : {}",
|
||||
"autoTypeErrorGlobal": "Pour utiliser le raccourci, sélectionner dans l'application là où vous voulez saisir le mot de passe",
|
||||
"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",
|
||||
@@ -311,6 +338,8 @@
|
||||
"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.",
|
||||
@@ -322,22 +351,25 @@
|
||||
"appCannotLock": "Vous avez des modifications non sauvegardées qui seront perdus. Continuer ?",
|
||||
"appAutoSave": "Sauver les modifications automatiquement",
|
||||
"appSaveError": "Sauver l'erreur",
|
||||
"appSaveErrorBody": "Echec sauvegarde auto du fichier",
|
||||
"appSaveErrorBodyMul": "Echec sauvegarde auto des fichiers",
|
||||
"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 au chargement des paramètres. Merci de revérifier l'URL de l'application ou contacter votre administrateur.",
|
||||
"appNotSupportedError": "Votre navigateur n'est pas supporté.",
|
||||
"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 une mise à jour automatique à partir de votre version n'est pas possible.",
|
||||
"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",
|
||||
@@ -354,17 +386,30 @@
|
||||
"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 pour mettre à jour",
|
||||
"setGenRestartToUpdate": "Redémarrer KeeWeb pour mettre à jour",
|
||||
"setGenDownloadAndRestart": "Télécharger la mise à jour et redémarrer",
|
||||
"setGenAppearance": "Apparence",
|
||||
"setGenTheme": "Thème",
|
||||
"setGenThemeFb": "Bleu plat",
|
||||
"setGenThemeDefault": "Défaut",
|
||||
"setGenThemeDark": "Sombre",
|
||||
"setGenThemeLight": "Clair",
|
||||
"setGenThemeBlue": "Bleu",
|
||||
"setGenThemeFb": "Bleu foncé",
|
||||
"setGenThemeBl": "Bleu clair",
|
||||
"setGenThemeBrown": "Brownie",
|
||||
"setGenThemeDb": "Marron foncé",
|
||||
"setGenThemeWh": "Blanc",
|
||||
"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",
|
||||
@@ -373,11 +418,14 @@
|
||||
"setGenFontSizeLargest": "Très large",
|
||||
"setGenTitlebarStyle": "Style de fenêtres (redémarrage requis)",
|
||||
"setGenTitlebarStyleDefault": "Par défaut",
|
||||
"setGenTitlebarStyleHidden": "Titre modifié",
|
||||
"setGenTitlebarStyleHiddenInset": "Titre modifié, fénêtre déplaçable",
|
||||
"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 en couleur dans la 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",
|
||||
@@ -387,7 +435,7 @@
|
||||
"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'app est inactive",
|
||||
"setGenLockInactive": "si l'application est inactive",
|
||||
"setGenNoAutoLock": "Ne pas verrouiller automatiquement",
|
||||
"setGenLockMinutes": "Dans {} minutes",
|
||||
"setGenLockHour": "Dans une heure",
|
||||
@@ -398,29 +446,52 @@
|
||||
"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 minimisée",
|
||||
"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 stocké en interne de l'appli",
|
||||
"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",
|
||||
"setFileSaveTo": "Enregistrer sous...",
|
||||
"setFileClose": "Fermer",
|
||||
"setFileSync": "Sync",
|
||||
"setFileSyncVerb": "Synchroniser",
|
||||
"setFileSaveToXml": "XML",
|
||||
"setFileSaveToHtml": "HTML",
|
||||
"setFileLastSync": "Dernière synchro",
|
||||
"setFileLastSyncUnknown": "inconnu",
|
||||
"setFileSyncInProgress": "synchronisation en cours",
|
||||
@@ -433,8 +504,12 @@
|
||||
"setFileKeyFile": "Fichier clé",
|
||||
"setFileSelKeyFile": "Sélectionner fichier clé",
|
||||
"setFileNames": "Noms",
|
||||
"setFileDefUser": "Nom par défaut",
|
||||
"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",
|
||||
@@ -458,13 +533,13 @@
|
||||
"setFileKdfParamsMem": "Mémoire, Ko",
|
||||
"setFileKdfParamsPar": "Parallèlisme",
|
||||
"setFileKeyChangeForce": "Demander à changer la clé après (jours)",
|
||||
"setFileUseKeyFile": "Utiliser fichier clé",
|
||||
"setFileUseGenKeyFile": "Utiliser fichier clé généré",
|
||||
"setFileUseOldKeyFile": "Utiliser ancien fichier clé",
|
||||
"setFileGenKeyFile": "Générer nouveau fichier clé",
|
||||
"setFileDontUseKeyFile": "Ne pas utiliser fichier clé",
|
||||
"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 mode de passe vide la laisse sans aucune protection. Voulez-vous vraiment faire cela ?",
|
||||
"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à",
|
||||
@@ -473,6 +548,17 @@
|
||||
"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",
|
||||
@@ -490,11 +576,14 @@
|
||||
"setShSave": "sauver tous les fichiers",
|
||||
"setShGen": "générer un mot de passe",
|
||||
"setShSet": "paramètres",
|
||||
"setShCopyPassGlobal": "copier le mot de passe (quand l'appli est en arrière-plan)",
|
||||
"setShCopyUserGlobal": "copier l'utilisateur (quand l'appli est en arrière-plan)",
|
||||
"setShCopyUrlGlobal": "copier le site web (quand l'appli est en arrière-plan)",
|
||||
"setShAutoTypeGlobal": "saisie automatique (quand l'appli est en arrière-plan)",
|
||||
"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",
|
||||
@@ -525,10 +614,30 @@
|
||||
"setPlTranslateLink": "traduire l'appli dans votre langue",
|
||||
"setPlAutoUpdate": "Mise à jour automatique",
|
||||
"setPlLoadGallery": "Charger le catalogue",
|
||||
"setAboutTitle": "A propos",
|
||||
"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",
|
||||
"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 {}.",
|
||||
@@ -538,7 +647,7 @@
|
||||
"setHelpProblems1": "Si quelque chose ne va pas, merci de {}",
|
||||
"setHelpProblems2": "ou {}",
|
||||
"setHelpOpenIssue": "faire connaître le problème sur GitHub",
|
||||
"setHelpContactLink": "contacter directement un développeur",
|
||||
"setHelpContactLink": "contactez le développeur directement",
|
||||
"setHelpAppInfo": "Information",
|
||||
"setHelpOtherPlatforms": "Autres plateformes",
|
||||
"setHelpDesktopApps": "Applis Desktop",
|
||||
@@ -548,8 +657,11 @@
|
||||
"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é à Dropbox (pas un dossier), définir le dossier avec vos fichiers Kdbx içi",
|
||||
"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 à",
|
||||
@@ -558,10 +670,32 @@
|
||||
"dropboxLinkCustom": "Votre appli Dropbox",
|
||||
"gdriveSharedWithMe": "Partagé avec moi",
|
||||
"webdavSaveMethod": "Méthode de sauvegarde",
|
||||
"webdavSaveMove": "Uploader un fichier temporaire et le déplacer",
|
||||
"webdavSavePut": "Ecraser le fichier kdbx avec PUT",
|
||||
"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 blockés",
|
||||
"authPopupRequiredBody": "Merci d'autoriser les popups pour cette appli dans votre navigateur ou essayez à nouveau."
|
||||
"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,21 +1,21 @@
|
||||
{
|
||||
"name": "KeeWeb",
|
||||
"short_name": "KeeWeb",
|
||||
"description": "Free cross-platform password manager compatible with KeePass",
|
||||
"display": "standalone",
|
||||
"orientation": "any",
|
||||
"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"
|
||||
}
|
||||
]
|
||||
"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,164 +0,0 @@
|
||||
const Backbone = require('backbone');
|
||||
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 AppRightsChecker = require('./comp/app-rights-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 FeatureTester = require('./comp/feature-tester');
|
||||
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() || FeatureDetector.isFrame) {
|
||||
return;
|
||||
}
|
||||
loadMixins();
|
||||
|
||||
const appModel = new AppModel();
|
||||
|
||||
Promise.resolve()
|
||||
.then(loadConfigs)
|
||||
.then(initModules)
|
||||
.then(loadRemoteConfig)
|
||||
.then(ensureCanRun)
|
||||
.then(showApp)
|
||||
.then(postInit)
|
||||
.catch(e => {
|
||||
appModel.appLogger.error('Error starting app', e);
|
||||
});
|
||||
|
||||
function loadMixins() {
|
||||
require('./mixins/view');
|
||||
require('./helpers');
|
||||
}
|
||||
|
||||
function ensureCanRun() {
|
||||
return FeatureTester.test()
|
||||
.catch(e => {
|
||||
Alerts.error({
|
||||
header: Locale.appSettingsError,
|
||||
body: Locale.appNotSupportedError + '<br/><br/>' + e,
|
||||
buttons: [],
|
||||
esc: false, enter: false, click: false
|
||||
});
|
||||
throw 'Feature testing failed: ' + 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() {
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
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) {
|
||||
return new Promise(resolve => {
|
||||
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();
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
showView();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function postInit() {
|
||||
Updater.init();
|
||||
SingleInstanceChecker.init();
|
||||
AppRightsChecker.init();
|
||||
setTimeout(() => PluginManager.runAutoUpdate(), Timeouts.AutoUpdatePluginsAfterStart);
|
||||
}
|
||||
|
||||
function showView() {
|
||||
appModel.prepare();
|
||||
new AppView({ model: appModel }).render();
|
||||
Backbone.trigger('app-ready');
|
||||
logStartupTime();
|
||||
}
|
||||
|
||||
function logStartupTime() {
|
||||
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,99 +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 || thisDomain.indexOf('.' + domain) > 0) {
|
||||
if (domain === thisDomain) {
|
||||
rank += 20;
|
||||
} else {
|
||||
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,21 +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 helperRelPath = `helper/${process.platform}/KeeWebHelper${ext}`;
|
||||
const helperPath = Launcher.getAppPath(helperRelPath);
|
||||
Launcher.ensureRunnable(helperPath);
|
||||
logger.debug('Using auto-type helper', helperPath);
|
||||
this._helperPath = helperPath;
|
||||
return helperPath;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = AutoTypeNativeHelper;
|
||||
@@ -1,44 +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');
|
||||
const AutoTypeSequenceType = require('../const/autotype-sequencetype');
|
||||
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;
|
||||
@@ -54,44 +57,45 @@ const AutoType = {
|
||||
}
|
||||
},
|
||||
|
||||
runAndHandleResult(result) {
|
||||
this.run(result, 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(result, callback) {
|
||||
run(result, windowId, callback) {
|
||||
this.running = true;
|
||||
let sequence;
|
||||
if (result.sequenceType === AutoTypeSequenceType.PASSWORD) {
|
||||
sequence = '{PASSWORD}';
|
||||
} else if (result.sequenceType === AutoTypeSequenceType.USERNAME) {
|
||||
sequence = '{USERNAME}';
|
||||
} else {
|
||||
sequence = result.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(result.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));
|
||||
logger.info('Resolved', this.printOps(runner.ops));
|
||||
if (result.entry.autoTypeObfuscation) {
|
||||
try {
|
||||
runner.obfuscate();
|
||||
@@ -100,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;
|
||||
@@ -123,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);
|
||||
}
|
||||
@@ -149,7 +153,7 @@ const AutoType = {
|
||||
},
|
||||
|
||||
hideWindow(callback) {
|
||||
logger.debug('Hide window');
|
||||
logger.info('Hide window');
|
||||
if (Launcher.isAppFocused()) {
|
||||
Launcher.hideApp();
|
||||
setTimeout(callback, Timeouts.AutoTypeAfterHide);
|
||||
@@ -158,89 +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 {
|
||||
if (!url) {
|
||||
if (!windowInfo.url) {
|
||||
// try to find a URL in the title
|
||||
const urlMatcher = new RegExp(
|
||||
'https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,4}\\b([-a-zA-Z0-9@:%_\\+.~#?&//=]*)'
|
||||
);
|
||||
const urlMatches = urlMatcher.exec(title);
|
||||
url = urlMatches && urlMatches.length > 0 ? urlMatches[0] : null;
|
||||
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.debug('Window title', title, url);
|
||||
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({ entry: 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;
|
||||
@@ -1,77 +0,0 @@
|
||||
const FeatureDetector = require('../util/feature-detector');
|
||||
const Alerts = require('../comp/alerts');
|
||||
const AppSettingsModel = require('../models/app-settings-model');
|
||||
const Launcher = require('./launcher');
|
||||
const Locale = require('../util/locale');
|
||||
|
||||
const AppRightsChecker = {
|
||||
AppPath: '/Applications/KeeWeb.app',
|
||||
|
||||
init() {
|
||||
if (!FeatureDetector.isDesktop || !FeatureDetector.isMac) {
|
||||
return;
|
||||
}
|
||||
if (AppSettingsModel.instance.get('skipFolderRightsWarning')) {
|
||||
return;
|
||||
}
|
||||
if (!Launcher.getAppPath().startsWith(this.AppPath)) {
|
||||
return;
|
||||
}
|
||||
this.needRunInstaller(needRun => {
|
||||
if (needRun) {
|
||||
this.showAlert();
|
||||
this.runInstaller();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
needRunInstaller(callback) {
|
||||
Launcher.statFile(this.AppPath, stat => {
|
||||
const folderIsRoot = stat && stat.uid === 0;
|
||||
callback(!folderIsRoot);
|
||||
});
|
||||
},
|
||||
|
||||
showAlert() {
|
||||
const command = 'sudo chown -R root ' + this.AppPath;
|
||||
this.alert = Alerts.alert({
|
||||
icon: 'lock',
|
||||
header: Locale.appRightsAlert,
|
||||
body: Locale.appRightsAlertBody1.replace('{}', `<code>${this.AppPath}</code>`) +
|
||||
'<br/>' + Locale.appRightsAlertBody2 + `: <pre>${command}</pre>`,
|
||||
buttons: [
|
||||
{result: 'skip', title: Locale.alertDoNotAsk, error: true},
|
||||
Alerts.buttons.ok
|
||||
],
|
||||
success: (result) => {
|
||||
if (result === 'skip') {
|
||||
this.dontAskAnymore();
|
||||
}
|
||||
this.alert = null;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
runInstaller() {
|
||||
Launcher.spawn({
|
||||
cmd: this.AppPath + '/Contents/Installer/KeeWeb\ Installer.app/Contents/MacOS/applet',
|
||||
complete: () => {
|
||||
this.needRunInstaller(needRun => {
|
||||
if (this.alert && !needRun) {
|
||||
this.alert.closeWithResult('cancel');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
dontAskAnymore() {
|
||||
this.needRunInstaller(needRun => {
|
||||
if (needRun) {
|
||||
AppSettingsModel.instance.set('skipFolderRightsWarning', true);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = AppRightsChecker;
|
||||
@@ -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,34 +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);
|
||||
const hasKeys = Object.keys(message).filter(key => key !== 'config').length > 0;
|
||||
if (!hasKeys) {
|
||||
return false;
|
||||
}
|
||||
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 };
|
||||
@@ -0,0 +1,160 @@
|
||||
import { Launcher } from 'comp/launcher';
|
||||
import { Keys } from 'const/keys';
|
||||
import { AppSettingsFieldName, AppSettings } from 'models/app-settings';
|
||||
import { Features } from 'util/features';
|
||||
import { Locale } from 'util/locale';
|
||||
|
||||
let allowedKeys: Map<number, string>;
|
||||
|
||||
function getAllowedKeys(): Map<number, string> {
|
||||
if (!allowedKeys) {
|
||||
allowedKeys = new Map<number, string>();
|
||||
for (const [name, code] of Object.entries(Keys)) {
|
||||
if (typeof code === 'string') {
|
||||
continue;
|
||||
}
|
||||
const keyName = name.replace('DOM_VK_', '');
|
||||
if (/^([0-9A-Z]|F\d{1,2})$/.test(keyName)) {
|
||||
allowedKeys.set(code, keyName);
|
||||
}
|
||||
}
|
||||
}
|
||||
return allowedKeys;
|
||||
}
|
||||
|
||||
const AllGlobalShortcuts = {
|
||||
copyPassword: { mac: 'Ctrl+Alt+C', all: 'Shift+Alt+C' },
|
||||
copyUser: { mac: 'Ctrl+Alt+B', all: 'Shift+Alt+B' },
|
||||
copyUrl: { mac: 'Ctrl+Alt+U', all: 'Shift+Alt+U' },
|
||||
copyOtp: undefined,
|
||||
autoType: { mac: 'Ctrl+Alt+T', all: 'Shift+Alt+T' },
|
||||
restoreApp: undefined
|
||||
};
|
||||
|
||||
export type GlobalShortcutType = keyof typeof AllGlobalShortcuts;
|
||||
|
||||
const GlobalShortcutAppSettingsFields: Record<GlobalShortcutType, AppSettingsFieldName> = {
|
||||
copyPassword: 'globalShortcutCopyPassword',
|
||||
copyUser: 'globalShortcutCopyUser',
|
||||
copyUrl: 'globalShortcutCopyUrl',
|
||||
copyOtp: 'globalShortcutCopyOtp',
|
||||
autoType: 'globalShortcutAutoType',
|
||||
restoreApp: 'globalShortcutRestoreApp'
|
||||
};
|
||||
|
||||
const Shortcuts = {
|
||||
keyEventToShortcut(event: KeyboardEvent): { valid: boolean; value: string } {
|
||||
const modifiers = [];
|
||||
if (event.ctrlKey) {
|
||||
modifiers.push('Ctrl');
|
||||
}
|
||||
if (event.altKey) {
|
||||
modifiers.push('Alt');
|
||||
}
|
||||
if (event.shiftKey) {
|
||||
modifiers.push('Shift');
|
||||
}
|
||||
if (Features.isMac && event.metaKey) {
|
||||
modifiers.push('Meta');
|
||||
}
|
||||
const keyName = getAllowedKeys().get(event.which);
|
||||
return {
|
||||
value: modifiers.join('+') + '+' + (keyName || '…'),
|
||||
valid: modifiers.length > 0 && !!keyName
|
||||
};
|
||||
},
|
||||
|
||||
presentShortcut(shortcutValue: string | undefined, formatting?: boolean): string {
|
||||
if (!shortcutValue) {
|
||||
return '-';
|
||||
}
|
||||
return shortcutValue
|
||||
.split(/\+/g)
|
||||
.map((part) => {
|
||||
switch (part) {
|
||||
case 'Ctrl':
|
||||
return this.ctrlShortcutSymbol(formatting);
|
||||
case 'Alt':
|
||||
return this.altShortcutSymbol(formatting);
|
||||
case 'Shift':
|
||||
return this.shiftShortcutSymbol(formatting);
|
||||
case 'Meta':
|
||||
return this.actionShortcutSymbol(formatting);
|
||||
default:
|
||||
return part;
|
||||
}
|
||||
})
|
||||
.join('');
|
||||
},
|
||||
|
||||
actionShortcutSymbol(formatting?: boolean): string {
|
||||
return Features.isMac ? '⌘' : this.formatShortcut(Locale.ctrlKey, formatting);
|
||||
},
|
||||
|
||||
altShortcutSymbol(formatting?: boolean): string {
|
||||
return Features.isMac ? '⌥' : this.formatShortcut(Locale.altKey, formatting);
|
||||
},
|
||||
|
||||
shiftShortcutSymbol(formatting?: boolean): string {
|
||||
return Features.isMac ? '⇧' : this.formatShortcut(Locale.shiftKey, formatting);
|
||||
},
|
||||
|
||||
ctrlShortcutSymbol(formatting?: boolean): string {
|
||||
return Features.isMac ? '⌃' : this.formatShortcut(Locale.ctrlKey, formatting);
|
||||
},
|
||||
|
||||
formatShortcut(shortcut: string, formatting?: boolean): string {
|
||||
return formatting ? `${shortcut} + ` : `${shortcut}+`;
|
||||
},
|
||||
|
||||
globalShortcutText(type: GlobalShortcutType, formatting?: boolean): string {
|
||||
return this.presentShortcut(this.globalShortcut(type), formatting);
|
||||
},
|
||||
|
||||
globalShortcut(type: GlobalShortcutType): string | undefined {
|
||||
const setting = GlobalShortcutAppSettingsFields[type];
|
||||
const appSettingsShortcut = AppSettings[setting];
|
||||
if (typeof appSettingsShortcut === 'string') {
|
||||
return appSettingsShortcut;
|
||||
}
|
||||
const globalShortcut = AllGlobalShortcuts[type];
|
||||
if (globalShortcut) {
|
||||
if (Features.isMac && globalShortcut.mac) {
|
||||
return globalShortcut.mac;
|
||||
}
|
||||
return globalShortcut.all;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
|
||||
setGlobalShortcut(type: GlobalShortcutType, value: string | null): void {
|
||||
if (!AllGlobalShortcuts[type]) {
|
||||
throw new Error('Bad shortcut: ' + type);
|
||||
}
|
||||
const setting = GlobalShortcutAppSettingsFields[type];
|
||||
if (value) {
|
||||
AppSettings.set(setting, value);
|
||||
} else {
|
||||
AppSettings.delete(setting);
|
||||
}
|
||||
Launcher?.ipcRenderer.invoke('set-global-shortcuts', { [setting]: value });
|
||||
},
|
||||
|
||||
screenshotToClipboardShortcut(): string {
|
||||
if (Features.isiOS) {
|
||||
return 'Sleep+Home';
|
||||
}
|
||||
if (Features.isMobile) {
|
||||
return '';
|
||||
}
|
||||
if (Features.isMac) {
|
||||
return 'Command-Shift-Control-4';
|
||||
}
|
||||
if (Features.isWindows) {
|
||||
return 'Alt+PrintScreen';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
export { Shortcuts };
|
||||
@@ -1,5 +1,5 @@
|
||||
const Backbone = require('backbone');
|
||||
const Launcher = require('./launcher');
|
||||
import { Events } from 'util/events';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
|
||||
const LocalStorageKeyName = 'instanceCheck';
|
||||
const LocalStorageResponseKeyName = 'instanceMaster';
|
||||
@@ -7,32 +7,39 @@ const LocalStorageResponseKeyName = 'instanceMaster';
|
||||
const instanceKey = Date.now().toString();
|
||||
|
||||
const SingleInstanceChecker = {
|
||||
init: function() {
|
||||
init(): void {
|
||||
if (Launcher) {
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
window.addEventListener('storage', SingleInstanceChecker.storageChanged);
|
||||
SingleInstanceChecker.setKey(LocalStorageKeyName, instanceKey);
|
||||
},
|
||||
|
||||
storageChanged: function(e) {
|
||||
storageChanged(e: StorageEvent): void {
|
||||
if (!e.newValue) {
|
||||
return;
|
||||
}
|
||||
if (e.key === LocalStorageKeyName && e.newValue !== instanceKey) {
|
||||
SingleInstanceChecker.setKey(LocalStorageResponseKeyName, instanceKey + Math.random().toString());
|
||||
SingleInstanceChecker.setKey(
|
||||
LocalStorageResponseKeyName,
|
||||
instanceKey + Math.random().toString()
|
||||
);
|
||||
} else if (e.key === LocalStorageResponseKeyName && e.newValue.indexOf(instanceKey) < 0) {
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
window.removeEventListener('storage', SingleInstanceChecker.storageChanged);
|
||||
Backbone.trigger('second-instance');
|
||||
Events.emit('second-instance');
|
||||
}
|
||||
},
|
||||
|
||||
setKey: function(key, value) {
|
||||
setKey(key: string, value: string): void {
|
||||
try {
|
||||
localStorage.setItem(key, value);
|
||||
setTimeout(() => { localStorage.removeItem(key); }, 100);
|
||||
setTimeout(() => {
|
||||
localStorage.removeItem(key);
|
||||
}, 100);
|
||||
} catch (e) {}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = SingleInstanceChecker;
|
||||
export { SingleInstanceChecker };
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Events } from 'util/events';
|
||||
|
||||
const ThemeWatcher = {
|
||||
dark: false,
|
||||
|
||||
init(): void {
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
if (mediaQuery && mediaQuery.addEventListener) {
|
||||
mediaQuery.addEventListener('change', (e) => {
|
||||
const dark = e.matches;
|
||||
this.dark = dark;
|
||||
Events.emit('dark-mode-changed', dark);
|
||||
});
|
||||
}
|
||||
this.dark = mediaQuery.matches;
|
||||
}
|
||||
};
|
||||
|
||||
export { ThemeWatcher };
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Features } from 'util/features';
|
||||
import { AppSettings } from 'models/app-settings';
|
||||
import { Events } from 'util/events';
|
||||
|
||||
export const WindowClass = {
|
||||
init(): void {
|
||||
const browserCssClass = Features.browserCssClass;
|
||||
if (browserCssClass) {
|
||||
document.body.classList.add(browserCssClass);
|
||||
}
|
||||
if (AppSettings.titlebarStyle !== 'default') {
|
||||
document.body.classList.add(`titlebar-${AppSettings.titlebarStyle}`);
|
||||
if (Features.renderCustomTitleBar) {
|
||||
document.body.classList.add('titlebar-custom');
|
||||
}
|
||||
}
|
||||
if (Features.isMobile) {
|
||||
document.body.classList.add('mobile');
|
||||
}
|
||||
|
||||
Events.on('enter-full-screen', () => document.body.classList.add('fullscreen'));
|
||||
Events.on('leave-full-screen', () => document.body.classList.remove('fullscreen'));
|
||||
},
|
||||
|
||||
setThemeClass(theme: string): void {
|
||||
for (const cls of document.body.classList) {
|
||||
if (cls.startsWith('th-')) {
|
||||
document.body.classList.remove(cls);
|
||||
}
|
||||
}
|
||||
document.body.classList.add(`th-${theme}`);
|
||||
},
|
||||
|
||||
setFontSizeClass(fontSize: number): void {
|
||||
for (const cls of document.body.classList) {
|
||||
if (cls.startsWith('font-size-')) {
|
||||
document.body.classList.remove(cls);
|
||||
}
|
||||
}
|
||||
document.body.classList.add(`font-size-${fontSize}`);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
import { ContextMenu, ContextMenuItem } from 'models/menu/context-menu';
|
||||
import { StringFormat } from 'util/formatting/string-format';
|
||||
import { Locale } from 'util/locale';
|
||||
import { Features } from 'util/features';
|
||||
import { Shortcuts } from 'comp/browser/shortcuts';
|
||||
import { Position } from 'util/types';
|
||||
import { FileManager } from 'models/file-manager';
|
||||
import { Entry } from 'models/entry';
|
||||
import { File } from 'models/file';
|
||||
|
||||
export class ContextMenuNew {
|
||||
static show(pos: Position): void {
|
||||
const newEntryMenuItem = new ContextMenuItem(
|
||||
'entry',
|
||||
'key',
|
||||
StringFormat.capFirst(Locale.entry),
|
||||
() => this.newEntryClicked()
|
||||
);
|
||||
if (!Features.isMobile) {
|
||||
const shortcut = Shortcuts.presentShortcut('Alt+N');
|
||||
newEntryMenuItem.hint = `(${Locale.searchShiftClickOr} ${shortcut})`;
|
||||
}
|
||||
|
||||
const newGroupMenuItem = new ContextMenuItem(
|
||||
'group',
|
||||
'folder',
|
||||
StringFormat.capFirst(Locale.group),
|
||||
() => this.newGroupClicked()
|
||||
);
|
||||
|
||||
const templateMenuItems: ContextMenuItem[] = [];
|
||||
const hasMultipleFiles = FileManager.files.length > 1;
|
||||
for (const file of FileManager.files) {
|
||||
for (const entry of file.allEntryTemplates()) {
|
||||
if (!entry.title) {
|
||||
continue;
|
||||
}
|
||||
const id = `tmpl:${entry.id}`;
|
||||
templateMenuItems.push(
|
||||
new ContextMenuItem(
|
||||
id,
|
||||
entry.icon ?? 'sticky-note-o',
|
||||
hasMultipleFiles ? `${file.name} / ${entry.title}` : entry.title,
|
||||
() => this.newFromTemplateClicked(entry, file)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
const collator = new Intl.Collator(undefined, { sensitivity: 'base' });
|
||||
templateMenuItems.sort((x, y) => {
|
||||
return collator.compare(x.title, y.title);
|
||||
});
|
||||
templateMenuItems.push(
|
||||
new ContextMenuItem(
|
||||
'tmpl',
|
||||
'sticky-note-o',
|
||||
StringFormat.capFirst(Locale.template),
|
||||
() => this.newTemplateClicked()
|
||||
)
|
||||
);
|
||||
|
||||
ContextMenu.toggle('new', pos, [newEntryMenuItem, newGroupMenuItem, ...templateMenuItems]);
|
||||
}
|
||||
|
||||
static newEntryClicked(): void {
|
||||
// TODO
|
||||
}
|
||||
|
||||
static newGroupClicked(): void {
|
||||
// TODO
|
||||
}
|
||||
|
||||
static newTemplateClicked(): void {
|
||||
// TODO
|
||||
}
|
||||
|
||||
static newFromTemplateClicked(entry: Entry, file: File): void {
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { ContextMenu, ContextMenuItem } from 'models/menu/context-menu';
|
||||
import { Position } from 'util/types';
|
||||
import { StringFormat } from 'util/formatting/string-format';
|
||||
import { Locale } from 'util/locale';
|
||||
import { QuerySort } from 'models/query';
|
||||
import { Workspace } from 'models/workspace';
|
||||
|
||||
const arrow = '→';
|
||||
|
||||
export class ContextMenuSort {
|
||||
static show(pos: Position): void {
|
||||
const items: ContextMenuItem[] = [
|
||||
new ContextMenuItem(
|
||||
QuerySort.TitleAsc,
|
||||
'sort-alpha-down',
|
||||
StringFormat.capFirst(Locale.title) + ' ' + Locale.searchAZ.with(arrow),
|
||||
() => this.itemClicked(QuerySort.TitleAsc)
|
||||
),
|
||||
new ContextMenuItem(
|
||||
QuerySort.TitleDesc,
|
||||
'sort-alpha-down-alt',
|
||||
StringFormat.capFirst(Locale.title) + ' ' + Locale.searchZA.with(arrow),
|
||||
() => this.itemClicked(QuerySort.TitleDesc)
|
||||
),
|
||||
new ContextMenuItem(
|
||||
QuerySort.WebsiteAsc,
|
||||
'sort-alpha-down',
|
||||
StringFormat.capFirst(Locale.website) + ' ' + Locale.searchAZ.with(arrow),
|
||||
() => this.itemClicked(QuerySort.WebsiteAsc)
|
||||
),
|
||||
new ContextMenuItem(
|
||||
QuerySort.WebsiteDesc,
|
||||
'sort-alpha-down-alt',
|
||||
StringFormat.capFirst(Locale.website) + ' ' + Locale.searchZA.with(arrow),
|
||||
() => this.itemClicked(QuerySort.WebsiteDesc)
|
||||
),
|
||||
new ContextMenuItem(
|
||||
QuerySort.UserAsc,
|
||||
'sort-alpha-down',
|
||||
StringFormat.capFirst(Locale.user) + ' ' + Locale.searchAZ.with(arrow),
|
||||
() => this.itemClicked(QuerySort.UserAsc)
|
||||
),
|
||||
new ContextMenuItem(
|
||||
QuerySort.UserDesc,
|
||||
'sort-alpha-down-alt',
|
||||
StringFormat.capFirst(Locale.user) + ' ' + Locale.searchZA.with(arrow),
|
||||
() => this.itemClicked(QuerySort.UserDesc)
|
||||
),
|
||||
new ContextMenuItem(
|
||||
QuerySort.CreatedAsc,
|
||||
'sort-numeric-down',
|
||||
StringFormat.capFirst(Locale.searchCreated) + ' ' + Locale.searchON.with(arrow),
|
||||
() => this.itemClicked(QuerySort.CreatedAsc)
|
||||
),
|
||||
new ContextMenuItem(
|
||||
QuerySort.CreatedDesc,
|
||||
'sort-numeric-down-alt',
|
||||
StringFormat.capFirst(Locale.searchCreated) + ' ' + Locale.searchNO.with(arrow),
|
||||
() => this.itemClicked(QuerySort.CreatedDesc)
|
||||
),
|
||||
new ContextMenuItem(
|
||||
QuerySort.UpdatedAsc,
|
||||
'sort-numeric-down',
|
||||
StringFormat.capFirst(Locale.searchUpdated) + ' ' + Locale.searchON.with(arrow),
|
||||
() => this.itemClicked(QuerySort.UpdatedAsc)
|
||||
),
|
||||
new ContextMenuItem(
|
||||
QuerySort.UpdatedDesc,
|
||||
'sort-numeric-down-alt',
|
||||
StringFormat.capFirst(Locale.searchUpdated) + ' ' + Locale.searchNO.with(arrow),
|
||||
() => this.itemClicked(QuerySort.UpdatedDesc)
|
||||
),
|
||||
new ContextMenuItem(
|
||||
QuerySort.AttachmentsDesc,
|
||||
'sort-amount-down',
|
||||
Locale.searchAttachments,
|
||||
() => this.itemClicked(QuerySort.AttachmentsDesc)
|
||||
),
|
||||
new ContextMenuItem(QuerySort.RankDesc, 'sort-amount-down', Locale.searchRank, () =>
|
||||
this.itemClicked(QuerySort.RankDesc)
|
||||
)
|
||||
];
|
||||
|
||||
const selectedItem = items.find((it) => it.id === Workspace.query.sort);
|
||||
|
||||
ContextMenu.toggle('sort', pos, items, selectedItem);
|
||||
}
|
||||
|
||||
private static itemClicked(sort: QuerySort) {
|
||||
Workspace.query.sort = sort;
|
||||
}
|
||||
}
|
||||