Compare commits
1204 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69afc698dc | ||
|
|
babe564f3a | ||
|
|
c10666be80 | ||
|
|
67ad3fb8c5 | ||
|
|
586b17c2d2 | ||
|
|
4909a1a47f | ||
|
|
99e603413c | ||
|
|
875f5cb656 | ||
|
|
7ad65ba5dc | ||
|
|
7e969b1a94 | ||
|
|
5689980c87 | ||
|
|
b14a6451ed | ||
|
|
7a52828bd1 | ||
|
|
6926f5d3d5 | ||
|
|
cb6283364a | ||
|
|
1e220799be | ||
|
|
f0ad53e9d4 | ||
|
|
b0bf0bd802 | ||
|
|
e3fc35211f | ||
|
|
8c77bcea2a | ||
|
|
1693605305 | ||
|
|
93ad9d3a30 | ||
|
|
15bc4ab2af | ||
|
|
6c0942acad | ||
|
|
351e702238 | ||
|
|
8119d69c1a | ||
|
|
13d11dd927 | ||
|
|
f4c47071fb | ||
|
|
7043d16108 | ||
|
|
eb587c4341 | ||
|
|
80bd4725d1 | ||
|
|
25ff46d527 | ||
|
|
7acd86fc94 | ||
|
|
26047da35c | ||
|
|
d3ab4f8804 | ||
|
|
29940d45ba | ||
|
|
c6296b009a | ||
|
|
130a638e2b | ||
|
|
12a5d9e52b | ||
|
|
2e69cdb593 | ||
|
|
6d6427f8cd | ||
|
|
2d73d8577e | ||
|
|
19e72af527 | ||
|
|
1692c584f4 | ||
|
|
ab8e1b6afe | ||
|
|
9a7c4bb13a | ||
|
|
b97298099c | ||
|
|
a7a91e00a2 | ||
|
|
ad7d8276a2 | ||
|
|
6d652ad70b | ||
|
|
94d5b4ad92 | ||
|
|
792d2733ff | ||
|
|
07e4b5e89f | ||
|
|
fe271038cb | ||
|
|
51cb4859e3 | ||
|
|
b4667ca894 | ||
|
|
f79cfb6319 | ||
|
|
97279b7c16 | ||
|
|
5b8a64c356 | ||
|
|
29777d86e0 | ||
|
|
58440148b3 | ||
|
|
bfcf43d428 | ||
|
|
b8448d57e5 | ||
|
|
000104365c | ||
|
|
8141310f7b | ||
|
|
f432d9d187 | ||
|
|
5ab8ccc8c8 | ||
|
|
fd9818707e | ||
|
|
7422ff8624 | ||
|
|
58cf7b5bf3 | ||
|
|
87d71e1213 | ||
|
|
a0cde5352f | ||
|
|
b6ed1c047d | ||
|
|
8d8cb7381a | ||
|
|
f3bbf5e06c | ||
|
|
a5f9b42e6f | ||
|
|
d053101c14 | ||
|
|
3022b15d3a | ||
|
|
6b541e1d14 | ||
|
|
586b148ed3 | ||
|
|
cf47a758ee | ||
|
|
89230815a2 | ||
|
|
60a176bba2 | ||
|
|
44b5c010ba | ||
|
|
425c65528b | ||
|
|
8ac6f58d8e | ||
|
|
f6ac724c60 | ||
|
|
9010346c0b | ||
|
|
41a3a8f14d | ||
|
|
a883abcd1c | ||
|
|
ab37de8f40 | ||
|
|
ac0e15eaa7 | ||
|
|
a6a7e7f0c2 | ||
|
|
3cacdf9a3a | ||
|
|
3ebbda3690 | ||
|
|
74abb13acf | ||
|
|
86addc00fd | ||
|
|
9cebea225c | ||
|
|
59bdb30321 | ||
|
|
d51fb7dfa2 | ||
|
|
b955c51a91 | ||
|
|
86ecd1ad20 | ||
|
|
c089b3bdbd | ||
|
|
9612b213aa | ||
|
|
83c9b52708 | ||
|
|
a7bde7c08a | ||
|
|
c8613b5975 | ||
|
|
87bb728601 | ||
|
|
e1f9d4fb08 | ||
|
|
14e6c1186c | ||
|
|
abeb142f0b | ||
|
|
d416756614 | ||
|
|
823eb9e9a4 | ||
|
|
6579092f4a | ||
|
|
c242c09025 | ||
|
|
89c9ed598c | ||
|
|
a3a592bd16 | ||
|
|
a161829913 | ||
|
|
b4473ad067 | ||
|
|
4752f99fcf | ||
|
|
6e757cf15c | ||
|
|
a87810db88 | ||
|
|
0ddb3e8081 | ||
|
|
e29aed8bcf | ||
|
|
d99a3257af | ||
|
|
a772b210cd | ||
|
|
860700ab91 | ||
|
|
ff5d90d468 | ||
|
|
43773310d5 | ||
|
|
2da65645b0 | ||
|
|
0c62d64987 | ||
|
|
546f6c2f8f | ||
|
|
a6ee15e3ba | ||
|
|
51793847cf | ||
|
|
f308aa3847 | ||
|
|
695b994577 | ||
|
|
2301d04c61 | ||
|
|
e46ec5172c | ||
|
|
691e108c82 | ||
|
|
166146a8f8 | ||
|
|
2a42cfc80f | ||
|
|
aff1f20ebe | ||
|
|
804fed799d | ||
|
|
b6450ba47c | ||
|
|
14a4234583 | ||
|
|
72cd165992 | ||
|
|
239aec2083 | ||
|
|
a43ab60dcb | ||
|
|
484b7f74e6 | ||
|
|
8c1ddcb019 | ||
|
|
306a5badef | ||
|
|
e82003cd57 | ||
|
|
6a6aafe0a9 | ||
|
|
4bd440e167 | ||
|
|
80d198787f | ||
|
|
7f7bae477b | ||
|
|
2917814ecc | ||
|
|
e268a86f20 | ||
|
|
9a13831e91 | ||
|
|
9f6e57b9ff | ||
|
|
86cf7491c3 | ||
|
|
681ec710a1 | ||
|
|
dc40d61766 | ||
|
|
326ff4ce24 | ||
|
|
ca81642c5c | ||
|
|
32e4b9762b | ||
|
|
0c94852246 | ||
|
|
d2f67e402b | ||
|
|
145e55fe68 | ||
|
|
fd6358c233 | ||
|
|
1d047ce9ef | ||
|
|
a27a0b8872 | ||
|
|
0e98a9d500 | ||
|
|
728b2e0b53 | ||
|
|
220ffa49c6 | ||
|
|
74d1ec58de | ||
|
|
b2d19f7e70 | ||
|
|
7db8dfec62 | ||
|
|
b0c0767dc7 | ||
|
|
2e880869f3 | ||
|
|
1edf949a7a | ||
|
|
04c85668fe | ||
|
|
fcefd863cd | ||
|
|
64bac7e50f | ||
|
|
f94c1b1851 | ||
|
|
3a013db311 | ||
|
|
0fd141316e | ||
|
|
2ed4f389c9 | ||
|
|
9a1a25ed17 | ||
|
|
173fae7472 | ||
|
|
82c9df2214 | ||
|
|
a49b6b81ae | ||
|
|
dffa7003bc | ||
|
|
818ff4909a | ||
|
|
d1e054d5c5 | ||
|
|
7689da9acd | ||
|
|
0bf336643d | ||
|
|
70dc5d19e7 | ||
|
|
4da91b0fa3 | ||
|
|
cb219c57c4 | ||
|
|
0be5500d78 | ||
|
|
94dcccdce3 | ||
|
|
91b6ffdbe2 | ||
|
|
baa3a73984 | ||
|
|
dd5c231c47 | ||
|
|
3a0d4e1cbc | ||
|
|
631f031daa | ||
|
|
a487539e6a | ||
|
|
ad7454236a | ||
|
|
e182fea4d1 | ||
|
|
6615061658 | ||
|
|
5c1d423806 | ||
|
|
171f97645f | ||
|
|
9511c233a2 | ||
|
|
adfc2bd8cf | ||
|
|
2adba27dca | ||
|
|
b18c85fc8e | ||
|
|
2e79a43827 | ||
|
|
0fec34bb56 | ||
|
|
db3e133199 | ||
|
|
971cb91615 | ||
|
|
e5e04f3d59 | ||
|
|
fd9f7388e8 | ||
|
|
6ae2297246 | ||
|
|
c6fb2e68b5 | ||
|
|
c84c685a8f | ||
|
|
102f4a8818 | ||
|
|
661d72fa8c | ||
|
|
38df962e43 | ||
|
|
e02563733f | ||
|
|
c246d2d1fe | ||
|
|
8dbed9a8b5 | ||
|
|
6ea4fa82a7 | ||
|
|
0d78ca6352 | ||
|
|
b82ad80d1c | ||
|
|
eece2ce7dc | ||
|
|
3bfd7dd7ab | ||
|
|
b0f3dffc3c | ||
|
|
f5234d0c11 | ||
|
|
8a12ccb01e | ||
|
|
fac2e86240 | ||
|
|
39b49bb9de | ||
|
|
406ccfa2ce | ||
|
|
16e6b96cc7 | ||
|
|
1e37c8a742 | ||
|
|
5f0389b36f | ||
|
|
8762d85d57 | ||
|
|
018bbd43f1 | ||
|
|
756e3fc556 | ||
|
|
7ac5f5dbcb | ||
|
|
65c613fd24 | ||
|
|
e0ad9007a4 | ||
|
|
c3cda5f547 | ||
|
|
12f379c03d | ||
|
|
e760c34ede | ||
|
|
187a15a55d | ||
|
|
3e4751a6b5 | ||
|
|
b05d87196a | ||
|
|
b8297848f6 | ||
|
|
de0f004a48 | ||
|
|
0bc9c1d4d2 | ||
|
|
3fccb59544 | ||
|
|
ea76aabf91 | ||
|
|
dc1c900bd2 | ||
|
|
b1e59d8f2a | ||
|
|
006148dbd0 | ||
|
|
974ba258f5 | ||
|
|
29d833e9d1 | ||
|
|
eea72405bb | ||
|
|
e500f1ed0b | ||
|
|
6c3bd2e6a1 | ||
|
|
e54d8968e8 | ||
|
|
9dca2e989a | ||
|
|
610c5d6ef5 | ||
|
|
7797b34852 | ||
|
|
99e9f326f7 | ||
|
|
0f2d73239b | ||
|
|
497b489ea9 | ||
|
|
51581f106a | ||
|
|
43e9aa02e0 | ||
|
|
089a41cc2b | ||
|
|
279ab101cc | ||
|
|
c0f278652e | ||
|
|
ac02cc78bc | ||
|
|
d42ac3af6b | ||
|
|
20cfe76091 | ||
|
|
b68170b489 | ||
|
|
21273fa9ca | ||
|
|
700afb1050 | ||
|
|
d943544e56 | ||
|
|
e994eb01dd | ||
|
|
59d953bbc4 | ||
|
|
12ecbf80e1 | ||
|
|
a5b76b43bf | ||
|
|
383055bfe2 | ||
|
|
24212e8e4c | ||
|
|
1513a53dd4 | ||
|
|
fe9dd29964 | ||
|
|
eece2bcc0f | ||
|
|
3ca63c0523 | ||
|
|
b46982918d | ||
|
|
13b075a1f3 | ||
|
|
f71a417ac5 | ||
|
|
27065f2906 | ||
|
|
b5d938fd47 | ||
|
|
46300fc0d4 | ||
|
|
93882b8b36 | ||
|
|
f19c6b8dd0 | ||
|
|
968e2c5928 | ||
|
|
44599b2d33 | ||
|
|
613f75fd25 | ||
|
|
c09ab5482e | ||
|
|
6f68e6cb62 | ||
|
|
43ac412efd | ||
|
|
7149c98564 | ||
|
|
0f4e46a758 | ||
|
|
23fb927cd6 | ||
|
|
d055a260ab | ||
|
|
d55fa5f7c0 | ||
|
|
5cc49bd246 | ||
|
|
c3c49decd1 | ||
|
|
127c997ea1 | ||
|
|
724d52ba10 | ||
|
|
5cee0fbf55 | ||
|
|
e44760d0e3 | ||
|
|
29be3c4411 | ||
|
|
0eba068910 | ||
|
|
04dee3f14c | ||
|
|
f4fd658c36 | ||
|
|
0e80d5612c | ||
|
|
9302ebc667 | ||
|
|
2265866f58 | ||
|
|
8ec6ee5ef0 | ||
|
|
a03d250bdb | ||
|
|
a0ebac41ea | ||
|
|
b4b3f5db67 | ||
|
|
b291f59e10 | ||
|
|
02de453952 | ||
|
|
4c53f6e408 | ||
|
|
4f4d67cccc | ||
|
|
ef056f0503 | ||
|
|
a6f5755b42 | ||
|
|
5ce6e37973 | ||
|
|
ebebd7ad8b | ||
|
|
f6c47e3dab | ||
|
|
de711e12dc | ||
|
|
b94fed2f21 | ||
|
|
9316881048 | ||
|
|
c60a990938 | ||
|
|
e9aaa96185 | ||
|
|
cb497daee4 | ||
|
|
4881fe4778 | ||
|
|
5d5d8ffb18 | ||
|
|
b707812a7e | ||
|
|
9de50dc6fc | ||
|
|
11d308b53b | ||
|
|
0e03ec4a03 | ||
|
|
68a04fad96 | ||
|
|
ce75fd0f34 | ||
|
|
d46b71b2d0 | ||
|
|
6492afc46f | ||
|
|
c9b068e1b3 | ||
|
|
5cdf15a419 | ||
|
|
24db720927 | ||
|
|
94754d3d9b | ||
|
|
bffdae1a9b | ||
|
|
a8b68c2d89 | ||
|
|
84f70f13d8 | ||
|
|
ef1048ec08 | ||
|
|
37bc2110f5 | ||
|
|
84fd828d36 | ||
|
|
a51b4d7958 | ||
|
|
117467d708 | ||
|
|
789509f9cf | ||
|
|
a323dc213d | ||
|
|
0ec998a618 | ||
|
|
0f5e9d59a8 | ||
|
|
2280fda916 | ||
|
|
8d4db788a3 | ||
|
|
c2bfc472fe | ||
|
|
09ffd258b7 | ||
|
|
4ae92ca557 | ||
|
|
e066054681 | ||
|
|
a0f4aadd37 | ||
|
|
3566dbc37c | ||
|
|
73a23e06ba | ||
|
|
a03576d415 | ||
|
|
079db14127 | ||
|
|
2f4764a01f | ||
|
|
2671a9807b | ||
|
|
da46deb495 | ||
|
|
0700bd331f | ||
|
|
14676d9277 | ||
|
|
3f9c3611ec | ||
|
|
26263f4d6b | ||
|
|
105a644599 | ||
|
|
ba54234734 | ||
|
|
d39af2c8ff | ||
|
|
eb07b78cce | ||
|
|
f9a13a90a8 | ||
|
|
6302091c54 | ||
|
|
1647fe9818 | ||
|
|
3ece6cb4db | ||
|
|
b04732cc24 | ||
|
|
828819907b | ||
|
|
c8c449d2ef | ||
|
|
6b5c202e93 | ||
|
|
56c67ae9a5 | ||
|
|
94d9467bc3 | ||
|
|
fb65616cee | ||
|
|
7e488b3cb1 | ||
|
|
9c6084397e | ||
|
|
73a0fe6203 | ||
|
|
cac1872459 | ||
|
|
65f4ca6356 | ||
|
|
d771b3da57 | ||
|
|
d52a062e0c | ||
|
|
ef350398e1 | ||
|
|
69fa8834ee | ||
|
|
ac9bbd3986 | ||
|
|
6f99f0762a | ||
|
|
999fc3faba | ||
|
|
ebd33666fc | ||
|
|
8537c5b8c3 | ||
|
|
3ff59eff6e | ||
|
|
46752de0e8 | ||
|
|
27629db924 | ||
|
|
1f00fd9d6c | ||
|
|
637bf87dad | ||
|
|
41415432db | ||
|
|
745d26dade | ||
|
|
134de0fcd9 | ||
|
|
ee0c4c083f | ||
|
|
5873ce34e4 | ||
|
|
9904999dd2 | ||
|
|
f5a35ad04b | ||
|
|
7301734b82 | ||
|
|
19ada28cf3 | ||
|
|
7655b35b77 | ||
|
|
0c200eae20 | ||
|
|
18d6c9ec1f | ||
|
|
cc16a2c8de | ||
|
|
91a6502310 | ||
|
|
bb8720247e | ||
|
|
b01470339b | ||
|
|
855fe00346 | ||
|
|
f957258a2c | ||
|
|
945a247274 | ||
|
|
59ecfe5186 | ||
|
|
dc5e9939c7 | ||
|
|
79821783e2 | ||
|
|
531e53eaec | ||
|
|
4f01f23458 | ||
|
|
3760b84fb7 | ||
|
|
afdb442597 | ||
|
|
eadcd17bbb | ||
|
|
67ca63b047 | ||
|
|
17d3450bd2 | ||
|
|
d7c700a4e5 | ||
|
|
fd659e40fe | ||
|
|
bdd7c75ebd | ||
|
|
ca5648fec4 | ||
|
|
978325ceff | ||
|
|
87ad9c630d | ||
|
|
6b0ca1b6dd | ||
|
|
3c9a505f4f | ||
|
|
7bb4ace875 | ||
|
|
499cd86955 | ||
|
|
79bad50ce3 | ||
|
|
2dff9c92fd | ||
|
|
d05557819e | ||
|
|
22a39406ff | ||
|
|
91d0fd656e | ||
|
|
4440a04ab8 | ||
|
|
5bf617d1db | ||
|
|
3cf209edd9 | ||
|
|
ad95ce23ec | ||
|
|
9e29d5dd04 | ||
|
|
4678ac0255 | ||
|
|
8660f979b9 | ||
|
|
c20265ddf8 | ||
|
|
2b90606d9c | ||
|
|
c75fd8ab80 | ||
|
|
fc13d71960 | ||
|
|
354179de30 | ||
|
|
e4ea924b1a | ||
|
|
f9b5dda123 | ||
|
|
4cd38d4502 | ||
|
|
70c9a5b6a3 | ||
|
|
4fb46de074 | ||
|
|
b0487f534c | ||
|
|
719eb9b50f | ||
|
|
f3148405d7 | ||
|
|
6da13f18af | ||
|
|
b3b6f510d8 | ||
|
|
01400ab955 | ||
|
|
8348848ef8 | ||
|
|
04920e7eee | ||
|
|
464c5208dc | ||
|
|
981254875b | ||
|
|
d941135fff | ||
|
|
28663f5cdf | ||
|
|
bfc12711ad | ||
|
|
076e729e39 | ||
|
|
570f5ca82d | ||
|
|
9749a9018d | ||
|
|
4724f55e6b | ||
|
|
8f68636bfc | ||
|
|
fefcb1db53 | ||
|
|
98fd5c5d7c | ||
|
|
e97a76e488 | ||
|
|
02f25899e9 | ||
|
|
3a0ef5110d | ||
|
|
36c5df7d03 | ||
|
|
4252ed05ae | ||
|
|
2adf3026fd | ||
|
|
2cc705c556 | ||
|
|
faff8ab219 | ||
|
|
e63e647ebc | ||
|
|
e2339321bb | ||
|
|
f7c57b1f15 | ||
|
|
a1fad509cf | ||
|
|
daec33f827 | ||
|
|
cf66845a38 | ||
|
|
7199f5b545 | ||
|
|
0490956551 | ||
|
|
8be2108cdc | ||
|
|
4595e509c9 | ||
|
|
17d7953d11 | ||
|
|
4adfe0812d | ||
|
|
7ca9c8752b | ||
|
|
ab53d23404 | ||
|
|
9894271145 | ||
|
|
5d9960156c | ||
|
|
aa6782d29b | ||
|
|
98d683a6a5 | ||
|
|
55a2e4db79 | ||
|
|
4db985e2e5 | ||
|
|
bbd95beb36 | ||
|
|
a9ee0dc9a1 | ||
|
|
46d3770a28 | ||
|
|
99ee63ce1f | ||
|
|
cb10ad685e | ||
|
|
748a21fb54 | ||
|
|
b3ffe867c6 | ||
|
|
fb1ffec37d | ||
|
|
9ea4f7157a | ||
|
|
a9b43bd347 | ||
|
|
b9ac193e77 | ||
|
|
1d4947b3ae | ||
|
|
74e098e9b1 | ||
|
|
e5dae225f1 | ||
|
|
0339089972 | ||
|
|
1e8bd61264 | ||
|
|
79613bc8de | ||
|
|
d0ec785e32 | ||
|
|
020b91fd21 | ||
|
|
f2406152fd | ||
|
|
ab3ef7be53 | ||
|
|
bb7484ff2e | ||
|
|
80061fd076 | ||
|
|
124e005fb1 | ||
|
|
8e760ef202 | ||
|
|
de740f6453 | ||
|
|
3bde90ae62 | ||
|
|
aee8dc2e07 | ||
|
|
b3952542f8 | ||
|
|
910f23a3f6 | ||
|
|
5cc9ac85af | ||
|
|
e10e22d038 | ||
|
|
aaeaec36a2 | ||
|
|
c16a593f3c | ||
|
|
50d9f47576 | ||
|
|
a53fb578ef | ||
|
|
1f89859a03 | ||
|
|
7c1cee6748 | ||
|
|
a4949ad615 | ||
|
|
a57f4654b0 | ||
|
|
8192d3addf | ||
|
|
308f461ad4 | ||
|
|
513b2ba367 | ||
|
|
69f47fa984 | ||
|
|
4daf553514 | ||
|
|
aeae148e0b | ||
|
|
1a9a265671 | ||
|
|
b06e9ad4b3 | ||
|
|
7ecce43a3d | ||
|
|
82cb41c207 | ||
|
|
c356242a8f | ||
|
|
61ad7c2eef | ||
|
|
311104ddf0 | ||
|
|
2b106fd1c9 | ||
|
|
1784e008ee | ||
|
|
33ccdbd7af | ||
|
|
d15f1ddc13 | ||
|
|
e7ea01cd60 | ||
|
|
f7ed426441 | ||
|
|
d30485b82e | ||
|
|
4e646ab922 | ||
|
|
4183ede58d | ||
|
|
dd85e99e4b | ||
|
|
2334a88452 | ||
|
|
ee64b18f75 | ||
|
|
647dac9e7c | ||
|
|
df2496eb67 | ||
|
|
859424b895 | ||
|
|
dde3f38bde | ||
|
|
01744c258e | ||
|
|
231aadbc27 | ||
|
|
a806a628bd | ||
|
|
c529fee1fa | ||
|
|
c36a0252e6 | ||
|
|
66486ec38e | ||
|
|
6f5c6bf1d9 | ||
|
|
8fc8f2dfda | ||
|
|
d36e031e87 | ||
|
|
efac0af4b9 | ||
|
|
d7e73524ad | ||
|
|
80998c955f | ||
|
|
d824cb6207 | ||
|
|
ab80d679e1 | ||
|
|
d4881faab1 | ||
|
|
7c74d61b43 | ||
|
|
8ad2906a17 | ||
|
|
0f4d19080a | ||
|
|
a9a13be6ca | ||
|
|
c66badf39e | ||
|
|
6c2fc54612 | ||
|
|
807c0eaa98 | ||
|
|
7d7b1e89e9 | ||
|
|
cafb7408d9 | ||
|
|
723f741bca | ||
|
|
6a3cc0f8be | ||
|
|
66c548fa75 | ||
|
|
0b42f7e9c5 | ||
|
|
35a995eddc | ||
|
|
c3afb23625 | ||
|
|
f15d97585b | ||
|
|
5f03c072ff | ||
|
|
ce94aea51a | ||
|
|
a27ae070fc | ||
|
|
7bbdc6a4e2 | ||
|
|
3188002ecb | ||
|
|
284d2ca70f | ||
|
|
57961aec5d | ||
|
|
1675d7f2d0 | ||
|
|
baec8d24c3 | ||
|
|
820d0b37db | ||
|
|
c18ac16208 | ||
|
|
2bbc09bf13 | ||
|
|
a968723277 | ||
|
|
8668957738 | ||
|
|
4498729e75 | ||
|
|
ac550fad5b | ||
|
|
c511ac32b6 | ||
|
|
ee48ce0f4e | ||
|
|
8a3d64491b | ||
|
|
b72cc803f0 | ||
|
|
69dd5c85ef | ||
|
|
ea17b2e142 | ||
|
|
da62fc4dc6 | ||
|
|
239630f681 | ||
|
|
d95d0cf8cf | ||
|
|
2b25ba942c | ||
|
|
c65369a746 | ||
|
|
fa7d5e7853 | ||
|
|
8ac47cbd4d | ||
|
|
eb85844503 | ||
|
|
010d0ed331 | ||
|
|
1f8f7765a3 | ||
|
|
68f416dda3 | ||
|
|
49e45faec0 | ||
|
|
c81516350a | ||
|
|
890f393fd6 | ||
|
|
e46969c5c4 | ||
|
|
1ec9b55645 | ||
|
|
b0caf7c13b | ||
|
|
302fc15dd7 | ||
|
|
6a2cf1a1c9 | ||
|
|
8ea73bc54a | ||
|
|
7cbab3925f | ||
|
|
246a1bd2be | ||
|
|
f7e2a89ed6 | ||
|
|
f94252edb9 | ||
|
|
b7b6b9803f | ||
|
|
807d868b74 | ||
|
|
c3e8c4666c | ||
|
|
926651ebb3 | ||
|
|
a7d5624582 | ||
|
|
03209740ec | ||
|
|
af6ae3433e | ||
|
|
41f4dd1d57 | ||
|
|
d5c1b67675 | ||
|
|
0b18d74ac9 | ||
|
|
fb3fe5f8c0 | ||
|
|
796c973fd4 | ||
|
|
7c6335c4d1 | ||
|
|
af2267c486 | ||
|
|
56d9e62610 | ||
|
|
7e18a169d4 | ||
|
|
74280eda34 | ||
|
|
e1309d4d95 | ||
|
|
14aa6f7454 | ||
|
|
1368d7d24e | ||
|
|
8c09b170c3 | ||
|
|
080409b984 | ||
|
|
f0ec276547 | ||
|
|
23aafcd7bc | ||
|
|
3718a126ac | ||
|
|
846dd07bf4 | ||
|
|
ba60062a24 | ||
|
|
ed4f928fde | ||
|
|
2a09d550e5 | ||
|
|
bb1ae4e616 | ||
|
|
828aa70a56 | ||
|
|
4021f3131d | ||
|
|
80ef81ca64 | ||
|
|
fec13d012b | ||
|
|
e8ca3fc287 | ||
|
|
d5260d801c | ||
|
|
916b1ec1fc | ||
|
|
7380bb5001 | ||
|
|
2e95fb556a | ||
|
|
90591cbfa2 | ||
|
|
929409db71 | ||
|
|
4263375fb2 | ||
|
|
bb5d149ba4 | ||
|
|
1a322dc0d3 | ||
|
|
d10da94f20 | ||
|
|
7eb28881cb | ||
|
|
823e04d189 | ||
|
|
ca5d4aeadb | ||
|
|
a4d937c4f3 | ||
|
|
fa4add6797 | ||
|
|
ec2ed4e6c5 | ||
|
|
6bd41d9a54 | ||
|
|
1ff2a205bc | ||
|
|
dd73c3249b | ||
|
|
75eef49317 | ||
|
|
e8858e0c7d | ||
|
|
df9142a6bf | ||
|
|
36f312403b | ||
|
|
d8983889ae | ||
|
|
bfaf2f2d29 | ||
|
|
2ba9c284ba | ||
|
|
ef2b4af28a | ||
|
|
ba042cd07d | ||
|
|
f8cb4cff4f | ||
|
|
92010b787b | ||
|
|
e142a8c587 | ||
|
|
759dcaa8b8 | ||
|
|
05939dcf1e | ||
|
|
34494819f5 | ||
|
|
a9491b7fa5 | ||
|
|
311a676aea | ||
|
|
2eab9c581c | ||
|
|
1284499c25 | ||
|
|
a74471b9f8 | ||
|
|
81f61a5b87 | ||
|
|
7b2446b6e0 | ||
|
|
60898f7536 | ||
|
|
b2fa7870b6 | ||
|
|
6ef2ff711a | ||
|
|
9f58dca10e | ||
|
|
35e499720b | ||
|
|
7820a80241 | ||
|
|
ffc6409488 | ||
|
|
8eaa411a80 | ||
|
|
f08714f25a | ||
|
|
1f3352ff80 | ||
|
|
2601844970 | ||
|
|
e4bbb8b279 | ||
|
|
a13e2aa494 | ||
|
|
b8383a2280 | ||
|
|
2cb5ef03ce | ||
|
|
5203c3a576 | ||
|
|
36dfb2dc0b | ||
|
|
3f6434b5a3 | ||
|
|
d9595a3485 | ||
|
|
b3352d0c1c | ||
|
|
7e15df1f15 | ||
|
|
b7e086c326 | ||
|
|
659e7b0585 | ||
|
|
501ab48da5 | ||
|
|
3b9ceea64b | ||
|
|
f3c7bbbcbf | ||
|
|
9b21a2775e | ||
|
|
b669d4c5ea | ||
|
|
a744242c70 | ||
|
|
7f963f71f8 | ||
|
|
9f0ab53e1f | ||
|
|
5fc16e9fb7 | ||
|
|
4329afba1c | ||
|
|
01f87beef5 | ||
|
|
45fecfb4f6 | ||
|
|
9b020e09ae | ||
|
|
3b1c05aba4 | ||
|
|
90a111944a | ||
|
|
ceb029afb0 | ||
|
|
dc769ce6a0 | ||
|
|
66b7e74f84 | ||
|
|
29ef0dfaf4 | ||
|
|
90144948f4 | ||
|
|
bda384953e | ||
|
|
b0fd2a8413 | ||
|
|
2403971063 | ||
|
|
22a0379202 | ||
|
|
c06a426490 | ||
|
|
908e144e1b | ||
|
|
0189019e54 | ||
|
|
5995835d03 | ||
|
|
16e637b256 | ||
|
|
ac2522e860 | ||
|
|
fdb3ad0efc | ||
|
|
f7a01f3c32 | ||
|
|
94fd2c7eff | ||
|
|
f917dfbbb2 | ||
|
|
40bfad6810 | ||
|
|
fefde66b7b | ||
|
|
6869adcc09 | ||
|
|
f5abaec551 | ||
|
|
52fbf693b5 | ||
|
|
bf3995496e | ||
|
|
f7470a032a | ||
|
|
64c9fe5f03 | ||
|
|
febfbc3cc8 | ||
|
|
5ea848b62e | ||
|
|
96118a98b1 | ||
|
|
d25f3a2d4e | ||
|
|
c0600746b6 | ||
|
|
24cb34408b | ||
|
|
f90ae87017 | ||
|
|
273a71e759 | ||
|
|
d5c6a8f7f1 | ||
|
|
b6bb2c37a1 | ||
|
|
3ea1184bab | ||
|
|
a45e5c4874 | ||
|
|
d5926f1d5c | ||
|
|
c08ac5468a | ||
|
|
32ad152079 | ||
|
|
94372918ac | ||
|
|
3ce364a5be | ||
|
|
e4105ea248 | ||
|
|
604a8b6bd6 | ||
|
|
fc82a62af3 | ||
|
|
67aeb3e257 | ||
|
|
d371ec6d6e | ||
|
|
01509eaae9 | ||
|
|
b0df25241a | ||
|
|
56447743b2 | ||
|
|
301dc39648 | ||
|
|
3195568399 | ||
|
|
200b13c97f | ||
|
|
47173f6dbd | ||
|
|
83bf1fe047 | ||
|
|
aa8310905d | ||
|
|
a67a82d638 | ||
|
|
65b0807903 | ||
|
|
ca4702a684 | ||
|
|
a89ffc7ffd | ||
|
|
4e75270e49 | ||
|
|
e08a1dc183 | ||
|
|
8fa31968c0 | ||
|
|
27e25978f2 | ||
|
|
5a0761ef1a | ||
|
|
52c40e6f5c | ||
|
|
be7a18384c | ||
|
|
b4693229d2 | ||
|
|
dc28be0657 | ||
|
|
ce6f33522f | ||
|
|
200ac9d81e | ||
|
|
078398daba | ||
|
|
9ac73ec6fc | ||
|
|
a469134d2f | ||
|
|
57690808dd | ||
|
|
4edc8ef10c | ||
|
|
576e66c60c | ||
|
|
0e53f225d0 | ||
|
|
5d656913a8 | ||
|
|
49cea5f45d | ||
|
|
255435e419 | ||
|
|
1657338640 | ||
|
|
eb66731784 | ||
|
|
07c9abc87b | ||
|
|
f5e8bad1ae | ||
|
|
38e9533bfa | ||
|
|
2cd0273382 | ||
|
|
d62d572387 | ||
|
|
b65fe57c68 | ||
|
|
0e5f6ef2dd | ||
|
|
45112ccfcf | ||
|
|
d26e937066 | ||
|
|
bb1bb42214 | ||
|
|
07c16dcbe2 | ||
|
|
1b4f5613ac | ||
|
|
3315bcd73e | ||
|
|
87f33b9a15 | ||
|
|
c06d364344 | ||
|
|
cbab56c17b | ||
|
|
585c78c232 | ||
|
|
e569b20b9f | ||
|
|
590e18ee43 | ||
|
|
a8edd09eae | ||
|
|
f3dae6fb48 | ||
|
|
b4672c7f39 | ||
|
|
e60a7b61a8 | ||
|
|
e443e5ab0d | ||
|
|
29f773d212 | ||
|
|
86d09450b0 | ||
|
|
c081da9545 | ||
|
|
4d0f447b25 | ||
|
|
9cc7c1b123 | ||
|
|
354742a545 | ||
|
|
b64fbfb6e4 | ||
|
|
3a2ff5c8d2 | ||
|
|
a5a4d9e0c2 | ||
|
|
cfeef1621a | ||
|
|
2e25b09bdf | ||
|
|
5ba70376e6 | ||
|
|
7c07d6ecf8 | ||
|
|
2788ecc32e | ||
|
|
26dc9e93b3 | ||
|
|
14bf6c9ac3 | ||
|
|
1db9669ed2 | ||
|
|
c466e6dfa3 | ||
|
|
1c00921ee1 | ||
|
|
df432542b5 | ||
|
|
fe989750e7 | ||
|
|
e008dea61e | ||
|
|
7a8c7c963b | ||
|
|
0ecf285a95 | ||
|
|
b14500351f | ||
|
|
97693e25b8 | ||
|
|
12d573bc55 | ||
|
|
68562aaec9 | ||
|
|
2c7e714856 | ||
|
|
a7183cc519 | ||
|
|
bfffb2856d | ||
|
|
d2be747fc1 | ||
|
|
7fb44d2782 | ||
|
|
d7b46205fa | ||
|
|
0e0835c2f7 | ||
|
|
e81d6cb86f | ||
|
|
8eef01437c | ||
|
|
0d446ee293 | ||
|
|
c0afe1792e | ||
|
|
11fdcad57d | ||
|
|
2ac94907e8 | ||
|
|
f7669b2bbc | ||
|
|
89d200243b | ||
|
|
013b078a44 | ||
|
|
06aefdedc2 | ||
|
|
4071a12c11 | ||
|
|
83cdb6efd7 | ||
|
|
040c0d3027 | ||
|
|
a44d4d62d0 | ||
|
|
f51d588510 | ||
|
|
54b2f73384 | ||
|
|
dc63f693f0 | ||
|
|
455db754d8 | ||
|
|
cbef8fc044 | ||
|
|
d8e45f849a | ||
|
|
f83defb37b | ||
|
|
5c8db11536 | ||
|
|
30aa5b33a3 | ||
|
|
ca41e69a17 | ||
|
|
af4d8ab183 | ||
|
|
c1a24cfbd1 | ||
|
|
86b406e2a4 | ||
|
|
1ec93330b0 | ||
|
|
4ed3c5fefe | ||
|
|
54d106ff4e | ||
|
|
996669ec44 | ||
|
|
1d38ff521a | ||
|
|
f0c1409da4 | ||
|
|
9617198db7 | ||
|
|
e4863b1ebd | ||
|
|
d51a4cc29d | ||
|
|
dcbb225355 | ||
|
|
cba449b493 | ||
|
|
559fe1232b | ||
|
|
a7f9ca489e | ||
|
|
74bf75a82e | ||
|
|
747629cebf | ||
|
|
aca6e9272b | ||
|
|
18e6f9a1b5 | ||
|
|
c3532d5c35 | ||
|
|
1a88f45c93 | ||
|
|
b49eda7fc7 | ||
|
|
f0b3bc70b2 | ||
|
|
08700edc22 | ||
|
|
d698c9bbea | ||
|
|
eea1b8eb40 | ||
|
|
0118c5a87c | ||
|
|
65a3374d9c | ||
|
|
536225bf12 | ||
|
|
e858d35617 | ||
|
|
6d87e85353 | ||
|
|
d1fed35c3e | ||
|
|
d9297bd40e | ||
|
|
a1926dfc00 | ||
|
|
d55ba039af | ||
|
|
c9ebc5a9f6 | ||
|
|
be0c2d97ba | ||
|
|
a03cc76eaa | ||
|
|
504c71d5c4 | ||
|
|
d0291c68d7 | ||
|
|
33e5e41eff | ||
|
|
221d5a0b8d | ||
|
|
b899487ee9 | ||
|
|
ff0b724a73 | ||
|
|
647c20a6a3 | ||
|
|
c36e63aa8d | ||
|
|
cb523ea656 | ||
|
|
22b99f4f6d | ||
|
|
05e681b59a | ||
|
|
07ee91c17a | ||
|
|
323a344c3a | ||
|
|
0172b1cf90 | ||
|
|
5a5e4066f6 | ||
|
|
3256f5aa15 | ||
|
|
476a9a337f | ||
|
|
1f1ce58bd9 | ||
|
|
bbb3dd3397 | ||
|
|
d7cee77c8b | ||
|
|
e2ac975c75 | ||
|
|
5e9352316c | ||
|
|
c73adb7c0d | ||
|
|
c3b41f56d4 | ||
|
|
6fe841af58 | ||
|
|
d97307de6f | ||
|
|
826dc2327f | ||
|
|
3466e504e3 | ||
|
|
ebd7f80947 | ||
|
|
43e827d8f5 | ||
|
|
5d5dc2e152 | ||
|
|
c0d2eaa897 | ||
|
|
13c70fc445 | ||
|
|
1e11d28765 | ||
|
|
7ee9bc43a0 | ||
|
|
372085ec0e | ||
|
|
e758b35837 | ||
|
|
dee7b86120 | ||
|
|
b3ec7e7a3a | ||
|
|
bbd01d023a | ||
|
|
36282c9fa9 | ||
|
|
daea07c409 | ||
|
|
49d7445d0e | ||
|
|
9bb0e5616a | ||
|
|
7e98f595ee | ||
|
|
a9fdc4b16e | ||
|
|
c06819b502 | ||
|
|
969faca658 | ||
|
|
61fbb099e2 | ||
|
|
fbd6356be0 | ||
|
|
1c40455bf3 | ||
|
|
15129634a6 | ||
|
|
89d35d7734 | ||
|
|
d959bfb4c9 | ||
|
|
9bc25514ae | ||
|
|
25210b57ba | ||
|
|
c9317e369c | ||
|
|
2195c19ed9 | ||
|
|
d13a9d72c9 | ||
|
|
55d230e41c | ||
|
|
ffa604f921 | ||
|
|
93bdfeeaa7 | ||
|
|
336c64e5a9 | ||
|
|
20a706d93d | ||
|
|
21cb8844fc | ||
|
|
4bc1a3b1d0 | ||
|
|
9df1f7cfa6 | ||
|
|
72cdf86802 | ||
|
|
9a53d5fdab | ||
|
|
458e6333a0 | ||
|
|
f42e483260 | ||
|
|
dda2135129 | ||
|
|
bc2831c17e | ||
|
|
310313c3b2 | ||
|
|
2122568461 | ||
|
|
144fd5b740 | ||
|
|
5f7a1a8203 | ||
|
|
258b396444 | ||
|
|
5861533793 | ||
|
|
3106bd09e8 | ||
|
|
a3475a02d2 | ||
|
|
fb1a1f35ad | ||
|
|
71aadde901 | ||
|
|
f7bfd8c206 | ||
|
|
ff83e5271b | ||
|
|
ec6edb2864 | ||
|
|
4f4a6f60c5 | ||
|
|
354e13678b | ||
|
|
f1b8822e20 | ||
|
|
6bf83b1034 | ||
|
|
7b1e4ede2a | ||
|
|
a554820115 | ||
|
|
cb9f9ea5b3 | ||
|
|
4051553a56 | ||
|
|
01a213354b | ||
|
|
962abfb38e | ||
|
|
ada4cf0dfd | ||
|
|
aff1137c3d | ||
|
|
7f24c27201 | ||
|
|
51c5e34220 | ||
|
|
10c2cc64a2 | ||
|
|
a14d21f8e4 | ||
|
|
8de715a153 | ||
|
|
e9ebe832de | ||
|
|
2c8feccea1 | ||
|
|
36083e654f | ||
|
|
8790b1938a | ||
|
|
7cd48ce3c4 | ||
|
|
4300f1005d | ||
|
|
9dec9c370c | ||
|
|
e9f460279a | ||
|
|
06b322e575 | ||
|
|
80d8d8c0cd | ||
|
|
4450db3be9 | ||
|
|
b875489ad3 | ||
|
|
ece3f3822f | ||
|
|
0780cdc80b | ||
|
|
1c0e027abb | ||
|
|
0dcb666bbd | ||
|
|
00ebca7a01 | ||
|
|
f9c8820303 | ||
|
|
3d414edda9 | ||
|
|
0e3f6f1d5e | ||
|
|
c4b0ef38c0 | ||
|
|
2e800bb2de | ||
|
|
505b970497 | ||
|
|
986bfd481c | ||
|
|
ab8d480a01 | ||
|
|
7e19b76403 | ||
|
|
cbc2f103ac | ||
|
|
6a7922cf17 | ||
|
|
8e23de2718 | ||
|
|
11aec5fba1 | ||
|
|
c98f213e2e | ||
|
|
8c11cfcad6 | ||
|
|
5fe88ffc6a | ||
|
|
7a8c7a4ee1 | ||
|
|
3343325195 | ||
|
|
edda16dc06 | ||
|
|
7a3d120123 | ||
|
|
92419c9182 | ||
|
|
68bb5bf371 | ||
|
|
b0d8bfcadd | ||
|
|
a58ad1daf6 | ||
|
|
657c6056c4 | ||
|
|
84daba8902 | ||
|
|
3e37f8a269 | ||
|
|
28d178c4be | ||
|
|
f560cd5930 | ||
|
|
dbf24c6cd3 | ||
|
|
0a4989ca47 | ||
|
|
507dbf5d0f | ||
|
|
536f36f4e7 | ||
|
|
c523ba2287 | ||
|
|
a3f1cbb3b1 | ||
|
|
561bb2f0f4 | ||
|
|
dbbd1b5f2c | ||
|
|
7f6ec2f732 | ||
|
|
e8649203cf | ||
|
|
765b0c3d6d | ||
|
|
248c40731c | ||
|
|
cde01a4863 | ||
|
|
1ee8a0188b | ||
|
|
32e8fc9bf5 | ||
|
|
3c87d895c5 | ||
|
|
82dd8a9b48 | ||
|
|
0996506fd1 | ||
|
|
3ee38ee1e2 | ||
|
|
cf9401d81c | ||
|
|
dbed5cc247 | ||
|
|
625a77fd74 | ||
|
|
f7856cd71a | ||
|
|
3e56c32376 | ||
|
|
56ecad07cd | ||
|
|
de8ef5f10b | ||
|
|
022df97b89 | ||
|
|
662480ef4c | ||
|
|
446743c4cf | ||
|
|
ebb670b328 | ||
|
|
74d253cdc3 | ||
|
|
a368b4a271 | ||
|
|
16a71f4c65 | ||
|
|
3790bca2e7 | ||
|
|
34fd830859 | ||
|
|
9c0be2fe7a | ||
|
|
e5f7f593b2 | ||
|
|
b189a9a435 | ||
|
|
479172f34d | ||
|
|
9eb2a43cd2 | ||
|
|
1110df67d9 | ||
|
|
9a88313199 | ||
|
|
f41560e285 | ||
|
|
4d37a1aca1 | ||
|
|
01fbb0e663 | ||
|
|
91e4bd7475 | ||
|
|
3a111499f1 | ||
|
|
4548981d1b | ||
|
|
d026d26ae9 | ||
|
|
cca7cd7962 |
2
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
github: [jonasbark]
|
||||
custom: ["https://paypal.me/boni", "https://donate.stripe.com/9B6aEX0muajY8bZ1Kl6J200"]
|
||||
342
.github/workflows/build.yml
vendored
@@ -1,95 +1,323 @@
|
||||
name: "Build"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
build_mac:
|
||||
description: 'Build for macOS'
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
build_github:
|
||||
description: 'Build for GitHub'
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
build_windows:
|
||||
description: 'Build for Windows'
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
build_android:
|
||||
description: 'Build for Android'
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
build_ios:
|
||||
description: 'Build for iOS'
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
SHOREBIRD_TOKEN: ${{ secrets.SHOREBIRD_TOKEN }}
|
||||
FLUTTER_VERSION_MAC: 3.41.0-0.0.pre
|
||||
FLUTTER_VERSION: 3.38.7
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build & Release
|
||||
runs-on: macos-latest
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
pages: write
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
#1 Checkout Repository
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
#2 Setup Java
|
||||
- name: Set Up Java
|
||||
uses: actions/setup-java@v3.12.0
|
||||
with:
|
||||
distribution: 'oracle'
|
||||
java-version: '17'
|
||||
submodules: recursive
|
||||
token: ${{ secrets.PAT_TOKEN }}
|
||||
|
||||
#3 Setup Flutter
|
||||
- name: Set Up Flutter
|
||||
- name: Install certificates
|
||||
if: inputs.build_mac || inputs.build_ios
|
||||
env:
|
||||
DEVELOPER_ID_APPLICATION_P12_BASE64_MAC: ${{ secrets.DEVELOPER_ID_APPLICATION_P12_BASE64_MAC }}
|
||||
DEVELOPER_ID_INSTALLER_P12_BASE64_MAC: ${{ secrets.DEVELOPER_ID_INSTALLER_P12_BASE64_MAC }}
|
||||
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
|
||||
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
|
||||
APPSTORE_PROFILE_IOS_BASE64: ${{ secrets.APPSTORE_PROFILE_IOS_BASE64 }}
|
||||
APPSTORE_PROFILE_MACOS_BASE64: ${{ secrets.APPSTORE_PROFILE_MACOS_BASE64 }}
|
||||
APPSTORE_PROFILE_DEV_IOS_BASE64: ${{ secrets.APPSTORE_PROFILE_DEV_IOS_BASE64 }}
|
||||
run: |
|
||||
# create variables
|
||||
DEVELOPER_ID_APPLICATION_CERTIFICATE_PATH=$RUNNER_TEMP/build_developerID_application_certificate.p12
|
||||
DEVELOPER_ID_INSTALLER_CERTIFICATE_PATH=$RUNNER_TEMP/build_developerID_installer_certificate.p12
|
||||
PP_PATH_IOS=$RUNNER_TEMP/build_pp_ios.mobileprovision
|
||||
PP_PATH_IOS_DEV=$RUNNER_TEMP/build_pp_ios_dev.mobileprovision
|
||||
PP_PATH_MACOS=$RUNNER_TEMP/build_pp_macos.provisionprofile
|
||||
KEYCHAIN_PATH=$RUNNER_TEMP/pg-signing.keychain-db
|
||||
|
||||
# import certificate and provisioning profile from secrets
|
||||
echo -n "$DEVELOPER_ID_APPLICATION_P12_BASE64_MAC" | base64 --decode --output $DEVELOPER_ID_APPLICATION_CERTIFICATE_PATH
|
||||
echo -n "$DEVELOPER_ID_INSTALLER_P12_BASE64_MAC" | base64 --decode --output $DEVELOPER_ID_INSTALLER_CERTIFICATE_PATH
|
||||
echo -n "$APPSTORE_PROFILE_IOS_BASE64" | base64 --decode -o $PP_PATH_IOS
|
||||
echo -n "$APPSTORE_PROFILE_DEV_IOS_BASE64" | base64 --decode -o $PP_PATH_IOS_DEV
|
||||
echo -n "$APPSTORE_PROFILE_MACOS_BASE64" | base64 --decode -o $PP_PATH_MACOS
|
||||
|
||||
# create temporary keychain
|
||||
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||
# security default-keychain -s $KEYCHAIN_PATH
|
||||
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
|
||||
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||
|
||||
# import certificate to keychain
|
||||
security import $DEVELOPER_ID_APPLICATION_CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
|
||||
security import $DEVELOPER_ID_INSTALLER_CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
|
||||
security list-keychain -d user -s $KEYCHAIN_PATH
|
||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||
|
||||
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
|
||||
cp $PP_PATH_IOS ~/Library/MobileDevice/Provisioning\ Profiles
|
||||
cp $PP_PATH_IOS_DEV ~/Library/MobileDevice/Provisioning\ Profiles
|
||||
cp $PP_PATH_MACOS ~/Library/MobileDevice/Provisioning\ Profiles
|
||||
|
||||
- name: 🐦 Setup Shorebird
|
||||
#if: inputs.build_mac || inputs.build_android || inputs.build_ios
|
||||
if: inputs.build_android || inputs.build_ios
|
||||
uses: shorebirdtech/setup-shorebird@v1
|
||||
with:
|
||||
cache: true
|
||||
|
||||
- name: Set Up Flutter maCOS
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: 'beta'
|
||||
flutter-version: ${{ env.FLUTTER_VERSION_MAC }}
|
||||
|
||||
- name: Generate translation files
|
||||
run: |
|
||||
flutter pub global activate intl_utils;
|
||||
flutter pub global run intl_utils:generate;
|
||||
|
||||
- name: 🚀 Shorebird Release macOS
|
||||
if: inputs.build_mac && false
|
||||
uses: shorebirdtech/shorebird-release@v1
|
||||
with:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
platform: macos
|
||||
args: "-- --obfuscate --split-debug-info=symbols --dart-define=VERIFYING_SHARED_SECRET=${{ secrets.VERIFYING_SHARED_SECRET }} --dart-define=REVENUECAT_API_KEY_IOS=${{ secrets.REVENUECAT_API_KEY_IOS }}"
|
||||
|
||||
- name: Flutter Release macOS
|
||||
if: inputs.build_mac
|
||||
run:
|
||||
flutter build macos --release --obfuscate --split-debug-info=symbols --dart-define=VERIFYING_SHARED_SECRET=${{ secrets.VERIFYING_SHARED_SECRET }} --dart-define=REVENUECAT_API_KEY_IOS=${{ secrets.REVENUECAT_API_KEY_IOS }}
|
||||
|
||||
- name: Set Up Flutter Rest
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: 'stable'
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
|
||||
#4 Install Dependencies
|
||||
- name: Install Dependencies
|
||||
run: flutter pub get
|
||||
- name: Decode Keystore
|
||||
if: inputs.build_android
|
||||
run: |
|
||||
echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > android/android.keystore;
|
||||
echo "${{ secrets.KEYSTORE_PROPERTIES }}" > android/keystore.properties;
|
||||
|
||||
#6 Building APK
|
||||
- name: Build APK
|
||||
run: flutter build apk --release
|
||||
|
||||
#8 Build app ( macos Build )
|
||||
- name: Build IPA
|
||||
run: flutter build macos --release
|
||||
|
||||
- name: Build Web
|
||||
run: flutter build web --release
|
||||
|
||||
#9 Upload Artifacts
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
- name: 🚀 Shorebird Release Android
|
||||
if: inputs.build_android
|
||||
uses: shorebirdtech/shorebird-release@v1
|
||||
with:
|
||||
name: Releases
|
||||
path: |
|
||||
build/app/outputs/flutter-apk/app-release.apk
|
||||
build/macos/Build/Products/Release/SwiftControl.app
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
platform: android
|
||||
args: "-- --obfuscate --split-debug-info=symbols --dart-define=REVENUECAT_API_KEY_ANDROID=${{ secrets.REVENUECAT_API_KEY_ANDROID }}"
|
||||
|
||||
- name: Extract latest changelog
|
||||
id: changelog
|
||||
run: |
|
||||
chmod +x scripts/get_latest_changelog.sh
|
||||
mkdir -p whatsnew
|
||||
./scripts/get_latest_changelog.sh | head -c 500 > whatsnew/whatsnew-en-US
|
||||
|
||||
- name: Generate release body
|
||||
if: inputs.build_github
|
||||
run: |
|
||||
chmod +x scripts/generate_release_body.sh
|
||||
./scripts/generate_release_body.sh > /tmp/release_body.md
|
||||
|
||||
- name: 🚀 Shorebird Release iOS
|
||||
if: inputs.build_ios
|
||||
uses: shorebirdtech/shorebird-release@v1
|
||||
with:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
platform: ios
|
||||
args: "--export-options-plist ios/ExportOptions.plist -- --dart-define=VERIFYING_SHARED_SECRET=${{ secrets.VERIFYING_SHARED_SECRET }} --dart-define=REVENUECAT_API_KEY_IOS=${{ secrets.REVENUECAT_API_KEY_IOS }}"
|
||||
|
||||
- name: Prepare App Store authentication key
|
||||
if: inputs.build_ios || inputs.build_mac
|
||||
env:
|
||||
API_KEY_BASE64: ${{ secrets.APPSTORE_API_KEY_FILE_BASE64 }}
|
||||
APPSTORE_API_KEY: ${{ secrets.APPSTORE_API_KEY }}
|
||||
run: |
|
||||
mkdir -p ./private_keys;
|
||||
printf %s "$API_KEY_BASE64" | base64 -D > "./private_keys/AuthKey_${APPSTORE_API_KEY}.p8";
|
||||
|
||||
- name: Upload to Play Store
|
||||
if: inputs.build_android
|
||||
uses: r0adkll/upload-google-play@v1
|
||||
with:
|
||||
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
|
||||
packageName: de.jonasbark.swiftcontrol
|
||||
releaseFiles: build/app/outputs/bundle/release/app-release.aab
|
||||
track: ${{ github.ref == 'refs/heads/main' && 'production' || 'alpha' }}
|
||||
whatsNewDirectory: whatsnew
|
||||
|
||||
- name: Upload to macOS App Store
|
||||
if: inputs.build_mac
|
||||
env:
|
||||
APPSTORE_API_KEY: ${{ secrets.APPSTORE_API_KEY }}
|
||||
APPSTORE_API_ISSUER_ID: ${{ secrets.APPSTORE_API_ISSUER_ID }}
|
||||
run: |
|
||||
productbuild --component "build/macos/Build/Products/Release/BikeControl.app" /Applications "BikeControl.pkg" --sign "3rd Party Mac Developer Installer: JONAS TASSILO BARK (UZRHKPVWN9)";
|
||||
xcrun altool --upload-app -f BikeControl.pkg -t osx --apiKey "$APPSTORE_API_KEY" --apiIssuer "$APPSTORE_API_ISSUER_ID";
|
||||
|
||||
- name: Upload to iOS App Store
|
||||
if: inputs.build_ios
|
||||
env:
|
||||
APPSTORE_API_KEY: ${{ secrets.APPSTORE_API_KEY }}
|
||||
APPSTORE_API_ISSUER_ID: ${{ secrets.APPSTORE_API_ISSUER_ID }}
|
||||
run: |
|
||||
xcrun altool --upload-app -f build/ios/ipa/swift_play.ipa -t ios --apiKey "$APPSTORE_API_KEY" --apiIssuer "$APPSTORE_API_ISSUER_ID";
|
||||
|
||||
|
||||
#10 Extract Version
|
||||
- name: Extract version from pubspec.yaml
|
||||
if: inputs.build_github
|
||||
id: extract_version
|
||||
run: |
|
||||
version=$(grep '^version: ' pubspec.yaml | cut -d ' ' -f 2 | tr -d '\r')
|
||||
echo "VERSION=$version" >> $GITHUB_ENV
|
||||
|
||||
#11 Check if Tag Exists
|
||||
- name: Check if Tag Exists
|
||||
id: check_tag
|
||||
run: |
|
||||
if git rev-parse "v${{ env.VERSION }}" >/dev/null 2>&1; then
|
||||
echo "TAG_EXISTS=true" >> $GITHUB_ENV
|
||||
else
|
||||
echo "TAG_EXISTS=false" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
#12 Modify Tag if it Exists
|
||||
- name: Modify Tag
|
||||
if: env.TAG_EXISTS == 'true'
|
||||
id: modify_tag
|
||||
run: |
|
||||
new_version="${{ env.VERSION }}-build-${{ github.run_number }}"
|
||||
echo "VERSION=$new_version" >> $GITHUB_ENV
|
||||
- name: Upload Symbols
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
overwrite: true
|
||||
name: Symbols
|
||||
path: symbols/
|
||||
|
||||
#13 Create Release
|
||||
- name: Create Release
|
||||
if: inputs.build_github
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
artifacts: "build/app/outputs/flutter-apk/app-release.apk,build/macos/Build/Products/Release/SwiftControl.app"
|
||||
artifacts: "build/app/outputs/flutter-apk/BikeControl.android.apk,build/macos/Build/Products/Release/BikeControl.macos.zip"
|
||||
allowUpdates: true
|
||||
prerelease: true
|
||||
bodyFile: /tmp/release_body.md
|
||||
tag: v${{ env.VERSION }}
|
||||
token: ${{ secrets.TOKEN }}
|
||||
|
||||
- name: Upload static files as artifact
|
||||
id: deployment
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: build/web
|
||||
windows:
|
||||
if: inputs.build_windows
|
||||
name: Build & Release on Windows
|
||||
runs-on: windows-2025
|
||||
|
||||
- name: Web Deploy
|
||||
uses: actions/deploy-pages@v4
|
||||
steps:
|
||||
#1 Checkout Repository
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
token: ${{ secrets.PAT_TOKEN }}
|
||||
|
||||
- name: Extract version from pubspec.yaml (Windows)
|
||||
shell: pwsh
|
||||
run: |
|
||||
$version = Select-String '^version: ' pubspec.yaml | ForEach-Object {
|
||||
($_ -split ' ')[1].Trim()
|
||||
}
|
||||
echo "VERSION=$version" >> $env:GITHUB_ENV
|
||||
|
||||
- name: 🐦 Setup Shorebird
|
||||
uses: shorebirdtech/setup-shorebird@v1
|
||||
with:
|
||||
cache: true
|
||||
|
||||
- name: Set Up Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: 'stable'
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
|
||||
- name: Generate translation files
|
||||
run: |
|
||||
flutter pub global activate intl_utils;
|
||||
flutter pub global run intl_utils:generate;
|
||||
|
||||
- name: 🚀 Shorebird Release Windows
|
||||
uses: shorebirdtech/shorebird-release@v1
|
||||
with:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
platform: windows
|
||||
args: "-- --obfuscate --split-debug-info=symbols-win"
|
||||
|
||||
- name: Zip directory (Windows)
|
||||
shell: pwsh
|
||||
run: |
|
||||
|
||||
$source = "C:\Windows\System32"
|
||||
$destination = "build\windows\x64\runner\Release"
|
||||
|
||||
# List of required DLLs
|
||||
$dlls = @("msvcp140.dll", "vcruntime140.dll", "vcruntime140_1.dll")
|
||||
|
||||
# Copy each file
|
||||
foreach ($dll in $dlls) {
|
||||
$srcPath = Join-Path $source $dll
|
||||
$destPath = Join-Path $destination $dll
|
||||
|
||||
if (Test-Path $srcPath) {
|
||||
Copy-Item -Path $srcPath -Destination $destPath -Force
|
||||
Write-Output "Copied $dll to $destination"
|
||||
} else {
|
||||
Write-Warning "$dll not found in $source"
|
||||
}
|
||||
}
|
||||
Compress-Archive -Path "build/windows/x64/runner/Release/*" -DestinationPath "build/windows/x64/runner/Release/bike_control.windows.zip"
|
||||
|
||||
- uses: microsoft/setup-msstore-cli@v1
|
||||
if: false
|
||||
|
||||
- name: Configure the Microsoft Store CLI
|
||||
if: false
|
||||
run: msstore reconfigure --tenantId $ --clientId $ --clientSecret $ --sellerId $
|
||||
|
||||
- name: Create MSIX package
|
||||
run: dart run msix:create
|
||||
|
||||
- name: Publish MSIX to the Microsoft Store
|
||||
if: false
|
||||
run: msstore publish -v "build/windows/x64/runner/Release/"
|
||||
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
overwrite: true
|
||||
name: Releases
|
||||
path: |
|
||||
build/windows/x64/runner/Release/bike_control.msix
|
||||
symbols-win/
|
||||
|
||||
190
.github/workflows/patch.yml
vendored
Normal file
@@ -0,0 +1,190 @@
|
||||
name: "Patch"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
SHOREBIRD_TOKEN: ${{ secrets.SHOREBIRD_TOKEN }}
|
||||
FLUTTER_VERSION: 3.38.5
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Patch iOS, Android & macOS
|
||||
runs-on: macos-latest
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
pages: write
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
#1 Checkout Repository
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
token: ${{ secrets.PAT_TOKEN }}
|
||||
|
||||
- name: 🐦 Setup Shorebird
|
||||
uses: shorebirdtech/setup-shorebird@v1
|
||||
with:
|
||||
cache: true
|
||||
|
||||
- name: Set Up Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: 'stable'
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
|
||||
- name: Generate translation files
|
||||
run: |
|
||||
flutter pub global activate intl_utils;
|
||||
flutter pub global run intl_utils:generate;
|
||||
|
||||
- name: Install certificates
|
||||
env:
|
||||
DEVELOPER_ID_APPLICATION_P12_BASE64_MAC: ${{ secrets.DEVELOPER_ID_APPLICATION_P12_BASE64_MAC }}
|
||||
DEVELOPER_ID_INSTALLER_P12_BASE64_MAC: ${{ secrets.DEVELOPER_ID_INSTALLER_P12_BASE64_MAC }}
|
||||
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
|
||||
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
|
||||
APPSTORE_PROFILE_IOS_BASE64: ${{ secrets.APPSTORE_PROFILE_IOS_BASE64 }}
|
||||
APPSTORE_PROFILE_MACOS_BASE64: ${{ secrets.APPSTORE_PROFILE_MACOS_BASE64 }}
|
||||
APPSTORE_PROFILE_DEV_IOS_BASE64: ${{ secrets.APPSTORE_PROFILE_DEV_IOS_BASE64 }}
|
||||
run: |
|
||||
# create variables
|
||||
DEVELOPER_ID_APPLICATION_CERTIFICATE_PATH=$RUNNER_TEMP/build_developerID_application_certificate.p12
|
||||
DEVELOPER_ID_INSTALLER_CERTIFICATE_PATH=$RUNNER_TEMP/build_developerID_installer_certificate.p12
|
||||
PP_PATH_IOS=$RUNNER_TEMP/build_pp_ios.mobileprovision
|
||||
PP_PATH_IOS_DEV=$RUNNER_TEMP/build_pp_ios_dev.mobileprovision
|
||||
PP_PATH_MACOS=$RUNNER_TEMP/build_pp_macos.provisionprofile
|
||||
KEYCHAIN_PATH=$RUNNER_TEMP/pg-signing.keychain-db
|
||||
|
||||
# import certificate and provisioning profile from secrets
|
||||
echo -n "$DEVELOPER_ID_APPLICATION_P12_BASE64_MAC" | base64 --decode --output $DEVELOPER_ID_APPLICATION_CERTIFICATE_PATH
|
||||
echo -n "$DEVELOPER_ID_INSTALLER_P12_BASE64_MAC" | base64 --decode --output $DEVELOPER_ID_INSTALLER_CERTIFICATE_PATH
|
||||
echo -n "$APPSTORE_PROFILE_IOS_BASE64" | base64 --decode -o $PP_PATH_IOS
|
||||
echo -n "$APPSTORE_PROFILE_DEV_IOS_BASE64" | base64 --decode -o $PP_PATH_IOS_DEV
|
||||
echo -n "$APPSTORE_PROFILE_MACOS_BASE64" | base64 --decode -o $PP_PATH_MACOS
|
||||
|
||||
# create temporary keychain
|
||||
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||
# security default-keychain -s $KEYCHAIN_PATH
|
||||
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
|
||||
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||
|
||||
# import certificate to keychain
|
||||
security import $DEVELOPER_ID_APPLICATION_CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
|
||||
security import $DEVELOPER_ID_INSTALLER_CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
|
||||
security list-keychain -d user -s $KEYCHAIN_PATH
|
||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||
|
||||
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
|
||||
cp $PP_PATH_IOS ~/Library/MobileDevice/Provisioning\ Profiles
|
||||
cp $PP_PATH_IOS_DEV ~/Library/MobileDevice/Provisioning\ Profiles
|
||||
cp $PP_PATH_MACOS ~/Library/MobileDevice/Provisioning\ Profiles
|
||||
|
||||
- name: Decode Keystore
|
||||
run: |
|
||||
echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > android/android.keystore;
|
||||
echo "${{ secrets.KEYSTORE_PROPERTIES }}" > android/keystore.properties;
|
||||
|
||||
- name: 🚀 Shorebird Patch macOS
|
||||
if: false # patch doesn't work: https://github.com/jonasbark/swiftcontrol/issues/143
|
||||
uses: shorebirdtech/shorebird-patch@v1
|
||||
with:
|
||||
platform: macos
|
||||
release-version: latest
|
||||
args: '--allow-asset-diffs --allow-native-diffs -- --dart-define=VERIFYING_SHARED_SECRET=${{ secrets.VERIFYING_SHARED_SECRET }} --dart-define=REVENUECAT_API_KEY_IOS=${{ secrets.REVENUECAT_API_KEY_IOS }}'
|
||||
|
||||
- name: 🚀 Shorebird Patch Android
|
||||
uses: shorebirdtech/shorebird-patch@v1
|
||||
with:
|
||||
platform: android
|
||||
release-version: latest
|
||||
args: '--allow-asset-diffs --allow-native-diffs -- --dart-define=REVENUECAT_API_KEY_ANDROID=${{ secrets.REVENUECAT_API_KEY_ANDROID }}'
|
||||
|
||||
- name: 🚀 Shorebird Patch iOS
|
||||
uses: shorebirdtech/shorebird-patch@v1
|
||||
with:
|
||||
platform: ios
|
||||
release-version: latest
|
||||
args: '--allow-asset-diffs --allow-native-diffs -- --dart-define=VERIFYING_SHARED_SECRET=${{ secrets.VERIFYING_SHARED_SECRET }} --dart-define=REVENUECAT_API_KEY_IOS=${{ secrets.REVENUECAT_API_KEY_IOS }}'
|
||||
|
||||
# shorebird struggles with the app from GitHub
|
||||
- name: Build macOS
|
||||
if: false
|
||||
run: flutter build macos --release;
|
||||
|
||||
- name: Sign macOS build
|
||||
if: false
|
||||
env:
|
||||
DEVELOPER_ID_APPLICATION_SIGNING_IDENTITY: ${{ secrets.DEVELOPER_ID_APPLICATION_SIGNING_IDENTITY }}
|
||||
run: |
|
||||
version=$(grep '^version: ' pubspec.yaml | cut -d ' ' -f 2 | tr -d '\r');
|
||||
echo "VERSION=$version" >> $GITHUB_ENV;
|
||||
cd build/macos/Build/Products/Release/;
|
||||
/usr/bin/codesign --deep --force -s "$DEVELOPER_ID_APPLICATION_SIGNING_IDENTITY" --entitlements ../../../../../macos/Runner/Release.entitlements --options runtime BikeControl.app -v;
|
||||
zip -r BikeControl.macos.zip BikeControl.app/;
|
||||
|
||||
#9 Upload Artifacts
|
||||
- name: Upload Artifacts
|
||||
if: false
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
overwrite: true
|
||||
name: Releases
|
||||
path: |
|
||||
build/macos/Build/Products/Release/BikeControl.macos.zip
|
||||
|
||||
- name: Generate release body
|
||||
if: false
|
||||
run: |
|
||||
chmod +x scripts/generate_release_body.sh
|
||||
./scripts/generate_release_body.sh > /tmp/release_body.md
|
||||
|
||||
# add artifact to release
|
||||
- name: Create Release
|
||||
if: false
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
allowUpdates: true
|
||||
artifacts: "build/macos/Build/Products/Release/BikeControl.macos.zip"
|
||||
bodyFile: /tmp/release_body.md
|
||||
prerelease: true
|
||||
tag: v${{ env.VERSION }}
|
||||
token: ${{ secrets.TOKEN }}
|
||||
|
||||
windows:
|
||||
name: Patch Windows
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
#1 Checkout Repository
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
token: ${{ secrets.PAT_TOKEN }}
|
||||
|
||||
- name: 🐦 Setup Shorebird
|
||||
uses: shorebirdtech/setup-shorebird@v1
|
||||
with:
|
||||
cache: true
|
||||
|
||||
- name: Set Up Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: 'stable'
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
|
||||
- name: Generate translation files
|
||||
run: |
|
||||
flutter pub global activate intl_utils;
|
||||
flutter pub global run intl_utils:generate;
|
||||
|
||||
- name: 🚀 Shorebird Patch Windows
|
||||
uses: shorebirdtech/shorebird-patch@v1
|
||||
with:
|
||||
platform: windows
|
||||
release-version: latest
|
||||
args: '--allow-asset-diffs --allow-native-diffs'
|
||||
61
.github/workflows/web.yml
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
name: "Build Web"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- web
|
||||
- wahoo_kickr_bike_shift
|
||||
- main
|
||||
paths:
|
||||
- '.github/workflows/web.yml'
|
||||
- 'lib/**'
|
||||
- 'accessibility/**'
|
||||
- 'keypress_simulator/**'
|
||||
- 'pubspec.yaml'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Web
|
||||
runs-on: macos-latest
|
||||
if: false
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
pages: write
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
#1 Checkout Repository
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
token: ${{ secrets.PAT_TOKEN }}
|
||||
|
||||
#3 Setup Flutter
|
||||
- name: Set Up Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: 'stable'
|
||||
|
||||
- name: Generate translation files
|
||||
|
||||
run: |
|
||||
flutter pub global activate intl_utils;
|
||||
flutter pub global run intl_utils:generate;
|
||||
|
||||
#4 Install Dependencies
|
||||
- name: Install Dependencies
|
||||
run: flutter pub get
|
||||
|
||||
- name: Build Web
|
||||
run: flutter build web --release --base-href "/swiftcontrol/"
|
||||
|
||||
- name: Upload static files as artifact
|
||||
id: deployment
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: build/web
|
||||
|
||||
- name: Web Deploy
|
||||
uses: actions/deploy-pages@v4
|
||||
10
.gitignore
vendored
@@ -10,8 +10,11 @@
|
||||
.history
|
||||
.svn/
|
||||
.swiftpm/
|
||||
debug/
|
||||
migrate_working_dir/
|
||||
|
||||
android/keystore.properties
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
@@ -38,8 +41,15 @@ app.*.symbols
|
||||
|
||||
# Obfuscation related
|
||||
app.*.map.json
|
||||
localazy.json
|
||||
|
||||
# Android Studio will place build artifacts here
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
|
||||
lib/gen/
|
||||
|
||||
service-account.json
|
||||
.env
|
||||
lib/generated
|
||||
|
||||
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "prop"]
|
||||
path = prop
|
||||
url = git@github.com:OpenBikeControl/prop.git
|
||||
24
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
|
||||
{
|
||||
"name": "swiftcontrol",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"program": "lib/main.dart"
|
||||
},
|
||||
{
|
||||
"name": "swiftcontrol (profile mode)",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "profile"
|
||||
},
|
||||
{
|
||||
"name": "swiftcontrol (release mode)",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "release"
|
||||
}
|
||||
]
|
||||
}
|
||||
270
CHANGELOG.md
Normal file
@@ -0,0 +1,270 @@
|
||||
### 4.6.0 (28-01-2026)
|
||||
|
||||
**Features**:
|
||||
- Improve Zwift Click V2 connection and handling
|
||||
- Buttons in Configuration are now grouped by device
|
||||
|
||||
### 4.5.0 (22-01-2026)
|
||||
|
||||
**Features**:
|
||||
- Android: simulate additional actions for local connection method (Left, Down, Right, Up, Select, Back, Home, Recent Apps)
|
||||
- control your phone with your controller
|
||||
- control UI within the trainer app (if supported)
|
||||
- BikeControl now supports individual mapping when you use more than one Cycplus BC2 and ThinkRider VS200 controller
|
||||
- Windows & macOS: allow configuration of volume keys on Bluetooth HID devices
|
||||
|
||||
### 4.4.0 (16-01-2026)
|
||||
|
||||
**Features**:
|
||||
- Support for Thinkrider VS200
|
||||
|
||||
**Fixes**:
|
||||
- Android: Local connection method allows passing keyboard events to the trainer app
|
||||
- macOS: Compatibility with macOS Tahoe
|
||||
- Windows: send keyboard events to the correct window when using multiple monitors or when another app is focused
|
||||
- Windows: fix media key detection
|
||||
|
||||
### 4.3.0 (07-01-2026)
|
||||
|
||||
**Features**:
|
||||
- Onboarding for new users
|
||||
- support controlling music & volume for Windows, macOS and Android
|
||||
- App is now available in Italian (thanks to Connect_Thanks2613)
|
||||
|
||||
**Fixes**:
|
||||
- Vibration setting now available for Zwift Ride devices
|
||||
|
||||
### 4.2.0 (20-12-2025)
|
||||
|
||||
BikeControl now offers a free trial period of 5 days for all features, so you can test everything before deciding to purchase a license. Please contact the support if you experience any issues!
|
||||
|
||||
**Features**:
|
||||
- support for SRAM AXS/eTap
|
||||
- only single or double click is supported (no individual button mapping possible, yet)
|
||||
- use your phone/tablet for steering by attaching your device on your handlebar!
|
||||
- App is now available in Polish (thanks to Wandrocek)
|
||||
|
||||
**Fixes**:
|
||||
- You will now be notified when a connection to your controller is lost
|
||||
- improved UI of the Keymap customization screen
|
||||
|
||||
### 4.1.0 (16-12-2025)
|
||||
|
||||
**Features**:
|
||||
- control your trainer manually without requiring a controller - just like a Companion app
|
||||
- support for Wahoo KICKR HEADWIND: control the fan via your controller
|
||||
|
||||
**Fixes**:
|
||||
- Gamepads: handle analog values correctly on Windows
|
||||
- MyWhoosh: updated default keymap to use the new A+D keys for steering
|
||||
|
||||
### 4.0.0 (07-12-2025)
|
||||
|
||||
- a brand-new design
|
||||
- Accessibility Permission is now optional on Android
|
||||
- Zwift is now fully supported on all operating systems
|
||||
- you can choose between network based control or bluetooth based control
|
||||
- MyWhoosh can now also be controlled with BikeControl running on the same iPad / iPhone
|
||||
- Translations available in German and French
|
||||
- support for Wahoo KICKR BIKE PRO
|
||||
- support for the OpenBikeControl protocol for supported Trainer apps
|
||||
- this enables seamless and official integration, independent of the operating system
|
||||
- learn more at https://openbikecontrol.org
|
||||
|
||||
### 3.6.0 (23-11-2025)
|
||||
|
||||
SwiftControl is now called BikeControl!
|
||||
|
||||
**Features:**
|
||||
- show a list of predefined keymaps for the selected trainer app when using a custom keymap
|
||||
- status icons so it's clear what's missing
|
||||
|
||||
**Fixes:**
|
||||
- Update Rouvy keymap to support virtual shifting in their latest version
|
||||
|
||||
### 3.5.0 (16-11-2025)
|
||||
**New Features:**
|
||||
- Dark mode support
|
||||
- Cycplus BC2 support (thanks @schneewoehner)
|
||||
- Ignored devices now persist across app restarts - remove them from ignored devices via the menu
|
||||
|
||||
**Fixes:**
|
||||
- resolve issues during app start
|
||||
|
||||
### 3.4.0 (08-11-2025)
|
||||
**New Features:**
|
||||
- Support for Shimano Di2
|
||||
- Support Keyboard shortcuts with modifier keys (Ctrl, Alt, Shift, ...)
|
||||
- Support cheap BLE HID remotes
|
||||
- add Keymap for Rouvy, supporting the new keyboard shortcuts for virtual shifting
|
||||
|
||||
**Fixes:**
|
||||
- fix detection of Elite Square Sterzo devices
|
||||
- recognize cheap Bluetooth device clicks also when BikeControl is in the background
|
||||
|
||||
### 3.3.0 (31-10-2025)
|
||||
|
||||
**New Features:**
|
||||
- Support for Elite Sterzo (thanks @michidk)
|
||||
- Support for Gamepads
|
||||
- Support for cheap bluetooth remotes (such as [these](https://www.amazon.com/s?k=bluetooth+remote))
|
||||
- you can now customize the Keymap right from the Customize section
|
||||
- show signal strength of connected devices (thanks @michidk)
|
||||
- Android and Windows only: simulate bluetooth controllers
|
||||
- enables gamepad and bluetooth remotes support for Zwift, Rouvy and Biketerra
|
||||
|
||||
**Fixes:**
|
||||
- fix firmware version display for Zwift Click V2 devices
|
||||
- fix touch position on some Android devices
|
||||
- Wahoo Kickr Bike Shift can now be connected
|
||||
- update default keymap for TrainingPeaks
|
||||
|
||||
### 3.2.0 (2025-10-22)
|
||||
- a brand-new way of controlling MyWhoosh:
|
||||
- device pairing no longer required as mouse emulation is no longer needed
|
||||
- BikeControl can now stay in the background
|
||||
- more devices can be controlled
|
||||
- do more, such as define Emotes, Camera angles and steering
|
||||
|
||||
### 3.1.0 (2025-10-17)
|
||||
- new app icon
|
||||
- adjusted MyWhoosh keyboard navigation mapping (thanks @bin101)
|
||||
- support for Wahook Kickr Bike Shift (thanks @MattW2)
|
||||
- initial support for Elite Square Smart Frame
|
||||
- reconnects to your device automatically when connection is lost
|
||||
- BikeControl now warns you if your device firmware is outdated
|
||||
- BikeControl is now available in Microsoft Store: https://apps.microsoft.com/detail/9NP42GS03Z26
|
||||
|
||||
### 3.0.3 (2025-10-12)
|
||||
- BikeControl now supports iOS!
|
||||
- Note that you can't run BikeControl and your trainer app on the same iPhone due to iOS limitations but...:
|
||||
- You can now use BikeControl as "remote control" for other devices, such as an iPad. Example scenario:
|
||||
- your phone (Android/iOS) runs BikeControl and connects to your Click devices
|
||||
- your iPad or other tablet runs e.g. MyWhoosh (does not need to have BikeControl installed)
|
||||
- after pairing BikeControl to your iPad / tablet via Bluetooth your phone will send the button presses to your iPad / tablet
|
||||
- Ride: analog paddles are now supported thanks to contributor @jmoro
|
||||
- you can now zoom in and out in the Keymap customization screen
|
||||
|
||||
### 2.6.3 (2025-10-01)
|
||||
- fix a few issues with the new touch placement feature
|
||||
- add a workaround for Zwift Click V2 which resets the device when button events are no longer sent
|
||||
- fix issue on Android and Desktop where only a "touch down" was sent, but no "touch up"
|
||||
- improve UI when handling custom keymaps around the edges of the screen
|
||||
|
||||
### 2.6.0 (2025-09-30)
|
||||
- refactor touch placements: show touches on screen, fix misplaced coordinates - should fix #64
|
||||
- show firmware version of connected device
|
||||
- Fix crashes on some Android devices
|
||||
- warn the user how to make Zwift Click V2 work properly
|
||||
- many UI improvements
|
||||
- add setting to enable or disable vibration on button press for Zwift Ride and Zwift Play controllers
|
||||
|
||||
### 2.5.0 (2025-09-25)
|
||||
- Improve usability
|
||||
- BikeControl is now available via the Play Store: https://play.google.com/store/apps/details?id=de.jonasbark.swiftcontrol
|
||||
- BikeControl will continue to be available to download for free on GitHub
|
||||
- contact me if you already donated and I'll get a voucher for you :)
|
||||
|
||||
### 2.4.0+1 (2025-09-17)
|
||||
- Windows: fix mouse clicks at wrong location due to display scaling (fixes #64)
|
||||
|
||||
### 2.4.0 (2025-09-16)
|
||||
- Show an overview of the keymap bindings
|
||||
- Allow customizing an existing keymap
|
||||
- Add more donation options
|
||||
|
||||
### 2.3.0 (2025-09-11)
|
||||
- Add support for latest Zwift Click v2
|
||||
|
||||
### 2.2.0 (2025-09-08)
|
||||
- Add Long Press Mode option for custom keymaps - buttons can now send sustained key presses instead of repeated taps, perfect for movement controls in games (fixes #61)
|
||||
- Windows: adjust key sending method to improve compatibility with more apps (fixes #62)
|
||||
|
||||
### 2.1.0 (2025-07-03)
|
||||
- Windows: automatically focus compatible training apps (MyWhoosh, IndieVelo, Biketerra) when sending keystrokes, enabling seamless multi-window usage
|
||||
|
||||
### 2.0.9 (2025-05-04)
|
||||
- you can now assign Escape and arrow down key to your custom keymap (#18)
|
||||
|
||||
### 2.0.8 (2025-05-02)
|
||||
- only use the light theme for the app
|
||||
- more troubleshooting information
|
||||
|
||||
### 2.0.7 (2025-04-18)
|
||||
- add Biketerra.com keymap
|
||||
- some UX improvements
|
||||
|
||||
### 2.0.6 (2025-04-15)
|
||||
- fix MyWhoosh up / downshift button assignment (I key vs K key)
|
||||
|
||||
### 2.0.5 (2025-04-13)
|
||||
- fix Zwift Click button assignment (#12)
|
||||
|
||||
### 2.0.4 (2025-04-10)
|
||||
- vibrate Zwift Play / Zwift Ride controller on gear shift (thanks @cagnulein, closes #16)
|
||||
|
||||
### 2.0.3 (2025-04-08)
|
||||
- adjust TrainingPeaks Virtual key mapping (#12)
|
||||
- attempt to reconnect device if connection is lost
|
||||
- Android: detect freeform windows for MyWhoosh + TrainingPeaks Virtual keymaps
|
||||
|
||||
### 2.0.2 (2025-04-07)
|
||||
- fix bluetooth scan issues on older Android devices by asking for location permission
|
||||
|
||||
### 2.0.1 (2025-04-06)
|
||||
- long pressing a button will trigger the action again every 250ms
|
||||
|
||||
### 2.0.0 (2025-04-06)
|
||||
- You can now customize the actions (touches, mouse clicks or keyboard keys) for all buttons on all supported Zwift devices
|
||||
- now shows the battery level of the connected devices
|
||||
- add more troubleshooting information
|
||||
|
||||
### 1.1.10 (2025-04-03)
|
||||
- Add more troubleshooting during connection
|
||||
|
||||
### 1.1.8 (2025-04-02)
|
||||
- Android: make sure the touch reassignment page is fullscreen
|
||||
|
||||
### 1.1.7 (2025-04-01)
|
||||
- Zwift Ride: fix connection issues by connecting only to the left controller
|
||||
- Windows: connect sequentially to fix (finally?) fix connection issues
|
||||
- Windows: change the way keyboard is simulated, should fix glitches
|
||||
|
||||
### 1.1.6 (2025-03-31)
|
||||
- Zwift Ride: add buttonPowerDown to shift gears
|
||||
- Zwift Play: Fix buttonShift assignment
|
||||
- Android: fix action to go to next song
|
||||
- App now checks if you run the latest available version
|
||||
|
||||
### 1.1.5 (2025-03-30)
|
||||
- fix bluetooth connection #6, also add missing entitlement on macOS
|
||||
|
||||
### 1.1.3 (2025-03-30)
|
||||
- Windows: fix custom keyboard profile recreation after restart, also warn when choosing MyWhoosh profile (may fix #7)
|
||||
- Zwift Ride: button map adjustments to prevent double shifting
|
||||
- potential fix for #6
|
||||
|
||||
### 1.1.1 (2025-03-30)
|
||||
- potential fix for Bluetooth device detection
|
||||
|
||||
### 1.1.0 (2025-03-30)
|
||||
- Windows & macOS: allow setting custom keymap and store the setting
|
||||
- Android: allow customizing the touch area, so it can work with any device without guesswork where the buttons are (#4)
|
||||
- Zwift Ride: update Zwift Ride decoding based on Feedback from @JayyajGH (#3)
|
||||
|
||||
### 1.0.6 (2025-03-29)
|
||||
- Another potential keyboard fix for Windows
|
||||
- Zwift Play: actually also use the dedicated shift buttons
|
||||
|
||||
### 1.0.5 (2025-03-29)
|
||||
- Zwift Ride: remap the shifter buttons to the correct values
|
||||
|
||||
### 1.0.0+4 (2025-03-29)
|
||||
- Zwift Ride: attempt to fix button parsing
|
||||
- Android: fix missing permissions
|
||||
- Windows: potential fix for key press issues
|
||||
|
||||
### 1.0.0+3 (2025-03-29)
|
||||
|
||||
- Windows: fix connection by using a different Bluetooth stack (issue #1)
|
||||
- Android: fix non-working touch propagation (issue #2)
|
||||
1
INSTRUCTIONS_IOS.md
Normal file
@@ -0,0 +1 @@
|
||||
Moved to [INSTRUCTIONS_MYWHOOSH_LINK.md](INSTRUCTIONS_MYWHOOSH_LINK.md)
|
||||
17
INSTRUCTIONS_LOCAL.md
Normal file
@@ -0,0 +1,17 @@
|
||||
## What is the Local connection method?
|
||||
*
|
||||
The Local connection method works by directly controlling the target trainer app on the same device by simulating user input (taps, keyboard inputs). This method does not require any network connection or additional hardware, making it the simplest and most straightforward way to connect to the trainer app.
|
||||
|
||||
There are predefined keymaps (touch positions or keyboard shortcuts) for popular trainer apps, allowing users to easily set up and start using the Local connection method without needing to configure anything manually. You can configure these keymaps in the Configuration tab. Note though that supported keyboard keys depend on the trainer app.
|
||||
|
||||
## When to use the Local connection method?
|
||||
*
|
||||
The Local connection method is ideal for users who:
|
||||
- Are running the trainer app on the same device as the controller app (e.g., both apps on a smartphone or tablet).
|
||||
- Do not want to deal with network configurations or potential connectivity issues.
|
||||
|
||||
## Limitations of the Local connection method
|
||||
*
|
||||
While the Local connection method is easy to set up and use, it has some limitations:
|
||||
- It may not work well with trainer apps that have complex user interfaces or require precise timing.
|
||||
- It is limited to the device on which both the controller and trainer apps are running, meaning it cannot be used for remote control scenarios.
|
||||
38
INSTRUCTIONS_MYWHOOSH_LINK.md
Normal file
@@ -0,0 +1,38 @@
|
||||
## Instructions for using the MyWhoosh "Link" connection method
|
||||
*
|
||||
1) launch MyWhoosh on the device of your choice
|
||||
2) make sure the "MyWhoosh Link" app is not active at the same time as BikeControl
|
||||
3) open BikeControl, follow the on-screen instructions
|
||||
|
||||
|
||||
Here's a video with a few explanations. Note it uses an older version, but the idea is the same.
|
||||
|
||||
[](https://www.youtube.com/watch?v=p8sgQhuufeI)
|
||||
[https://www.youtube.com/watch?v=p8sgQhuufeI](https://www.youtube.com/watch?v=p8sgQhuufeI)
|
||||
|
||||
## MyWhoosh "Link" method never connects
|
||||
*
|
||||
This is a network/local-discovery problem. BikeControl needs the same kind of local network access as MyWhoosh Link.
|
||||
|
||||
Checklist:
|
||||
- Use the MyWhoosh Link app to confirm if "Link" works in general
|
||||
- Use MyWhoosh Link app and connect, then close it, then open up BikeControl - this is key for some users
|
||||
- Both devices (if you use BikeControl on another device than MyWhoosh) are on the **same Wi‑Fi SSID**
|
||||
- Avoid “Guest” networks
|
||||
- Avoid “extenders/mesh guest mode” and networks with device isolation
|
||||
- If your router has it, disable:
|
||||
- “AP isolation / client isolation”
|
||||
- Try moving both devices to the same band:
|
||||
- Prefer **2.4 GHz** (often more reliable for local discovery than mixed/steering)
|
||||
- Temporarily disable:
|
||||
- VPNs
|
||||
- iCloud Private Relay (if enabled)
|
||||
- “Limit IP Address Tracking” (iOS Wi‑Fi option)
|
||||
- iOS Wi‑Fi settings for that network:
|
||||
- Turn off **Private Wi‑Fi Address**
|
||||
- Turn off **Limit IP Address Tracking**
|
||||
- Mesh networks: may work, but if it doesn’t, test with a simple router or phone hotspot.
|
||||
|
||||
Official MyWhoosh troubleshooting links:
|
||||
- https://mywhoosh.com/troubleshoot/
|
||||
- https://www.facebook.com/groups/mywhoosh/posts/1323791068858873/
|
||||
13
INSTRUCTIONS_REMOTE_CONTROL.md
Normal file
@@ -0,0 +1,13 @@
|
||||
## Remote control is not working - nothing happens
|
||||
*
|
||||
- Try to unpair it from your phone / computer Bluetooth settings, then re-pair it.
|
||||
- Try restarting the pairing process in BikeControl
|
||||
- try restarting Bluetooth on your phone and on the device you want to control
|
||||
- If your other device is an iOS device, go to Settings > Accessibility > Touch > AssistiveTouch > Pointer Devices > Devices and pair your device. Make sure AssistiveTouch is enabled.
|
||||
- it is very important that both devices (e.g. iPhone and iPad) receive the "pairing dialog" after initial connection. If you miss it, unpair and try again. It may take a few seconds for the dialog to appear. Afterwards you may need to click on "Reconnect" in BikeControl / restart BikeControl.
|
||||
|
||||
## Remote control only clicks on a single coordinate on my iPad
|
||||
*
|
||||
iOS seems to be buggy here - try this in the iOS settings:
|
||||
AssistiveTouch settings > Pointer Devices > Devices > Connected Devices > iPhone (or BikeControl iOS) > Button 1
|
||||
switch the setting to None, then back to Single-Tap and it should work again
|
||||
4
INSTRUCTIONS_ROUVY.md
Normal file
@@ -0,0 +1,4 @@
|
||||
## Local Connection method
|
||||
*
|
||||
The local connection method (avalable on Android, Windows and macOS) allows BikeControl to directly control Rouvy either using touch or keyboard keys. This way you don't need to select any "Controllers" at all in Rouvy.
|
||||
Make sure the "Virtual Shifting Controls" are enabled: https://support.rouvy.com/hc/en-us/articles/32452137189393-Virtual-Shifting#h_01K9SWGWYMAVQV108SQ9KWQAKC
|
||||
0
INSTRUCTIONS_ZWIFT.md
Normal file
109
README.md
@@ -1,33 +1,102 @@
|
||||
# SwiftControl
|
||||
# BikeControl (formerly SwiftControl)
|
||||
|
||||
<img src="logo.jpg" alt="SwiftControl Logo"/>
|
||||
<img src="logo.png" alt="BikeControl Logo"/>
|
||||
|
||||
### Description
|
||||
## Description
|
||||
|
||||
With SwiftControl you can control your favorite trainer app using your Zwift Click, Zwift Ride or Zwift Play devices. Primarily useful to perform virtual gear shifting.
|
||||
With BikeControl you can **control your favorite trainer app** using your Zwift Click, Zwift Ride, Zwift Play, Shimano Di2, or other similar devices. Here's what you can do with it, depending on your configuration:
|
||||
- Virtual Gear shifting
|
||||
- Steering / navigation
|
||||
- adjust workout intensity
|
||||
- control music on your device
|
||||
- more? If you can do it via keyboard, mouse, or touch, you can do it with BikeControl
|
||||
|
||||
### Supported Apps
|
||||
[](https://youtu.be/0r3LO5lFlyc)
|
||||
|
||||
|
||||
## Download
|
||||
Best follow our landing page and the "Get Started" button: [bikecontrol.app](https://bikecontrol.app/) to understand on which platform you want to run BikeControl. A testing period is available, allowing you to try out the full functionality of BikeControl:
|
||||
|
||||
<a href="https://play.google.com/store/apps/details?id=de.jonasbark.swiftcontrol"><img width="270" height="80" alt="GetItOnGooglePlay_Badge_Web_color_English" src="https://github.com/user-attachments/assets/a059d5a1-2efb-4f65-8117-ef6a99823b21" /></a>
|
||||
|
||||
<a href="https://apps.apple.com/us/app/swiftcontrol/id6753721284?platform=iphone"><img width="270" alt="App Store" src="https://github.com/user-attachments/assets/c23f977a-48f6-4951-811e-ae530dbfa014" /></a>
|
||||
|
||||
<a href="https://apps.apple.com/us/app/swiftcontrol/id6753721284?platform=mac"><img width="270" height="80" alt="Mac App Store" src="https://github.com/user-attachments/assets/b3552436-409c-43b0-ba7d-b6a72ae30ff1" /></a>
|
||||
|
||||
<a href="https://apps.microsoft.com/detail/9NP42GS03Z26"><img width="270" alt="Microsoft Store" src="https://github.com/user-attachments/assets/7a8a3cd6-ec26-4678-a850-732eedd27c48" /></a>
|
||||
|
||||
## Supported Apps
|
||||
- MyWhoosh
|
||||
- indieVelo / Training Peaks
|
||||
- let me know if you know others that can benefit
|
||||
- Zwift
|
||||
- TrainingPeaks Virtual
|
||||
- Biketerra.com
|
||||
- Rouvy
|
||||
- [OpenBikeControl](https://openbikecontrol.org) compatible apps
|
||||
- any other!
|
||||
- You can add custom mapping and adjust touch points or keyboard shortcuts to your liking
|
||||
|
||||
### Supported Devices
|
||||
## Supported Devices
|
||||
- Zwift Click
|
||||
- Zwift Click v2 (mostly, see issue #68)
|
||||
- Zwift Ride
|
||||
- Zwift Play
|
||||
- Shimano Di2
|
||||
- Configure your levers to use D-Fly channels with Shimano E-Tube app
|
||||
- SRAM AXS/eTap
|
||||
- Configure your levers not to do any action in the "SRAM AXS" app
|
||||
- only single or double click is supported (no individual button mapping possible, yet)
|
||||
- Wahoo Kickr Bike Shift
|
||||
- Wahoo Kickr Bike Pro
|
||||
- CYCPLUS BC2 Virtual Shifter
|
||||
- Thinkrider VS200 Virtual Shifter (beta)
|
||||
- Elite Sterzo Smart (for steering support)
|
||||
- Elite Square Smart Frame (beta)
|
||||
- Your Phone!
|
||||
- Mount your phone on the handlebar to detect e.g. steering
|
||||
- Available on Android and iOS
|
||||
- Gamepads
|
||||
- Keyboard input
|
||||
- like a Companion App
|
||||
- some trainers do not support keyboard input for all functions - now they do!
|
||||
- useful when remapping keys from other devices using e.g. AutoHotkey
|
||||
- Cheap Bluetooth buttons such as [these](https://www.amazon.com/s?k=bluetooth+remote) (beta)
|
||||
- BLE HID devices and classic Bluetooth HID devices are supported
|
||||
- works out of the box on Android
|
||||
- on Windows, iOS and macOS requires BikeControl to act as media player
|
||||
- We're working on creating an affordable alternative based on an open standard, supported by all major trainer apps
|
||||
- register your interest [here](https://openbikecontrol.org/#HARDWARE)
|
||||
|
||||
### Supported Platforms
|
||||
- Android
|
||||
- macOS
|
||||
- Windows
|
||||
Support for other devices can be added; check the issues tab here on GitHub.
|
||||
|
||||
### How does it work?
|
||||
The app connects to your Zwift device automatically.
|
||||
## Supported Accessories
|
||||
- Wahoo KICKR HEADWIND (beta)
|
||||
- control fan speed using your controller
|
||||
|
||||
- When using Android a "click" on a certain part of the screen is simulated to trigger the action.
|
||||
- When using macOS or Windows a keyboard click is used to trigger the action. Typically + and - keys are used to shift gears, while MyWhoosh uses K and I keys.
|
||||
## Supported Platforms
|
||||
|
||||
### TODO
|
||||
- test Zwift Ride
|
||||
- implement more actions for Play + Ride
|
||||
- shorebird?
|
||||
Follow the "Get Started" button over at [bikecontrol.app](https://bikecontrol.app) to understand on which platform you want to run BikeControl.
|
||||
You can even try it out in your [Browser](https://jonasbark.github.io/swiftcontrol/), if it supports Bluetooth connections. No controlling possible, though.
|
||||
|
||||
## Help
|
||||
Check the troubleshooting guide [here](TROUBLESHOOTING.md).
|
||||
|
||||
## How does it work?
|
||||
The app connects to your Controller devices (such as Zwift ones) automatically. BikeControl uses different methods of connecting to the trainer app, depending on the trainer app and operating system:
|
||||
- Connect to the trainer app on the same device or on another device using Network
|
||||
- available on Android, iOS, iPadOS, macOS, Windows
|
||||
- supported by e.g. MyWhoosh, Rouvy and Zwift
|
||||
- Connect to the trainer app on another device by simulating a Bluetooth device
|
||||
- available on Android, iOS, iPadOS, macOS, Windows
|
||||
- supported by e.g. Rouvy and Zwift
|
||||
- Directly control the trainer app via Accessibility features (simulating touch and keyboard input)
|
||||
- available on Android, macOS, Windows
|
||||
- supported by all trainer apps
|
||||
- Connect to the supported trainer app using the [OpenBikeControl](https://openbikecontrol.org) protocol
|
||||
- available on Android, iOS, iPadOS, macOS, Windows
|
||||
|
||||
## Donate
|
||||
Please consider donating to support the development of this app :)
|
||||
|
||||
- [via PayPal](https://paypal.me/boni)
|
||||
- [via Credit Card, Google Pay, Apple Pay, etc. (USD)](https://donate.stripe.com/8x24gzc5c4ZE3VJdt36J201)
|
||||
- [via Credit Card, Google Pay, Apple Pay, etc. (EUR)](https://donate.stripe.com/9B6aEX0muajY8bZ1Kl6J200)
|
||||
|
||||
38
TROUBLESHOOTING.md
Normal file
@@ -0,0 +1,38 @@
|
||||
## Click / Ride device cannot be found
|
||||
*
|
||||
This means BikeControl does NOT see the device via Bluetooth.
|
||||
- Put the controller into pairing mode (LED should blink)
|
||||
- Ensure the controller is NOT connected to another app/device (e.g. Zwift)
|
||||
- Update controller firmware in Zwift Companion, if available
|
||||
- Reboot Bluetooth / reboot phone/PC
|
||||
|
||||
## Click / Ride device does not send any data
|
||||
*
|
||||
You may need to update the firmware in Zwift Companion app.
|
||||
|
||||
## My Click v2 disconnects after a minute or buttons do not work
|
||||
*
|
||||
|
||||
To make your Click V2 work best you should connect it in the Zwift app once before a workout session.
|
||||
If you don't do that BikeControl will need to reconnect every minute.
|
||||
|
||||
1. Open Zwift app (not the Companion)
|
||||
2. Log in (subscription not required) → device connection screen
|
||||
3. Connect trainer, then connect Click v2
|
||||
4. Keep it connected for ~10–30 seconds
|
||||
5. Close Zwift completely, then connect in BikeControl
|
||||
|
||||
Details/updates: https://github.com/jonasbark/swiftcontrol/issues/68
|
||||
|
||||
## Android: Connection works, buttons work but nothing happens in MyWhoosh and similar
|
||||
*
|
||||
- especially for Redmi and other chinese Android devices please follow the instructions on [https://dontkillmyapp.com/](https://dontkillmyapp.com/):
|
||||
- disable battery optimization for BikeControl
|
||||
- enable auto start of BikeControl
|
||||
- grant accessibility permission for BikeControl
|
||||
- see [https://github.com/jonasbark/swiftcontrol/issues/38](https://github.com/OpenBikeControl/bikecontrol/issues/38) for more details
|
||||
|
||||
|
||||
## My Clicks do not get recognized in MyWhoosh, but I am connected / use local control
|
||||
*
|
||||
Make sure you've enabled Virtual Shifting in MyWhoosh's settings
|
||||
1
WINDOWS_STORE_VERSION.txt
Normal file
@@ -0,0 +1 @@
|
||||
4.6.2
|
||||
@@ -1,4 +1,4 @@
|
||||
// Autogenerated from Pigeon (v25.2.0), do not edit directly.
|
||||
// Autogenerated from Pigeon (v25.5.0), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
|
||||
|
||||
@@ -12,25 +12,57 @@ import io.flutter.plugin.common.StandardMethodCodec
|
||||
import io.flutter.plugin.common.StandardMessageCodec
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.nio.ByteBuffer
|
||||
private object AccessibilityApiPigeonUtils {
|
||||
|
||||
private fun wrapResult(result: Any?): List<Any?> {
|
||||
return listOf(result)
|
||||
}
|
||||
|
||||
private fun wrapError(exception: Throwable): List<Any?> {
|
||||
return if (exception is FlutterError) {
|
||||
listOf(
|
||||
exception.code,
|
||||
exception.message,
|
||||
exception.details
|
||||
)
|
||||
} else {
|
||||
listOf(
|
||||
exception.javaClass.simpleName,
|
||||
exception.toString(),
|
||||
"Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)
|
||||
)
|
||||
fun wrapResult(result: Any?): List<Any?> {
|
||||
return listOf(result)
|
||||
}
|
||||
|
||||
fun wrapError(exception: Throwable): List<Any?> {
|
||||
return if (exception is FlutterError) {
|
||||
listOf(
|
||||
exception.code,
|
||||
exception.message,
|
||||
exception.details
|
||||
)
|
||||
} else {
|
||||
listOf(
|
||||
exception.javaClass.simpleName,
|
||||
exception.toString(),
|
||||
"Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)
|
||||
)
|
||||
}
|
||||
}
|
||||
fun deepEquals(a: Any?, b: Any?): Boolean {
|
||||
if (a is ByteArray && b is ByteArray) {
|
||||
return a.contentEquals(b)
|
||||
}
|
||||
if (a is IntArray && b is IntArray) {
|
||||
return a.contentEquals(b)
|
||||
}
|
||||
if (a is LongArray && b is LongArray) {
|
||||
return a.contentEquals(b)
|
||||
}
|
||||
if (a is DoubleArray && b is DoubleArray) {
|
||||
return a.contentEquals(b)
|
||||
}
|
||||
if (a is Array<*> && b is Array<*>) {
|
||||
return a.size == b.size &&
|
||||
a.indices.all{ deepEquals(a[it], b[it]) }
|
||||
}
|
||||
if (a is List<*> && b is List<*>) {
|
||||
return a.size == b.size &&
|
||||
a.indices.all{ deepEquals(a[it], b[it]) }
|
||||
}
|
||||
if (a is Map<*, *> && b is Map<*, *>) {
|
||||
return a.size == b.size && a.all {
|
||||
(b as Map<Any?, Any?>).containsKey(it.key) &&
|
||||
deepEquals(it.value, b[it.key])
|
||||
}
|
||||
}
|
||||
return a == b
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,26 +77,62 @@ class FlutterError (
|
||||
val details: Any? = null
|
||||
) : Throwable()
|
||||
|
||||
enum class MediaAction(val raw: Int) {
|
||||
PLAY_PAUSE(0),
|
||||
NEXT(1),
|
||||
VOLUME_UP(2),
|
||||
VOLUME_DOWN(3);
|
||||
|
||||
companion object {
|
||||
fun ofRaw(raw: Int): MediaAction? {
|
||||
return values().firstOrNull { it.raw == raw }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class GlobalAction(val raw: Int) {
|
||||
BACK(0),
|
||||
DPAD_CENTER(1),
|
||||
DOWN(2),
|
||||
RIGHT(3),
|
||||
UP(4),
|
||||
LEFT(5),
|
||||
HOME(6),
|
||||
RECENTS(7);
|
||||
|
||||
companion object {
|
||||
fun ofRaw(raw: Int): GlobalAction? {
|
||||
return values().firstOrNull { it.raw == raw }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Generated class from Pigeon that represents data sent in messages. */
|
||||
data class WindowEvent (
|
||||
val packageName: String,
|
||||
val windowHeight: Long,
|
||||
val windowWidth: Long
|
||||
val top: Long,
|
||||
val bottom: Long,
|
||||
val right: Long,
|
||||
val left: Long
|
||||
)
|
||||
{
|
||||
companion object {
|
||||
fun fromList(pigeonVar_list: List<Any?>): WindowEvent {
|
||||
val packageName = pigeonVar_list[0] as String
|
||||
val windowHeight = pigeonVar_list[1] as Long
|
||||
val windowWidth = pigeonVar_list[2] as Long
|
||||
return WindowEvent(packageName, windowHeight, windowWidth)
|
||||
val top = pigeonVar_list[1] as Long
|
||||
val bottom = pigeonVar_list[2] as Long
|
||||
val right = pigeonVar_list[3] as Long
|
||||
val left = pigeonVar_list[4] as Long
|
||||
return WindowEvent(packageName, top, bottom, right, left)
|
||||
}
|
||||
}
|
||||
fun toList(): List<Any?> {
|
||||
return listOf(
|
||||
packageName,
|
||||
windowHeight,
|
||||
windowWidth,
|
||||
top,
|
||||
bottom,
|
||||
right,
|
||||
left,
|
||||
)
|
||||
}
|
||||
override fun equals(other: Any?): Boolean {
|
||||
@@ -74,10 +142,44 @@ data class WindowEvent (
|
||||
if (this === other) {
|
||||
return true
|
||||
}
|
||||
return packageName == other.packageName
|
||||
&& windowHeight == other.windowHeight
|
||||
&& windowWidth == other.windowWidth
|
||||
return AccessibilityApiPigeonUtils.deepEquals(toList(), other.toList()) }
|
||||
|
||||
override fun hashCode(): Int = toList().hashCode()
|
||||
}
|
||||
|
||||
/** Generated class from Pigeon that represents data sent in messages. */
|
||||
data class AKeyEvent (
|
||||
val source: String,
|
||||
val hidKey: String,
|
||||
val keyDown: Boolean,
|
||||
val keyUp: Boolean
|
||||
)
|
||||
{
|
||||
companion object {
|
||||
fun fromList(pigeonVar_list: List<Any?>): AKeyEvent {
|
||||
val source = pigeonVar_list[0] as String
|
||||
val hidKey = pigeonVar_list[1] as String
|
||||
val keyDown = pigeonVar_list[2] as Boolean
|
||||
val keyUp = pigeonVar_list[3] as Boolean
|
||||
return AKeyEvent(source, hidKey, keyDown, keyUp)
|
||||
}
|
||||
}
|
||||
fun toList(): List<Any?> {
|
||||
return listOf(
|
||||
source,
|
||||
hidKey,
|
||||
keyDown,
|
||||
keyUp,
|
||||
)
|
||||
}
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is AKeyEvent) {
|
||||
return false
|
||||
}
|
||||
if (this === other) {
|
||||
return true
|
||||
}
|
||||
return AccessibilityApiPigeonUtils.deepEquals(toList(), other.toList()) }
|
||||
|
||||
override fun hashCode(): Int = toList().hashCode()
|
||||
}
|
||||
@@ -85,17 +187,44 @@ private open class AccessibilityApiPigeonCodec : StandardMessageCodec() {
|
||||
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
|
||||
return when (type) {
|
||||
129.toByte() -> {
|
||||
return (readValue(buffer) as Long?)?.let {
|
||||
MediaAction.ofRaw(it.toInt())
|
||||
}
|
||||
}
|
||||
130.toByte() -> {
|
||||
return (readValue(buffer) as Long?)?.let {
|
||||
GlobalAction.ofRaw(it.toInt())
|
||||
}
|
||||
}
|
||||
131.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
WindowEvent.fromList(it)
|
||||
}
|
||||
}
|
||||
132.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
AKeyEvent.fromList(it)
|
||||
}
|
||||
}
|
||||
else -> super.readValueOfType(type, buffer)
|
||||
}
|
||||
}
|
||||
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
|
||||
when (value) {
|
||||
is WindowEvent -> {
|
||||
is MediaAction -> {
|
||||
stream.write(129)
|
||||
writeValue(stream, value.raw)
|
||||
}
|
||||
is GlobalAction -> {
|
||||
stream.write(130)
|
||||
writeValue(stream, value.raw)
|
||||
}
|
||||
is WindowEvent -> {
|
||||
stream.write(131)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is AKeyEvent -> {
|
||||
stream.write(132)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
else -> super.writeValue(stream, value)
|
||||
@@ -109,7 +238,12 @@ val AccessibilityApiPigeonMethodCodec = StandardMethodCodec(AccessibilityApiPige
|
||||
interface Accessibility {
|
||||
fun hasPermission(): Boolean
|
||||
fun openPermissions()
|
||||
fun performTouch(x: Double, y: Double)
|
||||
fun performTouch(x: Double, y: Double, isKeyDown: Boolean, isKeyUp: Boolean)
|
||||
fun performGlobalAction(action: GlobalAction)
|
||||
fun controlMedia(action: MediaAction)
|
||||
fun isRunning(): Boolean
|
||||
fun ignoreHidDevices()
|
||||
fun setHandledKeys(keys: List<String>)
|
||||
|
||||
companion object {
|
||||
/** The codec used by Accessibility. */
|
||||
@@ -127,7 +261,7 @@ interface Accessibility {
|
||||
val wrapped: List<Any?> = try {
|
||||
listOf(api.hasPermission())
|
||||
} catch (exception: Throwable) {
|
||||
wrapError(exception)
|
||||
AccessibilityApiPigeonUtils.wrapError(exception)
|
||||
}
|
||||
reply.reply(wrapped)
|
||||
}
|
||||
@@ -143,7 +277,7 @@ interface Accessibility {
|
||||
api.openPermissions()
|
||||
listOf(null)
|
||||
} catch (exception: Throwable) {
|
||||
wrapError(exception)
|
||||
AccessibilityApiPigeonUtils.wrapError(exception)
|
||||
}
|
||||
reply.reply(wrapped)
|
||||
}
|
||||
@@ -158,11 +292,98 @@ interface Accessibility {
|
||||
val args = message as List<Any?>
|
||||
val xArg = args[0] as Double
|
||||
val yArg = args[1] as Double
|
||||
val isKeyDownArg = args[2] as Boolean
|
||||
val isKeyUpArg = args[3] as Boolean
|
||||
val wrapped: List<Any?> = try {
|
||||
api.performTouch(xArg, yArg)
|
||||
api.performTouch(xArg, yArg, isKeyDownArg, isKeyUpArg)
|
||||
listOf(null)
|
||||
} catch (exception: Throwable) {
|
||||
wrapError(exception)
|
||||
AccessibilityApiPigeonUtils.wrapError(exception)
|
||||
}
|
||||
reply.reply(wrapped)
|
||||
}
|
||||
} else {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.accessibility.Accessibility.performGlobalAction$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { message, reply ->
|
||||
val args = message as List<Any?>
|
||||
val actionArg = args[0] as GlobalAction
|
||||
val wrapped: List<Any?> = try {
|
||||
api.performGlobalAction(actionArg)
|
||||
listOf(null)
|
||||
} catch (exception: Throwable) {
|
||||
AccessibilityApiPigeonUtils.wrapError(exception)
|
||||
}
|
||||
reply.reply(wrapped)
|
||||
}
|
||||
} else {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.accessibility.Accessibility.controlMedia$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { message, reply ->
|
||||
val args = message as List<Any?>
|
||||
val actionArg = args[0] as MediaAction
|
||||
val wrapped: List<Any?> = try {
|
||||
api.controlMedia(actionArg)
|
||||
listOf(null)
|
||||
} catch (exception: Throwable) {
|
||||
AccessibilityApiPigeonUtils.wrapError(exception)
|
||||
}
|
||||
reply.reply(wrapped)
|
||||
}
|
||||
} else {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.accessibility.Accessibility.isRunning$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { _, reply ->
|
||||
val wrapped: List<Any?> = try {
|
||||
listOf(api.isRunning())
|
||||
} catch (exception: Throwable) {
|
||||
AccessibilityApiPigeonUtils.wrapError(exception)
|
||||
}
|
||||
reply.reply(wrapped)
|
||||
}
|
||||
} else {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.accessibility.Accessibility.ignoreHidDevices$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { _, reply ->
|
||||
val wrapped: List<Any?> = try {
|
||||
api.ignoreHidDevices()
|
||||
listOf(null)
|
||||
} catch (exception: Throwable) {
|
||||
AccessibilityApiPigeonUtils.wrapError(exception)
|
||||
}
|
||||
reply.reply(wrapped)
|
||||
}
|
||||
} else {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.accessibility.Accessibility.setHandledKeys$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { message, reply ->
|
||||
val args = message as List<Any?>
|
||||
val keysArg = args[0] as List<String>
|
||||
val wrapped: List<Any?> = try {
|
||||
api.setHandledKeys(keysArg)
|
||||
listOf(null)
|
||||
} catch (exception: Throwable) {
|
||||
AccessibilityApiPigeonUtils.wrapError(exception)
|
||||
}
|
||||
reply.reply(wrapped)
|
||||
}
|
||||
@@ -223,3 +444,16 @@ abstract class StreamEventsStreamHandler : AccessibilityApiPigeonEventChannelWra
|
||||
}
|
||||
}
|
||||
|
||||
abstract class HidKeyPressedStreamHandler : AccessibilityApiPigeonEventChannelWrapper<AKeyEvent> {
|
||||
companion object {
|
||||
fun register(messenger: BinaryMessenger, streamHandler: HidKeyPressedStreamHandler, instanceName: String = "") {
|
||||
var channelName: String = "dev.flutter.pigeon.accessibility.EventChannelMethods.hidKeyPressed"
|
||||
if (instanceName.isNotEmpty()) {
|
||||
channelName += ".$instanceName"
|
||||
}
|
||||
val internalStreamHandler = AccessibilityApiPigeonStreamHandler<AKeyEvent>(streamHandler)
|
||||
EventChannel(messenger, channelName, AccessibilityApiPigeonMethodCodec).setStreamHandler(internalStreamHandler)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,48 +1,47 @@
|
||||
package de.jonasbark.accessibility
|
||||
|
||||
import AKeyEvent
|
||||
import Accessibility
|
||||
import GlobalAction
|
||||
import HidKeyPressedStreamHandler
|
||||
import MediaAction
|
||||
import PigeonEventSink
|
||||
import StreamEventsStreamHandler
|
||||
import WindowEvent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Rect
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.view.KeyEvent
|
||||
import androidx.core.content.ContextCompat.startActivity
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
|
||||
import io.flutter.plugin.common.MethodChannel.Result
|
||||
|
||||
|
||||
/** AccessibilityPlugin */
|
||||
class AccessibilityPlugin: FlutterPlugin, MethodCallHandler, Accessibility {
|
||||
class AccessibilityPlugin: FlutterPlugin, Accessibility {
|
||||
/// The MethodChannel that will the communication between Flutter and native Android
|
||||
///
|
||||
/// This local reference serves to register the plugin with the Flutter Engine and unregister it
|
||||
/// when the Flutter Engine is detached from the Activity
|
||||
private lateinit var channel : MethodChannel
|
||||
private lateinit var context: Context
|
||||
private lateinit var eventHandler: EventListener
|
||||
private lateinit var windowEventHandler: WindowEventListener
|
||||
private lateinit var hidEventHandler: HidEventListener
|
||||
|
||||
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "accessibility")
|
||||
|
||||
eventHandler = EventListener()
|
||||
windowEventHandler = WindowEventListener()
|
||||
hidEventHandler = HidEventListener()
|
||||
|
||||
context = flutterPluginBinding.applicationContext
|
||||
Accessibility.setUp(flutterPluginBinding.binaryMessenger, this)
|
||||
StreamEventsStreamHandler.register(flutterPluginBinding.binaryMessenger, eventHandler)
|
||||
Observable.fromService = eventHandler
|
||||
}
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: Result) {
|
||||
if (call.method == "getPlatformVersion") {
|
||||
result.success("Android ${android.os.Build.VERSION.RELEASE}")
|
||||
} else {
|
||||
result.notImplemented()
|
||||
}
|
||||
StreamEventsStreamHandler.register(flutterPluginBinding.binaryMessenger, windowEventHandler)
|
||||
HidKeyPressedStreamHandler.register(flutterPluginBinding.binaryMessenger, hidEventHandler)
|
||||
Observable.fromServiceWindow = windowEventHandler
|
||||
Observable.fromServiceKeys = hidEventHandler
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
@@ -54,32 +53,99 @@ class AccessibilityPlugin: FlutterPlugin, MethodCallHandler, Accessibility {
|
||||
return enabledServices != null && enabledServices.contains(context.packageName)
|
||||
}
|
||||
|
||||
override fun isRunning(): Boolean {
|
||||
return Observable.toService != null
|
||||
}
|
||||
|
||||
override fun openPermissions() {
|
||||
startActivity(context, Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
}, Bundle.EMPTY)
|
||||
}
|
||||
|
||||
override fun performTouch(x: Double, y: Double) {
|
||||
Observable.toService?.performTouch(x = x, y = y) ?: error("Service not running")
|
||||
override fun performTouch(x: Double, y: Double, isKeyDown: Boolean, isKeyUp: Boolean) {
|
||||
Observable.toService?.performTouch(x = x, y = y, isKeyUp = isKeyUp, isKeyDown = isKeyDown) ?: error("Service not running")
|
||||
}
|
||||
|
||||
override fun performGlobalAction(action: GlobalAction) {
|
||||
Observable.toService?.performGlobalAction(action) ?: error("Service not running")
|
||||
}
|
||||
|
||||
override fun controlMedia(action: MediaAction) {
|
||||
val audioService = context.getSystemService(Context.AUDIO_SERVICE) as android.media.AudioManager
|
||||
when (action) {
|
||||
MediaAction.PLAY_PAUSE -> {
|
||||
audioService.dispatchMediaKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE))
|
||||
audioService.dispatchMediaKeyEvent(KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE))
|
||||
}
|
||||
MediaAction.NEXT -> {
|
||||
audioService.dispatchMediaKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_NEXT))
|
||||
audioService.dispatchMediaKeyEvent(KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_NEXT))
|
||||
}
|
||||
MediaAction.VOLUME_DOWN -> {
|
||||
audioService.adjustVolume(android.media.AudioManager.ADJUST_LOWER, android.media.AudioManager.FLAG_SHOW_UI)
|
||||
}
|
||||
MediaAction.VOLUME_UP -> {
|
||||
audioService.adjustVolume(android.media.AudioManager.ADJUST_RAISE, android.media.AudioManager.FLAG_SHOW_UI)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun ignoreHidDevices() {
|
||||
Observable.ignoreHidDevices = true
|
||||
}
|
||||
|
||||
override fun setHandledKeys(keys: List<String>) {
|
||||
// Clear and update the concurrent set
|
||||
Observable.handledKeys = keys.toSet()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class EventListener : StreamEventsStreamHandler(), Receiver {
|
||||
class WindowEventListener : StreamEventsStreamHandler(), Receiver {
|
||||
private var eventSink: PigeonEventSink<WindowEvent>? = null
|
||||
|
||||
override fun onListen(p0: Any?, sink: PigeonEventSink<WindowEvent>) {
|
||||
eventSink = sink
|
||||
}
|
||||
|
||||
fun onEventsDone() {
|
||||
override fun onCancel(p0: Any?) {
|
||||
eventSink?.endOfStream()
|
||||
eventSink = null
|
||||
}
|
||||
|
||||
override fun onChange(packageName: String, windowWidth: Int, windowHeight: Int) {
|
||||
eventSink?.success(WindowEvent(packageName = packageName, windowWidth = windowWidth.toLong(), windowHeight = windowHeight.toLong()))
|
||||
override fun onChange(packageName: String, window: Rect) {
|
||||
eventSink?.success(WindowEvent(packageName = packageName, right = window.right.toLong(), left = window.left.toLong(), bottom = window.bottom.toLong(), top = window.top.toLong()))
|
||||
}
|
||||
|
||||
override fun onKeyEvent(event: KeyEvent) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
class HidEventListener : HidKeyPressedStreamHandler(), Receiver {
|
||||
|
||||
private var keyEventSink: PigeonEventSink<AKeyEvent>? = null
|
||||
|
||||
override fun onListen(p0: Any?, sink: PigeonEventSink<AKeyEvent>) {
|
||||
keyEventSink = sink
|
||||
}
|
||||
|
||||
override fun onChange(packageName: String, window: Rect) {
|
||||
|
||||
}
|
||||
|
||||
override fun onKeyEvent(event: KeyEvent) {
|
||||
val keyString = KeyEvent.keyCodeToString(event.keyCode)
|
||||
keyEventSink?.success(
|
||||
AKeyEvent(
|
||||
hidKey = keyString,
|
||||
source = event.device.name,
|
||||
keyUp = event.action == KeyEvent.ACTION_UP,
|
||||
keyDown = event.action == KeyEvent.ACTION_DOWN
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,19 @@ package de.jonasbark.accessibility
|
||||
import android.accessibilityservice.AccessibilityService
|
||||
import android.accessibilityservice.GestureDescription
|
||||
import android.accessibilityservice.GestureDescription.StrokeDescription
|
||||
import android.accessibilityservice.AccessibilityServiceInfo
|
||||
import android.content.Context
|
||||
import android.graphics.Path
|
||||
import android.graphics.Rect
|
||||
import android.media.AudioManager
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import android.view.InputDevice
|
||||
import android.view.KeyEvent
|
||||
import android.view.ViewConfiguration
|
||||
import android.view.accessibility.AccessibilityEvent
|
||||
import android.view.accessibility.AccessibilityEvent.CONTENT_CHANGE_TYPE_PANE_DISAPPEARED
|
||||
import android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
|
||||
import GlobalAction
|
||||
|
||||
|
||||
class AccessibilityService : AccessibilityService(), Listener {
|
||||
@@ -26,46 +31,101 @@ class AccessibilityService : AccessibilityService(), Listener {
|
||||
Observable.toService = null
|
||||
}
|
||||
|
||||
private val ignorePackages = listOf("com.android.systemui", "com.android.launcher", "com.android.settings")
|
||||
|
||||
override fun onAccessibilityEvent(event: AccessibilityEvent) {
|
||||
if (event.packageName == null || rootInActiveWindow == null) {
|
||||
return
|
||||
}
|
||||
if (event.contentChangeTypes == CONTENT_CHANGE_TYPE_PANE_DISAPPEARED) {
|
||||
if (event.eventType != TYPE_WINDOW_STATE_CHANGED || event.packageName in ignorePackages) {
|
||||
// we're not interested
|
||||
return
|
||||
}
|
||||
val currentPackageName = event.packageName.toString()
|
||||
val windowSize = getWindowSize()
|
||||
Observable.fromService?.onChange(packageName = currentPackageName, windowHeight = windowSize.bottom, windowWidth = windowSize.right)
|
||||
Observable.fromServiceWindow?.onChange(packageName = currentPackageName, window = windowSize)
|
||||
}
|
||||
|
||||
private fun getWindowSize(): Rect {
|
||||
val outBounds = Rect()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
rootInActiveWindow.getBoundsInWindow(outBounds)
|
||||
} else {
|
||||
rootInActiveWindow.getBoundsInScreen(outBounds)
|
||||
}
|
||||
rootInActiveWindow?.getBoundsInScreen(outBounds)
|
||||
return outBounds
|
||||
}
|
||||
|
||||
private fun simulateTap(x: Double, y: Double) {
|
||||
val gestureBuilder = GestureDescription.Builder()
|
||||
val path = Path()
|
||||
path.moveTo(x.toFloat(), y.toFloat())
|
||||
path.lineTo(x.toFloat()+1, y.toFloat())
|
||||
|
||||
val stroke = StrokeDescription(path, 0, ViewConfiguration.getTapTimeout().toLong())
|
||||
gestureBuilder.addStroke(stroke)
|
||||
|
||||
dispatchGesture(gestureBuilder.build(), null, null)
|
||||
}
|
||||
|
||||
override fun onInterrupt() {
|
||||
Log.d("AccessibilityService", "Service Interrupted")
|
||||
}
|
||||
|
||||
override fun performTouch(x: Double, y: Double) {
|
||||
simulateTap(x, y)
|
||||
override fun onServiceConnected() {
|
||||
super.onServiceConnected()
|
||||
// Request key event filtering so we receive onKeyEvent for hardware/HID media keys
|
||||
try {
|
||||
val info = serviceInfo ?: AccessibilityServiceInfo()
|
||||
info.flags = info.flags or AccessibilityServiceInfo.FLAG_REQUEST_FILTER_KEY_EVENTS
|
||||
// keep other capabilities as defined in XML
|
||||
setServiceInfo(info)
|
||||
} catch (e: Exception) {
|
||||
Log.w("AccessibilityService", "Failed to set service info for key events: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onKeyEvent(event: KeyEvent): Boolean {
|
||||
val keyString = KeyEvent.keyCodeToString(event.keyCode)
|
||||
// if currently active app is BikeControl => handle it, so keymap can be created
|
||||
if (!Observable.ignoreHidDevices && isBleRemote(event) && (rootInActiveWindow.packageName == "de.jonasbark.swiftcontrol" || Observable.handledKeys.contains(keyString))) {
|
||||
// Handle keys that have a keymap defined
|
||||
Log.d(
|
||||
"AccessibilityService",
|
||||
"onKeyEvent: keyCode=${event.keyCode} action=${event.action} scanCode=${event.scanCode} flags=${event.flags}"
|
||||
)
|
||||
|
||||
// Forward key events to the plugin (Flutter) and swallow them so they don't propagate.
|
||||
Observable.fromServiceKeys?.onKeyEvent(event)
|
||||
// Return true to indicate we've handled the event and it should be swallowed.
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private fun isBleRemote(event: KeyEvent): Boolean {
|
||||
val dev = InputDevice.getDevice(event.deviceId) ?: return false
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
dev.isExternal
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
override fun performGlobalAction(action: GlobalAction) {
|
||||
val mappedAction = when (action) {
|
||||
GlobalAction.BACK -> android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_BACK
|
||||
GlobalAction.DPAD_CENTER -> android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_DPAD_CENTER
|
||||
GlobalAction.DOWN -> android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_DPAD_DOWN
|
||||
GlobalAction.RIGHT -> android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_DPAD_RIGHT
|
||||
GlobalAction.UP -> android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_DPAD_UP
|
||||
GlobalAction.LEFT -> android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_DPAD_LEFT
|
||||
GlobalAction.HOME -> android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_HOME
|
||||
GlobalAction.RECENTS -> android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_RECENTS
|
||||
}
|
||||
performGlobalAction(mappedAction)
|
||||
}
|
||||
|
||||
override fun performTouch(x: Double, y: Double, isKeyDown: Boolean, isKeyUp: Boolean) {
|
||||
val gestureBuilder = GestureDescription.Builder()
|
||||
val path = Path()
|
||||
path.moveTo(x.toFloat(), y.toFloat())
|
||||
path.lineTo(x.toFloat()+1, y.toFloat())
|
||||
|
||||
val stroke = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
StrokeDescription(path, 0, ViewConfiguration.getTapTimeout().toLong(), isKeyDown && !isKeyUp)
|
||||
} else {
|
||||
// API 24–25: no “willContinue” support
|
||||
StrokeDescription(path, 0L, ViewConfiguration.getTapTimeout().toLong())
|
||||
}
|
||||
gestureBuilder.addStroke(stroke)
|
||||
|
||||
dispatchGesture(gestureBuilder.build(), null, null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,25 @@
|
||||
package de.jonasbark.accessibility
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.view.KeyEvent
|
||||
import GlobalAction
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
object Observable {
|
||||
var toService: Listener? = null
|
||||
var fromService: Receiver? = null
|
||||
var fromServiceWindow: Receiver? = null
|
||||
var fromServiceKeys: Receiver? = null
|
||||
var ignoreHidDevices: Boolean = false
|
||||
// Use concurrent set for thread-safe access from AccessibilityService and plugin
|
||||
var handledKeys: Set<String> = ConcurrentHashMap.newKeySet()
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun performTouch(x: Double, y: Double)
|
||||
fun performTouch(x: Double, y: Double, isKeyDown: Boolean, isKeyUp: Boolean)
|
||||
fun performGlobalAction(action: GlobalAction)
|
||||
}
|
||||
|
||||
interface Receiver {
|
||||
fun onChange(packageName: String, windowWidth: Int, windowHeight: Int)
|
||||
}
|
||||
fun onChange(packageName: String, window: Rect)
|
||||
fun onKeyEvent(event: KeyEvent)
|
||||
}
|
||||
|
||||
@@ -6,18 +6,59 @@ abstract class Accessibility {
|
||||
|
||||
void openPermissions();
|
||||
|
||||
void performTouch(double x, double y);
|
||||
void performTouch(double x, double y, {bool isKeyDown = true, bool isKeyUp = false});
|
||||
|
||||
void performGlobalAction(GlobalAction action);
|
||||
|
||||
void controlMedia(MediaAction action);
|
||||
|
||||
bool isRunning();
|
||||
|
||||
void ignoreHidDevices();
|
||||
|
||||
void setHandledKeys(List<String> keys);
|
||||
}
|
||||
|
||||
enum MediaAction { playPause, next, volumeUp, volumeDown }
|
||||
|
||||
enum GlobalAction {
|
||||
back,
|
||||
dpadCenter,
|
||||
down,
|
||||
right,
|
||||
up,
|
||||
left,
|
||||
home,
|
||||
recents,
|
||||
}
|
||||
|
||||
class WindowEvent {
|
||||
final String packageName;
|
||||
final int windowHeight;
|
||||
final int windowWidth;
|
||||
final int top;
|
||||
final int bottom;
|
||||
final int right;
|
||||
final int left;
|
||||
|
||||
WindowEvent({required this.packageName, required this.windowHeight, required this.windowWidth});
|
||||
WindowEvent({
|
||||
required this.packageName,
|
||||
required this.left,
|
||||
required this.right,
|
||||
required this.top,
|
||||
required this.bottom,
|
||||
});
|
||||
}
|
||||
|
||||
class AKeyEvent {
|
||||
final String source;
|
||||
final String hidKey;
|
||||
final bool keyDown;
|
||||
final bool keyUp;
|
||||
|
||||
AKeyEvent({required this.source, required this.hidKey, required this.keyDown, required this.keyUp});
|
||||
}
|
||||
|
||||
@EventChannelApi()
|
||||
abstract class EventChannelMethods {
|
||||
WindowEvent streamEvents();
|
||||
AKeyEvent hidKeyPressed();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Autogenerated from Pigeon (v25.2.0), do not edit directly.
|
||||
// Autogenerated from Pigeon (v25.5.0), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers
|
||||
|
||||
@@ -14,25 +14,65 @@ PlatformException _createConnectionError(String channelName) {
|
||||
message: 'Unable to establish connection on channel: "$channelName".',
|
||||
);
|
||||
}
|
||||
bool _deepEquals(Object? a, Object? b) {
|
||||
if (a is List && b is List) {
|
||||
return a.length == b.length &&
|
||||
a.indexed
|
||||
.every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]));
|
||||
}
|
||||
if (a is Map && b is Map) {
|
||||
return a.length == b.length && a.entries.every((MapEntry<Object?, Object?> entry) =>
|
||||
(b as Map<Object?, Object?>).containsKey(entry.key) &&
|
||||
_deepEquals(entry.value, b[entry.key]));
|
||||
}
|
||||
return a == b;
|
||||
}
|
||||
|
||||
|
||||
enum MediaAction {
|
||||
playPause,
|
||||
next,
|
||||
volumeUp,
|
||||
volumeDown,
|
||||
}
|
||||
|
||||
enum GlobalAction {
|
||||
back,
|
||||
dpadCenter,
|
||||
down,
|
||||
right,
|
||||
up,
|
||||
left,
|
||||
home,
|
||||
recents,
|
||||
}
|
||||
|
||||
class WindowEvent {
|
||||
WindowEvent({
|
||||
required this.packageName,
|
||||
required this.windowHeight,
|
||||
required this.windowWidth,
|
||||
required this.top,
|
||||
required this.bottom,
|
||||
required this.right,
|
||||
required this.left,
|
||||
});
|
||||
|
||||
String packageName;
|
||||
|
||||
int windowHeight;
|
||||
int top;
|
||||
|
||||
int windowWidth;
|
||||
int bottom;
|
||||
|
||||
int right;
|
||||
|
||||
int left;
|
||||
|
||||
List<Object?> _toList() {
|
||||
return <Object?>[
|
||||
packageName,
|
||||
windowHeight,
|
||||
windowWidth,
|
||||
top,
|
||||
bottom,
|
||||
right,
|
||||
left,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -43,8 +83,10 @@ class WindowEvent {
|
||||
result as List<Object?>;
|
||||
return WindowEvent(
|
||||
packageName: result[0]! as String,
|
||||
windowHeight: result[1]! as int,
|
||||
windowWidth: result[2]! as int,
|
||||
top: result[1]! as int,
|
||||
bottom: result[2]! as int,
|
||||
right: result[3]! as int,
|
||||
left: result[4]! as int,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -57,10 +99,63 @@ class WindowEvent {
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
}
|
||||
return
|
||||
packageName == other.packageName
|
||||
&& windowHeight == other.windowHeight
|
||||
&& windowWidth == other.windowWidth;
|
||||
return _deepEquals(encode(), other.encode());
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
||||
int get hashCode => Object.hashAll(_toList())
|
||||
;
|
||||
}
|
||||
|
||||
class AKeyEvent {
|
||||
AKeyEvent({
|
||||
required this.source,
|
||||
required this.hidKey,
|
||||
required this.keyDown,
|
||||
required this.keyUp,
|
||||
});
|
||||
|
||||
String source;
|
||||
|
||||
String hidKey;
|
||||
|
||||
bool keyDown;
|
||||
|
||||
bool keyUp;
|
||||
|
||||
List<Object?> _toList() {
|
||||
return <Object?>[
|
||||
source,
|
||||
hidKey,
|
||||
keyDown,
|
||||
keyUp,
|
||||
];
|
||||
}
|
||||
|
||||
Object encode() {
|
||||
return _toList(); }
|
||||
|
||||
static AKeyEvent decode(Object result) {
|
||||
result as List<Object?>;
|
||||
return AKeyEvent(
|
||||
source: result[0]! as String,
|
||||
hidKey: result[1]! as String,
|
||||
keyDown: result[2]! as bool,
|
||||
keyUp: result[3]! as bool,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
||||
bool operator ==(Object other) {
|
||||
if (other is! AKeyEvent || other.runtimeType != runtimeType) {
|
||||
return false;
|
||||
}
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
}
|
||||
return _deepEquals(encode(), other.encode());
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -77,8 +172,17 @@ class _PigeonCodec extends StandardMessageCodec {
|
||||
if (value is int) {
|
||||
buffer.putUint8(4);
|
||||
buffer.putInt64(value);
|
||||
} else if (value is WindowEvent) {
|
||||
} else if (value is MediaAction) {
|
||||
buffer.putUint8(129);
|
||||
writeValue(buffer, value.index);
|
||||
} else if (value is GlobalAction) {
|
||||
buffer.putUint8(130);
|
||||
writeValue(buffer, value.index);
|
||||
} else if (value is WindowEvent) {
|
||||
buffer.putUint8(131);
|
||||
writeValue(buffer, value.encode());
|
||||
} else if (value is AKeyEvent) {
|
||||
buffer.putUint8(132);
|
||||
writeValue(buffer, value.encode());
|
||||
} else {
|
||||
super.writeValue(buffer, value);
|
||||
@@ -89,7 +193,15 @@ class _PigeonCodec extends StandardMessageCodec {
|
||||
Object? readValueOfType(int type, ReadBuffer buffer) {
|
||||
switch (type) {
|
||||
case 129:
|
||||
final int? value = readValue(buffer) as int?;
|
||||
return value == null ? null : MediaAction.values[value];
|
||||
case 130:
|
||||
final int? value = readValue(buffer) as int?;
|
||||
return value == null ? null : GlobalAction.values[value];
|
||||
case 131:
|
||||
return WindowEvent.decode(readValue(buffer)!);
|
||||
case 132:
|
||||
return AKeyEvent.decode(readValue(buffer)!);
|
||||
default:
|
||||
return super.readValueOfType(type, buffer);
|
||||
}
|
||||
@@ -162,14 +274,134 @@ class Accessibility {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> performTouch(double x, double y) async {
|
||||
Future<void> performTouch(double x, double y, {bool isKeyDown = true, bool isKeyUp = false, }) async {
|
||||
final String pigeonVar_channelName = 'dev.flutter.pigeon.accessibility.Accessibility.performTouch$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[x, y]);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[x, y, isKeyDown, isKeyUp]);
|
||||
final List<Object?>? pigeonVar_replyList =
|
||||
await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> performGlobalAction(GlobalAction action) async {
|
||||
final String pigeonVar_channelName = 'dev.flutter.pigeon.accessibility.Accessibility.performGlobalAction$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[action]);
|
||||
final List<Object?>? pigeonVar_replyList =
|
||||
await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> controlMedia(MediaAction action) async {
|
||||
final String pigeonVar_channelName = 'dev.flutter.pigeon.accessibility.Accessibility.controlMedia$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[action]);
|
||||
final List<Object?>? pigeonVar_replyList =
|
||||
await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> isRunning() async {
|
||||
final String pigeonVar_channelName = 'dev.flutter.pigeon.accessibility.Accessibility.isRunning$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
||||
final List<Object?>? pigeonVar_replyList =
|
||||
await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else if (pigeonVar_replyList[0] == null) {
|
||||
throw PlatformException(
|
||||
code: 'null-error',
|
||||
message: 'Host platform returned null value for non-null return value.',
|
||||
);
|
||||
} else {
|
||||
return (pigeonVar_replyList[0] as bool?)!;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> ignoreHidDevices() async {
|
||||
final String pigeonVar_channelName = 'dev.flutter.pigeon.accessibility.Accessibility.ignoreHidDevices$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
||||
final List<Object?>? pigeonVar_replyList =
|
||||
await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setHandledKeys(List<String> keys) async {
|
||||
final String pigeonVar_channelName = 'dev.flutter.pigeon.accessibility.Accessibility.setHandledKeys$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[keys]);
|
||||
final List<Object?>? pigeonVar_replyList =
|
||||
await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
@@ -197,3 +429,14 @@ Stream<WindowEvent> streamEvents( {String instanceName = ''}) {
|
||||
});
|
||||
}
|
||||
|
||||
Stream<AKeyEvent> hidKeyPressed( {String instanceName = ''}) {
|
||||
if (instanceName.isNotEmpty) {
|
||||
instanceName = '.$instanceName';
|
||||
}
|
||||
final EventChannel hidKeyPressedChannel =
|
||||
EventChannel('dev.flutter.pigeon.accessibility.EventChannelMethods.hidKeyPressed$instanceName', pigeonMethodCodec);
|
||||
return hidKeyPressedChannel.receiveBroadcastStream().map((dynamic event) {
|
||||
return event as AKeyEvent;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,10 @@ linter:
|
||||
rules:
|
||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||
- require_trailing_commas
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
formatter:
|
||||
page_width: 120
|
||||
trailing_commas: preserve
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import java.io.FileInputStream
|
||||
import java.util.*
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("kotlin-android")
|
||||
@@ -5,10 +8,15 @@ plugins {
|
||||
id("dev.flutter.flutter-gradle-plugin")
|
||||
}
|
||||
|
||||
|
||||
val keystorePropertiesFile = rootProject.file("keystore.properties")
|
||||
val keystoreProperties = Properties()
|
||||
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
|
||||
|
||||
android {
|
||||
namespace = "de.jonasbark.swift_play"
|
||||
namespace = "de.jonasbark.swiftcontrol"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = "27.0.12077973"
|
||||
ndkVersion = "28.2.13676358"
|
||||
|
||||
compileOptions {
|
||||
// Flag to enable support for the new language APIs
|
||||
@@ -24,7 +32,7 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId = "de.jonasbark.swift_play"
|
||||
applicationId = "de.jonasbark.swiftcontrol"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
minSdk = 24
|
||||
@@ -33,11 +41,18 @@ android {
|
||||
versionName = flutter.versionName
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
create("config") {
|
||||
keyAlias = keystoreProperties["keyAlias"] as String
|
||||
keyPassword = keystoreProperties["keyPassword"] as String
|
||||
storeFile = file("../${keystoreProperties["storeFile"] as String}")
|
||||
storePassword = keystoreProperties["storePassword"] as String
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// TODO: Add your own signing config for the release build.
|
||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
signingConfig = signingConfigs.getByName("config")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
@@ -1,7 +1,10 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Allow Bluetooth -->
|
||||
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />
|
||||
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE"/>
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
|
||||
|
||||
<!-- New Bluetooth permissions in Android 12
|
||||
https://developer.android.com/about/versions/12/features/bluetooth-permissions -->
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />
|
||||
@@ -14,13 +17,17 @@
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE"/>
|
||||
|
||||
<uses-permission android:name="android.permission.BILLING"/>
|
||||
|
||||
<!-- legacy for Android 9 or lower -->
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="28" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="28" tools:replace="android:maxSdkVersion" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
|
||||
<!-- to check if you have the latest version -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:label="SwiftControl"
|
||||
android:label="BikeControl"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
package de.jonasbark.swift_play
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
class MainActivity : FlutterActivity()
|
||||
@@ -0,0 +1,46 @@
|
||||
package de.jonasbark.swiftcontrol
|
||||
|
||||
import android.hardware.input.InputManager
|
||||
import android.os.Handler
|
||||
import android.view.InputDevice
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
import io.flutter.embedding.android.FlutterFragmentActivity
|
||||
import org.flame_engine.gamepads_android.GamepadsCompatibleActivity
|
||||
|
||||
class MainActivity: FlutterFragmentActivity(), GamepadsCompatibleActivity {
|
||||
var keyListener: ((KeyEvent) -> Boolean)? = null
|
||||
var motionListener: ((MotionEvent) -> Boolean)? = null
|
||||
|
||||
override fun isGamepadsInputDevice(device: InputDevice): Boolean {
|
||||
return device.sources and InputDevice.SOURCE_GAMEPAD == InputDevice.SOURCE_GAMEPAD
|
||||
|| device.sources and InputDevice.SOURCE_JOYSTICK == InputDevice.SOURCE_JOYSTICK
|
||||
// Some bluetooth keyboards are identified as GamePad. Check if it is ALPHABETIC keyboard.
|
||||
// && device.keyboardType != InputDevice.KEYBOARD_TYPE_ALPHABETIC
|
||||
}
|
||||
|
||||
override fun dispatchGenericMotionEvent(motionEvent: MotionEvent): Boolean {
|
||||
return motionListener?.invoke(motionEvent) ?: false
|
||||
}
|
||||
|
||||
override fun dispatchKeyEvent(keyEvent: KeyEvent): Boolean {
|
||||
if (keyListener?.invoke(keyEvent) == true) {
|
||||
return true
|
||||
}
|
||||
return super.dispatchKeyEvent(keyEvent)
|
||||
}
|
||||
|
||||
override fun registerInputDeviceListener(
|
||||
listener: InputManager.InputDeviceListener, handler: Handler?) {
|
||||
val inputManager = getSystemService(INPUT_SERVICE) as InputManager
|
||||
inputManager.registerInputDeviceListener(listener, null)
|
||||
}
|
||||
|
||||
override fun registerKeyEventHandler(handler: (KeyEvent) -> Boolean) {
|
||||
keyListener = handler
|
||||
}
|
||||
|
||||
override fun registerMotionEventHandler(handler: (MotionEvent) -> Boolean) {
|
||||
motionListener = handler
|
||||
}
|
||||
}
|
||||
BIN
android/app/src/main/res/drawable-hdpi/ic_notification.png
Normal file
|
After Width: | Height: | Size: 785 B |
BIN
android/app/src/main/res/drawable-mdpi/ic_notification.png
Normal file
|
After Width: | Height: | Size: 476 B |
@@ -4,9 +4,9 @@
|
||||
<item android:drawable="?android:colorBackground" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
android:src="@mipmap/ic_launcher" />
|
||||
</item>
|
||||
</layer-list>
|
||||
|
||||
BIN
android/app/src/main/res/drawable-xhdpi/ic_notification.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
android/app/src/main/res/drawable-xxhdpi/ic_notification.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
android/app/src/main/res/drawable-xxxhdpi/ic_notification.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
@@ -4,9 +4,9 @@
|
||||
<item android:drawable="@android:color/white" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
android:src="@mipmap/ic_launcher" />
|
||||
</item>
|
||||
</layer-list>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@mipmap/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@mipmap/ic_launcher_monochrome"/>
|
||||
</adaptive-icon>
|
||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 8.0 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 4.3 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 13 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 125 KiB |
3
android/app/src/main/res/raw/keep.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:keep="@drawable/*" />
|
||||
@@ -1,8 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<accessibility-service
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:accessibilityEventTypes="typeWindowStateChanged|typeViewClicked"
|
||||
android:accessibilityFeedbackType="feedbackGeneric"
|
||||
android:accessibilityFlags="flagDefault"
|
||||
android:accessibilityFlags="flagDefault|flagRequestFilterKeyEvents"
|
||||
android:canRetrieveWindowContent="true"
|
||||
android:canRequestFilterKeyEvents="true"
|
||||
android:canPerformGestures="true"
|
||||
android:notificationTimeout="100"/>
|
||||
|
||||
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||
|
||||
@@ -18,8 +18,8 @@ pluginManagement {
|
||||
|
||||
plugins {
|
||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||
id("com.android.application") version "8.7.0" apply false
|
||||
id("org.jetbrains.kotlin.android") version "1.8.22" apply false
|
||||
id("com.android.application") version "8.11.1" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
|
||||
}
|
||||
|
||||
include(":app")
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
name: Bug Report
|
||||
about: Create a report to help us improve
|
||||
title: "fix: "
|
||||
labels: bug
|
||||
---
|
||||
|
||||
**Description**
|
||||
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**Steps To Reproduce**
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected Behavior**
|
||||
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Additional Context**
|
||||
|
||||
Add any other context about the problem here.
|
||||
@@ -1,14 +0,0 @@
|
||||
---
|
||||
name: Build System
|
||||
about: Changes that affect the build system or external dependencies
|
||||
title: "build: "
|
||||
labels: build
|
||||
---
|
||||
|
||||
**Description**
|
||||
|
||||
Describe what changes need to be done to the build system and why.
|
||||
|
||||
**Requirements**
|
||||
|
||||
- [ ] The build system is passing
|
||||
@@ -1,14 +0,0 @@
|
||||
---
|
||||
name: Chore
|
||||
about: Other changes that don't modify src or test files
|
||||
title: "chore: "
|
||||
labels: chore
|
||||
---
|
||||
|
||||
**Description**
|
||||
|
||||
Clearly describe what change is needed and why. If this changes code then please use another issue type.
|
||||
|
||||
**Requirements**
|
||||
|
||||
- [ ] No functional changes to the code
|
||||
@@ -1,14 +0,0 @@
|
||||
---
|
||||
name: Continuous Integration
|
||||
about: Changes to the CI configuration files and scripts
|
||||
title: "ci: "
|
||||
labels: ci
|
||||
---
|
||||
|
||||
**Description**
|
||||
|
||||
Describe what changes need to be done to the ci/cd system and why.
|
||||
|
||||
**Requirements**
|
||||
|
||||
- [ ] The ci system is passing
|
||||
@@ -1 +0,0 @@
|
||||
blank_issues_enabled: false
|
||||
@@ -1,14 +0,0 @@
|
||||
---
|
||||
name: Documentation
|
||||
about: Improve the documentation so all collaborators have a common understanding
|
||||
title: "docs: "
|
||||
labels: documentation
|
||||
---
|
||||
|
||||
**Description**
|
||||
|
||||
Clearly describe what documentation you are looking to add or improve.
|
||||
|
||||
**Requirements**
|
||||
|
||||
- [ ] Requirements go here
|
||||
@@ -1,18 +0,0 @@
|
||||
---
|
||||
name: Feature Request
|
||||
about: A new feature to be added to the project
|
||||
title: "feat: "
|
||||
labels: feature
|
||||
---
|
||||
|
||||
**Description**
|
||||
|
||||
Clearly describe what you are looking to add. The more context the better.
|
||||
|
||||
**Requirements**
|
||||
|
||||
- [ ] Checklist of requirements to be fulfilled
|
||||
|
||||
**Additional Context**
|
||||
|
||||
Add any other context or screenshots about the feature request go here.
|
||||
@@ -1,14 +0,0 @@
|
||||
---
|
||||
name: Performance Update
|
||||
about: A code change that improves performance
|
||||
title: "perf: "
|
||||
labels: performance
|
||||
---
|
||||
|
||||
**Description**
|
||||
|
||||
Clearly describe what code needs to be changed and what the performance impact is going to be. Bonus point's if you can tie this directly to user experience.
|
||||
|
||||
**Requirements**
|
||||
|
||||
- [ ] There is no drop in test coverage.
|
||||
@@ -1,14 +0,0 @@
|
||||
---
|
||||
name: Refactor
|
||||
about: A code change that neither fixes a bug nor adds a feature
|
||||
title: "refactor: "
|
||||
labels: refactor
|
||||
---
|
||||
|
||||
**Description**
|
||||
|
||||
Clearly describe what needs to be refactored and why. Please provide links to related issues (bugs or upcoming features) in order to help prioritize.
|
||||
|
||||
**Requirements**
|
||||
|
||||
- [ ] There is no drop in test coverage.
|
||||
@@ -1,16 +0,0 @@
|
||||
---
|
||||
name: Revert Commit
|
||||
about: Reverts a previous commit
|
||||
title: "revert: "
|
||||
labels: revert
|
||||
---
|
||||
|
||||
**Description**
|
||||
|
||||
Provide a link to a PR/Commit that you are looking to revert and why.
|
||||
|
||||
**Requirements**
|
||||
|
||||
- [ ] Change has been reverted
|
||||
- [ ] No change in test coverage has happened
|
||||
- [ ] A new ticket is created for any follow on work that needs to happen
|
||||
@@ -1,14 +0,0 @@
|
||||
---
|
||||
name: Style Changes
|
||||
about: Changes that do not affect the meaning of the code (white space, formatting, missing semi-colons, etc)
|
||||
title: "style: "
|
||||
labels: style
|
||||
---
|
||||
|
||||
**Description**
|
||||
|
||||
Clearly describe what you are looking to change and why.
|
||||
|
||||
**Requirements**
|
||||
|
||||
- [ ] There is no drop in test coverage.
|
||||
@@ -1,14 +0,0 @@
|
||||
---
|
||||
name: Test
|
||||
about: Adding missing tests or correcting existing tests
|
||||
title: "test: "
|
||||
labels: test
|
||||
---
|
||||
|
||||
**Description**
|
||||
|
||||
List out the tests that need to be added or changed. Please also include any information as to why this was not covered in the past.
|
||||
|
||||
**Requirements**
|
||||
|
||||
- [ ] There is no drop in test coverage.
|
||||
@@ -1,27 +0,0 @@
|
||||
<!--
|
||||
Thanks for contributing!
|
||||
|
||||
Provide a description of your changes below and a general summary in the title
|
||||
|
||||
Please look at the following checklist to ensure that your PR can be accepted quickly:
|
||||
-->
|
||||
|
||||
## Status
|
||||
|
||||
**READY/IN DEVELOPMENT/HOLD**
|
||||
|
||||
## Description
|
||||
|
||||
<!--- Describe your changes in detail -->
|
||||
|
||||
## Type of Change
|
||||
|
||||
<!--- Put an `x` in all the boxes that apply: -->
|
||||
|
||||
- [ ] ✨ New feature (non-breaking change which adds functionality)
|
||||
- [ ] 🛠️ Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] ❌ Breaking change (fix or feature that would cause existing functionality to change)
|
||||
- [ ] 🧹 Code refactor
|
||||
- [ ] ✅ Build configuration change
|
||||
- [ ] 📝 Documentation
|
||||
- [ ] 🗑️ Chore
|
||||
21
flutter_blue_plus_windows/.github/cspell.json
vendored
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"version": "0.2",
|
||||
"$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json",
|
||||
"dictionaries": ["vgv_allowed", "vgv_forbidden"],
|
||||
"dictionaryDefinitions": [
|
||||
{
|
||||
"name": "vgv_allowed",
|
||||
"path": "https://raw.githubusercontent.com/verygoodopensource/very_good_dictionaries/main/allowed.txt",
|
||||
"description": "Allowed VGV Spellings"
|
||||
},
|
||||
{
|
||||
"name": "vgv_forbidden",
|
||||
"path": "https://raw.githubusercontent.com/verygoodopensource/very_good_dictionaries/main/forbidden.txt",
|
||||
"description": "Forbidden VGV Spellings"
|
||||
}
|
||||
],
|
||||
"useGitignore": true,
|
||||
"words": [
|
||||
"flutter_blue_plus_windows"
|
||||
]
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
version: 2
|
||||
enable-beta-ecosystems: true
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
- package-ecosystem: "pub"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
12
flutter_blue_plus_windows/.gitignore
vendored
@@ -1,12 +0,0 @@
|
||||
# See https://www.dartlang.org/guides/libraries/private-files
|
||||
|
||||
# Files and directories created by pub
|
||||
.dart_tool/
|
||||
.packages
|
||||
build/
|
||||
pubspec.lock
|
||||
/.idea/
|
||||
/.flutter-plugins
|
||||
/.flutter-plugins-dependencies
|
||||
|
||||
interanl_example
|
||||
@@ -1,106 +0,0 @@
|
||||
## 1.26.1
|
||||
* Add setOptions.
|
||||
|
||||
## 1.26.0
|
||||
* Set `flutter_blue_plus` upperbound <1.35.0 (due to api changes)
|
||||
|
||||
## 1.25.0
|
||||
* Ensure tracing connection when reconnection occurs after force disconnection.
|
||||
|
||||
## 1.24.22
|
||||
* Upgrade FBP version to `>=1.32.4 <=1.40.0` #24.
|
||||
|
||||
## 1.24.21
|
||||
* fix: startScan() doesn't return correct ScanResult #25
|
||||
|
||||
## 1.24.20
|
||||
* Downgrade FBP version to `>=1.32.4 <=1.33.6` due to the breaking changes.
|
||||
* After upgrade process, the dependencies will be returned to `>=1.34.4 <1.40.0` #24.
|
||||
|
||||
## 1.24.19
|
||||
* Fix a bug with `onValueReceived` of emitting write packet #22.
|
||||
|
||||
## 1.24.18
|
||||
* Add implementation of `BluetoothDeviceWindow.fromId()` #21.
|
||||
|
||||
## 1.24.15
|
||||
* Fix a bug w.r.t. company ID in manufacturer data. (@betto-a #18)
|
||||
|
||||
## 1.24.14
|
||||
* Implement cancelOnDisconnect (@jefflongo #16)
|
||||
|
||||
## 1.24.12
|
||||
* Fix minor bug w.r.t. `characteristic.isNotifying`.
|
||||
|
||||
## 1.24.11
|
||||
* Fix breaking changes of FBP w.r.t. `systemDevices(List withServices)`.
|
||||
|
||||
## 1.24.10
|
||||
* Add support for `cancelWhenScanComplete`
|
||||
|
||||
## 1.24.9
|
||||
* Implement scan filter (including `withServices`, `withRemoteIds`, `withNames`).
|
||||
|
||||
## 1.24.8
|
||||
* Keep manufacturer data when scanning.
|
||||
|
||||
## 1.24.7
|
||||
* Keep service uuids when scanning.
|
||||
|
||||
## 1.24.0
|
||||
* Update `README.md`.
|
||||
|
||||
## 1.23.6
|
||||
* Add unimplemented notification for `read` or `write`.
|
||||
|
||||
## 1.14.0
|
||||
* Remove dependencies `ffi` and `win32` to avoid compile error for web
|
||||
|
||||
## 1.9.5
|
||||
* Apply `flutter blue plus` to `1.28.13`.
|
||||
|
||||
## 1.9.0
|
||||
* Apply a breaking changes `Guid` in `Flutter blue plus` packages.
|
||||
* Use `uuid128` instead of `toString()`.
|
||||
|
||||
## 1.8.10
|
||||
* Fix `Guid` bug related with `Flutter blue plus` packages.
|
||||
|
||||
## 1.8.0
|
||||
* Fix bug with Guid converted from string due to starting/ending with '{ }' in `WinBLE`
|
||||
|
||||
## 1.7.0
|
||||
* Apply `flutter blue plus 1.28.5` (there is several breaking changes.).
|
||||
|
||||
## 1.6.6
|
||||
* Add cache for storing characteristics.
|
||||
|
||||
## 1.6.0
|
||||
* Apply `Flutter blue plus 1.26.0`, (there is a breaking change with `connect()`).
|
||||
|
||||
## 1.5.7
|
||||
* Remove connection by OS when performing `startScan`.
|
||||
|
||||
## 1.5.3
|
||||
* Write logs when connection state stream is started/terminated.
|
||||
|
||||
## 1.5.2
|
||||
* Fix a bug of features added in `1.5.1`
|
||||
|
||||
## 1.5.1
|
||||
* Remove device from connected device list when device is disconnected.
|
||||
|
||||
## 1.5.0
|
||||
* Split functionality of `disconnect` / `removeBond`.
|
||||
|
||||
## 1.4.0
|
||||
* Implement `Subscribe/Unsubscribe Characteristic`.
|
||||
|
||||
## 1.1.0
|
||||
* Implement `Read/Write Characteristic`.
|
||||
|
||||
## 1.0.5
|
||||
* Change `rxdart` version to `0.27.7`.
|
||||
|
||||
## 1.0.0
|
||||
* Initial release (using Github action).
|
||||
@@ -1,7 +0,0 @@
|
||||
Copyright 2023 Himchan Park
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -1,54 +0,0 @@
|
||||
[](https://pub.dartlang.org/packages/flutter_blue_plus_windows)
|
||||
|
||||
## Flutter Blue Plus Windows
|
||||
|
||||
This project is a wrapper library for `Flutter Blue Plus` and `Win_ble`.
|
||||
It allows `Flutter_blue_plus` to operate on Windows.
|
||||
|
||||
With minimal effort, you can use Flutter Blue Plus on Windows.
|
||||
|
||||
## Usage
|
||||
Only you need to do is change the import statement.
|
||||
|
||||
```dart
|
||||
// instead of import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||
import 'package:flutter_blue_plus_windows/flutter_blue_plus_windows.dart';
|
||||
|
||||
// Alternatively, you can hide FlutterBluePlus when importing the FBP statement
|
||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart' hide FlutterBluePlus;
|
||||
import 'package:flutter_blue_plus_windows/flutter_blue_plus_windows.dart';
|
||||
|
||||
```
|
||||
|
||||
### Scan devices
|
||||
```dart
|
||||
final scannedDevices = <ScanResult>{};
|
||||
|
||||
const timeout = Duration(seconds: 3);
|
||||
FlutterBluePlus.startScan(timeout: timeout);
|
||||
|
||||
final sub = FlutterBluePlus.scanResults.expand((e)=>e).listen(scannedDevices.add);
|
||||
|
||||
await Future.delayed(timeout);
|
||||
sub.cancel();
|
||||
scannedDevices.forEach(print);
|
||||
```
|
||||
|
||||
### Connect a device
|
||||
```dart
|
||||
final scannedDevice = scannedDevices
|
||||
.where((scanResult) => scanResult.device.platformName == DEVICE_NAME)
|
||||
.firstOrNull;
|
||||
final device = scannedDevice?.device;
|
||||
device?.connect();
|
||||
```
|
||||
|
||||
### Disconnect the device
|
||||
```dart
|
||||
device?.disconnect();
|
||||
```
|
||||
|
||||
Check out the usage of Flutter Blue Plus on [Flutter Blue Plus](https://pub.dev/packages/flutter_blue_plus)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
library flutter_blue_plus_windows;
|
||||
|
||||
export 'package:flutter_blue_plus/flutter_blue_plus.dart' hide FlutterBluePlus;
|
||||
export 'package:win_ble/win_ble.dart';
|
||||
export 'package:win_ble/win_file.dart';
|
||||
|
||||
export 'src/flutter_blue_plus_windows.dart';
|
||||
@@ -1,19 +0,0 @@
|
||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||
import 'package:win_ble/win_ble.dart';
|
||||
|
||||
extension BluetoothAdapterStateExtension on BleState {
|
||||
BluetoothAdapterState toAdapterState() {
|
||||
switch(this){
|
||||
case BleState.On:
|
||||
return BluetoothAdapterState.on;
|
||||
case BleState.Off:
|
||||
return BluetoothAdapterState.off;
|
||||
case BleState.Unknown:
|
||||
return BluetoothAdapterState.unknown;
|
||||
case BleState.Disabled:
|
||||
return BluetoothAdapterState.unavailable;
|
||||
case BleState.Unsupported:
|
||||
return BluetoothAdapterState.unauthorized;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import 'package:flutter_blue_plus_platform_interface/flutter_blue_plus_platform_interface.dart';
|
||||
import 'package:flutter_blue_plus_windows/flutter_blue_plus_windows.dart';
|
||||
|
||||
extension BluetoothCharacteristicExtension on BluetoothCharacteristic {
|
||||
BmBluetoothCharacteristic toProto() {
|
||||
return BmBluetoothCharacteristic(
|
||||
remoteId: DeviceIdentifier(remoteId.str),
|
||||
serviceUuid: serviceUuid,
|
||||
characteristicUuid: characteristicUuid,
|
||||
descriptors: [for (final d in descriptors) d.toProto()],
|
||||
properties: properties.toProto(),
|
||||
primaryServiceUuid: null, // TODO: API changes
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||
import 'package:flutter_blue_plus_platform_interface/flutter_blue_plus_platform_interface.dart';
|
||||
|
||||
extension BluetoothDescriptorExtension on BluetoothDescriptor {
|
||||
BmBluetoothDescriptor toProto() {
|
||||
return BmBluetoothDescriptor(
|
||||
remoteId: DeviceIdentifier(remoteId.str),
|
||||
serviceUuid: serviceUuid,
|
||||
characteristicUuid: characteristicUuid,
|
||||
descriptorUuid: descriptorUuid,
|
||||
primaryServiceUuid: null, // TODO: API changes
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import 'package:flutter_blue_plus_platform_interface/flutter_blue_plus_platform_interface.dart';
|
||||
import 'package:flutter_blue_plus_windows/flutter_blue_plus_windows.dart';
|
||||
|
||||
extension BluetoothServiceExtension on BluetoothService {
|
||||
BmBluetoothService toProto() {
|
||||
return BmBluetoothService(
|
||||
serviceUuid: serviceUuid,
|
||||
remoteId: DeviceIdentifier(remoteId.str),
|
||||
characteristics: [for (final c in characteristics) c.toProto()],
|
||||
primaryServiceUuid: null, // TODO: API changes
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||
import 'package:flutter_blue_plus_platform_interface/flutter_blue_plus_platform_interface.dart';
|
||||
|
||||
extension CharacteristicPropertiesExtension on CharacteristicProperties {
|
||||
BmCharacteristicProperties toProto() {
|
||||
return BmCharacteristicProperties(
|
||||
broadcast: broadcast,
|
||||
read: read,
|
||||
writeWithoutResponse: writeWithoutResponse,
|
||||
write: write,
|
||||
notify: notify,
|
||||
indicate: indicate,
|
||||
authenticatedSignedWrites: authenticatedSignedWrites,
|
||||
extendedProperties: extendedProperties,
|
||||
notifyEncryptionRequired: notifyEncryptionRequired,
|
||||
indicateEncryptionRequired: indicateEncryptionRequired,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
export 'bluetooth_adapter_state_extension.dart';
|
||||
export 'bluetooth_characteristic_extension.dart';
|
||||
export 'bluetooth_descriptor_extension.dart';
|
||||
export 'bluetooth_service_extension.dart';
|
||||
export 'characteristic_properties_extension.dart';
|
||||
@@ -1,3 +0,0 @@
|
||||
export 'extension/extension.dart';
|
||||
export 'windows/windows.dart';
|
||||
export 'wrapper/wrapper.dart';
|
||||
@@ -1,160 +0,0 @@
|
||||
part of 'windows.dart';
|
||||
|
||||
class BluetoothCharacteristicWindows extends BluetoothCharacteristic {
|
||||
final DeviceIdentifier remoteId;
|
||||
final Guid serviceUuid;
|
||||
final Guid? secondaryServiceUuid;
|
||||
final Guid characteristicUuid;
|
||||
final List<BluetoothDescriptor> descriptors;
|
||||
|
||||
final Properties propertiesWinBle;
|
||||
|
||||
BluetoothCharacteristicWindows({
|
||||
required this.remoteId,
|
||||
required this.serviceUuid,
|
||||
required this.characteristicUuid,
|
||||
required this.descriptors,
|
||||
required this.propertiesWinBle,
|
||||
this.secondaryServiceUuid,
|
||||
}) : super.fromProto(
|
||||
BmBluetoothCharacteristic(
|
||||
remoteId: DeviceIdentifier(remoteId.str),
|
||||
serviceUuid: serviceUuid,
|
||||
characteristicUuid: characteristicUuid,
|
||||
descriptors: [
|
||||
for (final descriptor in descriptors)
|
||||
BmBluetoothDescriptor(
|
||||
remoteId: DeviceIdentifier(descriptor.remoteId.str),
|
||||
serviceUuid: descriptor.serviceUuid,
|
||||
characteristicUuid: descriptor.characteristicUuid,
|
||||
descriptorUuid: descriptor.uuid,
|
||||
primaryServiceUuid: null, // TODO: API changes
|
||||
),
|
||||
],
|
||||
properties: BmCharacteristicProperties(
|
||||
broadcast: propertiesWinBle.broadcast ?? false,
|
||||
read: propertiesWinBle.read ?? false,
|
||||
writeWithoutResponse: propertiesWinBle.writeWithoutResponse ?? false,
|
||||
write: propertiesWinBle.write ?? false,
|
||||
notify: propertiesWinBle.notify ?? false,
|
||||
indicate: propertiesWinBle.indicate ?? false,
|
||||
authenticatedSignedWrites: propertiesWinBle.authenticatedSignedWrites ?? false,
|
||||
// TODO: implementation missing
|
||||
extendedProperties: false,
|
||||
// TODO: implementation missing
|
||||
notifyEncryptionRequired: false,
|
||||
// TODO: implementation missing
|
||||
indicateEncryptionRequired: false,
|
||||
),
|
||||
primaryServiceUuid: null, // TODO: API changes
|
||||
),
|
||||
);
|
||||
|
||||
String get _address => remoteId.str.toLowerCase();
|
||||
|
||||
String get _key => "$serviceUuid:$characteristicUuid";
|
||||
|
||||
FBP.BluetoothDevice get device =>
|
||||
FlutterBluePlusWindows.connectedDevices.firstWhere((device) => device.remoteId == remoteId);
|
||||
|
||||
/// this variable is updated:
|
||||
/// - anytime `read()` is called
|
||||
/// - anytime `write()` is called
|
||||
/// - anytime a notification arrives (if subscribed)
|
||||
List<int> get lastValue => FlutterBluePlusWindows._lastChrs[remoteId]?[_key] ?? [];
|
||||
|
||||
/// this stream emits values:
|
||||
/// - anytime `read()` is called
|
||||
/// - anytime `write()` is called
|
||||
/// - anytime a notification arrives (if subscribed)
|
||||
/// - and when first listened to, it re-emits the last value for convenience
|
||||
Stream<List<int>> get lastValueStream => _mergeStreams(
|
||||
[
|
||||
WinBle.characteristicValueStreamOf(
|
||||
address: _address,
|
||||
serviceId: serviceUuid.str128,
|
||||
characteristicId: characteristicUuid.str128,
|
||||
),
|
||||
FlutterBluePlusWindows._charReadWriteStream.where((e) => e.$1 == _key).map((e) => e.$2)
|
||||
],
|
||||
).map((p) => <int>[...p]).newStreamWithInitialValue(lastValue).asBroadcastStream();
|
||||
|
||||
/// this stream emits values:
|
||||
/// - anytime `read()` is called
|
||||
/// - anytime a notification arrives (if subscribed)
|
||||
Stream<List<int>> get onValueReceived => _mergeStreams(
|
||||
[
|
||||
WinBle.characteristicValueStreamOf(
|
||||
address: _address,
|
||||
serviceId: serviceUuid.str128,
|
||||
characteristicId: characteristicUuid.str128,
|
||||
),
|
||||
FlutterBluePlusWindows._charReadStream.where((e) => e.$1 == _key).map((e) => e.$2)
|
||||
],
|
||||
).map((p) => <int>[...p]).asBroadcastStream();
|
||||
|
||||
// TODO: need to verify
|
||||
bool get isNotifying => FlutterBluePlusWindows._isNotifying[remoteId]?[_key] ?? false;
|
||||
|
||||
Future<List<int>> read({int timeout = 15}) async {
|
||||
final value = await WinBle.read(
|
||||
address: _address,
|
||||
serviceId: serviceUuid.str128,
|
||||
characteristicId: characteristicUuid.str128,
|
||||
);
|
||||
FlutterBluePlusWindows._charReadWriteStreamController.add((_key, value));
|
||||
FlutterBluePlusWindows._charReadStreamController.add((_key, value));
|
||||
FlutterBluePlusWindows._lastChrs[remoteId] ??= {};
|
||||
FlutterBluePlusWindows._lastChrs[remoteId]?[_key] = value;
|
||||
return value;
|
||||
}
|
||||
|
||||
Future<void> write(List<int> value,
|
||||
{bool allowLongWrite = false, bool withoutResponse = false, int timeout = 15}) async {
|
||||
await WinBle.write(
|
||||
address: _address,
|
||||
service: serviceUuid.str128,
|
||||
characteristic: characteristicUuid.str128,
|
||||
data: Uint8List.fromList(value),
|
||||
writeWithResponse: !withoutResponse, // propertiesWinBle.writeWithoutResponse ?? false,
|
||||
);
|
||||
FlutterBluePlusWindows._charReadWriteStreamController.add((_key, value));
|
||||
FlutterBluePlusWindows._lastChrs[remoteId] ??= {};
|
||||
FlutterBluePlusWindows._lastChrs[remoteId]?[_key] = value;
|
||||
}
|
||||
|
||||
// TODO: need to verify
|
||||
Future<bool> setNotifyValue(
|
||||
bool notify, {
|
||||
int timeout = 15, // TODO: missing implementation
|
||||
bool forceIndications = false, // TODO: missing implementation
|
||||
}) async {
|
||||
/// unSubscribeFromCharacteristic
|
||||
try {
|
||||
await WinBle.unSubscribeFromCharacteristic(
|
||||
address: _address,
|
||||
serviceId: serviceUuid.str128,
|
||||
characteristicId: characteristicUuid.str128,
|
||||
);
|
||||
} catch (e) {
|
||||
log('WinBle.unSubscribeFromCharacteristic was performed '
|
||||
'before setNotifyValue()');
|
||||
}
|
||||
|
||||
/// set notify
|
||||
try {
|
||||
if (notify) {
|
||||
await WinBle.subscribeToCharacteristic(
|
||||
address: _address,
|
||||
serviceId: serviceUuid.str128,
|
||||
characteristicId: characteristicUuid.str128,
|
||||
);
|
||||
}
|
||||
FlutterBluePlusWindows._isNotifying[remoteId] ??= {};
|
||||
FlutterBluePlusWindows._isNotifying[remoteId]?[_key] = notify;
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,310 +0,0 @@
|
||||
// Bluetooth Device Page:
|
||||
// https://github.com/boskokg/flutter_blue_plus/blob/master/lib/src/bluetooth_device.dart
|
||||
|
||||
part of 'windows.dart';
|
||||
|
||||
class BluetoothDeviceWindows extends FBP.BluetoothDevice {
|
||||
BluetoothDeviceWindows({required super.remoteId});
|
||||
|
||||
// used for 'servicesStream' public api
|
||||
final _services = StreamController<List<BluetoothServiceWindows>>.broadcast();
|
||||
|
||||
// used for 'isDiscoveringServices' public api
|
||||
final _isDiscoveringServices = _StreamController(initialValue: false);
|
||||
|
||||
String get _address => remoteId.str.toLowerCase();
|
||||
|
||||
/// Create a device from an id
|
||||
/// - to connect, this device must have been discovered by your app in a previous scan
|
||||
/// - iOS uses 128-bit uuids the remoteId, e.g. e006b3a7-ef7b-4980-a668-1f8005f84383
|
||||
/// - Android uses 48-bit mac addresses as the remoteId, e.g. 06:E5:28:3B:FD:E0
|
||||
static FBP.BluetoothDevice fromId(String remoteId) {
|
||||
if (Platform.isWindows) {
|
||||
return BluetoothDeviceWindows(remoteId: DeviceIdentifier(remoteId.toUpperCase()));
|
||||
}
|
||||
return FBP.BluetoothDevice.fromId(remoteId);
|
||||
}
|
||||
|
||||
/// platform name
|
||||
/// - this name is kept track of by the platform
|
||||
/// - this name usually persist between app restarts
|
||||
/// - iOS: after you connect, iOS uses the GAP name characteristic (0x2A00)
|
||||
/// if it exists. Otherwise iOS use the advertised name.
|
||||
/// - Android: always uses the advertised name
|
||||
String get platformName => FlutterBluePlusWindows._platformNames[remoteId] ?? "";
|
||||
|
||||
/// Advertised Named
|
||||
/// - this is the name advertised by the device during scanning
|
||||
/// - it is only available after you scan with FlutterBluePlus
|
||||
/// - it is cleared when the app restarts.
|
||||
/// - not all devices advertise a name
|
||||
String get advName => FlutterBluePlusWindows._advNames[remoteId] ?? "";
|
||||
|
||||
// stream return whether or not we are currently discovering services
|
||||
@Deprecated("planed for removal (Jan 2024). It can be easily implemented yourself") // deprecated on Aug 2023
|
||||
Stream<bool> get isDiscoveringServices => _isDiscoveringServices.stream;
|
||||
|
||||
/// Get services
|
||||
/// - returns empty if discoverServices() has not been called
|
||||
/// or if your device does not have any services (rare)
|
||||
List<BluetoothServiceWindows> get servicesList => FlutterBluePlusWindows._knownServices[remoteId] ?? [];
|
||||
|
||||
/// Stream of bluetooth services offered by the remote device
|
||||
/// - this stream is only updated when you call discoverServices()
|
||||
@Deprecated("planed for removal (Jan 2024). It can be easily implemented yourself") // deprecated on Aug 2023
|
||||
Stream<List<BluetoothService>> get servicesStream {
|
||||
if (FlutterBluePlusWindows._knownServices[remoteId] != null) {
|
||||
return _services.stream.newStreamWithInitialValue(
|
||||
FlutterBluePlusWindows._knownServices[remoteId]!,
|
||||
);
|
||||
} else {
|
||||
return _services.stream;
|
||||
}
|
||||
}
|
||||
|
||||
/// Register a subscription to be canceled when the device is disconnected.
|
||||
/// This function simplifies cleanup, so you can prevent creating duplicate stream subscriptions.
|
||||
/// - this is an optional convenience function
|
||||
/// - prevents accidentally creating duplicate subscriptions on each reconnection.
|
||||
/// - [next] if true, the the stream will be canceled only on the *next* disconnection.
|
||||
/// This is useful if you setup your subscriptions before you connect.
|
||||
/// - [delayed] Note: This option is only meant for `connectionState` subscriptions.
|
||||
/// When `true`, we cancel after a small delay. This ensures the `connectionState`
|
||||
/// listener receives the `disconnected` event.
|
||||
void cancelWhenDisconnected(StreamSubscription subscription, {bool next = false, bool delayed = false}) {
|
||||
if (isConnected == false && next == false) {
|
||||
subscription.cancel(); // cancel immediately if already disconnected.
|
||||
} else if (delayed) {
|
||||
FlutterBluePlusWindows._delayedSubscriptions[remoteId] ??= [];
|
||||
FlutterBluePlusWindows._delayedSubscriptions[remoteId]!.add(subscription);
|
||||
} else {
|
||||
FlutterBluePlusWindows._deviceSubscriptions[remoteId] ??= [];
|
||||
FlutterBluePlusWindows._deviceSubscriptions[remoteId]!.add(subscription);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if this device is currently connected to your app
|
||||
bool get isConnected {
|
||||
return FlutterBluePlusWindows.connectedDevices.contains(this);
|
||||
}
|
||||
|
||||
/// Returns true if this device is currently disconnected from your app
|
||||
bool get isDisconnected => isConnected == false;
|
||||
|
||||
Future<void> connect({
|
||||
Duration? timeout = const Duration(seconds: 35), // TODO: implementation missing
|
||||
bool autoConnect = false, // TODO: implementation missing
|
||||
int? mtu = 512, // TODO: implementation missing
|
||||
}) async {
|
||||
try {
|
||||
await WinBle.connect(_address);
|
||||
FlutterBluePlusWindows._deviceSet.add(this);
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> disconnect({
|
||||
int androidDelay = 2000, // TODO: implementation missing
|
||||
int timeout = 35, // TODO: implementation missing
|
||||
bool queue = true, // TODO: implementation missing
|
||||
}) async {
|
||||
try {
|
||||
await WinBle.disconnect(_address);
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
} finally {
|
||||
FlutterBluePlusWindows._deviceSet.remove(this);
|
||||
|
||||
FlutterBluePlusWindows._deviceSubscriptions[remoteId]?.forEach((s) => s.cancel());
|
||||
FlutterBluePlusWindows._deviceSubscriptions.remove(remoteId);
|
||||
// use delayed to update the stream before we cancel it
|
||||
Future.delayed(Duration.zero).then((_) {
|
||||
FlutterBluePlusWindows._delayedSubscriptions[remoteId]?.forEach((s) => s.cancel());
|
||||
FlutterBluePlusWindows._delayedSubscriptions.remove(remoteId);
|
||||
});
|
||||
|
||||
FlutterBluePlusWindows._lastChrs[remoteId]?.clear();
|
||||
FlutterBluePlusWindows._isNotifying[remoteId]?.clear();
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<BluetoothService>> discoverServices({
|
||||
bool subscribeToServicesChanged = true, // TODO: implementation missing
|
||||
int timeout = 15, // TODO: implementation missing
|
||||
}) async {
|
||||
List<BluetoothServiceWindows> result = List.from(FlutterBluePlusWindows._knownServices[remoteId] ?? []);
|
||||
|
||||
try {
|
||||
_isDiscoveringServices.add(true);
|
||||
|
||||
final response = await WinBle.discoverServices(_address);
|
||||
FlutterBluePlusWindows._characteristicCache[remoteId] ??= <String, List<BluetoothCharacteristic>>{};
|
||||
|
||||
for (final serviceId in response) {
|
||||
final characteristic = await WinBle.discoverCharacteristics(
|
||||
address: _address,
|
||||
serviceId: serviceId,
|
||||
);
|
||||
FlutterBluePlusWindows._characteristicCache[remoteId] ??= {};
|
||||
FlutterBluePlusWindows._characteristicCache[remoteId]?[serviceId] ??= [
|
||||
...characteristic.map(
|
||||
(e) => BluetoothCharacteristicWindows(
|
||||
remoteId: remoteId,
|
||||
serviceUuid: Guid(serviceId),
|
||||
characteristicUuid: Guid(e.uuid),
|
||||
descriptors: [],
|
||||
// TODO: implementation missing
|
||||
propertiesWinBle: e.properties,
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
result = [
|
||||
...response.map(
|
||||
(p) => BluetoothServiceWindows(
|
||||
remoteId: remoteId,
|
||||
serviceUuid: Guid(p),
|
||||
// TODO: implementation missing
|
||||
isPrimary: true,
|
||||
// TODO: implementation missing
|
||||
characteristics: FlutterBluePlusWindows._characteristicCache[remoteId]![p]!,
|
||||
// TODO: implementation missing
|
||||
includedServices: [],
|
||||
),
|
||||
)
|
||||
];
|
||||
|
||||
FlutterBluePlusWindows._knownServices[remoteId] = result;
|
||||
|
||||
_services.add(result);
|
||||
} finally {
|
||||
_isDiscoveringServices.add(false);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
DisconnectReason? get disconnectReason {
|
||||
// TODO: nothing to do
|
||||
return null;
|
||||
}
|
||||
|
||||
Stream<BluetoothConnectionState> get connectionState async* {
|
||||
await FlutterBluePlusWindows._initialize();
|
||||
|
||||
final map = FlutterBluePlusWindows._connectionStream.latestValue;
|
||||
|
||||
if (map[_address] != null) {
|
||||
yield map[_address]!.isConnected;
|
||||
}
|
||||
|
||||
yield* WinBle.connectionStreamOf(_address).map((e) => e.isConnected);
|
||||
}
|
||||
|
||||
Stream<int> get mtu async* {
|
||||
bool isEmitted = false;
|
||||
int retryCount = 0;
|
||||
while (!isEmitted) {
|
||||
if (retryCount > 3) throw "Device not found!";
|
||||
retryCount++;
|
||||
try {
|
||||
yield await WinBle.getMaxMtuSize(_address);
|
||||
isEmitted = true;
|
||||
} catch (e) {
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
log(e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<int> readRssi({int timeout = 15}) async {
|
||||
return FlutterBluePlusWindows._rssiMap[remoteId] ?? -100;
|
||||
}
|
||||
|
||||
Future<int> requestMtu(
|
||||
int desiredMtu, {
|
||||
double predelay = 0.35,
|
||||
int timeout = 15,
|
||||
}) async {
|
||||
// https://github.com/rohitsangwan01/win_ble/issues/8
|
||||
return await WinBle.getMaxMtuSize(_address);
|
||||
}
|
||||
|
||||
Future<void> requestConnectionPriority({
|
||||
required ConnectionPriority connectionPriorityRequest,
|
||||
}) async {
|
||||
// TODO: nothing to do
|
||||
return;
|
||||
}
|
||||
|
||||
/// Set the preferred connection (Android Only)
|
||||
/// - [txPhy] bitwise OR of all allowed phys for Tx, e.g. (Phy.le2m.mask | Phy.leCoded.mask)
|
||||
/// - [txPhy] bitwise OR of all allowed phys for Rx, e.g. (Phy.le2m.mask | Phy.leCoded.mask)
|
||||
/// - [option] preferred coding to use when transmitting on Phy.leCoded
|
||||
/// Please note that this is just a recommendation given to the system.
|
||||
Future<void> setPreferredPhy({
|
||||
required int txPhy,
|
||||
required int rxPhy,
|
||||
required PhyCoding option,
|
||||
}) async {
|
||||
// TODO: implementation missing
|
||||
}
|
||||
|
||||
Future<void> createBond({
|
||||
Uint8List? pin,
|
||||
int timeout = 90, // TODO: implementation missing
|
||||
}) async {
|
||||
try {
|
||||
await WinBle.pair(_address);
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> removeBond({
|
||||
int timeout = 30, // TODO: implementation missing
|
||||
}) async {
|
||||
try {
|
||||
await WinBle.unPair(_address);
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> clearGattCache() async {
|
||||
// TODO: implementation missing
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is BluetoothDeviceWindows && runtimeType == other.runtimeType && remoteId == other.remoteId);
|
||||
|
||||
@override
|
||||
int get hashCode => remoteId.hashCode;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'BluetoothDevice{'
|
||||
'remoteId: $remoteId, '
|
||||
'platformName: $platformName, '
|
||||
'services: ${FlutterBluePlusWindows._knownServices[remoteId]}'
|
||||
'}';
|
||||
}
|
||||
|
||||
@Deprecated('Use createBond() instead')
|
||||
Future<void> pair() async => await createBond();
|
||||
|
||||
@Deprecated('Use remoteId instead')
|
||||
DeviceIdentifier get id => remoteId;
|
||||
|
||||
@Deprecated('Use localName instead')
|
||||
String get name => localName;
|
||||
|
||||
@Deprecated('Use connectionState instead')
|
||||
Stream<BluetoothConnectionState> get state => connectionState;
|
||||
|
||||
@Deprecated('Use servicesStream instead')
|
||||
Stream<List<BluetoothService>> get services => servicesStream;
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
part of 'windows.dart';
|
||||
|
||||
class BluetoothServiceWindows extends BluetoothService {
|
||||
final DeviceIdentifier remoteId;
|
||||
final Guid serviceUuid;
|
||||
final bool isPrimary;
|
||||
final List<BluetoothCharacteristic> characteristics;
|
||||
final List<BluetoothService> includedServices;
|
||||
|
||||
BluetoothServiceWindows({
|
||||
required this.remoteId,
|
||||
required this.serviceUuid,
|
||||
required this.isPrimary,
|
||||
required this.characteristics,
|
||||
required this.includedServices,
|
||||
}) : super.fromProto(
|
||||
BmBluetoothService(
|
||||
remoteId: DeviceIdentifier(remoteId.str),
|
||||
serviceUuid: serviceUuid,
|
||||
characteristics: [for (final c in characteristics) c.toProto()],
|
||||
primaryServiceUuid: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1,353 +0,0 @@
|
||||
part of 'windows.dart';
|
||||
|
||||
class FlutterBluePlusWindows {
|
||||
static bool _initialized = false;
|
||||
|
||||
static BluetoothAdapterState _state = BluetoothAdapterState.unknown;
|
||||
|
||||
// stream used for the isScanning public api
|
||||
static final _isScanning = _StreamController(initialValue: false);
|
||||
|
||||
// we always keep track of these device variables
|
||||
static final _platformNames = <DeviceIdentifier, String>{};
|
||||
static final _advNames = <DeviceIdentifier, String>{};
|
||||
static final _rssiMap = <DeviceIdentifier, int?>{};
|
||||
static final _knownServices = <DeviceIdentifier, List<BluetoothServiceWindows>>{};
|
||||
static final Map<DeviceIdentifier, Map<String, List<int>>> _lastChrs = {};
|
||||
static final Map<DeviceIdentifier, Map<String, bool>> _isNotifying = {};
|
||||
static final Map<DeviceIdentifier, Map<String, List<BluetoothCharacteristic>>> _characteristicCache = {};
|
||||
static final Map<DeviceIdentifier, List<StreamSubscription>> _deviceSubscriptions = {};
|
||||
static final Map<DeviceIdentifier, List<StreamSubscription>> _delayedSubscriptions = {};
|
||||
static final List<StreamSubscription> _scanSubscriptions = [];
|
||||
|
||||
// stream used for the scanResults public api
|
||||
static final _scanResultsList = _StreamController(initialValue: <ScanResult>[]);
|
||||
|
||||
// the subscription to the scan results stream
|
||||
static StreamSubscription<BleDevice?>? _scanSubscription;
|
||||
|
||||
// timeout for scanning that can be cancelled by stopScan
|
||||
static Timer? _scanTimeout;
|
||||
|
||||
static List<BluetoothDeviceWindows> get _devices => [..._deviceSet];
|
||||
|
||||
static final _deviceSet = <BluetoothDeviceWindows>{};
|
||||
static final _removedDeviceTracer = <BluetoothDeviceWindows, StreamSubscription>{};
|
||||
|
||||
// static final _unhandledDeviceSet = <BluetoothDeviceWindows>{};
|
||||
|
||||
/// Flutter blue plus windows
|
||||
static final _charReadWriteStreamController = StreamController<(String, List<int>)>();
|
||||
static final _charReadStreamController = StreamController<(String, List<int>)>();
|
||||
|
||||
static final _charReadWriteStream = _charReadWriteStreamController.stream.asBroadcastStream();
|
||||
static final _charReadStream = _charReadStreamController.stream.asBroadcastStream();
|
||||
|
||||
/// Flutter blue plus windows
|
||||
static final _connectionStream = _StreamController(initialValue: <String, bool>{});
|
||||
|
||||
static Future<void> _initialize() async {
|
||||
if (_initialized) return;
|
||||
await WinBle.initialize(
|
||||
serverPath: await WinServer.path(),
|
||||
enableLog: false,
|
||||
);
|
||||
|
||||
WinBle.connectionStream.listen(
|
||||
(event) {
|
||||
log('$event - event');
|
||||
if (event['device'] == null) return;
|
||||
if (event['connected'] == null) return;
|
||||
|
||||
final map = _connectionStream.latestValue;
|
||||
map[event['device']] = event['connected'];
|
||||
|
||||
log('$map - map');
|
||||
_connectionStream.add(map);
|
||||
|
||||
if (!event['connected']) {
|
||||
final removingDevices = [
|
||||
..._deviceSet.where(
|
||||
(device) => device._address == event['device'],
|
||||
),
|
||||
];
|
||||
for (final device in removingDevices) {
|
||||
_deviceSet.remove(device);
|
||||
if (!_removedDeviceTracer.keys.contains(device)) {
|
||||
_removedDeviceTracer[device] = Stream.periodic(const Duration(seconds: 10), (_) => device).listen(
|
||||
(event) {
|
||||
if(event.isConnected) {
|
||||
_removedDeviceTracer[device]?.cancel();
|
||||
_removedDeviceTracer.remove(device);
|
||||
return;
|
||||
}
|
||||
event.connect();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
_deviceSubscriptions[device.remoteId]?.forEach((s) => s.cancel());
|
||||
_deviceSubscriptions.remove(device.remoteId);
|
||||
// use delayed to update the stream before we cancel it
|
||||
Future.delayed(Duration.zero).then((_) {
|
||||
_delayedSubscriptions[device.remoteId]?.forEach((s) => s.cancel());
|
||||
_delayedSubscriptions.remove(device.remoteId);
|
||||
});
|
||||
|
||||
_lastChrs[device.remoteId]?.clear();
|
||||
_isNotifying[device.remoteId]?.clear();
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
static Future<bool> get isSupported async {
|
||||
return true;
|
||||
}
|
||||
|
||||
static Future<String> get adapterName async {
|
||||
return 'Windows';
|
||||
}
|
||||
|
||||
static Stream<bool> get isScanning => _isScanning.stream;
|
||||
|
||||
static bool get isScanningNow => _isScanning.latestValue;
|
||||
|
||||
static Future<void> turnOn({int timeout = 10}) async {
|
||||
await _initialize();
|
||||
await WinBle.updateBluetoothState(true);
|
||||
}
|
||||
|
||||
// TODO: compare with original lib
|
||||
static Stream<List<ScanResult>> get scanResults => _scanResultsList.stream;
|
||||
|
||||
static Stream<BluetoothAdapterState> get adapterState async* {
|
||||
await _initialize();
|
||||
yield _state;
|
||||
yield* WinBle.bleState.asBroadcastStream().map(
|
||||
(s) {
|
||||
_state = s.toAdapterState();
|
||||
return _state;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Start a scan, and return a stream of results
|
||||
/// Note: scan filters use an "or" behavior. i.e. if you set `withServices` & `withNames` we
|
||||
/// return all the advertisments that match any of the specified services *or* any of the specified names.
|
||||
/// - [withServices] filter by advertised services
|
||||
/// - [withRemoteIds] filter for known remoteIds (iOS: 128-bit guid, android: 48-bit mac address)
|
||||
/// - [withNames] filter by advertised names (exact match)
|
||||
/// - [withKeywords] filter by advertised names (matches any substring)
|
||||
/// - [withMsd] filter by manfacture specific data
|
||||
/// - [withServiceData] filter by service data
|
||||
/// - [timeout] calls stopScan after a specified duration
|
||||
/// - [removeIfGone] if true, remove devices after they've stopped advertising for X duration
|
||||
/// - [continuousUpdates] If `true`, we continually update 'lastSeen' & 'rssi' by processing
|
||||
/// duplicate advertisements. This takes more power. You typically should not use this option.
|
||||
/// - [continuousDivisor] Useful to help performance. If divisor is 3, then two-thirds of advertisements are
|
||||
/// ignored, and one-third are processed. This reduces main-thread usage caused by the platform channel.
|
||||
/// The scan counting is per-device so you always get the 1st advertisement from each device.
|
||||
/// If divisor is 1, all advertisements are returned. This argument only matters for `continuousUpdates` mode.
|
||||
/// - [oneByOne] if `true`, we will stream every advertistment one by one, possibly including duplicates.
|
||||
/// If `false`, we deduplicate the advertisements, and return a list of devices.
|
||||
/// - [androidLegacy] Android only. If `true`, scan on 1M phy only.
|
||||
/// If `false`, scan on all supported phys. How the radio cycles through all the supported phys is purely
|
||||
/// dependent on the your Bluetooth stack implementation.
|
||||
/// - [androidScanMode] choose the android scan mode to use when scanning
|
||||
/// - [androidUsesFineLocation] request `ACCESS_FINE_LOCATION` permission at runtime
|
||||
static Future<void> startScan({
|
||||
List<Guid> withServices = const [],
|
||||
List<String> withRemoteIds = const [],
|
||||
List<String> withNames = const [],
|
||||
//TODO: implementation missing
|
||||
List<String> withKeywords = const [],
|
||||
//TODO: implementation missing
|
||||
List<MsdFilter> withMsd = const [],
|
||||
List<ServiceDataFilter> withServiceData = const [],
|
||||
Duration? timeout,
|
||||
Duration? removeIfGone,
|
||||
bool continuousUpdates = false,
|
||||
int continuousDivisor = 1,
|
||||
bool oneByOne = false,
|
||||
bool androidLegacy = false,
|
||||
AndroidScanMode androidScanMode = AndroidScanMode.lowLatency,
|
||||
bool androidUsesFineLocation = false,
|
||||
}) async {
|
||||
await _initialize();
|
||||
|
||||
// stop existing scan
|
||||
if (_isScanning.latestValue == true) {
|
||||
await stopScan();
|
||||
}
|
||||
|
||||
// push to stream
|
||||
_isScanning.add(true);
|
||||
|
||||
// Start timer *after* stream is being listened to, to make sure the
|
||||
// timeout does not fire before _buffer is set
|
||||
if (timeout != null) {
|
||||
_scanTimeout = Timer(timeout, stopScan);
|
||||
}
|
||||
|
||||
/// remove connection by OS.
|
||||
/// The reason why we add this logic is
|
||||
/// to avoid uncontrollable devices and to make consistency.
|
||||
|
||||
/// add WinBle scanning
|
||||
WinBle.startScanning();
|
||||
|
||||
// check every 250ms for gone devices?
|
||||
late Stream<BleDevice?> outputStream;
|
||||
if (removeIfGone != null) {
|
||||
outputStream = _mergeStreams([WinBle.scanStream, Stream.periodic(Duration(milliseconds: 250))]);
|
||||
} else {
|
||||
outputStream = WinBle.scanStream;
|
||||
}
|
||||
|
||||
final output = <ScanResult>[];
|
||||
|
||||
// listen & push to `scanResults` stream
|
||||
_scanSubscription = outputStream.listen(
|
||||
(BleDevice? winBleDevice) {
|
||||
// print(winBleDevice?.serviceUuids);
|
||||
if (winBleDevice == null) {
|
||||
// if null, this is just a periodic update for removing old results
|
||||
output.removeWhere((elm) => DateTime.now().difference(elm.timeStamp) > removeIfGone!);
|
||||
|
||||
// push to stream
|
||||
_scanResultsList.add(List.from(output));
|
||||
} else {
|
||||
final remoteId = DeviceIdentifier(winBleDevice.address.toUpperCase());
|
||||
final scanResult = output.where((sr) => sr.device.remoteId == remoteId).firstOrNull;
|
||||
final deviceName = winBleDevice.name.isNotEmpty ? winBleDevice.name : scanResult?.device.platformName ?? '';
|
||||
final serviceUuids = winBleDevice.serviceUuids.isNotEmpty
|
||||
? [...winBleDevice.serviceUuids.map((e) => Guid((e as String).replaceAll(RegExp(r'[{}]'), '')))]
|
||||
: scanResult?.advertisementData.serviceUuids ?? [];
|
||||
|
||||
final manufacturerData = winBleDevice.manufacturerData.isNotEmpty
|
||||
? {
|
||||
if (winBleDevice.manufacturerData.length >= 2)
|
||||
winBleDevice.manufacturerData[0] + (winBleDevice.manufacturerData[1] << 8):
|
||||
winBleDevice.manufacturerData.sublist(2),
|
||||
}
|
||||
: scanResult?.advertisementData.manufacturerData ?? {};
|
||||
|
||||
final rssi = int.tryParse(winBleDevice.rssi) ?? -100;
|
||||
|
||||
FlutterBluePlusWindows._platformNames[remoteId] = deviceName;
|
||||
FlutterBluePlusWindows._advNames[remoteId] = deviceName;
|
||||
FlutterBluePlusWindows._rssiMap[remoteId] = rssi;
|
||||
|
||||
final device = BluetoothDeviceWindows(remoteId: remoteId);
|
||||
|
||||
String hex(int value) => value.toRadixString(16).padLeft(2, '0');
|
||||
String hexToId(Iterable<int> values) => values.map((e) => hex(e)).join();
|
||||
|
||||
final sr = ScanResult(
|
||||
device: device,
|
||||
advertisementData: AdvertisementData(
|
||||
advName: deviceName,
|
||||
txPowerLevel: winBleDevice.adStructures?.where((e) => e.type == 10).singleOrNull?.data.firstOrNull,
|
||||
//TODO: Should verify
|
||||
connectable: !winBleDevice.advType.contains('Non'),
|
||||
manufacturerData: manufacturerData,
|
||||
serviceData: {
|
||||
for (final advStructures in winBleDevice.adStructures ?? <AdStructure>[])
|
||||
if (advStructures.type == 0x16 && advStructures.data.length >= 2)
|
||||
Guid(hexToId(advStructures.data.sublist(0, 2).reversed)): advStructures.data.sublist(2),
|
||||
for (final advStructures in winBleDevice.adStructures ?? <AdStructure>[])
|
||||
if (advStructures.type == 0x20 && advStructures.data.length >= 4)
|
||||
Guid(hexToId(advStructures.data.sublist(0, 4).reversed)): advStructures.data.sublist(4),
|
||||
for (final advStructures in winBleDevice.adStructures ?? <AdStructure>[])
|
||||
if (advStructures.type == 0x21 && advStructures.data.length >= 16)
|
||||
Guid(hexToId(advStructures.data.sublist(0, 16).reversed)): advStructures.data.sublist(16),
|
||||
},
|
||||
serviceUuids: serviceUuids,
|
||||
appearance: null,
|
||||
),
|
||||
rssi: rssi,
|
||||
timeStamp: DateTime.now(),
|
||||
);
|
||||
|
||||
// filter with services
|
||||
final isFilteredWithServices =
|
||||
withServices.isNotEmpty && serviceUuids.where((service) => withServices.contains(service)).isEmpty;
|
||||
|
||||
// filter with remote ids
|
||||
final isFilteredWithRemoteIds = withRemoteIds.isNotEmpty && !withRemoteIds.contains(remoteId);
|
||||
|
||||
// filter with names
|
||||
final isFilteredWithNames = withNames.isNotEmpty && !withNames.contains(deviceName);
|
||||
|
||||
if (isFilteredWithServices || isFilteredWithRemoteIds || isFilteredWithNames) {
|
||||
_scanResultsList.add(List.from(output));
|
||||
return;
|
||||
}
|
||||
|
||||
// add result to output
|
||||
if (oneByOne) {
|
||||
output
|
||||
..clear()
|
||||
..add(sr);
|
||||
} else {
|
||||
output.addOrUpdate(sr);
|
||||
}
|
||||
|
||||
// push to stream
|
||||
_scanResultsList.add(List.from(output));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
static List<FBP.BluetoothDevice> get connectedDevices {
|
||||
return _devices;
|
||||
}
|
||||
|
||||
static Future<List<BluetoothDeviceWindows>> get bondedDevices async {
|
||||
return _devices;
|
||||
}
|
||||
|
||||
/// Stops a scan for Bluetooth Low Energy devices
|
||||
static Future<void> stopScan() async {
|
||||
await _initialize();
|
||||
WinBle.stopScanning();
|
||||
_scanSubscription?.cancel();
|
||||
_scanTimeout?.cancel();
|
||||
_isScanning.add(false);
|
||||
|
||||
for (var subscription in _scanSubscriptions) {
|
||||
subscription.cancel();
|
||||
}
|
||||
|
||||
_scanResultsList.latestValue = [];
|
||||
}
|
||||
|
||||
/// Register a subscription to be canceled when scanning is complete.
|
||||
/// This function simplifies cleanup, so you can prevent creating duplicate stream subscriptions.
|
||||
/// - this is an optional convenience function
|
||||
/// - prevents accidentally creating duplicate subscriptions before each scan
|
||||
static void cancelWhenScanComplete(StreamSubscription subscription) {
|
||||
_scanSubscriptions.add(subscription);
|
||||
}
|
||||
|
||||
/// Sets the internal FlutterBlue log level
|
||||
static Future<void> setLogLevel(LogLevel level, {color = true}) async {
|
||||
// Nothing to implement
|
||||
return;
|
||||
}
|
||||
|
||||
static Future<void> turnOff({int timeout = 10}) async {
|
||||
await _initialize();
|
||||
await WinBle.updateBluetoothState(false);
|
||||
}
|
||||
|
||||
// TODO: need to test
|
||||
static Future<bool> get isOn async {
|
||||
await _initialize();
|
||||
return await WinBle.bleState.asBroadcastStream().first == BleState.On;
|
||||
}
|
||||
}
|
||||
@@ -1,433 +0,0 @@
|
||||
part of 'windows.dart';
|
||||
|
||||
String _hexEncode(List<int> numbers) {
|
||||
return numbers
|
||||
.map((n) => (n & 0xFF).toRadixString(16).padLeft(2, '0'))
|
||||
.join();
|
||||
}
|
||||
|
||||
List<int> _hexDecode(String hex) {
|
||||
List<int> numbers = [];
|
||||
for (int i = 0; i < hex.length; i += 2) {
|
||||
String hexPart = hex.substring(i, i + 2);
|
||||
int num = int.parse(hexPart, radix: 16);
|
||||
numbers.add(num);
|
||||
}
|
||||
return numbers;
|
||||
}
|
||||
|
||||
int _compareAsciiLowerCase(String a, String b) {
|
||||
const int upperCaseA = 0x41;
|
||||
const int upperCaseZ = 0x5a;
|
||||
const int asciiCaseBit = 0x20;
|
||||
var defaultResult = 0;
|
||||
for (var i = 0; i < a.length; i++) {
|
||||
if (i >= b.length) return 1;
|
||||
var aChar = a.codeUnitAt(i);
|
||||
var bChar = b.codeUnitAt(i);
|
||||
if (aChar == bChar) continue;
|
||||
var aLowerCase = aChar;
|
||||
var bLowerCase = bChar;
|
||||
// Upper case if ASCII letters.
|
||||
if (upperCaseA <= bChar && bChar <= upperCaseZ) {
|
||||
bLowerCase += asciiCaseBit;
|
||||
}
|
||||
if (upperCaseA <= aChar && aChar <= upperCaseZ) {
|
||||
aLowerCase += asciiCaseBit;
|
||||
}
|
||||
if (aLowerCase != bLowerCase) return (aLowerCase - bLowerCase).sign;
|
||||
if (defaultResult == 0) defaultResult = aChar - bChar;
|
||||
}
|
||||
if (b.length > a.length) return -1;
|
||||
return defaultResult.sign;
|
||||
}
|
||||
|
||||
// This is a reimplementation of BehaviorSubject from RxDart library.
|
||||
// It is essentially a stream but:
|
||||
// 1. we cache the latestValue of the stream
|
||||
// 2. the "latestValue" is re-emitted whenever the stream is listened to
|
||||
class _StreamController<T> {
|
||||
T latestValue;
|
||||
|
||||
final StreamController<T> _controller = StreamController<T>.broadcast();
|
||||
|
||||
_StreamController({required T initialValue})
|
||||
: this.latestValue = initialValue;
|
||||
|
||||
Stream<T> get stream => _controller.stream;
|
||||
|
||||
T get value => latestValue;
|
||||
|
||||
void add(T newValue) {
|
||||
latestValue = newValue;
|
||||
_controller.add(newValue);
|
||||
}
|
||||
|
||||
void listen(Function(T) onData,
|
||||
{Function? onError, void Function()? onDone, bool? cancelOnError}) {
|
||||
onData(latestValue);
|
||||
_controller.stream.listen(onData,
|
||||
onError: onError, onDone: onDone, cancelOnError: cancelOnError);
|
||||
}
|
||||
|
||||
Future<void> close() {
|
||||
return _controller.close();
|
||||
}
|
||||
}
|
||||
|
||||
// imediately starts listening to a broadcast stream and
|
||||
// buffering it in a new single-subscription stream
|
||||
class _BufferStream<T> {
|
||||
final Stream<T> _inputStream;
|
||||
late final StreamSubscription? _subscription;
|
||||
late final StreamController<T> _controller;
|
||||
late bool hasReceivedValue = false;
|
||||
|
||||
_BufferStream.listen(this._inputStream) {
|
||||
_controller = StreamController<T>(
|
||||
onCancel: () {
|
||||
_subscription?.cancel();
|
||||
},
|
||||
onPause: () {
|
||||
_subscription?.pause();
|
||||
},
|
||||
onResume: () {
|
||||
_subscription?.resume();
|
||||
},
|
||||
onListen: () {}, // inputStream is already listened to
|
||||
);
|
||||
|
||||
// immediately start listening to the inputStream
|
||||
_subscription = _inputStream.listen(
|
||||
(data) {
|
||||
hasReceivedValue = true;
|
||||
_controller.add(data);
|
||||
},
|
||||
onError: (e) {
|
||||
_controller.addError(e);
|
||||
},
|
||||
onDone: () {
|
||||
_controller.close();
|
||||
},
|
||||
cancelOnError: false,
|
||||
);
|
||||
}
|
||||
|
||||
void close() {
|
||||
_subscription?.cancel();
|
||||
_controller.close();
|
||||
}
|
||||
|
||||
Stream<T> get stream async* {
|
||||
yield* _controller.stream;
|
||||
}
|
||||
}
|
||||
|
||||
// helper for 'doOnDone' method for streams.
|
||||
class _OnDoneTransformer<T> extends StreamTransformerBase<T, T> {
|
||||
final Function onDone;
|
||||
|
||||
_OnDoneTransformer({required this.onDone});
|
||||
|
||||
@override
|
||||
Stream<T> bind(Stream<T> stream) {
|
||||
if (stream.isBroadcast) {
|
||||
return _bindBroadcast(stream);
|
||||
}
|
||||
return _bindSingleSubscription(stream);
|
||||
}
|
||||
|
||||
Stream<T> _bindSingleSubscription(Stream<T> stream) {
|
||||
StreamController<T>? controller;
|
||||
StreamSubscription<T>? subscription;
|
||||
|
||||
controller = StreamController<T>(
|
||||
onListen: () {
|
||||
subscription = stream.listen(
|
||||
controller?.add,
|
||||
onError: controller?.addError,
|
||||
onDone: () {
|
||||
onDone();
|
||||
controller?.close();
|
||||
},
|
||||
);
|
||||
},
|
||||
onPause: ([Future<dynamic>? resumeSignal]) {
|
||||
subscription?.pause(resumeSignal);
|
||||
},
|
||||
onResume: () {
|
||||
subscription?.resume();
|
||||
},
|
||||
onCancel: () {
|
||||
return subscription?.cancel();
|
||||
},
|
||||
sync: true,
|
||||
);
|
||||
|
||||
return controller.stream;
|
||||
}
|
||||
|
||||
Stream<T> _bindBroadcast(Stream<T> stream) {
|
||||
StreamController<T>? controller;
|
||||
StreamSubscription<T>? subscription;
|
||||
|
||||
controller = StreamController<T>.broadcast(
|
||||
onListen: () {
|
||||
subscription = stream
|
||||
.listen(controller?.add, onError: controller?.addError, onDone: () {
|
||||
onDone();
|
||||
controller?.close();
|
||||
});
|
||||
},
|
||||
onCancel: () {
|
||||
subscription?.cancel();
|
||||
},
|
||||
sync: true,
|
||||
);
|
||||
|
||||
return controller.stream;
|
||||
}
|
||||
}
|
||||
|
||||
// helper for 'doOnCancel' method for streams.
|
||||
class _OnCancelTransformer<T> extends StreamTransformerBase<T, T> {
|
||||
final Function onCancel;
|
||||
|
||||
_OnCancelTransformer({required this.onCancel});
|
||||
|
||||
@override
|
||||
Stream<T> bind(Stream<T> stream) {
|
||||
if (stream.isBroadcast) {
|
||||
return _bindBroadcast(stream);
|
||||
}
|
||||
|
||||
return _bindSingleSubscription(stream);
|
||||
}
|
||||
|
||||
Stream<T> _bindSingleSubscription(Stream<T> stream) {
|
||||
StreamController<T>? controller;
|
||||
StreamSubscription<T>? subscription;
|
||||
|
||||
controller = StreamController<T>(
|
||||
onListen: () {
|
||||
subscription = stream.listen(
|
||||
controller?.add,
|
||||
onError: (Object error) {
|
||||
controller?.addError(error);
|
||||
controller?.close();
|
||||
},
|
||||
onDone: controller?.close,
|
||||
);
|
||||
},
|
||||
onPause: ([Future<dynamic>? resumeSignal]) {
|
||||
subscription?.pause(resumeSignal);
|
||||
},
|
||||
onResume: () {
|
||||
subscription?.resume();
|
||||
},
|
||||
onCancel: () {
|
||||
onCancel();
|
||||
return subscription?.cancel();
|
||||
},
|
||||
sync: true,
|
||||
);
|
||||
|
||||
return controller.stream;
|
||||
}
|
||||
|
||||
Stream<T> _bindBroadcast(Stream<T> stream) {
|
||||
StreamController<T>? controller;
|
||||
StreamSubscription<T>? subscription;
|
||||
|
||||
controller = StreamController<T>.broadcast(
|
||||
onListen: () {
|
||||
subscription = stream.listen(
|
||||
controller?.add,
|
||||
onError: (Object error) {
|
||||
controller?.addError(error);
|
||||
controller?.close();
|
||||
},
|
||||
onDone: controller?.close,
|
||||
);
|
||||
},
|
||||
onCancel: () {
|
||||
onCancel();
|
||||
subscription?.cancel();
|
||||
},
|
||||
sync: true,
|
||||
);
|
||||
|
||||
return controller.stream;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper for 'newStreamWithInitialValue' method for streams.
|
||||
class _NewStreamWithInitialValueTransformer<T>
|
||||
extends StreamTransformerBase<T, T> {
|
||||
final T initialValue;
|
||||
|
||||
_NewStreamWithInitialValueTransformer(this.initialValue);
|
||||
|
||||
@override
|
||||
Stream<T> bind(Stream<T> stream) {
|
||||
return _bindSingleSubscription(stream);
|
||||
}
|
||||
|
||||
Stream<T> _bindSingleSubscription(Stream<T> stream) {
|
||||
StreamController<T>? controller;
|
||||
StreamSubscription<T>? subscription;
|
||||
|
||||
controller = StreamController<T>(
|
||||
onListen: () {
|
||||
// Emit the initial value
|
||||
controller?.add(initialValue);
|
||||
|
||||
subscription = stream.listen(
|
||||
controller?.add,
|
||||
onError: (Object error) {
|
||||
controller?.addError(error);
|
||||
controller?.close();
|
||||
},
|
||||
onDone: controller?.close,
|
||||
);
|
||||
},
|
||||
onPause: ([Future<dynamic>? resumeSignal]) {
|
||||
subscription?.pause(resumeSignal);
|
||||
},
|
||||
onResume: () {
|
||||
subscription?.resume();
|
||||
},
|
||||
onCancel: () {
|
||||
return subscription?.cancel();
|
||||
},
|
||||
sync: true,
|
||||
);
|
||||
|
||||
return controller.stream;
|
||||
}
|
||||
}
|
||||
|
||||
extension _StreamDoOnDone<T> on Stream<T> {
|
||||
// ignore: unused_element
|
||||
Stream<T> doOnDone(void Function() onDone) {
|
||||
return transform(_OnDoneTransformer(onDone: onDone));
|
||||
}
|
||||
}
|
||||
|
||||
extension _StreamDoOnCancel<T> on Stream<T> {
|
||||
// ignore: unused_element
|
||||
Stream<T> doOnCancel(void Function() onCancel) {
|
||||
return transform(_OnCancelTransformer(onCancel: onCancel));
|
||||
}
|
||||
}
|
||||
|
||||
extension _StreamNewStreamWithInitialValue<T> on Stream<T> {
|
||||
Stream<T> newStreamWithInitialValue(T initialValue) {
|
||||
return transform(_NewStreamWithInitialValueTransformer(initialValue));
|
||||
}
|
||||
}
|
||||
|
||||
// ignore: unused_element
|
||||
Stream<T> _mergeStreams<T>(List<Stream<T>> streams) {
|
||||
StreamController<T> controller = StreamController<T>();
|
||||
List<StreamSubscription<T>> subscriptions = [];
|
||||
|
||||
void handleData(T data) {
|
||||
if (!controller.isClosed) {
|
||||
controller.add(data);
|
||||
}
|
||||
}
|
||||
|
||||
void handleError(Object error, StackTrace stackTrace) {
|
||||
if (!controller.isClosed) {
|
||||
controller.addError(error, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
void handleDone() {
|
||||
if (subscriptions.every((s) => s.isPaused)) {
|
||||
controller.close();
|
||||
}
|
||||
}
|
||||
|
||||
void subscribeToStream(Stream<T> stream) {
|
||||
final s =
|
||||
stream.listen(handleData, onError: handleError, onDone: handleDone);
|
||||
subscriptions.add(s);
|
||||
}
|
||||
|
||||
streams.forEach(subscribeToStream);
|
||||
|
||||
controller.onCancel = () async {
|
||||
await Future.wait(subscriptions.map((s) => s.cancel()));
|
||||
};
|
||||
|
||||
return controller.stream;
|
||||
}
|
||||
|
||||
// dart is single threaded, but still has task switching.
|
||||
// this mutex lets a single task through at a time.
|
||||
class _Mutex {
|
||||
final StreamController _controller = StreamController.broadcast();
|
||||
int current = 0;
|
||||
int issued = 0;
|
||||
|
||||
Future<void> take() async {
|
||||
int mine = issued;
|
||||
issued++;
|
||||
// tasks are executed in the same order they call take()
|
||||
while (mine != current) {
|
||||
await _controller.stream.first; // wait
|
||||
}
|
||||
}
|
||||
|
||||
void give() {
|
||||
current++;
|
||||
_controller.add(null); // release waiting tasks
|
||||
}
|
||||
}
|
||||
|
||||
// Create mutexes in a parrallel-safe way,
|
||||
class _MutexFactory {
|
||||
static final _Mutex _global = _Mutex();
|
||||
static final Map<String, _Mutex> _all = {};
|
||||
|
||||
static Future<_Mutex> getMutexForKey(String key) async {
|
||||
_Mutex? value;
|
||||
await _global.take();
|
||||
{
|
||||
_all[key] ??= _Mutex();
|
||||
value = _all[key];
|
||||
}
|
||||
_global.give();
|
||||
return value!;
|
||||
}
|
||||
}
|
||||
|
||||
String _black(String s) {
|
||||
// Use ANSI escape codes
|
||||
return '\x1B[1;30m$s\x1B[0m';
|
||||
}
|
||||
|
||||
// ignore: unused_element
|
||||
String _green(String s) {
|
||||
// Use ANSI escape codes
|
||||
return '\x1B[1;32m$s\x1B[0m';
|
||||
}
|
||||
|
||||
String _magenta(String s) {
|
||||
// Use ANSI escape codes
|
||||
return '\x1B[1;35m$s\x1B[0m';
|
||||
}
|
||||
|
||||
String _brown(String s) {
|
||||
// Use ANSI escape codes
|
||||
return '\x1B[1;33m$s\x1B[0m';
|
||||
}
|
||||
|
||||
extension Boolean2ConnectionState on bool {
|
||||
BluetoothConnectionState get isConnected {
|
||||
if (this) return BluetoothConnectionState.connected;
|
||||
return BluetoothConnectionState.disconnected;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart' as FBP;
|
||||
import 'package:flutter_blue_plus_platform_interface/flutter_blue_plus_platform_interface.dart';
|
||||
import 'package:flutter_blue_plus_windows/flutter_blue_plus_windows.dart';
|
||||
|
||||
part 'bluetooth_characteristic_windows.dart';
|
||||
part 'bluetooth_device_windows.dart';
|
||||
part 'bluetooth_service_windows.dart';
|
||||
part 'flutter_blue_plus_windows.dart';
|
||||
part 'util.dart';
|
||||
@@ -1,141 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart' as FBP;
|
||||
import 'package:flutter_blue_plus_windows/flutter_blue_plus_windows.dart';
|
||||
|
||||
class FlutterBluePlus {
|
||||
static Future<void> startScan({
|
||||
List<Guid> withServices = const [],
|
||||
List<String> withRemoteIds = const [],
|
||||
List<String> withNames = const [],
|
||||
List<String> withKeywords = const [],
|
||||
List<MsdFilter> withMsd = const [],
|
||||
List<ServiceDataFilter> withServiceData = const [],
|
||||
Duration? timeout,
|
||||
Duration? removeIfGone,
|
||||
bool continuousUpdates = false,
|
||||
int continuousDivisor = 1,
|
||||
bool oneByOne = false,
|
||||
bool androidLegacy = false,
|
||||
AndroidScanMode androidScanMode = AndroidScanMode.lowLatency,
|
||||
bool androidUsesFineLocation = false,
|
||||
List<Guid> webOptionalServices = const [],
|
||||
}) async {
|
||||
if (!kIsWeb && Platform.isWindows) {
|
||||
return await FlutterBluePlusWindows.startScan(
|
||||
withServices: withServices,
|
||||
withRemoteIds: withRemoteIds,
|
||||
withNames: withNames,
|
||||
withKeywords: withKeywords,
|
||||
withMsd: withMsd,
|
||||
withServiceData: withServiceData,
|
||||
timeout: timeout,
|
||||
removeIfGone: removeIfGone,
|
||||
continuousUpdates: continuousUpdates,
|
||||
continuousDivisor: continuousDivisor,
|
||||
oneByOne: oneByOne,
|
||||
androidLegacy: androidLegacy,
|
||||
androidScanMode: androidScanMode,
|
||||
androidUsesFineLocation: androidUsesFineLocation,
|
||||
);
|
||||
}
|
||||
|
||||
return await FBP.FlutterBluePlus.startScan(
|
||||
withServices: withServices,
|
||||
withRemoteIds: withRemoteIds,
|
||||
withNames: withNames,
|
||||
withKeywords: withKeywords,
|
||||
withMsd: withMsd,
|
||||
withServiceData: withServiceData,
|
||||
timeout: timeout,
|
||||
removeIfGone: removeIfGone,
|
||||
continuousUpdates: continuousUpdates,
|
||||
continuousDivisor: continuousDivisor,
|
||||
oneByOne: oneByOne,
|
||||
androidLegacy: androidLegacy,
|
||||
androidScanMode: androidScanMode,
|
||||
androidUsesFineLocation: androidUsesFineLocation,
|
||||
webOptionalServices: webOptionalServices,
|
||||
);
|
||||
}
|
||||
|
||||
static Stream<BluetoothAdapterState> get adapterState {
|
||||
if (!kIsWeb && Platform.isWindows) return FlutterBluePlusWindows.adapterState;
|
||||
return FBP.FlutterBluePlus.adapterState;
|
||||
}
|
||||
|
||||
static Stream<List<ScanResult>> get scanResults {
|
||||
if (!kIsWeb && Platform.isWindows) return FlutterBluePlusWindows.scanResults;
|
||||
return FBP.FlutterBluePlus.scanResults;
|
||||
}
|
||||
|
||||
static bool get isScanningNow {
|
||||
if (!kIsWeb && Platform.isWindows) return FlutterBluePlusWindows.isScanningNow;
|
||||
return FBP.FlutterBluePlus.isScanningNow;
|
||||
}
|
||||
|
||||
static Stream<bool> get isScanning {
|
||||
if (!kIsWeb && Platform.isWindows) return FlutterBluePlusWindows.isScanning;
|
||||
return FBP.FlutterBluePlus.isScanning;
|
||||
}
|
||||
|
||||
static Future<void> stopScan() async {
|
||||
if (!kIsWeb && Platform.isWindows) return await FlutterBluePlusWindows.stopScan();
|
||||
return await FBP.FlutterBluePlus.stopScan();
|
||||
}
|
||||
|
||||
static Future<void> setLogLevel(LogLevel level, {color = true}) async {
|
||||
if (!kIsWeb && Platform.isWindows) return FlutterBluePlusWindows.setLogLevel(level, color: color);
|
||||
return FBP.FlutterBluePlus.setLogLevel(level, color: color);
|
||||
}
|
||||
|
||||
/// TODO: need to verify
|
||||
static LogLevel get logLevel => FBP.FlutterBluePlus.logLevel;
|
||||
|
||||
static Future<void> setOptions({bool restoreState = false, bool showPowerAlert = true}) async {
|
||||
if (!kIsWeb && Platform.isWindows) return;
|
||||
FBP.FlutterBluePlus.setOptions(restoreState: restoreState, showPowerAlert: showPowerAlert);
|
||||
}
|
||||
|
||||
static Future<bool> get isSupported async {
|
||||
if (!kIsWeb && Platform.isWindows) return await FlutterBluePlusWindows.isSupported;
|
||||
return await FBP.FlutterBluePlus.isSupported;
|
||||
}
|
||||
|
||||
static Future<String> get adapterName async {
|
||||
if (!kIsWeb && Platform.isWindows) return await FlutterBluePlusWindows.adapterName;
|
||||
return await FBP.FlutterBluePlus.adapterName;
|
||||
}
|
||||
|
||||
static Future<void> turnOn({int timeout = 60}) async {
|
||||
if (!kIsWeb && Platform.isWindows) return await FlutterBluePlusWindows.turnOn(timeout: timeout);
|
||||
return await FBP.FlutterBluePlus.turnOn(timeout: timeout);
|
||||
}
|
||||
|
||||
static List<FBP.BluetoothDevice> get connectedDevices {
|
||||
if (!kIsWeb && Platform.isWindows) return FlutterBluePlusWindows.connectedDevices;
|
||||
return FBP.FlutterBluePlus.connectedDevices;
|
||||
}
|
||||
|
||||
static Future<List<FBP.BluetoothDevice>> systemDevices(List<Guid> withServices) async {
|
||||
//TODO: connected devices => system devices
|
||||
if (!kIsWeb && Platform.isWindows) return FlutterBluePlusWindows.connectedDevices;
|
||||
return await FBP.FlutterBluePlus.systemDevices(withServices);
|
||||
}
|
||||
|
||||
static Future<PhySupport> getPhySupport() {
|
||||
return FBP.FlutterBluePlus.getPhySupport();
|
||||
}
|
||||
|
||||
static Future<List<FBP.BluetoothDevice>> get bondedDevices async {
|
||||
if (!kIsWeb && Platform.isWindows) return FlutterBluePlusWindows.connectedDevices;
|
||||
return await FBP.FlutterBluePlus.bondedDevices;
|
||||
}
|
||||
|
||||
static void cancelWhenScanComplete(StreamSubscription subscription) {
|
||||
if (!kIsWeb && Platform.isWindows) return FlutterBluePlusWindows.cancelWhenScanComplete(subscription);
|
||||
return FBP.FlutterBluePlus.cancelWhenScanComplete(subscription);
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export 'flutter_blue_plus_wrapper.dart';
|
||||
@@ -1,13 +0,0 @@
|
||||
name: flutter_blue_plus_windows
|
||||
description: Flutter blue plus for Windows
|
||||
version: 1.26.1
|
||||
repository: https://github.com/chan150/flutter_blue_plus_windows
|
||||
#publish_to: none
|
||||
|
||||
environment:
|
||||
sdk: ">=3.0.0 <4.0.0"
|
||||
|
||||
dependencies:
|
||||
flutter_blue_plus: ">=1.32.4"
|
||||
win_ble: ">=1.1.1"
|
||||
stream_with_value: ">=0.5.0"
|
||||
@@ -1,32 +0,0 @@
|
||||
# flutter pub run flutter_launcher_icons
|
||||
flutter_launcher_icons:
|
||||
image_path: "icon.png"
|
||||
|
||||
android: "ic_launcher"
|
||||
# image_path_android: "assets/icon/icon.png"
|
||||
min_sdk_android: 24 # android min sdk min:16, default 21
|
||||
# adaptive_icon_background: "assets/icon/background.png"
|
||||
# adaptive_icon_foreground: "assets/icon/foreground.png"
|
||||
# adaptive_icon_monochrome: "assets/icon/monochrome.png"
|
||||
|
||||
ios: false
|
||||
# image_path_ios: "assets/icon/icon.png"
|
||||
remove_alpha_channel_ios: true
|
||||
# image_path_ios_dark_transparent: "assets/icon/icon_dark.png"
|
||||
# image_path_ios_tinted_grayscale: "assets/icon/icon_tinted.png"
|
||||
# desaturate_tinted_to_grayscale_ios: true
|
||||
|
||||
web:
|
||||
generate: true
|
||||
image_path: "icon.png"
|
||||
background_color: "#ffffff"
|
||||
theme_color: "#ffffff"
|
||||
|
||||
windows:
|
||||
generate: true
|
||||
image_path: "icon.png"
|
||||
icon_size: 48 # min:48, max:256, default: 48
|
||||
|
||||
macos:
|
||||
generate: true
|
||||
image_path: "icon.png"
|
||||
BIN
icon.png
|
Before Width: | Height: | Size: 555 KiB After Width: | Height: | Size: 26 KiB |
29
ios/ExportOptions.plist
Normal file
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>destination</key>
|
||||
<string>export</string>
|
||||
<key>generateAppStoreInformation</key>
|
||||
<false/>
|
||||
<key>manageAppVersionAndBuildNumber</key>
|
||||
<true/>
|
||||
<key>method</key>
|
||||
<string>app-store-connect</string>
|
||||
<key>signingStyle</key>
|
||||
<string>manual</string>
|
||||
<key>provisioningProfiles</key>
|
||||
<dict>
|
||||
<key>de.jonasbark.swiftcontrol.darwin</key>
|
||||
<string>ios app store</string>
|
||||
</dict>
|
||||
<key>stripSwiftSymbols</key>
|
||||
<true/>
|
||||
<key>teamID</key>
|
||||
<string>UZRHKPVWN9</string>
|
||||
<key>testFlightInternalTestingOnly</key>
|
||||
<false/>
|
||||
<key>uploadSymbols</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||