mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
Compare commits
1370 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8353f7648 | ||
|
|
8d042d2134 | ||
|
|
a406eb83e1 | ||
|
|
531b45210a | ||
|
|
319db64393 | ||
|
|
24f6c90f72 | ||
|
|
3e493263c3 | ||
|
|
916b4ff091 | ||
|
|
dad8f3e695 | ||
|
|
7a46fac41a | ||
|
|
79f3bfab96 | ||
|
|
0d9cc7ef83 | ||
|
|
0ddcc5c1c6 | ||
|
|
5ef6a2b688 | ||
|
|
7766d7053d | ||
|
|
36e15d3809 | ||
|
|
ed14c45fd2 | ||
|
|
12c4b4d711 | ||
|
|
1bdc1164f8 | ||
|
|
71944ab6cd | ||
|
|
984b3dabdc | ||
|
|
9abf136ea2 | ||
|
|
a2f224225e | ||
|
|
6566871b15 | ||
|
|
dda900041a | ||
|
|
cce80ac5aa | ||
|
|
036a3761ea | ||
|
|
48878d2d8c | ||
|
|
dca90916fe | ||
|
|
ba1908d605 | ||
|
|
590bf20204 | ||
|
|
b02a5a265d | ||
|
|
b1b18ee215 | ||
|
|
8e4f7258ff | ||
|
|
2e0db3b90f | ||
|
|
54dc790cb6 | ||
|
|
ed6fe34192 | ||
|
|
10fcbb9990 | ||
|
|
980c1a1060 | ||
|
|
cd9825380d | ||
|
|
3b3a59fd62 | ||
|
|
73f657f84e | ||
|
|
c9baab7ac9 | ||
|
|
72269db8e6 | ||
|
|
ffa974b4e5 | ||
|
|
727aac4b82 | ||
|
|
9e66f71774 | ||
|
|
d511872c00 | ||
|
|
c46a52d1ca | ||
|
|
c04da3ab83 | ||
|
|
cb60492657 | ||
|
|
118d6e2a73 | ||
|
|
918eb0f4a9 | ||
|
|
11e0f8c3a4 | ||
|
|
a99b8399fe | ||
|
|
7e9a626891 | ||
|
|
cecd90e675 | ||
|
|
2d7d025682 | ||
|
|
12fc597b56 | ||
|
|
886cd75ad2 | ||
|
|
2b50c4faae | ||
|
|
9c3dfcd09f | ||
|
|
cb52fa6ab7 | ||
|
|
faeba8119a | ||
|
|
89b338fb86 | ||
|
|
60ef8e648c | ||
|
|
498913e71d | ||
|
|
6c1f7fade8 | ||
|
|
0f829d0b91 | ||
|
|
0717933459 | ||
|
|
516059e390 | ||
|
|
d4f472a64e | ||
|
|
97d7eab1a7 | ||
|
|
6c8add051f | ||
|
|
248da93d05 | ||
|
|
e9dcd8b7ba | ||
|
|
935d41dc26 | ||
|
|
0f0ee9ffde | ||
|
|
b703bcdd52 | ||
|
|
8e127d0339 | ||
|
|
add111fe3f | ||
|
|
bf44637400 | ||
|
|
657e6ae7d3 | ||
|
|
4ddb164bbd | ||
|
|
5c241d7491 | ||
|
|
90360022a4 | ||
|
|
ca5f5cf0e3 | ||
|
|
befcc9b37c | ||
|
|
a46350c76a | ||
|
|
d72a15937d | ||
|
|
56c1b92316 | ||
|
|
91cd27298d | ||
|
|
98bb98c265 | ||
|
|
e970bb4d4e | ||
|
|
ed83da5669 | ||
|
|
7e7f85352b | ||
|
|
013622a2a7 | ||
|
|
fb4da697a2 | ||
|
|
4e56cb4e74 | ||
|
|
649b142b46 | ||
|
|
34257cad60 | ||
|
|
382a77d2c5 | ||
|
|
3745c44455 | ||
|
|
9d9ab1ffd2 | ||
|
|
938a87ceca | ||
|
|
583bf74724 | ||
|
|
26e4529f27 | ||
|
|
e404d02965 | ||
|
|
c610100c13 | ||
|
|
8d4e03bdb8 | ||
|
|
77b631be87 | ||
|
|
e403c41530 | ||
|
|
5b2fb1fb38 | ||
|
|
cf4268d1c3 | ||
|
|
e6a91a9959 | ||
|
|
f22b6449d4 | ||
|
|
58d910dc57 | ||
|
|
11645fdc9a | ||
|
|
177e60739c | ||
|
|
5f992352ee | ||
|
|
216b6f536f | ||
|
|
1e176151a0 | ||
|
|
604fd2be44 | ||
|
|
27c4d35a9b | ||
|
|
a9e6a68369 | ||
|
|
6c1c9fe600 | ||
|
|
af9050c34b | ||
|
|
4c46e16160 | ||
|
|
590b4c82d2 | ||
|
|
19fa107fea | ||
|
|
ea7a2d271d | ||
|
|
55673cdb4b | ||
|
|
25037e3f5f | ||
|
|
94ebbab68a | ||
|
|
4174b5e1e4 | ||
|
|
101e5e6ff1 | ||
|
|
669f3ef404 | ||
|
|
4c7cbadc2f | ||
|
|
2176553b26 | ||
|
|
024eeaf6bf | ||
|
|
83883c4b05 | ||
|
|
b062fc056f | ||
|
|
3e32fca356 | ||
|
|
9c2f2398f1 | ||
|
|
6d6003fd80 | ||
|
|
f9df44a2cd | ||
|
|
7c670de7c2 | ||
|
|
20854ba5d7 | ||
|
|
2e8bb357fc | ||
|
|
810725cc6b | ||
|
|
8d7fa1d397 | ||
|
|
6b6a9a9935 | ||
|
|
8e80c3bde6 | ||
|
|
de0d022268 | ||
|
|
8092a440f4 | ||
|
|
d78d313372 | ||
|
|
1a7957bee6 | ||
|
|
23caeb477c | ||
|
|
2856803371 | ||
|
|
ffb37d3e12 | ||
|
|
bb1dfcde9b | ||
|
|
1594683191 | ||
|
|
0d4775bb7d | ||
|
|
a08a45640d | ||
|
|
d1518a490f | ||
|
|
f95d41e093 | ||
|
|
a07af87cb1 | ||
|
|
e634e2cbff | ||
|
|
c8cebc2b18 | ||
|
|
e01869afda | ||
|
|
b4ec480405 | ||
|
|
12ab11a4e4 | ||
|
|
bb3c8fbe0c | ||
|
|
20930617bb | ||
|
|
0f34829dab | ||
|
|
4221797569 | ||
|
|
437b7da0f7 | ||
|
|
31f5698437 | ||
|
|
666d525243 | ||
|
|
9066913710 | ||
|
|
ebc1a85190 | ||
|
|
4c44877af4 | ||
|
|
c03c778efb | ||
|
|
b6339d38fa | ||
|
|
42e04d459b | ||
|
|
769179d04b | ||
|
|
a7c67bbb35 | ||
|
|
b869eb8d1d | ||
|
|
7d9de4b0d2 | ||
|
|
a49eb464c8 | ||
|
|
48f67b1886 | ||
|
|
ce4a37b292 | ||
|
|
5623bf253b | ||
|
|
38c0fae064 | ||
|
|
28065f7231 | ||
|
|
bb841fcc3b | ||
|
|
1c29fb2db3 | ||
|
|
229987ee90 | ||
|
|
533a58d083 | ||
|
|
7958b17968 | ||
|
|
a773b51ada | ||
|
|
05f15d9831 | ||
|
|
c6c413202c | ||
|
|
f1b80ab90f | ||
|
|
558240279f | ||
|
|
3492aac2c0 | ||
|
|
fc93d0904e | ||
|
|
2adbc334a2 | ||
|
|
05c8dd2fe1 | ||
|
|
88fd1e56d0 | ||
|
|
49f89e33b5 | ||
|
|
d0a47c84b9 | ||
|
|
bfaff5cca5 | ||
|
|
30d7f67d42 | ||
|
|
7e332fa615 | ||
|
|
3d8fd1d352 | ||
|
|
db0c86d50c | ||
|
|
6e17d5d756 | ||
|
|
e2ac32139c | ||
|
|
348a83a1d9 | ||
|
|
4cf942ac3a | ||
|
|
f632a4b773 | ||
|
|
e2fa122362 | ||
|
|
6a65f8090f | ||
|
|
637d926f7a | ||
|
|
7f5efc5ca1 | ||
|
|
1885a48241 | ||
|
|
f0584ac853 | ||
|
|
0f269455b1 | ||
|
|
25aa5bf2af | ||
|
|
92551c58a5 | ||
|
|
1bfd147421 | ||
|
|
26ad473592 | ||
|
|
10d3ba268d | ||
|
|
7883e7a0a9 | ||
|
|
da4b961686 | ||
|
|
fb84cb03f3 | ||
|
|
c9e3c628e8 | ||
|
|
fbeaafa8d3 | ||
|
|
68454a8a6e | ||
|
|
bd880d304e | ||
|
|
386737acac | ||
|
|
3690042d2c | ||
|
|
c00d804073 | ||
|
|
e2abc1b265 | ||
|
|
3d9ef2c63f | ||
|
|
b13c29ab91 | ||
|
|
df4f08b071 | ||
|
|
83cab764a1 | ||
|
|
b784a14aee | ||
|
|
629c2eb4c6 | ||
|
|
c73689fbc3 | ||
|
|
1298b76b77 | ||
|
|
4b6ee4ea98 | ||
|
|
10d353defe | ||
|
|
9ca7c8431b | ||
|
|
a8d3cdf62c | ||
|
|
764c674350 | ||
|
|
f31abe83b0 | ||
|
|
4fe3b55720 | ||
|
|
8e96d1a461 | ||
|
|
9f652c33b4 | ||
|
|
d5a2695766 | ||
|
|
ced824d791 | ||
|
|
0131b92c7a | ||
|
|
a3d1c43580 | ||
|
|
66fe0b8245 | ||
|
|
adfd5425f0 | ||
|
|
8fde82a0eb | ||
|
|
787ccad384 | ||
|
|
693bae7631 | ||
|
|
f6d5a27e0b | ||
|
|
592440f2b1 | ||
|
|
b84ca9b283 | ||
|
|
7445d54d22 | ||
|
|
f37d4c47cc | ||
|
|
02a29c6af4 | ||
|
|
7fa78f0304 | ||
|
|
e3527f47ea | ||
|
|
e6c4cc25e5 | ||
|
|
1e3306ef28 | ||
|
|
6f17aa21c2 | ||
|
|
b8acead444 | ||
|
|
eb261f503b | ||
|
|
cc397d45a9 | ||
|
|
01bbb54054 | ||
|
|
2b7c2048ef | ||
|
|
b297f3d3a3 | ||
|
|
a3b7c6808b | ||
|
|
a1f82e1652 | ||
|
|
690d33ece3 | ||
|
|
d7579cce9e | ||
|
|
e468c48da4 | ||
|
|
e6cae1a79b | ||
|
|
95122e91f5 | ||
|
|
bf2acfec38 | ||
|
|
9e1b8c1a72 | ||
|
|
d7dd89d719 | ||
|
|
20a865f012 | ||
|
|
87676d2994 | ||
|
|
001dad9a79 | ||
|
|
a412c7978a | ||
|
|
8e8e9d35ac | ||
|
|
545c4208d9 | ||
|
|
50f5e850dd | ||
|
|
b9a43147dc | ||
|
|
ae3559ca74 | ||
|
|
1a373e1c40 | ||
|
|
d122276e71 | ||
|
|
156cb3055c | ||
|
|
6aa35d592e | ||
|
|
b290055e83 | ||
|
|
c051c5cd50 | ||
|
|
06711d3b8b | ||
|
|
4f2db28196 | ||
|
|
3681340587 | ||
|
|
9e182a2e47 | ||
|
|
e5736567f6 | ||
|
|
a2d12eb225 | ||
|
|
5ae159de5b | ||
|
|
8c98aedac2 | ||
|
|
7ba4f8de4a | ||
|
|
6f6292494c | ||
|
|
793476432b | ||
|
|
9d7ac851bd | ||
|
|
54e693d0e2 | ||
|
|
969b3ccbcd | ||
|
|
8a2edf274a | ||
|
|
171eb2dd8b | ||
|
|
76cc22d5af | ||
|
|
2be10eb4ca | ||
|
|
297cb5c3ef | ||
|
|
9a20b0e0de | ||
|
|
66fb1d7c60 | ||
|
|
5ed94c8abc | ||
|
|
6213193cda | ||
|
|
5b0a30774e | ||
|
|
d08ee30a7a | ||
|
|
167570559f | ||
|
|
e376da4482 | ||
|
|
1b7a475631 | ||
|
|
5fcd01224c | ||
|
|
bc1f9d3734 | ||
|
|
c0369e6e76 | ||
|
|
3cc4f13f4d | ||
|
|
c27817eaf0 | ||
|
|
f239866c68 | ||
|
|
7f0851bcb1 | ||
|
|
652ec40d82 | ||
|
|
d56d85b702 | ||
|
|
9be546d6c7 | ||
|
|
aab213a6b9 | ||
|
|
89505fce30 | ||
|
|
d106c5144b | ||
|
|
175571a130 | ||
|
|
d363cf8137 | ||
|
|
ec41ec2377 | ||
|
|
64e9150f11 | ||
|
|
08bf147559 | ||
|
|
ca94ef58e8 | ||
|
|
b34851bd12 | ||
|
|
5b6ea73513 | ||
|
|
2d00ab1d3e | ||
|
|
e068104253 | ||
|
|
5e2536c129 | ||
|
|
f72e4692e4 | ||
|
|
a76e2fea7f | ||
|
|
09425e1c43 | ||
|
|
ba7de5d33d | ||
|
|
2770dfde56 | ||
|
|
9b2d36b78a | ||
|
|
3bd6906c80 | ||
|
|
ac62ebadf0 | ||
|
|
54622d6759 | ||
|
|
0a7ed3b52f | ||
|
|
0e366a6269 | ||
|
|
7350c1d4d5 | ||
|
|
d87650de08 | ||
|
|
4865224458 | ||
|
|
49d5cd36df | ||
|
|
130033cf4b | ||
|
|
c293a273ca | ||
|
|
90c73e5e3d | ||
|
|
fbc88bb4cd | ||
|
|
1e8819af0c | ||
|
|
763fee0c14 | ||
|
|
71b8144c3c | ||
|
|
bfd10f6903 | ||
|
|
10c79da35c | ||
|
|
d7d88a9ebb | ||
|
|
a32b9ca8c0 | ||
|
|
7de420e32b | ||
|
|
dc1f0f1095 | ||
|
|
18a3ae7bff | ||
|
|
35867a0a00 | ||
|
|
a05e09908c | ||
|
|
6fca1c919e | ||
|
|
d7e1ad73cc | ||
|
|
7e56c8a011 | ||
|
|
d6bc053950 | ||
|
|
813be4af2b | ||
|
|
df1a272090 | ||
|
|
f1ae421853 | ||
|
|
99b7bdab27 | ||
|
|
fbb9e09237 | ||
|
|
21a6fdd100 | ||
|
|
2f52c1aa1b | ||
|
|
56d6234f1c | ||
|
|
81dcadb959 | ||
|
|
89d726de2a | ||
|
|
9211fe476d | ||
|
|
3116f3d363 | ||
|
|
846cc65d8e | ||
|
|
7a7760c3dd | ||
|
|
78e5bf2a60 | ||
|
|
a2c11af82b | ||
|
|
89fca85ca8 | ||
|
|
536f1a392e | ||
|
|
5d843ecae8 | ||
|
|
bbc12ec041 | ||
|
|
72dae631fe | ||
|
|
245c492906 | ||
|
|
3a8c64b02d | ||
|
|
7a5e55e5f0 | ||
|
|
16895716e9 | ||
|
|
9030bffdbd | ||
|
|
4bea4101a1 | ||
|
|
ec182ade39 | ||
|
|
35a5e27e2d | ||
|
|
9a597f2b4d | ||
|
|
fc2e3fec63 | ||
|
|
0f6245a072 | ||
|
|
86581e6a00 | ||
|
|
330e6c09e7 | ||
|
|
6c695f09e7 | ||
|
|
b05c2c1e1a | ||
|
|
820304ce19 | ||
|
|
1017501153 | ||
|
|
256ae5c4b0 | ||
|
|
a03f62832a | ||
|
|
dd07baff59 | ||
|
|
647c55eaf7 | ||
|
|
1fea8edb74 | ||
|
|
2dd416028d | ||
|
|
3f0f118c8b | ||
|
|
e50a2772f8 | ||
|
|
354b04bea4 | ||
|
|
60ec9b5c49 | ||
|
|
83f02f5c91 | ||
|
|
5c16b86edf | ||
|
|
80c283a370 | ||
|
|
23d0b73e20 | ||
|
|
8bac530504 | ||
|
|
8321e400c1 | ||
|
|
552b6547db | ||
|
|
e10b9254ed | ||
|
|
6841ad5410 | ||
|
|
f7b3a7c812 | ||
|
|
3596f59430 | ||
|
|
43e3376d2e | ||
|
|
18c8a04cbc | ||
|
|
0355993b00 | ||
|
|
e204daa844 | ||
|
|
55f9857dec | ||
|
|
e571204a84 | ||
|
|
988db485a2 | ||
|
|
f3cb85ea59 | ||
|
|
5695200940 | ||
|
|
44ca9c3480 | ||
|
|
b5c860db1d | ||
|
|
ef69e329fc | ||
|
|
9321a7df14 | ||
|
|
684fafdfdd | ||
|
|
c2e797615d | ||
|
|
f9facda521 | ||
|
|
1906d68541 | ||
|
|
cea1402dde | ||
|
|
b07420a193 | ||
|
|
9be8b71f09 | ||
|
|
edc76faeb4 | ||
|
|
e990e0ff5b | ||
|
|
336e51d104 | ||
|
|
c79e4831e1 | ||
|
|
daf35a4972 | ||
|
|
495ace1246 | ||
|
|
51df66e362 | ||
|
|
11f5ea5074 | ||
|
|
c2fcf648e0 | ||
|
|
2ae57ae34c | ||
|
|
0e0ef0e5a2 | ||
|
|
d4a757be49 | ||
|
|
08a571f076 | ||
|
|
f3998a190c | ||
|
|
6eeef62568 | ||
|
|
e63c4f3b6e | ||
|
|
6462023d58 | ||
|
|
110f571b40 | ||
|
|
eaa90b31d8 | ||
|
|
29b675b26a | ||
|
|
889ed58f0c | ||
|
|
7fc30aea93 | ||
|
|
45275f0a2c | ||
|
|
15f31a1698 | ||
|
|
bd186e7fe1 | ||
|
|
190e0e179f | ||
|
|
103d3832a0 | ||
|
|
e1079396c3 | ||
|
|
ca164b37db | ||
|
|
f40255bc8a | ||
|
|
8e837b8e56 | ||
|
|
2d8c7a39ca | ||
|
|
8bc60e55e0 | ||
|
|
45bb6e94fc | ||
|
|
e0930924b1 | ||
|
|
b898c466aa | ||
|
|
c7e26d3ab7 | ||
|
|
4fe8d8c233 | ||
|
|
cf8096f276 | ||
|
|
b501e52f5d | ||
|
|
9b0498974c | ||
|
|
430216a345 | ||
|
|
3f6bebd540 | ||
|
|
96d7d18f6d | ||
|
|
4f49077bee | ||
|
|
33aef7eb54 | ||
|
|
cdf3905a51 | ||
|
|
c17db8c9b4 | ||
|
|
34a58bc5da | ||
|
|
7ca37697d4 | ||
|
|
978b18570a | ||
|
|
0c9dd46239 | ||
|
|
860ef6081b | ||
|
|
1303ae5682 | ||
|
|
d1c2298be9 | ||
|
|
7ffc5f7864 | ||
|
|
33a9654fe0 | ||
|
|
1dc1643071 | ||
|
|
48e845c0b1 | ||
|
|
738a27e9b6 | ||
|
|
5d297c1f19 | ||
|
|
80b0b6031e | ||
|
|
ec7b481d27 | ||
|
|
7208b72561 | ||
|
|
5976ddeb72 | ||
|
|
2d5626b0cd | ||
|
|
c9ca9ad4ba | ||
|
|
f848d1b7b2 | ||
|
|
56bdfa72d1 | ||
|
|
668d89018d | ||
|
|
6d0288102d | ||
|
|
8ec5026e1b | ||
|
|
c8dedd50d5 | ||
|
|
ffb7c25635 | ||
|
|
96b93c904c | ||
|
|
634a6aa3bb | ||
|
|
09366f5bed | ||
|
|
9ee123b028 | ||
|
|
8392a025f3 | ||
|
|
563ae0eeaf | ||
|
|
04368c62e7 | ||
|
|
2c7ffd979e | ||
|
|
fba475c170 | ||
|
|
20b4e22746 | ||
|
|
b58cf3677d | ||
|
|
90d35718b0 | ||
|
|
d4a6ff0acf | ||
|
|
ca01d15dde | ||
|
|
19848aafb0 | ||
|
|
82106ccc0f | ||
|
|
e05c94d9e4 | ||
|
|
493992e25c | ||
|
|
e1cc50c579 | ||
|
|
e784c8dbf5 | ||
|
|
9a2f813ea2 | ||
|
|
3906fd46af | ||
|
|
5108114f2f | ||
|
|
2e2d7fe515 | ||
|
|
72b4ff31ed | ||
|
|
6e873abc7b | ||
|
|
528687cf63 | ||
|
|
e73023509e | ||
|
|
0f243fae7f | ||
|
|
054135df55 | ||
|
|
afafd6dc0b | ||
|
|
9ac32b0bee | ||
|
|
54e4c1333d | ||
|
|
233f8cd0f3 | ||
|
|
6b60373886 | ||
|
|
d8ef49139d | ||
|
|
a9b5b3aa38 | ||
|
|
2f9c101222 | ||
|
|
b07763ad05 | ||
|
|
922c3d5611 | ||
|
|
ebdea4fadf | ||
|
|
6751942179 | ||
|
|
b097aa0c29 | ||
|
|
0228e6e488 | ||
|
|
ce8b544ff5 | ||
|
|
21299d153f | ||
|
|
9d3a0f0b70 | ||
|
|
f1628eec66 | ||
|
|
9f8ff12155 | ||
|
|
d8f8de2abe | ||
|
|
4b41578024 | ||
|
|
693636c942 | ||
|
|
556dce9bed | ||
|
|
7264f5493f | ||
|
|
cdd5bf1a76 | ||
|
|
c00d8db5ad | ||
|
|
4a0f126c34 | ||
|
|
4cbc9f18c7 | ||
|
|
f244812be1 | ||
|
|
db84dff4e6 | ||
|
|
361c2a4a24 | ||
|
|
bf07f2e72c | ||
|
|
c895b2acb0 | ||
|
|
063836266d | ||
|
|
2a1584e2dd | ||
|
|
8c6f9c9875 | ||
|
|
b612729426 | ||
|
|
cc42cd6e45 | ||
|
|
3727bff865 | ||
|
|
e06e6e2b5a | ||
|
|
3afc52187f | ||
|
|
2aa07a25a7 | ||
|
|
52c08c03e1 | ||
|
|
99f2b313cd | ||
|
|
3e0539f8c6 | ||
|
|
fb8daa4ed9 | ||
|
|
04bd9ddc2c | ||
|
|
9221435bad | ||
|
|
31c8e850cd | ||
|
|
7b08c965ec | ||
|
|
f08466245a | ||
|
|
93af079316 | ||
|
|
3c41823928 | ||
|
|
5a74b8458b | ||
|
|
cc2df8d680 | ||
|
|
defe62b54b | ||
|
|
37c529e012 | ||
|
|
1df6ac00b0 | ||
|
|
0a848a32f7 | ||
|
|
09d8cefcef | ||
|
|
15b8747280 | ||
|
|
4ae58c8890 | ||
|
|
908ed0cff5 | ||
|
|
b0694e97aa | ||
|
|
b9c18e7d4f | ||
|
|
9f5d578aea | ||
|
|
2d1aa2333b | ||
|
|
8f88998d34 | ||
|
|
5353d8be81 | ||
|
|
dcc0866de1 | ||
|
|
2a5ac092f0 | ||
|
|
dfe96160ed | ||
|
|
522d7debe8 | ||
|
|
3c7f85eda4 | ||
|
|
d9ee6665ef | ||
|
|
4db825debd | ||
|
|
7664944ff2 | ||
|
|
ec2af81e62 | ||
|
|
ee4dc394d4 | ||
|
|
9f7aca4a9b | ||
|
|
80c0920f10 | ||
|
|
6a01ca403f | ||
|
|
7084d5a558 | ||
|
|
afc969d00f | ||
|
|
53255c3a9c | ||
|
|
443d4f7c02 | ||
|
|
71e0374640 | ||
|
|
c42628b4fa | ||
|
|
b637e0a028 | ||
|
|
14af7af2d1 | ||
|
|
277767f101 | ||
|
|
14282b6734 | ||
|
|
54a2817962 | ||
|
|
97fb8d680f | ||
|
|
65cfb29cfd | ||
|
|
c6963aa1d3 | ||
|
|
0564e5814b | ||
|
|
c42f84f246 | ||
|
|
f8a6b02c80 | ||
|
|
9bcb1b2939 | ||
|
|
76f834ff1b | ||
|
|
e1150a4226 | ||
|
|
4d22e6c352 | ||
|
|
0b6309f175 | ||
|
|
c604df96e9 | ||
|
|
20f6a74fbe | ||
|
|
74306f0a76 | ||
|
|
3cd7c3ce00 | ||
|
|
3cfe122c7b | ||
|
|
4f423c47cd | ||
|
|
cbbd09b8a5 | ||
|
|
1946afbfe8 | ||
|
|
e1b01dc407 | ||
|
|
527c27f675 | ||
|
|
e63941275e | ||
|
|
16603697a5 | ||
|
|
1fed54b615 | ||
|
|
cf40045acd | ||
|
|
161c5462ba | ||
|
|
852b2f517b | ||
|
|
8142ef1dee | ||
|
|
5ff8a5b1d5 | ||
|
|
5161bbe660 | ||
|
|
69c4d19f8b | ||
|
|
c83a29fb18 | ||
|
|
113baac567 | ||
|
|
d57eca279f | ||
|
|
282581940a | ||
|
|
63009ba332 | ||
|
|
6579a4647d | ||
|
|
22ffc80aeb | ||
|
|
1da7e1ef35 | ||
|
|
ea7833efe4 | ||
|
|
08f17c4f05 | ||
|
|
b6e2740746 | ||
|
|
3221f930c4 | ||
|
|
e0240784b5 | ||
|
|
2b4010720a | ||
|
|
0f8553b45d | ||
|
|
d8740768c7 | ||
|
|
269d21a4c0 | ||
|
|
566bd04e3f | ||
|
|
bef1feea0a | ||
|
|
f808469285 | ||
|
|
63f86613c3 | ||
|
|
067a51775e | ||
|
|
d9cc67ce6d | ||
|
|
59e4fb82b9 | ||
|
|
a9a705f118 | ||
|
|
6409c193c4 | ||
|
|
950a4a00a6 | ||
|
|
67c940f3a8 | ||
|
|
324b3eb081 | ||
|
|
d4a1953747 | ||
|
|
b348debc72 | ||
|
|
fc01ddbb1c | ||
|
|
7f797b1039 | ||
|
|
3bd76b2dfd | ||
|
|
8d03fcee2e | ||
|
|
9149e3f201 | ||
|
|
6d99416641 | ||
|
|
6e0f2fc4ce | ||
|
|
fe3339f645 | ||
|
|
11e05f73d9 | ||
|
|
566572b013 | ||
|
|
8b60d26f5e | ||
|
|
50924e4189 | ||
|
|
cc68183940 | ||
|
|
55164ef60f | ||
|
|
de568140d2 | ||
|
|
445a14278b | ||
|
|
1d3ab39dca | ||
|
|
35c8e39beb | ||
|
|
c3a598464f | ||
|
|
0a01d85f40 | ||
|
|
52bb909f0b | ||
|
|
59a7a2c807 | ||
|
|
6beeecb0f7 | ||
|
|
c2157e2df1 | ||
|
|
d4e672d630 | ||
|
|
f5f256809c | ||
|
|
aee46e8640 | ||
|
|
9ce911771e | ||
|
|
af3aaa8ce7 | ||
|
|
b9e0669c46 | ||
|
|
71eaf2d789 | ||
|
|
c02ef0e76e | ||
|
|
6044fb4ce1 | ||
|
|
df0f8e01b9 | ||
|
|
1e4ff43bfc | ||
|
|
ad1d42b5dd | ||
|
|
bea1078d20 | ||
|
|
71c8fce4a0 | ||
|
|
8e898de968 | ||
|
|
ed0524d207 | ||
|
|
55589f872a | ||
|
|
90eec91c4e | ||
|
|
8b3f5aa536 | ||
|
|
b639960b8c | ||
|
|
545d12c57d | ||
|
|
b7096ddc32 | ||
|
|
4f269d3fa9 | ||
|
|
fea899d09a | ||
|
|
f77f14b0ee | ||
|
|
ac7da2d2d6 | ||
|
|
5e8acf7d74 | ||
|
|
634bdc5bd6 | ||
|
|
2ee53dfbf7 | ||
|
|
ff2d5e89f9 | ||
|
|
d71d3a5094 | ||
|
|
e931f3ae41 | ||
|
|
1c76114bb0 | ||
|
|
d7e09abde3 | ||
|
|
ba0a113cc9 | ||
|
|
aed134f589 | ||
|
|
06b3219dcb | ||
|
|
85ffd6024b | ||
|
|
0682e9bed1 | ||
|
|
b9418f1272 | ||
|
|
4652392c71 | ||
|
|
b6ecfdcf63 | ||
|
|
96e6882ec1 | ||
|
|
365dd5ee17 | ||
|
|
9f63291fda | ||
|
|
34cba653f8 | ||
|
|
010b6dde79 | ||
|
|
13cf433889 | ||
|
|
7af3c5febf | ||
|
|
d3824f274d | ||
|
|
48822fffa7 | ||
|
|
ff7274fe3c | ||
|
|
7e68d9a49b | ||
|
|
fe4984a9ae | ||
|
|
0e4c3208be | ||
|
|
50377388dd | ||
|
|
56128a12a8 | ||
|
|
81ae8a4687 | ||
|
|
2e99ff96c0 | ||
|
|
5d0bf8f882 | ||
|
|
b03dae638f | ||
|
|
976c30e0d2 | ||
|
|
c4090bbfe1 | ||
|
|
f562e159e8 | ||
|
|
6c2f457e0a | ||
|
|
5f2dd1a6a2 | ||
|
|
9b8c1c42c9 | ||
|
|
d400cda33b | ||
|
|
d59ed3248e | ||
|
|
d5813ef3fc | ||
|
|
78fd3ca2af | ||
|
|
7ca0dba461 | ||
|
|
6eb703ffff | ||
|
|
09781b1328 | ||
|
|
10eb5ad104 | ||
|
|
3b3fe5201b | ||
|
|
890b115b37 | ||
|
|
b924a79ccb | ||
|
|
1cce95ef6a | ||
|
|
2018956034 | ||
|
|
c3dc8e551e | ||
|
|
d008b391e0 | ||
|
|
30f7ae0d67 | ||
|
|
8e942c0e21 | ||
|
|
5ed017b026 | ||
|
|
177da1a4d7 | ||
|
|
363c0fb2a0 | ||
|
|
e75edcaeb8 | ||
|
|
e96a4cf59d | ||
|
|
e116668ef9 | ||
|
|
3bf1887362 | ||
|
|
3e1f962acf | ||
|
|
539a852363 | ||
|
|
aece803905 | ||
|
|
47089bdaab | ||
|
|
a34b8f2630 | ||
|
|
fa6301b2de | ||
|
|
37d6cea2d3 | ||
|
|
e3e8eff5c9 | ||
|
|
07415913ed | ||
|
|
34238051cf | ||
|
|
77a7c6ec75 | ||
|
|
a858aa8f55 | ||
|
|
af50c7bdc3 | ||
|
|
1e84f0f604 | ||
|
|
ab0e664073 | ||
|
|
e08b731548 | ||
|
|
c15ff61f92 | ||
|
|
dae16b9b8d | ||
|
|
f3bca63232 | ||
|
|
50e570eb36 | ||
|
|
970aa5eefe | ||
|
|
7cc0d232be | ||
|
|
50be508a8b | ||
|
|
fd4efe009c | ||
|
|
207355e41d | ||
|
|
8c56c0bddf | ||
|
|
6317d0eec6 | ||
|
|
eb81f7174f | ||
|
|
ed5749543a | ||
|
|
05e01ab9ac | ||
|
|
44fbc6eed5 | ||
|
|
69d5e33150 | ||
|
|
b5baf3ebea | ||
|
|
b393d36186 | ||
|
|
6eb52b8961 | ||
|
|
6ec659e3ef | ||
|
|
c0c7546cfc | ||
|
|
79f81a6274 | ||
|
|
42b2fd17a9 | ||
|
|
bdf5ccd3a9 | ||
|
|
88a6e59f46 | ||
|
|
d7d3549c82 | ||
|
|
071b75bcb5 | ||
|
|
fc6ff5a7a0 | ||
|
|
19f37f4c3a | ||
|
|
78b4c7db85 | ||
|
|
efcce81536 | ||
|
|
2316c36a05 | ||
|
|
3cdf9197b9 | ||
|
|
354e5ecf99 | ||
|
|
2e0dd1cfa1 | ||
|
|
df9be30394 | ||
|
|
3b1fe2119c | ||
|
|
a6a28ce8b1 | ||
|
|
44d9eb8556 | ||
|
|
43f7cecaff | ||
|
|
2eeb097194 | ||
|
|
3159e67c9d | ||
|
|
bb946b707a | ||
|
|
27002c5035 | ||
|
|
50de9ba6d6 | ||
|
|
4c560210cd | ||
|
|
5cd7ae72f4 | ||
|
|
3f1862775e | ||
|
|
dd1f66ea01 | ||
|
|
130202108c | ||
|
|
ebcd7d5915 | ||
|
|
fdbb183064 | ||
|
|
a8e6ead59f | ||
|
|
f387eba946 | ||
|
|
67ec700caf | ||
|
|
d537ce22f8 | ||
|
|
d9e89ad610 | ||
|
|
100f753e9b | ||
|
|
b16cd145c1 | ||
|
|
ed4fdb7561 | ||
|
|
c5373f6abf | ||
|
|
b3d2fea6d8 | ||
|
|
b60cca42ce | ||
|
|
1a8227a939 | ||
|
|
d0c733b1b3 | ||
|
|
48d08984ec | ||
|
|
7a031e384c | ||
|
|
4d5a8ccb80 | ||
|
|
e36d5d8ce7 | ||
|
|
b1e352dd54 | ||
|
|
fe3babd4fc | ||
|
|
bad7db69dc | ||
|
|
2856da83c1 | ||
|
|
0f510b153c | ||
|
|
236bff1c10 | ||
|
|
a63c8b036b | ||
|
|
727390db4c | ||
|
|
988a3bc09e | ||
|
|
e391965acb | ||
|
|
770eec5296 | ||
|
|
eb2b8d06a1 | ||
|
|
7c41a37daa | ||
|
|
5c2127ec52 | ||
|
|
edb145091d | ||
|
|
c23eb505d1 | ||
|
|
4fcc12d717 | ||
|
|
166591002c | ||
|
|
63aca6ea37 | ||
|
|
53174091d6 | ||
|
|
052bbb9d75 | ||
|
|
b85b2ec30a | ||
|
|
c33f72db7e | ||
|
|
1e9eaf5db8 | ||
|
|
a6c038ff5a | ||
|
|
c71da5413a | ||
|
|
15d979bc2c | ||
|
|
a0dff3fbc1 | ||
|
|
8e6eaf728d | ||
|
|
aedfbfc632 | ||
|
|
d1a5cc8e84 | ||
|
|
d7d17a93cd | ||
|
|
5b2b272f76 | ||
|
|
80b7775412 | ||
|
|
446c61fac6 | ||
|
|
c06e6d1a6b | ||
|
|
d3f5274b24 | ||
|
|
b440aaab37 | ||
|
|
ae206c35a5 | ||
|
|
e1f9d60eb9 | ||
|
|
352c0f875d | ||
|
|
d7b13276a2 | ||
|
|
650a156736 | ||
|
|
5bfdc583b1 | ||
|
|
07947c5e1e | ||
|
|
71043be078 | ||
|
|
2a4bd508c1 | ||
|
|
ce6c2cb1b2 | ||
|
|
a8bc55dcb5 | ||
|
|
1bb7286920 | ||
|
|
4e4b59ebda | ||
|
|
09883130ce | ||
|
|
3fc0abfbab | ||
|
|
e965b56824 | ||
|
|
0c1fcad06f | ||
|
|
6318bbe187 | ||
|
|
01e749ac88 | ||
|
|
5b8b84a34c | ||
|
|
ea25a661ee | ||
|
|
dafb0caac4 | ||
|
|
077a86e1b4 | ||
|
|
0290675e11 | ||
|
|
9e868e7e3f | ||
|
|
19cb3eaf6a | ||
|
|
8e6a410cfc | ||
|
|
bdb3616977 | ||
|
|
a3a0c59fa3 | ||
|
|
3e9e91d71c | ||
|
|
104b4b2680 | ||
|
|
c5dd19ef30 | ||
|
|
135fd03f88 | ||
|
|
64f3738c77 | ||
|
|
73a6fb6f65 | ||
|
|
7c3c7680c0 | ||
|
|
a9486362a8 | ||
|
|
1d2a7bf85a | ||
|
|
d6310bc4f0 | ||
|
|
03c1e58922 | ||
|
|
71e975a68c | ||
|
|
fd9790147b | ||
|
|
bafb5d7b0a | ||
|
|
f464f2a6e7 | ||
|
|
35a38f6c33 | ||
|
|
9f8582cd7a | ||
|
|
c83921f4f9 | ||
|
|
059dcdfc11 | ||
|
|
c9de2f02f8 | ||
|
|
b1832983a5 | ||
|
|
d68964bfa2 | ||
|
|
c4cbf941cf | ||
|
|
9be2ef0924 | ||
|
|
107da050a5 | ||
|
|
6143c99454 | ||
|
|
68fc103d7f | ||
|
|
1b05643b64 | ||
|
|
e6a38ad6e2 | ||
|
|
fe7985d564 | ||
|
|
9b02e1f74e | ||
|
|
60d5966863 | ||
|
|
4e57e3a66c | ||
|
|
247355e142 | ||
|
|
1bc80d5dd9 | ||
|
|
f064a815cf | ||
|
|
d2dc307bc0 | ||
|
|
b18959514d | ||
|
|
64a3aa583f | ||
|
|
40e5c04ca1 | ||
|
|
7487368abc | ||
|
|
f7d41eb81b | ||
|
|
8a54cc20ce | ||
|
|
7b726d86d3 | ||
|
|
368883779e | ||
|
|
bc8e77ae02 | ||
|
|
548435b4fd | ||
|
|
b18d0a8bdb | ||
|
|
054ee4cdbe | ||
|
|
b28bb38f52 | ||
|
|
af92bda06d | ||
|
|
1c652320e6 | ||
|
|
76972d3849 | ||
|
|
12b51eb239 | ||
|
|
42dc582ec1 | ||
|
|
52da1c97d9 | ||
|
|
4dd7833603 | ||
|
|
00a80ae7a7 | ||
|
|
f3ce6975c9 | ||
|
|
5e026c0c20 | ||
|
|
9e24d2cb91 | ||
|
|
be15b836fc | ||
|
|
30d10bb37f | ||
|
|
43c74f04ac | ||
|
|
93bc231076 | ||
|
|
89214fb58a | ||
|
|
07f440768e | ||
|
|
19c81049dd | ||
|
|
58a47aa35d | ||
|
|
bb76e4e601 | ||
|
|
4c7f966e7c | ||
|
|
27daffe515 | ||
|
|
4b6231ba8b | ||
|
|
6a22a16997 | ||
|
|
f30c6fc577 | ||
|
|
d9a30f6450 | ||
|
|
c06fdb9134 | ||
|
|
34f53fd6b0 | ||
|
|
9367dfc180 | ||
|
|
7784fe439c | ||
|
|
7ae91b1af3 | ||
|
|
566b7d3f0a | ||
|
|
4fb2e3d14e | ||
|
|
3c2581d7f1 | ||
|
|
31cd8949fe | ||
|
|
818aac715e | ||
|
|
e79239eddc | ||
|
|
b31a7408d3 | ||
|
|
fba42fff17 | ||
|
|
16fd256fd6 | ||
|
|
44304f25d6 | ||
|
|
aaa8e4dc78 | ||
|
|
ceb05e8651 | ||
|
|
281e042ab7 | ||
|
|
8f43de6967 | ||
|
|
3eb5fead1f | ||
|
|
78ffeb8cf2 | ||
|
|
74d54990c9 | ||
|
|
1415df2003 | ||
|
|
a77c80f7c1 | ||
|
|
eb7a89292b | ||
|
|
b7d9f4cedb | ||
|
|
222d4919a7 | ||
|
|
a0a20f0599 | ||
|
|
cce8d1ac23 | ||
|
|
a34a4790b7 | ||
|
|
b6cca4547c | ||
|
|
b166b18ab7 | ||
|
|
6f516ee120 | ||
|
|
3220a0b8f3 | ||
|
|
0f94ec673a | ||
|
|
8e740c8e3a | ||
|
|
0a6c534228 | ||
|
|
393c861660 | ||
|
|
9d5a58c395 | ||
|
|
672649cb9c | ||
|
|
34f2b7d7f7 | ||
|
|
6d2c7b0561 | ||
|
|
c4402c6fc8 | ||
|
|
e07b33c882 | ||
|
|
248b098e54 | ||
|
|
ba28621b84 | ||
|
|
680c6cc51a | ||
|
|
60770da446 | ||
|
|
b0c3b714dc | ||
|
|
a3f98a69b7 | ||
|
|
f75a11c5bc | ||
|
|
2fd5b388ea | ||
|
|
ad24aeffed | ||
|
|
77d7b9d077 | ||
|
|
d787bba9a7 | ||
|
|
3d555e7442 | ||
|
|
d313812580 | ||
|
|
ad14684a98 | ||
|
|
bee2cfa1e0 | ||
|
|
d04f493e57 | ||
|
|
8fac98d0a1 | ||
|
|
219e00334f | ||
|
|
3fef743874 | ||
|
|
8f10f4a9ee | ||
|
|
754533f78b | ||
|
|
d7b8f85f4a | ||
|
|
2ed32b318d | ||
|
|
c323646b1a | ||
|
|
c8e82cbd09 | ||
|
|
e08dea95c8 | ||
|
|
4c00ff3169 | ||
|
|
3444dcdaee | ||
|
|
94f716fcab | ||
|
|
4436a1efee | ||
|
|
a9eb17ee50 | ||
|
|
b4d5e57b17 | ||
|
|
88a2088987 | ||
|
|
2a4b4de3ef | ||
|
|
52fe90bfb4 | ||
|
|
cec6f309d8 | ||
|
|
b8afbd7332 | ||
|
|
a6b6076e08 | ||
|
|
6d206c5a6d | ||
|
|
bdbd1e77e8 | ||
|
|
ae9a755013 | ||
|
|
9d3d3c80f2 | ||
|
|
201d3731d9 | ||
|
|
16371af1ce | ||
|
|
c19e9ad197 | ||
|
|
74d6016695 | ||
|
|
fdb0cf508a | ||
|
|
ab1fe67dd4 | ||
|
|
d429304a26 | ||
|
|
fd2952b44d | ||
|
|
4218b636fe | ||
|
|
29b74624c7 | ||
|
|
37cab1b30c | ||
|
|
6693cb13f5 | ||
|
|
54c6eaa0cd | ||
|
|
749dcc6ed2 | ||
|
|
414db19fa7 | ||
|
|
458a8b3efb | ||
|
|
eee57e29d0 | ||
|
|
84e91d2bdc | ||
|
|
394fd1d44c | ||
|
|
1f61d934f8 | ||
|
|
1ef8d65dce | ||
|
|
02496214ea | ||
|
|
af0e1180a5 | ||
|
|
c241d21a4a | ||
|
|
7117ba05ee | ||
|
|
1f4578c04c | ||
|
|
ba35f1269b | ||
|
|
a02a236ca4 | ||
|
|
a04829b04e | ||
|
|
475db2bbe8 | ||
|
|
d0c240f50f | ||
|
|
82f3e99f0e | ||
|
|
a2ee428d26 | ||
|
|
600137c911 | ||
|
|
dc3fada88f | ||
|
|
0c474a979f | ||
|
|
062cd2ad1d | ||
|
|
baef9ddacd | ||
|
|
6614771c3d | ||
|
|
569cb8bd44 | ||
|
|
b29bc75dcb | ||
|
|
05cd01d1d6 | ||
|
|
c2eca7dc39 | ||
|
|
6cd830c5b5 | ||
|
|
8c5e0c948f | ||
|
|
f59e2399b9 | ||
|
|
57470a9799 | ||
|
|
33a5ca845a | ||
|
|
9d3888f71d | ||
|
|
6c4e9488e6 | ||
|
|
9093e4f8c6 | ||
|
|
dbd5e2c289 | ||
|
|
407ad03bcb | ||
|
|
abe287f47c | ||
|
|
4c4092d8dc | ||
|
|
71e224a131 | ||
|
|
79d35bba5f | ||
|
|
1697544cb6 | ||
|
|
a74a59914b | ||
|
|
08c8b8c09f | ||
|
|
bf98a731b9 | ||
|
|
d7c7ec8f5e | ||
|
|
b2e754dfd5 | ||
|
|
7d5001de18 | ||
|
|
36804402bd | ||
|
|
5fcb166060 | ||
|
|
ed064fbe24 | ||
|
|
4c7a4fec1f | ||
|
|
1a95632ddf | ||
|
|
14ffe3c675 | ||
|
|
617c62f6c6 | ||
|
|
19061df586 | ||
|
|
e61151dc43 | ||
|
|
9be59385d0 | ||
|
|
4cd08917cf | ||
|
|
86ca41ae73 | ||
|
|
25a62c8700 | ||
|
|
a865a3b35d | ||
|
|
94b8a66db7 | ||
|
|
69dd038311 | ||
|
|
d87a7ce32b | ||
|
|
76dc35433b | ||
|
|
49a42dd1b4 | ||
|
|
f717376a17 | ||
|
|
8bb2b50a4c | ||
|
|
d6f40ec488 | ||
|
|
0ec38faf1d | ||
|
|
72bd4c202a | ||
|
|
a761ec880b | ||
|
|
6d5b95c616 | ||
|
|
b68b299cd4 | ||
|
|
f982d80ffe | ||
|
|
cef3e8e4bb | ||
|
|
fd8222ba80 | ||
|
|
2f8e5b91b0 | ||
|
|
f6c75c2107 | ||
|
|
4bc34f64e8 | ||
|
|
32ae3a1b67 | ||
|
|
4e4707a9de | ||
|
|
524fb6a07e | ||
|
|
dae84aef16 | ||
|
|
f4d6ef371a | ||
|
|
ed1d3685fa | ||
|
|
9755186195 | ||
|
|
32b75b4a4f | ||
|
|
f2a7536298 | ||
|
|
2f9e7e11dc | ||
|
|
edb4d743e9 | ||
|
|
b1557dd4af | ||
|
|
93b3ac4b89 | ||
|
|
ce0729923c | ||
|
|
85c038719e | ||
|
|
f069ee9094 | ||
|
|
16551396b9 | ||
|
|
155eecd86f | ||
|
|
93c2b1bf7c | ||
|
|
ab3513a51c | ||
|
|
f60585f609 | ||
|
|
8737217513 | ||
|
|
bdd349e242 | ||
|
|
2582a9fa20 | ||
|
|
6b54ca28b0 | ||
|
|
c001041ba8 | ||
|
|
54c2cd13f6 | ||
|
|
3b0def8159 | ||
|
|
5017fa462c | ||
|
|
f9e7b82b15 | ||
|
|
87c6e3085a | ||
|
|
2c40d2fe43 | ||
|
|
5aebf3f281 | ||
|
|
0ecc01d328 | ||
|
|
556a4c881c | ||
|
|
08038dec94 | ||
|
|
c6ed0bdc2a | ||
|
|
8baf1d437e | ||
|
|
2b4e95e64a | ||
|
|
6bf7f00f0a | ||
|
|
406c29f0d6 | ||
|
|
4a50eec281 | ||
|
|
65eb6484a9 | ||
|
|
29831d61bb | ||
|
|
cd62e7b5dc | ||
|
|
bc2648c216 | ||
|
|
589e37a646 | ||
|
|
fdeec11896 | ||
|
|
a6a544a488 | ||
|
|
287f3f8a5f | ||
|
|
4b15add524 | ||
|
|
3b425413a2 | ||
|
|
74167a91e3 | ||
|
|
61aca74096 | ||
|
|
3777e2b6d8 | ||
|
|
179babe313 | ||
|
|
59d5cd62bc | ||
|
|
5bae202031 | ||
|
|
383c0aeffc | ||
|
|
6044f87e80 | ||
|
|
c6c7ff882e | ||
|
|
76736b7850 | ||
|
|
949344a51e | ||
|
|
b0997449c1 | ||
|
|
f7f52e269a | ||
|
|
9e85d2e364 | ||
|
|
b636f253e6 | ||
|
|
b8f743eb4c | ||
|
|
4e88ce3f48 | ||
|
|
587ab629aa | ||
|
|
8aa36f38c8 | ||
|
|
2cf6f81729 | ||
|
|
f5fd6b569c | ||
|
|
4b7db3cbd6 | ||
|
|
3f45c6a94a | ||
|
|
1f458d3b56 | ||
|
|
5be732d533 | ||
|
|
a8a305fce0 | ||
|
|
9e8a950580 | ||
|
|
c8a9e5e947 | ||
|
|
d2229f69b6 | ||
|
|
02662a390d | ||
|
|
e7ec5e3dd4 | ||
|
|
a3a819cacb | ||
|
|
9df53637d1 | ||
|
|
d57eb6c8d4 | ||
|
|
555188058d | ||
|
|
debd4159f1 | ||
|
|
684ff1080b | ||
|
|
563cb74997 | ||
|
|
bfa1c91d33 | ||
|
|
865c865942 | ||
|
|
dd29b4ad44 | ||
|
|
b816d1b866 | ||
|
|
b10a6e19df | ||
|
|
f673000d67 | ||
|
|
12c4e37a95 | ||
|
|
a2efa11699 | ||
|
|
6807afbe6d | ||
|
|
b1cb852673 | ||
|
|
46d49d8fdf | ||
|
|
99ff3b2ec5 | ||
|
|
e5285b1992 | ||
|
|
f6ae71a99f | ||
|
|
fb6635344b | ||
|
|
3abd05f6b3 |
574 changed files with 32447 additions and 16934 deletions
2
.github/ISSUE_TEMPLATE/bug-report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug-report.yaml
vendored
|
|
@ -16,6 +16,8 @@ body:
|
|||
I have checked that this issue has not already been reported.
|
||||
- label: >
|
||||
I am using the latest version of Flow Launcher.
|
||||
- label: >
|
||||
I am using the prerelease version of Flow Launcher.
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
|
|
|
|||
3
.github/actions/spelling/allow.txt
vendored
3
.github/actions/spelling/allow.txt
vendored
|
|
@ -6,3 +6,6 @@ runcount
|
|||
Firefox
|
||||
Português
|
||||
Português (Brasil)
|
||||
favicons
|
||||
moz
|
||||
workaround
|
||||
|
|
|
|||
24
.github/actions/spelling/expect.txt
vendored
24
.github/actions/spelling/expect.txt
vendored
|
|
@ -1,3 +1,5 @@
|
|||
# This file should contain names of products, companies, or individuals that aren't in a standard dictionary (e.g., GitHub, Keptn, VSCode).
|
||||
|
||||
crowdin
|
||||
DWM
|
||||
workflows
|
||||
|
|
@ -34,7 +36,6 @@ mscorlib
|
|||
pythonw
|
||||
dotnet
|
||||
winget
|
||||
jjw24
|
||||
wolframalpha
|
||||
gmail
|
||||
duckduckgo
|
||||
|
|
@ -49,7 +50,6 @@ srchadmin
|
|||
EWX
|
||||
dlgtext
|
||||
CMD
|
||||
appref-ms
|
||||
appref
|
||||
TSource
|
||||
runas
|
||||
|
|
@ -57,7 +57,6 @@ dpi
|
|||
popup
|
||||
ptr
|
||||
pluginindicator
|
||||
TobiasSekan
|
||||
img
|
||||
resx
|
||||
bak
|
||||
|
|
@ -68,9 +67,6 @@ dlg
|
|||
ddd
|
||||
dddd
|
||||
clearlogfolder
|
||||
ACCENT_ENABLE_TRANSPARENTGRADIENT
|
||||
ACCENT_ENABLE_BLURBEHIND
|
||||
WCA_ACCENT_POLICY
|
||||
HGlobal
|
||||
dopusrt
|
||||
firefox
|
||||
|
|
@ -91,22 +87,24 @@ keyevent
|
|||
KListener
|
||||
requery
|
||||
vkcode
|
||||
čeština
|
||||
Polski
|
||||
Srpski
|
||||
Português
|
||||
Português (Brasil)
|
||||
Italiano
|
||||
Slovenský
|
||||
quicklook
|
||||
Tiếng Việt
|
||||
Droplex
|
||||
Preinstalled
|
||||
errormetadatafile
|
||||
noresult
|
||||
pluginsmanager
|
||||
alreadyexists
|
||||
JsonRPC
|
||||
JsonRPCV2
|
||||
Softpedia
|
||||
img
|
||||
Reloadable
|
||||
metadatas
|
||||
WMP
|
||||
VSTHRD
|
||||
CJK
|
||||
Msix
|
||||
dummyprofile
|
||||
browserbookmark
|
||||
copyurl
|
||||
|
|
|
|||
27
.github/actions/spelling/patterns.txt
vendored
27
.github/actions/spelling/patterns.txt
vendored
|
|
@ -1,4 +1,6 @@
|
|||
# See https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples:-patterns
|
||||
# This file should contain strings that contain a mix of letters and numbers, or specific symbols
|
||||
|
||||
|
||||
# Questionably acceptable forms of `in to`
|
||||
# Personally, I prefer `log into`, but people object
|
||||
|
|
@ -121,3 +123,28 @@
|
|||
|
||||
# version suffix <word>v#
|
||||
(?:(?<=[A-Z]{2})V|(?<=[a-z]{2}|[A-Z]{2})v)\d+(?:\b|(?=[a-zA-Z_]))
|
||||
|
||||
# Non-English
|
||||
[a-zA-Z]*[ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýÿĀāŁłŃńŅņŒœŚśŠšŜŝŸŽžź][a-zA-Z]{3}[a-zA-ZÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýÿĀāŁłŃńŅņŒœŚśŠšŜŝŸŽžź]*
|
||||
|
||||
|
||||
\bjjw24\b
|
||||
\bappref-ms\b
|
||||
\bTobiasSekan\b
|
||||
\bJsonRPC\b
|
||||
\bJsonRPCV2\b
|
||||
\bTiếng Việt\b
|
||||
\bPortuguês (Brasil)\b
|
||||
\bčeština\b
|
||||
\bPortuguês\b
|
||||
\bIoc\b
|
||||
\bXiao\s*He\b
|
||||
\bZi\s*Ran\s*Ma\b
|
||||
\bWei\s*Ruan\b
|
||||
\bZhi\s*Neng\s*ABC\b
|
||||
\bZi\s*Guang\s*Pin\s*Yin\b
|
||||
\bPin\s*Yin\s*Jia\s*Jia\b
|
||||
\bXing\s*Kong\s*Jian\s*Dao\b
|
||||
\bDa\s*Niu\b
|
||||
\bXiao\s*Lang\b
|
||||
\b[Ss]ettings [Ss]ettings\b
|
||||
|
|
|
|||
115
.github/update_release_pr.py
vendored
115
.github/update_release_pr.py
vendored
|
|
@ -1,17 +1,18 @@
|
|||
from os import getenv
|
||||
from typing import Optional
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
def get_github_prs(token: str, owner: str, repo: str, label: str = "", state: str = "all") -> list[dict]:
|
||||
"""
|
||||
Fetches pull requests from a GitHub repository that match a given milestone and label.
|
||||
Fetches pull requests from a GitHub repository that match a given label and state.
|
||||
|
||||
Args:
|
||||
token (str): GitHub token.
|
||||
owner (str): The owner of the repository.
|
||||
repo (str): The name of the repository.
|
||||
label (str): The label name.
|
||||
label (str): The label name. Filter is not applied when empty string.
|
||||
state (str): State of PR, e.g. open, closed, all
|
||||
|
||||
Returns:
|
||||
|
|
@ -23,39 +24,10 @@ def get_github_prs(token: str, owner: str, repo: str, label: str = "", state: st
|
|||
"Accept": "application/vnd.github.v3+json",
|
||||
}
|
||||
|
||||
milestone_id = None
|
||||
milestone_url = f"https://api.github.com/repos/{owner}/{repo}/milestones"
|
||||
params = {"state": "open"}
|
||||
|
||||
try:
|
||||
response = requests.get(milestone_url, headers=headers, params=params)
|
||||
response.raise_for_status()
|
||||
milestones = response.json()
|
||||
|
||||
if len(milestones) > 2:
|
||||
print("More than two milestones found, unable to determine the milestone required.")
|
||||
exit(1)
|
||||
|
||||
# milestones.pop()
|
||||
for ms in milestones:
|
||||
if ms["title"] != "Future":
|
||||
milestone_id = ms["number"]
|
||||
print(f"Gathering PRs with milestone {ms['title']}...")
|
||||
break
|
||||
|
||||
if not milestone_id:
|
||||
print(f"No suitable milestone found in repository '{owner}/{repo}'.")
|
||||
exit(1)
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Error fetching milestones: {e}")
|
||||
exit(1)
|
||||
|
||||
# This endpoint allows filtering by milestone and label. A PR in GH's perspective is a type of issue.
|
||||
# This endpoint allows filtering by label(and milestone). A PR in GH's perspective is a type of issue.
|
||||
prs_url = f"https://api.github.com/repos/{owner}/{repo}/issues"
|
||||
params = {
|
||||
"state": state,
|
||||
"milestone": milestone_id,
|
||||
"labels": label,
|
||||
"per_page": 100,
|
||||
}
|
||||
|
|
@ -83,14 +55,18 @@ def get_github_prs(token: str, owner: str, repo: str, label: str = "", state: st
|
|||
return all_prs
|
||||
|
||||
|
||||
def get_prs(pull_request_items: list[dict], label: str = "", state: str = "all") -> list[dict]:
|
||||
def get_prs(
|
||||
pull_request_items: list[dict], label: str = "", state: str = "all", milestone_title: Optional[str] = None
|
||||
) -> list[dict]:
|
||||
"""
|
||||
Returns a list of pull requests after applying the label and state filters.
|
||||
|
||||
Args:
|
||||
pull_request_items (list[dict]): List of PR items.
|
||||
label (str): The label name.
|
||||
label (str): The label name. Filter is not applied when empty string.
|
||||
state (str): State of PR, e.g. open, closed, all
|
||||
milestone_title (Optional[str]): The milestone title to filter by. This is the milestone number you created
|
||||
in GitHub, e.g. '1.20.0'. If None, no milestone filtering is applied.
|
||||
|
||||
Returns:
|
||||
list: A list of dictionaries, where each dictionary represents a pull request.
|
||||
|
|
@ -99,15 +75,47 @@ def get_prs(pull_request_items: list[dict], label: str = "", state: str = "all")
|
|||
pr_list = []
|
||||
count = 0
|
||||
for pr in pull_request_items:
|
||||
if pr["state"] == state and [item for item in pr["labels"] if item["name"] == label]:
|
||||
pr_list.append(pr)
|
||||
count += 1
|
||||
if state not in [pr["state"], "all"]:
|
||||
continue
|
||||
|
||||
print(f"Found {count} PRs with {label if label else 'no'} label and state as {state}")
|
||||
if label and not [item for item in pr["labels"] if item["name"] == label]:
|
||||
continue
|
||||
|
||||
if milestone_title:
|
||||
if pr["milestone"] is None or pr["milestone"]["title"] != milestone_title:
|
||||
continue
|
||||
|
||||
pr_list.append(pr)
|
||||
count += 1
|
||||
|
||||
print(
|
||||
f"Found {count} PRs with {label if label else 'no filter on'} label, state as {state}, and milestone {milestone_title if milestone_title else "any"}"
|
||||
)
|
||||
|
||||
return pr_list
|
||||
|
||||
|
||||
def get_prs_assignees(pull_request_items: list[dict]) -> list[str]:
|
||||
"""
|
||||
Returns a list of pull request assignees, excludes jjw24.
|
||||
|
||||
Args:
|
||||
pull_request_items (list[dict]): List of PR items to get the assignees from.
|
||||
|
||||
Returns:
|
||||
list: A list of strs, where each string is an assignee name. List is not distinct, so can contain
|
||||
duplicate names.
|
||||
Returns an empty list if none are found.
|
||||
"""
|
||||
assignee_list = []
|
||||
for pr in pull_request_items:
|
||||
[assignee_list.append(assignee["login"]) for assignee in pr["assignees"] if assignee["login"] != "jjw24"]
|
||||
|
||||
print(f"Found {len(assignee_list)} assignees")
|
||||
|
||||
return assignee_list
|
||||
|
||||
|
||||
def get_pr_descriptions(pull_request_items: list[dict]) -> str:
|
||||
"""
|
||||
Returns the concatenated string of pr title and number in the format of
|
||||
|
|
@ -185,15 +193,16 @@ if __name__ == "__main__":
|
|||
|
||||
print(f"Fetching {state} PRs for {repository_owner}/{repository_name} ...")
|
||||
|
||||
pull_requests = get_github_prs(github_token, repository_owner, repository_name)
|
||||
# First, get all PRs to find the release PR and determine the milestone
|
||||
all_pull_requests = get_github_prs(github_token, repository_owner, repository_name)
|
||||
|
||||
if not pull_requests:
|
||||
print("No matching pull requests found")
|
||||
if not all_pull_requests:
|
||||
print("No pull requests found")
|
||||
exit(1)
|
||||
|
||||
print(f"\nFound total of {len(pull_requests)} pull requests")
|
||||
print(f"\nFound total of {len(all_pull_requests)} pull requests")
|
||||
|
||||
release_pr = get_prs(pull_requests, "release", "open")
|
||||
release_pr = get_prs(all_pull_requests, "release", "open")
|
||||
|
||||
if len(release_pr) != 1:
|
||||
print(f"Unable to find the exact release PR. Returned result: {release_pr}")
|
||||
|
|
@ -201,13 +210,29 @@ if __name__ == "__main__":
|
|||
|
||||
print(f"Found release PR: {release_pr[0]['title']}")
|
||||
|
||||
enhancement_prs = get_prs(pull_requests, "enhancement", "closed")
|
||||
bug_fix_prs = get_prs(pull_requests, "bug", "closed")
|
||||
release_milestone_title = release_pr[0].get("milestone", {}).get("title", None)
|
||||
|
||||
if not release_milestone_title:
|
||||
print("Release PR does not have a milestone assigned.")
|
||||
exit(1)
|
||||
|
||||
print(f"Using milestone number: {release_milestone_title}")
|
||||
|
||||
enhancement_prs = get_prs(all_pull_requests, "enhancement", "closed", release_milestone_title)
|
||||
bug_fix_prs = get_prs(all_pull_requests, "bug", "closed", release_milestone_title)
|
||||
|
||||
if len(enhancement_prs) == 0 and len(bug_fix_prs) == 0:
|
||||
print(f"No PRs with {release_milestone_title} milestone were found")
|
||||
|
||||
description_content = "# Release notes\n"
|
||||
description_content += f"## Features\n{get_pr_descriptions(enhancement_prs)}" if enhancement_prs else ""
|
||||
description_content += f"## Bug fixes\n{get_pr_descriptions(bug_fix_prs)}" if bug_fix_prs else ""
|
||||
|
||||
assignees = list(set(get_prs_assignees(enhancement_prs) + get_prs_assignees(bug_fix_prs)))
|
||||
assignees.sort(key=str.lower)
|
||||
|
||||
description_content += f"### Authors:\n{', '.join(assignees)}"
|
||||
|
||||
update_pull_request_description(
|
||||
github_token, repository_owner, repository_name, release_pr[0]["number"], description_content
|
||||
)
|
||||
|
|
|
|||
115
.github/workflows/default_plugins.yml
vendored
115
.github/workflows/default_plugins.yml
vendored
|
|
@ -3,53 +3,37 @@ name: Publish Default Plugins
|
|||
on:
|
||||
push:
|
||||
branches: ['master']
|
||||
paths: ['Plugins/**']
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
publish:
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: 7.0.x
|
||||
dotnet-version: 9.0.x
|
||||
|
||||
- name: Determine New Plugin Updates
|
||||
uses: dorny/paths-filter@v3
|
||||
id: changes
|
||||
with:
|
||||
filters: |
|
||||
browserbookmark:
|
||||
- 'Plugins/Flow.Launcher.Plugin.BrowserBookmark/plugin.json'
|
||||
calculator:
|
||||
- 'Plugins/Flow.Launcher.Plugin.Calculator/plugin.json'
|
||||
explorer:
|
||||
- 'Plugins/Flow.Launcher.Plugin.Explorer/plugin.json'
|
||||
pluginindicator:
|
||||
- 'Plugins/Flow.Launcher.Plugin.PluginIndicator/plugin.json'
|
||||
pluginsmanager:
|
||||
- 'Plugins/Flow.Launcher.Plugin.PluginsManager/plugin.json'
|
||||
processkiller:
|
||||
- 'Plugins/Flow.Launcher.Plugin.ProcessKiller/plugin.json'
|
||||
program:
|
||||
- 'Plugins/Flow.Launcher.Plugin.Program/plugin.json'
|
||||
shell:
|
||||
- 'Plugins/Flow.Launcher.Plugin.Shell/plugin.json'
|
||||
sys:
|
||||
- 'Plugins/Flow.Launcher.Plugin.Sys/plugin.json'
|
||||
url:
|
||||
- 'Plugins/Flow.Launcher.Plugin.Url/plugin.json'
|
||||
websearch:
|
||||
- 'Plugins/Flow.Launcher.Plugin.WebSearch/plugin.json'
|
||||
windowssettings:
|
||||
- 'Plugins/Flow.Launcher.Plugin.WindowsSettings/plugin.json'
|
||||
base: 'master'
|
||||
- name: Update Plugins To Production Version
|
||||
run: |
|
||||
$version = "1.0.0"
|
||||
Get-Content appveyor.yml | ForEach-Object {
|
||||
if ($_ -match "version:\s*'(\d+\.\d+\.\d+)\.") {
|
||||
$version = $matches[1]
|
||||
}
|
||||
}
|
||||
|
||||
$jsonFiles = Get-ChildItem -Path ".\Plugins\*\plugin.json"
|
||||
foreach ($file in $jsonFiles) {
|
||||
$plugin_old_ver = Get-Content $file.FullName -Raw | ConvertFrom-Json
|
||||
(Get-Content $file) -replace '"Version"\s*:\s*".*?"', "`"Version`": `"$version`"" | Set-Content $file
|
||||
$plugin_new_ver = Get-Content $file.FullName -Raw | ConvertFrom-Json
|
||||
Write-Host "Updated" $plugin_old_ver.Name "version from" $plugin_old_ver.Version "to" $plugin_new_ver.Version
|
||||
}
|
||||
|
||||
- name: Get BrowserBookmark Version
|
||||
if: steps.changes.outputs.browserbookmark == 'true'
|
||||
id: updated-version-browserbookmark
|
||||
uses: notiz-dev/github-action-json-property@release
|
||||
with:
|
||||
|
|
@ -57,14 +41,12 @@ jobs:
|
|||
prop_path: 'Version'
|
||||
|
||||
- name: Build BrowserBookmark
|
||||
if: steps.changes.outputs.browserbookmark == 'true'
|
||||
run: |
|
||||
dotnet publish 'Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.BrowserBookmark"
|
||||
dotnet publish 'Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj' --framework net9.0-windows -c Release -o "Flow.Launcher.Plugin.BrowserBookmark"
|
||||
7z a -tzip "Flow.Launcher.Plugin.BrowserBookmark.zip" "./Flow.Launcher.Plugin.BrowserBookmark/*"
|
||||
rm -r "Flow.Launcher.Plugin.BrowserBookmark"
|
||||
|
||||
- name: Publish BrowserBookmark
|
||||
if: steps.changes.outputs.browserbookmark == 'true'
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
repository: "Flow-Launcher/Flow.Launcher.Plugin.BrowserBookmark"
|
||||
|
|
@ -76,7 +58,6 @@ jobs:
|
|||
|
||||
|
||||
- name: Get Calculator Version
|
||||
if: steps.changes.outputs.calculator == 'true'
|
||||
id: updated-version-calculator
|
||||
uses: notiz-dev/github-action-json-property@release
|
||||
with:
|
||||
|
|
@ -84,14 +65,12 @@ jobs:
|
|||
prop_path: 'Version'
|
||||
|
||||
- name: Build Calculator
|
||||
if: steps.changes.outputs.calculator == 'true'
|
||||
run: |
|
||||
dotnet publish 'Plugins/Flow.Launcher.Plugin.Calculator/Flow.Launcher.Plugin.Calculator.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.Calculator"
|
||||
dotnet publish 'Plugins/Flow.Launcher.Plugin.Calculator/Flow.Launcher.Plugin.Calculator.csproj' --framework net9.0-windows -c Release -o "Flow.Launcher.Plugin.Calculator"
|
||||
7z a -tzip "Flow.Launcher.Plugin.Calculator.zip" "./Flow.Launcher.Plugin.Calculator/*"
|
||||
rm -r "Flow.Launcher.Plugin.Calculator"
|
||||
|
||||
- name: Publish Calculator
|
||||
if: steps.changes.outputs.calculator == 'true'
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
repository: "Flow-Launcher/Flow.Launcher.Plugin.Calculator"
|
||||
|
|
@ -103,7 +82,6 @@ jobs:
|
|||
|
||||
|
||||
- name: Get Explorer Version
|
||||
if: steps.changes.outputs.explorer == 'true'
|
||||
id: updated-version-explorer
|
||||
uses: notiz-dev/github-action-json-property@release
|
||||
with:
|
||||
|
|
@ -111,14 +89,12 @@ jobs:
|
|||
prop_path: 'Version'
|
||||
|
||||
- name: Build Explorer
|
||||
if: steps.changes.outputs.explorer == 'true'
|
||||
run: |
|
||||
dotnet publish 'Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.Explorer"
|
||||
dotnet publish 'Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj' --framework net9.0-windows -c Release -o "Flow.Launcher.Plugin.Explorer"
|
||||
7z a -tzip "Flow.Launcher.Plugin.Explorer.zip" "./Flow.Launcher.Plugin.Explorer/*"
|
||||
rm -r "Flow.Launcher.Plugin.Explorer"
|
||||
|
||||
- name: Publish Explorer
|
||||
if: steps.changes.outputs.explorer == 'true'
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
repository: "Flow-Launcher/Flow.Launcher.Plugin.Explorer"
|
||||
|
|
@ -130,7 +106,6 @@ jobs:
|
|||
|
||||
|
||||
- name: Get PluginIndicator Version
|
||||
if: steps.changes.outputs.pluginindicator == 'true'
|
||||
id: updated-version-pluginindicator
|
||||
uses: notiz-dev/github-action-json-property@release
|
||||
with:
|
||||
|
|
@ -138,14 +113,12 @@ jobs:
|
|||
prop_path: 'Version'
|
||||
|
||||
- name: Build PluginIndicator
|
||||
if: steps.changes.outputs.pluginindicator == 'true'
|
||||
run: |
|
||||
dotnet publish 'Plugins/Flow.Launcher.Plugin.PluginIndicator/Flow.Launcher.Plugin.PluginIndicator.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.PluginIndicator"
|
||||
dotnet publish 'Plugins/Flow.Launcher.Plugin.PluginIndicator/Flow.Launcher.Plugin.PluginIndicator.csproj' --framework net9.0-windows -c Release -o "Flow.Launcher.Plugin.PluginIndicator"
|
||||
7z a -tzip "Flow.Launcher.Plugin.PluginIndicator.zip" "./Flow.Launcher.Plugin.PluginIndicator/*"
|
||||
rm -r "Flow.Launcher.Plugin.PluginIndicator"
|
||||
|
||||
- name: Publish PluginIndicator
|
||||
if: steps.changes.outputs.pluginindicator == 'true'
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
repository: "Flow-Launcher/Flow.Launcher.Plugin.PluginIndicator"
|
||||
|
|
@ -157,7 +130,6 @@ jobs:
|
|||
|
||||
|
||||
- name: Get PluginsManager Version
|
||||
if: steps.changes.outputs.pluginsmanager == 'true'
|
||||
id: updated-version-pluginsmanager
|
||||
uses: notiz-dev/github-action-json-property@release
|
||||
with:
|
||||
|
|
@ -165,14 +137,12 @@ jobs:
|
|||
prop_path: 'Version'
|
||||
|
||||
- name: Build PluginsManager
|
||||
if: steps.changes.outputs.pluginsmanager == 'true'
|
||||
run: |
|
||||
dotnet publish 'Plugins/Flow.Launcher.Plugin.PluginsManager/Flow.Launcher.Plugin.PluginsManager.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.PluginsManager"
|
||||
dotnet publish 'Plugins/Flow.Launcher.Plugin.PluginsManager/Flow.Launcher.Plugin.PluginsManager.csproj' --framework net9.0-windows -c Release -o "Flow.Launcher.Plugin.PluginsManager"
|
||||
7z a -tzip "Flow.Launcher.Plugin.PluginsManager.zip" "./Flow.Launcher.Plugin.PluginsManager/*"
|
||||
rm -r "Flow.Launcher.Plugin.PluginsManager"
|
||||
|
||||
- name: Publish PluginsManager
|
||||
if: steps.changes.outputs.pluginsmanager == 'true'
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
repository: "Flow-Launcher/Flow.Launcher.Plugin.PluginsManager"
|
||||
|
|
@ -184,7 +154,6 @@ jobs:
|
|||
|
||||
|
||||
- name: Get ProcessKiller Version
|
||||
if: steps.changes.outputs.processkiller == 'true'
|
||||
id: updated-version-processkiller
|
||||
uses: notiz-dev/github-action-json-property@release
|
||||
with:
|
||||
|
|
@ -192,14 +161,12 @@ jobs:
|
|||
prop_path: 'Version'
|
||||
|
||||
- name: Build ProcessKiller
|
||||
if: steps.changes.outputs.processkiller == 'true'
|
||||
run: |
|
||||
dotnet publish 'Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.ProcessKiller"
|
||||
dotnet publish 'Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj' --framework net9.0-windows -c Release -o "Flow.Launcher.Plugin.ProcessKiller"
|
||||
7z a -tzip "Flow.Launcher.Plugin.ProcessKiller.zip" "./Flow.Launcher.Plugin.ProcessKiller/*"
|
||||
rm -r "Flow.Launcher.Plugin.ProcessKiller"
|
||||
|
||||
- name: Publish ProcessKiller
|
||||
if: steps.changes.outputs.processkiller == 'true'
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
repository: "Flow-Launcher/Flow.Launcher.Plugin.ProcessKiller"
|
||||
|
|
@ -211,7 +178,6 @@ jobs:
|
|||
|
||||
|
||||
- name: Get Program Version
|
||||
if: steps.changes.outputs.program == 'true'
|
||||
id: updated-version-program
|
||||
uses: notiz-dev/github-action-json-property@release
|
||||
with:
|
||||
|
|
@ -219,14 +185,12 @@ jobs:
|
|||
prop_path: 'Version'
|
||||
|
||||
- name: Build Program
|
||||
if: steps.changes.outputs.program == 'true'
|
||||
run: |
|
||||
dotnet publish 'Plugins/Flow.Launcher.Plugin.Program/Flow.Launcher.Plugin.Program.csproj' --framework net7.0-windows10.0.19041.0 -c Release -o "Flow.Launcher.Plugin.Program"
|
||||
dotnet publish 'Plugins/Flow.Launcher.Plugin.Program/Flow.Launcher.Plugin.Program.csproj' --framework net9.0-windows10.0.19041.0 -c Release -o "Flow.Launcher.Plugin.Program"
|
||||
7z a -tzip "Flow.Launcher.Plugin.Program.zip" "./Flow.Launcher.Plugin.Program/*"
|
||||
rm -r "Flow.Launcher.Plugin.Program"
|
||||
|
||||
- name: Publish Program
|
||||
if: steps.changes.outputs.program == 'true'
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
repository: "Flow-Launcher/Flow.Launcher.Plugin.Program"
|
||||
|
|
@ -238,7 +202,6 @@ jobs:
|
|||
|
||||
|
||||
- name: Get Shell Version
|
||||
if: steps.changes.outputs.shell == 'true'
|
||||
id: updated-version-shell
|
||||
uses: notiz-dev/github-action-json-property@release
|
||||
with:
|
||||
|
|
@ -246,14 +209,12 @@ jobs:
|
|||
prop_path: 'Version'
|
||||
|
||||
- name: Build Shell
|
||||
if: steps.changes.outputs.shell == 'true'
|
||||
run: |
|
||||
dotnet publish 'Plugins/Flow.Launcher.Plugin.Shell/Flow.Launcher.Plugin.Shell.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.Shell"
|
||||
dotnet publish 'Plugins/Flow.Launcher.Plugin.Shell/Flow.Launcher.Plugin.Shell.csproj' --framework net9.0-windows -c Release -o "Flow.Launcher.Plugin.Shell"
|
||||
7z a -tzip "Flow.Launcher.Plugin.Shell.zip" "./Flow.Launcher.Plugin.Shell/*"
|
||||
rm -r "Flow.Launcher.Plugin.Shell"
|
||||
|
||||
- name: Publish Shell
|
||||
if: steps.changes.outputs.shell == 'true'
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
repository: "Flow-Launcher/Flow.Launcher.Plugin.Shell"
|
||||
|
|
@ -265,7 +226,6 @@ jobs:
|
|||
|
||||
|
||||
- name: Get Sys Version
|
||||
if: steps.changes.outputs.sys == 'true'
|
||||
id: updated-version-sys
|
||||
uses: notiz-dev/github-action-json-property@release
|
||||
with:
|
||||
|
|
@ -273,14 +233,12 @@ jobs:
|
|||
prop_path: 'Version'
|
||||
|
||||
- name: Build Sys
|
||||
if: steps.changes.outputs.sys == 'true'
|
||||
run: |
|
||||
dotnet publish 'Plugins/Flow.Launcher.Plugin.Sys/Flow.Launcher.Plugin.Sys.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.Sys"
|
||||
dotnet publish 'Plugins/Flow.Launcher.Plugin.Sys/Flow.Launcher.Plugin.Sys.csproj' --framework net9.0-windows -c Release -o "Flow.Launcher.Plugin.Sys"
|
||||
7z a -tzip "Flow.Launcher.Plugin.Sys.zip" "./Flow.Launcher.Plugin.Sys/*"
|
||||
rm -r "Flow.Launcher.Plugin.Sys"
|
||||
|
||||
- name: Publish Sys
|
||||
if: steps.changes.outputs.sys == 'true'
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
repository: "Flow-Launcher/Flow.Launcher.Plugin.Sys"
|
||||
|
|
@ -292,7 +250,6 @@ jobs:
|
|||
|
||||
|
||||
- name: Get Url Version
|
||||
if: steps.changes.outputs.url == 'true'
|
||||
id: updated-version-url
|
||||
uses: notiz-dev/github-action-json-property@release
|
||||
with:
|
||||
|
|
@ -300,14 +257,12 @@ jobs:
|
|||
prop_path: 'Version'
|
||||
|
||||
- name: Build Url
|
||||
if: steps.changes.outputs.url == 'true'
|
||||
run: |
|
||||
dotnet publish 'Plugins/Flow.Launcher.Plugin.Url/Flow.Launcher.Plugin.Url.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.Url"
|
||||
dotnet publish 'Plugins/Flow.Launcher.Plugin.Url/Flow.Launcher.Plugin.Url.csproj' --framework net9.0-windows -c Release -o "Flow.Launcher.Plugin.Url"
|
||||
7z a -tzip "Flow.Launcher.Plugin.Url.zip" "./Flow.Launcher.Plugin.Url/*"
|
||||
rm -r "Flow.Launcher.Plugin.Url"
|
||||
|
||||
- name: Publish Url
|
||||
if: steps.changes.outputs.url == 'true'
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
repository: "Flow-Launcher/Flow.Launcher.Plugin.Url"
|
||||
|
|
@ -319,7 +274,6 @@ jobs:
|
|||
|
||||
|
||||
- name: Get WebSearch Version
|
||||
if: steps.changes.outputs.websearch == 'true'
|
||||
id: updated-version-websearch
|
||||
uses: notiz-dev/github-action-json-property@release
|
||||
with:
|
||||
|
|
@ -327,14 +281,12 @@ jobs:
|
|||
prop_path: 'Version'
|
||||
|
||||
- name: Build WebSearch
|
||||
if: steps.changes.outputs.websearch == 'true'
|
||||
run: |
|
||||
dotnet publish 'Plugins/Flow.Launcher.Plugin.WebSearch/Flow.Launcher.Plugin.WebSearch.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.WebSearch"
|
||||
dotnet publish 'Plugins/Flow.Launcher.Plugin.WebSearch/Flow.Launcher.Plugin.WebSearch.csproj' --framework net9.0-windows -c Release -o "Flow.Launcher.Plugin.WebSearch"
|
||||
7z a -tzip "Flow.Launcher.Plugin.WebSearch.zip" "./Flow.Launcher.Plugin.WebSearch/*"
|
||||
rm -r "Flow.Launcher.Plugin.WebSearch"
|
||||
|
||||
- name: Publish WebSearch
|
||||
if: steps.changes.outputs.websearch == 'true'
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
repository: "Flow-Launcher/Flow.Launcher.Plugin.WebSearch"
|
||||
|
|
@ -346,7 +298,6 @@ jobs:
|
|||
|
||||
|
||||
- name: Get WindowsSettings Version
|
||||
if: steps.changes.outputs.windowssettings == 'true'
|
||||
id: updated-version-windowssettings
|
||||
uses: notiz-dev/github-action-json-property@release
|
||||
with:
|
||||
|
|
@ -354,14 +305,12 @@ jobs:
|
|||
prop_path: 'Version'
|
||||
|
||||
- name: Build WindowsSettings
|
||||
if: steps.changes.outputs.windowssettings == 'true'
|
||||
run: |
|
||||
dotnet publish 'Plugins/Flow.Launcher.Plugin.WindowsSettings/Flow.Launcher.Plugin.WindowsSettings.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.WindowsSettings"
|
||||
dotnet publish 'Plugins/Flow.Launcher.Plugin.WindowsSettings/Flow.Launcher.Plugin.WindowsSettings.csproj' --framework net9.0-windows -c Release -o "Flow.Launcher.Plugin.WindowsSettings"
|
||||
7z a -tzip "Flow.Launcher.Plugin.WindowsSettings.zip" "./Flow.Launcher.Plugin.WindowsSettings/*"
|
||||
rm -r "Flow.Launcher.Plugin.WindowsSettings"
|
||||
|
||||
- name: Publish WindowsSettings
|
||||
if: steps.changes.outputs.windowssettings == 'true'
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
repository: "Flow-Launcher/Flow.Launcher.Plugin.WindowsSettings"
|
||||
|
|
|
|||
18
.github/workflows/dotnet.yml
vendored
18
.github/workflows/dotnet.yml
vendored
|
|
@ -16,11 +16,11 @@ jobs:
|
|||
|
||||
runs-on: windows-latest
|
||||
env:
|
||||
FlowVersion: 1.19.5
|
||||
FlowVersion: 1.20.2
|
||||
NUGET_CERT_REVOCATION_MODE: offline
|
||||
BUILD_NUMBER: ${{ github.run_number }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set Flow.Launcher.csproj version
|
||||
id: update
|
||||
uses: vers-one/dotnet-project-version-updater@v1.7
|
||||
|
|
@ -29,9 +29,9 @@ jobs:
|
|||
"**/SolutionAssemblyInfo.cs"
|
||||
version: ${{ env.FlowVersion }}.${{ env.BUILD_NUMBER }}
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: 7.0.x
|
||||
dotnet-version: 9.0.x
|
||||
# cache: true
|
||||
# cache-dependency-path: |
|
||||
# Flow.Launcher/packages.lock.json
|
||||
|
|
@ -54,28 +54,28 @@ jobs:
|
|||
shell: powershell
|
||||
run: .\Scripts\post_build.ps1
|
||||
- name: Upload Plugin Nupkg
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: Plugin nupkg
|
||||
path: |
|
||||
Output\Release\Flow.Launcher.Plugin.*.nupkg
|
||||
compression-level: 0
|
||||
- name: Upload Setup
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: Flow Installer
|
||||
path: |
|
||||
Output\Packages\Flow-Launcher-*.exe
|
||||
compression-level: 0
|
||||
- name: Upload Portable Version
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: Portable Version
|
||||
path: |
|
||||
Output\Packages\Flow-Launcher-Portable.zip
|
||||
compression-level: 0
|
||||
- name: Upload Full Nupkg
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: Full nupkg
|
||||
path: |
|
||||
|
|
@ -83,7 +83,7 @@ jobs:
|
|||
|
||||
compression-level: 0
|
||||
- name: Upload Release Information
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: RELEASES
|
||||
path: |
|
||||
|
|
|
|||
2
.github/workflows/pr_assignee.yml
vendored
2
.github/workflows/pr_assignee.yml
vendored
|
|
@ -14,4 +14,4 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Assign PR to creator
|
||||
uses: toshimaru/auto-author-assign@v2.1.1
|
||||
uses: toshimaru/auto-author-assign@v3.0.1
|
||||
|
|
|
|||
4
.github/workflows/release_pr.yml
vendored
4
.github/workflows/release_pr.yml
vendored
|
|
@ -11,9 +11,9 @@ jobs:
|
|||
update-pr:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.x"
|
||||
|
||||
|
|
|
|||
4
.github/workflows/spelling.yml
vendored
4
.github/workflows/spelling.yml
vendored
|
|
@ -72,7 +72,7 @@ jobs:
|
|||
steps:
|
||||
- name: check-spelling
|
||||
id: spelling
|
||||
uses: check-spelling/check-spelling@prerelease
|
||||
uses: check-spelling/check-spelling@v0.0.25
|
||||
with:
|
||||
suppress_push_for_open_pull_request: 1
|
||||
checkout: true
|
||||
|
|
@ -128,7 +128,7 @@ jobs:
|
|||
if: (success() || failure()) && needs.spelling.outputs.followup && contains(github.event_name, 'pull_request')
|
||||
steps:
|
||||
- name: comment
|
||||
uses: check-spelling/check-spelling@prerelease
|
||||
uses: check-spelling/check-spelling@v0.0.25
|
||||
with:
|
||||
checkout: true
|
||||
spell_check_this: check-spelling/spell-check-this@main
|
||||
|
|
|
|||
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
|
|
@ -18,7 +18,7 @@ jobs:
|
|||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
- uses: actions/stale@v10
|
||||
with:
|
||||
stale-issue-message: 'This issue is stale because it has been open ${{ env.days-before-stale }} days with no activity. Remove stale label or comment or this will be closed in ${{ env.days-before-stale }} days.\n\nAlternatively this issue can be kept open by adding one of the following labels:\n${{ env.exempt-issue-labels }}'
|
||||
days-before-stale: ${{ env.days-before-stale }}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
<Project>
|
||||
<PropertyGroup>
|
||||
<AccelerateBuildsInVisualStudio>true</AccelerateBuildsInVisualStudio>
|
||||
<!-- Workaround https://github.com/dotnet/runtime/issues/109682 -->
|
||||
<CETCompat>false</CETCompat>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
|
@ -3,10 +3,8 @@ using System.IO;
|
|||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Windows;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Infrastructure;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
using Microsoft.Win32;
|
||||
using Squirrel;
|
||||
|
|
@ -17,8 +15,6 @@ namespace Flow.Launcher.Core.Configuration
|
|||
{
|
||||
private static readonly string ClassName = nameof(Portable);
|
||||
|
||||
private readonly IPublicAPI API = Ioc.Default.GetRequiredService<IPublicAPI>();
|
||||
|
||||
/// <summary>
|
||||
/// As at Squirrel.Windows version 1.5.2, UpdateManager needs to be disposed after finish
|
||||
/// </summary>
|
||||
|
|
@ -45,14 +41,13 @@ namespace Flow.Launcher.Core.Configuration
|
|||
#endif
|
||||
IndicateDeletion(DataLocation.PortableDataPath);
|
||||
|
||||
API.ShowMsgBox("Flow Launcher needs to restart to finish disabling portable mode, " +
|
||||
"after the restart your portable data profile will be deleted and roaming data profile kept");
|
||||
PublicApi.Instance.ShowMsgBox(Localize.restartToDisablePortableMode());
|
||||
|
||||
UpdateManager.RestartApp(Constant.ApplicationFileName);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
API.LogException(ClassName, "Error occurred while disabling portable mode", e);
|
||||
PublicApi.Instance.LogException(ClassName, "Error occurred while disabling portable mode", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -69,14 +64,13 @@ namespace Flow.Launcher.Core.Configuration
|
|||
#endif
|
||||
IndicateDeletion(DataLocation.RoamingDataPath);
|
||||
|
||||
API.ShowMsgBox("Flow Launcher needs to restart to finish enabling portable mode, " +
|
||||
"after the restart your roaming data profile will be deleted and portable data profile kept");
|
||||
PublicApi.Instance.ShowMsgBox(Localize.restartToEnablePortableMode());
|
||||
|
||||
UpdateManager.RestartApp(Constant.ApplicationFileName);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
API.LogException(ClassName, "Error occurred while enabling portable mode", e);
|
||||
PublicApi.Instance.LogException(ClassName, "Error occurred while enabling portable mode", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -96,13 +90,13 @@ namespace Flow.Launcher.Core.Configuration
|
|||
|
||||
public void MoveUserDataFolder(string fromLocation, string toLocation)
|
||||
{
|
||||
FilesFolders.CopyAll(fromLocation, toLocation, (s) => API.ShowMsgBox(s));
|
||||
FilesFolders.CopyAll(fromLocation, toLocation, (s) => PublicApi.Instance.ShowMsgBox(s));
|
||||
VerifyUserDataAfterMove(fromLocation, toLocation);
|
||||
}
|
||||
|
||||
public void VerifyUserDataAfterMove(string fromLocation, string toLocation)
|
||||
{
|
||||
FilesFolders.VerifyBothFolderFilesEqual(fromLocation, toLocation, (s) => API.ShowMsgBox(s));
|
||||
FilesFolders.VerifyBothFolderFilesEqual(fromLocation, toLocation, (s) => PublicApi.Instance.ShowMsgBox(s));
|
||||
}
|
||||
|
||||
public void CreateShortcuts()
|
||||
|
|
@ -141,8 +135,8 @@ namespace Flow.Launcher.Core.Configuration
|
|||
public void PreStartCleanUpAfterPortabilityUpdate()
|
||||
{
|
||||
// Specify here so this method does not rely on other environment variables to initialise
|
||||
var portableDataDir = Path.Combine(Directory.GetParent(Assembly.GetExecutingAssembly().Location.NonNull()).ToString(), "UserData");
|
||||
var roamingDataDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "FlowLauncher");
|
||||
var portableDataDir = DataLocation.PortableDataPath;
|
||||
var roamingDataDir = DataLocation.RoamingDataPath;
|
||||
|
||||
// Get full path to the .dead files for each case
|
||||
var portableDataDeleteFilePath = Path.Combine(portableDataDir, DataLocation.DeletionIndicatorFile);
|
||||
|
|
@ -152,13 +146,12 @@ namespace Flow.Launcher.Core.Configuration
|
|||
// delete it and prompt the user to pick the portable data location
|
||||
if (File.Exists(roamingDataDeleteFilePath))
|
||||
{
|
||||
FilesFolders.RemoveFolderIfExists(roamingDataDir, (s) => API.ShowMsgBox(s));
|
||||
FilesFolders.RemoveFolderIfExists(roamingDataDir, (s) => PublicApi.Instance.ShowMsgBox(s));
|
||||
|
||||
if (API.ShowMsgBox("Flow Launcher has detected you enabled portable mode, " +
|
||||
"would you like to move it to a different location?", string.Empty,
|
||||
MessageBoxButton.YesNo) == MessageBoxResult.Yes)
|
||||
if (PublicApi.Instance.ShowMsgBox(Localize.moveToDifferentLocation(),
|
||||
string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.Yes)
|
||||
{
|
||||
FilesFolders.OpenPath(Constant.RootDirectory, (s) => API.ShowMsgBox(s));
|
||||
FilesFolders.OpenPath(Constant.RootDirectory, (s) => PublicApi.Instance.ShowMsgBox(s));
|
||||
|
||||
Environment.Exit(0);
|
||||
}
|
||||
|
|
@ -167,10 +160,9 @@ namespace Flow.Launcher.Core.Configuration
|
|||
// delete it and notify the user about it.
|
||||
else if (File.Exists(portableDataDeleteFilePath))
|
||||
{
|
||||
FilesFolders.RemoveFolderIfExists(portableDataDir, (s) => API.ShowMsgBox(s));
|
||||
FilesFolders.RemoveFolderIfExists(portableDataDir, (s) => PublicApi.Instance.ShowMsgBox(s));
|
||||
|
||||
API.ShowMsgBox("Flow Launcher has detected you disabled portable mode, " +
|
||||
"the relevant shortcuts and uninstaller entry have been created");
|
||||
PublicApi.Instance.ShowMsgBox(Localize.shortcutsUninstallerCreated());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -181,9 +173,7 @@ namespace Flow.Launcher.Core.Configuration
|
|||
|
||||
if (roamingLocationExists && portableLocationExists)
|
||||
{
|
||||
API.ShowMsgBox(string.Format("Flow Launcher detected your user data exists both in {0} and " +
|
||||
"{1}. {2}{2}Please delete {1} in order to proceed. No changes have occurred.",
|
||||
DataLocation.PortableDataPath, DataLocation.RoamingDataPath, Environment.NewLine));
|
||||
PublicApi.Instance.ShowMsgBox(Localize.userDataDuplicated(DataLocation.PortableDataPath, DataLocation.RoamingDataPath, Environment.NewLine));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ using System.Text.Json;
|
|||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Infrastructure.Http;
|
||||
using Flow.Launcher.Plugin;
|
||||
|
||||
|
|
@ -18,13 +17,9 @@ namespace Flow.Launcher.Core.ExternalPlugins
|
|||
{
|
||||
private static readonly string ClassName = nameof(CommunityPluginSource);
|
||||
|
||||
// We should not initialize API in static constructor because it will create another API instance
|
||||
private static IPublicAPI api = null;
|
||||
private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService<IPublicAPI>();
|
||||
|
||||
private string latestEtag = "";
|
||||
|
||||
private List<UserPlugin> plugins = new();
|
||||
private List<UserPlugin> plugins = [];
|
||||
|
||||
private static readonly JsonSerializerOptions PluginStoreItemSerializationOption = new()
|
||||
{
|
||||
|
|
@ -41,7 +36,7 @@ namespace Flow.Launcher.Core.ExternalPlugins
|
|||
/// </remarks>
|
||||
public async Task<List<UserPlugin>> FetchAsync(CancellationToken token)
|
||||
{
|
||||
API.LogInfo(ClassName, $"Loading plugins from {ManifestFileUrl}");
|
||||
PublicApi.Instance.LogInfo(ClassName, $"Loading plugins from {ManifestFileUrl}");
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, ManifestFileUrl);
|
||||
|
||||
|
|
@ -59,29 +54,40 @@ namespace Flow.Launcher.Core.ExternalPlugins
|
|||
.ConfigureAwait(false);
|
||||
latestEtag = response.Headers.ETag?.Tag;
|
||||
|
||||
API.LogInfo(ClassName, $"Loaded {plugins.Count} plugins from {ManifestFileUrl}");
|
||||
PublicApi.Instance.LogInfo(ClassName, $"Loaded {plugins.Count} plugins from {ManifestFileUrl}");
|
||||
return plugins;
|
||||
}
|
||||
else if (response.StatusCode == HttpStatusCode.NotModified)
|
||||
{
|
||||
API.LogInfo(ClassName, $"Resource {ManifestFileUrl} has not been modified.");
|
||||
PublicApi.Instance.LogInfo(ClassName, $"Resource {ManifestFileUrl} has not been modified.");
|
||||
return plugins;
|
||||
}
|
||||
else
|
||||
{
|
||||
API.LogWarn(ClassName, $"Failed to load resource {ManifestFileUrl} with response {response.StatusCode}");
|
||||
PublicApi.Instance.LogWarn(ClassName, $"Failed to load resource {ManifestFileUrl} with response {response.StatusCode}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (token.IsCancellationRequested)
|
||||
{
|
||||
PublicApi.Instance.LogDebug(ClassName, $"Fetching from {ManifestFileUrl} was cancelled by caller.");
|
||||
return null;
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// Likely an HttpClient timeout or external cancellation not requested by our token
|
||||
PublicApi.Instance.LogWarn(ClassName, $"Fetching from {ManifestFileUrl} timed out.");
|
||||
return null;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (e is HttpRequestException or WebException or SocketException || e.InnerException is TimeoutException)
|
||||
{
|
||||
API.LogException(ClassName, $"Check your connection and proxy settings to {ManifestFileUrl}.", e);
|
||||
PublicApi.Instance.LogException(ClassName, $"Check your connection and proxy settings to {ManifestFileUrl}.", e);
|
||||
}
|
||||
else
|
||||
{
|
||||
API.LogException(ClassName, "Error Occurred", e);
|
||||
PublicApi.Instance.LogException(ClassName, "Error Occurred", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ using System.IO;
|
|||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Forms;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
|
|
@ -15,7 +14,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
|||
{
|
||||
private static readonly string ClassName = nameof(AbstractPluginEnvironment);
|
||||
|
||||
protected readonly IPublicAPI API = Ioc.Default.GetRequiredService<IPublicAPI>();
|
||||
protected readonly IPublicAPI API = PublicApi.Instance;
|
||||
|
||||
internal abstract string Language { get; }
|
||||
|
||||
|
|
@ -58,15 +57,10 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
|||
return SetPathForPluginPairs(PluginsSettingsFilePath, Language);
|
||||
}
|
||||
|
||||
var noRuntimeMessage = string.Format(
|
||||
API.GetTranslation("runtimePluginInstalledChooseRuntimePrompt"),
|
||||
Language,
|
||||
EnvName,
|
||||
Environment.NewLine
|
||||
);
|
||||
var noRuntimeMessage = Localize.runtimePluginInstalledChooseRuntimePrompt(Language, EnvName, Environment.NewLine);
|
||||
if (API.ShowMsgBox(noRuntimeMessage, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No)
|
||||
{
|
||||
var msg = string.Format(API.GetTranslation("runtimePluginChooseRuntimeExecutable"), EnvName);
|
||||
var msg = Localize.runtimePluginChooseRuntimeExecutable(EnvName);
|
||||
|
||||
var selectedFile = GetFileFromDialog(msg, FileDialogFilter);
|
||||
|
||||
|
|
@ -77,12 +71,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
|||
// Nothing selected because user pressed cancel from the file dialog window
|
||||
else
|
||||
{
|
||||
var forceDownloadMessage = string.Format(
|
||||
API.GetTranslation("runtimeExecutableInvalidChooseDownload"),
|
||||
Language,
|
||||
EnvName,
|
||||
Environment.NewLine
|
||||
);
|
||||
var forceDownloadMessage = Localize.runtimeExecutableInvalidChooseDownload(Language, EnvName, Environment.NewLine);
|
||||
|
||||
// Let users select valid path or choose to download
|
||||
while (string.IsNullOrEmpty(selectedFile))
|
||||
|
|
@ -120,7 +109,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
|||
}
|
||||
else
|
||||
{
|
||||
API.ShowMsgBox(string.Format(API.GetTranslation("runtimePluginUnableToSetExecutablePath"), Language));
|
||||
API.ShowMsgBox(Localize.runtimePluginUnableToSetExecutablePath(Language));
|
||||
API.LogError(ClassName,
|
||||
$"Not able to successfully set {EnvName} path, setting's plugin executable path variable is still an empty string.",
|
||||
$"{Language}Environment");
|
||||
|
|
@ -248,7 +237,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
|||
private static string GetUpdatedEnvironmentPath(string filePath)
|
||||
{
|
||||
var index = filePath.IndexOf(DataLocation.PluginEnvironments);
|
||||
|
||||
|
||||
// get the substring after "Environments" because we can not determine it dynamically
|
||||
var executablePathSubstring = filePath[(index + DataLocation.PluginEnvironments.Length)..];
|
||||
return $"{DataLocation.PluginEnvironmentsPath}{executablePathSubstring}";
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
|||
{
|
||||
internal class PythonEnvironment : AbstractPluginEnvironment
|
||||
{
|
||||
private static readonly string ClassName = nameof(PythonEnvironment);
|
||||
|
||||
internal override string Language => AllowedLanguage.Python;
|
||||
|
||||
internal override string EnvName => DataLocation.PythonEnvironmentName;
|
||||
|
|
@ -39,9 +41,20 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
|||
|
||||
// Python 3.11.4 is no longer Windows 7 compatible. If user is on Win 7 and
|
||||
// uses Python plugin they need to custom install and use v3.8.9
|
||||
JTF.Run(() => DroplexPackage.Drop(App.python_3_11_4_embeddable, InstallPath));
|
||||
JTF.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await DroplexPackage.Drop(App.python_3_11_4_embeddable, InstallPath);
|
||||
|
||||
PluginsSettingsFilePath = ExecutablePath;
|
||||
PluginsSettingsFilePath = ExecutablePath;
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
API.ShowMsgError(Localize.failToInstallPythonEnv());
|
||||
API.LogException(ClassName, "Failed to install Python environment", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
internal override PluginPair CreatePluginPair(string filePath, PluginMetadata metadata)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
|||
{
|
||||
internal class TypeScriptEnvironment : AbstractPluginEnvironment
|
||||
{
|
||||
private static readonly string ClassName = nameof(TypeScriptEnvironment);
|
||||
|
||||
internal override string Language => AllowedLanguage.TypeScript;
|
||||
|
||||
internal override string EnvName => DataLocation.NodeEnvironmentName;
|
||||
|
|
@ -34,9 +36,20 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
|||
{
|
||||
FilesFolders.RemoveFolderIfExists(InstallPath, (s) => API.ShowMsgBox(s));
|
||||
|
||||
JTF.Run(() => DroplexPackage.Drop(App.nodejs_16_18_0, InstallPath));
|
||||
JTF.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await DroplexPackage.Drop(App.nodejs_16_18_0, InstallPath);
|
||||
|
||||
PluginsSettingsFilePath = ExecutablePath;
|
||||
PluginsSettingsFilePath = ExecutablePath;
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
API.ShowMsgError(Localize.failToInstallTypeScriptEnv());
|
||||
API.LogException(ClassName, "Failed to install TypeScript environment", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
internal override PluginPair CreatePluginPair(string filePath, PluginMetadata metadata)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
|||
{
|
||||
internal class TypeScriptV2Environment : AbstractPluginEnvironment
|
||||
{
|
||||
private static readonly string ClassName = nameof(TypeScriptV2Environment);
|
||||
|
||||
internal override string Language => AllowedLanguage.TypeScriptV2;
|
||||
|
||||
internal override string EnvName => DataLocation.NodeEnvironmentName;
|
||||
|
|
@ -34,9 +36,20 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
|||
{
|
||||
FilesFolders.RemoveFolderIfExists(InstallPath, (s) => API.ShowMsgBox(s));
|
||||
|
||||
JTF.Run(() => DroplexPackage.Drop(App.nodejs_16_18_0, InstallPath));
|
||||
JTF.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await DroplexPackage.Drop(App.nodejs_16_18_0, InstallPath);
|
||||
|
||||
PluginsSettingsFilePath = ExecutablePath;
|
||||
PluginsSettingsFilePath = ExecutablePath;
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
API.ShowMsgError(Localize.failToInstallTypeScriptEnv());
|
||||
API.LogException(ClassName, "Failed to install TypeScript environment", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
internal override PluginPair CreatePluginPair(string filePath, PluginMetadata metadata)
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Flow.Launcher.Core.Plugin;
|
||||
|
||||
namespace Flow.Launcher.Core.ExternalPlugins
|
||||
{
|
||||
|
|
@ -12,10 +12,10 @@ namespace Flow.Launcher.Core.ExternalPlugins
|
|||
private static readonly string ClassName = nameof(PluginsManifest);
|
||||
|
||||
private static readonly CommunityPluginStore mainPluginStore =
|
||||
new("https://raw.githubusercontent.com/Flow-Launcher/Flow.Launcher.PluginsManifest/plugin_api_v2/plugins.json",
|
||||
"https://fastly.jsdelivr.net/gh/Flow-Launcher/Flow.Launcher.PluginsManifest@plugin_api_v2/plugins.json",
|
||||
"https://gcore.jsdelivr.net/gh/Flow-Launcher/Flow.Launcher.PluginsManifest@plugin_api_v2/plugins.json",
|
||||
"https://cdn.jsdelivr.net/gh/Flow-Launcher/Flow.Launcher.PluginsManifest@plugin_api_v2/plugins.json");
|
||||
new("https://raw.githubusercontent.com/Flow-Launcher/Flow.Launcher.PluginsManifest/main/plugins.json",
|
||||
"https://fastly.jsdelivr.net/gh/Flow-Launcher/Flow.Launcher.PluginsManifest@main/plugins.json",
|
||||
"https://gcore.jsdelivr.net/gh/Flow-Launcher/Flow.Launcher.PluginsManifest@main/plugins.json",
|
||||
"https://cdn.jsdelivr.net/gh/Flow-Launcher/Flow.Launcher.PluginsManifest@main/plugins.json");
|
||||
|
||||
private static readonly SemaphoreSlim manifestUpdateLock = new(1);
|
||||
|
||||
|
|
@ -26,31 +26,47 @@ namespace Flow.Launcher.Core.ExternalPlugins
|
|||
|
||||
public static async Task<bool> UpdateManifestAsync(bool usePrimaryUrlOnly = false, CancellationToken token = default)
|
||||
{
|
||||
bool lockAcquired = false;
|
||||
try
|
||||
{
|
||||
await manifestUpdateLock.WaitAsync(token).ConfigureAwait(false);
|
||||
lockAcquired = true;
|
||||
|
||||
if (UserPlugins == null || usePrimaryUrlOnly || DateTime.Now.Subtract(lastFetchedAt) >= fetchTimeout)
|
||||
{
|
||||
var results = await mainPluginStore.FetchAsync(token, usePrimaryUrlOnly).ConfigureAwait(false);
|
||||
|
||||
// If the results are empty, we shouldn't update the manifest because the results are invalid.
|
||||
if (results.Count != 0)
|
||||
{
|
||||
UserPlugins = results;
|
||||
lastFetchedAt = DateTime.Now;
|
||||
if (results.Count == 0)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
var updatedPluginResults = new List<UserPlugin>();
|
||||
|
||||
for (int i = 0; i < results.Count; i++)
|
||||
{
|
||||
if (PluginManager.IsMinimumAppVersionSatisfied(results[i].Name, results[i].MinimumAppVersion))
|
||||
updatedPluginResults.Add(results[i]);
|
||||
}
|
||||
|
||||
UserPlugins = updatedPluginResults;
|
||||
|
||||
lastFetchedAt = DateTime.Now;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Ignored
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Ioc.Default.GetRequiredService<IPublicAPI>().LogException(ClassName, "Http request failed", e);
|
||||
PublicApi.Instance.LogException(ClassName, "Http request failed", e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
manifestUpdateLock.Release();
|
||||
// Only release the lock if it was acquired
|
||||
if (lockAcquired) manifestUpdateLock.Release();
|
||||
}
|
||||
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0-windows</TargetFramework>
|
||||
<TargetFramework>net9.0-windows</TargetFramework>
|
||||
<UseWpf>true</UseWpf>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<OutputType>Library</OutputType>
|
||||
|
|
@ -12,6 +12,7 @@
|
|||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
|
|
@ -33,6 +34,7 @@
|
|||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<Prefer32Bit>false</Prefer32Bit>
|
||||
<NoWarn>$(NoWarn);FLSG0007</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
@ -54,11 +56,25 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Droplex" Version="1.7.0" />
|
||||
<PackageReference Include="FSharp.Core" Version="9.0.201" />
|
||||
<PackageReference Include="Meziantou.Framework.Win32.Jobs" Version="3.4.0" />
|
||||
<PackageReference Include="Flow.Launcher.Localization" Version="0.0.6" />
|
||||
<PackageReference Include="FSharp.Core" Version="9.0.303" />
|
||||
<PackageReference Include="Meziantou.Framework.Win32.Jobs" Version="3.4.5" />
|
||||
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" />
|
||||
<PackageReference Include="squirrel.windows" Version="1.5.2" NoWarn="NU1701" />
|
||||
<PackageReference Include="StreamJsonRpc" Version="2.21.10" />
|
||||
<PackageReference Include="SemanticVersioning" Version="3.0.0" />
|
||||
<!-- Do not upgrade this to higher version since higher version removes UpdateManager instance -->
|
||||
<PackageReference Include="squirrel.windows" Version="1.9.0" NoWarn="NU1701" />
|
||||
<PackageReference Include="StreamJsonRpc" Version="2.22.11" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<FLLUseDependencyInjection>true</FLLUseDependencyInjection>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Remove="Languages\en.xaml" />
|
||||
<AdditionalFiles Include="..\Flow.Launcher\Languages\en.xaml">
|
||||
<Link>Languages\en.xaml</Link>
|
||||
</AdditionalFiles>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
12
Flow.Launcher.Core/Plugin/IResultUpdateRegister.cs
Normal file
12
Flow.Launcher.Core/Plugin/IResultUpdateRegister.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
using Flow.Launcher.Plugin;
|
||||
|
||||
namespace Flow.Launcher.Core.Plugin;
|
||||
|
||||
public interface IResultUpdateRegister
|
||||
{
|
||||
/// <summary>
|
||||
/// Register a plugin to receive results updated event.
|
||||
/// </summary>
|
||||
/// <param name="pair"></param>
|
||||
void RegisterResultsUpdatedEvent(PluginPair pair);
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@ using Flow.Launcher.Plugin;
|
|||
|
||||
namespace Flow.Launcher.Core.Plugin
|
||||
{
|
||||
public class JsonRPCPluginSettings
|
||||
public class JsonRPCPluginSettings : ISavable
|
||||
{
|
||||
public required JsonRpcConfigurationModel? Configuration { get; init; }
|
||||
|
||||
|
|
@ -27,6 +27,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
private JsonStorage<ConcurrentDictionary<string, object?>> _storage = null!;
|
||||
|
||||
private static readonly double MainGridColumn0MaxWidthRatio = 0.6;
|
||||
private static readonly Thickness SettingPanelMargin = (Thickness)Application.Current.FindResource("SettingPanelMargin");
|
||||
private static readonly Thickness SettingPanelItemLeftMargin = (Thickness)Application.Current.FindResource("SettingPanelItemLeftMargin");
|
||||
private static readonly Thickness SettingPanelItemTopBottomMargin = (Thickness)Application.Current.FindResource("SettingPanelItemTopBottomMargin");
|
||||
|
|
@ -156,7 +157,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
{
|
||||
if (!NeedCreateSettingPanel()) return null!;
|
||||
|
||||
// Create main grid with two columns (Column 1: Auto, Column 2: *)
|
||||
// Create main grid with two columns (Column 0: Auto, Column 1: *)
|
||||
var mainPanel = new Grid { Margin = SettingPanelMargin, VerticalAlignment = VerticalAlignment.Center };
|
||||
mainPanel.ColumnDefinitions.Add(new ColumnDefinition()
|
||||
{
|
||||
|
|
@ -200,7 +201,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
{
|
||||
Text = attributes.Label,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
TextWrapping = TextWrapping.WrapWithOverflow
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
};
|
||||
|
||||
// Create a text block for description
|
||||
|
|
@ -211,7 +212,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
{
|
||||
Text = attributes.Description,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
TextWrapping = TextWrapping.WrapWithOverflow
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
};
|
||||
|
||||
desc.SetResourceReference(TextBlock.StyleProperty, "SettingPanelTextBlockDescriptionStyle"); // for theme change
|
||||
|
|
@ -247,7 +248,8 @@ namespace Flow.Launcher.Core.Plugin
|
|||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = SettingPanelItemLeftTopBottomMargin,
|
||||
Text = Settings[attributes.Name] as string ?? string.Empty,
|
||||
ToolTip = attributes.Description
|
||||
ToolTip = attributes.Description,
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
};
|
||||
|
||||
textBox.TextChanged += (_, _) =>
|
||||
|
|
@ -269,7 +271,8 @@ namespace Flow.Launcher.Core.Plugin
|
|||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = SettingPanelItemLeftMargin,
|
||||
Text = Settings[attributes.Name] as string ?? string.Empty,
|
||||
ToolTip = attributes.Description
|
||||
ToolTip = attributes.Description,
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
};
|
||||
|
||||
textBox.TextChanged += (_, _) =>
|
||||
|
|
@ -282,7 +285,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = SettingPanelItemLeftMargin,
|
||||
Content = API.GetTranslation("select")
|
||||
Content = Localize.select()
|
||||
};
|
||||
|
||||
Btn.Click += (_, _) =>
|
||||
|
|
@ -333,7 +336,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = SettingPanelItemLeftTopBottomMargin,
|
||||
TextWrapping = TextWrapping.WrapWithOverflow,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
AcceptsReturn = true,
|
||||
Text = Settings[attributes.Name] as string ?? string.Empty,
|
||||
ToolTip = attributes.Description
|
||||
|
|
@ -488,6 +491,8 @@ namespace Flow.Launcher.Core.Plugin
|
|||
rowCount++;
|
||||
}
|
||||
|
||||
mainPanel.SizeChanged += MainPanel_SizeChanged;
|
||||
|
||||
// Wrap the main grid in a user control
|
||||
return new UserControl()
|
||||
{
|
||||
|
|
@ -495,6 +500,28 @@ namespace Flow.Launcher.Core.Plugin
|
|||
};
|
||||
}
|
||||
|
||||
private void MainPanel_SizeChanged(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
if (sender is not Grid grid) return;
|
||||
|
||||
var workingWidth = grid.ActualWidth;
|
||||
|
||||
if (workingWidth <= 0) return;
|
||||
|
||||
var constrainedWidth = MainGridColumn0MaxWidthRatio * workingWidth;
|
||||
|
||||
// Set MaxWidth of column 0 and its children
|
||||
// We must set MaxWidth of its children to make text wrapping work correctly
|
||||
grid.ColumnDefinitions[0].MaxWidth = constrainedWidth;
|
||||
foreach (var child in grid.Children)
|
||||
{
|
||||
if (child is FrameworkElement element && Grid.GetColumn(element) == 0 && Grid.GetColumnSpan(element) == 1)
|
||||
{
|
||||
element.MaxWidth = constrainedWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool NeedSaveInSettings(string type)
|
||||
{
|
||||
return type != "textBlock" && type != "separator" && type != "hyperlink";
|
||||
|
|
|
|||
|
|
@ -100,11 +100,11 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
RPC = new JsonRpc(handler, new JsonRPCPublicAPI(Context.API));
|
||||
|
||||
RPC.AddLocalRpcMethod("UpdateResults", new Action<string, JsonRPCQueryResponseModel>((rawQuery, response) =>
|
||||
RPC.AddLocalRpcMethod("UpdateResults", new Action<string, JsonRPCQueryResponseModel>((trimmedQuery, response) =>
|
||||
{
|
||||
var results = ParseResults(response);
|
||||
ResultsUpdated?.Invoke(this,
|
||||
new ResultUpdatedEventArgs { Query = new Query() { RawQuery = rawQuery }, Results = results });
|
||||
new ResultUpdatedEventArgs { Query = new Query() { TrimmedQuery = trimmedQuery }, Results = results });
|
||||
}));
|
||||
RPC.SynchronizationContext = null;
|
||||
RPC.StartListening();
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ using System.IO;
|
|||
using Flow.Launcher.Infrastructure;
|
||||
using Flow.Launcher.Plugin;
|
||||
using System.Text.Json;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
|
||||
namespace Flow.Launcher.Core.Plugin
|
||||
{
|
||||
|
|
@ -13,10 +14,6 @@ namespace Flow.Launcher.Core.Plugin
|
|||
{
|
||||
private static readonly string ClassName = nameof(PluginConfig);
|
||||
|
||||
// We should not initialize API in static constructor because it will create another API instance
|
||||
private static IPublicAPI api = null;
|
||||
private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService<IPublicAPI>();
|
||||
|
||||
/// <summary>
|
||||
/// Parse plugin metadata in the given directories
|
||||
/// </summary>
|
||||
|
|
@ -30,15 +27,26 @@ namespace Flow.Launcher.Core.Plugin
|
|||
// todo use linq when diable plugin is implmented since parallel.foreach + list is not thread saft
|
||||
foreach (var directory in directories)
|
||||
{
|
||||
if (File.Exists(Path.Combine(directory, "NeedDelete.txt")))
|
||||
if (File.Exists(Path.Combine(directory, DataLocation.PluginDeleteFile)))
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.Delete(directory, true);
|
||||
var fullyDeleted = FilesFolders.TryDeleteDirectoryRobust(directory, maxRetries: 3, retryDelayMs: 200);
|
||||
if (!fullyDeleted)
|
||||
{
|
||||
PublicApi.Instance.LogWarn(ClassName, $"Directory <{directory}> was not fully deleted.");
|
||||
|
||||
// Directory was not fully deleted, recreate the marker file so deletion will be retried on next startup
|
||||
var markerFilePath = Path.Combine(directory, DataLocation.PluginDeleteFile);
|
||||
if (!File.Exists(markerFilePath))
|
||||
{
|
||||
File.WriteAllText(markerFilePath, string.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
API.LogException(ClassName, $"Can't delete <{directory}>", e);
|
||||
PublicApi.Instance.LogException(ClassName, $"Can't delete <{directory}>", e);
|
||||
}
|
||||
}
|
||||
else
|
||||
|
|
@ -55,7 +63,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
duplicateList
|
||||
.ForEach(
|
||||
x => API.LogWarn(ClassName,
|
||||
x => PublicApi.Instance.LogWarn(ClassName,
|
||||
string.Format("Duplicate plugin name: {0}, id: {1}, version: {2} " +
|
||||
"not loaded due to version not the highest of the duplicates",
|
||||
x.Name, x.ID, x.Version),
|
||||
|
|
@ -107,7 +115,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
string configPath = Path.Combine(pluginDirectory, Constant.PluginMetadataFileName);
|
||||
if (!File.Exists(configPath))
|
||||
{
|
||||
API.LogError(ClassName, $"Didn't find config file <{configPath}>");
|
||||
PublicApi.Instance.LogError(ClassName, $"Didn't find config file <{configPath}>");
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -123,19 +131,19 @@ namespace Flow.Launcher.Core.Plugin
|
|||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
API.LogException(ClassName, $"Invalid json for config <{configPath}>", e);
|
||||
PublicApi.Instance.LogException(ClassName, $"Invalid json for config <{configPath}>", e);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!AllowedLanguage.IsAllowed(metadata.Language))
|
||||
{
|
||||
API.LogError(ClassName, $"Invalid language <{metadata.Language}> for config <{configPath}>");
|
||||
PublicApi.Instance.LogError(ClassName, $"Invalid language <{metadata.Language}> for config <{configPath}>");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!File.Exists(metadata.ExecuteFilePath))
|
||||
{
|
||||
API.LogError(ClassName, $"Execute file path didn't exist <{metadata.ExecuteFilePath}> for conifg <{configPath}");
|
||||
PublicApi.Instance.LogError(ClassName, $"Execute file path didn't exist <{metadata.ExecuteFilePath}> for conifg <{configPath}");
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
|||
462
Flow.Launcher.Core/Plugin/PluginInstaller.cs
Normal file
462
Flow.Launcher.Core/Plugin/PluginInstaller.cs
Normal file
|
|
@ -0,0 +1,462 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
|
||||
namespace Flow.Launcher.Core.Plugin;
|
||||
|
||||
/// <summary>
|
||||
/// Class for installing, updating, and uninstalling plugins.
|
||||
/// </summary>
|
||||
public static class PluginInstaller
|
||||
{
|
||||
private static readonly string ClassName = nameof(PluginInstaller);
|
||||
|
||||
private static readonly Settings Settings = Ioc.Default.GetRequiredService<Settings>();
|
||||
|
||||
/// <summary>
|
||||
/// Installs a plugin and restarts the application if required by settings. Prompts user for confirmation and handles download if needed.
|
||||
/// </summary>
|
||||
/// <param name="newPlugin">The plugin to install.</param>
|
||||
/// <returns>A Task representing the asynchronous install operation.</returns>
|
||||
public static async Task InstallPluginAndCheckRestartAsync(UserPlugin newPlugin)
|
||||
{
|
||||
if (PublicApi.Instance.PluginModified(newPlugin.ID))
|
||||
{
|
||||
PublicApi.Instance.ShowMsgError(Localize.pluginModifiedAlreadyTitle(newPlugin.Name),
|
||||
Localize.pluginModifiedAlreadyMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
if (PublicApi.Instance.ShowMsgBox(
|
||||
Localize.InstallPromptSubtitle(newPlugin.Name, newPlugin.Author, Environment.NewLine),
|
||||
Localize.InstallPromptTitle(),
|
||||
button: MessageBoxButton.YesNo) != MessageBoxResult.Yes) return;
|
||||
|
||||
try
|
||||
{
|
||||
// at minimum should provide a name, but handle plugin that is not downloaded from plugins manifest and is a url download
|
||||
var downloadFilename = string.IsNullOrEmpty(newPlugin.Version)
|
||||
? $"{newPlugin.Name}-{Guid.NewGuid()}.zip"
|
||||
: $"{newPlugin.Name}-{newPlugin.Version}.zip";
|
||||
|
||||
var filePath = Path.Combine(Path.GetTempPath(), downloadFilename);
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
|
||||
if (!newPlugin.IsFromLocalInstallPath)
|
||||
{
|
||||
await DownloadFileAsync(
|
||||
$"{Localize.DownloadingPlugin()} {newPlugin.Name}",
|
||||
newPlugin.UrlDownload, filePath, cts);
|
||||
}
|
||||
else
|
||||
{
|
||||
filePath = newPlugin.LocalInstallPath;
|
||||
}
|
||||
|
||||
// check if user cancelled download before installing plugin
|
||||
if (cts.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
throw new FileNotFoundException($"Plugin {newPlugin.ID} zip file not found at {filePath}", filePath);
|
||||
}
|
||||
|
||||
if (!PublicApi.Instance.InstallPlugin(newPlugin, filePath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!newPlugin.IsFromLocalInstallPath)
|
||||
{
|
||||
File.Delete(filePath);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
PublicApi.Instance.LogException(ClassName, "Failed to install plugin", e);
|
||||
PublicApi.Instance.ShowMsgError(Localize.ErrorInstallingPlugin());
|
||||
return; // do not restart on failure
|
||||
}
|
||||
|
||||
if (Settings.AutoRestartAfterChanging)
|
||||
{
|
||||
PublicApi.Instance.RestartApp();
|
||||
}
|
||||
else
|
||||
{
|
||||
PublicApi.Instance.ShowMsg(
|
||||
Localize.installbtn(),
|
||||
Localize.InstallSuccessNoRestart(newPlugin.Name));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Installs a plugin from a local zip file and restarts the application if required by settings. Validates the zip and prompts user for confirmation.
|
||||
/// </summary>
|
||||
/// <param name="filePath">The path to the plugin zip file.</param>
|
||||
/// <returns>A Task representing the asynchronous install operation.</returns>
|
||||
public static async Task InstallPluginAndCheckRestartAsync(string filePath)
|
||||
{
|
||||
UserPlugin plugin;
|
||||
try
|
||||
{
|
||||
using ZipArchive archive = ZipFile.OpenRead(filePath);
|
||||
var pluginJsonEntry = archive.Entries.FirstOrDefault(x => x.Name == "plugin.json") ??
|
||||
throw new FileNotFoundException("The zip file does not contain a plugin.json file.");
|
||||
|
||||
using Stream stream = pluginJsonEntry.Open();
|
||||
plugin = JsonSerializer.Deserialize<UserPlugin>(stream);
|
||||
plugin.IcoPath = "Images\\zipfolder.png";
|
||||
plugin.LocalInstallPath = filePath;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
PublicApi.Instance.LogException(ClassName, "Failed to validate zip file", e);
|
||||
PublicApi.Instance.ShowMsgError(Localize.ZipFileNotHavePluginJson());
|
||||
return;
|
||||
}
|
||||
|
||||
if (PublicApi.Instance.PluginModified(plugin.ID))
|
||||
{
|
||||
PublicApi.Instance.ShowMsgError(Localize.pluginModifiedAlreadyTitle(plugin.Name),
|
||||
Localize.pluginModifiedAlreadyMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
if (Settings.ShowUnknownSourceWarning)
|
||||
{
|
||||
if (!InstallSourceKnown(plugin.Website)
|
||||
&& PublicApi.Instance.ShowMsgBox(Localize.InstallFromUnknownSourceSubtitle(Environment.NewLine),
|
||||
Localize.InstallFromUnknownSourceTitle(),
|
||||
MessageBoxButton.YesNo) == MessageBoxResult.No)
|
||||
return;
|
||||
}
|
||||
|
||||
await InstallPluginAndCheckRestartAsync(plugin);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uninstalls a plugin and restarts the application if required by settings. Prompts user for confirmation and whether to keep plugin settings.
|
||||
/// </summary>
|
||||
/// <param name="oldPlugin">The plugin metadata to uninstall.</param>
|
||||
/// <returns>A Task representing the asynchronous uninstall operation.</returns>
|
||||
public static async Task UninstallPluginAndCheckRestartAsync(PluginMetadata oldPlugin)
|
||||
{
|
||||
if (PublicApi.Instance.PluginModified(oldPlugin.ID))
|
||||
{
|
||||
PublicApi.Instance.ShowMsgError(Localize.pluginModifiedAlreadyTitle(oldPlugin.Name),
|
||||
Localize.pluginModifiedAlreadyMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
if (PublicApi.Instance.ShowMsgBox(
|
||||
Localize.UninstallPromptSubtitle(oldPlugin.Name, oldPlugin.Author, Environment.NewLine),
|
||||
Localize.UninstallPromptTitle(),
|
||||
button: MessageBoxButton.YesNo) != MessageBoxResult.Yes) return;
|
||||
|
||||
var removePluginSettings = PublicApi.Instance.ShowMsgBox(
|
||||
Localize.KeepPluginSettingsSubtitle(),
|
||||
Localize.KeepPluginSettingsTitle(),
|
||||
button: MessageBoxButton.YesNo) == MessageBoxResult.No;
|
||||
|
||||
try
|
||||
{
|
||||
if (!await PublicApi.Instance.UninstallPluginAsync(oldPlugin, removePluginSettings))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
PublicApi.Instance.LogException(ClassName, "Failed to uninstall plugin", e);
|
||||
PublicApi.Instance.ShowMsgError(Localize.ErrorUninstallingPlugin());
|
||||
return; // don not restart on failure
|
||||
}
|
||||
|
||||
if (Settings.AutoRestartAfterChanging)
|
||||
{
|
||||
PublicApi.Instance.RestartApp();
|
||||
}
|
||||
else
|
||||
{
|
||||
PublicApi.Instance.ShowMsg(
|
||||
Localize.uninstallbtn(),
|
||||
Localize.UninstallSuccessNoRestart(oldPlugin.Name));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates a plugin to a new version and restarts the application if required by settings. Prompts user for confirmation and handles download if needed.
|
||||
/// </summary>
|
||||
/// <param name="newPlugin">The new plugin version to install.</param>
|
||||
/// <param name="oldPlugin">The existing plugin metadata to update.</param>
|
||||
/// <returns>A Task representing the asynchronous update operation.</returns>
|
||||
public static async Task UpdatePluginAndCheckRestartAsync(UserPlugin newPlugin, PluginMetadata oldPlugin)
|
||||
{
|
||||
if (PublicApi.Instance.ShowMsgBox(
|
||||
Localize.UpdatePromptSubtitle(oldPlugin.Name, oldPlugin.Author, Environment.NewLine),
|
||||
Localize.UpdatePromptTitle(),
|
||||
button: MessageBoxButton.YesNo) != MessageBoxResult.Yes) return;
|
||||
|
||||
try
|
||||
{
|
||||
var filePath = Path.Combine(Path.GetTempPath(), $"{newPlugin.Name}-{newPlugin.Version}.zip");
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
|
||||
if (!newPlugin.IsFromLocalInstallPath)
|
||||
{
|
||||
await DownloadFileAsync(
|
||||
$"{Localize.DownloadingPlugin()} {newPlugin.Name}",
|
||||
newPlugin.UrlDownload, filePath, cts);
|
||||
}
|
||||
else
|
||||
{
|
||||
filePath = newPlugin.LocalInstallPath;
|
||||
}
|
||||
|
||||
// check if user cancelled download before installing plugin
|
||||
if (cts.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await PublicApi.Instance.UpdatePluginAsync(oldPlugin, newPlugin, filePath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
PublicApi.Instance.LogException(ClassName, "Failed to update plugin", e);
|
||||
PublicApi.Instance.ShowMsgError(Localize.ErrorUpdatingPlugin());
|
||||
return; // do not restart on failure
|
||||
}
|
||||
|
||||
if (Settings.AutoRestartAfterChanging)
|
||||
{
|
||||
PublicApi.Instance.RestartApp();
|
||||
}
|
||||
else
|
||||
{
|
||||
PublicApi.Instance.ShowMsg(
|
||||
Localize.updatebtn(),
|
||||
Localize.UpdateSuccessNoRestart(newPlugin.Name));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the plugin to the latest version available from its source.
|
||||
/// </summary>
|
||||
/// <param name="updateAllPlugins">Action to execute when the user chooses to update all plugins.</param>
|
||||
/// <param name="silentUpdate">If true, do not show any messages when there is no update available.</param>
|
||||
/// <param name="usePrimaryUrlOnly">If true, only use the primary URL for updates.</param>
|
||||
/// <param name="token">Cancellation token to cancel the update operation.</param>
|
||||
/// <returns></returns>
|
||||
public static async Task CheckForPluginUpdatesAsync(Action<List<PluginUpdateInfo>> updateAllPlugins, bool silentUpdate = true, bool usePrimaryUrlOnly = false, CancellationToken token = default)
|
||||
{
|
||||
// Update the plugin manifest
|
||||
await PublicApi.Instance.UpdatePluginManifestAsync(usePrimaryUrlOnly, token);
|
||||
|
||||
// Get all plugins that can be updated
|
||||
var resultsForUpdate = (
|
||||
from existingPlugin in PublicApi.Instance.GetAllPlugins()
|
||||
join pluginUpdateSource in PublicApi.Instance.GetPluginManifest()
|
||||
on existingPlugin.Metadata.ID equals pluginUpdateSource.ID
|
||||
where string.Compare(existingPlugin.Metadata.Version, pluginUpdateSource.Version,
|
||||
StringComparison.InvariantCulture) <
|
||||
0 // if current version precedes version of the plugin from update source (e.g. PluginsManifest)
|
||||
&& !PublicApi.Instance.PluginModified(existingPlugin.Metadata.ID)
|
||||
select
|
||||
new PluginUpdateInfo()
|
||||
{
|
||||
ID = existingPlugin.Metadata.ID,
|
||||
Name = existingPlugin.Metadata.Name,
|
||||
Author = existingPlugin.Metadata.Author,
|
||||
CurrentVersion = existingPlugin.Metadata.Version,
|
||||
NewVersion = pluginUpdateSource.Version,
|
||||
IcoPath = existingPlugin.Metadata.IcoPath,
|
||||
PluginExistingMetadata = existingPlugin.Metadata,
|
||||
PluginNewUserPlugin = pluginUpdateSource
|
||||
}).ToList();
|
||||
|
||||
// No updates
|
||||
if (resultsForUpdate.Count == 0)
|
||||
{
|
||||
if (!silentUpdate)
|
||||
{
|
||||
PublicApi.Instance.ShowMsg(Localize.updateNoResultTitle(), Localize.updateNoResultSubtitle());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// If all plugins are modified, just return
|
||||
if (resultsForUpdate.All(x => PublicApi.Instance.PluginModified(x.ID)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Show message box with button to update all plugins
|
||||
PublicApi.Instance.ShowMsgWithButton(
|
||||
Localize.updateAllPluginsTitle(),
|
||||
Localize.updateAllPluginsButtonContent(),
|
||||
() =>
|
||||
{
|
||||
updateAllPlugins(resultsForUpdate);
|
||||
},
|
||||
string.Join(", ", resultsForUpdate.Select(x => x.PluginExistingMetadata.Name)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates all plugins that have available updates.
|
||||
/// </summary>
|
||||
/// <param name="resultsForUpdate"></param>
|
||||
/// <param name="restart"></param>
|
||||
public static async Task UpdateAllPluginsAsync(IEnumerable<PluginUpdateInfo> resultsForUpdate, bool restart)
|
||||
{
|
||||
var anyPluginSuccess = false;
|
||||
await Task.WhenAll(resultsForUpdate.Select(async plugin =>
|
||||
{
|
||||
var downloadToFilePath = Path.Combine(Path.GetTempPath(), $"{plugin.Name}-{plugin.NewVersion}.zip");
|
||||
|
||||
try
|
||||
{
|
||||
using var cts = new CancellationTokenSource();
|
||||
|
||||
await DownloadFileAsync(
|
||||
$"{Localize.DownloadingPlugin()} {plugin.PluginNewUserPlugin.Name}",
|
||||
plugin.PluginNewUserPlugin.UrlDownload, downloadToFilePath, cts);
|
||||
|
||||
// check if user cancelled download before installing plugin
|
||||
if (cts.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await PublicApi.Instance.UpdatePluginAsync(plugin.PluginExistingMetadata, plugin.PluginNewUserPlugin, downloadToFilePath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
anyPluginSuccess = true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
PublicApi.Instance.LogException(ClassName, "Failed to update plugin", e);
|
||||
PublicApi.Instance.ShowMsgError(Localize.ErrorUpdatingPlugin());
|
||||
}
|
||||
}));
|
||||
|
||||
if (!anyPluginSuccess) return;
|
||||
|
||||
if (restart)
|
||||
{
|
||||
PublicApi.Instance.RestartApp();
|
||||
}
|
||||
else
|
||||
{
|
||||
PublicApi.Instance.ShowMsg(
|
||||
Localize.updatebtn(),
|
||||
Localize.PluginsUpdateSuccessNoRestart());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Downloads a file from a URL to a local path, optionally showing a progress box and handling cancellation.
|
||||
/// </summary>
|
||||
/// <param name="progressBoxTitle">The title for the progress box.</param>
|
||||
/// <param name="downloadUrl">The URL to download from.</param>
|
||||
/// <param name="filePath">The local file path to save to.</param>
|
||||
/// <param name="cts">Cancellation token source for cancelling the download.</param>
|
||||
/// <param name="deleteFile">Whether to delete the file if it already exists.</param>
|
||||
/// <param name="showProgress">Whether to show a progress box during download.</param>
|
||||
/// <returns>A Task representing the asynchronous download operation.</returns>
|
||||
private static async Task DownloadFileAsync(string progressBoxTitle, string downloadUrl, string filePath, CancellationTokenSource cts, bool deleteFile = true, bool showProgress = true)
|
||||
{
|
||||
if (deleteFile && File.Exists(filePath))
|
||||
File.Delete(filePath);
|
||||
|
||||
if (showProgress)
|
||||
{
|
||||
var exceptionHappened = false;
|
||||
await PublicApi.Instance.ShowProgressBoxAsync(progressBoxTitle,
|
||||
async (reportProgress) =>
|
||||
{
|
||||
if (reportProgress == null)
|
||||
{
|
||||
// when reportProgress is null, it means there is exception with the progress box
|
||||
// so we record it with exceptionHappened and return so that progress box will close instantly
|
||||
exceptionHappened = true;
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
await PublicApi.Instance.HttpDownloadAsync(downloadUrl, filePath, reportProgress, cts.Token).ConfigureAwait(false);
|
||||
}
|
||||
}, cts.Cancel);
|
||||
|
||||
// if exception happened while downloading and user does not cancel downloading,
|
||||
// we need to redownload the plugin
|
||||
if (exceptionHappened && (!cts.IsCancellationRequested))
|
||||
await PublicApi.Instance.HttpDownloadAsync(downloadUrl, filePath, token: cts.Token).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await PublicApi.Instance.HttpDownloadAsync(downloadUrl, filePath, token: cts.Token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the plugin install source is a known/approved source (e.g., GitHub and matches an existing plugin author).
|
||||
/// </summary>
|
||||
/// <param name="url">The URL to check.</param>
|
||||
/// <returns>True if the source is known, otherwise false.</returns>
|
||||
private static bool InstallSourceKnown(string url)
|
||||
{
|
||||
if (string.IsNullOrEmpty(url))
|
||||
return false;
|
||||
|
||||
var pieces = url.Split('/');
|
||||
|
||||
if (pieces.Length < 4)
|
||||
return false;
|
||||
|
||||
var author = pieces[3];
|
||||
var acceptedHost = "github.com";
|
||||
var acceptedSource = "https://github.com";
|
||||
var constructedUrlPart = string.Format("{0}/{1}/", acceptedSource, author);
|
||||
|
||||
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri) || uri.Host != acceptedHost)
|
||||
return false;
|
||||
|
||||
return PublicApi.Instance.GetAllPlugins().Any(x =>
|
||||
!string.IsNullOrEmpty(x.Metadata.Website) &&
|
||||
x.Metadata.Website.StartsWith(constructedUrlPart)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public record PluginUpdateInfo
|
||||
{
|
||||
public string ID { get; init; }
|
||||
public string Name { get; init; }
|
||||
public string Author { get; init; }
|
||||
public string CurrentVersion { get; init; }
|
||||
public string NewVersion { get; init; }
|
||||
public string IcoPath { get; init; }
|
||||
public PluginMetadata PluginExistingMetadata { get; init; }
|
||||
public UserPlugin PluginNewUserPlugin { get; init; }
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -2,9 +2,6 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Core.ExternalPlugins.Environments;
|
||||
#pragma warning disable IDE0005
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
|
|
@ -18,10 +15,6 @@ namespace Flow.Launcher.Core.Plugin
|
|||
{
|
||||
private static readonly string ClassName = nameof(PluginsLoader);
|
||||
|
||||
// We should not initialize API in static constructor because it will create another API instance
|
||||
private static IPublicAPI api = null;
|
||||
private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService<IPublicAPI>();
|
||||
|
||||
public static List<PluginPair> Plugins(List<PluginMetadata> metadatas, PluginsSettings settings)
|
||||
{
|
||||
var dotnetPlugins = DotNetPlugins(metadatas);
|
||||
|
|
@ -55,7 +48,7 @@ namespace Flow.Launcher.Core.Plugin
|
|||
return plugins;
|
||||
}
|
||||
|
||||
private static IEnumerable<PluginPair> DotNetPlugins(List<PluginMetadata> source)
|
||||
private static List<PluginPair> DotNetPlugins(List<PluginMetadata> source)
|
||||
{
|
||||
var erroredPlugins = new List<string>();
|
||||
|
||||
|
|
@ -64,73 +57,71 @@ namespace Flow.Launcher.Core.Plugin
|
|||
|
||||
foreach (var metadata in metadatas)
|
||||
{
|
||||
var milliseconds = API.StopwatchLogDebug(ClassName, $"Constructor init cost for {metadata.Name}", () =>
|
||||
var milliseconds = PublicApi.Instance.StopwatchLogDebug(ClassName, $"Constructor init cost for {metadata.Name}", () =>
|
||||
{
|
||||
Assembly assembly = null;
|
||||
IAsyncPlugin plugin = null;
|
||||
|
||||
try
|
||||
{
|
||||
Assembly assembly = null;
|
||||
IAsyncPlugin plugin = null;
|
||||
var assemblyLoader = new PluginAssemblyLoader(metadata.ExecuteFilePath);
|
||||
assembly = assemblyLoader.LoadAssemblyAndDependencies();
|
||||
|
||||
try
|
||||
{
|
||||
var assemblyLoader = new PluginAssemblyLoader(metadata.ExecuteFilePath);
|
||||
assembly = assemblyLoader.LoadAssemblyAndDependencies();
|
||||
var type = assemblyLoader.FromAssemblyGetTypeOfInterface(assembly,
|
||||
typeof(IAsyncPlugin));
|
||||
|
||||
var type = assemblyLoader.FromAssemblyGetTypeOfInterface(assembly,
|
||||
typeof(IAsyncPlugin));
|
||||
plugin = Activator.CreateInstance(type) as IAsyncPlugin;
|
||||
|
||||
plugin = Activator.CreateInstance(type) as IAsyncPlugin;
|
||||
|
||||
metadata.AssemblyName = assembly.GetName().Name;
|
||||
}
|
||||
metadata.AssemblyName = assembly.GetName().Name;
|
||||
}
|
||||
#if DEBUG
|
||||
catch (Exception)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
#else
|
||||
catch (Exception e) when (assembly == null)
|
||||
{
|
||||
Log.Exception(ClassName, $"Couldn't load assembly for the plugin: {metadata.Name}", e);
|
||||
}
|
||||
catch (InvalidOperationException e)
|
||||
{
|
||||
Log.Exception(ClassName, $"Can't find the required IPlugin interface for the plugin: <{metadata.Name}>", e);
|
||||
}
|
||||
catch (ReflectionTypeLoadException e)
|
||||
{
|
||||
Log.Exception(ClassName, $"The GetTypes method was unable to load assembly types for the plugin: <{metadata.Name}>", e);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Exception(ClassName, $"The following plugin has errored and can not be loaded: <{metadata.Name}>", e);
|
||||
}
|
||||
catch (Exception e) when (assembly == null)
|
||||
{
|
||||
PublicApi.Instance.LogException(ClassName, $"Couldn't load assembly for the plugin: {metadata.Name}", e);
|
||||
}
|
||||
catch (InvalidOperationException e)
|
||||
{
|
||||
PublicApi.Instance.LogException(ClassName, $"Can't find the required IPlugin interface for the plugin: <{metadata.Name}>", e);
|
||||
}
|
||||
catch (ReflectionTypeLoadException e)
|
||||
{
|
||||
PublicApi.Instance.LogException(ClassName, $"The GetTypes method was unable to load assembly types for the plugin: <{metadata.Name}>", e);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
PublicApi.Instance.LogException(ClassName, $"The following plugin has errored and can not be loaded: <{metadata.Name}>", e);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (plugin == null)
|
||||
{
|
||||
erroredPlugins.Add(metadata.Name);
|
||||
return;
|
||||
}
|
||||
if (plugin == null)
|
||||
{
|
||||
erroredPlugins.Add(metadata.Name);
|
||||
return;
|
||||
}
|
||||
|
||||
plugins.Add(new PluginPair { Plugin = plugin, Metadata = metadata });
|
||||
});
|
||||
|
||||
plugins.Add(new PluginPair { Plugin = plugin, Metadata = metadata });
|
||||
});
|
||||
metadata.InitTime += milliseconds;
|
||||
PublicApi.Instance.LogDebug(ClassName, $"Constructor cost for <{metadata.Name}> is <{metadata.InitTime}ms>");
|
||||
}
|
||||
|
||||
if (erroredPlugins.Count > 0)
|
||||
{
|
||||
var errorPluginString = string.Join(Environment.NewLine, erroredPlugins);
|
||||
|
||||
var errorMessage = "The following "
|
||||
+ (erroredPlugins.Count > 1 ? "plugins have " : "plugin has ")
|
||||
+ "errored and cannot be loaded:";
|
||||
var errorMessage = erroredPlugins.Count > 1 ?
|
||||
Localize.pluginsHaveErrored():
|
||||
Localize.pluginHasErrored();
|
||||
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
Ioc.Default.GetRequiredService<IPublicAPI>().ShowMsgBox($"{errorMessage}{Environment.NewLine}{Environment.NewLine}" +
|
||||
$"{errorPluginString}{Environment.NewLine}{Environment.NewLine}" +
|
||||
$"Please refer to the logs for more information", "",
|
||||
MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
});
|
||||
PublicApi.Instance.ShowMsgError($"{errorMessage}{Environment.NewLine}{Environment.NewLine}" +
|
||||
$"{errorPluginString}{Environment.NewLine}{Environment.NewLine}" +
|
||||
Localize.referToLogs());
|
||||
}
|
||||
|
||||
return plugins;
|
||||
|
|
|
|||
|
|
@ -6,15 +6,16 @@ namespace Flow.Launcher.Core.Plugin
|
|||
{
|
||||
public static class QueryBuilder
|
||||
{
|
||||
public static Query Build(string text, Dictionary<string, PluginPair> nonGlobalPlugins)
|
||||
public static Query Build(string originalQuery, string trimmedQuery, Dictionary<string, PluginPair> nonGlobalPlugins)
|
||||
{
|
||||
// home query
|
||||
if (string.IsNullOrEmpty(text))
|
||||
if (string.IsNullOrEmpty(trimmedQuery))
|
||||
{
|
||||
return new Query()
|
||||
{
|
||||
Search = string.Empty,
|
||||
RawQuery = string.Empty,
|
||||
OriginalQuery = string.Empty,
|
||||
TrimmedQuery = string.Empty,
|
||||
SearchTerms = Array.Empty<string>(),
|
||||
ActionKeyword = string.Empty,
|
||||
IsHomeQuery = true
|
||||
|
|
@ -22,14 +23,13 @@ namespace Flow.Launcher.Core.Plugin
|
|||
}
|
||||
|
||||
// replace multiple white spaces with one white space
|
||||
var terms = text.Split(Query.TermSeparator, StringSplitOptions.RemoveEmptyEntries);
|
||||
var terms = trimmedQuery.Split(Query.TermSeparator, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (terms.Length == 0)
|
||||
{
|
||||
// nothing was typed
|
||||
return null;
|
||||
}
|
||||
|
||||
var rawQuery = text;
|
||||
string actionKeyword, search;
|
||||
string possibleActionKeyword = terms[0];
|
||||
string[] searchTerms;
|
||||
|
|
@ -38,21 +38,22 @@ namespace Flow.Launcher.Core.Plugin
|
|||
{
|
||||
// use non global plugin for query
|
||||
actionKeyword = possibleActionKeyword;
|
||||
search = terms.Length > 1 ? rawQuery[(actionKeyword.Length + 1)..].TrimStart() : string.Empty;
|
||||
search = terms.Length > 1 ? trimmedQuery[(actionKeyword.Length + 1)..].TrimStart() : string.Empty;
|
||||
searchTerms = terms[1..];
|
||||
}
|
||||
else
|
||||
{
|
||||
// non action keyword
|
||||
actionKeyword = string.Empty;
|
||||
search = rawQuery.TrimStart();
|
||||
search = trimmedQuery.TrimStart();
|
||||
searchTerms = terms;
|
||||
}
|
||||
|
||||
return new Query()
|
||||
{
|
||||
Search = search,
|
||||
RawQuery = rawQuery,
|
||||
OriginalQuery = originalQuery,
|
||||
TrimmedQuery = trimmedQuery,
|
||||
SearchTerms = searchTerms,
|
||||
ActionKeyword = actionKeyword,
|
||||
IsHomeQuery = false
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ namespace Flow.Launcher.Core.Resource
|
|||
public static Language German = new Language("de", "Deutsch");
|
||||
public static Language Korean = new Language("ko", "한국어");
|
||||
public static Language Serbian = new Language("sr", "Srpski");
|
||||
public static Language Serbian_Cyrillic = new Language("sr-Cyrl-RS", "Српски");
|
||||
public static Language Portuguese_Portugal = new Language("pt-pt", "Português");
|
||||
public static Language Portuguese_Brazil = new Language("pt-br", "Português (Brasil)");
|
||||
public static Language Spanish = new Language("es", "Spanish");
|
||||
|
|
@ -47,6 +48,7 @@ namespace Flow.Launcher.Core.Resource
|
|||
German,
|
||||
Korean,
|
||||
Serbian,
|
||||
Serbian_Cyrillic,
|
||||
Portuguese_Portugal,
|
||||
Portuguese_Brazil,
|
||||
Spanish,
|
||||
|
|
@ -79,7 +81,8 @@ namespace Flow.Launcher.Core.Resource
|
|||
"da" => "System",
|
||||
"de" => "System",
|
||||
"ko" => "시스템",
|
||||
"sr" => "Систем",
|
||||
"sr" => "Sistem",
|
||||
"sr-Cyrl-RS" => "Систем",
|
||||
"pt-pt" => "Sistema",
|
||||
"pt-br" => "Sistema",
|
||||
"es" => "Sistema",
|
||||
|
|
|
|||
|
|
@ -1,50 +1,43 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using Flow.Launcher.Core.Plugin;
|
||||
using Flow.Launcher.Infrastructure;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
using System.Globalization;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
|
||||
namespace Flow.Launcher.Core.Resource
|
||||
{
|
||||
public class Internationalization
|
||||
public class Internationalization : IDisposable
|
||||
{
|
||||
private static readonly string ClassName = nameof(Internationalization);
|
||||
|
||||
// We should not initialize API in static constructor because it will create another API instance
|
||||
private static IPublicAPI api = null;
|
||||
private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService<IPublicAPI>();
|
||||
|
||||
private const string Folder = "Languages";
|
||||
private const string DefaultLanguageCode = "en";
|
||||
private const string DefaultFile = "en.xaml";
|
||||
private const string Extension = ".xaml";
|
||||
private readonly Settings _settings;
|
||||
private readonly List<string> _languageDirectories = new();
|
||||
private readonly List<ResourceDictionary> _oldResources = new();
|
||||
private readonly string SystemLanguageCode;
|
||||
private readonly List<string> _languageDirectories = [];
|
||||
private readonly List<ResourceDictionary> _oldResources = [];
|
||||
private static string SystemLanguageCode;
|
||||
private readonly SemaphoreSlim _langChangeLock = new(1, 1);
|
||||
|
||||
public Internationalization(Settings settings)
|
||||
{
|
||||
_settings = settings;
|
||||
AddFlowLauncherLanguageDirectory();
|
||||
SystemLanguageCode = GetSystemLanguageCodeAtStartup();
|
||||
}
|
||||
|
||||
private void AddFlowLauncherLanguageDirectory()
|
||||
{
|
||||
var directory = Path.Combine(Constant.ProgramDirectory, Folder);
|
||||
_languageDirectories.Add(directory);
|
||||
}
|
||||
#region Initialization
|
||||
|
||||
private static string GetSystemLanguageCodeAtStartup()
|
||||
/// <summary>
|
||||
/// Initialize the system language code based on the current culture.
|
||||
/// </summary>
|
||||
public static void InitSystemLanguageCode()
|
||||
{
|
||||
var availableLanguages = AvailableLanguages.GetAvailableLanguages();
|
||||
|
||||
|
|
@ -65,40 +58,11 @@ namespace Flow.Launcher.Core.Resource
|
|||
string.Equals(languageCode, threeLetterCode, StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(languageCode, fullName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return languageCode;
|
||||
SystemLanguageCode = languageCode;
|
||||
}
|
||||
}
|
||||
|
||||
return DefaultLanguageCode;
|
||||
}
|
||||
|
||||
private void AddPluginLanguageDirectories()
|
||||
{
|
||||
foreach (var plugin in PluginManager.GetPluginsForInterface<IPluginI18n>())
|
||||
{
|
||||
var location = Assembly.GetAssembly(plugin.Plugin.GetType()).Location;
|
||||
var dir = Path.GetDirectoryName(location);
|
||||
if (dir != null)
|
||||
{
|
||||
var pluginThemeDirectory = Path.Combine(dir, Folder);
|
||||
_languageDirectories.Add(pluginThemeDirectory);
|
||||
}
|
||||
else
|
||||
{
|
||||
API.LogError(ClassName, $"Can't find plugin path <{location}> for <{plugin.Metadata.Name}>");
|
||||
}
|
||||
}
|
||||
|
||||
LoadDefaultLanguage();
|
||||
}
|
||||
|
||||
private void LoadDefaultLanguage()
|
||||
{
|
||||
// Removes language files loaded before any plugins were loaded.
|
||||
// Prevents the language Flow started in from overwriting English if the user switches back to English
|
||||
RemoveOldLanguageFiles();
|
||||
LoadLanguage(AvailableLanguages.English);
|
||||
_oldResources.Clear();
|
||||
SystemLanguageCode = DefaultLanguageCode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -116,13 +80,64 @@ namespace Flow.Launcher.Core.Resource
|
|||
// Get language by language code and change language
|
||||
var language = GetLanguageByLanguageCode(languageCode);
|
||||
|
||||
// Add Flow Launcher language directory
|
||||
AddFlowLauncherLanguageDirectory();
|
||||
|
||||
// Add plugin language directories first so that we can load language files from plugins
|
||||
AddPluginLanguageDirectories();
|
||||
|
||||
// Load default language resources
|
||||
LoadDefaultLanguage();
|
||||
|
||||
// Change language
|
||||
await ChangeLanguageAsync(language);
|
||||
await ChangeLanguageAsync(language, false);
|
||||
}
|
||||
|
||||
private void AddFlowLauncherLanguageDirectory()
|
||||
{
|
||||
// Check if Flow Launcher language directory exists
|
||||
var directory = Path.Combine(Constant.ProgramDirectory, Folder);
|
||||
if (!Directory.Exists(directory))
|
||||
{
|
||||
PublicApi.Instance.LogError(ClassName, $"Flow Launcher language directory can't be found <{directory}>");
|
||||
return;
|
||||
}
|
||||
|
||||
_languageDirectories.Add(directory);
|
||||
}
|
||||
|
||||
private void AddPluginLanguageDirectories()
|
||||
{
|
||||
foreach (var pluginsDir in PluginManager.Directories)
|
||||
{
|
||||
if (!Directory.Exists(pluginsDir)) continue;
|
||||
|
||||
// Enumerate all top directories in the plugin directory
|
||||
foreach (var dir in Directory.GetDirectories(pluginsDir))
|
||||
{
|
||||
// Check if the directory contains a language folder
|
||||
var pluginLanguageDir = Path.Combine(dir, Folder);
|
||||
if (!Directory.Exists(pluginLanguageDir)) continue;
|
||||
|
||||
// Check if the language directory contains default language file since it will be checked later
|
||||
_languageDirectories.Add(pluginLanguageDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadDefaultLanguage()
|
||||
{
|
||||
// Removes language files loaded before any plugins were loaded.
|
||||
// Prevents the language Flow started in from overwriting English if the user switches back to English
|
||||
RemoveOldLanguageFiles();
|
||||
LoadLanguage(AvailableLanguages.English);
|
||||
_oldResources.Clear();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Change Language
|
||||
|
||||
/// <summary>
|
||||
/// Change language during runtime. Will change app language and plugin language & save settings.
|
||||
/// </summary>
|
||||
|
|
@ -151,11 +166,11 @@ namespace Flow.Launcher.Core.Resource
|
|||
|
||||
private static Language GetLanguageByLanguageCode(string languageCode)
|
||||
{
|
||||
var lowercase = languageCode.ToLower();
|
||||
var language = AvailableLanguages.GetAvailableLanguages().FirstOrDefault(o => o.LanguageCode.ToLower() == lowercase);
|
||||
var language = AvailableLanguages.GetAvailableLanguages().
|
||||
FirstOrDefault(o => o.LanguageCode.Equals(languageCode, StringComparison.OrdinalIgnoreCase));
|
||||
if (language == null)
|
||||
{
|
||||
API.LogError(ClassName, $"Language code can't be found <{languageCode}>");
|
||||
PublicApi.Instance.LogError(ClassName, $"Language code can't be found <{languageCode}>");
|
||||
return AvailableLanguages.English;
|
||||
}
|
||||
else
|
||||
|
|
@ -164,24 +179,62 @@ namespace Flow.Launcher.Core.Resource
|
|||
}
|
||||
}
|
||||
|
||||
private async Task ChangeLanguageAsync(Language language)
|
||||
private async Task ChangeLanguageAsync(Language language, bool updateMetadata = true)
|
||||
{
|
||||
// Remove old language files and load language
|
||||
RemoveOldLanguageFiles();
|
||||
if (language != AvailableLanguages.English)
|
||||
{
|
||||
LoadLanguage(language);
|
||||
}
|
||||
await _langChangeLock.WaitAsync();
|
||||
|
||||
try
|
||||
{
|
||||
// Remove old language files and load language
|
||||
RemoveOldLanguageFiles();
|
||||
if (language != AvailableLanguages.English)
|
||||
{
|
||||
LoadLanguage(language);
|
||||
}
|
||||
|
||||
// Change culture info
|
||||
ChangeCultureInfo(language.LanguageCode);
|
||||
|
||||
if (updateMetadata)
|
||||
{
|
||||
// Raise event for plugins after culture is set
|
||||
await Task.Run(UpdatePluginMetadataTranslations);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
PublicApi.Instance.LogException(ClassName, $"Failed to change language to <{language.LanguageCode}>", e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_langChangeLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public static void ChangeCultureInfo(string languageCode)
|
||||
{
|
||||
// Culture of main thread
|
||||
// Use CreateSpecificCulture to preserve possible user-override settings in Windows, if Flow's language culture is the same as Windows's
|
||||
CultureInfo.CurrentCulture = CultureInfo.CreateSpecificCulture(language.LanguageCode);
|
||||
CultureInfo.CurrentUICulture = CultureInfo.CurrentCulture;
|
||||
|
||||
// Raise event for plugins after culture is set
|
||||
await Task.Run(UpdatePluginMetadataTranslations);
|
||||
CultureInfo currentCulture;
|
||||
try
|
||||
{
|
||||
currentCulture = CultureInfo.CreateSpecificCulture(languageCode);
|
||||
}
|
||||
catch (CultureNotFoundException)
|
||||
{
|
||||
currentCulture = CultureInfo.CreateSpecificCulture(SystemLanguageCode);
|
||||
}
|
||||
CultureInfo.CurrentCulture = currentCulture;
|
||||
CultureInfo.CurrentUICulture = currentCulture;
|
||||
var thread = Thread.CurrentThread;
|
||||
thread.CurrentCulture = currentCulture;
|
||||
thread.CurrentUICulture = currentCulture;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Prompt Pinyin
|
||||
|
||||
public bool PromptShouldUsePinyin(string languageCodeToSet)
|
||||
{
|
||||
var languageToSet = GetLanguageByLanguageCode(languageCodeToSet);
|
||||
|
|
@ -194,14 +247,18 @@ namespace Flow.Launcher.Core.Resource
|
|||
|
||||
// No other languages should show the following text so just make it hard-coded
|
||||
// "Do you want to search with pinyin?"
|
||||
string text = languageToSet == AvailableLanguages.Chinese ? "是否启用拼音搜索?" : "是否啓用拼音搜索?" ;
|
||||
string text = languageToSet == AvailableLanguages.Chinese ? "是否启用拼音搜索?" : "是否啓用拼音搜索?";
|
||||
|
||||
if (Ioc.Default.GetRequiredService<IPublicAPI>().ShowMsgBox(text, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No)
|
||||
if (PublicApi.Instance.ShowMsgBox(text, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Language Resources Management
|
||||
|
||||
private void RemoveOldLanguageFiles()
|
||||
{
|
||||
var dicts = Application.Current.Resources.MergedDictionaries;
|
||||
|
|
@ -209,6 +266,7 @@ namespace Flow.Launcher.Core.Resource
|
|||
{
|
||||
dicts.Remove(r);
|
||||
}
|
||||
_oldResources.Clear();
|
||||
}
|
||||
|
||||
private void LoadLanguage(Language language)
|
||||
|
|
@ -237,45 +295,6 @@ namespace Flow.Launcher.Core.Resource
|
|||
}
|
||||
}
|
||||
|
||||
public List<Language> LoadAvailableLanguages()
|
||||
{
|
||||
var list = AvailableLanguages.GetAvailableLanguages();
|
||||
list.Insert(0, new Language(Constant.SystemLanguageCode, AvailableLanguages.GetSystemTranslation(SystemLanguageCode)));
|
||||
return list;
|
||||
}
|
||||
|
||||
public static string GetTranslation(string key)
|
||||
{
|
||||
var translation = Application.Current.TryFindResource(key);
|
||||
if (translation is string)
|
||||
{
|
||||
return translation.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
API.LogError(ClassName, $"No Translation for key {key}");
|
||||
return $"No Translation for key {key}";
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdatePluginMetadataTranslations()
|
||||
{
|
||||
foreach (var p in PluginManager.GetPluginsForInterface<IPluginI18n>())
|
||||
{
|
||||
if (p.Plugin is not IPluginI18n pluginI18N) return;
|
||||
try
|
||||
{
|
||||
p.Metadata.Name = pluginI18N.GetTranslatedPluginTitle();
|
||||
p.Metadata.Description = pluginI18N.GetTranslatedPluginDescription();
|
||||
pluginI18N.OnCultureInfoChanged(CultureInfo.CurrentCulture);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
API.LogException(ClassName, $"Failed for <{p.Metadata.Name}>", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string LanguageFile(string folder, string language)
|
||||
{
|
||||
if (Directory.Exists(folder))
|
||||
|
|
@ -287,7 +306,7 @@ namespace Flow.Launcher.Core.Resource
|
|||
}
|
||||
else
|
||||
{
|
||||
API.LogError(ClassName, $"Language path can't be found <{path}>");
|
||||
PublicApi.Instance.LogError(ClassName, $"Language path can't be found <{path}>");
|
||||
var english = Path.Combine(folder, DefaultFile);
|
||||
if (File.Exists(english))
|
||||
{
|
||||
|
|
@ -295,7 +314,7 @@ namespace Flow.Launcher.Core.Resource
|
|||
}
|
||||
else
|
||||
{
|
||||
API.LogError(ClassName, $"Default English Language path can't be found <{path}>");
|
||||
PublicApi.Instance.LogError(ClassName, $"Default English Language path can't be found <{path}>");
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
|
@ -305,5 +324,85 @@ namespace Flow.Launcher.Core.Resource
|
|||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Available Languages
|
||||
|
||||
public List<Language> LoadAvailableLanguages()
|
||||
{
|
||||
var list = AvailableLanguages.GetAvailableLanguages();
|
||||
list.Insert(0, new Language(Constant.SystemLanguageCode, AvailableLanguages.GetSystemTranslation(SystemLanguageCode)));
|
||||
return list;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Get Translations
|
||||
|
||||
public static string GetTranslation(string key)
|
||||
{
|
||||
var translation = Application.Current.TryFindResource(key);
|
||||
if (translation is string)
|
||||
{
|
||||
return translation.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
PublicApi.Instance.LogError(ClassName, $"No Translation for key {key}");
|
||||
return $"No Translation for key {key}";
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Update Metadata
|
||||
|
||||
public static void UpdatePluginMetadataTranslations()
|
||||
{
|
||||
// Update plugin metadata name & description
|
||||
foreach (var p in PluginManager.GetTranslationPlugins())
|
||||
{
|
||||
if (p.Plugin is not IPluginI18n pluginI18N) return;
|
||||
try
|
||||
{
|
||||
p.Metadata.Name = pluginI18N.GetTranslatedPluginTitle();
|
||||
p.Metadata.Description = pluginI18N.GetTranslatedPluginDescription();
|
||||
pluginI18N.OnCultureInfoChanged(CultureInfo.CurrentCulture);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
PublicApi.Instance.LogException(ClassName, $"Failed for <{p.Metadata.Name}>", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void UpdatePluginMetadataTranslation(PluginPair p)
|
||||
{
|
||||
// Update plugin metadata name & description
|
||||
if (p.Plugin is not IPluginI18n pluginI18N) return;
|
||||
try
|
||||
{
|
||||
p.Metadata.Name = pluginI18N.GetTranslatedPluginTitle();
|
||||
p.Metadata.Description = pluginI18N.GetTranslatedPluginDescription();
|
||||
pluginI18N.OnCultureInfoChanged(CultureInfo.CurrentCulture);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
PublicApi.Instance.LogException(ClassName, $"Failed for <{p.Metadata.Name}>", e);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
RemoveOldLanguageFiles();
|
||||
_langChangeLock.Dispose();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,38 +0,0 @@
|
|||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace Flow.Launcher.Core.Resource
|
||||
{
|
||||
[Obsolete("LocalizationConverter is obsolete. Use with Flow.Launcher.Localization NuGet package instead.")]
|
||||
public class LocalizationConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (targetType == typeof(string) && value != null)
|
||||
{
|
||||
FieldInfo fi = value.GetType().GetField(value.ToString());
|
||||
if (fi != null)
|
||||
{
|
||||
string localizedDescription = string.Empty;
|
||||
var attributes = (DescriptionAttribute[])fi.GetCustomAttributes(typeof(DescriptionAttribute), false);
|
||||
if ((attributes.Length > 0) && (!String.IsNullOrEmpty(attributes[0].Description)))
|
||||
{
|
||||
localizedDescription = attributes[0].Description;
|
||||
}
|
||||
|
||||
return (!String.IsNullOrEmpty(localizedDescription)) ? localizedDescription : value.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
using System.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Plugin;
|
||||
|
||||
namespace Flow.Launcher.Core.Resource
|
||||
{
|
||||
public class LocalizedDescriptionAttribute : DescriptionAttribute
|
||||
{
|
||||
// We should not initialize API in static constructor because it will create another API instance
|
||||
private static IPublicAPI api = null;
|
||||
private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService<IPublicAPI>();
|
||||
|
||||
private readonly string _resourceKey;
|
||||
|
||||
public LocalizedDescriptionAttribute(string resourceKey)
|
||||
{
|
||||
_resourceKey = resourceKey;
|
||||
}
|
||||
|
||||
public override string Description
|
||||
{
|
||||
get
|
||||
{
|
||||
string description = API.GetTranslation(_resourceKey);
|
||||
return string.IsNullOrWhiteSpace(description) ?
|
||||
string.Format("[[{0}]]", _resourceKey) : description;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -124,8 +124,8 @@ namespace Flow.Launcher.Core.Resource
|
|||
try
|
||||
{
|
||||
// Load a ResourceDictionary for the specified theme.
|
||||
var themeName = _settings.Theme;
|
||||
var dict = GetThemeResourceDictionary(themeName);
|
||||
var theme = _settings.Theme;
|
||||
var dict = GetThemeResourceDictionary(theme);
|
||||
|
||||
// Apply font settings to the theme resource.
|
||||
ApplyFontSettings(dict);
|
||||
|
|
@ -292,10 +292,10 @@ namespace Flow.Launcher.Core.Resource
|
|||
dict["ItemHotkeyStyle"] is Style resultHotkeyItemStyle &&
|
||||
dict["ItemHotkeySelectedStyle"] is Style resultHotkeyItemSelectedStyle)
|
||||
{
|
||||
Setter fontFamily = new Setter(TextBlock.FontFamilyProperty, new FontFamily(_settings.ResultFont));
|
||||
Setter fontStyle = new Setter(TextBlock.FontStyleProperty, FontHelper.GetFontStyleFromInvariantStringOrNormal(_settings.ResultFontStyle));
|
||||
Setter fontWeight = new Setter(TextBlock.FontWeightProperty, FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.ResultFontWeight));
|
||||
Setter fontStretch = new Setter(TextBlock.FontStretchProperty, FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.ResultFontStretch));
|
||||
var fontFamily = new Setter(TextBlock.FontFamilyProperty, new FontFamily(_settings.ResultFont));
|
||||
var fontStyle = new Setter(TextBlock.FontStyleProperty, FontHelper.GetFontStyleFromInvariantStringOrNormal(_settings.ResultFontStyle));
|
||||
var fontWeight = new Setter(TextBlock.FontWeightProperty, FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.ResultFontWeight));
|
||||
var fontStretch = new Setter(TextBlock.FontStretchProperty, FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.ResultFontStretch));
|
||||
|
||||
Setter[] setters = { fontFamily, fontStyle, fontWeight, fontStretch };
|
||||
Array.ForEach(
|
||||
|
|
@ -307,10 +307,10 @@ namespace Flow.Launcher.Core.Resource
|
|||
dict["ItemSubTitleStyle"] is Style resultSubItemStyle &&
|
||||
dict["ItemSubTitleSelectedStyle"] is Style resultSubItemSelectedStyle)
|
||||
{
|
||||
Setter fontFamily = new Setter(TextBlock.FontFamilyProperty, new FontFamily(_settings.ResultSubFont));
|
||||
Setter fontStyle = new Setter(TextBlock.FontStyleProperty, FontHelper.GetFontStyleFromInvariantStringOrNormal(_settings.ResultSubFontStyle));
|
||||
Setter fontWeight = new Setter(TextBlock.FontWeightProperty, FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.ResultSubFontWeight));
|
||||
Setter fontStretch = new Setter(TextBlock.FontStretchProperty, FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.ResultSubFontStretch));
|
||||
var fontFamily = new Setter(TextBlock.FontFamilyProperty, new FontFamily(_settings.ResultSubFont));
|
||||
var fontStyle = new Setter(TextBlock.FontStyleProperty, FontHelper.GetFontStyleFromInvariantStringOrNormal(_settings.ResultSubFontStyle));
|
||||
var fontWeight = new Setter(TextBlock.FontWeightProperty, FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.ResultSubFontWeight));
|
||||
var fontStretch = new Setter(TextBlock.FontStretchProperty, FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.ResultSubFontStretch));
|
||||
|
||||
Setter[] setters = { fontFamily, fontStyle, fontWeight, fontStretch };
|
||||
Array.ForEach(
|
||||
|
|
@ -395,7 +395,7 @@ namespace Flow.Launcher.Core.Resource
|
|||
|
||||
public List<ThemeData> GetAvailableThemes()
|
||||
{
|
||||
List<ThemeData> themes = new List<ThemeData>();
|
||||
var themes = new List<ThemeData>();
|
||||
foreach (var themeDirectory in _themeDirectories)
|
||||
{
|
||||
var filePaths = Directory
|
||||
|
|
@ -410,8 +410,7 @@ namespace Flow.Launcher.Core.Resource
|
|||
|
||||
public bool ChangeTheme(string theme = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(theme))
|
||||
theme = _settings.Theme;
|
||||
if (string.IsNullOrEmpty(theme)) theme = _settings.Theme;
|
||||
|
||||
string path = GetThemePath(theme);
|
||||
try
|
||||
|
|
@ -426,13 +425,14 @@ namespace Flow.Launcher.Core.Resource
|
|||
|
||||
_settings.Theme = theme;
|
||||
|
||||
//always allow re-loading default theme, in case of failure of switching to a new theme from default theme
|
||||
// Always allow re-loading default theme, in case of failure of switching to a new theme from default theme
|
||||
if (_oldTheme != theme || theme == Constant.DefaultTheme)
|
||||
{
|
||||
_oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath);
|
||||
}
|
||||
|
||||
BlurEnabled = IsBlurTheme();
|
||||
// Check if blur is enabled
|
||||
BlurEnabled = Win32Helper.IsBackdropSupported() && IsThemeBlurEnabled(resourceDict);
|
||||
|
||||
// Apply blur and drop shadow effect so that we do not need to call it again
|
||||
_ = RefreshFrameAsync();
|
||||
|
|
@ -444,17 +444,27 @@ namespace Flow.Launcher.Core.Resource
|
|||
_api.LogError(ClassName, $"Theme <{theme}> path can't be found");
|
||||
if (theme != Constant.DefaultTheme)
|
||||
{
|
||||
_api.ShowMsgBox(string.Format(_api.GetTranslation("theme_load_failure_path_not_exists"), theme));
|
||||
_api.ShowMsgBox(Localize.theme_load_failure_path_not_exists(theme));
|
||||
ChangeTheme(Constant.DefaultTheme);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
catch (XamlParseException)
|
||||
catch (XamlParseException e)
|
||||
{
|
||||
_api.LogError(ClassName, $"Theme <{theme}> fail to parse");
|
||||
_api.LogException(ClassName, $"Theme <{theme}> fail to parse xaml", e);
|
||||
if (theme != Constant.DefaultTheme)
|
||||
{
|
||||
_api.ShowMsgBox(string.Format(_api.GetTranslation("theme_load_failure_parse_error"), theme));
|
||||
_api.ShowMsgBox(Localize.theme_load_failure_parse_error(theme));
|
||||
ChangeTheme(Constant.DefaultTheme);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_api.LogException(ClassName, $"Theme <{theme}> fail to load", e);
|
||||
if (theme != Constant.DefaultTheme)
|
||||
{
|
||||
_api.ShowMsgBox(Localize.theme_load_failure_parse_error(theme));
|
||||
ChangeTheme(Constant.DefaultTheme);
|
||||
}
|
||||
return false;
|
||||
|
|
@ -657,21 +667,29 @@ namespace Flow.Launcher.Core.Resource
|
|||
if (mainWindow == null) return;
|
||||
|
||||
// Check if the theme supports blur
|
||||
bool hasBlur = dict.Contains("ThemeBlurEnabled") && dict["ThemeBlurEnabled"] is bool b && b;
|
||||
var hasBlur = IsThemeBlurEnabled(dict);
|
||||
if (BlurEnabled && hasBlur && Win32Helper.IsBackdropSupported())
|
||||
{
|
||||
// If the BackdropType is Mica or MicaAlt, set the windowborderstyle's background to transparent
|
||||
if (backdropType == BackdropTypes.Mica || backdropType == BackdropTypes.MicaAlt)
|
||||
if (backdropType is BackdropTypes.Mica or BackdropTypes.MicaAlt)
|
||||
{
|
||||
windowBorderStyle.Setters.Remove(windowBorderStyle.Setters.OfType<Setter>().FirstOrDefault(x => x.Property.Name == "Background"));
|
||||
windowBorderStyle.Setters.Add(new Setter(Border.BackgroundProperty, new SolidColorBrush(Color.FromArgb(1, 0, 0, 0))));
|
||||
windowBorderStyle.Setters.Remove(windowBorderStyle.Setters.OfType<Setter>().FirstOrDefault(x => x.Property == Control.BackgroundProperty));
|
||||
windowBorderStyle.Setters.Add(new Setter(Border.BackgroundProperty, ThemeHelper.GetFrozenSolidColorBrush(Color.FromArgb(1, 0, 0, 0))));
|
||||
}
|
||||
else if (backdropType == BackdropTypes.Acrylic)
|
||||
{
|
||||
windowBorderStyle.Setters.Remove(windowBorderStyle.Setters.OfType<Setter>().FirstOrDefault(x => x.Property.Name == "Background"));
|
||||
windowBorderStyle.Setters.Add(new Setter(Border.BackgroundProperty, new SolidColorBrush(Colors.Transparent)));
|
||||
windowBorderStyle.Setters.Remove(windowBorderStyle.Setters.OfType<Setter>().FirstOrDefault(x => x.Property == Control.BackgroundProperty));
|
||||
windowBorderStyle.Setters.Add(new Setter(Border.BackgroundProperty, Brushes.Transparent));
|
||||
}
|
||||
|
||||
// For themes with blur enabled, the window border is rendered by the system, so it's treated as a simple rectangle regardless of thickness.
|
||||
//(This is to avoid issues when the window is forcibly changed to a rectangular shape during snap scenarios.)
|
||||
var cornerRadiusSetter = windowBorderStyle.Setters.OfType<Setter>().FirstOrDefault(x => x.Property == Border.CornerRadiusProperty);
|
||||
if (cornerRadiusSetter != null)
|
||||
cornerRadiusSetter.Value = new CornerRadius(0);
|
||||
else
|
||||
windowBorderStyle.Setters.Add(new Setter(Border.CornerRadiusProperty, new CornerRadius(0)));
|
||||
|
||||
// Apply the blur effect
|
||||
Win32Helper.DWMSetBackdropForWindow(mainWindow, backdropType);
|
||||
ColorizeWindow(theme, backdropType);
|
||||
|
|
@ -780,12 +798,12 @@ namespace Flow.Launcher.Core.Resource
|
|||
Application.Current.Resources["WindowBorderStyle"] is Style originalStyle)
|
||||
{
|
||||
// Copy the original style, including the base style if it exists
|
||||
CopyStyle(originalStyle, previewStyle);
|
||||
ThemeHelper.CopyStyle(originalStyle, previewStyle);
|
||||
}
|
||||
|
||||
// Apply background color (remove transparency in color)
|
||||
Color backgroundColor = Color.FromRgb(bgColor.Value.R, bgColor.Value.G, bgColor.Value.B);
|
||||
previewStyle.Setters.Add(new Setter(Border.BackgroundProperty, new SolidColorBrush(backgroundColor)));
|
||||
var backgroundColor = Color.FromRgb(bgColor.Value.R, bgColor.Value.G, bgColor.Value.B);
|
||||
previewStyle.Setters.Add(new Setter(Border.BackgroundProperty, ThemeHelper.GetFrozenSolidColorBrush(backgroundColor)));
|
||||
|
||||
// The blur theme keeps the corner round fixed (applying DWM code to modify it causes rendering issues).
|
||||
// The non-blur theme retains the previously set WindowBorderStyle.
|
||||
|
|
@ -799,21 +817,6 @@ namespace Flow.Launcher.Core.Resource
|
|||
Application.Current.Resources["PreviewWindowBorderStyle"] = previewStyle;
|
||||
}
|
||||
|
||||
private void CopyStyle(Style originalStyle, Style targetStyle)
|
||||
{
|
||||
// If the style is based on another style, copy the base style first
|
||||
if (originalStyle.BasedOn != null)
|
||||
{
|
||||
CopyStyle(originalStyle.BasedOn, targetStyle);
|
||||
}
|
||||
|
||||
// Copy the setters from the original style
|
||||
foreach (var setter in originalStyle.Setters.OfType<Setter>())
|
||||
{
|
||||
targetStyle.Setters.Add(new Setter(setter.Property, setter.Value));
|
||||
}
|
||||
}
|
||||
|
||||
private void ColorizeWindow(string theme, BackdropTypes backdropType)
|
||||
{
|
||||
var dict = GetThemeResourceDictionary(theme);
|
||||
|
|
@ -823,20 +826,16 @@ namespace Flow.Launcher.Core.Resource
|
|||
if (mainWindow == null) return;
|
||||
|
||||
// Check if the theme supports blur
|
||||
bool hasBlur = dict.Contains("ThemeBlurEnabled") && dict["ThemeBlurEnabled"] is bool b && b;
|
||||
var hasBlur = IsThemeBlurEnabled(dict);
|
||||
|
||||
// SystemBG value check (Auto, Light, Dark)
|
||||
string systemBG = dict.Contains("SystemBG") ? dict["SystemBG"] as string : "Auto"; // 기본값 Auto
|
||||
var systemBG = dict.Contains("SystemBG") ? dict["SystemBG"] as string : "Auto"; // 기본값 Auto
|
||||
|
||||
// Check the user's ColorScheme setting
|
||||
string colorScheme = _settings.ColorScheme;
|
||||
|
||||
// Check system dark mode setting (read AppsUseLightTheme value)
|
||||
int themeValue = (int)Registry.GetValue(@"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize", "AppsUseLightTheme", 1);
|
||||
bool isSystemDark = themeValue == 0;
|
||||
var colorScheme = _settings.ColorScheme;
|
||||
|
||||
// Final decision on whether to use dark mode
|
||||
bool useDarkMode = false;
|
||||
var useDarkMode = false;
|
||||
|
||||
// If systemBG is not "Auto", prioritize it over ColorScheme and set the mode based on systemBG value
|
||||
if (systemBG == "Dark")
|
||||
|
|
@ -851,11 +850,20 @@ namespace Flow.Launcher.Core.Resource
|
|||
{
|
||||
// If systemBG is "Auto", decide based on ColorScheme
|
||||
if (colorScheme == "Dark")
|
||||
{
|
||||
useDarkMode = true;
|
||||
}
|
||||
else if (colorScheme == "Light")
|
||||
{
|
||||
useDarkMode = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Check system dark mode setting (read AppsUseLightTheme value)
|
||||
var themeValue = (int)Registry.GetValue(@"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize", "AppsUseLightTheme", 1);
|
||||
var isSystemDark = themeValue == 0;
|
||||
useDarkMode = isSystemDark; // Auto (based on system setting)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply DWM Dark Mode
|
||||
|
|
@ -897,25 +905,20 @@ namespace Flow.Launcher.Core.Resource
|
|||
else
|
||||
{
|
||||
// Only set the background to transparent if the theme supports blur
|
||||
if (backdropType == BackdropTypes.Mica || backdropType == BackdropTypes.MicaAlt)
|
||||
if (backdropType is BackdropTypes.Mica or BackdropTypes.MicaAlt)
|
||||
{
|
||||
mainWindow.Background = new SolidColorBrush(Color.FromArgb(1, 0, 0, 0));
|
||||
mainWindow.Background = ThemeHelper.GetFrozenSolidColorBrush(Color.FromArgb(1, 0, 0, 0));
|
||||
}
|
||||
else
|
||||
{
|
||||
mainWindow.Background = new SolidColorBrush(selectedBG);
|
||||
mainWindow.Background = ThemeHelper.GetFrozenSolidColorBrush(selectedBG);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsBlurTheme()
|
||||
private static bool IsThemeBlurEnabled(ResourceDictionary dict)
|
||||
{
|
||||
if (!Win32Helper.IsBackdropSupported()) // Windows 11 미만이면 무조건 false
|
||||
return false;
|
||||
|
||||
var resource = Application.Current.TryFindResource("ThemeBlurEnabled");
|
||||
|
||||
return resource is bool b && b;
|
||||
return dict.Contains("ThemeBlurEnabled") && dict["ThemeBlurEnabled"] is bool enabled && enabled;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
|
|
|||
30
Flow.Launcher.Core/Resource/ThemeHelper.cs
Normal file
30
Flow.Launcher.Core/Resource/ThemeHelper.cs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace Flow.Launcher.Core.Resource;
|
||||
|
||||
public static class ThemeHelper
|
||||
{
|
||||
public static void CopyStyle(Style originalStyle, Style targetStyle)
|
||||
{
|
||||
// If the style is based on another style, copy the base style first
|
||||
if (originalStyle.BasedOn != null)
|
||||
{
|
||||
CopyStyle(originalStyle.BasedOn, targetStyle);
|
||||
}
|
||||
|
||||
// Copy the setters from the original style
|
||||
foreach (var setter in originalStyle.Setters.OfType<Setter>())
|
||||
{
|
||||
targetStyle.Setters.Add(new Setter(setter.Property, setter.Value));
|
||||
}
|
||||
}
|
||||
|
||||
public static SolidColorBrush GetFrozenSolidColorBrush(Color color)
|
||||
{
|
||||
var brush = new SolidColorBrush(color);
|
||||
brush.Freeze();
|
||||
return brush;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Plugin;
|
||||
|
||||
namespace Flow.Launcher.Core.Resource
|
||||
{
|
||||
public class TranslationConverter : IValueConverter
|
||||
{
|
||||
// We should not initialize API in static constructor because it will create another API instance
|
||||
private static IPublicAPI api = null;
|
||||
private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService<IPublicAPI>();
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
var key = value.ToString();
|
||||
if (string.IsNullOrEmpty(key)) return key;
|
||||
return API.GetTranslation(key);
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) =>
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
}
|
||||
|
|
@ -41,29 +41,30 @@ namespace Flow.Launcher.Core
|
|||
try
|
||||
{
|
||||
if (!silentUpdate)
|
||||
_api.ShowMsg(_api.GetTranslation("pleaseWait"),
|
||||
_api.GetTranslation("update_flowlauncher_update_check"));
|
||||
_api.ShowMsg(Localize.pleaseWait(),
|
||||
Localize.update_flowlauncher_update_check());
|
||||
|
||||
using var updateManager = await GitHubUpdateManagerAsync(GitHubRepository).ConfigureAwait(false);
|
||||
|
||||
// UpdateApp CheckForUpdate will return value only if the app is squirrel installed
|
||||
var newUpdateInfo = await updateManager.CheckForUpdate().NonNull().ConfigureAwait(false);
|
||||
|
||||
var newReleaseVersion = Version.Parse(newUpdateInfo.FutureReleaseEntry.Version.ToString());
|
||||
var currentVersion = Version.Parse(Constant.Version);
|
||||
var newReleaseVersion =
|
||||
SemanticVersioning.Version.Parse(newUpdateInfo.FutureReleaseEntry.Version.ToString());
|
||||
var currentVersion = SemanticVersioning.Version.Parse(Constant.Version);
|
||||
|
||||
_api.LogInfo(ClassName, $"Future Release <{Formatted(newUpdateInfo.FutureReleaseEntry)}>");
|
||||
|
||||
if (newReleaseVersion <= currentVersion)
|
||||
{
|
||||
if (!silentUpdate)
|
||||
_api.ShowMsgBox(_api.GetTranslation("update_flowlauncher_already_on_latest"));
|
||||
_api.ShowMsgBox(Localize.update_flowlauncher_already_on_latest());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!silentUpdate)
|
||||
_api.ShowMsg(_api.GetTranslation("update_flowlauncher_update_found"),
|
||||
_api.GetTranslation("update_flowlauncher_updating"));
|
||||
_api.ShowMsg(Localize.update_flowlauncher_update_found(),
|
||||
Localize.update_flowlauncher_updating());
|
||||
|
||||
await updateManager.DownloadReleases(newUpdateInfo.ReleasesToApply).ConfigureAwait(false);
|
||||
|
||||
|
|
@ -71,12 +72,12 @@ namespace Flow.Launcher.Core
|
|||
|
||||
if (DataLocation.PortableDataLocationInUse())
|
||||
{
|
||||
var targetDestination = updateManager.RootAppDirectory + $"\\app-{newReleaseVersion}\\{DataLocation.PortableFolderName}";
|
||||
var targetDestination = updateManager.RootAppDirectory +
|
||||
$"\\app-{newReleaseVersion}\\{DataLocation.PortableFolderName}";
|
||||
FilesFolders.CopyAll(DataLocation.PortableDataPath, targetDestination, (s) => _api.ShowMsgBox(s));
|
||||
if (!FilesFolders.VerifyBothFolderFilesEqual(DataLocation.PortableDataPath, targetDestination, (s) => _api.ShowMsgBox(s)))
|
||||
_api.ShowMsgBox(string.Format(_api.GetTranslation("update_flowlauncher_fail_moving_portable_user_profile_data"),
|
||||
DataLocation.PortableDataPath,
|
||||
targetDestination));
|
||||
if (!FilesFolders.VerifyBothFolderFilesEqual(DataLocation.PortableDataPath, targetDestination,
|
||||
(s) => _api.ShowMsgBox(s)))
|
||||
_api.ShowMsgBox(Localize.update_flowlauncher_fail_moving_portable_user_profile_data(DataLocation.PortableDataPath, targetDestination));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -87,25 +88,28 @@ namespace Flow.Launcher.Core
|
|||
|
||||
_api.LogInfo(ClassName, $"Update success:{newVersionTips}");
|
||||
|
||||
if (_api.ShowMsgBox(newVersionTips, _api.GetTranslation("update_flowlauncher_new_update"), MessageBoxButton.YesNo) == MessageBoxResult.Yes)
|
||||
if (_api.ShowMsgBox(newVersionTips, Localize.update_flowlauncher_new_update(),
|
||||
MessageBoxButton.YesNo) == MessageBoxResult.Yes)
|
||||
{
|
||||
UpdateManager.RestartApp(Constant.ApplicationFileName);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (e is HttpRequestException or WebException or SocketException || e.InnerException is TimeoutException)
|
||||
if (e is HttpRequestException or WebException or SocketException ||
|
||||
e.InnerException is TimeoutException)
|
||||
{
|
||||
_api.LogException(ClassName, $"Check your connection and proxy settings to github-cloud.s3.amazonaws.com.", e);
|
||||
_api.LogException(ClassName,
|
||||
$"Check your connection and proxy settings to github-cloud.s3.amazonaws.com.", e);
|
||||
}
|
||||
else
|
||||
{
|
||||
_api.LogException(ClassName, $"Error Occurred", e);
|
||||
}
|
||||
|
||||
|
||||
if (!silentUpdate)
|
||||
_api.ShowMsg(_api.GetTranslation("update_flowlauncher_fail"),
|
||||
_api.GetTranslation("update_flowlauncher_check_connection"));
|
||||
_api.ShowMsgError(Localize.update_flowlauncher_fail(),
|
||||
Localize.update_flowlauncher_check_connection());
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -116,14 +120,11 @@ namespace Flow.Launcher.Core
|
|||
[UsedImplicitly]
|
||||
private class GithubRelease
|
||||
{
|
||||
[JsonPropertyName("prerelease")]
|
||||
public bool Prerelease { get; [UsedImplicitly] set; }
|
||||
[JsonPropertyName("prerelease")] public bool Prerelease { get; [UsedImplicitly] set; }
|
||||
|
||||
[JsonPropertyName("published_at")]
|
||||
public DateTime PublishedAt { get; [UsedImplicitly] set; }
|
||||
[JsonPropertyName("published_at")] public DateTime PublishedAt { get; [UsedImplicitly] set; }
|
||||
|
||||
[JsonPropertyName("html_url")]
|
||||
public string HtmlUrl { get; [UsedImplicitly] set; }
|
||||
[JsonPropertyName("html_url")] public string HtmlUrl { get; [UsedImplicitly] set; }
|
||||
}
|
||||
|
||||
// https://github.com/Squirrel/Squirrel.Windows/blob/master/src/Squirrel/UpdateManager.Factory.cs
|
||||
|
|
@ -138,10 +139,7 @@ namespace Flow.Launcher.Core
|
|||
var latest = releases.Where(r => !r.Prerelease).OrderByDescending(r => r.PublishedAt).First();
|
||||
var latestUrl = latest.HtmlUrl.Replace("/tag/", "/download/");
|
||||
|
||||
var client = new WebClient
|
||||
{
|
||||
Proxy = Http.WebProxy
|
||||
};
|
||||
var client = new WebClient { Proxy = Http.WebProxy };
|
||||
var downloader = new FileDownloader(client);
|
||||
|
||||
var manager = new UpdateManager(latestUrl, urlDownloader: downloader);
|
||||
|
|
@ -149,19 +147,16 @@ namespace Flow.Launcher.Core
|
|||
return manager;
|
||||
}
|
||||
|
||||
private string NewVersionTips(string version)
|
||||
private static string NewVersionTips(string version)
|
||||
{
|
||||
var tips = string.Format(_api.GetTranslation("newVersionTips"), version);
|
||||
var tips = Localize.newVersionTips(version);
|
||||
|
||||
return tips;
|
||||
}
|
||||
|
||||
private static string Formatted<T>(T t)
|
||||
{
|
||||
var formatted = JsonSerializer.Serialize(t, new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true
|
||||
});
|
||||
var formatted = JsonSerializer.Serialize(t, new JsonSerializerOptions { WriteIndented = true });
|
||||
|
||||
return formatted;
|
||||
}
|
||||
|
|
|
|||
1197
Flow.Launcher.Core/packages.lock.json
Normal file
1197
Flow.Launcher.Core/packages.lock.json
Normal file
File diff suppressed because it is too large
Load diff
1115
Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs
Normal file
1115
Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs
Normal file
File diff suppressed because it is too large
Load diff
63
Flow.Launcher.Infrastructure/DialogJump/DialogJumpPair.cs
Normal file
63
Flow.Launcher.Infrastructure/DialogJump/DialogJumpPair.cs
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
using Flow.Launcher.Plugin;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.DialogJump;
|
||||
|
||||
public class DialogJumpExplorerPair
|
||||
{
|
||||
public IDialogJumpExplorer Plugin { get; init; }
|
||||
|
||||
public PluginMetadata Metadata { get; init; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Metadata.Name;
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (obj is DialogJumpExplorerPair r)
|
||||
{
|
||||
return string.Equals(r.Metadata.ID, Metadata.ID);
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
var hashcode = Metadata.ID?.GetHashCode() ?? 0;
|
||||
return hashcode;
|
||||
}
|
||||
}
|
||||
|
||||
public class DialogJumpDialogPair
|
||||
{
|
||||
public IDialogJumpDialog Plugin { get; init; }
|
||||
|
||||
public PluginMetadata Metadata { get; init; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Metadata.Name;
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (obj is DialogJumpDialogPair r)
|
||||
{
|
||||
return string.Equals(r.Metadata.ID, Metadata.ID);
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
var hashcode = Metadata.ID?.GetHashCode() ?? 0;
|
||||
return hashcode;
|
||||
}
|
||||
}
|
||||
345
Flow.Launcher.Infrastructure/DialogJump/Models/WindowsDialog.cs
Normal file
345
Flow.Launcher.Infrastructure/DialogJump/Models/WindowsDialog.cs
Normal file
|
|
@ -0,0 +1,345 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
using WindowsInput;
|
||||
using WindowsInput.Native;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.DialogJump.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Class for handling Windows File Dialog instances in DialogJump.
|
||||
/// </summary>
|
||||
public class WindowsDialog : IDialogJumpDialog
|
||||
{
|
||||
private const string WindowsDialogClassName = "#32770";
|
||||
|
||||
public IDialogJumpDialogWindow CheckDialogWindow(IntPtr hwnd)
|
||||
{
|
||||
// Is it a Win32 dialog box?
|
||||
if (GetClassName(new(hwnd)) == WindowsDialogClassName)
|
||||
{
|
||||
// Is it a windows file dialog?
|
||||
var dialogType = GetFileDialogType(new(hwnd));
|
||||
if (dialogType != DialogType.Others)
|
||||
{
|
||||
return new WindowsDialogWindow(hwnd, dialogType);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
#region Help Methods
|
||||
|
||||
private static unsafe string GetClassName(HWND handle)
|
||||
{
|
||||
fixed (char* buf = new char[256])
|
||||
{
|
||||
return PInvoke.GetClassName(handle, buf, 256) switch
|
||||
{
|
||||
0 => string.Empty,
|
||||
_ => new string(buf),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static DialogType GetFileDialogType(HWND handle)
|
||||
{
|
||||
// Is it a Windows Open file dialog?
|
||||
var fileEditor = PInvoke.GetDlgItem(handle, 0x047C);
|
||||
if (fileEditor != HWND.Null && GetClassName(fileEditor) == "ComboBoxEx32") return DialogType.Open;
|
||||
|
||||
// Is it a Windows Save or Save As file dialog?
|
||||
fileEditor = PInvoke.GetDlgItem(handle, 0x0000);
|
||||
if (fileEditor != HWND.Null && GetClassName(fileEditor) == "DUIViewWndClassName") return DialogType.SaveOrSaveAs;
|
||||
|
||||
return DialogType.Others;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
public class WindowsDialogWindow : IDialogJumpDialogWindow
|
||||
{
|
||||
public IntPtr Handle { get; private set; } = IntPtr.Zero;
|
||||
|
||||
// After jumping folder, file editor handle of Save / SaveAs file dialogs cannot be found anymore
|
||||
// So we need to cache the current tab and use the original handle
|
||||
private IDialogJumpDialogWindowTab _currentTab { get; set; } = null;
|
||||
|
||||
private readonly DialogType _dialogType;
|
||||
|
||||
internal WindowsDialogWindow(IntPtr handle, DialogType dialogType)
|
||||
{
|
||||
Handle = handle;
|
||||
_dialogType = dialogType;
|
||||
}
|
||||
|
||||
public IDialogJumpDialogWindowTab GetCurrentTab()
|
||||
{
|
||||
return _currentTab ??= new WindowsDialogTab(Handle, _dialogType);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public class WindowsDialogTab : IDialogJumpDialogWindowTab
|
||||
{
|
||||
#region Public Properties
|
||||
|
||||
public IntPtr Handle { get; private set; } = IntPtr.Zero;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Fields
|
||||
|
||||
private static readonly string ClassName = nameof(WindowsDialogTab);
|
||||
|
||||
private static readonly InputSimulator _inputSimulator = new();
|
||||
|
||||
private readonly DialogType _dialogType;
|
||||
|
||||
private bool _legacy { get; set; } = false;
|
||||
private HWND _pathControl { get; set; } = HWND.Null;
|
||||
private HWND _pathEditor { get; set; } = HWND.Null;
|
||||
private HWND _fileEditor { get; set; } = HWND.Null;
|
||||
private HWND _openButton { get; set; } = HWND.Null;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
|
||||
internal WindowsDialogTab(IntPtr handle, DialogType dialogType)
|
||||
{
|
||||
Handle = handle;
|
||||
_dialogType = dialogType;
|
||||
Log.Debug(ClassName, $"File dialog type: {dialogType}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
|
||||
public string GetCurrentFolder()
|
||||
{
|
||||
if (_pathEditor.IsNull && !GetPathControlEditor()) return string.Empty;
|
||||
return GetWindowText(_pathEditor);
|
||||
}
|
||||
|
||||
public string GetCurrentFile()
|
||||
{
|
||||
if (_fileEditor.IsNull && !GetFileEditor()) return string.Empty;
|
||||
return GetWindowText(_fileEditor);
|
||||
}
|
||||
|
||||
public bool JumpFolder(string path, bool auto)
|
||||
{
|
||||
if (auto)
|
||||
{
|
||||
// Use legacy jump folder method for auto Dialog Jump because file editor is default value.
|
||||
// After setting path using file editor, we do not need to revert its value.
|
||||
return JumpFolderWithFileEditor(path, false);
|
||||
}
|
||||
|
||||
// Alt-D or Ctrl-L to focus on the path input box
|
||||
// "ComboBoxEx32" is not visible when the path editor is not with the keyboard focus
|
||||
_inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.LMENU, VirtualKeyCode.VK_D);
|
||||
// _inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.LCONTROL, VirtualKeyCode.VK_L);
|
||||
|
||||
if (_pathControl.IsNull && !GetPathControlEditor())
|
||||
{
|
||||
// https://github.com/idkidknow/Flow.Launcher.Plugin.DirQuickJump/issues/1
|
||||
// The dialog is a legacy one, so we can only edit file editor directly.
|
||||
Log.Debug(ClassName, "Legacy dialog, using legacy jump folder method");
|
||||
return JumpFolderWithFileEditor(path, true);
|
||||
}
|
||||
|
||||
var timeOut = !SpinWait.SpinUntil(() =>
|
||||
{
|
||||
var style = PInvoke.GetWindowLongPtr(_pathControl, WINDOW_LONG_PTR_INDEX.GWL_STYLE);
|
||||
return (style & (int)WINDOW_STYLE.WS_VISIBLE) != 0;
|
||||
}, 1000);
|
||||
if (timeOut)
|
||||
{
|
||||
// Path control is not visible, so we can only edit file editor directly.
|
||||
Log.Debug(ClassName, "Path control is not visible, using legacy jump folder method");
|
||||
return JumpFolderWithFileEditor(path, true);
|
||||
}
|
||||
|
||||
if (_pathEditor.IsNull)
|
||||
{
|
||||
// Path editor cannot be found, so we can only edit file editor directly.
|
||||
Log.Debug(ClassName, "Path editor cannot be found, using legacy jump folder method");
|
||||
return JumpFolderWithFileEditor(path, true);
|
||||
}
|
||||
SetWindowText(_pathEditor, path);
|
||||
|
||||
_inputSimulator.Keyboard.KeyPress(VirtualKeyCode.RETURN);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool JumpFile(string path)
|
||||
{
|
||||
if (_fileEditor.IsNull && !GetFileEditor()) return false;
|
||||
SetWindowText(_fileEditor, path);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Open()
|
||||
{
|
||||
if (_openButton.IsNull && !GetOpenButton()) return false;
|
||||
PInvoke.PostMessage(_openButton, PInvoke.BM_CLICK, 0, 0);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
#region Get Handles
|
||||
|
||||
private bool GetPathControlEditor()
|
||||
{
|
||||
// Get the handle of the path editor
|
||||
// Must use PInvoke.FindWindowEx because PInvoke.GetDlgItem(Handle, 0x0000) will get another control
|
||||
_pathControl = PInvoke.FindWindowEx(new(Handle), HWND.Null, "WorkerW", null); // 0x0000
|
||||
_pathControl = PInvoke.FindWindowEx(_pathControl, HWND.Null, "ReBarWindow32", null); // 0xA005
|
||||
_pathControl = PInvoke.FindWindowEx(_pathControl, HWND.Null, "Address Band Root", null); // 0xA205
|
||||
_pathControl = PInvoke.FindWindowEx(_pathControl, HWND.Null, "msctls_progress32", null); // 0x0000
|
||||
_pathControl = PInvoke.FindWindowEx(_pathControl, HWND.Null, "ComboBoxEx32", null); // 0xA205
|
||||
if (_pathControl == HWND.Null)
|
||||
{
|
||||
_pathEditor = HWND.Null;
|
||||
_legacy = true;
|
||||
Log.Info(ClassName, "Legacy dialog");
|
||||
}
|
||||
else
|
||||
{
|
||||
_pathEditor = PInvoke.GetDlgItem(_pathControl, 0xA205); // ComboBox
|
||||
_pathEditor = PInvoke.GetDlgItem(_pathEditor, 0xA205); // Edit
|
||||
if (_pathEditor == HWND.Null)
|
||||
{
|
||||
_legacy = true;
|
||||
Log.Error(ClassName, "Failed to find path editor handle");
|
||||
}
|
||||
}
|
||||
|
||||
return !_legacy;
|
||||
}
|
||||
|
||||
private bool GetFileEditor()
|
||||
{
|
||||
if (_dialogType == DialogType.Open)
|
||||
{
|
||||
// Get the handle of the file name editor of Open file dialog
|
||||
_fileEditor = PInvoke.GetDlgItem(new(Handle), 0x047C); // ComboBoxEx32
|
||||
_fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x047C); // ComboBox
|
||||
_fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x047C); // Edit
|
||||
}
|
||||
else
|
||||
{
|
||||
// Get the handle of the file name editor of Save / SaveAs file dialog
|
||||
_fileEditor = PInvoke.GetDlgItem(new(Handle), 0x0000); // DUIViewWndClassName
|
||||
_fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x0000); // DirectUIHWND
|
||||
_fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x0000); // FloatNotifySink
|
||||
_fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x0000); // ComboBox
|
||||
_fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x03E9); // Edit
|
||||
}
|
||||
|
||||
if (_fileEditor == HWND.Null)
|
||||
{
|
||||
Log.Error(ClassName, "Failed to find file name editor handle");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool GetOpenButton()
|
||||
{
|
||||
// Get the handle of the open button
|
||||
_openButton = PInvoke.GetDlgItem(new(Handle), 0x0001); // Open/Save/SaveAs Button
|
||||
if (_openButton == HWND.Null)
|
||||
{
|
||||
Log.Error(ClassName, "Failed to find open button handle");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Windows Text
|
||||
|
||||
private static unsafe string GetWindowText(HWND handle)
|
||||
{
|
||||
int length;
|
||||
Span<char> buffer = stackalloc char[1000];
|
||||
fixed (char* pBuffer = buffer)
|
||||
{
|
||||
// If the control has no title bar or text, or if the control handle is invalid, the return value is zero.
|
||||
length = (int)PInvoke.SendMessage(handle, PInvoke.WM_GETTEXT, 1000, (nint)pBuffer);
|
||||
}
|
||||
|
||||
return buffer[..length].ToString();
|
||||
}
|
||||
|
||||
private static unsafe nint SetWindowText(HWND handle, string text)
|
||||
{
|
||||
fixed (char* textPtr = text + '\0')
|
||||
{
|
||||
return PInvoke.SendMessage(handle, PInvoke.WM_SETTEXT, 0, (nint)textPtr).Value;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Legacy Jump Folder
|
||||
|
||||
private bool JumpFolderWithFileEditor(string path, bool resetFocus)
|
||||
{
|
||||
// For Save / Save As dialog, the default value in file editor is not null and it can cause strange behaviors.
|
||||
if (resetFocus && _dialogType == DialogType.SaveOrSaveAs) return false;
|
||||
|
||||
if (_fileEditor.IsNull && !GetFileEditor()) return false;
|
||||
SetWindowText(_fileEditor, path);
|
||||
|
||||
if (_openButton.IsNull && !GetOpenButton()) return false;
|
||||
PInvoke.SendMessage(_openButton, PInvoke.BM_CLICK, 0, 0);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
internal enum DialogType
|
||||
{
|
||||
Others,
|
||||
Open,
|
||||
SaveOrSaveAs
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,260 @@
|
|||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.System.Com;
|
||||
using Windows.Win32.UI.Shell;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.DialogJump.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Class for handling Windows Explorer instances in DialogJump.
|
||||
/// </summary>
|
||||
public class WindowsExplorer : IDialogJumpExplorer
|
||||
{
|
||||
public IDialogJumpExplorerWindow CheckExplorerWindow(IntPtr hwnd)
|
||||
{
|
||||
IDialogJumpExplorerWindow explorerWindow = null;
|
||||
|
||||
// Is it from Explorer?
|
||||
var processName = Win32Helper.GetProcessNameFromHwnd(new(hwnd));
|
||||
if (processName.Equals("explorer.exe", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
EnumerateShellWindows((shellWindow) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (shellWindow is not IWebBrowser2 explorer) return true;
|
||||
|
||||
if (explorer.HWND != hwnd) return true;
|
||||
|
||||
explorerWindow = new WindowsExplorerWindow(hwnd);
|
||||
return false;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignored
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
return explorerWindow;
|
||||
}
|
||||
|
||||
internal static unsafe void EnumerateShellWindows(Func<object, bool> action)
|
||||
{
|
||||
// Create an instance of ShellWindows
|
||||
var clsidShellWindows = new Guid("9BA05972-F6A8-11CF-A442-00A0C90A8F39"); // ShellWindowsClass
|
||||
var iidIShellWindows = typeof(IShellWindows).GUID; // IShellWindows
|
||||
|
||||
var result = PInvoke.CoCreateInstance(
|
||||
&clsidShellWindows,
|
||||
null,
|
||||
CLSCTX.CLSCTX_ALL,
|
||||
&iidIShellWindows,
|
||||
out var shellWindowsObj);
|
||||
|
||||
if (result.Failed) return;
|
||||
|
||||
var shellWindows = (IShellWindows)shellWindowsObj;
|
||||
|
||||
// Enumerate the shell windows
|
||||
var count = shellWindows.Count;
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
if (!action(shellWindows.Item(i)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public class WindowsExplorerWindow : IDialogJumpExplorerWindow
|
||||
{
|
||||
public IntPtr Handle { get; }
|
||||
|
||||
private static Guid _shellBrowserGuid = typeof(IShellBrowser).GUID;
|
||||
|
||||
internal WindowsExplorerWindow(IntPtr handle)
|
||||
{
|
||||
Handle = handle;
|
||||
}
|
||||
|
||||
public string GetExplorerPath()
|
||||
{
|
||||
if (Handle == IntPtr.Zero) return null;
|
||||
|
||||
var activeTabHandle = GetActiveTabHandle(new(Handle));
|
||||
if (activeTabHandle.IsNull) return null;
|
||||
|
||||
var window = GetExplorerByTabHandle(activeTabHandle);
|
||||
if (window == null) return null;
|
||||
|
||||
var path = GetLocation(window);
|
||||
return path;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
// Inspired by: https://github.com/w4po/ExplorerTabUtility
|
||||
|
||||
private static HWND GetActiveTabHandle(HWND windowHandle)
|
||||
{
|
||||
// Active tab always at the top of the z-index, so it is the first child of the ShellTabWindowClass.
|
||||
var activeTab = PInvoke.FindWindowEx(windowHandle, HWND.Null, "ShellTabWindowClass", null);
|
||||
return activeTab;
|
||||
}
|
||||
|
||||
private static IWebBrowser2 GetExplorerByTabHandle(HWND tabHandle)
|
||||
{
|
||||
if (tabHandle.IsNull) return null;
|
||||
|
||||
IWebBrowser2 window = null;
|
||||
WindowsExplorer.EnumerateShellWindows((shellWindow) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return StartSTAThread(() =>
|
||||
{
|
||||
if (shellWindow is not IWebBrowser2 explorer) return true;
|
||||
|
||||
if (explorer is not IServiceProvider sp) return true;
|
||||
|
||||
sp.QueryService(ref _shellBrowserGuid, ref _shellBrowserGuid, out var shellBrowser);
|
||||
if (shellBrowser == null) return true;
|
||||
|
||||
try
|
||||
{
|
||||
shellBrowser.GetWindow(out var hWnd); // Must execute in STA thread to get this hWnd
|
||||
|
||||
if (hWnd == tabHandle)
|
||||
{
|
||||
window = explorer;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignored
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.ReleaseComObject(shellBrowser);
|
||||
}
|
||||
|
||||
return true;
|
||||
}) ?? true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignored
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
return window;
|
||||
}
|
||||
|
||||
private static bool? StartSTAThread(Func<bool> action)
|
||||
{
|
||||
bool? result = null;
|
||||
var thread = new Thread(() =>
|
||||
{
|
||||
result = action();
|
||||
})
|
||||
{
|
||||
IsBackground = true
|
||||
};
|
||||
thread.SetApartmentState(ApartmentState.STA);
|
||||
thread.Start();
|
||||
thread.Join();
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string GetLocation(IWebBrowser2 window)
|
||||
{
|
||||
var path = window.LocationURL.ToString();
|
||||
if (!string.IsNullOrWhiteSpace(path)) return NormalizeLocation(path);
|
||||
|
||||
// Recycle Bin, This PC, etc
|
||||
if (window.Document is not IShellFolderViewDual folderView) return null;
|
||||
|
||||
// Attempt to get the path from the folder view
|
||||
try
|
||||
{
|
||||
// CSWin32 Folder does not have Self, so we need to use dynamic type here
|
||||
// Use dynamic to bypass static typing
|
||||
dynamic folder = folderView.Folder;
|
||||
|
||||
// Access the Self property via dynamic binding
|
||||
dynamic folderItem = folder.Self;
|
||||
|
||||
// Get path from the folder item
|
||||
path = folderItem.Path;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return NormalizeLocation(path);
|
||||
}
|
||||
|
||||
private static string NormalizeLocation(string location)
|
||||
{
|
||||
if (location.IndexOf('%') > -1)
|
||||
location = Environment.ExpandEnvironmentVariables(location);
|
||||
|
||||
if (location.StartsWith("::", StringComparison.Ordinal))
|
||||
location = $"shell:{location}";
|
||||
|
||||
else if (location.StartsWith("{", StringComparison.Ordinal))
|
||||
location = $"shell:::{location}";
|
||||
|
||||
location = location.Trim(' ', '/', '\\', '\n', '\'', '"');
|
||||
|
||||
return location.Replace('/', '\\');
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
#region COM Interfaces
|
||||
|
||||
// Inspired by: https://github.com/w4po/ExplorerTabUtility
|
||||
|
||||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
[Guid("6d5140c1-7436-11ce-8034-00aa006009fa")]
|
||||
[ComImport]
|
||||
public interface IServiceProvider
|
||||
{
|
||||
[PreserveSig]
|
||||
int QueryService(ref Guid guidService, ref Guid riid, [MarshalAs(UnmanagedType.Interface)] out IShellBrowser ppvObject);
|
||||
}
|
||||
|
||||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
[Guid("000214E2-0000-0000-C000-000000000046")]
|
||||
[ComImport]
|
||||
public interface IShellBrowser
|
||||
{
|
||||
[PreserveSig]
|
||||
int GetWindow(out nint handle);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
|
@ -1,8 +1,4 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Windows.Win32;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure
|
||||
{
|
||||
|
|
@ -13,9 +9,10 @@ namespace Flow.Launcher.Infrastructure
|
|||
/// </summary>
|
||||
public static string GetActiveExplorerPath()
|
||||
{
|
||||
var explorerWindow = GetActiveExplorer();
|
||||
string locationUrl = explorerWindow?.LocationURL;
|
||||
return !string.IsNullOrEmpty(locationUrl) ? GetDirectoryPath(new Uri(locationUrl).LocalPath) : null;
|
||||
var explorerPath = DialogJump.DialogJump.GetActiveExplorerPath();
|
||||
return !string.IsNullOrEmpty(explorerPath) ?
|
||||
GetDirectoryPath(new Uri(explorerPath).LocalPath) :
|
||||
null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -23,74 +20,12 @@ namespace Flow.Launcher.Infrastructure
|
|||
/// </summary>
|
||||
private static string GetDirectoryPath(string path)
|
||||
{
|
||||
if (!path.EndsWith("\\"))
|
||||
if (!path.EndsWith('\\'))
|
||||
{
|
||||
return path + "\\";
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the file explorer that is currently in the foreground
|
||||
/// </summary>
|
||||
private static dynamic GetActiveExplorer()
|
||||
{
|
||||
Type type = Type.GetTypeFromProgID("Shell.Application");
|
||||
if (type == null) return null;
|
||||
dynamic shell = Activator.CreateInstance(type);
|
||||
if (shell == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var explorerWindows = new List<dynamic>();
|
||||
var openWindows = shell.Windows();
|
||||
for (int i = 0; i < openWindows.Count; i++)
|
||||
{
|
||||
var window = openWindows.Item(i);
|
||||
if (window == null) continue;
|
||||
|
||||
// find the desired window and make sure that it is indeed a file explorer
|
||||
// we don't want the Internet Explorer or the classic control panel
|
||||
// ToLower() is needed, because Windows can report the path as "C:\\Windows\\Explorer.EXE"
|
||||
if (Path.GetFileName((string)window.FullName)?.ToLower() == "explorer.exe")
|
||||
{
|
||||
explorerWindows.Add(window);
|
||||
}
|
||||
}
|
||||
|
||||
if (explorerWindows.Count == 0) return null;
|
||||
|
||||
var zOrders = GetZOrder(explorerWindows);
|
||||
|
||||
return explorerWindows.Zip(zOrders).MinBy(x => x.Second).First;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the z-order for one or more windows atomically with respect to each other. In Windows, smaller z-order is higher. If the window is not top level, the z order is returned as -1.
|
||||
/// </summary>
|
||||
private static IEnumerable<int> GetZOrder(List<dynamic> hWnds)
|
||||
{
|
||||
var z = new int[hWnds.Count];
|
||||
for (var i = 0; i < hWnds.Count; i++) z[i] = -1;
|
||||
|
||||
var index = 0;
|
||||
var numRemaining = hWnds.Count;
|
||||
PInvoke.EnumWindows((wnd, _) =>
|
||||
{
|
||||
var searchIndex = hWnds.FindIndex(x => new IntPtr(x.HWND) == wnd);
|
||||
if (searchIndex != -1)
|
||||
{
|
||||
z[searchIndex] = index;
|
||||
numRemaining--;
|
||||
if (numRemaining == 0) return false;
|
||||
}
|
||||
index++;
|
||||
return true;
|
||||
}, IntPtr.Zero);
|
||||
|
||||
return z;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0-windows</TargetFramework>
|
||||
<TargetFramework>net9.0-windows</TargetFramework>
|
||||
<ProjectGuid>{4FD29318-A8AB-4D8F-AA47-60BC241B8DA3}</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<UseWpf>true</UseWpf>
|
||||
|
|
@ -12,6 +12,7 @@
|
|||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
|
|
@ -33,6 +34,7 @@
|
|||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<Prefer32Bit>false</Prefer32Bit>
|
||||
<NoWarn>$(NoWarn);FLSG0007</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
@ -53,28 +55,43 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Ben.Demystifier" Version="0.4.1" />
|
||||
<PackageReference Include="BitFaster.Caching" Version="2.5.3" />
|
||||
<PackageReference Include="BitFaster.Caching" Version="2.5.4" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
||||
<PackageReference Include="Fody" Version="6.5.5">
|
||||
<PackageReference Include="Flow.Launcher.Localization" Version="0.0.6" />
|
||||
<PackageReference Include="Fody" Version="6.9.3">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="ini-parser" Version="2.5.2" />
|
||||
<PackageReference Include="InputSimulator" Version="1.0.4" />
|
||||
<PackageReference Include="MemoryPack" Version="1.21.4" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Threading" Version="17.12.19" />
|
||||
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.106">
|
||||
<PackageReference Include="Microsoft.VisualStudio.Threading" Version="17.14.15" />
|
||||
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.205">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="NLog" Version="4.7.10" />
|
||||
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0">
|
||||
<PackageReference Include="NHotkey.Wpf" Version="3.0.0" />
|
||||
<PackageReference Include="NLog" Version="6.0.4" />
|
||||
<PackageReference Include="NLog.OutputDebugString" Version="6.0.4" />
|
||||
<PackageReference Include="PropertyChanged.Fody" Version="4.1.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="SharpVectors.Wpf" Version="1.8.4.2" />
|
||||
<PackageReference Include="SharpVectors.Wpf" Version="1.8.5" />
|
||||
<!-- Do not upgrade this to higher version since it can cause this issue on WinForm platform: -->
|
||||
<!-- PlatformNotSupportedException: SystemEvents is not supported on this platform. -->
|
||||
<PackageReference Include="System.Drawing.Common" Version="7.0.0" />
|
||||
<!--ToolGood.Words.Pinyin v3.0.2.6 results in high memory usage when search with pinyin is enabled-->
|
||||
<!--Bumping to it or higher needs to test and ensure this is no longer a problem-->
|
||||
<PackageReference Include="ToolGood.Words.Pinyin" Version="3.0.1.4" />
|
||||
<PackageReference Include="ToolGood.Words.Pinyin" Version="3.1.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<FLLUseDependencyInjection>true</FLLUseDependencyInjection>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Remove="Languages\en.xaml" />
|
||||
<AdditionalFiles Include="..\Flow.Launcher\Languages\en.xaml">
|
||||
<Link>Languages\en.xaml</Link>
|
||||
</AdditionalFiles>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
</Project>
|
||||
|
|
@ -4,10 +4,8 @@ using System.Net;
|
|||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.Http
|
||||
|
|
@ -78,7 +76,7 @@ namespace Flow.Launcher.Infrastructure.Http
|
|||
}
|
||||
catch (UriFormatException e)
|
||||
{
|
||||
Ioc.Default.GetRequiredService<IPublicAPI>().ShowMsg("Please try again", "Unable to parse Http Proxy");
|
||||
PublicApi.Instance.ShowMsgError(Localize.pleaseTryAgain(), Localize.parseProxyFailed());
|
||||
Log.Exception(ClassName, "Unable to parse Uri", e);
|
||||
}
|
||||
}
|
||||
|
|
@ -220,5 +218,18 @@ namespace Flow.Launcher.Infrastructure.Http
|
|||
return new HttpResponseMessage(HttpStatusCode.InternalServerError);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<string> GetStringAsync(string url, CancellationToken token = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
Log.Debug(ClassName, $"Url <{url}>");
|
||||
return await client.GetStringAsync(url, token);
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
22
Flow.Launcher.Infrastructure/IAlphabet.cs
Normal file
22
Flow.Launcher.Infrastructure/IAlphabet.cs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
namespace Flow.Launcher.Infrastructure
|
||||
{
|
||||
/// <summary>
|
||||
/// Translate a language to English letters using a given rule.
|
||||
/// </summary>
|
||||
public interface IAlphabet
|
||||
{
|
||||
/// <summary>
|
||||
/// Translate a string to English letters, using a given rule.
|
||||
/// </summary>
|
||||
/// <param name="stringToTranslate">String to translate.</param>
|
||||
/// <returns></returns>
|
||||
public (string translation, TranslationMapping map) Translate(string stringToTranslate);
|
||||
|
||||
/// <summary>
|
||||
/// Determine if a string should be translated to English letter with this Alphabet.
|
||||
/// </summary>
|
||||
/// <param name="stringToTranslate">String to translate.</param>
|
||||
/// <returns></returns>
|
||||
public bool ShouldTranslate(string stringToTranslate);
|
||||
}
|
||||
}
|
||||
|
|
@ -19,37 +19,42 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
private static readonly string ClassName = nameof(ImageLoader);
|
||||
|
||||
private static readonly ImageCache ImageCache = new();
|
||||
private static SemaphoreSlim storageLock { get; } = new SemaphoreSlim(1, 1);
|
||||
private static Lock storageLock { get; } = new();
|
||||
private static BinaryStorage<List<(string, bool)>> _storage;
|
||||
private static readonly ConcurrentDictionary<string, string> GuidToKey = new();
|
||||
private static IImageHashGenerator _hashGenerator;
|
||||
private static ImageHashGenerator _hashGenerator;
|
||||
private static readonly bool EnableImageHash = true;
|
||||
public static ImageSource Image { get; } = new BitmapImage(new Uri(Constant.ImageIcon));
|
||||
public static ImageSource MissingImage { get; } = new BitmapImage(new Uri(Constant.MissingImgIcon));
|
||||
public static ImageSource LoadingImage { get; } = new BitmapImage(new Uri(Constant.LoadingImgIcon));
|
||||
public static ImageSource Image => ImageCache[Constant.ImageIcon, false];
|
||||
public static ImageSource MissingImage => ImageCache[Constant.MissingImgIcon, false];
|
||||
public static ImageSource LoadingImage => ImageCache[Constant.LoadingImgIcon, false];
|
||||
public const int SmallIconSize = 64;
|
||||
public const int FullIconSize = 256;
|
||||
public const int FullImageSize = 320;
|
||||
|
||||
private static readonly string[] ImageExtensions = { ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".ico" };
|
||||
private static readonly string[] ImageExtensions = [".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".ico"];
|
||||
private static readonly string SvgExtension = ".svg";
|
||||
|
||||
public static async Task InitializeAsync()
|
||||
{
|
||||
_storage = new BinaryStorage<List<(string, bool)>>("Image");
|
||||
_hashGenerator = new ImageHashGenerator();
|
||||
|
||||
var usage = await LoadStorageToConcurrentDictionaryAsync();
|
||||
_storage.ClearData();
|
||||
|
||||
ImageCache.Initialize(usage);
|
||||
|
||||
foreach (var icon in new[] { Constant.DefaultIcon, Constant.MissingImgIcon })
|
||||
var usage = await Task.Run(() =>
|
||||
{
|
||||
ImageSource img = new BitmapImage(new Uri(icon));
|
||||
img.Freeze();
|
||||
ImageCache[icon, false] = img;
|
||||
}
|
||||
_storage = new BinaryStorage<List<(string, bool)>>("Image");
|
||||
_hashGenerator = new ImageHashGenerator();
|
||||
|
||||
var usage = LoadStorageToConcurrentDictionary();
|
||||
_storage.ClearData();
|
||||
|
||||
ImageCache.Initialize(usage);
|
||||
|
||||
foreach (var icon in new[] { Constant.DefaultIcon, Constant.ImageIcon, Constant.MissingImgIcon, Constant.LoadingImgIcon })
|
||||
{
|
||||
ImageSource img = new BitmapImage(new Uri(icon));
|
||||
img.Freeze();
|
||||
ImageCache[icon, false] = img;
|
||||
}
|
||||
|
||||
return usage;
|
||||
});
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
|
|
@ -64,42 +69,26 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
});
|
||||
}
|
||||
|
||||
public static async Task SaveAsync()
|
||||
public static void Save()
|
||||
{
|
||||
await storageLock.WaitAsync();
|
||||
|
||||
try
|
||||
lock (storageLock)
|
||||
{
|
||||
await _storage.SaveAsync(ImageCache.EnumerateEntries()
|
||||
.Select(x => x.Key)
|
||||
.ToList());
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Log.Exception(ClassName, "Failed to save image cache to file", e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
storageLock.Release();
|
||||
try
|
||||
{
|
||||
_storage.Save([.. ImageCache.EnumerateEntries().Select(x => x.Key)]);
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Log.Exception(ClassName, "Failed to save image cache to file", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task WaitSaveAsync()
|
||||
private static List<(string, bool)> LoadStorageToConcurrentDictionary()
|
||||
{
|
||||
await storageLock.WaitAsync();
|
||||
storageLock.Release();
|
||||
}
|
||||
|
||||
private static async Task<List<(string, bool)>> LoadStorageToConcurrentDictionaryAsync()
|
||||
{
|
||||
await storageLock.WaitAsync();
|
||||
try
|
||||
lock (storageLock)
|
||||
{
|
||||
return await _storage.TryLoadAsync(new List<(string, bool)>());
|
||||
}
|
||||
finally
|
||||
{
|
||||
storageLock.Release();
|
||||
return _storage.TryLoad([]);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -174,7 +163,7 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
Log.Exception(ClassName, $"Failed to get thumbnail for {path} on first try", e);
|
||||
Log.Exception(ClassName, $"Failed to get thumbnail for {path} on second try", e2);
|
||||
|
||||
ImageSource image = ImageCache[Constant.MissingImgIcon, false];
|
||||
ImageSource image = MissingImage;
|
||||
ImageCache[path, false] = image;
|
||||
imageResult = new ImageResult(image, ImageType.Error);
|
||||
}
|
||||
|
|
@ -273,7 +262,7 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
}
|
||||
else
|
||||
{
|
||||
image = ImageCache[Constant.MissingImgIcon, false];
|
||||
image = MissingImage;
|
||||
path = Constant.MissingImgIcon;
|
||||
}
|
||||
|
||||
|
|
@ -338,7 +327,7 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
return img;
|
||||
}
|
||||
|
||||
private static ImageSource LoadFullImage(string path)
|
||||
private static BitmapImage LoadFullImage(string path)
|
||||
{
|
||||
BitmapImage image = new BitmapImage();
|
||||
image.BeginInit();
|
||||
|
|
@ -375,7 +364,7 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
return image;
|
||||
}
|
||||
|
||||
private static ImageSource LoadSvgImage(string path, bool loadFullImage = false)
|
||||
private static RenderTargetBitmap LoadSvgImage(string path, bool loadFullImage = false)
|
||||
{
|
||||
// Set up drawing settings
|
||||
var desiredHeight = loadFullImage ? FullImageSize : SmallIconSize;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Media.Imaging;
|
||||
using IniParser;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.UI.Shell;
|
||||
using Windows.Win32.Graphics.Gdi;
|
||||
using Windows.Win32.UI.Shell;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.Image
|
||||
{
|
||||
|
|
@ -35,9 +36,32 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
|
||||
private static readonly HRESULT S_PATHNOTFOUND = (HRESULT)0x8004B205;
|
||||
|
||||
private const string UrlExtension = ".url";
|
||||
|
||||
/// <summary>
|
||||
/// Obtains a BitmapSource thumbnail for the specified file.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If the file is a Windows URL shortcut (".url"), the method attempts to resolve the shortcut's icon and use that for the thumbnail; otherwise it requests a thumbnail for the file path. The native HBITMAP used to create the BitmapSource is always released to avoid native memory leaks.
|
||||
/// </remarks>
|
||||
/// <param name="fileName">Path to the file (can be a regular file or a ".url" shortcut).</param>
|
||||
/// <param name="width">Requested thumbnail width in pixels.</param>
|
||||
/// <param name="height">Requested thumbnail height in pixels.</param>
|
||||
/// <param name="options">Thumbnail extraction options (flags) controlling fallback and caching behavior.</param>
|
||||
/// <returns>A BitmapSource representing the requested thumbnail.</returns>
|
||||
public static BitmapSource GetThumbnail(string fileName, int width, int height, ThumbnailOptions options)
|
||||
{
|
||||
HBITMAP hBitmap = GetHBitmap(Path.GetFullPath(fileName), width, height, options);
|
||||
HBITMAP hBitmap;
|
||||
|
||||
var extension = Path.GetExtension(fileName);
|
||||
if (string.Equals(extension, UrlExtension, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
hBitmap = GetHBitmapForUrlFile(fileName, width, height, options);
|
||||
}
|
||||
else
|
||||
{
|
||||
hBitmap = GetHBitmap(Path.GetFullPath(fileName), width, height, options);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -50,6 +74,21 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Obtains a native HBITMAP for the specified file at the requested size using the Windows Shell image factory.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If <paramref name="options"/> is <see cref="ThumbnailOptions.ThumbnailOnly"/> and thumbnail extraction fails
|
||||
/// due to extraction errors or a missing path, the method falls back to requesting an icon (<see cref="ThumbnailOptions.IconOnly"/>).
|
||||
/// The returned HBITMAP is a raw GDI handle; the caller is responsible for releasing it (e.g., via DeleteObject) to avoid native memory leaks.
|
||||
/// </remarks>
|
||||
/// <param name="fileName">Path to the file to thumbnail.</param>
|
||||
/// <param name="width">Requested thumbnail width in pixels.</param>
|
||||
/// <param name="height">Requested thumbnail height in pixels.</param>
|
||||
/// <param name="options">Thumbnail request flags that control behavior (e.g., ThumbnailOnly, IconOnly).</param>
|
||||
/// <returns>An HBITMAP handle containing the image. Caller must free the handle when finished.</returns>
|
||||
/// <exception cref="COMException">If creating the shell item fails (HRESULT returned by SHCreateItemFromParsingName).</exception>
|
||||
/// <exception cref="InvalidOperationException">If the shell item does not expose IShellItemImageFactory or if an unexpected error occurs while obtaining the image.</exception>
|
||||
private static unsafe HBITMAP GetHBitmap(string fileName, int width, int height, ThumbnailOptions options)
|
||||
{
|
||||
var retCode = PInvoke.SHCreateItemFromParsingName(
|
||||
|
|
@ -108,5 +147,44 @@ namespace Flow.Launcher.Infrastructure.Image
|
|||
|
||||
return hBitmap;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Obtains an HBITMAP for a Windows .url shortcut by resolving its IconFile entry and delegating to GetHBitmap.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The method parses the .url file as an INI, looks in the "InternetShortcut" section for the "IconFile" entry,
|
||||
/// and requests a bitmap for that icon path. If no IconFile is present or any error occurs while reading or
|
||||
/// resolving the icon, it falls back to requesting a thumbnail for the .url file itself.
|
||||
/// </remarks>
|
||||
/// <param name="fileName">Path to the .url shortcut file.</param>
|
||||
/// <param name="width">Requested thumbnail width (pixels).</param>
|
||||
/// <param name="height">Requested thumbnail height (pixels).</param>
|
||||
/// <param name="options">ThumbnailOptions flags controlling extraction behavior.</param>
|
||||
/// <returns>An HBITMAP containing the requested image; callers are responsible for freeing the native handle.</returns>
|
||||
private static unsafe HBITMAP GetHBitmapForUrlFile(string fileName, int width, int height, ThumbnailOptions options)
|
||||
{
|
||||
HBITMAP hBitmap;
|
||||
|
||||
try
|
||||
{
|
||||
var parser = new FileIniDataParser();
|
||||
var data = parser.ReadFile(fileName);
|
||||
var urlSection = data["InternetShortcut"];
|
||||
|
||||
var iconPath = urlSection?["IconFile"];
|
||||
if (!File.Exists(iconPath))
|
||||
{
|
||||
// If the IconFile is missing, throw exception to fallback to the default icon
|
||||
throw new FileNotFoundException("Icon file not specified in Internet shortcut (.url) file.");
|
||||
}
|
||||
hBitmap = GetHBitmap(Path.GetFullPath(iconPath), width, height, options);
|
||||
}
|
||||
catch
|
||||
{
|
||||
hBitmap = GetHBitmap(Path.GetFullPath(fileName), width, height, options);
|
||||
}
|
||||
|
||||
return hBitmap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ namespace Flow.Launcher.Infrastructure.Logger
|
|||
|
||||
var fileTarget = new FileTarget
|
||||
{
|
||||
FileName = CurrentLogDirectory.Replace(@"\", "/") + "/${shortdate}.txt",
|
||||
FileName = CurrentLogDirectory.Replace(@"\", "/") + "/Flow.Launcher.${date:format=yyyy-MM-dd}.log",
|
||||
Layout = layout
|
||||
};
|
||||
|
||||
|
|
@ -65,26 +65,22 @@ namespace Flow.Launcher.Infrastructure.Logger
|
|||
|
||||
public static void SetLogLevel(LOGLEVEL level)
|
||||
{
|
||||
switch (level)
|
||||
var rule = LogManager.Configuration.FindRuleByName("file");
|
||||
|
||||
var nlogLevel = level switch
|
||||
{
|
||||
case LOGLEVEL.DEBUG:
|
||||
UseDebugLogLevel();
|
||||
break;
|
||||
default:
|
||||
UseInfoLogLevel();
|
||||
break;
|
||||
}
|
||||
Info(nameof(Logger), $"Using log level: {level}.");
|
||||
}
|
||||
LOGLEVEL.NONE => LogLevel.Off,
|
||||
LOGLEVEL.ERROR => LogLevel.Error,
|
||||
LOGLEVEL.DEBUG => LogLevel.Debug,
|
||||
_ => LogLevel.Info
|
||||
};
|
||||
|
||||
private static void UseDebugLogLevel()
|
||||
{
|
||||
LogManager.Configuration.FindRuleByName("file").SetLoggingLevels(LogLevel.Debug, LogLevel.Fatal);
|
||||
}
|
||||
rule.SetLoggingLevels(nlogLevel, LogLevel.Fatal);
|
||||
|
||||
private static void UseInfoLogLevel()
|
||||
{
|
||||
LogManager.Configuration.FindRuleByName("file").SetLoggingLevels(LogLevel.Info, LogLevel.Fatal);
|
||||
LogManager.ReconfigExistingLoggers();
|
||||
|
||||
// We can't log Info when level is set to Error or None, so we use Debug
|
||||
Debug(nameof(Logger), $"Using log level: {level}.");
|
||||
}
|
||||
|
||||
private static void LogFaultyFormat(string message)
|
||||
|
|
@ -169,7 +165,9 @@ namespace Flow.Launcher.Infrastructure.Logger
|
|||
|
||||
public enum LOGLEVEL
|
||||
{
|
||||
DEBUG,
|
||||
INFO
|
||||
NONE,
|
||||
ERROR,
|
||||
INFO,
|
||||
DEBUG
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,14 +34,13 @@ WINDOW_STYLE
|
|||
SetLastError
|
||||
WINDOW_EX_STYLE
|
||||
|
||||
GetSystemMetrics
|
||||
EnumDisplayMonitors
|
||||
MonitorFromWindow
|
||||
GetMonitorInfo
|
||||
MONITORINFOEXW
|
||||
|
||||
WM_ENTERSIZEMOVE
|
||||
WM_EXITSIZEMOVE
|
||||
WM_NCLBUTTONDBLCLK
|
||||
WM_SYSCOMMAND
|
||||
|
||||
SC_MAXIMIZE
|
||||
SC_MINIMIZE
|
||||
|
||||
OleInitialize
|
||||
OleUninitialize
|
||||
|
|
@ -61,3 +60,37 @@ LOCALE_TRANSIENT_KEYBOARD4
|
|||
SHParseDisplayName
|
||||
SHOpenFolderAndSelectItems
|
||||
CoTaskMemFree
|
||||
|
||||
SetWinEventHook
|
||||
UnhookWinEvent
|
||||
SendMessage
|
||||
EVENT_SYSTEM_FOREGROUND
|
||||
WINEVENT_OUTOFCONTEXT
|
||||
WM_SETTEXT
|
||||
IShellFolderViewDual2
|
||||
CoCreateInstance
|
||||
CLSCTX
|
||||
IShellWindows
|
||||
IWebBrowser2
|
||||
EVENT_OBJECT_DESTROY
|
||||
EVENT_OBJECT_LOCATIONCHANGE
|
||||
EVENT_SYSTEM_MOVESIZESTART
|
||||
EVENT_SYSTEM_MOVESIZEEND
|
||||
GetDlgItem
|
||||
PostMessage
|
||||
BM_CLICK
|
||||
WM_GETTEXT
|
||||
OpenProcess
|
||||
QueryFullProcessImageName
|
||||
EVENT_OBJECT_HIDE
|
||||
EVENT_SYSTEM_DIALOGEND
|
||||
|
||||
DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS
|
||||
WM_POWERBROADCAST
|
||||
PBT_APMRESUMEAUTOMATIC
|
||||
PBT_APMRESUMESUSPEND
|
||||
PowerRegisterSuspendResumeNotification
|
||||
PowerUnregisterSuspendResumeNotification
|
||||
DeviceNotifyCallbackRoutine
|
||||
|
||||
MonitorFromWindow
|
||||
|
|
@ -1,209 +1,196 @@
|
|||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using JetBrains.Annotations;
|
||||
using System.Text.Json;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using ToolGood.Words.Pinyin;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure
|
||||
{
|
||||
public class TranslationMapping
|
||||
{
|
||||
private bool constructed;
|
||||
|
||||
private List<int> originalIndexs = new List<int>();
|
||||
private List<int> translatedIndexs = new List<int>();
|
||||
private int translatedLength = 0;
|
||||
|
||||
public string key { get; private set; }
|
||||
|
||||
public void setKey(string key)
|
||||
{
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public void AddNewIndex(int originalIndex, int translatedIndex, int length)
|
||||
{
|
||||
if (constructed)
|
||||
throw new InvalidOperationException("Mapping shouldn't be changed after constructed");
|
||||
|
||||
originalIndexs.Add(originalIndex);
|
||||
translatedIndexs.Add(translatedIndex);
|
||||
translatedIndexs.Add(translatedIndex + length);
|
||||
translatedLength += length - 1;
|
||||
}
|
||||
|
||||
public int MapToOriginalIndex(int translatedIndex)
|
||||
{
|
||||
if (translatedIndex > translatedIndexs.Last())
|
||||
return translatedIndex - translatedLength - 1;
|
||||
|
||||
int lowerBound = 0;
|
||||
int upperBound = originalIndexs.Count - 1;
|
||||
|
||||
int count = 0;
|
||||
|
||||
// Corner case handle
|
||||
if (translatedIndex < translatedIndexs[0])
|
||||
return translatedIndex;
|
||||
if (translatedIndex > translatedIndexs.Last())
|
||||
{
|
||||
int indexDef = 0;
|
||||
for (int k = 0; k < originalIndexs.Count; k++)
|
||||
{
|
||||
indexDef += translatedIndexs[k * 2 + 1] - translatedIndexs[k * 2];
|
||||
}
|
||||
|
||||
return translatedIndex - indexDef - 1;
|
||||
}
|
||||
|
||||
// Binary Search with Range
|
||||
for (int i = originalIndexs.Count / 2;; count++)
|
||||
{
|
||||
if (translatedIndex < translatedIndexs[i * 2])
|
||||
{
|
||||
// move to lower middle
|
||||
upperBound = i;
|
||||
i = (i + lowerBound) / 2;
|
||||
}
|
||||
else if (translatedIndex > translatedIndexs[i * 2 + 1] - 1)
|
||||
{
|
||||
lowerBound = i;
|
||||
// move to upper middle
|
||||
// due to floor of integer division, move one up on corner case
|
||||
i = (i + upperBound + 1) / 2;
|
||||
}
|
||||
else
|
||||
return originalIndexs[i];
|
||||
|
||||
if (upperBound - lowerBound <= 1 &&
|
||||
translatedIndex > translatedIndexs[lowerBound * 2 + 1] &&
|
||||
translatedIndex < translatedIndexs[upperBound * 2])
|
||||
{
|
||||
int indexDef = 0;
|
||||
|
||||
for (int j = 0; j < upperBound; j++)
|
||||
{
|
||||
indexDef += translatedIndexs[j * 2 + 1] - translatedIndexs[j * 2];
|
||||
}
|
||||
|
||||
return translatedIndex - indexDef - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void endConstruct()
|
||||
{
|
||||
if (constructed)
|
||||
throw new InvalidOperationException("Mapping has already been constructed");
|
||||
constructed = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Translate a language to English letters using a given rule.
|
||||
/// </summary>
|
||||
public interface IAlphabet
|
||||
{
|
||||
/// <summary>
|
||||
/// Translate a string to English letters, using a given rule.
|
||||
/// </summary>
|
||||
/// <param name="stringToTranslate">String to translate.</param>
|
||||
/// <returns></returns>
|
||||
public (string translation, TranslationMapping map) Translate(string stringToTranslate);
|
||||
|
||||
/// <summary>
|
||||
/// Determine if a string can be translated to English letter with this Alphabet.
|
||||
/// </summary>
|
||||
/// <param name="stringToTranslate">String to translate.</param>
|
||||
/// <returns></returns>
|
||||
public bool CanBeTranslated(string stringToTranslate);
|
||||
}
|
||||
|
||||
public class PinyinAlphabet : IAlphabet
|
||||
{
|
||||
private ConcurrentDictionary<string, (string translation, TranslationMapping map)> _pinyinCache =
|
||||
new ConcurrentDictionary<string, (string translation, TranslationMapping map)>();
|
||||
|
||||
private Settings _settings;
|
||||
private readonly ConcurrentDictionary<string, (string translation, TranslationMapping map)> _pinyinCache = new();
|
||||
private readonly Settings _settings;
|
||||
private ReadOnlyDictionary<string, string> currentDoublePinyinTable;
|
||||
|
||||
public PinyinAlphabet()
|
||||
{
|
||||
Initialize(Ioc.Default.GetRequiredService<Settings>());
|
||||
_settings = Ioc.Default.GetRequiredService<Settings>();
|
||||
LoadDoublePinyinTable();
|
||||
|
||||
_settings.PropertyChanged += (sender, e) =>
|
||||
{
|
||||
switch (e.PropertyName)
|
||||
{
|
||||
case nameof(Settings.ShouldUsePinyin):
|
||||
if (_settings.ShouldUsePinyin)
|
||||
{
|
||||
Reload();
|
||||
}
|
||||
break;
|
||||
case nameof(Settings.UseDoublePinyin):
|
||||
case nameof(Settings.DoublePinyinSchema):
|
||||
if (_settings.UseDoublePinyin)
|
||||
{
|
||||
Reload();
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void Initialize([NotNull] Settings settings)
|
||||
public void Reload()
|
||||
{
|
||||
_settings = settings ?? throw new ArgumentNullException(nameof(settings));
|
||||
LoadDoublePinyinTable();
|
||||
_pinyinCache.Clear();
|
||||
}
|
||||
|
||||
public bool CanBeTranslated(string stringToTranslate)
|
||||
private void CreateDoublePinyinTableFromStream(Stream jsonStream)
|
||||
{
|
||||
return WordsHelper.HasChinese(stringToTranslate);
|
||||
var table = JsonSerializer.Deserialize<Dictionary<string, Dictionary<string, string>>>(jsonStream) ??
|
||||
throw new InvalidOperationException("Failed to deserialize double pinyin table: result is null");
|
||||
|
||||
var schemaKey = _settings.DoublePinyinSchema.ToString();
|
||||
if (!table.TryGetValue(schemaKey, out var schemaDict))
|
||||
{
|
||||
throw new ArgumentException($"DoublePinyinSchema '{schemaKey}' is invalid or double pinyin table is broken.");
|
||||
}
|
||||
|
||||
currentDoublePinyinTable = new ReadOnlyDictionary<string, string>(schemaDict);
|
||||
}
|
||||
|
||||
private void LoadDoublePinyinTable()
|
||||
{
|
||||
if (!_settings.UseDoublePinyin)
|
||||
{
|
||||
currentDoublePinyinTable = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>());
|
||||
return;
|
||||
}
|
||||
|
||||
var tablePath = Path.Combine(AppContext.BaseDirectory, "Resources", "double_pinyin.json");
|
||||
try
|
||||
{
|
||||
using var fs = File.OpenRead(tablePath);
|
||||
CreateDoublePinyinTableFromStream(fs);
|
||||
}
|
||||
catch (FileNotFoundException e)
|
||||
{
|
||||
Log.Exception(nameof(PinyinAlphabet), $"Double pinyin table file not found: {tablePath}", e);
|
||||
currentDoublePinyinTable = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>());
|
||||
}
|
||||
catch (DirectoryNotFoundException e)
|
||||
{
|
||||
Log.Exception(nameof(PinyinAlphabet), $"Directory not found for double pinyin table: {tablePath}", e);
|
||||
currentDoublePinyinTable = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>());
|
||||
}
|
||||
catch (UnauthorizedAccessException e)
|
||||
{
|
||||
Log.Exception(nameof(PinyinAlphabet), $"Access denied to double pinyin table: {tablePath}", e);
|
||||
currentDoublePinyinTable = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>());
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Log.Exception(nameof(PinyinAlphabet), $"Failed to load double pinyin table from file: {tablePath}", e);
|
||||
currentDoublePinyinTable = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>());
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShouldTranslate(string stringToTranslate)
|
||||
{
|
||||
// If the query (stringToTranslate) does NOT contain Chinese characters,
|
||||
// we should translate the target string to pinyin for matching
|
||||
return _settings.ShouldUsePinyin && !ContainsChinese(stringToTranslate);
|
||||
}
|
||||
|
||||
public (string translation, TranslationMapping map) Translate(string content)
|
||||
{
|
||||
if (_settings.ShouldUsePinyin)
|
||||
{
|
||||
if (!_pinyinCache.ContainsKey(content))
|
||||
{
|
||||
return BuildCacheFromContent(content);
|
||||
}
|
||||
else
|
||||
{
|
||||
return _pinyinCache[content];
|
||||
}
|
||||
}
|
||||
return (content, null);
|
||||
if (!_settings.ShouldUsePinyin || !ContainsChinese(content))
|
||||
return (content, null);
|
||||
|
||||
return _pinyinCache.TryGetValue(content, out var cached) ? cached : BuildCacheFromContent(content);
|
||||
}
|
||||
|
||||
private (string translation, TranslationMapping map) BuildCacheFromContent(string content)
|
||||
{
|
||||
if (WordsHelper.HasChinese(content))
|
||||
var resultList = WordsHelper.GetPinyinList(content);
|
||||
var resultBuilder = new StringBuilder(_settings.UseDoublePinyin ? 3 : 4); // Pre-allocate with estimated capacity
|
||||
var map = new TranslationMapping();
|
||||
|
||||
var previousIsChinese = false;
|
||||
|
||||
for (var i = 0; i < resultList.Length; i++)
|
||||
{
|
||||
var resultList = WordsHelper.GetPinyinList(content);
|
||||
|
||||
StringBuilder resultBuilder = new StringBuilder();
|
||||
TranslationMapping map = new TranslationMapping();
|
||||
|
||||
bool pre = false;
|
||||
|
||||
for (int i = 0; i < resultList.Length; i++)
|
||||
if (IsChineseCharacter(content[i]))
|
||||
{
|
||||
if (content[i] >= 0x3400 && content[i] <= 0x9FD5)
|
||||
var translated = _settings.UseDoublePinyin ? ToDoublePinyin(resultList[i]) : resultList[i];
|
||||
|
||||
if (i > 0 && content[i - 1] != ' ')
|
||||
{
|
||||
map.AddNewIndex(i, resultBuilder.Length, resultList[i].Length + 1);
|
||||
resultBuilder.Append(' ');
|
||||
resultBuilder.Append(resultList[i]);
|
||||
pre = true;
|
||||
}
|
||||
else
|
||||
|
||||
map.AddNewIndex(resultBuilder.Length, translated.Length);
|
||||
resultBuilder.Append(translated);
|
||||
previousIsChinese = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Add space after Chinese characters before non-Chinese characters
|
||||
if (previousIsChinese)
|
||||
{
|
||||
if (pre)
|
||||
previousIsChinese = false;
|
||||
if (content[i] != ' ')
|
||||
{
|
||||
pre = false;
|
||||
resultBuilder.Append(' ');
|
||||
}
|
||||
|
||||
resultBuilder.Append(resultList[i]);
|
||||
}
|
||||
|
||||
map.AddNewIndex(resultBuilder.Length, 1);
|
||||
resultBuilder.Append(content[i]);
|
||||
}
|
||||
|
||||
map.endConstruct();
|
||||
|
||||
var key = resultBuilder.ToString();
|
||||
map.setKey(key);
|
||||
|
||||
return _pinyinCache[content] = (key, map);
|
||||
}
|
||||
else
|
||||
|
||||
map.EndConstruct();
|
||||
|
||||
var translation = resultBuilder.ToString();
|
||||
var result = (translation, map);
|
||||
|
||||
return _pinyinCache[content] = result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optimized Chinese character detection using the comprehensive CJK Unicode ranges
|
||||
/// </summary>
|
||||
private static bool ContainsChinese(ReadOnlySpan<char> text)
|
||||
{
|
||||
foreach (var c in text)
|
||||
{
|
||||
return (content, null);
|
||||
if (IsChineseCharacter(c))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a character is a Chinese character using comprehensive Unicode ranges
|
||||
/// Covers CJK Unified Ideographs, Extension A
|
||||
/// </summary>
|
||||
private static bool IsChineseCharacter(char c)
|
||||
{
|
||||
return (c >= 0x4E00 && c <= 0x9FFF) || // CJK Unified Ideographs
|
||||
(c >= 0x3400 && c <= 0x4DBF); // CJK Extension A
|
||||
}
|
||||
|
||||
private string ToDoublePinyin(string fullPinyin)
|
||||
{
|
||||
return currentDoublePinyinTable.TryGetValue(fullPinyin, out var doublePinyinValue)
|
||||
? doublePinyinValue
|
||||
: fullPinyin;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ using MemoryPack;
|
|||
namespace Flow.Launcher.Infrastructure.Storage
|
||||
{
|
||||
/// <summary>
|
||||
/// Stroage object using binary data
|
||||
/// Storage object using binary data
|
||||
/// Normally, it has better performance, but not readable
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
|
|
@ -53,6 +53,45 @@ namespace Flow.Launcher.Infrastructure.Storage
|
|||
FilePath = Path.Combine(DirectoryPath, $"{filename}{FileSuffix}");
|
||||
}
|
||||
|
||||
public T TryLoad(T defaultData)
|
||||
{
|
||||
if (Data != null) return Data;
|
||||
|
||||
if (File.Exists(FilePath))
|
||||
{
|
||||
if (new FileInfo(FilePath).Length == 0)
|
||||
{
|
||||
Log.Error(ClassName, $"Zero length cache file <{FilePath}>");
|
||||
Data = defaultData;
|
||||
Save();
|
||||
}
|
||||
|
||||
var bytes = File.ReadAllBytes(FilePath);
|
||||
Data = Deserialize(bytes, defaultData);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Info(ClassName, "Cache file not exist, load default data");
|
||||
Data = defaultData;
|
||||
Save();
|
||||
}
|
||||
return Data;
|
||||
}
|
||||
|
||||
private T Deserialize(ReadOnlySpan<byte> bytes, T defaultData)
|
||||
{
|
||||
try
|
||||
{
|
||||
var t = MemoryPackSerializer.Deserialize<T>(bytes);
|
||||
return t ?? defaultData;
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Log.Exception(ClassName, $"Deserialize error for file <{FilePath}>", e);
|
||||
return defaultData;
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<T> TryLoadAsync(T defaultData)
|
||||
{
|
||||
if (Data != null) return Data;
|
||||
|
|
@ -79,26 +118,31 @@ namespace Flow.Launcher.Infrastructure.Storage
|
|||
return Data;
|
||||
}
|
||||
|
||||
private static async ValueTask<T> DeserializeAsync(Stream stream, T defaultData)
|
||||
private async ValueTask<T> DeserializeAsync(Stream stream, T defaultData)
|
||||
{
|
||||
try
|
||||
{
|
||||
var t = await MemoryPackSerializer.DeserializeAsync<T>(stream);
|
||||
return t ?? defaultData;
|
||||
}
|
||||
catch (System.Exception)
|
||||
catch (System.Exception e)
|
||||
{
|
||||
// Log.Exception($"|BinaryStorage.Deserialize|Deserialize error for file <{FilePath}>", e);
|
||||
Log.Exception(ClassName, $"Deserialize error for file <{FilePath}>", e);
|
||||
return defaultData;
|
||||
}
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
Save(Data.NonNull());
|
||||
}
|
||||
|
||||
public void Save(T data)
|
||||
{
|
||||
// User may delete the directory, so we need to check it
|
||||
FilesFolders.ValidateDirectory(DirectoryPath);
|
||||
|
||||
var serialized = MemoryPackSerializer.Serialize(Data);
|
||||
var serialized = MemoryPackSerializer.Serialize(data);
|
||||
File.WriteAllBytes(FilePath, serialized);
|
||||
}
|
||||
|
||||
|
|
@ -107,15 +151,6 @@ namespace Flow.Launcher.Infrastructure.Storage
|
|||
await SaveAsync(Data.NonNull());
|
||||
}
|
||||
|
||||
// ImageCache need to convert data into concurrent dictionary for usage,
|
||||
// so we would better to clear the data
|
||||
public void ClearData()
|
||||
{
|
||||
Data = default;
|
||||
}
|
||||
|
||||
// ImageCache storages data in its class,
|
||||
// so we need to pass it to SaveAsync
|
||||
public async ValueTask SaveAsync(T data)
|
||||
{
|
||||
// User may delete the directory, so we need to check it
|
||||
|
|
@ -124,5 +159,12 @@ namespace Flow.Launcher.Infrastructure.Storage
|
|||
await using var stream = new FileStream(FilePath, FileMode.Create);
|
||||
await MemoryPackSerializer.SerializeAsync(stream, data);
|
||||
}
|
||||
|
||||
// ImageCache need to convert data into concurrent dictionary for usage,
|
||||
// so we would better to clear the data
|
||||
public void ClearData()
|
||||
{
|
||||
Data = default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,13 @@
|
|||
using System.Threading.Tasks;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.Storage
|
||||
{
|
||||
public class FlowLauncherJsonStorage<T> : JsonStorage<T> where T : new()
|
||||
// Expose ISaveable interface in derived class to make sure we are calling the new version of Save method
|
||||
public class FlowLauncherJsonStorage<T> : JsonStorage<T>, ISavable where T : new()
|
||||
{
|
||||
private static readonly string ClassName = "FlowLauncherJsonStorage";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.Storage
|
||||
{
|
||||
public class PluginBinaryStorage<T> : BinaryStorage<T> where T : new()
|
||||
// Expose ISaveable interface in derived class to make sure we are calling the new version of Save method
|
||||
public class PluginBinaryStorage<T> : BinaryStorage<T>, ISavable where T : new()
|
||||
{
|
||||
private static readonly string ClassName = "PluginBinaryStorage";
|
||||
|
||||
|
|
|
|||
|
|
@ -2,11 +2,13 @@
|
|||
using System.Threading.Tasks;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.Storage
|
||||
{
|
||||
public class PluginJsonStorage<T> : JsonStorage<T> where T : new()
|
||||
// Expose ISaveable interface in derived class to make sure we are calling the new version of Save method
|
||||
public class PluginJsonStorage<T> : JsonStorage<T>, ISavable where T : new()
|
||||
{
|
||||
// Use assembly name to check which plugin is using this storage
|
||||
public readonly string AssemblyName;
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ namespace Flow.Launcher.Infrastructure
|
|||
|
||||
query = query.Trim();
|
||||
TranslationMapping translationMapping = null;
|
||||
if (_alphabet is not null && !_alphabet.CanBeTranslated(query))
|
||||
if (_alphabet is not null && _alphabet.ShouldTranslate(query))
|
||||
{
|
||||
// We assume that if a query can be translated (containing characters of a language, like Chinese)
|
||||
// it actually means user doesn't want it to be translated to English letters.
|
||||
|
|
@ -228,7 +228,7 @@ namespace Flow.Launcher.Infrastructure
|
|||
return new MatchResult(false, UserSettingSearchPrecision);
|
||||
}
|
||||
|
||||
private bool IsAcronym(string stringToCompare, int compareStringIndex)
|
||||
private static bool IsAcronym(string stringToCompare, int compareStringIndex)
|
||||
{
|
||||
if (IsAcronymChar(stringToCompare, compareStringIndex) || IsAcronymNumber(stringToCompare, compareStringIndex))
|
||||
return true;
|
||||
|
|
@ -237,7 +237,7 @@ namespace Flow.Launcher.Infrastructure
|
|||
}
|
||||
|
||||
// When counting acronyms, treat a set of numbers as one acronym ie. Visual 2019 as 2 acronyms instead of 5
|
||||
private bool IsAcronymCount(string stringToCompare, int compareStringIndex)
|
||||
private static bool IsAcronymCount(string stringToCompare, int compareStringIndex)
|
||||
{
|
||||
if (IsAcronymChar(stringToCompare, compareStringIndex))
|
||||
return true;
|
||||
|
|
|
|||
34
Flow.Launcher.Infrastructure/TranslationMapping.cs
Normal file
34
Flow.Launcher.Infrastructure/TranslationMapping.cs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure
|
||||
{
|
||||
public class TranslationMapping
|
||||
{
|
||||
private bool _isConstructed;
|
||||
|
||||
// Assuming one original item maps to multi translated items
|
||||
// list[i] is the last translated index + 1 of original index i
|
||||
private readonly List<int> _originalToTranslated = new();
|
||||
|
||||
public void AddNewIndex(int translatedIndex, int length)
|
||||
{
|
||||
if (_isConstructed)
|
||||
throw new InvalidOperationException("Mapping shouldn't be changed after construction");
|
||||
_originalToTranslated.Add(translatedIndex + length);
|
||||
}
|
||||
|
||||
public int MapToOriginalIndex(int translatedIndex)
|
||||
{
|
||||
var searchResult = _originalToTranslated.BinarySearch(translatedIndex);
|
||||
return searchResult >= 0 ? searchResult + 1 : ~searchResult;
|
||||
}
|
||||
|
||||
public void EndConstruct()
|
||||
{
|
||||
if (_isConstructed)
|
||||
throw new InvalidOperationException("Mapping has already been constructed");
|
||||
_isConstructed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
using System;
|
||||
using System.Windows.Markup;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.UI
|
||||
{
|
||||
[Obsolete("EnumBindingSourceExtension is obsolete. Use with Flow.Launcher.Localization NuGet package instead.")]
|
||||
public class EnumBindingSourceExtension : MarkupExtension
|
||||
{
|
||||
private Type _enumType;
|
||||
public Type EnumType
|
||||
{
|
||||
get { return _enumType; }
|
||||
set
|
||||
{
|
||||
if (value != _enumType)
|
||||
{
|
||||
if (value != null)
|
||||
{
|
||||
Type enumType = Nullable.GetUnderlyingType(value) ?? value;
|
||||
if (!enumType.IsEnum)
|
||||
{
|
||||
throw new ArgumentException("Type must represent an enum.");
|
||||
}
|
||||
}
|
||||
|
||||
_enumType = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public EnumBindingSourceExtension() { }
|
||||
|
||||
public EnumBindingSourceExtension(Type enumType)
|
||||
{
|
||||
EnumType = enumType;
|
||||
}
|
||||
|
||||
public override object ProvideValue(IServiceProvider serviceProvider)
|
||||
{
|
||||
if (_enumType == null)
|
||||
{
|
||||
throw new InvalidOperationException("The EnumType must be specified.");
|
||||
}
|
||||
|
||||
Type actualEnumType = Nullable.GetUnderlyingType(_enumType) ?? _enumType;
|
||||
Array enumValues = Enum.GetValues(actualEnumType);
|
||||
|
||||
if (actualEnumType == _enumType)
|
||||
{
|
||||
return enumValues;
|
||||
}
|
||||
|
||||
Array tempArray = Array.CreateInstance(actualEnumType, enumValues.Length + 1);
|
||||
enumValues.CopyTo(tempArray, 1);
|
||||
return tempArray;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
using System.Text.Json.Serialization;
|
||||
using Flow.Launcher.Plugin;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.UserSettings
|
||||
{
|
||||
public class CustomBrowserViewModel : BaseModel
|
||||
{
|
||||
public string Name { get; set; }
|
||||
[JsonIgnore]
|
||||
public string DisplayName => Name == "Default" ? Localize.defaultBrowser_default() : Name;
|
||||
public string Path { get; set; }
|
||||
public string PrivateArg { get; set; }
|
||||
public bool EnablePrivate { get; set; }
|
||||
|
|
@ -26,8 +28,10 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
Editable = Editable
|
||||
};
|
||||
}
|
||||
|
||||
public void OnDisplayNameChanged()
|
||||
{
|
||||
OnPropertyChanged(nameof(DisplayName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
using Flow.Launcher.Plugin;
|
||||
using System.Text.Json.Serialization;
|
||||
using Flow.Launcher.Plugin;
|
||||
|
||||
namespace Flow.Launcher.ViewModel
|
||||
namespace Flow.Launcher.Infrastructure.UserSettings
|
||||
{
|
||||
public class CustomExplorerViewModel : BaseModel
|
||||
{
|
||||
public string Name { get; set; }
|
||||
[JsonIgnore]
|
||||
public string DisplayName => Name == "Explorer" ? Localize.fileManagerExplorer() : Name;
|
||||
public string Path { get; set; }
|
||||
public string FileArgument { get; set; } = "\"%d\"";
|
||||
public string DirectoryArgument { get; set; } = "\"%d\"";
|
||||
|
|
@ -21,5 +24,10 @@ namespace Flow.Launcher.ViewModel
|
|||
Editable = Editable
|
||||
};
|
||||
}
|
||||
|
||||
public void OnDisplayNameChanged()
|
||||
{
|
||||
OnPropertyChanged(nameof(DisplayName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,6 +53,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
{
|
||||
public string Description { get; set; }
|
||||
|
||||
public string LocalizedDescription => PublicApi.Instance.GetTranslation(Description);
|
||||
|
||||
public BaseBuiltinShortcutModel(string key, string description)
|
||||
{
|
||||
Key = key;
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
{
|
||||
public const string PortableFolderName = "UserData";
|
||||
public const string DeletionIndicatorFile = ".dead";
|
||||
public static string PortableDataPath = Path.Combine(Constant.ProgramDirectory, PortableFolderName);
|
||||
public static string RoamingDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "FlowLauncher");
|
||||
public static readonly string PortableDataPath = Path.Combine(Constant.ProgramDirectory, PortableFolderName);
|
||||
public static readonly string RoamingDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "FlowLauncher");
|
||||
public static string DataDirectory()
|
||||
{
|
||||
if (PortableDataLocationInUse())
|
||||
|
|
@ -19,7 +19,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
|
||||
public static bool PortableDataLocationInUse()
|
||||
{
|
||||
if (Directory.Exists(PortableDataPath) && !File.Exists(DeletionIndicatorFile))
|
||||
if (Directory.Exists(PortableDataPath) &&
|
||||
!File.Exists(Path.Combine(PortableDataPath, DeletionIndicatorFile)))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
|
|
@ -39,6 +40,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
public const string PythonEnvironmentName = "Python";
|
||||
public const string NodeEnvironmentName = "Node.js";
|
||||
public const string PluginEnvironments = "Environments";
|
||||
public const string PluginDeleteFile = "NeedDelete.txt";
|
||||
public static readonly string PluginEnvironmentsPath = Path.Combine(DataDirectory(), PluginEnvironments);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using Flow.Launcher.Plugin;
|
||||
using System;
|
||||
using Flow.Launcher.Plugin;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.UserSettings
|
||||
{
|
||||
|
|
@ -6,5 +7,26 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
{
|
||||
public string Hotkey { get; set; }
|
||||
public string ActionKeyword { get; set; }
|
||||
|
||||
public CustomPluginHotkey(string hotkey, string actionKeyword)
|
||||
{
|
||||
Hotkey = hotkey;
|
||||
ActionKeyword = actionKeyword;
|
||||
}
|
||||
|
||||
public override bool Equals(object other)
|
||||
{
|
||||
if (other is CustomPluginHotkey otherHotkey)
|
||||
{
|
||||
return Hotkey == otherHotkey.Hotkey && ActionKeyword == otherHotkey.ActionKeyword;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Hotkey, ActionKeyword);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ using CommunityToolkit.Mvvm.DependencyInjection;
|
|||
using Flow.Launcher.Infrastructure.Hotkey;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Infrastructure.Storage;
|
||||
using Flow.Launcher.Localization.Attributes;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Flow.Launcher.Plugin.SharedModels;
|
||||
using Flow.Launcher.ViewModel;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure.UserSettings
|
||||
{
|
||||
|
|
@ -25,7 +25,13 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
|
||||
public void Initialize()
|
||||
{
|
||||
// Initialize dependency injection instances after Ioc.Default is created
|
||||
_stringMatcher = Ioc.Default.GetRequiredService<StringMatcher>();
|
||||
|
||||
// Initialize application resources after application is created
|
||||
var settingWindowFont = new FontFamily(SettingWindowFont);
|
||||
Application.Current.Resources["SettingWindowFont"] = settingWindowFont;
|
||||
Application.Current.Resources["ContentControlThemeFontFamily"] = settingWindowFont;
|
||||
}
|
||||
|
||||
public void Save()
|
||||
|
|
@ -33,11 +39,38 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
_storage.Save();
|
||||
}
|
||||
|
||||
private string _theme = Constant.DefaultTheme;
|
||||
public string Hotkey { get; set; } = $"{KeyConstant.Alt} + {KeyConstant.Space}";
|
||||
public string OpenResultModifiers { get; set; } = KeyConstant.Alt;
|
||||
|
||||
private string _openResultModifiers = KeyConstant.Alt;
|
||||
public string OpenResultModifiers
|
||||
{
|
||||
get => _openResultModifiers;
|
||||
set
|
||||
{
|
||||
if (_openResultModifiers != value)
|
||||
{
|
||||
_openResultModifiers = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string ColorScheme { get; set; } = "System";
|
||||
public bool ShowOpenResultHotkey { get; set; } = true;
|
||||
|
||||
private bool _showOpenResultHotkey = true;
|
||||
public bool ShowOpenResultHotkey
|
||||
{
|
||||
get => _showOpenResultHotkey;
|
||||
set
|
||||
{
|
||||
if (_showOpenResultHotkey != value)
|
||||
{
|
||||
_showOpenResultHotkey = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public double WindowSize { get; set; } = 580;
|
||||
public string PreviewHotkey { get; set; } = $"F1";
|
||||
public string AutoCompleteHotkey { get; set; } = $"{KeyConstant.Ctrl} + Tab";
|
||||
|
|
@ -53,6 +86,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
public string OpenHistoryHotkey { get; set; } = $"Ctrl+H";
|
||||
public string CycleHistoryUpHotkey { get; set; } = $"{KeyConstant.Alt} + Up";
|
||||
public string CycleHistoryDownHotkey { get; set; } = $"{KeyConstant.Alt} + Down";
|
||||
public string DialogJumpHotkey { get; set; } = $"{KeyConstant.Alt} + G";
|
||||
|
||||
private string _language = Constant.SystemLanguageCode;
|
||||
public string Language
|
||||
|
|
@ -60,16 +94,20 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
get => _language;
|
||||
set
|
||||
{
|
||||
_language = value;
|
||||
OnPropertyChanged();
|
||||
if (_language != value)
|
||||
{
|
||||
_language = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
private string _theme = Constant.DefaultTheme;
|
||||
public string Theme
|
||||
{
|
||||
get => _theme;
|
||||
set
|
||||
{
|
||||
if (value != _theme)
|
||||
if (_theme != value)
|
||||
{
|
||||
_theme = value;
|
||||
OnPropertyChanged();
|
||||
|
|
@ -78,7 +116,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
}
|
||||
}
|
||||
public bool UseDropShadowEffect { get; set; } = true;
|
||||
public BackdropTypes BackdropType{ get; set; } = BackdropTypes.None;
|
||||
public BackdropTypes BackdropType { get; set; } = BackdropTypes.None;
|
||||
public string ReleaseNotesVersion { get; set; } = string.Empty;
|
||||
|
||||
/* Appearance Settings. It should be separated from the setting later.*/
|
||||
public double WindowHeightSize { get; set; } = 42;
|
||||
|
|
@ -115,8 +154,11 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
{
|
||||
_settingWindowFont = value;
|
||||
OnPropertyChanged();
|
||||
Application.Current.Resources["SettingWindowFont"] = new FontFamily(value);
|
||||
Application.Current.Resources["ContentControlThemeFontFamily"] = new FontFamily(value);
|
||||
if (Application.Current != null)
|
||||
{
|
||||
Application.Current.Resources["SettingWindowFont"] = new FontFamily(value);
|
||||
Application.Current.Resources["ContentControlThemeFontFamily"] = new FontFamily(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -187,9 +229,13 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public int MaxHistoryResultsToShowForHomePage { get; set; } = 5;
|
||||
|
||||
public bool AutoRestartAfterChanging { get; set; } = false;
|
||||
public bool ShowUnknownSourceWarning { get; set; } = true;
|
||||
public bool AutoUpdatePlugins { get; set; } = true;
|
||||
|
||||
public int CustomExplorerIndex { get; set; } = 0;
|
||||
|
||||
[JsonIgnore]
|
||||
|
|
@ -278,13 +324,70 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
}
|
||||
};
|
||||
|
||||
public bool EnableDialogJump { get; set; } = true;
|
||||
|
||||
public bool AutoDialogJump { get; set; } = false;
|
||||
|
||||
public bool ShowDialogJumpWindow { get; set; } = false;
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public DialogJumpWindowPositions DialogJumpWindowPosition { get; set; } = DialogJumpWindowPositions.UnderDialog;
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public DialogJumpResultBehaviours DialogJumpResultBehaviour { get; set; } = DialogJumpResultBehaviours.LeftClick;
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public DialogJumpFileResultBehaviours DialogJumpFileResultBehaviour { get; set; } = DialogJumpFileResultBehaviours.FullPath;
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public LOGLEVEL LogLevel { get; set; } = LOGLEVEL.INFO;
|
||||
|
||||
/// <summary>
|
||||
/// when false Alphabet static service will always return empty results
|
||||
/// </summary>
|
||||
public bool ShouldUsePinyin { get; set; } = false;
|
||||
private bool _useAlphabet = true;
|
||||
public bool ShouldUsePinyin
|
||||
{
|
||||
get => _useAlphabet;
|
||||
set
|
||||
{
|
||||
if (_useAlphabet != value)
|
||||
{
|
||||
_useAlphabet = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool _useDoublePinyin = false;
|
||||
public bool UseDoublePinyin
|
||||
{
|
||||
get => _useDoublePinyin;
|
||||
set
|
||||
{
|
||||
if (_useDoublePinyin != value)
|
||||
{
|
||||
_useDoublePinyin = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private DoublePinyinSchemas _doublePinyinSchema = DoublePinyinSchemas.XiaoHe;
|
||||
|
||||
[JsonInclude, JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public DoublePinyinSchemas DoublePinyinSchema
|
||||
{
|
||||
get => _doublePinyinSchema;
|
||||
set
|
||||
{
|
||||
if (_doublePinyinSchema != value)
|
||||
{
|
||||
_doublePinyinSchema = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool AlwaysPreview { get; set; } = false;
|
||||
|
||||
|
|
@ -297,9 +400,12 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
get => _querySearchPrecision;
|
||||
set
|
||||
{
|
||||
_querySearchPrecision = value;
|
||||
if (_stringMatcher != null)
|
||||
_stringMatcher.UserSettingSearchPrecision = value;
|
||||
if (_querySearchPrecision != value)
|
||||
{
|
||||
_querySearchPrecision = value;
|
||||
if (_stringMatcher != null)
|
||||
_stringMatcher.UserSettingSearchPrecision = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -366,12 +472,30 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
get => _hideNotifyIcon;
|
||||
set
|
||||
{
|
||||
_hideNotifyIcon = value;
|
||||
OnPropertyChanged();
|
||||
if (_hideNotifyIcon != value)
|
||||
{
|
||||
_hideNotifyIcon = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
public bool LeaveCmdOpen { get; set; }
|
||||
public bool HideWhenDeactivated { get; set; } = true;
|
||||
public bool ShowTaskbarWhenInvoked { get; set; } = false;
|
||||
|
||||
private bool _showAtTopmost = false;
|
||||
public bool ShowAtTopmost
|
||||
{
|
||||
get => _showAtTopmost;
|
||||
set
|
||||
{
|
||||
if (_showAtTopmost != value)
|
||||
{
|
||||
_showAtTopmost = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool SearchQueryResultsWithDelay { get; set; }
|
||||
public int SearchDelayTime { get; set; } = 150;
|
||||
|
|
@ -391,6 +515,21 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public LastQueryMode LastQueryMode { get; set; } = LastQueryMode.Selected;
|
||||
|
||||
private HistoryStyle _historyStyle = HistoryStyle.Query;
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public HistoryStyle HistoryStyle
|
||||
{
|
||||
get => _historyStyle;
|
||||
set
|
||||
{
|
||||
if (_historyStyle != value)
|
||||
{
|
||||
_historyStyle = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public AnimationSpeeds AnimationSpeed { get; set; } = AnimationSpeeds.Medium;
|
||||
public int CustomAnimationLength { get; set; } = 360;
|
||||
|
|
@ -408,7 +547,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
{
|
||||
var list = FixedHotkeys();
|
||||
|
||||
// Customizeable hotkeys
|
||||
// Customizable hotkeys
|
||||
if (!string.IsNullOrEmpty(Hotkey))
|
||||
list.Add(new(Hotkey, "flowlauncherHotkey", () => Hotkey = ""));
|
||||
if (!string.IsNullOrEmpty(PreviewHotkey))
|
||||
|
|
@ -428,7 +567,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
if (!string.IsNullOrEmpty(SettingWindowHotkey))
|
||||
list.Add(new(SettingWindowHotkey, "SettingWindowHotkey", () => SettingWindowHotkey = ""));
|
||||
if (!string.IsNullOrEmpty(OpenHistoryHotkey))
|
||||
list.Add(new(OpenHistoryHotkey, "OpenHistoryHotkey", () => OpenHistoryHotkey = ""));
|
||||
list.Add(new(OpenHistoryHotkey, "OpenHistoryHotkey", () => OpenHistoryHotkey = ""));
|
||||
if (!string.IsNullOrEmpty(OpenContextMenuHotkey))
|
||||
list.Add(new(OpenContextMenuHotkey, "OpenContextMenuHotkey", () => OpenContextMenuHotkey = ""));
|
||||
if (!string.IsNullOrEmpty(SelectNextPageHotkey))
|
||||
|
|
@ -439,6 +578,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
list.Add(new(CycleHistoryUpHotkey, "CycleHistoryUpHotkey", () => CycleHistoryUpHotkey = ""));
|
||||
if (!string.IsNullOrEmpty(CycleHistoryDownHotkey))
|
||||
list.Add(new(CycleHistoryDownHotkey, "CycleHistoryDownHotkey", () => CycleHistoryDownHotkey = ""));
|
||||
if (!string.IsNullOrEmpty(DialogJumpHotkey))
|
||||
list.Add(new(DialogJumpHotkey, "dialogJumpHotkey", () => DialogJumpHotkey = ""));
|
||||
|
||||
// Custom Query Hotkeys
|
||||
foreach (var customPluginHotkey in CustomPluginHotkeys)
|
||||
|
|
@ -534,9 +675,51 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
|
||||
public enum BackdropTypes
|
||||
{
|
||||
None,
|
||||
None,
|
||||
Acrylic,
|
||||
Mica,
|
||||
MicaAlt
|
||||
}
|
||||
|
||||
public enum DoublePinyinSchemas
|
||||
{
|
||||
XiaoHe,
|
||||
ZiRanMa,
|
||||
WeiRuan,
|
||||
ZhiNengABC,
|
||||
ZiGuangPinYin,
|
||||
PinYinJiaJia,
|
||||
XingKongJianDao,
|
||||
DaNiu,
|
||||
XiaoLang
|
||||
}
|
||||
|
||||
public enum DialogJumpWindowPositions
|
||||
{
|
||||
UnderDialog,
|
||||
FollowDefault
|
||||
}
|
||||
|
||||
public enum DialogJumpResultBehaviours
|
||||
{
|
||||
LeftClick,
|
||||
RightClick
|
||||
}
|
||||
|
||||
public enum DialogJumpFileResultBehaviours
|
||||
{
|
||||
FullPath,
|
||||
FullPathOpen,
|
||||
Directory
|
||||
}
|
||||
|
||||
[EnumLocalize]
|
||||
public enum HistoryStyle
|
||||
{
|
||||
[EnumLocalizeKey(nameof(Localize.queryHistory))]
|
||||
Query,
|
||||
|
||||
[EnumLocalizeKey(nameof(Localize.executedHistory))]
|
||||
LastOpened
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,10 +13,14 @@ using System.Windows.Interop;
|
|||
using System.Windows.Markup;
|
||||
using System.Windows.Media;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin.SharedModels;
|
||||
using Microsoft.Win32;
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.Graphics.Dwm;
|
||||
using Windows.Win32.System.Power;
|
||||
using Windows.Win32.System.Threading;
|
||||
using Windows.Win32.UI.Input.KeyboardAndMouse;
|
||||
using Windows.Win32.UI.Shell.Common;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
|
|
@ -118,9 +122,9 @@ namespace Flow.Launcher.Infrastructure
|
|||
|
||||
#region Window Foreground
|
||||
|
||||
public static nint GetForegroundWindow()
|
||||
public static unsafe nint GetForegroundWindow()
|
||||
{
|
||||
return PInvoke.GetForegroundWindow().Value;
|
||||
return (nint)PInvoke.GetForegroundWindow().Value;
|
||||
}
|
||||
|
||||
public static bool SetForegroundWindow(Window window)
|
||||
|
|
@ -138,6 +142,11 @@ namespace Flow.Launcher.Infrastructure
|
|||
return IsForegroundWindow(GetWindowHandle(window));
|
||||
}
|
||||
|
||||
public static bool IsForegroundWindow(nint handle)
|
||||
{
|
||||
return IsForegroundWindow(new HWND(handle));
|
||||
}
|
||||
|
||||
internal static bool IsForegroundWindow(HWND handle)
|
||||
{
|
||||
return handle.Equals(PInvoke.GetForegroundWindow());
|
||||
|
|
@ -279,15 +288,15 @@ namespace Flow.Launcher.Infrastructure
|
|||
{
|
||||
var hWndDesktop = PInvoke.FindWindowEx(hWnd, HWND.Null, "SHELLDLL_DefView", null);
|
||||
hWndDesktop = PInvoke.FindWindowEx(hWndDesktop, HWND.Null, "SysListView32", "FolderView");
|
||||
if (hWndDesktop.Value != IntPtr.Zero)
|
||||
if (hWndDesktop != HWND.Null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var monitorInfo = MonitorInfo.GetNearestDisplayMonitor(hWnd);
|
||||
return (appBounds.bottom - appBounds.top) == monitorInfo.RectMonitor.Height &&
|
||||
(appBounds.right - appBounds.left) == monitorInfo.RectMonitor.Width;
|
||||
return (appBounds.bottom - appBounds.top) == monitorInfo.Bounds.Height &&
|
||||
(appBounds.right - appBounds.left) == monitorInfo.Bounds.Width;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
|
@ -324,6 +333,11 @@ namespace Flow.Launcher.Infrastructure
|
|||
|
||||
public const int WM_ENTERSIZEMOVE = (int)PInvoke.WM_ENTERSIZEMOVE;
|
||||
public const int WM_EXITSIZEMOVE = (int)PInvoke.WM_EXITSIZEMOVE;
|
||||
public const int WM_NCLBUTTONDBLCLK = (int)PInvoke.WM_NCLBUTTONDBLCLK;
|
||||
public const int WM_SYSCOMMAND = (int)PInvoke.WM_SYSCOMMAND;
|
||||
|
||||
public const int SC_MAXIMIZE = (int)PInvoke.SC_MAXIMIZE;
|
||||
public const int SC_MINIMIZE = (int)PInvoke.SC_MINIMIZE;
|
||||
|
||||
#endregion
|
||||
|
||||
|
|
@ -339,6 +353,16 @@ namespace Flow.Launcher.Infrastructure
|
|||
return new(windowHelper.Handle);
|
||||
}
|
||||
|
||||
internal static HWND GetMainWindowHandle()
|
||||
{
|
||||
// When application is exiting, the Application.Current will be null
|
||||
if (Application.Current == null) return HWND.Null;
|
||||
|
||||
// Get the FL main window
|
||||
var hwnd = GetWindowHandle(Application.Current.MainWindow, true);
|
||||
return hwnd;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region STA Thread
|
||||
|
|
@ -475,7 +499,7 @@ namespace Flow.Launcher.Infrastructure
|
|||
/// Restores the previously backed-up keyboard layout.
|
||||
/// If it wasn't backed up or has already been restored, this method does nothing.
|
||||
/// </summary>
|
||||
public static void RestorePreviousKeyboardLayout()
|
||||
public unsafe static void RestorePreviousKeyboardLayout()
|
||||
{
|
||||
if (_previousLayout == HKL.Null) return;
|
||||
|
||||
|
|
@ -486,7 +510,7 @@ namespace Flow.Launcher.Infrastructure
|
|||
hwnd,
|
||||
PInvoke.WM_INPUTLANGCHANGEREQUEST,
|
||||
PInvoke.INPUTLANGCHANGE_FORWARD,
|
||||
_previousLayout.Value
|
||||
new LPARAM((nint)_previousLayout.Value)
|
||||
);
|
||||
|
||||
_previousLayout = HKL.Null;
|
||||
|
|
@ -756,6 +780,62 @@ namespace Flow.Launcher.Infrastructure
|
|||
|
||||
#endregion
|
||||
|
||||
#region Window Rect
|
||||
|
||||
public static unsafe bool GetWindowRect(nint handle, out Rect outRect)
|
||||
{
|
||||
var rect = new RECT();
|
||||
var result = PInvoke.GetWindowRect(new(handle), &rect);
|
||||
if (!result)
|
||||
{
|
||||
outRect = new Rect();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Convert RECT to Rect
|
||||
outRect = new Rect(
|
||||
rect.left,
|
||||
rect.top,
|
||||
rect.right - rect.left,
|
||||
rect.bottom - rect.top
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Window Process
|
||||
|
||||
internal static unsafe string GetProcessNameFromHwnd(HWND hWnd)
|
||||
{
|
||||
return Path.GetFileName(GetProcessPathFromHwnd(hWnd));
|
||||
}
|
||||
|
||||
internal static unsafe string GetProcessPathFromHwnd(HWND hWnd)
|
||||
{
|
||||
uint pid;
|
||||
var threadId = PInvoke.GetWindowThreadProcessId(hWnd, &pid);
|
||||
if (threadId == 0) return string.Empty;
|
||||
|
||||
var process = PInvoke.OpenProcess(PROCESS_ACCESS_RIGHTS.PROCESS_QUERY_LIMITED_INFORMATION, false, pid);
|
||||
if (process != HWND.Null)
|
||||
{
|
||||
using var safeHandle = new SafeProcessHandle((nint)process.Value, true);
|
||||
uint capacity = 2000;
|
||||
Span<char> buffer = new char[capacity];
|
||||
if (!PInvoke.QueryFullProcessImageName(safeHandle, PROCESS_NAME_FORMAT.PROCESS_NAME_WIN32, buffer, ref capacity))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return buffer[..(int)capacity].ToString();
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Explorer
|
||||
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/shlobj_core/nf-shlobj_core-shopenfolderandselectitems
|
||||
|
|
@ -786,5 +866,182 @@ namespace Flow.Launcher.Infrastructure
|
|||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Win32 Dark Mode
|
||||
|
||||
/*
|
||||
* Inspired by https://github.com/ysc3839/win32-darkmode
|
||||
*/
|
||||
|
||||
[DllImport("uxtheme.dll", EntryPoint = "#135", SetLastError = true)]
|
||||
private static extern int SetPreferredAppMode(int appMode);
|
||||
|
||||
public static void EnableWin32DarkMode(string colorScheme)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Undocumented API from Windows 10 1809
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) &&
|
||||
Environment.OSVersion.Version.Build >= 17763)
|
||||
{
|
||||
var flag = colorScheme switch
|
||||
{
|
||||
Constant.Light => 3, // ForceLight
|
||||
Constant.Dark => 2, // ForceDark
|
||||
Constant.System => 1, // AllowDark
|
||||
_ => 0 // Default
|
||||
};
|
||||
_ = SetPreferredAppMode(flag);
|
||||
}
|
||||
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore errors on unsupported OS
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region File / Folder Dialog
|
||||
|
||||
public static string SelectFile()
|
||||
{
|
||||
var dlg = new OpenFileDialog();
|
||||
var result = dlg.ShowDialog();
|
||||
if (result == true)
|
||||
return dlg.FileName;
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Sleep Mode Listener
|
||||
|
||||
private static Action _func;
|
||||
private static PDEVICE_NOTIFY_CALLBACK_ROUTINE _callback = null;
|
||||
private static DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS _recipient;
|
||||
private static SafeHandle _recipientHandle;
|
||||
private static HPOWERNOTIFY _handle = HPOWERNOTIFY.Null;
|
||||
|
||||
/// <summary>
|
||||
/// Registers a listener for sleep mode events.
|
||||
/// Inspired from: https://github.com/XKaguya/LenovoLegionToolkit
|
||||
/// https://blog.csdn.net/mochounv/article/details/114668594
|
||||
/// </summary>
|
||||
/// <param name="func"></param>
|
||||
/// <exception cref="Win32Exception"></exception>
|
||||
public static unsafe void RegisterSleepModeListener(Action func)
|
||||
{
|
||||
if (_callback != null)
|
||||
{
|
||||
// Only register if not already registered
|
||||
return;
|
||||
}
|
||||
|
||||
_func = func;
|
||||
_callback = new PDEVICE_NOTIFY_CALLBACK_ROUTINE(DeviceNotifyCallback);
|
||||
_recipient = new DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS()
|
||||
{
|
||||
Callback = _callback,
|
||||
Context = null
|
||||
};
|
||||
|
||||
_recipientHandle = new StructSafeHandle<DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS>(_recipient);
|
||||
_handle = PInvoke.PowerRegisterSuspendResumeNotification(
|
||||
REGISTER_NOTIFICATION_FLAGS.DEVICE_NOTIFY_CALLBACK,
|
||||
_recipientHandle,
|
||||
out var handle) == WIN32_ERROR.ERROR_SUCCESS ?
|
||||
new HPOWERNOTIFY(new IntPtr(handle)) :
|
||||
HPOWERNOTIFY.Null;
|
||||
if (_handle.IsNull)
|
||||
{
|
||||
throw new Win32Exception("Error registering for power notifications: " + Marshal.GetLastWin32Error());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters the sleep mode listener.
|
||||
/// </summary>
|
||||
public static void UnregisterSleepModeListener()
|
||||
{
|
||||
if (!_handle.IsNull)
|
||||
{
|
||||
PInvoke.PowerUnregisterSuspendResumeNotification(_handle);
|
||||
_handle = HPOWERNOTIFY.Null;
|
||||
_func = null;
|
||||
_callback = null;
|
||||
_recipientHandle = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe uint DeviceNotifyCallback(void* context, uint type, void* setting)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case PInvoke.PBT_APMRESUMEAUTOMATIC:
|
||||
// Operation is resuming automatically from a low-power state.This message is sent every time the system resumes
|
||||
_func?.Invoke();
|
||||
break;
|
||||
|
||||
case PInvoke.PBT_APMRESUMESUSPEND:
|
||||
// Operation is resuming from a low-power state.This message is sent after PBT_APMRESUMEAUTOMATIC if the resume is triggered by user input, such as pressing a key
|
||||
_func?.Invoke();
|
||||
break;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private sealed class StructSafeHandle<T> : SafeHandle where T : struct
|
||||
{
|
||||
private readonly nint _ptr = nint.Zero;
|
||||
|
||||
public StructSafeHandle(T recipient) : base(nint.Zero, true)
|
||||
{
|
||||
var pRecipient = Marshal.AllocHGlobal(Marshal.SizeOf<T>());
|
||||
Marshal.StructureToPtr(recipient, pRecipient, false);
|
||||
SetHandle(pRecipient);
|
||||
_ptr = pRecipient;
|
||||
}
|
||||
|
||||
public override bool IsInvalid => handle == nint.Zero;
|
||||
|
||||
protected override bool ReleaseHandle()
|
||||
{
|
||||
Marshal.FreeHGlobal(_ptr);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Taskbar
|
||||
|
||||
public static unsafe void ShowTaskbar()
|
||||
{
|
||||
// Find the taskbar window
|
||||
var taskbarHwnd = PInvoke.FindWindowEx(HWND.Null, HWND.Null, "Shell_TrayWnd", null);
|
||||
if (taskbarHwnd == HWND.Null) return;
|
||||
|
||||
// Magic from https://github.com/Oliviaophia/SmartTaskbar
|
||||
const uint TrayBarFlag = 0x05D1;
|
||||
var mon = PInvoke.MonitorFromWindow(taskbarHwnd, Windows.Win32.Graphics.Gdi.MONITOR_FROM_FLAGS.MONITOR_DEFAULTTONEAREST);
|
||||
PInvoke.PostMessage(taskbarHwnd, TrayBarFlag, new WPARAM(1), new LPARAM((nint)mon.Value));
|
||||
}
|
||||
|
||||
public static void HideTaskbar()
|
||||
{
|
||||
// Find the taskbar window
|
||||
var taskbarHwnd = PInvoke.FindWindowEx(HWND.Null, HWND.Null, "Shell_TrayWnd", null);
|
||||
if (taskbarHwnd == HWND.Null) return;
|
||||
|
||||
// Magic from https://github.com/Oliviaophia/SmartTaskbar
|
||||
const uint TrayBarFlag = 0x05D1;
|
||||
PInvoke.PostMessage(taskbarHwnd, TrayBarFlag, new WPARAM(0), IntPtr.Zero);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
|
|||
210
Flow.Launcher.Infrastructure/packages.lock.json
Normal file
210
Flow.Launcher.Infrastructure/packages.lock.json
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
{
|
||||
"version": 1,
|
||||
"dependencies": {
|
||||
"net9.0-windows7.0": {
|
||||
"Ben.Demystifier": {
|
||||
"type": "Direct",
|
||||
"requested": "[0.4.1, )",
|
||||
"resolved": "0.4.1",
|
||||
"contentHash": "axFeEMfmEORy3ipAzOXG/lE+KcNptRbei3F0C4kQCdeiQtW+qJW90K5iIovITGrdLt8AjhNCwk5qLSX9/rFpoA==",
|
||||
"dependencies": {
|
||||
"System.Reflection.Metadata": "5.0.0"
|
||||
}
|
||||
},
|
||||
"BitFaster.Caching": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.5.4, )",
|
||||
"resolved": "2.5.4",
|
||||
"contentHash": "1QroTY1PVCZOSG9FnkkCrmCKk/+bZCgI/YXq376HnYwUDJ4Ho0EaV4YaA/5v5WYLnwIwIO7RZkdWbg9pxIpueQ=="
|
||||
},
|
||||
"CommunityToolkit.Mvvm": {
|
||||
"type": "Direct",
|
||||
"requested": "[8.4.0, )",
|
||||
"resolved": "8.4.0",
|
||||
"contentHash": "tqVU8yc/ADO9oiTRyTnwhFN68hCwvkliMierptWOudIAvWY1mWCh5VFh+guwHJmpMwfg0J0rY+yyd5Oy7ty9Uw=="
|
||||
},
|
||||
"Flow.Launcher.Localization": {
|
||||
"type": "Direct",
|
||||
"requested": "[0.0.6, )",
|
||||
"resolved": "0.0.6",
|
||||
"contentHash": "WNI/TLGPDr3XdOW8gaALN0Uyz9h+bzqOaNZev2nHEuA3HW9o7XuqaM6C0PqNi96mNgxiypwWpVazBNzaylJ2Aw=="
|
||||
},
|
||||
"Fody": {
|
||||
"type": "Direct",
|
||||
"requested": "[6.9.3, )",
|
||||
"resolved": "6.9.3",
|
||||
"contentHash": "1CUGgFdyECDKgi5HaUBhdv6k+VG9Iy4OCforGfHyar3xQXAJypZkzymgKtWj/4SPd6nSG0Qi7NH71qHrDSZLaA=="
|
||||
},
|
||||
"ini-parser": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.5.2, )",
|
||||
"resolved": "2.5.2",
|
||||
"contentHash": "hp3gKmC/14+6eKLgv7Jd1Z7OV86lO+tNfOXr/stQbwmRhdQuXVSvrRAuAe7G5+lwhkov0XkqZ8/bn1PYWMx6eg=="
|
||||
},
|
||||
"InputSimulator": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.0.4, )",
|
||||
"resolved": "1.0.4",
|
||||
"contentHash": "D0LvRCPQMX6/FJHBjng+RO+wRDuHTJrfo7IAc7rmkPvRqchdVGJWg3y70peOtDy3OLNK+HSOwVkH4GiuLnkKgA=="
|
||||
},
|
||||
"MemoryPack": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.21.4, )",
|
||||
"resolved": "1.21.4",
|
||||
"contentHash": "wy3JTBNBsO8LfQcBvvYsWr3lm2Oakolrfu0UQ3oSJSEiD+7ye0GUhYTaXuYYBowqsXBXWD9gf2218ae0JRiYVQ==",
|
||||
"dependencies": {
|
||||
"MemoryPack.Core": "1.21.4",
|
||||
"MemoryPack.Generator": "1.21.4"
|
||||
}
|
||||
},
|
||||
"Microsoft.VisualStudio.Threading": {
|
||||
"type": "Direct",
|
||||
"requested": "[17.14.15, )",
|
||||
"resolved": "17.14.15",
|
||||
"contentHash": "1DrCusT3xNLSlaJg77BsUSAzrhjdZBAvvsS0PMzyPM+fGais6SnISOhqdZQop8VVMIBLsYm2gyF9W7THjgavwA==",
|
||||
"dependencies": {
|
||||
"Microsoft.VisualStudio.Threading.Analyzers": "17.14.15",
|
||||
"Microsoft.VisualStudio.Threading.Only": "17.14.15",
|
||||
"Microsoft.VisualStudio.Validation": "17.8.8"
|
||||
}
|
||||
},
|
||||
"Microsoft.Windows.CsWin32": {
|
||||
"type": "Direct",
|
||||
"requested": "[0.3.205, )",
|
||||
"resolved": "0.3.205",
|
||||
"contentHash": "U5wGAnyKd7/I2YMd43nogm81VMtjiKzZ9dsLMVI4eAB7jtv5IEj0gprj0q/F3iRmAIaGv5omOf8iSYx2+nE6BQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Windows.SDK.Win32Docs": "0.1.42-alpha",
|
||||
"Microsoft.Windows.SDK.Win32Metadata": "61.0.15-preview",
|
||||
"Microsoft.Windows.WDK.Win32Metadata": "0.12.8-experimental"
|
||||
}
|
||||
},
|
||||
"NHotkey.Wpf": {
|
||||
"type": "Direct",
|
||||
"requested": "[3.0.0, )",
|
||||
"resolved": "3.0.0",
|
||||
"contentHash": "BIUKlhTG5KtFf9OQzWvkmVmktt5/FFj6AOEgag8Uf0R2YdZt5ajUzs3sVskcJcT2TztWlEHKQr1jFj3KQ0D9Nw==",
|
||||
"dependencies": {
|
||||
"NHotkey": "3.0.0"
|
||||
}
|
||||
},
|
||||
"NLog": {
|
||||
"type": "Direct",
|
||||
"requested": "[6.0.4, )",
|
||||
"resolved": "6.0.4",
|
||||
"contentHash": "Xr+lIk1ZlTTFXEqnxQVLxrDqZlt2tm5X+/AhJbaY2emb/dVtGDiU5QuEtj3gHtwV/SWlP/rJ922I/BPuOJXlRw=="
|
||||
},
|
||||
"NLog.OutputDebugString": {
|
||||
"type": "Direct",
|
||||
"requested": "[6.0.4, )",
|
||||
"resolved": "6.0.4",
|
||||
"contentHash": "TOP2Ap9BbE98B/l/TglnguowOD0rXo8B/20xAgvj9shO/kf6IJ5M4QMhVxq72mrneJ/ANhHY7Jcd+xJbzuI5PA==",
|
||||
"dependencies": {
|
||||
"NLog": "6.0.4"
|
||||
}
|
||||
},
|
||||
"PropertyChanged.Fody": {
|
||||
"type": "Direct",
|
||||
"requested": "[4.1.0, )",
|
||||
"resolved": "4.1.0",
|
||||
"contentHash": "6v+f9cI8YjnZH2WBHuOqWEAo8DFFNGFIdU8xD3AsL6fhV6Y8oAmVWd7XKk49t8DpeUBwhR/X+97+6Epvek0Y3A==",
|
||||
"dependencies": {
|
||||
"Fody": "6.6.4"
|
||||
}
|
||||
},
|
||||
"SharpVectors.Wpf": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.8.5, )",
|
||||
"resolved": "1.8.5",
|
||||
"contentHash": "WURdBDq5AE8RjKV9pFS7lNkJe81gxja9SaMGE4URq9GJUZ6M+5DGUL0Lm3B0iYW2/Meyowaz4ffGsyW+RBSTtg=="
|
||||
},
|
||||
"System.Drawing.Common": {
|
||||
"type": "Direct",
|
||||
"requested": "[7.0.0, )",
|
||||
"resolved": "7.0.0",
|
||||
"contentHash": "KIX+oBU38pxkKPxvLcLfIkOV5Ien8ReN78wro7OF5/erwcmortzeFx+iBswlh2Vz6gVne0khocQudGwaO1Ey6A==",
|
||||
"dependencies": {
|
||||
"Microsoft.Win32.SystemEvents": "7.0.0"
|
||||
}
|
||||
},
|
||||
"ToolGood.Words.Pinyin": {
|
||||
"type": "Direct",
|
||||
"requested": "[3.1.0.3, )",
|
||||
"resolved": "3.1.0.3",
|
||||
"contentHash": "VKcf8sUq/+LyY99WgLhOu7Q32ROEyR30/2xCCj9ADRi45wVC7kpXrYCf9vH1qirkmrIfpL8inoxAbrqAlfXxsQ=="
|
||||
},
|
||||
"JetBrains.Annotations": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2025.2.2",
|
||||
"contentHash": "0X56ZRizuHdrnPpgXjWV7f2tQO1FlQg5O1967OGKnI/4ZRNOK642J8L7brM1nYvrxTTU5TP1yRyXLRLaXLPQ8A=="
|
||||
},
|
||||
"MemoryPack.Core": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.21.4",
|
||||
"contentHash": "6RszGorZ0ejNmp37ZcboPBMvvPCuNW2jlrdQfcs/lMzE5b3pmPF6hsm/laDc34hRlbAST1ZxaX/DvYu2DF5sBQ=="
|
||||
},
|
||||
"MemoryPack.Generator": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.21.4",
|
||||
"contentHash": "g14EsSS85yn0lHTi0J9ivqlZMf09A2iI51fmI+0KkzIzyCbWOBWPi5mdaY7YWmXprk12aYh9u/qfWHQUYthlwg=="
|
||||
},
|
||||
"Microsoft.VisualStudio.Threading.Analyzers": {
|
||||
"type": "Transitive",
|
||||
"resolved": "17.14.15",
|
||||
"contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw=="
|
||||
},
|
||||
"Microsoft.VisualStudio.Threading.Only": {
|
||||
"type": "Transitive",
|
||||
"resolved": "17.14.15",
|
||||
"contentHash": "NqONyw1RXyj9P3k5e1uU2k9kc1ptwuU5NJQzG+MPq7vQVHUzBY8HLuJf/N2Rw5H/myD96CVxziDxmjawPuzntw==",
|
||||
"dependencies": {
|
||||
"Microsoft.VisualStudio.Validation": "17.8.8"
|
||||
}
|
||||
},
|
||||
"Microsoft.VisualStudio.Validation": {
|
||||
"type": "Transitive",
|
||||
"resolved": "17.8.8",
|
||||
"contentHash": "rWXThIpyQd4YIXghNkiv2+VLvzS+MCMKVRDR0GAMlflsdo+YcAN2g2r5U1Ah98OFjQMRexTFtXQQ2LkajxZi3g=="
|
||||
},
|
||||
"Microsoft.Win32.SystemEvents": {
|
||||
"type": "Transitive",
|
||||
"resolved": "7.0.0",
|
||||
"contentHash": "2nXPrhdAyAzir0gLl8Yy8S5Mnm/uBSQQA7jEsILOS1MTyS7DbmV1NgViMtvV1sfCD1ebITpNwb1NIinKeJgUVQ=="
|
||||
},
|
||||
"Microsoft.Windows.SDK.Win32Docs": {
|
||||
"type": "Transitive",
|
||||
"resolved": "0.1.42-alpha",
|
||||
"contentHash": "Z/9po23gUA9aoukirh2ItMU2ZS9++Js9Gdds9fu5yuMojDrmArvY2y+tq9985tR3cxFxpZO1O35Wjfo0khj5HA=="
|
||||
},
|
||||
"Microsoft.Windows.SDK.Win32Metadata": {
|
||||
"type": "Transitive",
|
||||
"resolved": "61.0.15-preview",
|
||||
"contentHash": "cysex3dazKtCPALCluC2XX3f5Aedy9H2pw5jb+TW5uas2rkem1Z7FRnbUrg2vKx0pk0Qz+4EJNr37HdYTEcvEQ=="
|
||||
},
|
||||
"Microsoft.Windows.WDK.Win32Metadata": {
|
||||
"type": "Transitive",
|
||||
"resolved": "0.12.8-experimental",
|
||||
"contentHash": "3n8R44/Z96Ly+ty4eYVJfESqbzvpw96lRLs3zOzyDmr1x1Kw7FNn5CyE416q+bZQV3e1HRuMUvyegMeRE/WedA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Windows.SDK.Win32Metadata": "61.0.15-preview"
|
||||
}
|
||||
},
|
||||
"NHotkey": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0",
|
||||
"contentHash": "IEghs0QqWsQYH0uUmvIl0Ye6RaebWRh38eB6ToOkDnQucTYRGFOgtig0gSxlwCszTilYFz3n1ZuY762x+kDR3A=="
|
||||
},
|
||||
"System.Reflection.Metadata": {
|
||||
"type": "Transitive",
|
||||
"resolved": "5.0.0",
|
||||
"contentHash": "5NecZgXktdGg34rh1OenY1rFNDCI8xSjFr+Z4OU4cU06AQHUdRnIIEeWENu3Wl4YowbzkymAIMvi3WyK9U53pQ=="
|
||||
},
|
||||
"flow.launcher.plugin": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"JetBrains.Annotations": "[2025.2.2, )"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
92
Flow.Launcher.Plugin/DialogJumpResult.cs
Normal file
92
Flow.Launcher.Plugin/DialogJumpResult.cs
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
namespace Flow.Launcher.Plugin
|
||||
{
|
||||
/// <summary>
|
||||
/// Describes a result of a <see cref="Query"/> executed by a plugin in Dialog Jump window
|
||||
/// </summary>
|
||||
public class DialogJumpResult : Result
|
||||
{
|
||||
/// <summary>
|
||||
/// This holds the path which can be provided by plugin to be navigated to the
|
||||
/// file dialog when records in Dialog Jump window is right clicked on a result.
|
||||
/// </summary>
|
||||
public required string DialogJumpPath { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Clones the current Dialog Jump result
|
||||
/// </summary>
|
||||
public new DialogJumpResult Clone()
|
||||
{
|
||||
return new DialogJumpResult
|
||||
{
|
||||
Title = Title,
|
||||
SubTitle = SubTitle,
|
||||
ActionKeywordAssigned = ActionKeywordAssigned,
|
||||
CopyText = CopyText,
|
||||
AutoCompleteText = AutoCompleteText,
|
||||
IcoPath = IcoPath,
|
||||
BadgeIcoPath = BadgeIcoPath,
|
||||
RoundedIcon = RoundedIcon,
|
||||
Icon = Icon,
|
||||
BadgeIcon = BadgeIcon,
|
||||
Glyph = Glyph,
|
||||
Action = Action,
|
||||
AsyncAction = AsyncAction,
|
||||
Score = Score,
|
||||
TitleHighlightData = TitleHighlightData,
|
||||
OriginQuery = OriginQuery,
|
||||
PluginDirectory = PluginDirectory,
|
||||
ContextData = ContextData,
|
||||
PluginID = PluginID,
|
||||
TitleToolTip = TitleToolTip,
|
||||
SubTitleToolTip = SubTitleToolTip,
|
||||
PreviewPanel = PreviewPanel,
|
||||
ProgressBar = ProgressBar,
|
||||
ProgressBarColor = ProgressBarColor,
|
||||
Preview = Preview,
|
||||
AddSelectedCount = AddSelectedCount,
|
||||
RecordKey = RecordKey,
|
||||
ShowBadge = ShowBadge,
|
||||
DialogJumpPath = DialogJumpPath
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert <see cref="Result"/> to <see cref="DialogJumpResult"/>.
|
||||
/// </summary>
|
||||
public static DialogJumpResult From(Result result, string dialogJumpPath)
|
||||
{
|
||||
return new DialogJumpResult
|
||||
{
|
||||
Title = result.Title,
|
||||
SubTitle = result.SubTitle,
|
||||
ActionKeywordAssigned = result.ActionKeywordAssigned,
|
||||
CopyText = result.CopyText,
|
||||
AutoCompleteText = result.AutoCompleteText,
|
||||
IcoPath = result.IcoPath,
|
||||
BadgeIcoPath = result.BadgeIcoPath,
|
||||
RoundedIcon = result.RoundedIcon,
|
||||
Icon = result.Icon,
|
||||
BadgeIcon = result.BadgeIcon,
|
||||
Glyph = result.Glyph,
|
||||
Action = result.Action,
|
||||
AsyncAction = result.AsyncAction,
|
||||
Score = result.Score,
|
||||
TitleHighlightData = result.TitleHighlightData,
|
||||
OriginQuery = result.OriginQuery,
|
||||
PluginDirectory = result.PluginDirectory,
|
||||
ContextData = result.ContextData,
|
||||
PluginID = result.PluginID,
|
||||
TitleToolTip = result.TitleToolTip,
|
||||
SubTitleToolTip = result.SubTitleToolTip,
|
||||
PreviewPanel = result.PreviewPanel,
|
||||
ProgressBar = result.ProgressBar,
|
||||
ProgressBarColor = result.ProgressBarColor,
|
||||
Preview = result.Preview,
|
||||
AddSelectedCount = result.AddSelectedCount,
|
||||
RecordKey = result.RecordKey,
|
||||
ShowBadge = result.ShowBadge,
|
||||
DialogJumpPath = dialogJumpPath
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -39,7 +39,14 @@ namespace Flow.Launcher.Plugin
|
|||
/// <param name="sender"></param>
|
||||
/// <param name="args"></param>
|
||||
public delegate void VisibilityChangedEventHandler(object sender, VisibilityChangedEventArgs args);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A delegate for when the actual application theme is changed
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="args"></param>
|
||||
public delegate void ActualApplicationThemeChangedEventHandler(object sender, ActualApplicationThemeChangedEventArgs args);
|
||||
|
||||
/// <summary>
|
||||
/// The event args for <see cref="VisibilityChangedEventHandler"/>
|
||||
/// </summary>
|
||||
|
|
@ -77,4 +84,15 @@ namespace Flow.Launcher.Plugin
|
|||
/// </summary>
|
||||
public Query Query { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The event args for <see cref="ActualApplicationThemeChangedEventHandler"/>
|
||||
/// </summary>
|
||||
public class ActualApplicationThemeChangedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// <see langword="true"/> if the application has changed actual theme
|
||||
/// </summary>
|
||||
public bool IsDark { get; init; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0-windows</TargetFramework>
|
||||
<TargetFramework>net9.0-windows</TargetFramework>
|
||||
<ProjectGuid>{8451ECDD-2EA4-4966-BB0A-7BBC40138E80}</ProjectGuid>
|
||||
<UseWPF>true</UseWPF>
|
||||
<OutputType>Library</OutputType>
|
||||
|
|
@ -11,13 +11,14 @@
|
|||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<Version>4.5.0</Version>
|
||||
<PackageVersion>4.5.0</PackageVersion>
|
||||
<AssemblyVersion>4.5.0</AssemblyVersion>
|
||||
<FileVersion>4.5.0</FileVersion>
|
||||
<Version>5.2.0</Version>
|
||||
<PackageVersion>5.2.0</PackageVersion>
|
||||
<AssemblyVersion>5.2.0</AssemblyVersion>
|
||||
<FileVersion>5.2.0</FileVersion>
|
||||
<PackageId>Flow.Launcher.Plugin</PackageId>
|
||||
<Authors>Flow-Launcher</Authors>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
|
|
@ -27,6 +28,7 @@
|
|||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<PackageReadmeFile>Readme.md</PackageReadmeFile>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(APPVEYOR)' == 'True'">
|
||||
|
|
@ -66,17 +68,17 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Fody" Version="6.5.5">
|
||||
<PackageReference Include="Fody" Version="6.9.3">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2024.3.0" />
|
||||
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.106">
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="10.0.103" PrivateAssets="All" />
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2025.2.2" />
|
||||
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.269">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0">
|
||||
<PackageReference Include="PropertyChanged.Fody" Version="4.1.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
|
|
|||
24
Flow.Launcher.Plugin/Interfaces/IAsyncDialogJump.cs
Normal file
24
Flow.Launcher.Plugin/Interfaces/IAsyncDialogJump.cs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
|
||||
namespace Flow.Launcher.Plugin
|
||||
{
|
||||
/// <summary>
|
||||
/// Asynchronous Dialog Jump Model
|
||||
/// </summary>
|
||||
public interface IAsyncDialogJump : IFeatures
|
||||
{
|
||||
/// <summary>
|
||||
/// Asynchronous querying for Dialog Jump window
|
||||
/// </summary>
|
||||
/// <para>
|
||||
/// If the Querying method requires high IO transmission
|
||||
/// or performing CPU intense jobs (performing better with cancellation), please use this IAsyncDialogJump interface
|
||||
/// </para>
|
||||
/// <param name="query">Query to search</param>
|
||||
/// <param name="token">Cancel when querying job is obsolete</param>
|
||||
/// <returns></returns>
|
||||
Task<List<DialogJumpResult>> QueryDialogJumpAsync(Query query, CancellationToken token);
|
||||
}
|
||||
}
|
||||
29
Flow.Launcher.Plugin/Interfaces/IDialogJump.cs
Normal file
29
Flow.Launcher.Plugin/Interfaces/IDialogJump.cs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Flow.Launcher.Plugin
|
||||
{
|
||||
/// <summary>
|
||||
/// Synchronous Dialog Jump Model
|
||||
/// <para>
|
||||
/// If the Querying method requires high IO transmission
|
||||
/// or performing CPU intense jobs (performing better with cancellation), please try the IAsyncDialogJump interface
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public interface IDialogJump : IAsyncDialogJump
|
||||
{
|
||||
/// <summary>
|
||||
/// Querying for Dialog Jump window
|
||||
/// <para>
|
||||
/// This method will be called within a Task.Run,
|
||||
/// so please avoid synchrously wait for long.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="query">Query to search</param>
|
||||
/// <returns></returns>
|
||||
List<DialogJumpResult> QueryDialogJump(Query query);
|
||||
|
||||
Task<List<DialogJumpResult>> IAsyncDialogJump.QueryDialogJumpAsync(Query query, CancellationToken token) => Task.Run(() => QueryDialogJump(query), token);
|
||||
}
|
||||
}
|
||||
96
Flow.Launcher.Plugin/Interfaces/IDialogJumpDialog.cs
Normal file
96
Flow.Launcher.Plugin/Interfaces/IDialogJumpDialog.cs
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
using System;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Flow.Launcher.Plugin
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for handling file dialog instances in DialogJump.
|
||||
/// </summary>
|
||||
public interface IDialogJumpDialog : IFeatures, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Check if the foreground window is a file dialog instance.
|
||||
/// </summary>
|
||||
/// <param name="hwnd">
|
||||
/// The handle of the foreground window to check.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// The window if the foreground window is a file dialog instance. Null if it is not.
|
||||
/// </returns>
|
||||
IDialogJumpDialogWindow? CheckDialogWindow(IntPtr hwnd);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for handling a specific file dialog window in DialogJump.
|
||||
/// </summary>
|
||||
public interface IDialogJumpDialogWindow : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The handle of the dialog window.
|
||||
/// </summary>
|
||||
IntPtr Handle { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get the current tab of the dialog window.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
IDialogJumpDialogWindowTab GetCurrentTab();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for handling a specific tab in a file dialog window in DialogJump.
|
||||
/// </summary>
|
||||
public interface IDialogJumpDialogWindowTab : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The handle of the dialog tab.
|
||||
/// </summary>
|
||||
IntPtr Handle { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get the current folder path of the dialog tab.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
string GetCurrentFolder();
|
||||
|
||||
/// <summary>
|
||||
/// Get the current file of the dialog tab.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
string GetCurrentFile();
|
||||
|
||||
/// <summary>
|
||||
/// Jump to a folder in the dialog tab.
|
||||
/// </summary>
|
||||
/// <param name="path">
|
||||
/// The path to the folder to jump to.
|
||||
/// </param>
|
||||
/// <param name="auto">
|
||||
/// Whether folder jump is under automatical mode.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// True if the jump was successful, false otherwise.
|
||||
/// </returns>
|
||||
bool JumpFolder(string path, bool auto);
|
||||
|
||||
/// <summary>
|
||||
/// Jump to a file in the dialog tab.
|
||||
/// </summary>
|
||||
/// <param name="path">
|
||||
/// The path to the file to jump to.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// True if the jump was successful, false otherwise.
|
||||
/// </returns>
|
||||
bool JumpFile(string path);
|
||||
|
||||
/// <summary>
|
||||
/// Open the file in the dialog tab.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// True if the file was opened successfully, false otherwise.
|
||||
/// </returns>
|
||||
bool Open();
|
||||
}
|
||||
}
|
||||
40
Flow.Launcher.Plugin/Interfaces/IDialogJumpExplorer.cs
Normal file
40
Flow.Launcher.Plugin/Interfaces/IDialogJumpExplorer.cs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
using System;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Flow.Launcher.Plugin
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for handling file explorer instances in DialogJump.
|
||||
/// </summary>
|
||||
public interface IDialogJumpExplorer : IFeatures, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Check if the foreground window is a Windows Explorer instance.
|
||||
/// </summary>
|
||||
/// <param name="hwnd">
|
||||
/// The handle of the foreground window to check.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// The window if the foreground window is a file explorer instance. Null if it is not.
|
||||
/// </returns>
|
||||
IDialogJumpExplorerWindow? CheckExplorerWindow(IntPtr hwnd);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for handling a specific file explorer window in DialogJump.
|
||||
/// </summary>
|
||||
public interface IDialogJumpExplorerWindow : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The handle of the explorer window.
|
||||
/// </summary>
|
||||
IntPtr Handle { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get the current folder path of the explorer window.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
string? GetExplorerPath();
|
||||
}
|
||||
}
|
||||
|
|
@ -32,6 +32,6 @@ namespace Flow.Launcher.Plugin
|
|||
|
||||
Task IAsyncPlugin.InitAsync(PluginInitContext context) => Task.Run(() => Init(context));
|
||||
|
||||
Task<List<Result>> IAsyncPlugin.QueryAsync(Query query, CancellationToken token) => Task.Run(() => Query(query));
|
||||
Task<List<Result>> IAsyncPlugin.QueryAsync(Query query, CancellationToken token) => Task.Run(() => Query(query), token);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,8 +23,8 @@ namespace Flow.Launcher.Plugin
|
|||
/// </summary>
|
||||
/// <param name="query">query text</param>
|
||||
/// <param name="requery">
|
||||
/// Force requery. By default, Flow Launcher will not fire query if your query is same with existing one.
|
||||
/// Set this to <see langword="true"/> to force Flow Launcher requerying
|
||||
/// Force requery. By default, Flow Launcher will not fire query if your query is same with existing one.
|
||||
/// Set this to <see langword="true"/> to force Flow Launcher re-querying
|
||||
/// </param>
|
||||
void ChangeQuery(string query, bool requery = false);
|
||||
|
||||
|
|
@ -49,7 +49,7 @@ namespace Flow.Launcher.Plugin
|
|||
/// </summary>
|
||||
/// <param name="text">Text to save on clipboard</param>
|
||||
/// <param name="directCopy">When true it will directly copy the file/folder from the path specified in text</param>
|
||||
/// <param name="showDefaultNotification">Whether to show the default notification from this method after copy is done.
|
||||
/// <param name="showDefaultNotification">Whether to show the default notification from this method after copy is done.
|
||||
/// It will show file/folder/text is copied successfully.
|
||||
/// Turn this off to show your own notification after copy is done.</param>>
|
||||
public void CopyToClipboard(string text, bool directCopy = false, bool showDefaultNotification = true);
|
||||
|
|
@ -65,7 +65,7 @@ namespace Flow.Launcher.Plugin
|
|||
void SavePluginSettings();
|
||||
|
||||
/// <summary>
|
||||
/// Reloads any Plugins that have the
|
||||
/// Reloads any Plugins that have the
|
||||
/// IReloadable implemented. It refeshes
|
||||
/// Plugin's in memory data with new content
|
||||
/// added by user.
|
||||
|
|
@ -84,11 +84,20 @@ namespace Flow.Launcher.Plugin
|
|||
/// <param name="subTitle">Optional message subtitle</param>
|
||||
void ShowMsgError(string title, string subTitle = "");
|
||||
|
||||
/// <summary>
|
||||
/// Show the error message using Flow's standard error icon.
|
||||
/// </summary>
|
||||
/// <param name="title">Message title</param>
|
||||
/// <param name="buttonText">Message button content</param>
|
||||
/// <param name="buttonAction">Message button action</param>
|
||||
/// <param name="subTitle">Optional message subtitle</param>
|
||||
void ShowMsgErrorWithButton(string title, string buttonText, Action buttonAction, string subTitle = "");
|
||||
|
||||
/// <summary>
|
||||
/// Show the MainWindow when hiding
|
||||
/// </summary>
|
||||
void ShowMainWindow();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Focus the query text box in the main window
|
||||
/// </summary>
|
||||
|
|
@ -106,7 +115,7 @@ namespace Flow.Launcher.Plugin
|
|||
bool IsMainWindowVisible();
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when the visibility of the main window has changed. Currently, the plugin will continue to be subscribed even if it is turned off.
|
||||
/// Invoked when the visibility of the main window has changed. Currently, the plugin will continue to be subscribed even if it is turned off.
|
||||
/// </summary>
|
||||
event VisibilityChangedEventHandler VisibilityChanged;
|
||||
|
||||
|
|
@ -127,6 +136,27 @@ namespace Flow.Launcher.Plugin
|
|||
/// <param name="useMainWindowAsOwner">when true will use main windows as the owner</param>
|
||||
void ShowMsg(string title, string subTitle, string iconPath, bool useMainWindowAsOwner = true);
|
||||
|
||||
/// <summary>
|
||||
/// Show message box with button
|
||||
/// </summary>
|
||||
/// <param name="title">Message title</param>
|
||||
/// <param name="buttonText">Message button content</param>
|
||||
/// <param name="buttonAction">Message button action</param>
|
||||
/// <param name="subTitle">Message subtitle</param>
|
||||
/// <param name="iconPath">Message icon path (relative path to your plugin folder)</param>
|
||||
void ShowMsgWithButton(string title, string buttonText, Action buttonAction, string subTitle = "", string iconPath = "");
|
||||
|
||||
/// <summary>
|
||||
/// Show message box with button
|
||||
/// </summary>
|
||||
/// <param name="title">Message title</param>
|
||||
/// <param name="buttonText">Message button content</param>
|
||||
/// <param name="buttonAction">Message button action</param>
|
||||
/// <param name="subTitle">Message subtitle</param>
|
||||
/// <param name="iconPath">Message icon path (relative path to your plugin folder)</param>
|
||||
/// <param name="useMainWindowAsOwner">when true will use main windows as the owner</param>
|
||||
void ShowMsgWithButton(string title, string buttonText, Action buttonAction, string subTitle, string iconPath, bool useMainWindowAsOwner = true);
|
||||
|
||||
/// <summary>
|
||||
/// Open setting dialog
|
||||
/// </summary>
|
||||
|
|
@ -141,11 +171,23 @@ namespace Flow.Launcher.Plugin
|
|||
string GetTranslation(string key);
|
||||
|
||||
/// <summary>
|
||||
/// Get all loaded plugins
|
||||
/// Get all loaded plugins
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Will also return any plugins not fully initialized yet
|
||||
/// </remarks>
|
||||
/// <returns></returns>
|
||||
List<PluginPair> GetAllPlugins();
|
||||
|
||||
/// <summary>
|
||||
/// Get all initialized plugins
|
||||
/// </summary>
|
||||
/// <param name="includeFailed">
|
||||
/// Whether to include plugins that failed to initialize
|
||||
/// </param>
|
||||
/// <returns></returns>
|
||||
List<PluginPair> GetAllInitializedPlugins(bool includeFailed);
|
||||
|
||||
/// <summary>
|
||||
/// Registers a callback function for global keyboard events.
|
||||
/// </summary>
|
||||
|
|
@ -199,7 +241,7 @@ namespace Flow.Launcher.Plugin
|
|||
MatchResult FuzzySearch(string query, string stringToCompare);
|
||||
|
||||
/// <summary>
|
||||
/// Http download the spefic url and return as string
|
||||
/// Http download the specific url and return as string
|
||||
/// </summary>
|
||||
/// <param name="url">URL to call Http Get</param>
|
||||
/// <param name="token">Cancellation Token</param>
|
||||
|
|
@ -207,7 +249,7 @@ namespace Flow.Launcher.Plugin
|
|||
Task<string> HttpGetStringAsync(string url, CancellationToken token = default);
|
||||
|
||||
/// <summary>
|
||||
/// Http download the spefic url and return as stream
|
||||
/// Http download the specific url and return as stream
|
||||
/// </summary>
|
||||
/// <param name="url">URL to call Http Get</param>
|
||||
/// <param name="token">Cancellation Token</param>
|
||||
|
|
@ -275,8 +317,8 @@ namespace Flow.Launcher.Plugin
|
|||
void LogError(string className, string message, [CallerMemberName] string methodName = "");
|
||||
|
||||
/// <summary>
|
||||
/// Log an Exception. Will throw if in debug mode so developer will be aware,
|
||||
/// otherwise logs the eror message. This is the primary logging method used for Flow
|
||||
/// Log an Exception. Will throw if in debug mode so developer will be aware,
|
||||
/// otherwise logs the eror message. This is the primary logging method used for Flow
|
||||
/// </summary>
|
||||
void LogException(string className, string message, Exception e, [CallerMemberName] string methodName = "");
|
||||
|
||||
|
|
@ -306,13 +348,28 @@ namespace Flow.Launcher.Plugin
|
|||
public void OpenDirectory(string DirectoryPath, string FileNameOrFilePath = null);
|
||||
|
||||
/// <summary>
|
||||
/// Opens the URL with the given Uri object.
|
||||
/// Opens the URL using the browser with the given Uri object, even if the URL is a local file.
|
||||
/// The browser and mode used is based on what's configured in Flow's default browser settings.
|
||||
/// </summary>
|
||||
public void OpenWebUrl(Uri url, bool? inPrivate = null);
|
||||
|
||||
/// <summary>
|
||||
/// Opens the URL using the browser with the given string, even if the URL is a local file.
|
||||
/// The browser and mode used is based on what's configured in Flow's default browser settings.
|
||||
/// Non-C# plugins should use this method.
|
||||
/// </summary>
|
||||
public void OpenWebUrl(string url, bool? inPrivate = null);
|
||||
|
||||
/// <summary>
|
||||
/// Opens the URL with the given Uri object in browser if scheme is Http or Https.
|
||||
/// If the URL is a local file, it will instead be opened with the default application for that file type.
|
||||
/// The browser and mode used is based on what's configured in Flow's default browser settings.
|
||||
/// </summary>
|
||||
public void OpenUrl(Uri url, bool? inPrivate = null);
|
||||
|
||||
/// <summary>
|
||||
/// Opens the URL with the given string.
|
||||
/// Opens the URL with the given string in browser if scheme is Http or Https.
|
||||
/// If the URL is a local file, it will instead be opened with the default application for that file type.
|
||||
/// The browser and mode used is based on what's configured in Flow's default browser settings.
|
||||
/// Non-C# plugins should use this method.
|
||||
/// </summary>
|
||||
|
|
@ -348,7 +405,7 @@ namespace Flow.Launcher.Plugin
|
|||
|
||||
/// <summary>
|
||||
/// Reloads the query.
|
||||
/// When current results are from context menu or history, it will go back to query results before requerying.
|
||||
/// When current results are from context menu or history, it will go back to query results before re-querying.
|
||||
/// </summary>
|
||||
/// <param name="reselect">Choose the first result after reload if true; keep the last selected result if false. Default is true.</param>
|
||||
public void ReQuery(bool reselect = true);
|
||||
|
|
@ -502,8 +559,10 @@ namespace Flow.Launcher.Plugin
|
|||
/// <param name="zipFilePath">
|
||||
/// Path to the zip file containing the plugin. It will be unzipped to the temporary directory, removed and installed.
|
||||
/// </param>
|
||||
/// <returns></returns>
|
||||
public Task UpdatePluginAsync(PluginMetadata pluginMetadata, UserPlugin plugin, string zipFilePath);
|
||||
/// <returns>
|
||||
/// True if the plugin is updated successfully, false otherwise.
|
||||
/// </returns>
|
||||
public Task<bool> UpdatePluginAsync(PluginMetadata pluginMetadata, UserPlugin plugin, string zipFilePath);
|
||||
|
||||
/// <summary>
|
||||
/// Install a plugin. By default will remove the zip file if installation is from url,
|
||||
|
|
@ -513,7 +572,10 @@ namespace Flow.Launcher.Plugin
|
|||
/// <param name="zipFilePath">
|
||||
/// Path to the zip file containing the plugin. It will be unzipped to the temporary directory, removed and installed.
|
||||
/// </param>
|
||||
public void InstallPlugin(UserPlugin plugin, string zipFilePath);
|
||||
/// <returns>
|
||||
/// True if the plugin is installed successfully, false otherwise.
|
||||
/// </returns>
|
||||
public bool InstallPlugin(UserPlugin plugin, string zipFilePath);
|
||||
|
||||
/// <summary>
|
||||
/// Uninstall a plugin
|
||||
|
|
@ -522,8 +584,10 @@ namespace Flow.Launcher.Plugin
|
|||
/// <param name="removePluginSettings">
|
||||
/// Plugin has their own settings. If this is set to true, the plugin settings will be removed.
|
||||
/// </param>
|
||||
/// <returns></returns>
|
||||
public Task UninstallPluginAsync(PluginMetadata pluginMetadata, bool removePluginSettings = false);
|
||||
/// <returns>
|
||||
/// True if the plugin is updated successfully, false otherwise.
|
||||
/// </returns>
|
||||
public Task<bool> UninstallPluginAsync(PluginMetadata pluginMetadata, bool removePluginSettings = false);
|
||||
|
||||
/// <summary>
|
||||
/// Log debug message of the time taken to execute a method
|
||||
|
|
@ -550,5 +614,28 @@ namespace Flow.Launcher.Plugin
|
|||
/// </summary>
|
||||
/// <returns>The time taken to execute the method in milliseconds</returns>
|
||||
public Task<long> StopwatchLogInfoAsync(string className, string message, Func<Task> action, [CallerMemberName] string methodName = "");
|
||||
|
||||
/// <summary>
|
||||
/// Representing whether the application is using a dark theme
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
bool IsApplicationDarkTheme();
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when the actual theme of the application has changed. Currently, the plugin will continue to be subscribed even if it is turned off.
|
||||
/// </summary>
|
||||
event ActualApplicationThemeChangedEventHandler ActualApplicationThemeChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Get the user data directory of Flow Launcher.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
string GetDataDirectory();
|
||||
|
||||
/// <summary>
|
||||
/// Get the log directory of Flow Launcher.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
string GetLogDirectory();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,4 +5,12 @@ GetWindowTextLength
|
|||
WM_KEYDOWN
|
||||
WM_KEYUP
|
||||
WM_SYSKEYDOWN
|
||||
WM_SYSKEYUP
|
||||
WM_SYSKEYUP
|
||||
|
||||
GetSystemMetrics
|
||||
EnumDisplayMonitors
|
||||
MonitorFromWindow
|
||||
GetMonitorInfo
|
||||
MONITORINFOEXW
|
||||
GetCursorPos
|
||||
MonitorFromPoint
|
||||
|
|
@ -137,6 +137,11 @@ namespace Flow.Launcher.Plugin
|
|||
[JsonIgnore]
|
||||
public int QueryCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The minimum Flow Launcher version required for this plugin. Default is "".
|
||||
/// </summary>
|
||||
public string MinimumAppVersion { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The path to the plugin settings directory which is not validated.
|
||||
/// It is used to store plugin settings files and data files.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Text.Json.Serialization;
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Flow.Launcher.Plugin
|
||||
{
|
||||
|
|
@ -8,11 +9,29 @@ namespace Flow.Launcher.Plugin
|
|||
public class Query
|
||||
{
|
||||
/// <summary>
|
||||
/// Raw query, this includes action keyword if it has.
|
||||
/// It has handled buildin custom query shortkeys and build-in shortcuts, and it trims the whitespace.
|
||||
/// We didn't recommend use this property directly. You should always use Search property.
|
||||
/// Original query, exactly how the user has typed into the search box.
|
||||
/// We don't recommend using this property directly. You should always use Search property.
|
||||
/// </summary>
|
||||
public string RawQuery { get; internal init; }
|
||||
public string OriginalQuery { get; internal init; }
|
||||
|
||||
/// <summary>
|
||||
/// Raw query, this includes action keyword if it has.
|
||||
/// It has handled built-in custom query hotkeys and built-in shortcuts, and it trims the whitespace.
|
||||
/// We don't recommend using this property directly. You should always use Search property.
|
||||
/// </summary>
|
||||
[Obsolete("RawQuery is renamed to TrimmedQuery. This property will be removed. Update the code to use TrimmedQuery instead.")]
|
||||
public string RawQuery {
|
||||
get => TrimmedQuery;
|
||||
internal init { TrimmedQuery = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Original query but with trimmed whitespace. Includes action keyword.
|
||||
/// It has handled built-in custom query hotkeys and build-in shortcuts.
|
||||
/// If you need the exact original query from the search box, use OriginalQuery property instead.
|
||||
/// We don't recommend using this property directly. You should always use Search property.
|
||||
/// </summary>
|
||||
public string TrimmedQuery { get; internal init; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the query was forced to execute again.
|
||||
|
|
@ -28,7 +47,7 @@ namespace Flow.Launcher.Plugin
|
|||
|
||||
/// <summary>
|
||||
/// Search part of a query.
|
||||
/// This will not include action keyword if exclusive plugin gets it, otherwise it should be same as RawQuery.
|
||||
/// This will not include action keyword if exclusive plugin gets it, otherwise it should be same as TrimmedQuery.
|
||||
/// Since we allow user to switch a exclusive plugin to generic plugin,
|
||||
/// so this property will always give you the "real" query part of the query
|
||||
/// </summary>
|
||||
|
|
@ -103,6 +122,6 @@ namespace Flow.Launcher.Plugin
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() => RawQuery;
|
||||
public override string ToString() => TrimmedQuery;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,13 @@ using System.IO;
|
|||
using System.Threading.Tasks;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Flow.Launcher.Plugin
|
||||
{
|
||||
/// <summary>
|
||||
/// Describes a result of a <see cref="Query"/> executed by a plugin
|
||||
/// Describes a result of a <see cref="Query"/> executed by a plugin.
|
||||
/// This or its child classes is serializable.
|
||||
/// </summary>
|
||||
public class Result
|
||||
{
|
||||
|
|
@ -21,6 +23,8 @@ namespace Flow.Launcher.Plugin
|
|||
|
||||
private string _icoPath;
|
||||
|
||||
private string _icoPathAbsolute;
|
||||
|
||||
private string _copyText = string.Empty;
|
||||
|
||||
private string _badgeIcoPath;
|
||||
|
|
@ -57,19 +61,34 @@ namespace Flow.Launcher.Plugin
|
|||
/// for user on the plugin result. If autocomplete action for example is tab, pressing tab will have
|
||||
/// the default constructed autocomplete text (result's Title), or the text provided here if not empty.
|
||||
/// </summary>
|
||||
/// <remarks>When a value is not set, the <see cref="Title"/> will be used.</remarks>
|
||||
/// <remarks>
|
||||
/// When a value is not set, the <see cref="Title"/> will be used.
|
||||
/// Please include the action keyword prefix when necessary because Flow does not prepend it automatically.
|
||||
/// </remarks>
|
||||
public string AutoCompleteText { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The image to be displayed for the result.
|
||||
/// Path or URI to the icon image for this result.
|
||||
/// Updates <see cref="IcoPathAbsolute"/> appropriately when set.
|
||||
/// </summary>
|
||||
/// <value>Can be a local file path or a URL.</value>
|
||||
/// <remarks>GlyphInfo is prioritized if not null</remarks>
|
||||
/// <remarks>
|
||||
/// Preferred usage: provide a path relative to the plugin directory (for example: "Images\icon.png").
|
||||
/// Because <see cref="IcoPath"/> is serialized, using relative paths keeps the icon reference portable
|
||||
/// when Flow is moved.
|
||||
///
|
||||
/// Accepted formats:
|
||||
/// - Relative file paths (resolved against <see cref="PluginDirectory"/> into <see cref="IcoPathAbsolute"/>)
|
||||
/// - Absolute file paths (left as-is)
|
||||
/// - HTTP/HTTPS URLs (left as-is)
|
||||
/// - Data URIs (left as-is)
|
||||
/// </remarks>
|
||||
public string IcoPath
|
||||
{
|
||||
get => _icoPath;
|
||||
set
|
||||
{
|
||||
_icoPath = value;
|
||||
|
||||
// As a standard this property will handle prepping and converting to absolute local path for icon image processing
|
||||
if (!string.IsNullOrEmpty(value)
|
||||
&& !string.IsNullOrEmpty(PluginDirectory)
|
||||
|
|
@ -78,15 +97,23 @@ namespace Flow.Launcher.Plugin
|
|||
&& !value.StartsWith("https://", StringComparison.OrdinalIgnoreCase)
|
||||
&& !value.StartsWith("data:image", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_icoPath = Path.Combine(PluginDirectory, value);
|
||||
_icoPathAbsolute = Path.Combine(PluginDirectory, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
_icoPath = value;
|
||||
_icoPathAbsolute = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Absolute path or URI which is used to load and display the result icon for Flow.
|
||||
/// This is populated by the <see cref="IcoPath"/> setter.
|
||||
/// If a relative path was provided to <see cref="IcoPath"/>, this property will contain the resolved
|
||||
/// absolute local path after combining with <see cref="PluginDirectory"/>.
|
||||
/// </summary>
|
||||
public string IcoPathAbsolute => _icoPathAbsolute;
|
||||
|
||||
/// <summary>
|
||||
/// The image to be displayed for the badge of the result.
|
||||
/// </summary>
|
||||
|
|
@ -128,17 +155,34 @@ namespace Flow.Launcher.Plugin
|
|||
/// <summary>
|
||||
/// Delegate to load an icon for this result.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public IconDelegate Icon = null;
|
||||
|
||||
/// <summary>
|
||||
/// Delegate to load an icon for the badge of this result.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public IconDelegate BadgeIcon = null;
|
||||
|
||||
private GlyphInfo _glyph;
|
||||
|
||||
/// <summary>
|
||||
/// Information for Glyph Icon (Prioritized than IcoPath/Icon if user enable Glyph Icons)
|
||||
/// </summary>
|
||||
public GlyphInfo Glyph { get; init; }
|
||||
public GlyphInfo Glyph
|
||||
{
|
||||
get => _glyph;
|
||||
init => _glyph = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the Glyph Icon after initialization
|
||||
/// </summary>
|
||||
/// <param name="glyph"></param>
|
||||
public void SetGlyph(GlyphInfo glyph)
|
||||
{
|
||||
_glyph = glyph;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An action to take in the form of a function call when the result has been selected.
|
||||
|
|
@ -148,6 +192,7 @@ namespace Flow.Launcher.Plugin
|
|||
/// Its result determines what happens to Flow Launcher's query form:
|
||||
/// when true, the form will be hidden; when false, it will stay in focus.
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public Func<ActionContext, bool> Action { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -158,6 +203,7 @@ namespace Flow.Launcher.Plugin
|
|||
/// Its result determines what happens to Flow Launcher's query form:
|
||||
/// when true, the form will be hidden; when false, it will stay in focus.
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public Func<ActionContext, ValueTask<bool>> AsyncAction { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -200,11 +246,13 @@ namespace Flow.Launcher.Plugin
|
|||
/// <example>
|
||||
/// As external information for ContextMenu
|
||||
/// </example>
|
||||
[JsonIgnore]
|
||||
public object ContextData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Plugin ID that generated this result
|
||||
/// </summary>
|
||||
[JsonInclude]
|
||||
public string PluginID { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -220,6 +268,7 @@ namespace Flow.Launcher.Plugin
|
|||
/// <summary>
|
||||
/// Customized Preview Panel
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public Lazy<UserControl> PreviewPanel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -257,6 +306,17 @@ namespace Flow.Launcher.Plugin
|
|||
/// </summary>
|
||||
public bool ShowBadge { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// This holds the text which can be shown as a query suggestion.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When a value is not set, the <see cref="Title"/> will be used.
|
||||
/// Do not include the action keyword prefix because Flow prepends it automatically.
|
||||
/// If the it does not start with the query text, it will not be shown as a suggestion.
|
||||
/// So make sure to set this value to start with the query text.
|
||||
/// </remarks>
|
||||
public string QuerySuggestionText { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Run this result, asynchronously
|
||||
/// </summary>
|
||||
|
|
@ -308,6 +368,7 @@ namespace Flow.Launcher.Plugin
|
|||
AddSelectedCount = AddSelectedCount,
|
||||
RecordKey = RecordKey,
|
||||
ShowBadge = ShowBadge,
|
||||
QuerySuggestionText = QuerySuggestionText
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -337,6 +398,7 @@ namespace Flow.Launcher.Plugin
|
|||
/// <summary>
|
||||
/// Delegate to get the preview panel's image
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public IconDelegate PreviewDelegate { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -130,6 +130,119 @@ namespace Flow.Launcher.Plugin.SharedCommands
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to delete a directory robustly with retry logic for locked files.
|
||||
/// This method tries to delete files individually with retries, then removes empty directories.
|
||||
/// Returns true if the directory was completely deleted, false if some files/folders remain.
|
||||
/// </summary>
|
||||
/// <param name="path">The directory path to delete</param>
|
||||
/// <param name="maxRetries">Maximum number of retry attempts for locked files (default: 3)</param>
|
||||
/// <param name="retryDelayMs">Delay in milliseconds between retries (default: 100ms)</param>
|
||||
/// <returns>True if directory was fully deleted, false if some items remain</returns>
|
||||
public static bool TryDeleteDirectoryRobust(string path, int maxRetries = 3, int retryDelayMs = 100)
|
||||
{
|
||||
if (!Directory.Exists(path))
|
||||
return true;
|
||||
|
||||
bool fullyDeleted = true;
|
||||
|
||||
try
|
||||
{
|
||||
// First, try to delete all files in the directory tree
|
||||
var files = Directory.GetFiles(path, "*", SearchOption.AllDirectories);
|
||||
foreach (var file in files)
|
||||
{
|
||||
bool fileDeleted = false;
|
||||
for (int attempt = 0; attempt <= maxRetries; attempt++)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Remove read-only attribute if present
|
||||
var fileInfo = new FileInfo(file);
|
||||
if (fileInfo.Exists && fileInfo.IsReadOnly)
|
||||
{
|
||||
fileInfo.IsReadOnly = false;
|
||||
}
|
||||
|
||||
File.Delete(file);
|
||||
fileDeleted = true;
|
||||
break;
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
// File is in use or access denied, wait and retry
|
||||
if (attempt < maxRetries)
|
||||
{
|
||||
System.Threading.Thread.Sleep(retryDelayMs);
|
||||
}
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
// File is in use, wait and retry
|
||||
if (attempt < maxRetries)
|
||||
{
|
||||
System.Threading.Thread.Sleep(retryDelayMs);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Other exceptions, don't retry
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!fileDeleted)
|
||||
{
|
||||
fullyDeleted = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Then, try to delete all empty directories (from deepest to shallowest)
|
||||
var directories = Directory.GetDirectories(path, "*", SearchOption.AllDirectories)
|
||||
.OrderByDescending(d => d.Length) // Delete deeper directories first
|
||||
.ToArray();
|
||||
|
||||
foreach (var directory in directories)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(directory) && !Directory.EnumerateFileSystemEntries(directory).Any())
|
||||
{
|
||||
Directory.Delete(directory, false);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If we can't delete an empty directory, mark as not fully deleted
|
||||
fullyDeleted = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, try to delete the root directory itself
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(path) && !Directory.EnumerateFileSystemEntries(path).Any())
|
||||
{
|
||||
Directory.Delete(path, false);
|
||||
}
|
||||
else if (Directory.Exists(path))
|
||||
{
|
||||
fullyDeleted = false;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
fullyDeleted = false;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
fullyDeleted = false;
|
||||
}
|
||||
|
||||
return fullyDeleted;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a directory exists
|
||||
/// </summary>
|
||||
|
|
@ -150,6 +263,16 @@ namespace Flow.Launcher.Plugin.SharedCommands
|
|||
return File.Exists(filePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a file or directory exists
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
/// <returns></returns>
|
||||
public static bool FileOrLocationExists(this string path)
|
||||
{
|
||||
return LocationExists(path) || FileExists(path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Open a directory window (using the OS's default handler, usually explorer)
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
using Microsoft.Win32;
|
||||
using System;
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace Flow.Launcher.Plugin.SharedCommands
|
||||
{
|
||||
|
|
@ -13,7 +14,7 @@ namespace Flow.Launcher.Plugin.SharedCommands
|
|||
{
|
||||
private static string GetDefaultBrowserPath()
|
||||
{
|
||||
string name = string.Empty;
|
||||
var name = string.Empty;
|
||||
try
|
||||
{
|
||||
using var regDefault = Registry.CurrentUser.OpenSubKey("Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\http\\UserChoice", false);
|
||||
|
|
@ -23,8 +24,7 @@ namespace Flow.Launcher.Plugin.SharedCommands
|
|||
name = regKey.GetValue(null).ToString().ToLower().Replace("\"", "");
|
||||
|
||||
if (!name.EndsWith("exe"))
|
||||
name = name.Substring(0, name.LastIndexOf(".exe") + 4);
|
||||
|
||||
name = name[..(name.LastIndexOf(".exe") + 4)];
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
|
@ -65,12 +65,21 @@ namespace Flow.Launcher.Plugin.SharedCommands
|
|||
{
|
||||
Process.Start(psi)?.Dispose();
|
||||
}
|
||||
catch (System.ComponentModel.Win32Exception)
|
||||
// This error may be thrown if browser path is incorrect
|
||||
catch (Win32Exception)
|
||||
{
|
||||
Process.Start(new ProcessStartInfo
|
||||
try
|
||||
{
|
||||
FileName = url, UseShellExecute = true
|
||||
});
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = url,
|
||||
UseShellExecute = true
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw; // Re-throw the exception if we cannot open the URL in the default browser
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -100,12 +109,20 @@ namespace Flow.Launcher.Plugin.SharedCommands
|
|||
Process.Start(psi)?.Dispose();
|
||||
}
|
||||
// This error may be thrown if browser path is incorrect
|
||||
catch (System.ComponentModel.Win32Exception)
|
||||
catch (Win32Exception)
|
||||
{
|
||||
Process.Start(new ProcessStartInfo
|
||||
try
|
||||
{
|
||||
FileName = url, UseShellExecute = true
|
||||
});
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = url,
|
||||
UseShellExecute = true
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw; // Re-throw the exception if we cannot open the URL in the default browser
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
using Windows.Win32;
|
||||
|
|
@ -7,13 +6,16 @@ using Windows.Win32.Foundation;
|
|||
using Windows.Win32.Graphics.Gdi;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure;
|
||||
namespace Flow.Launcher.Plugin.SharedModels;
|
||||
|
||||
/// <summary>
|
||||
/// Contains full information about a display monitor.
|
||||
/// Codes are edited from: <see href="https://github.com/Jack251970/DesktopWidgets3">.
|
||||
/// Inspired from: https://github.com/Jack251970/DesktopWidgets3.
|
||||
/// </summary>
|
||||
internal class MonitorInfo
|
||||
/// <remarks>
|
||||
/// Use this class to replace the System.Windows.Forms.Screen class which can cause possible System.PlatformNotSupportedException.
|
||||
/// </remarks>
|
||||
public class MonitorInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the display monitors (including invisible pseudo-monitors associated with the mirroring drivers).
|
||||
|
|
@ -23,14 +25,14 @@ internal class MonitorInfo
|
|||
{
|
||||
var monitorCount = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CMONITORS);
|
||||
var list = new List<MonitorInfo>(monitorCount);
|
||||
var callback = new MONITORENUMPROC((HMONITOR monitor, HDC deviceContext, RECT* rect, LPARAM data) =>
|
||||
var callback = new MONITORENUMPROC((monitor, deviceContext, rect, data) =>
|
||||
{
|
||||
list.Add(new MonitorInfo(monitor, rect));
|
||||
return true;
|
||||
});
|
||||
var dwData = new LPARAM();
|
||||
var hdc = new HDC();
|
||||
bool ok = PInvoke.EnumDisplayMonitors(hdc, (RECT?)null, callback, dwData);
|
||||
bool ok = PInvoke.EnumDisplayMonitors(hdc, null, callback, dwData);
|
||||
if (!ok)
|
||||
{
|
||||
Marshal.ThrowExceptionForHR(Marshal.GetLastWin32Error());
|
||||
|
|
@ -43,11 +45,11 @@ internal class MonitorInfo
|
|||
/// </summary>
|
||||
/// <param name="hwnd">Window handle</param>
|
||||
/// <returns>The display monitor that is nearest to a given window, or null if no monitor is found.</returns>
|
||||
public static unsafe MonitorInfo GetNearestDisplayMonitor(HWND hwnd)
|
||||
public static unsafe MonitorInfo GetNearestDisplayMonitor(nint hwnd)
|
||||
{
|
||||
var nearestMonitor = PInvoke.MonitorFromWindow(hwnd, MONITOR_FROM_FLAGS.MONITOR_DEFAULTTONEAREST);
|
||||
var nearestMonitor = PInvoke.MonitorFromWindow(new(hwnd), MONITOR_FROM_FLAGS.MONITOR_DEFAULTTONEAREST);
|
||||
MonitorInfo nearestMonitorInfo = null;
|
||||
var callback = new MONITORENUMPROC((HMONITOR monitor, HDC deviceContext, RECT* rect, LPARAM data) =>
|
||||
var callback = new MONITORENUMPROC((monitor, deviceContext, rect, data) =>
|
||||
{
|
||||
if (monitor == nearestMonitor)
|
||||
{
|
||||
|
|
@ -58,7 +60,7 @@ internal class MonitorInfo
|
|||
});
|
||||
var dwData = new LPARAM();
|
||||
var hdc = new HDC();
|
||||
bool ok = PInvoke.EnumDisplayMonitors(hdc, (RECT?)null, callback, dwData);
|
||||
bool ok = PInvoke.EnumDisplayMonitors(hdc, null, callback, dwData);
|
||||
if (!ok)
|
||||
{
|
||||
Marshal.ThrowExceptionForHR(Marshal.GetLastWin32Error());
|
||||
|
|
@ -66,17 +68,75 @@ internal class MonitorInfo
|
|||
return nearestMonitorInfo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the primary display monitor (the one that contains the taskbar).
|
||||
/// </summary>
|
||||
/// <returns>The primary display monitor, or null if no monitor is found.</returns>
|
||||
public static unsafe MonitorInfo GetPrimaryDisplayMonitor()
|
||||
{
|
||||
var primaryMonitor = PInvoke.MonitorFromWindow(new HWND(nint.Zero), MONITOR_FROM_FLAGS.MONITOR_DEFAULTTOPRIMARY);
|
||||
MonitorInfo primaryMonitorInfo = null;
|
||||
var callback = new MONITORENUMPROC((monitor, deviceContext, rect, data) =>
|
||||
{
|
||||
if (monitor == primaryMonitor)
|
||||
{
|
||||
primaryMonitorInfo = new MonitorInfo(monitor, rect);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
var dwData = new LPARAM();
|
||||
var hdc = new HDC();
|
||||
bool ok = PInvoke.EnumDisplayMonitors(hdc, null, callback, dwData);
|
||||
if (!ok)
|
||||
{
|
||||
Marshal.ThrowExceptionForHR(Marshal.GetLastWin32Error());
|
||||
}
|
||||
return primaryMonitorInfo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the display monitor that contains the cursor.
|
||||
/// </summary>
|
||||
/// <returns>The display monitor that contains the cursor, or null if no monitor is found.</returns>
|
||||
public static unsafe MonitorInfo GetCursorDisplayMonitor()
|
||||
{
|
||||
if (!PInvoke.GetCursorPos(out var pt))
|
||||
{
|
||||
Marshal.ThrowExceptionForHR(Marshal.GetLastWin32Error());
|
||||
}
|
||||
var cursorMonitor = PInvoke.MonitorFromPoint(pt, MONITOR_FROM_FLAGS.MONITOR_DEFAULTTONEAREST);
|
||||
MonitorInfo cursorMonitorInfo = null;
|
||||
var callback = new MONITORENUMPROC((monitor, deviceContext, rect, data) =>
|
||||
{
|
||||
if (monitor == cursorMonitor)
|
||||
{
|
||||
cursorMonitorInfo = new MonitorInfo(monitor, rect);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
var dwData = new LPARAM();
|
||||
var hdc = new HDC();
|
||||
bool ok = PInvoke.EnumDisplayMonitors(hdc, null, callback, dwData);
|
||||
if (!ok)
|
||||
{
|
||||
Marshal.ThrowExceptionForHR(Marshal.GetLastWin32Error());
|
||||
}
|
||||
return cursorMonitorInfo;
|
||||
}
|
||||
|
||||
private readonly HMONITOR _monitor;
|
||||
|
||||
internal unsafe MonitorInfo(HMONITOR monitor, RECT* rect)
|
||||
{
|
||||
RectMonitor =
|
||||
Bounds =
|
||||
new Rect(new Point(rect->left, rect->top),
|
||||
new Point(rect->right, rect->bottom));
|
||||
_monitor = monitor;
|
||||
var info = new MONITORINFOEXW() { monitorInfo = new MONITORINFO() { cbSize = (uint)sizeof(MONITORINFOEXW) } };
|
||||
GetMonitorInfo(monitor, ref info);
|
||||
RectWork =
|
||||
WorkingArea =
|
||||
new Rect(new Point(info.monitorInfo.rcWork.left, info.monitorInfo.rcWork.top),
|
||||
new Point(info.monitorInfo.rcWork.right, info.monitorInfo.rcWork.bottom));
|
||||
Name = new string(info.szDevice.AsSpan()).Replace("\0", "").Trim();
|
||||
|
|
@ -93,7 +153,7 @@ internal class MonitorInfo
|
|||
/// <remarks>
|
||||
/// <note>If the monitor is not the primary display monitor, some of the rectangle's coordinates may be negative values.</note>
|
||||
/// </remarks>
|
||||
public Rect RectMonitor { get; }
|
||||
public Rect Bounds { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the work area rectangle of the display monitor, expressed in virtual-screen coordinates.
|
||||
|
|
@ -101,15 +161,15 @@ internal class MonitorInfo
|
|||
/// <remarks>
|
||||
/// <note>If the monitor is not the primary display monitor, some of the rectangle's coordinates may be negative values.</note>
|
||||
/// </remarks>
|
||||
public Rect RectWork { get; }
|
||||
public Rect WorkingArea { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets if the monitor is the the primary display monitor.
|
||||
/// Gets if the monitor is the primary display monitor.
|
||||
/// </summary>
|
||||
public bool IsPrimary => _monitor == PInvoke.MonitorFromWindow(new(IntPtr.Zero), MONITOR_FROM_FLAGS.MONITOR_DEFAULTTOPRIMARY);
|
||||
public bool IsPrimary => _monitor == PInvoke.MonitorFromWindow(new(nint.Zero), MONITOR_FROM_FLAGS.MONITOR_DEFAULTTOPRIMARY);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() => $"{Name} {RectMonitor.Width}x{RectMonitor.Height}";
|
||||
public override string ToString() => $"{Name} {Bounds.Width}x{Bounds.Height}";
|
||||
|
||||
private static unsafe bool GetMonitorInfo(HMONITOR hMonitor, ref MONITORINFOEXW lpmi)
|
||||
{
|
||||
|
|
@ -76,5 +76,10 @@ namespace Flow.Launcher.Plugin
|
|||
/// Indicates whether the plugin is installed from a local path
|
||||
/// </summary>
|
||||
public bool IsFromLocalInstallPath => !string.IsNullOrEmpty(LocalInstallPath);
|
||||
|
||||
/// <summary>
|
||||
/// The minimum Flow Launcher version required for this plugin. Default is "".
|
||||
/// </summary>
|
||||
public string MinimumAppVersion { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
77
Flow.Launcher.Plugin/packages.lock.json
Normal file
77
Flow.Launcher.Plugin/packages.lock.json
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
{
|
||||
"version": 1,
|
||||
"dependencies": {
|
||||
"net9.0-windows7.0": {
|
||||
"Fody": {
|
||||
"type": "Direct",
|
||||
"requested": "[6.9.3, )",
|
||||
"resolved": "6.9.3",
|
||||
"contentHash": "1CUGgFdyECDKgi5HaUBhdv6k+VG9Iy4OCforGfHyar3xQXAJypZkzymgKtWj/4SPd6nSG0Qi7NH71qHrDSZLaA=="
|
||||
},
|
||||
"JetBrains.Annotations": {
|
||||
"type": "Direct",
|
||||
"requested": "[2025.2.2, )",
|
||||
"resolved": "2025.2.2",
|
||||
"contentHash": "0X56ZRizuHdrnPpgXjWV7f2tQO1FlQg5O1967OGKnI/4ZRNOK642J8L7brM1nYvrxTTU5TP1yRyXLRLaXLPQ8A=="
|
||||
},
|
||||
"Microsoft.SourceLink.GitHub": {
|
||||
"type": "Direct",
|
||||
"requested": "[10.0.103, )",
|
||||
"resolved": "10.0.103",
|
||||
"contentHash": "qZk7r40ftpZY+/sO019sgWAWfNqC2CLSspDdAxNYCJU/bCi/8jVwvOMjzb/d5gjCRNzQ4OCYgBfhdpQyVwLTyw==",
|
||||
"dependencies": {
|
||||
"Microsoft.Build.Tasks.Git": "10.0.103",
|
||||
"Microsoft.SourceLink.Common": "10.0.103"
|
||||
}
|
||||
},
|
||||
"Microsoft.Windows.CsWin32": {
|
||||
"type": "Direct",
|
||||
"requested": "[0.3.269, )",
|
||||
"resolved": "0.3.269",
|
||||
"contentHash": "O4GVJ0ymxcoFRGS07VcoEClj7A9PIciHIjWDrPymzonhYlOfM7V0ZqGBUK19cUH3BPca9MfSOH0KLK/9JzQ8+Q==",
|
||||
"dependencies": {
|
||||
"Microsoft.Windows.SDK.Win32Docs": "0.1.42-alpha",
|
||||
"Microsoft.Windows.SDK.Win32Metadata": "69.0.7-preview",
|
||||
"Microsoft.Windows.WDK.Win32Metadata": "0.13.25-experimental"
|
||||
}
|
||||
},
|
||||
"PropertyChanged.Fody": {
|
||||
"type": "Direct",
|
||||
"requested": "[4.1.0, )",
|
||||
"resolved": "4.1.0",
|
||||
"contentHash": "6v+f9cI8YjnZH2WBHuOqWEAo8DFFNGFIdU8xD3AsL6fhV6Y8oAmVWd7XKk49t8DpeUBwhR/X+97+6Epvek0Y3A==",
|
||||
"dependencies": {
|
||||
"Fody": "6.6.4"
|
||||
}
|
||||
},
|
||||
"Microsoft.Build.Tasks.Git": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.103",
|
||||
"contentHash": "QoiCMcPuxC6eqRQmrmF9zBY96ejIznXtve/lJJbonGD9I5Aygf2AUCOWslGiCEtBbfWRSuUnepBjuuVOdAl5ag=="
|
||||
},
|
||||
"Microsoft.SourceLink.Common": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.103",
|
||||
"contentHash": "cMtGW5/r0ck72Jg2QwZcNTX59z+iB/B1kW84VMa/eX8L19DhHIuIcQjfK0pgLLBxd60Jl0Bj9GUolcM0MnJnZA=="
|
||||
},
|
||||
"Microsoft.Windows.SDK.Win32Docs": {
|
||||
"type": "Transitive",
|
||||
"resolved": "0.1.42-alpha",
|
||||
"contentHash": "Z/9po23gUA9aoukirh2ItMU2ZS9++Js9Gdds9fu5yuMojDrmArvY2y+tq9985tR3cxFxpZO1O35Wjfo0khj5HA=="
|
||||
},
|
||||
"Microsoft.Windows.SDK.Win32Metadata": {
|
||||
"type": "Transitive",
|
||||
"resolved": "69.0.7-preview",
|
||||
"contentHash": "RJoNjQJVCIDNLPbvYuaygCFknTyAxOUE45of1voj0jjOgJa9MB2m1/G8L8F3IYc+2EFG5aqa/9y8PEx7Tk2tLQ=="
|
||||
},
|
||||
"Microsoft.Windows.WDK.Win32Metadata": {
|
||||
"type": "Transitive",
|
||||
"resolved": "0.13.25-experimental",
|
||||
"contentHash": "IM50tb/+UIwBr9FMr6ZKcZjCMW+Axo6NjGqKxgjUfyCY8dRnYUfrJEXxAaXoWtYP4X8EmASmC1Jtwh4XucseZg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Windows.SDK.Win32Metadata": "63.0.31-preview"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
265
Flow.Launcher.Test/ChineseDetectionPerformanceTest.cs
Normal file
265
Flow.Launcher.Test/ChineseDetectionPerformanceTest.cs
Normal file
|
|
@ -0,0 +1,265 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework.Legacy;
|
||||
using Flow.Launcher.Infrastructure;
|
||||
using ToolGood.Words.Pinyin;
|
||||
|
||||
namespace Flow.Launcher.Test
|
||||
{
|
||||
/// <summary>
|
||||
/// Performance test comparing ContainsChinese() vs WordsHelper.HasChinese()
|
||||
///
|
||||
/// This test verifies:
|
||||
/// 1. Both methods produce identical results (correctness)
|
||||
/// 2. Performance characteristics of both implementations
|
||||
/// 3. Memory allocation patterns
|
||||
///
|
||||
/// The ContainsChinese() method uses optimized Unicode range checking with ReadOnlySpan
|
||||
/// while WordsHelper.HasChinese() uses the ToolGood.Words library implementation.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class ChineseDetectionPerformanceTest
|
||||
{
|
||||
private readonly List<string> _testStrings = new()
|
||||
{
|
||||
// Pure English - should return false
|
||||
"Hello World",
|
||||
"Visual Studio Code",
|
||||
"Microsoft Office 2023",
|
||||
"Adobe Photoshop Creative Suite",
|
||||
"Google Chrome Browser Application",
|
||||
|
||||
// Pure Chinese - should return true
|
||||
"你好世界",
|
||||
"微软办公软件",
|
||||
"谷歌浏览器",
|
||||
"北京大学计算机科学与技术学院",
|
||||
"中华人民共和国国家发展和改革委员会",
|
||||
|
||||
// Mixed content - should return true
|
||||
"Hello 世界",
|
||||
"Visual Studio 代码编辑器",
|
||||
"QQ音乐 Music Player",
|
||||
"Windows 10 操作系统",
|
||||
"GitHub 代码仓库管理平台",
|
||||
|
||||
// Edge cases
|
||||
"",
|
||||
" ",
|
||||
"123456",
|
||||
"!@#$%^&*()",
|
||||
"café résumé naïve", // Accented characters (not Chinese)
|
||||
|
||||
// Long strings for performance testing
|
||||
"This is a very long English string that contains no Chinese characters but is designed to test performance with longer text content that might appear in file names or application descriptions",
|
||||
"这是一个非常长的中文字符串,包含了很多汉字,用来测试在处理较长中文文本时的性能表现,比如可能出现在文件名或应用程序描述中的文本内容",
|
||||
"This is a mixed 混合内容的字符串 that contains both English and Chinese characters 中英文混合 to test performance with 复杂的文本内容 in real-world scenarios 真实场景中的应用"
|
||||
};
|
||||
|
||||
[Test]
|
||||
public void ContainsChinese_CorrectnessTest()
|
||||
{
|
||||
// Verify ContainsChinese works correctly for known cases
|
||||
ClassicAssert.IsFalse(ContainsChinese("Hello World"), "Pure English should return false");
|
||||
ClassicAssert.IsTrue(ContainsChinese("你好世界"), "Pure Chinese should return true");
|
||||
ClassicAssert.IsTrue(ContainsChinese("Hello 世界"), "Mixed content should return true");
|
||||
ClassicAssert.IsFalse(ContainsChinese(""), "Empty string should return false");
|
||||
ClassicAssert.IsFalse(ContainsChinese("123456"), "Numbers should return false");
|
||||
ClassicAssert.IsFalse(ContainsChinese("café résumé"), "Accented characters should return false");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WordsHelper_CorrectnessTest()
|
||||
{
|
||||
// Verify WordsHelper.HasChinese works correctly for known cases
|
||||
ClassicAssert.IsFalse(WordsHelper.HasChinese("Hello World"), "Pure English should return false");
|
||||
ClassicAssert.IsTrue(WordsHelper.HasChinese("你好世界"), "Pure Chinese should return true");
|
||||
ClassicAssert.IsTrue(WordsHelper.HasChinese("Hello 世界"), "Mixed content should return true");
|
||||
ClassicAssert.IsFalse(WordsHelper.HasChinese(""), "Empty string should return false");
|
||||
ClassicAssert.IsFalse(WordsHelper.HasChinese("123456"), "Numbers should return false");
|
||||
ClassicAssert.IsFalse(WordsHelper.HasChinese("café résumé"), "Accented characters should return false");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BothMethods_ShouldProduceSameResults()
|
||||
{
|
||||
// Critical test: verify both methods produce identical results for all test cases
|
||||
foreach (var testString in _testStrings)
|
||||
{
|
||||
var wordsHelperResult = WordsHelper.HasChinese(testString);
|
||||
var containsChineseResult = ContainsChinese(testString);
|
||||
|
||||
ClassicAssert.AreEqual(wordsHelperResult, containsChineseResult,
|
||||
$"Results differ for string: '{testString}'. WordsHelper: {wordsHelperResult}, ContainsChinese: {containsChineseResult}");
|
||||
}
|
||||
|
||||
Console.WriteLine($"✓ Both methods produce identical results for all {_testStrings.Count} test cases");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PerformanceComparison_BasicBenchmark()
|
||||
{
|
||||
const int iterations = 1000000;
|
||||
|
||||
Console.WriteLine("=== CHINESE CHARACTER DETECTION PERFORMANCE TEST ===");
|
||||
Console.WriteLine($"Test iterations: {iterations:N0}");
|
||||
Console.WriteLine($"Test strings: {_testStrings.Count}");
|
||||
Console.WriteLine($"Total operations: {iterations * _testStrings.Count:N0}");
|
||||
Console.WriteLine();
|
||||
|
||||
// Warmup to ensure JIT compilation
|
||||
Console.WriteLine("Warming up...");
|
||||
for (int i = 0; i < 1000; i++)
|
||||
{
|
||||
foreach (var testString in _testStrings)
|
||||
{
|
||||
_ = ContainsChinese(testString);
|
||||
_ = WordsHelper.HasChinese(testString);
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark ContainsChinese method
|
||||
GC.Collect();
|
||||
GC.WaitForPendingFinalizers();
|
||||
GC.Collect();
|
||||
|
||||
var sw1 = System.Diagnostics.Stopwatch.StartNew();
|
||||
for (int i = 0; i < iterations; i++)
|
||||
{
|
||||
foreach (var testString in _testStrings)
|
||||
{
|
||||
_ = ContainsChinese(testString);
|
||||
}
|
||||
}
|
||||
sw1.Stop();
|
||||
|
||||
// Benchmark WordsHelper.HasChinese method
|
||||
GC.Collect();
|
||||
GC.WaitForPendingFinalizers();
|
||||
GC.Collect();
|
||||
|
||||
var sw2 = System.Diagnostics.Stopwatch.StartNew();
|
||||
for (int i = 0; i < iterations; i++)
|
||||
{
|
||||
foreach (var testString in _testStrings)
|
||||
{
|
||||
_ = WordsHelper.HasChinese(testString);
|
||||
}
|
||||
}
|
||||
sw2.Stop();
|
||||
|
||||
// Calculate and display results
|
||||
var containsChineseMs = sw1.Elapsed.TotalMilliseconds;
|
||||
var wordsHelperMs = sw2.Elapsed.TotalMilliseconds;
|
||||
var speedRatio = wordsHelperMs / containsChineseMs;
|
||||
var timeDifference = wordsHelperMs - containsChineseMs;
|
||||
|
||||
Console.WriteLine("RESULTS:");
|
||||
Console.WriteLine($"ContainsChinese(): {containsChineseMs:F3} ms");
|
||||
Console.WriteLine($"WordsHelper.HasChinese(): {wordsHelperMs:F3} ms");
|
||||
Console.WriteLine($"Time difference: {timeDifference:F3} ms");
|
||||
Console.WriteLine($"Speed improvement: {speedRatio:F2}x");
|
||||
Console.WriteLine($"Performance gain: {((speedRatio - 1) * 100):F1}%");
|
||||
Console.WriteLine();
|
||||
|
||||
if (speedRatio > 1.0)
|
||||
{
|
||||
Console.WriteLine($"✓ ContainsChinese() is {speedRatio:F2}x faster than WordsHelper.HasChinese()");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"⚠ WordsHelper.HasChinese() is {(1/speedRatio):F2}x faster than ContainsChinese()");
|
||||
}
|
||||
|
||||
// Test always passes - this is a measurement test
|
||||
ClassicAssert.IsTrue(true);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PerformanceComparison_ByStringType()
|
||||
{
|
||||
Console.WriteLine("=== PERFORMANCE BY STRING TYPE ===");
|
||||
|
||||
var categories = new Dictionary<string, List<string>>
|
||||
{
|
||||
["Pure English"] = _testStrings.Where(s => !ContainsChinese(s) && s.All(c => c <= 127)).ToList(),
|
||||
["Pure Chinese"] = _testStrings.Where(s => ContainsChinese(s) && s.All(c => IsChineseCharacter(c) || char.IsWhiteSpace(c))).ToList(),
|
||||
["Mixed Content"] = _testStrings.Where(s => ContainsChinese(s) && s.Any(c => c <= 127 && char.IsLetter(c))).ToList(),
|
||||
["Edge Cases"] = _testStrings.Where(s => string.IsNullOrWhiteSpace(s) || s.All(c => !char.IsLetter(c))).ToList()
|
||||
};
|
||||
|
||||
foreach (var category in categories)
|
||||
{
|
||||
if (category.Value.Count == 0) continue;
|
||||
|
||||
Console.WriteLine($"\n{category.Key} ({category.Value.Count} strings):");
|
||||
|
||||
var sample = category.Value.First();
|
||||
var displayText = sample.Length > 40 ? sample.Substring(0, 40) + "..." : sample;
|
||||
Console.WriteLine($" Sample: '{displayText}'");
|
||||
|
||||
const int categoryIterations = 5000;
|
||||
|
||||
// Test each method
|
||||
var sw1 = System.Diagnostics.Stopwatch.StartNew();
|
||||
for (int i = 0; i < categoryIterations; i++)
|
||||
{
|
||||
foreach (var str in category.Value)
|
||||
{
|
||||
_ = ContainsChinese(str);
|
||||
}
|
||||
}
|
||||
sw1.Stop();
|
||||
|
||||
var sw2 = System.Diagnostics.Stopwatch.StartNew();
|
||||
for (int i = 0; i < categoryIterations; i++)
|
||||
{
|
||||
foreach (var str in category.Value)
|
||||
{
|
||||
_ = WordsHelper.HasChinese(str);
|
||||
}
|
||||
}
|
||||
sw2.Stop();
|
||||
|
||||
var ratio = (double)sw2.ElapsedTicks / sw1.ElapsedTicks;
|
||||
Console.WriteLine($" Performance: ContainsChinese is {ratio:F2}x faster");
|
||||
}
|
||||
|
||||
ClassicAssert.IsTrue(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optimized Chinese character detection using comprehensive CJK Unicode ranges
|
||||
/// This method uses ReadOnlySpan for better performance and covers all CJK character ranges
|
||||
/// </summary>
|
||||
private static bool ContainsChinese(ReadOnlySpan<char> text)
|
||||
{
|
||||
foreach (var c in text)
|
||||
{
|
||||
if (IsChineseCharacter(c))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a character is a Chinese character using comprehensive Unicode ranges
|
||||
/// Covers CJK Unified Ideographs and all extension blocks
|
||||
/// </summary>
|
||||
private static bool IsChineseCharacter(char c)
|
||||
{
|
||||
return (c >= 0x4E00 && c <= 0x9FFF) || // CJK Unified Ideographs (most common Chinese characters)
|
||||
(c >= 0x3400 && c <= 0x4DBF) || // CJK Extension A
|
||||
(c >= 0x20000 && c <= 0x2A6DF) || // CJK Extension B
|
||||
(c >= 0x2A700 && c <= 0x2B73F) || // CJK Extension C
|
||||
(c >= 0x2B740 && c <= 0x2B81F) || // CJK Extension D
|
||||
(c >= 0x2B820 && c <= 0x2CEAF) || // CJK Extension E
|
||||
(c >= 0x2CEB0 && c <= 0x2EBEF) || // CJK Extension F
|
||||
(c >= 0xF900 && c <= 0xFAFF) || // CJK Compatibility Ideographs
|
||||
(c >= 0x2F800 && c <= 0x2FA1F); // CJK Compatibility Supplement
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
using Flow.Launcher.Plugin.SharedCommands;
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework.Legacy;
|
||||
using System.IO;
|
||||
|
||||
namespace Flow.Launcher.Test
|
||||
{
|
||||
|
|
@ -50,5 +51,89 @@ namespace Flow.Launcher.Test
|
|||
{
|
||||
ClassicAssert.AreEqual(expectedResult, FilesFolders.PathContains(parentPath, path, allowEqual: expectedResult));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryDeleteDirectoryRobust_WhenDirectoryDoesNotExist_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
string nonExistentPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
|
||||
|
||||
// Act
|
||||
bool result = FilesFolders.TryDeleteDirectoryRobust(nonExistentPath);
|
||||
|
||||
// Assert
|
||||
ClassicAssert.IsTrue(result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryDeleteDirectoryRobust_WhenDirectoryIsEmpty_DeletesSuccessfully()
|
||||
{
|
||||
// Arrange
|
||||
string tempDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
|
||||
Directory.CreateDirectory(tempDir);
|
||||
|
||||
// Act
|
||||
bool result = FilesFolders.TryDeleteDirectoryRobust(tempDir);
|
||||
|
||||
// Assert
|
||||
ClassicAssert.IsTrue(result);
|
||||
ClassicAssert.IsFalse(Directory.Exists(tempDir));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryDeleteDirectoryRobust_WhenDirectoryHasFiles_DeletesSuccessfully()
|
||||
{
|
||||
// Arrange
|
||||
string tempDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
|
||||
Directory.CreateDirectory(tempDir);
|
||||
File.WriteAllText(Path.Combine(tempDir, "test.txt"), "test content");
|
||||
|
||||
// Act
|
||||
bool result = FilesFolders.TryDeleteDirectoryRobust(tempDir);
|
||||
|
||||
// Assert
|
||||
ClassicAssert.IsTrue(result);
|
||||
ClassicAssert.IsFalse(Directory.Exists(tempDir));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryDeleteDirectoryRobust_WhenDirectoryHasNestedStructure_DeletesSuccessfully()
|
||||
{
|
||||
// Arrange
|
||||
string tempDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
|
||||
Directory.CreateDirectory(tempDir);
|
||||
string subDir1 = Path.Combine(tempDir, "SubDir1");
|
||||
string subDir2 = Path.Combine(tempDir, "SubDir2");
|
||||
Directory.CreateDirectory(subDir1);
|
||||
Directory.CreateDirectory(subDir2);
|
||||
File.WriteAllText(Path.Combine(subDir1, "file1.txt"), "content1");
|
||||
File.WriteAllText(Path.Combine(subDir2, "file2.txt"), "content2");
|
||||
File.WriteAllText(Path.Combine(tempDir, "root.txt"), "root content");
|
||||
|
||||
// Act
|
||||
bool result = FilesFolders.TryDeleteDirectoryRobust(tempDir);
|
||||
|
||||
// Assert
|
||||
ClassicAssert.IsTrue(result);
|
||||
ClassicAssert.IsFalse(Directory.Exists(tempDir));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryDeleteDirectoryRobust_WhenFileIsReadOnly_RemovesAttributeAndDeletes()
|
||||
{
|
||||
// Arrange
|
||||
string tempDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
|
||||
Directory.CreateDirectory(tempDir);
|
||||
string filePath = Path.Combine(tempDir, "readonly.txt");
|
||||
File.WriteAllText(filePath, "readonly content");
|
||||
File.SetAttributes(filePath, FileAttributes.ReadOnly);
|
||||
|
||||
// Act
|
||||
bool result = FilesFolders.TryDeleteDirectoryRobust(tempDir);
|
||||
|
||||
// Assert
|
||||
ClassicAssert.IsTrue(result);
|
||||
ClassicAssert.IsFalse(Directory.Exists(tempDir));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0-windows10.0.19041.0</TargetFramework>
|
||||
<TargetFramework>net9.0-windows10.0.19041.0</TargetFramework>
|
||||
<ProjectGuid>{FF742965-9A80-41A5-B042-D6C7D3A21708}</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
|
|
@ -39,6 +39,7 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Plugins\Flow.Launcher.Plugin.Calculator\Flow.Launcher.Plugin.Calculator.csproj" />
|
||||
<ProjectReference Include="..\Plugins\Flow.Launcher.Plugin.Explorer\Flow.Launcher.Plugin.Explorer.csproj" />
|
||||
<ProjectReference Include="..\Plugins\Flow.Launcher.Plugin.Program\Flow.Launcher.Plugin.Program.csproj" />
|
||||
<ProjectReference Include="..\Plugins\Flow.Launcher.Plugin.Url\Flow.Launcher.Plugin.Url.csproj" />
|
||||
|
|
@ -48,13 +49,13 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Moq" Version="4.18.4" />
|
||||
<PackageReference Include="nunit" Version="4.3.2" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0">
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="nunit" Version="4.4.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="5.1.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
134
Flow.Launcher.Test/Plugins/CalculatorTest.cs
Normal file
134
Flow.Launcher.Test/Plugins/CalculatorTest.cs
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Flow.Launcher.Plugin.Calculator;
|
||||
using Mages.Core;
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework.Legacy;
|
||||
|
||||
namespace Flow.Launcher.Test.Plugins
|
||||
{
|
||||
[TestFixture]
|
||||
public class CalculatorPluginTest
|
||||
{
|
||||
private readonly Main _plugin;
|
||||
private readonly Settings _settings = new()
|
||||
{
|
||||
DecimalSeparator = DecimalSeparator.UseSystemLocale,
|
||||
MaxDecimalPlaces = 10,
|
||||
ShowErrorMessage = false, // Make sure we return the empty results when error occurs
|
||||
UseThousandsSeparator = true // Default value
|
||||
};
|
||||
private readonly Engine _engine = new(new Configuration
|
||||
{
|
||||
Scope = new Dictionary<string, object>
|
||||
{
|
||||
{ "e", Math.E }, // e is not contained in the default mages engine
|
||||
}
|
||||
});
|
||||
|
||||
public CalculatorPluginTest()
|
||||
{
|
||||
_plugin = new Main();
|
||||
|
||||
var settingField = typeof(Main).GetField("_settings", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
if (settingField == null)
|
||||
Assert.Fail("Could not find field '_settings' on Flow.Launcher.Plugin.Calculator.Main");
|
||||
settingField.SetValue(_plugin, _settings);
|
||||
|
||||
var engineField = typeof(Main).GetField("MagesEngine", BindingFlags.NonPublic | BindingFlags.Static);
|
||||
if (engineField == null)
|
||||
Assert.Fail("Could not find static field 'MagesEngine' on Flow.Launcher.Plugin.Calculator.Main");
|
||||
engineField.SetValue(null, _engine);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ThousandsSeparatorTest_Enabled()
|
||||
{
|
||||
_settings.UseThousandsSeparator = true;
|
||||
|
||||
_settings.DecimalSeparator = DecimalSeparator.Dot;
|
||||
var result = GetCalculationResult("1000+234");
|
||||
// When thousands separator is enabled, the result should contain a separator
|
||||
// Since decimal separator is dot, thousands separator should be comma
|
||||
ClassicAssert.AreEqual("1,234", result);
|
||||
|
||||
_settings.DecimalSeparator = DecimalSeparator.Comma;
|
||||
var result2 = GetCalculationResult("1000+234");
|
||||
// When thousands separator is enabled, the result should contain a separator
|
||||
// Since decimal separator is comma, thousands separator should be dot
|
||||
ClassicAssert.AreEqual("1.234", result2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ThousandsSeparatorTest_Disabled()
|
||||
{
|
||||
_settings.UseThousandsSeparator = false;
|
||||
_settings.DecimalSeparator = DecimalSeparator.UseSystemLocale;
|
||||
|
||||
var result = GetCalculationResult("1000+234");
|
||||
ClassicAssert.AreEqual("1234", result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ThousandsSeparatorTest_LargeNumber()
|
||||
{
|
||||
_settings.UseThousandsSeparator = false;
|
||||
_settings.DecimalSeparator = DecimalSeparator.UseSystemLocale;
|
||||
|
||||
var result = GetCalculationResult("1000000+234567");
|
||||
ClassicAssert.AreEqual("1234567", result);
|
||||
}
|
||||
|
||||
// Basic operations
|
||||
[TestCase(@"1+1", "2")]
|
||||
[TestCase(@"2-1", "1")]
|
||||
[TestCase(@"2*2", "4")]
|
||||
[TestCase(@"4/2", "2")]
|
||||
[TestCase(@"2^3", "8")]
|
||||
// Decimal places
|
||||
[TestCase(@"10/3", "3.3333333333")]
|
||||
// Parentheses
|
||||
[TestCase(@"(1+2)*3", "9")]
|
||||
[TestCase(@"2^(1+2)", "8")]
|
||||
// Functions
|
||||
[TestCase(@"pow(2,3)", "8")]
|
||||
[TestCase(@"min(1,-1,-2)", "-2")]
|
||||
[TestCase(@"max(1,-1,-2)", "1")]
|
||||
[TestCase(@"sqrt(16)", "4")]
|
||||
[TestCase(@"sin(pi)", "0.0000000000")]
|
||||
[TestCase(@"cos(0)", "1")]
|
||||
[TestCase(@"tan(0)", "0")]
|
||||
[TestCase(@"log10(100)", "2")]
|
||||
[TestCase(@"log(100)", "2")]
|
||||
[TestCase(@"log2(8)", "3")]
|
||||
[TestCase(@"ln(e)", "1")]
|
||||
[TestCase(@"abs(-5)", "5")]
|
||||
// Constants
|
||||
[TestCase(@"pi", "3.1415926536")]
|
||||
// Complex expressions
|
||||
[TestCase(@"(2+3)*sqrt(16)-log(100)/ln(e)", "18")]
|
||||
[TestCase(@"sin(pi/2)+cos(0)+tan(0)", "2")]
|
||||
// Error handling (should return empty result)
|
||||
[TestCase(@"10/0", "")]
|
||||
[TestCase(@"sqrt(-1)", "")]
|
||||
[TestCase(@"log(0)", "")]
|
||||
[TestCase(@"invalid_expression", "")]
|
||||
public void CalculatorTest(string expression, string result)
|
||||
{
|
||||
_settings.UseThousandsSeparator = false;
|
||||
_settings.DecimalSeparator = DecimalSeparator.Dot;
|
||||
|
||||
ClassicAssert.AreEqual(GetCalculationResult(expression), result);
|
||||
}
|
||||
|
||||
private string GetCalculationResult(string expression)
|
||||
{
|
||||
var results = _plugin.Query(new Plugin.Query()
|
||||
{
|
||||
Search = expression
|
||||
});
|
||||
return results.Count > 0 ? results[0].Title : string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -16,9 +16,9 @@ namespace Flow.Launcher.Test
|
|||
{">", new PluginPair {Metadata = new PluginMetadata {ActionKeywords = new List<string> {">"}}}}
|
||||
};
|
||||
|
||||
Query q = QueryBuilder.Build("> ping google.com -n 20 -6", nonGlobalPlugins);
|
||||
Query q = QueryBuilder.Build("> ping google.com -n 20 -6", "> ping google.com -n 20 -6", nonGlobalPlugins);
|
||||
|
||||
ClassicAssert.AreEqual("> ping google.com -n 20 -6", q.RawQuery);
|
||||
ClassicAssert.AreEqual("> ping google.com -n 20 -6", q.TrimmedQuery);
|
||||
ClassicAssert.AreEqual("ping google.com -n 20 -6", q.Search, "Search should not start with the ActionKeyword.");
|
||||
ClassicAssert.AreEqual(">", q.ActionKeyword);
|
||||
|
||||
|
|
@ -39,10 +39,10 @@ namespace Flow.Launcher.Test
|
|||
{">", new PluginPair {Metadata = new PluginMetadata {ActionKeywords = new List<string> {">"}, Disabled = true}}}
|
||||
};
|
||||
|
||||
Query q = QueryBuilder.Build("> ping google.com -n 20 -6", nonGlobalPlugins);
|
||||
Query q = QueryBuilder.Build("> ping google.com -n 20 -6", "> ping google.com -n 20 -6", nonGlobalPlugins);
|
||||
|
||||
ClassicAssert.AreEqual("> ping google.com -n 20 -6", q.Search);
|
||||
ClassicAssert.AreEqual(q.Search, q.RawQuery, "RawQuery should be equal to Search.");
|
||||
ClassicAssert.AreEqual(q.Search, q.TrimmedQuery, "TrimmedQuery should be equal to Search.");
|
||||
ClassicAssert.AreEqual(6, q.SearchTerms.Length, "The length of SearchTerms should match.");
|
||||
ClassicAssert.AreNotEqual(">", q.ActionKeyword, "ActionKeyword should not match that of a disabled plugin.");
|
||||
ClassicAssert.AreEqual("ping google.com -n 20 -6", q.SecondToEndSearch, "SecondToEndSearch should be trimmed of multiple whitespace characters");
|
||||
|
|
@ -51,7 +51,7 @@ namespace Flow.Launcher.Test
|
|||
[Test]
|
||||
public void GenericPluginQueryTest()
|
||||
{
|
||||
Query q = QueryBuilder.Build("file.txt file2 file3", new Dictionary<string, PluginPair>());
|
||||
Query q = QueryBuilder.Build("file.txt file2 file3", "file.txt file2 file3", new Dictionary<string, PluginPair>());
|
||||
|
||||
ClassicAssert.AreEqual("file.txt file2 file3", q.Search);
|
||||
ClassicAssert.AreEqual("", q.ActionKeyword);
|
||||
|
|
|
|||
71
Flow.Launcher.Test/TranslationMappingTest.cs
Normal file
71
Flow.Launcher.Test/TranslationMappingTest.cs
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Flow.Launcher.Infrastructure;
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework.Legacy;
|
||||
|
||||
namespace Flow.Launcher.Test
|
||||
{
|
||||
[TestFixture]
|
||||
public class TranslationMappingTest
|
||||
{
|
||||
[Test]
|
||||
public void AddNewIndex_ShouldAddTranslatedIndexPlusLength()
|
||||
{
|
||||
var mapping = new TranslationMapping();
|
||||
mapping.AddNewIndex(5, 3);
|
||||
mapping.AddNewIndex(8, 2);
|
||||
|
||||
// 5+3=8, 8+2=10
|
||||
ClassicAssert.AreEqual(2, GetOriginalToTranslatedCount(mapping));
|
||||
ClassicAssert.AreEqual(8, GetOriginalToTranslatedAt(mapping, 0));
|
||||
ClassicAssert.AreEqual(10, GetOriginalToTranslatedAt(mapping, 1));
|
||||
}
|
||||
|
||||
|
||||
[TestCase(0, 0)] // "F" -> "F"
|
||||
[TestCase(1, 1)] // "l" -> "l"
|
||||
[TestCase(2, 2)] // "o" -> "o"
|
||||
[TestCase(3, 3)] // "w" -> "w"
|
||||
[TestCase(4, 4)] // " " -> " "
|
||||
[TestCase(5, 5)] // "Y" (translated from "用") -> original index 5
|
||||
[TestCase(6, 5)] // "o" (translated from "用") -> original index 5
|
||||
[TestCase(7, 5)] // "n" (translated from "用") -> original index 5
|
||||
[TestCase(8, 5)] // "g" (translated from "用") -> original index 5
|
||||
[TestCase(10, 6)] // "H" (translated from "户") -> original index 6
|
||||
[TestCase(11, 6)] // "u" (translated from "户") -> original index 6
|
||||
public void MapToOriginalIndex_ShouldReturnExpectedIndex(int translatedIndex, int expectedOriginalIndex)
|
||||
{
|
||||
var mapping = new TranslationMapping();
|
||||
// Test case :
|
||||
// 0123456
|
||||
// Flow 用户
|
||||
// 012345678901
|
||||
// Flow Yong Hu
|
||||
mapping.AddNewIndex(0, 1); // F
|
||||
mapping.AddNewIndex(1, 1); // l
|
||||
mapping.AddNewIndex(2, 1); // o
|
||||
mapping.AddNewIndex(3, 1); // w
|
||||
mapping.AddNewIndex(4, 1); // ' '
|
||||
mapping.AddNewIndex(5, 4); // 用 -> Yong
|
||||
mapping.AddNewIndex(10, 2); // 户 -> Hu
|
||||
|
||||
var result = mapping.MapToOriginalIndex(translatedIndex);
|
||||
ClassicAssert.AreEqual(expectedOriginalIndex, result);
|
||||
}
|
||||
|
||||
private static int GetOriginalToTranslatedCount(TranslationMapping mapping)
|
||||
{
|
||||
var field = typeof(TranslationMapping).GetField("_originalToTranslated", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
var list = (List<int>)field.GetValue(mapping);
|
||||
return list.Count;
|
||||
}
|
||||
|
||||
private static int GetOriginalToTranslatedAt(TranslationMapping mapping, int index)
|
||||
{
|
||||
var field = typeof(TranslationMapping).GetField("_originalToTranslated", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
var list = (List<int>)field.GetValue(mapping);
|
||||
return list[index];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -14,102 +14,102 @@
|
|||
<WindowChrome.WindowChrome>
|
||||
<WindowChrome CaptionHeight="32" ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" />
|
||||
</WindowChrome.WindowChrome>
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
<RowDefinition Height="80" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid>
|
||||
<StackPanel Grid.Row="0">
|
||||
<StackPanel>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button
|
||||
Grid.Column="4"
|
||||
Click="BtnCancel_OnClick"
|
||||
Style="{StaticResource TitleBarCloseButtonStyle}">
|
||||
<Path
|
||||
Width="46"
|
||||
Height="32"
|
||||
Data="M 18,11 27,20 M 18,20 27,11"
|
||||
Stroke="{Binding Path=Foreground, RelativeSource={RelativeSource AncestorType={x:Type Button}}}"
|
||||
StrokeThickness="1">
|
||||
<Path.Style>
|
||||
<Style TargetType="Path">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding Path=IsActive, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}" Value="False">
|
||||
<Setter Property="Opacity" Value="0.5" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Path.Style>
|
||||
</Path>
|
||||
</Button>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="26 12 26 0">
|
||||
<StackPanel Grid.Row="0" Margin="0 0 0 12">
|
||||
<TextBlock
|
||||
Grid.Column="0"
|
||||
Margin="0 0 0 0"
|
||||
FontSize="20"
|
||||
FontWeight="SemiBold"
|
||||
Text="{DynamicResource actionKeywordsTitle}"
|
||||
TextAlignment="Left" />
|
||||
</StackPanel>
|
||||
<StackPanel>
|
||||
<TextBlock
|
||||
FontSize="14"
|
||||
Text="{DynamicResource actionkeyword_tips}"
|
||||
TextAlignment="Left"
|
||||
TextWrapping="WrapWithOverflow" />
|
||||
</StackPanel>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel Margin="0 18 0 0" Orientation="Horizontal">
|
||||
<TextBlock
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="14"
|
||||
Text="{DynamicResource currentActionKeywords}" />
|
||||
<TextBlock
|
||||
x:Name="tbOldActionKeyword"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Margin="14 10 10 10"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource Color05B}" />
|
||||
</StackPanel>
|
||||
<StackPanel Margin="0 0 0 10" Orientation="Horizontal">
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="14"
|
||||
Text="{DynamicResource newActionKeyword}" />
|
||||
<TextBox
|
||||
x:Name="tbAction"
|
||||
Width="105"
|
||||
Margin="10 10 15 10"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<Border
|
||||
<Button
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Right"
|
||||
Click="BtnCancel_OnClick"
|
||||
Style="{StaticResource TitleBarCloseButtonStyle}">
|
||||
<Path
|
||||
Width="46"
|
||||
Height="32"
|
||||
Data="M 18,11 27,20 M 18,20 27,11"
|
||||
Stroke="{Binding Path=Foreground, RelativeSource={RelativeSource AncestorType={x:Type Button}}}"
|
||||
StrokeThickness="1">
|
||||
<Path.Style>
|
||||
<Style TargetType="Path">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding Path=IsActive, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}" Value="False">
|
||||
<Setter Property="Opacity" Value="0.5" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Path.Style>
|
||||
</Path>
|
||||
</Button>
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="2"
|
||||
Margin="26 12 26 12"
|
||||
FontSize="20"
|
||||
FontWeight="SemiBold"
|
||||
Text="{DynamicResource actionKeywordsTitle}"
|
||||
TextAlignment="Left" />
|
||||
<TextBlock
|
||||
Grid.Row="2"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="2"
|
||||
Margin="26 0 26 10"
|
||||
FontSize="14"
|
||||
Text="{DynamicResource actionkeyword_tips}"
|
||||
TextAlignment="Left"
|
||||
TextWrapping="WrapWithOverflow" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="3"
|
||||
Grid.Column="0"
|
||||
Margin="26 0 10 0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="14"
|
||||
Text="{DynamicResource currentActionKeywords}" />
|
||||
<TextBox
|
||||
x:Name="tbOldActionKeyword"
|
||||
Grid.Row="3"
|
||||
Grid.Column="1"
|
||||
Margin="10 10 26 6"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource Color05B}"
|
||||
IsReadOnly="True"
|
||||
Text="List of old keyword(s)" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="4"
|
||||
Grid.Column="0"
|
||||
Margin="26 6 10 12"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="14"
|
||||
Text="{DynamicResource newActionKeyword}" />
|
||||
<TextBox
|
||||
x:Name="tbAction"
|
||||
Grid.Row="4"
|
||||
Grid.Column="1"
|
||||
Margin="10 6 26 10"
|
||||
VerticalAlignment="Center" />
|
||||
|
||||
<Border
|
||||
Grid.Row="5"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="2"
|
||||
Margin="0 10 0 0"
|
||||
Background="{DynamicResource PopupButtonAreaBGColor}"
|
||||
BorderBrush="{DynamicResource PopupButtonAreaBorderColor}"
|
||||
BorderThickness="0 1 0 0">
|
||||
|
|
@ -117,15 +117,15 @@
|
|||
<Button
|
||||
x:Name="btnCancel"
|
||||
Width="145"
|
||||
Height="30"
|
||||
Margin="10 0 5 0"
|
||||
Height="38"
|
||||
Margin="10 0 10 0"
|
||||
Click="BtnCancel_OnClick"
|
||||
Content="{DynamicResource cancel}" />
|
||||
<Button
|
||||
x:Name="btnDone"
|
||||
Width="145"
|
||||
Height="30"
|
||||
Margin="5 0 10 0"
|
||||
Height="38"
|
||||
Margin="10 0 10 0"
|
||||
Click="btnDone_OnClick"
|
||||
Style="{StaticResource AccentButtonStyle}">
|
||||
<TextBlock x:Name="lblAdd" Text="{DynamicResource done}" />
|
||||
|
|
|
|||
|
|
@ -20,7 +20,9 @@ namespace Flow.Launcher
|
|||
|
||||
private void ActionKeyword_OnLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
tbOldActionKeyword.Text = string.Join(Query.ActionKeywordSeparator, _plugin.Metadata.ActionKeywords.ToArray());
|
||||
tbOldActionKeyword.Text = string.Join(Query.ActionKeywordSeparator, _plugin.Metadata.ActionKeywords);
|
||||
tbAction.Text = tbOldActionKeyword.Text;
|
||||
tbAction.SelectAll();
|
||||
tbAction.Focus();
|
||||
}
|
||||
|
||||
|
|
@ -33,38 +35,39 @@ namespace Flow.Launcher
|
|||
{
|
||||
var oldActionKeywords = _plugin.Metadata.ActionKeywords;
|
||||
|
||||
var newActionKeywords = tbAction.Text.Split(Query.ActionKeywordSeparator).ToList();
|
||||
newActionKeywords.RemoveAll(string.IsNullOrEmpty);
|
||||
newActionKeywords = newActionKeywords.Distinct().ToList();
|
||||
var newActionKeywords = tbAction.Text.Split(Query.ActionKeywordSeparator)
|
||||
.Where(s => !string.IsNullOrEmpty(s))
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
newActionKeywords = newActionKeywords.Count > 0 ? newActionKeywords : new() { Query.GlobalPluginWildcardSign };
|
||||
|
||||
var addedActionKeywords = newActionKeywords.Except(oldActionKeywords).ToList();
|
||||
var removedActionKeywords = oldActionKeywords.Except(newActionKeywords).ToList();
|
||||
if (!addedActionKeywords.Any(App.API.ActionKeywordAssigned))
|
||||
|
||||
if (addedActionKeywords.Any(App.API.ActionKeywordAssigned))
|
||||
{
|
||||
if (oldActionKeywords.Count != newActionKeywords.Count)
|
||||
{
|
||||
ReplaceActionKeyword(_plugin.Metadata.ID, removedActionKeywords, addedActionKeywords);
|
||||
return;
|
||||
}
|
||||
App.API.ShowMsgBox(Localize.newActionKeywordsHasBeenAssigned());
|
||||
return;
|
||||
}
|
||||
|
||||
var sortedOldActionKeywords = oldActionKeywords.OrderBy(s => s).ToList();
|
||||
var sortedNewActionKeywords = newActionKeywords.OrderBy(s => s).ToList();
|
||||
if (oldActionKeywords.Count != newActionKeywords.Count)
|
||||
{
|
||||
ReplaceActionKeyword(_plugin.Metadata.ID, removedActionKeywords, addedActionKeywords);
|
||||
return;
|
||||
}
|
||||
|
||||
if (sortedOldActionKeywords.SequenceEqual(sortedNewActionKeywords))
|
||||
{
|
||||
// User just changes the sequence of action keywords
|
||||
App.API.ShowMsgBox(App.API.GetTranslation("newActionKeywordsSameAsOld"));
|
||||
}
|
||||
else
|
||||
{
|
||||
ReplaceActionKeyword(_plugin.Metadata.ID, removedActionKeywords, addedActionKeywords);
|
||||
}
|
||||
var sortedOldActionKeywords = oldActionKeywords.OrderBy(s => s).ToList();
|
||||
var sortedNewActionKeywords = newActionKeywords.OrderBy(s => s).ToList();
|
||||
|
||||
if (sortedOldActionKeywords.SequenceEqual(sortedNewActionKeywords))
|
||||
{
|
||||
// User just changes the sequence of action keywords
|
||||
App.API.ShowMsgBox(Localize.newActionKeywordsSameAsOld());
|
||||
}
|
||||
else
|
||||
{
|
||||
App.API.ShowMsgBox(App.API.GetTranslation("newActionKeywordsHasBeenAssigned"));
|
||||
ReplaceActionKeyword(_plugin.Metadata.ID, removedActionKeywords, addedActionKeywords);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
x:Class="Flow.Launcher.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:ui="http://schemas.modernwpf.com/2019"
|
||||
xmlns:sys="clr-namespace:System;assembly=mscorlib"
|
||||
xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern"
|
||||
ShutdownMode="OnMainWindowClose"
|
||||
Startup="OnStartup">
|
||||
<Application.Resources>
|
||||
|
|
@ -10,17 +11,17 @@
|
|||
<ResourceDictionary.MergedDictionaries>
|
||||
<ui:ThemeResources>
|
||||
<ui:ThemeResources.ThemeDictionaries>
|
||||
<ResourceDictionary x:Key="Light">
|
||||
<ResourceDictionary x:Key="Light" ui:ThemeDictionary.Key="Light">
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="pack://application:,,,/Resources/Light.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key="Dark">
|
||||
<ResourceDictionary x:Key="Dark" ui:ThemeDictionary.Key="Dark">
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="pack://application:,,,/Resources/Dark.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key="HighContrast">
|
||||
<ResourceDictionary x:Key="HighContrast" ui:ThemeDictionary.Key="HighContrast">
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="pack://application:,,,/Resources/Dark.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
|
@ -33,6 +34,15 @@
|
|||
<ResourceDictionary Source="pack://application:,,,/Themes/Win11Light.xaml" />
|
||||
<ResourceDictionary Source="pack://application:,,,/Languages/en.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
||||
<!-- Override styles in UI.Modern.WPF -->
|
||||
<Thickness x:Key="ListViewItemCompactSelectedBorderThemeThickness">2</Thickness>
|
||||
<sys:Double x:Key="CheckBoxMinWidth">0</sys:Double>
|
||||
<sys:Double x:Key="GridViewItemMinWidth">0</sys:Double>
|
||||
<sys:Double x:Key="GridViewItemMinHeight">40</sys:Double>
|
||||
<sys:Double x:Key="ListViewItemMinWidth">0</sys:Double>
|
||||
<sys:Double x:Key="ListViewItemMinHeight">36</sys:Double>
|
||||
<SolidColorBrush x:Key="NavigationViewSelectionIndicatorForeground" Color="#FF0063B1" />
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
|
|
@ -16,11 +16,13 @@ using Flow.Launcher.Infrastructure;
|
|||
using Flow.Launcher.Infrastructure.Http;
|
||||
using Flow.Launcher.Infrastructure.Image;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Infrastructure.DialogJump;
|
||||
using Flow.Launcher.Infrastructure.Storage;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Flow.Launcher.SettingPages.ViewModels;
|
||||
using Flow.Launcher.ViewModel;
|
||||
using iNKORE.UI.WPF.Modern.Common;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.VisualStudio.Threading;
|
||||
|
|
@ -41,9 +43,10 @@ namespace Flow.Launcher
|
|||
private static readonly string ClassName = nameof(App);
|
||||
|
||||
private static bool _disposed;
|
||||
private static Settings _settings;
|
||||
private static MainWindow _mainWindow;
|
||||
private readonly MainViewModel _mainVM;
|
||||
private readonly Settings _settings;
|
||||
private readonly Internationalization _internationalization;
|
||||
|
||||
// To prevent two disposals running at the same time.
|
||||
private static readonly object _disposingLock = new();
|
||||
|
|
@ -54,19 +57,11 @@ namespace Flow.Launcher
|
|||
|
||||
public App()
|
||||
{
|
||||
// Do not use bitmap cache since it can cause WPF second window freezing issue
|
||||
ShadowAssist.UseBitmapCache = false;
|
||||
|
||||
// Initialize settings
|
||||
try
|
||||
{
|
||||
var storage = new FlowLauncherJsonStorage<Settings>();
|
||||
_settings = storage.Load();
|
||||
_settings.SetStorage(storage);
|
||||
_settings.WMPInstalled = WindowsMediaPlayerHelper.IsWindowsMediaPlayerInstalled();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ShowErrorMsgBoxAndFailFast("Cannot load setting storage, please check local data directory", e);
|
||||
return;
|
||||
}
|
||||
_settings.WMPInstalled = WindowsMediaPlayerHelper.IsWindowsMediaPlayerInstalled();
|
||||
|
||||
// Configure the dependency injection container
|
||||
try
|
||||
|
|
@ -117,22 +112,13 @@ namespace Flow.Launcher
|
|||
API = Ioc.Default.GetRequiredService<IPublicAPI>();
|
||||
_settings.Initialize();
|
||||
_mainVM = Ioc.Default.GetRequiredService<MainViewModel>();
|
||||
_internationalization = Ioc.Default.GetRequiredService<Internationalization>();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ShowErrorMsgBoxAndFailFast("Cannot initialize api and settings, please open new issue in Flow.Launcher", e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Local function
|
||||
static void ShowErrorMsgBoxAndFailFast(string message, Exception e)
|
||||
{
|
||||
// Firstly show users the message
|
||||
MessageBox.Show(e.ToString(), message, MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
|
||||
// Flow cannot construct its App instance, so ensure Flow crashes w/ the exception info.
|
||||
Environment.FailFast(message, e);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
|
@ -142,6 +128,29 @@ namespace Flow.Launcher
|
|||
[STAThread]
|
||||
public static void Main()
|
||||
{
|
||||
// Initialize settings so that we can get language code
|
||||
try
|
||||
{
|
||||
var storage = new FlowLauncherJsonStorage<Settings>();
|
||||
_settings = storage.Load();
|
||||
_settings.SetStorage(storage);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ShowErrorMsgBoxAndFailFast("Cannot load setting storage, please check local data directory", e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize system language before changing culture info
|
||||
Internationalization.InitSystemLanguageCode();
|
||||
|
||||
// Change culture info before application creation to localize WinForm windows
|
||||
if (_settings.Language != Constant.SystemLanguageCode)
|
||||
{
|
||||
Internationalization.ChangeCultureInfo(_settings.Language);
|
||||
}
|
||||
|
||||
// Start the application as a single instance
|
||||
if (SingleInstance<App>.InitializeAsFirstInstance())
|
||||
{
|
||||
using var application = new App();
|
||||
|
|
@ -152,6 +161,19 @@ namespace Flow.Launcher
|
|||
|
||||
#endregion
|
||||
|
||||
#region Fail Fast
|
||||
|
||||
private static void ShowErrorMsgBoxAndFailFast(string message, Exception e)
|
||||
{
|
||||
// Firstly show users the message
|
||||
MessageBox.Show(e.ToString(), message, MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
|
||||
// Flow cannot construct its App instance, so ensure Flow crashes w/ the exception info.
|
||||
Environment.FailFast(message, e);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region App Events
|
||||
|
||||
#pragma warning disable VSTHRD100 // Avoid async void methods
|
||||
|
|
@ -165,12 +187,23 @@ namespace Flow.Launcher
|
|||
// So set to OnExplicitShutdown to prevent the application from shutting down before main window is created
|
||||
Current.ShutdownMode = ShutdownMode.OnExplicitShutdown;
|
||||
|
||||
// Setup log level before any logging is done
|
||||
Log.SetLogLevel(_settings.LogLevel);
|
||||
|
||||
// Update dynamic resources base on settings
|
||||
Current.Resources["SettingWindowFont"] = new FontFamily(_settings.SettingWindowFont);
|
||||
Current.Resources["ContentControlThemeFontFamily"] = new FontFamily(_settings.SettingWindowFont);
|
||||
|
||||
// Initialize notification system before any notification api is called
|
||||
Notification.Install();
|
||||
|
||||
// Enable Win32 dark mode if the system is in dark mode before creating all windows
|
||||
Win32Helper.EnableWin32DarkMode(_settings.ColorScheme);
|
||||
|
||||
// Initialize language before portable clean up since it needs translations
|
||||
await _internationalization.InitializeLanguageAsync();
|
||||
|
||||
// Clean up after portability update
|
||||
Ioc.Default.GetRequiredService<Portable>().PreStartCleanUpAfterPortabilityUpdate();
|
||||
|
||||
API.LogInfo(ClassName, "Begin Flow Launcher startup ----------------------------------------------------");
|
||||
|
|
@ -180,29 +213,25 @@ namespace Flow.Launcher
|
|||
RegisterDispatcherUnhandledException();
|
||||
RegisterTaskSchedulerUnhandledException();
|
||||
|
||||
var imageLoadertask = ImageLoader.InitializeAsync();
|
||||
|
||||
AbstractPluginEnvironment.PreStartPluginExecutablePathUpdate(_settings);
|
||||
|
||||
PluginManager.LoadPlugins(_settings.PluginSettings);
|
||||
|
||||
// Register ResultsUpdated event after all plugins are loaded
|
||||
Ioc.Default.GetRequiredService<MainViewModel>().RegisterResultsUpdatedEvent();
|
||||
var imageLoaderTask = ImageLoader.InitializeAsync();
|
||||
|
||||
Http.Proxy = _settings.Proxy;
|
||||
|
||||
await PluginManager.InitializePluginsAsync();
|
||||
// Initialize plugin manifest before initializing plugins so that they can use the manifest instantly
|
||||
await API.UpdatePluginManifestAsync();
|
||||
|
||||
// Change language after all plugins are initialized because we need to update plugin title based on their api
|
||||
await Ioc.Default.GetRequiredService<Internationalization>().InitializeLanguageAsync();
|
||||
|
||||
await imageLoadertask;
|
||||
await imageLoaderTask;
|
||||
|
||||
_mainWindow = new MainWindow();
|
||||
|
||||
Current.MainWindow = _mainWindow;
|
||||
Current.MainWindow.Title = Constant.FlowLauncher;
|
||||
|
||||
// Initialize Dialog Jump before hotkey mapper since hotkey mapper will register its hotkey
|
||||
// Initialize Dialog Jump after main window is created so that it can access main window handle
|
||||
DialogJump.InitializeDialogJump();
|
||||
DialogJump.SetupDialogJump(_settings.EnableDialogJump);
|
||||
|
||||
// Initialize hotkey mapper instantly after main window is created because
|
||||
// it will steal focus from main window which causes window hide
|
||||
HotKeyMapper.Initialize();
|
||||
|
|
@ -218,7 +247,35 @@ namespace Flow.Launcher
|
|||
AutoUpdates();
|
||||
|
||||
API.SaveAppAllSettings();
|
||||
API.LogInfo(ClassName, "End Flow Launcher startup ----------------------------------------------------");
|
||||
API.LogInfo(ClassName, "End Flow Launcher startup ------------------------------------------------------");
|
||||
|
||||
_ = API.StopwatchLogInfoAsync(ClassName, "Startup cost", async () =>
|
||||
{
|
||||
API.LogInfo(ClassName, "Begin plugin initialization ----------------------------------------------------");
|
||||
|
||||
AbstractPluginEnvironment.PreStartPluginExecutablePathUpdate(_settings);
|
||||
|
||||
PluginManager.LoadPlugins(_settings.PluginSettings);
|
||||
|
||||
await PluginManager.InitializePluginsAsync(_mainVM);
|
||||
|
||||
// Refresh the history results after plugins are initialized so that we can parse the absolute icon paths
|
||||
_mainVM.RefreshLastOpenedHistoryResults();
|
||||
|
||||
// Refresh home page after plugins are initialized because users may open main window during plugin initialization
|
||||
// And home page is created without full plugin list
|
||||
if (_settings.ShowHomePage && _mainVM.QueryResultsSelected() && string.IsNullOrEmpty(_mainVM.QueryText))
|
||||
{
|
||||
_mainVM.QueryResults();
|
||||
}
|
||||
|
||||
AutoPluginUpdates();
|
||||
|
||||
// Save all settings since we possibly update the plugin environment paths
|
||||
API.SaveAppAllSettings();
|
||||
|
||||
API.LogInfo(ClassName, "End plugin initialization ------------------------------------------------------");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -228,7 +285,7 @@ namespace Flow.Launcher
|
|||
/// Check startup only for Release
|
||||
/// </summary>
|
||||
[Conditional("RELEASE")]
|
||||
private void AutoStartup()
|
||||
private static void AutoStartup()
|
||||
{
|
||||
// we try to enable auto-startup on first launch, or reenable if it was removed
|
||||
// but the user still has the setting set
|
||||
|
|
@ -243,13 +300,13 @@ namespace Flow.Launcher
|
|||
// but if it fails (permissions, etc) then don't keep retrying
|
||||
// this also gives the user a visual indication in the Settings widget
|
||||
_settings.StartFlowLauncherOnSystemStartup = false;
|
||||
API.ShowMsg(API.GetTranslation("setAutoStartFailed"), e.Message);
|
||||
API.ShowMsgError(Localize.setAutoStartFailed(), e.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Conditional("RELEASE")]
|
||||
private void AutoUpdates()
|
||||
private static void AutoUpdates()
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
|
|
@ -266,6 +323,38 @@ namespace Flow.Launcher
|
|||
});
|
||||
}
|
||||
|
||||
[Conditional("RELEASE")]
|
||||
private static void AutoPluginUpdates()
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
if (_settings.AutoUpdatePlugins)
|
||||
{
|
||||
// check plugin updates every 5 hour
|
||||
var timer = new PeriodicTimer(TimeSpan.FromHours(5));
|
||||
await PluginInstaller.CheckForPluginUpdatesAsync((plugins) =>
|
||||
{
|
||||
Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
var pluginUpdateWindow = new PluginUpdateWindow(plugins);
|
||||
pluginUpdateWindow.ShowDialog();
|
||||
});
|
||||
});
|
||||
|
||||
while (await timer.WaitForNextTickAsync())
|
||||
// check updates on startup
|
||||
await PluginInstaller.CheckForPluginUpdatesAsync((plugins) =>
|
||||
{
|
||||
Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
var pluginUpdateWindow = new PluginUpdateWindow(plugins);
|
||||
pluginUpdateWindow.ShowDialog();
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Register Events
|
||||
|
|
@ -357,6 +446,8 @@ namespace Flow.Launcher
|
|||
// since some resources owned by the thread need to be disposed.
|
||||
_mainWindow?.Dispatcher.Invoke(_mainWindow.Dispose);
|
||||
_mainVM?.Dispose();
|
||||
DialogJump.Dispose();
|
||||
_internationalization.Dispose();
|
||||
}
|
||||
|
||||
API.LogInfo(ClassName, "End Flow Launcher dispose ----------------------------------------------------");
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ using System.Windows.Input;
|
|||
|
||||
namespace Flow.Launcher.Converters;
|
||||
|
||||
internal class BoolToIMEConversionModeConverter : IValueConverter
|
||||
public class BoolToIMEConversionModeConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
|
|
@ -22,7 +22,7 @@ internal class BoolToIMEConversionModeConverter : IValueConverter
|
|||
}
|
||||
}
|
||||
|
||||
internal class BoolToIMEStateConverter : IValueConverter
|
||||
public class BoolToIMEStateConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
|
|
|
|||
91
Flow.Launcher/Converters/CornerRadiusFilterConverter.cs
Normal file
91
Flow.Launcher/Converters/CornerRadiusFilterConverter.cs
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace Flow.Launcher.Converters;
|
||||
|
||||
public class CornerRadiusFilterConverter : DependencyObject, IValueConverter
|
||||
{
|
||||
public CornerRadiusFilterKind Filter { get; set; }
|
||||
|
||||
public double Scale { get; set; } = 1.0;
|
||||
|
||||
public static CornerRadius Convert(CornerRadius radius, CornerRadiusFilterKind filterKind)
|
||||
{
|
||||
CornerRadius result = radius;
|
||||
|
||||
switch (filterKind)
|
||||
{
|
||||
case CornerRadiusFilterKind.Top:
|
||||
result.BottomLeft = 0;
|
||||
result.BottomRight = 0;
|
||||
break;
|
||||
case CornerRadiusFilterKind.Right:
|
||||
result.TopLeft = 0;
|
||||
result.BottomLeft = 0;
|
||||
break;
|
||||
case CornerRadiusFilterKind.Bottom:
|
||||
result.TopLeft = 0;
|
||||
result.TopRight = 0;
|
||||
break;
|
||||
case CornerRadiusFilterKind.Left:
|
||||
result.TopRight = 0;
|
||||
result.BottomRight = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
var cornerRadius = (CornerRadius)value;
|
||||
|
||||
var scale = Scale;
|
||||
if (!double.IsNaN(scale))
|
||||
{
|
||||
cornerRadius.TopLeft *= scale;
|
||||
cornerRadius.TopRight *= scale;
|
||||
cornerRadius.BottomRight *= scale;
|
||||
cornerRadius.BottomLeft *= scale;
|
||||
}
|
||||
|
||||
var filterType = Filter;
|
||||
if (filterType == CornerRadiusFilterKind.TopLeftValue ||
|
||||
filterType == CornerRadiusFilterKind.BottomRightValue)
|
||||
{
|
||||
return GetDoubleValue(cornerRadius, filterType);
|
||||
}
|
||||
|
||||
return Convert(cornerRadius, filterType);
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private static double GetDoubleValue(CornerRadius radius, CornerRadiusFilterKind filterKind)
|
||||
{
|
||||
switch (filterKind)
|
||||
{
|
||||
case CornerRadiusFilterKind.TopLeftValue:
|
||||
return radius.TopLeft;
|
||||
case CornerRadiusFilterKind.BottomRightValue:
|
||||
return radius.BottomRight;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public enum CornerRadiusFilterKind
|
||||
{
|
||||
None,
|
||||
Top,
|
||||
Right,
|
||||
Bottom,
|
||||
Left,
|
||||
TopLeftValue,
|
||||
BottomRightValue
|
||||
}
|
||||
32
Flow.Launcher/Converters/PlacementRectangleConverter.cs
Normal file
32
Flow.Launcher/Converters/PlacementRectangleConverter.cs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace Flow.Launcher.Converters;
|
||||
|
||||
public class PlacementRectangleConverter : IMultiValueConverter
|
||||
{
|
||||
public Thickness Margin { get; set; }
|
||||
|
||||
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (values.Length == 2 &&
|
||||
values[0] is double width &&
|
||||
values[1] is double height)
|
||||
{
|
||||
var margin = Margin;
|
||||
var topLeft = new Point(margin.Left, margin.Top);
|
||||
var bottomRight = new Point(width - margin.Right, height - margin.Bottom);
|
||||
var rect = new Rect(topLeft, bottomRight);
|
||||
return rect;
|
||||
}
|
||||
|
||||
return Rect.Empty;
|
||||
}
|
||||
|
||||
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
|
@ -33,15 +33,39 @@ public class QuerySuggestionBoxConverter : IMultiValueConverter
|
|||
{
|
||||
var selectedResult = selectedItem.Result;
|
||||
var selectedResultActionKeyword = string.IsNullOrEmpty(selectedResult.ActionKeywordAssigned) ? "" : selectedResult.ActionKeywordAssigned + " ";
|
||||
var selectedResultPossibleSuggestion = selectedResultActionKeyword + selectedResult.Title;
|
||||
|
||||
if (!selectedResultPossibleSuggestion.StartsWith(queryText, StringComparison.CurrentCultureIgnoreCase))
|
||||
string selectedResultPossibleSuggestion = null;
|
||||
|
||||
// Firstly check if the result has QuerySuggestionText
|
||||
if (!string.IsNullOrEmpty(selectedResult.QuerySuggestionText))
|
||||
{
|
||||
selectedResultPossibleSuggestion = selectedResultActionKeyword + selectedResult.QuerySuggestionText;
|
||||
|
||||
// If this QuerySuggestionText does not start with the queryText, set it to null
|
||||
if (!selectedResultPossibleSuggestion.StartsWith(queryText, StringComparison.CurrentCultureIgnoreCase))
|
||||
{
|
||||
selectedResultPossibleSuggestion = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Then check Title as suggestion
|
||||
if (string.IsNullOrEmpty(selectedResultPossibleSuggestion))
|
||||
{
|
||||
selectedResultPossibleSuggestion = selectedResultActionKeyword + selectedResult.Title;
|
||||
|
||||
// If this QuerySuggestionText does not start with the queryText, set it to null
|
||||
if (!selectedResultPossibleSuggestion.StartsWith(queryText, StringComparison.CurrentCultureIgnoreCase))
|
||||
{
|
||||
selectedResultPossibleSuggestion = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(selectedResultPossibleSuggestion))
|
||||
return string.Empty;
|
||||
|
||||
|
||||
// For AutocompleteQueryCommand.
|
||||
// When user typed lower case and result title is uppercase, we still want to display suggestion
|
||||
selectedItem.QuerySuggestionText = queryText + selectedResultPossibleSuggestion.Substring(queryText.Length);
|
||||
selectedItem.QuerySuggestionText = string.Concat(queryText, selectedResultPossibleSuggestion.AsSpan(queryText.Length));
|
||||
|
||||
// Check if Text will be larger than our QueryTextBox
|
||||
Typeface typeface = new Typeface(queryTextBox.FontFamily, queryTextBox.FontStyle, queryTextBox.FontWeight, queryTextBox.FontStretch);
|
||||
|
|
|
|||
|
|
@ -1,20 +1,19 @@
|
|||
using Flow.Launcher.Plugin.Everything.Everything;
|
||||
using Flow.Launcher.Plugin.Explorer.Helper;
|
||||
using System;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace Flow.Launcher.Plugin.Explorer.Views.Converters;
|
||||
namespace Flow.Launcher.Converters;
|
||||
|
||||
public class EnumNameConverter : IValueConverter
|
||||
public class SharedSizeGroupConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return value is SortOption option ? option.GetTranslatedName() : value;
|
||||
return (Visibility)value != Visibility.Collapsed ? (string)parameter : null;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ using System.Windows.Input;
|
|||
|
||||
namespace Flow.Launcher.Converters;
|
||||
|
||||
class StringToKeyBindingConverter : IValueConverter
|
||||
public class StringToKeyBindingConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -119,7 +119,8 @@
|
|||
Grid.Column="1"
|
||||
Margin="10"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center" />
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding ActionKeyword}" />
|
||||
<Button
|
||||
x:Name="btnTestActionKeyword"
|
||||
Grid.Row="1"
|
||||
|
|
@ -150,7 +151,20 @@
|
|||
Margin="5 0 10 0"
|
||||
Click="btnAdd_OnClick"
|
||||
Style="{StaticResource AccentButtonStyle}">
|
||||
<TextBlock x:Name="lblAdd" Text="{DynamicResource done}" />
|
||||
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
|
||||
<TextBlock
|
||||
x:Name="tbAdd"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Text="{DynamicResource done}"
|
||||
Visibility="Collapsed" />
|
||||
<TextBlock
|
||||
x:Name="tbUpdate"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Text="{DynamicResource update}"
|
||||
Visibility="Collapsed" />
|
||||
</Grid>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue