Compare commits
493 Commits
fitnesstra
...
main
Author | SHA1 | Date |
---|---|---|
Savanni D'Gerinel | 5d04c84437 | |
Savanni D'Gerinel | 791f2be3c5 | |
Savanni D'Gerinel | 74b7f1c6f7 | |
Savanni D'Gerinel | 9c490a84a4 | |
Savanni D'Gerinel | 724cc1a3f0 | |
Savanni D'Gerinel | 8f71760604 | |
Savanni D'Gerinel | 11abde345e | |
Savanni D'Gerinel | a5b76c8171 | |
Savanni D'Gerinel | 9b23dd5acd | |
Savanni D'Gerinel | 54225ca729 | |
Savanni D'Gerinel | 95b46de7fc | |
Savanni D'Gerinel | caaf9c57c6 | |
Savanni D'Gerinel | 81d452694d | |
Savanni D'Gerinel | 88cf32047b | |
Savanni D'Gerinel | 6cae7dbb0e | |
Savanni D'Gerinel | 80776c65d8 | |
Savanni D'Gerinel | 1c54e0832b | |
Savanni D'Gerinel | aee4528fb3 | |
Savanni D'Gerinel | 0535b6da5a | |
Savanni D'Gerinel | b55324feab | |
Savanni D'Gerinel | 50d8a9670e | |
Savanni D'Gerinel | 9cda35e766 | |
Savanni D'Gerinel | d0f461a5eb | |
Savanni D'Gerinel | 70c013218a | |
Savanni D'Gerinel | 37c7e04820 | |
Savanni D'Gerinel | 291663d4a3 | |
Savanni D'Gerinel | 2b0fc7639e | |
Savanni D'Gerinel | 80d8dedbaf | |
Savanni D'Gerinel | d7a70119c8 | |
Savanni D'Gerinel | 54c4b99ab6 | |
Savanni D'Gerinel | ef5415303b | |
Savanni D'Gerinel | 8d183d6d8c | |
Savanni D'Gerinel | 0b949111d2 | |
Savanni D'Gerinel | 6164cb3b39 | |
Savanni D'Gerinel | 22f0f9061c | |
Savanni D'Gerinel | 0bb5e62f96 | |
Savanni D'Gerinel | 06aedc34bb | |
Savanni D'Gerinel | 84b077e20c | |
Savanni D'Gerinel | fc2e88add2 | |
Savanni D'Gerinel | 15c4ae9bad | |
Savanni D'Gerinel | 7dd531b493 | |
Savanni D'Gerinel | cbfb3f2e37 | |
Savanni D'Gerinel | 9540a2c5bb | |
Savanni D'Gerinel | 6165d65977 | |
Savanni D'Gerinel | 4f8a1636c1 | |
Savanni D'Gerinel | 20b02fbd90 | |
Savanni D'Gerinel | 278ec27b4e | |
Savanni D'Gerinel | 8b7add37c1 | |
Savanni D'Gerinel | 5441a3c441 | |
Savanni D'Gerinel | b1374229f3 | |
Savanni D'Gerinel | bc5042c004 | |
Savanni D'Gerinel | 0534143d6b | |
Savanni D'Gerinel | d7f5269e15 | |
Savanni D'Gerinel | c913e9da37 | |
Savanni D'Gerinel | c50bd652f1 | |
Savanni D'Gerinel | 093e1f7f8a | |
Savanni D'Gerinel | 3c94f906a6 | |
Savanni D'Gerinel | 0aecaee760 | |
Savanni D'Gerinel | baeb458126 | |
Savanni D'Gerinel | da144a58ec | |
Savanni D'Gerinel | f09af67193 | |
Savanni D'Gerinel | 32391a46e7 | |
Savanni D'Gerinel | 0a62c96b0f | |
Savanni D'Gerinel | 78863ee709 | |
Savanni D'Gerinel | 5cdcf0499c | |
Savanni D'Gerinel | b982f2c1cc | |
Savanni D'Gerinel | 46b25cc6c5 | |
Savanni D'Gerinel | 9fbc630500 | |
Savanni D'Gerinel | b481d5d058 | |
Savanni D'Gerinel | 7a06b8cf39 | |
Savanni D'Gerinel | 3192c0a142 | |
Savanni D'Gerinel | acf7ca0c9a | |
Savanni D'Gerinel | 64138b9e90 | |
Savanni D'Gerinel | e587d269e9 | |
Savanni D'Gerinel | 57aadd7597 | |
Savanni D'Gerinel | b70d927eac | |
Savanni D'Gerinel | 3a7f204883 | |
Savanni D'Gerinel | 642351f248 | |
Savanni D'Gerinel | d9bb9d92e5 | |
Savanni D'Gerinel | 30e7bdb817 | |
Savanni D'Gerinel | 556f91b70b | |
Savanni D'Gerinel | 894575b0fb | |
Savanni D'Gerinel | d964ab0d2f | |
Savanni D'Gerinel | 74c8eb6861 | |
Savanni D'Gerinel | 3aac3b8393 | |
Savanni D'Gerinel | 295f0a0411 | |
Savanni D'Gerinel | e694ba74ca | |
Savanni D'Gerinel | 5f9cd2622a | |
Savanni D'Gerinel | 48271389ad | |
Savanni D'Gerinel | 49571b0f82 | |
Savanni D'Gerinel | 89a289a1ae | |
Savanni D'Gerinel | fe082773e3 | |
Savanni D'Gerinel | db9efbaedd | |
Savanni D'Gerinel | 1d959117aa | |
Savanni D'Gerinel | a5990a2a30 | |
Savanni D'Gerinel | bd6d5b62e3 | |
Savanni D'Gerinel | 82c1765513 | |
Savanni D'Gerinel | de54ec676f | |
Savanni D'Gerinel | 5612c89a61 | |
Savanni D'Gerinel | c3c144e035 | |
Savanni D'Gerinel | 05a6dcf3af | |
Savanni D'Gerinel | b98e0bdcea | |
Savanni D'Gerinel | 3d4a298dc1 | |
Savanni D'Gerinel | 2d7fbb9a4b | |
Savanni D'Gerinel | 3a5cb17e09 | |
Savanni D'Gerinel | f6c82cbcb0 | |
Savanni D'Gerinel | 74b00d94b1 | |
Savanni D'Gerinel | ddf83b3018 | |
Savanni D'Gerinel | a1f41a440f | |
Savanni D'Gerinel | e838f601ca | |
Savanni D'Gerinel | 1b0a90a332 | |
Savanni D'Gerinel | 4dc6e3151b | |
Savanni D'Gerinel | 79cec6e21d | |
Savanni D'Gerinel | 32cc74edfa | |
Savanni D'Gerinel | d3d3260091 | |
Savanni D'Gerinel | a1441f7bb1 | |
Savanni D'Gerinel | 9e7350b087 | |
Savanni D'Gerinel | 0032f16422 | |
Savanni D'Gerinel | a5d51dab70 | |
Savanni D'Gerinel | c24a5f515f | |
Savanni D'Gerinel | d70ca08db2 | |
Savanni D'Gerinel | 55b6327d42 | |
Savanni D'Gerinel | 1c2f40c868 | |
Savanni D'Gerinel | 1527942f9c | |
Savanni D'Gerinel | 7ba758b325 | |
Savanni D'Gerinel | aed4735209 | |
Savanni D'Gerinel | c14b20b79e | |
Savanni D'Gerinel | 56ff5527ba | |
Savanni D'Gerinel | f7f55d74fd | |
Savanni D'Gerinel | 843924afef | |
Savanni D'Gerinel | 86d7ca0b01 | |
Savanni D'Gerinel | d137ee2481 | |
Savanni D'Gerinel | 74dbb58ed9 | |
Savanni D'Gerinel | 9a18496c95 | |
Savanni D'Gerinel | 01776a534e | |
Savanni D'Gerinel | b33d17c256 | |
Savanni D'Gerinel | ab59eedef5 | |
Savanni D'Gerinel | 4dd6afeae7 | |
Savanni D'Gerinel | cb2bec4287 | |
Savanni D'Gerinel | 5a93c4fdcd | |
Savanni D'Gerinel | 6e5cbc0930 | |
Savanni D'Gerinel | 73a5ab89a3 | |
Savanni D'Gerinel | 291dc32fe5 | |
Savanni D'Gerinel | b5c42e3ac3 | |
Savanni D'Gerinel | 6394d89331 | |
Savanni D'Gerinel | 38b1e62b60 | |
Savanni D'Gerinel | 4acf034b8d | |
Savanni D'Gerinel | 1aff203afc | |
Savanni D'Gerinel | 9fc9d2b758 | |
Savanni D'Gerinel | 76f4b31466 | |
Savanni D'Gerinel | 73052a0694 | |
Savanni D'Gerinel | 2c42c35dfe | |
Savanni D'Gerinel | afe693fe10 | |
Savanni D'Gerinel | af1422d523 | |
Savanni D'Gerinel | 792e20d44b | |
Savanni D'Gerinel | 8016188b29 | |
Savanni D'Gerinel | 74df2880bb | |
Savanni D'Gerinel | 96c4201680 | |
Savanni D'Gerinel | ecdd38ebbc | |
Savanni D'Gerinel | 2fb8728856 | |
Savanni D'Gerinel | a7d43ef184 | |
Savanni D'Gerinel | 9727d35116 | |
Savanni D'Gerinel | 1d6155d9e5 | |
Savanni D'Gerinel | a8bf540517 | |
Savanni D'Gerinel | 3db870d790 | |
Savanni D'Gerinel | 24276d172b | |
Savanni D'Gerinel | 96317f5692 | |
Savanni D'Gerinel | c1e797f3ae | |
Savanni D'Gerinel | 304008c674 | |
Savanni D'Gerinel | 772188b470 | |
Savanni D'Gerinel | bc31522c95 | |
Savanni D'Gerinel | 6c68564a77 | |
Savanni D'Gerinel | 55c1a6372f | |
Savanni D'Gerinel | 2cbd539bf4 | |
Savanni D'Gerinel | 7d14308def | |
Savanni D'Gerinel | dcd6301bb9 | |
Savanni D'Gerinel | 69567db486 | |
Savanni D'Gerinel | f8d66bbb69 | |
Savanni D'Gerinel | dce11dde2b | |
Savanni D'Gerinel | 3f9a7072eb | |
Savanni D'Gerinel | 7ec48ded5d | |
Savanni D'Gerinel | 9461c387fe | |
Savanni D'Gerinel | d4c48c4443 | |
Savanni D'Gerinel | 9bedb7a76c | |
Savanni D'Gerinel | 1fe318068b | |
Savanni D'Gerinel | 18e7e4fe2f | |
Savanni D'Gerinel | 1c2c4982a1 | |
Savanni D'Gerinel | c075b7ed6e | |
Savanni D'Gerinel | 56d0a53666 | |
Savanni D'Gerinel | b00acc64a3 | |
Savanni D'Gerinel | 104760c754 | |
Savanni D'Gerinel | 1e6555ef61 | |
Savanni D'Gerinel | 2e2ff6b47e | |
Savanni D'Gerinel | 2d22397382 | |
Savanni D'Gerinel | 2c7666304a | |
Savanni D'Gerinel | 0007522b26 | |
Savanni D'Gerinel | b7b9b1b29f | |
Savanni D'Gerinel | 2e3d5fc5a4 | |
Savanni D'Gerinel | a25b76d230 | |
Savanni D'Gerinel | 9970161c30 | |
Savanni D'Gerinel | 7bd4885b09 | |
Savanni D'Gerinel | b5dcee3737 | |
Savanni D'Gerinel | 0c3ae062c8 | |
Savanni D'Gerinel | f422e233a1 | |
Savanni D'Gerinel | 7a6e902fdd | |
Savanni D'Gerinel | 6d9e2ea382 | |
Savanni D'Gerinel | 04a48574d3 | |
Savanni D'Gerinel | e13e7cf4c3 | |
Savanni D'Gerinel | 383f809191 | |
Savanni D'Gerinel | d269924827 | |
Savanni D'Gerinel | 8049859816 | |
Savanni D'Gerinel | ac343a2af6 | |
Savanni D'Gerinel | 5cd0e822c6 | |
Savanni D'Gerinel | fe5e4ed044 | |
Savanni D'Gerinel | e30668ca8e | |
Savanni D'Gerinel | 149587f0bd | |
Savanni D'Gerinel | d2f4ec97c0 | |
Savanni D'Gerinel | c94b7db484 | |
Savanni D'Gerinel | 85e2494c3b | |
Savanni D'Gerinel | af8f9b0244 | |
Savanni D'Gerinel | 1b3ca7439d | |
Savanni D'Gerinel | 3dc8be0d26 | |
Savanni D'Gerinel | 43cd408e2c | |
Savanni D'Gerinel | 3a728a51b4 | |
Savanni D'Gerinel | f19090311b | |
Savanni D'Gerinel | dedcc76df0 | |
Savanni D'Gerinel | 6678ab9852 | |
Savanni D'Gerinel | 9c200f555c | |
Savanni D'Gerinel | 3ca8bf64cc | |
Savanni D'Gerinel | 87994012fa | |
Savanni D'Gerinel | 50268ffadc | |
Savanni D'Gerinel | beedeba8dc | |
Savanni D'Gerinel | db188ea75a | |
Savanni D'Gerinel | 104ffc5782 | |
Savanni D'Gerinel | 38db3d6780 | |
Savanni D'Gerinel | 0dd0a5f7cc | |
Savanni D'Gerinel | acdf9ec150 | |
Savanni D'Gerinel | 0ebdcd7c2a | |
Savanni D'Gerinel | baf652173c | |
Savanni D'Gerinel | c4befcc6de | |
Savanni D'Gerinel | a7d6d82ec2 | |
Savanni D'Gerinel | f3a453d151 | |
Savanni D'Gerinel | b9aa434278 | |
Savanni D'Gerinel | 83a4839b1d | |
Savanni D'Gerinel | 0e0d67a9ac | |
Savanni D'Gerinel | e5fb605816 | |
Savanni D'Gerinel | f9db002464 | |
Savanni D'Gerinel | 0ac9bb74a6 | |
Savanni D'Gerinel | f034dfcb8b | |
Savanni D'Gerinel | 7abb33c4fe | |
Savanni D'Gerinel | 581979fc54 | |
Savanni D'Gerinel | bf93625225 | |
Savanni D'Gerinel | 778da0b651 | |
Savanni D'Gerinel | 8b53114d0d | |
Savanni D'Gerinel | 42e931d780 | |
Savanni D'Gerinel | 532210db03 | |
Savanni D'Gerinel | 37f6334c9f | |
Savanni D'Gerinel | 3310c460ba | |
Savanni D'Gerinel | 6d14cdbe2a | |
Savanni D'Gerinel | c46ab1b389 | |
Savanni D'Gerinel | 168ba6eb40 | |
Savanni D'Gerinel | 7e3ee9a5b7 | |
Savanni D'Gerinel | 86a6d386d2 | |
Savanni D'Gerinel | e461cb9908 | |
Savanni D'Gerinel | 942e91009e | |
Savanni D'Gerinel | 48113d6ccb | |
Savanni D'Gerinel | d878f4e82c | |
Savanni D'Gerinel | 7949033857 | |
Savanni D'Gerinel | ce874e1d30 | |
Savanni D'Gerinel | 07b8bb7bfe | |
Savanni D'Gerinel | a403c1b1b3 | |
Savanni D'Gerinel | 9a014af75a | |
Savanni D'Gerinel | 448231739b | |
Savanni D'Gerinel | b0027032a4 | |
Savanni D'Gerinel | 41bbfa14f3 | |
Savanni D'Gerinel | 66876e41c0 | |
Savanni D'Gerinel | ee348c29cb | |
Savanni D'Gerinel | e96b8087e2 | |
Savanni D'Gerinel | 12df1f4b9b | |
Savanni D'Gerinel | c2e34db79c | |
Savanni D'Gerinel | 0fbfb4f1ad | |
Savanni D'Gerinel | c2e78d7c54 | |
Savanni D'Gerinel | 2ceccbf38d | |
Savanni D'Gerinel | fbf6a9e76e | |
Savanni D'Gerinel | 52f814e663 | |
Savanni D'Gerinel | 4114874156 | |
Savanni D'Gerinel | b756e8ca81 | |
Savanni D'Gerinel | 3cb742d863 | |
Savanni D'Gerinel | 27e1691854 | |
Savanni D'Gerinel | 2d2e82f41a | |
Savanni D'Gerinel | 78c017ede7 | |
Savanni D'Gerinel | cfdceff055 | |
Savanni D'Gerinel | 07b4cb31ce | |
Savanni D'Gerinel | b3f88a49aa | |
Savanni D'Gerinel | 3f1316b3dd | |
Savanni D'Gerinel | ef057eca66 | |
Savanni D'Gerinel | c70e1d943d | |
Savanni D'Gerinel | 7711f68993 | |
Savanni D'Gerinel | f13b3effd6 | |
Savanni D'Gerinel | 4cdd2b6b0f | |
Savanni D'Gerinel | 6c831567eb | |
Savanni D'Gerinel | 0afe0c1b88 | |
Savanni D'Gerinel | e0f3cdb50a | |
Savanni D'Gerinel | efac7e43eb | |
Savanni D'Gerinel | 37c60e4346 | |
Savanni D'Gerinel | 2084061526 | |
Savanni D'Gerinel | 79422b5c7a | |
Savanni D'Gerinel | 3f2feee4dd | |
Savanni D'Gerinel | 5443015868 | |
Savanni D'Gerinel | 49b1865818 | |
Savanni D'Gerinel | 10849687e3 | |
Savanni D'Gerinel | d441e19479 | |
Savanni D'Gerinel | 5496e9ce10 | |
Savanni D'Gerinel | 7b6b7ec011 | |
Savanni D'Gerinel | e657320b28 | |
Savanni D'Gerinel | bdcd7ee18e | |
Savanni D'Gerinel | f9974e79a7 | |
Savanni D'Gerinel | 4200432e1f | |
Savanni D'Gerinel | 525b5389a1 | |
Savanni D'Gerinel | d4a5e0f55d | |
Savanni D'Gerinel | 1d89254413 | |
Savanni D'Gerinel | 2f6be84a43 | |
Savanni D'Gerinel | f7403b43a3 | |
Savanni D'Gerinel | 2e7e159325 | |
Savanni D'Gerinel | 1e11069282 | |
Savanni D'Gerinel | c38d680e57 | |
Savanni D'Gerinel | 9bb32a378c | |
Savanni D'Gerinel | b3bfa84691 | |
Savanni D'Gerinel | f53c7200e6 | |
Savanni D'Gerinel | 491c80b42b | |
Savanni D'Gerinel | 5e4db0032b | |
Savanni D'Gerinel | 4a7d741224 | |
Savanni D'Gerinel | 6aedff8cda | |
Savanni D'Gerinel | 535ea6cd9d | |
Savanni D'Gerinel | da8281636a | |
Savanni D'Gerinel | b448ab7656 | |
Savanni D'Gerinel | 75a90bbdff | |
Savanni D'Gerinel | 94aa67a156 | |
Savanni D'Gerinel | ee5f4646df | |
Savanni D'Gerinel | 561ec70a65 | |
Savanni D'Gerinel | 14f0a74af8 | |
Savanni D'Gerinel | 68b62464f0 | |
Savanni D'Gerinel | da6bf3bfea | |
Savanni D'Gerinel | 3e87e13526 | |
Savanni D'Gerinel | 88938e44c8 | |
Savanni D'Gerinel | 89a1aa7ee5 | |
Savanni D'Gerinel | 22e25256a5 | |
Savanni D'Gerinel | 9787ed3e67 | |
Savanni D'Gerinel | 396f6e3bcf | |
Savanni D'Gerinel | 8521db333b | |
Savanni D'Gerinel | 4a7b23544e | |
Savanni D'Gerinel | a06c9fae25 | |
Savanni D'Gerinel | f05e0a15f1 | |
Savanni D'Gerinel | 634c404ae9 | |
Savanni D'Gerinel | e36657591b | |
Savanni D'Gerinel | 7077724e15 | |
Savanni D'Gerinel | 4816c9f4cf | |
Savanni D'Gerinel | 207d099607 | |
Savanni D'Gerinel | 59061c02ce | |
Savanni D'Gerinel | 3d460e5840 | |
Savanni D'Gerinel | d6c2a9519b | |
Savanni D'Gerinel | 7805a55322 | |
Savanni D'Gerinel | 1489121877 | |
Savanni D'Gerinel | 562d4871a1 | |
Savanni D'Gerinel | 16c8dcb682 | |
Savanni D'Gerinel | cc828c417a | |
Savanni D'Gerinel | 784f3ff7f4 | |
Savanni D'Gerinel | 5439e2ac04 | |
Savanni D'Gerinel | 0bf6e079a2 | |
Savanni D'Gerinel | 3998538e88 | |
Savanni D'Gerinel | 793cd67218 | |
Savanni D'Gerinel | ff13ff3c0e | |
Savanni D'Gerinel | cc3ad372e6 | |
Savanni D'Gerinel | 3c063af525 | |
Savanni D'Gerinel | aa64bf4c7e | |
Savanni D'Gerinel | f75e0d4d65 | |
Savanni D'Gerinel | d8534a08eb | |
Savanni D'Gerinel | e5d0b7d20f | |
Savanni D'Gerinel | e9ffab1187 | |
Savanni D'Gerinel | a584fb4de3 | |
Savanni D'Gerinel | e3f4ca246d | |
Savanni D'Gerinel | 07b7351501 | |
Savanni D'Gerinel | 70a295d4b1 | |
Savanni D'Gerinel | 5478d388cb | |
Savanni D'Gerinel | e203b17c8b | |
Savanni D'Gerinel | 69583dfd64 | |
Savanni D'Gerinel | d59c2585db | |
Savanni D'Gerinel | a6fcbfac71 | |
Savanni D'Gerinel | 4f940099da | |
Savanni D'Gerinel | f4735dd16b | |
Savanni D'Gerinel | b662ac519a | |
Savanni D'Gerinel | efec8dfe5a | |
Savanni D'Gerinel | 0765d94a5e | |
Savanni D'Gerinel | 40b33797f3 | |
Savanni D'Gerinel | 24e88da8e2 | |
Savanni D'Gerinel | 456d872b40 | |
Savanni D'Gerinel | c76396a010 | |
Savanni D'Gerinel | b2ca3d195d | |
Savanni D'Gerinel | 1f2c7b8002 | |
Savanni D'Gerinel | 8e36091a19 | |
Savanni D'Gerinel | 3e13cbcdaa | |
Savanni D'Gerinel | 038ccac637 | |
Savanni D'Gerinel | 4afdd16414 | |
Savanni D'Gerinel | 6a6dc3dc1e | |
Savanni D'Gerinel | 63884a32f6 | |
Savanni D'Gerinel | bfd23bff5b | |
Savanni D'Gerinel | ab97e14ad7 | |
Savanni D'Gerinel | 1eaa7746ff | |
Savanni D'Gerinel | 5e2b7fbc99 | |
Savanni D'Gerinel | e2d3875587 | |
Savanni D'Gerinel | 4714a06daa | |
Savanni D'Gerinel | 0078f1db27 | |
Savanni D'Gerinel | 6fb7749642 | |
Savanni D'Gerinel | 28192ff8d7 | |
Savanni D'Gerinel | f403b8632f | |
Savanni D'Gerinel | 36b0238662 | |
Savanni D'Gerinel | 6f31ac1e8d | |
Savanni D'Gerinel | 96fc16d0f0 | |
Savanni D'Gerinel | 12ac34117d | |
Savanni D'Gerinel | 98fac30ed6 | |
Savanni D'Gerinel | 679510b010 | |
Savanni D'Gerinel | ab33db4a22 | |
Savanni D'Gerinel | 2eb7c5ca8c | |
Savanni D'Gerinel | 7521fdf2e9 | |
Savanni D'Gerinel | 43281c4dd3 | |
Savanni D'Gerinel | b6b371d7d4 | |
Savanni D'Gerinel | 09e5f88dd4 | |
Savanni D'Gerinel | 69d838b755 | |
Savanni D'Gerinel | b4f3fc1db6 | |
Savanni D'Gerinel | e4a6cb2f72 | |
Savanni D'Gerinel | 0cc232c3b5 | |
Savanni D'Gerinel | 5e56a56dd8 | |
Savanni D'Gerinel | 919d242daa | |
Savanni D'Gerinel | 11ca8661fa | |
Savanni D'Gerinel | 7ebbbfd03b | |
Savanni D'Gerinel | 38443f4120 | |
Savanni D'Gerinel | 52ae75ca5c | |
Savanni D'Gerinel | a1cae2ed2b | |
Savanni D'Gerinel | 1ac7ce43f2 | |
Savanni D'Gerinel | fa8391cea0 | |
Savanni D'Gerinel | 169032a637 | |
Savanni D'Gerinel | e0a97878e8 | |
Savanni D'Gerinel | 9ba402d54f | |
Savanni D'Gerinel | 58d9a12e9f | |
Savanni D'Gerinel | 08029a2afd | |
Savanni D'Gerinel | d4fc937613 | |
Savanni D'Gerinel | 9130aa5c77 | |
Savanni D'Gerinel | 5a471a3dbf | |
Savanni D'Gerinel | 92ca170a24 | |
Savanni D'Gerinel | d79598eaa3 | |
Savanni D'Gerinel | 9533934e05 | |
Savanni D'Gerinel | b9808a1862 | |
Savanni D'Gerinel | c51fced23a | |
Savanni D'Gerinel | 23011aacc0 | |
Savanni D'Gerinel | bce5659ceb | |
Savanni D'Gerinel | 084a558740 | |
Savanni D'Gerinel | 744511a552 | |
Savanni D'Gerinel | c8d21d0e25 | |
Savanni D'Gerinel | 1b9a8eee67 | |
Savanni D'Gerinel | 741f963606 | |
Savanni D'Gerinel | 82deabce48 | |
Savanni D'Gerinel | 4b30bf288a | |
Savanni D'Gerinel | 96c6f2dfbf | |
Savanni D'Gerinel | 90b0f830e0 | |
Savanni D'Gerinel | e3957a5dbe | |
Savanni D'Gerinel | ba814fd899 | |
Savanni D'Gerinel | 9137909a64 | |
Savanni D'Gerinel | 12f4f9dde6 | |
Savanni D'Gerinel | fd4c6ff935 | |
Savanni D'Gerinel | 3cca3a7f89 | |
Savanni D'Gerinel | 4fd07b240e | |
Savanni D'Gerinel | 2469cd78fa | |
Savanni D'Gerinel | f24fb5eae9 | |
Savanni D'Gerinel | 798566d01c | |
Savanni D'Gerinel | 6fd4d9df96 | |
savanni | d9531c69ad | |
Savanni D'Gerinel | b15658dd0c | |
Savanni D'Gerinel | 9266387833 | |
Savanni D'Gerinel | ebb7062041 | |
Savanni D'Gerinel | d0db274110 | |
Savanni D'Gerinel | 32ed1a2464 | |
Savanni D'Gerinel | 6456389eef | |
Savanni D'Gerinel | ef045117be | |
Savanni D'Gerinel | 6f6579b7a7 | |
Savanni D'Gerinel | b7f5f4ba34 | |
Savanni D'Gerinel | 7546a81876 | |
Savanni D'Gerinel | d4a55ccb17 | |
Savanni D'Gerinel | 587c1d763d | |
Savanni D'Gerinel | a779220173 | |
Savanni D'Gerinel | 343969a898 | |
Savanni D'Gerinel | 6c87d23537 | |
Savanni D'Gerinel | 140ad0f22c | |
Savanni D'Gerinel | 6c4226ec66 |
|
@ -3,3 +3,9 @@ target
|
||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
result
|
result
|
||||||
|
*.tgz
|
||||||
|
*.tar.gz
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite-shm
|
||||||
|
*.sqlite-wal
|
||||||
|
file-service/var
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"rust-analyzer.showUnlinkedFileNotification": false
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,34 @@
|
||||||
|
[workspace]
|
||||||
|
resolver = "2"
|
||||||
|
members = [
|
||||||
|
"authdb",
|
||||||
|
"bike-lights/bike",
|
||||||
|
"bike-lights/core",
|
||||||
|
"bike-lights/simulator",
|
||||||
|
"changeset",
|
||||||
|
"config",
|
||||||
|
"config-derive",
|
||||||
|
"coordinates",
|
||||||
|
"cyberpunk-splash",
|
||||||
|
"dashboard",
|
||||||
|
"emseries",
|
||||||
|
"file-service",
|
||||||
|
"fitnesstrax/core",
|
||||||
|
"fitnesstrax/app",
|
||||||
|
"fluent-ergonomics",
|
||||||
|
"geo-types",
|
||||||
|
"gm-control-panel",
|
||||||
|
"hex-grid",
|
||||||
|
"icon-test",
|
||||||
|
"ifc",
|
||||||
|
"memorycache",
|
||||||
|
"nom-training",
|
||||||
|
"otg/core",
|
||||||
|
"otg/gtk",
|
||||||
|
"result-extended",
|
||||||
|
"screenplay",
|
||||||
|
"sgf",
|
||||||
|
"timezone-testing",
|
||||||
|
"tree",
|
||||||
|
"visions/server", "gm-dash/server",
|
||||||
|
]
|
60
Makefile
60
Makefile
|
@ -1,60 +0,0 @@
|
||||||
|
|
||||||
changeset-dev:
|
|
||||||
cd changeset && make dev
|
|
||||||
|
|
||||||
changeset-test:
|
|
||||||
cd changeset && make test
|
|
||||||
|
|
||||||
coordinates-dev:
|
|
||||||
cd coordinates && make dev
|
|
||||||
|
|
||||||
coordinates-test:
|
|
||||||
cd coordinates && make test
|
|
||||||
|
|
||||||
emseries-dev:
|
|
||||||
cd emseries && make dev
|
|
||||||
|
|
||||||
emseries-test:
|
|
||||||
cd emseries && make test
|
|
||||||
|
|
||||||
flow-dev:
|
|
||||||
cd flow && make dev
|
|
||||||
|
|
||||||
flow-test:
|
|
||||||
cd flow && make test
|
|
||||||
|
|
||||||
fluent-ergonomics-dev:
|
|
||||||
cd fluent-ergonomics && make dev
|
|
||||||
|
|
||||||
fluent-ergonomics-test:
|
|
||||||
cd fluent-ergonomics && make test
|
|
||||||
|
|
||||||
ifc-dev:
|
|
||||||
cd ifc && make dev
|
|
||||||
|
|
||||||
ifc-test:
|
|
||||||
cd ifc && make test
|
|
||||||
|
|
||||||
kifu-core/dev:
|
|
||||||
cd kifu/kifu-core && make test
|
|
||||||
|
|
||||||
kifu-core/test:
|
|
||||||
cd kifu/kifu-core && make test
|
|
||||||
|
|
||||||
kifu-core/test-oneshot:
|
|
||||||
cd kifu/kifu-core && make test-oneshot
|
|
||||||
|
|
||||||
kifu-gtk:
|
|
||||||
cd kifu/gtk && make release
|
|
||||||
|
|
||||||
kifu-gtk/dev:
|
|
||||||
cd kifu/gtk && make dev
|
|
||||||
|
|
||||||
kifu-pwa:
|
|
||||||
cd kifu/pwa && make release
|
|
||||||
|
|
||||||
kifu-pwa/dev:
|
|
||||||
pushd kifu/pwa && make dev
|
|
||||||
|
|
||||||
kifu-pwa/server:
|
|
||||||
pushd kifu/pwa && make server
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
[package]
|
||||||
|
name = "authdb"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "authdb"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "auth-cli"
|
||||||
|
path = "src/bin/cli.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
base64ct = { version = "1", features = [ "alloc" ] }
|
||||||
|
clap = { version = "4", features = [ "derive" ] }
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
sha2 = { version = "0.10" }
|
||||||
|
sqlx = { version = "0.7", features = [ "runtime-tokio", "sqlite" ] }
|
||||||
|
# sqlformat introduced a mistaken breaking change in 0.2.7
|
||||||
|
sqlformat = { version = "=0.2.6" }
|
||||||
|
thiserror = { version = "1" }
|
||||||
|
tokio = { version = "1", features = [ "full" ] }
|
||||||
|
uuid = { version = "0.4", features = [ "serde", "v4" ] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
cool_asserts = "*"
|
|
@ -0,0 +1,11 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INTEGER PRIMARY KEY NOT NULL,
|
||||||
|
username TEXT NOT NULL,
|
||||||
|
token TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS sessions (
|
||||||
|
token TEXT NOT NULL,
|
||||||
|
user_id INTEGER,
|
||||||
|
FOREIGN KEY(user_id) REFERENCES users(id)
|
||||||
|
);
|
|
@ -0,0 +1,40 @@
|
||||||
|
use authdb::{AuthDB, Username};
|
||||||
|
use clap::{Parser, Subcommand};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[derive(Subcommand, Debug)]
|
||||||
|
enum Commands {
|
||||||
|
AddUser { username: String },
|
||||||
|
DeleteUser { username: String },
|
||||||
|
ListUsers,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
struct Args {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: Commands,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
pub async fn main() {
|
||||||
|
let args = Args::parse();
|
||||||
|
let authdb = AuthDB::new(PathBuf::from(&std::env::var("AUTHDB").unwrap()))
|
||||||
|
.await
|
||||||
|
.expect("to be able to open the database");
|
||||||
|
|
||||||
|
match args.command {
|
||||||
|
Commands::AddUser { username } => {
|
||||||
|
match authdb.add_user(Username::from(username.clone())).await {
|
||||||
|
Ok(token) => {
|
||||||
|
println!("User {} created. Auth token: {}", username, *token);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
println!("Could not create user {}", username);
|
||||||
|
println!("\tError: {:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Commands::DeleteUser { .. } => {}
|
||||||
|
Commands::ListUsers => {}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,302 @@
|
||||||
|
use base64ct::{Base64, Encoding};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use sqlx::{
|
||||||
|
sqlite::{SqlitePool, SqliteRow},
|
||||||
|
Row,
|
||||||
|
};
|
||||||
|
use std::ops::Deref;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use thiserror::Error;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum AuthError {
|
||||||
|
#[error("authentication token is duplicated")]
|
||||||
|
DuplicateAuthToken,
|
||||||
|
|
||||||
|
#[error("session token is duplicated")]
|
||||||
|
DuplicateSessionToken,
|
||||||
|
|
||||||
|
#[error("database failed")]
|
||||||
|
SqlError(sqlx::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<sqlx::Error> for AuthError {
|
||||||
|
fn from(err: sqlx::Error) -> AuthError {
|
||||||
|
AuthError::SqlError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq)]
|
||||||
|
pub struct Username(String);
|
||||||
|
|
||||||
|
impl From<String> for Username {
|
||||||
|
fn from(s: String) -> Self {
|
||||||
|
Self(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for Username {
|
||||||
|
fn from(s: &str) -> Self {
|
||||||
|
Self(s.to_owned())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Username> for String {
|
||||||
|
fn from(s: Username) -> Self {
|
||||||
|
Self::from(&s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&Username> for String {
|
||||||
|
fn from(s: &Username) -> Self {
|
||||||
|
let Username(s) = s;
|
||||||
|
Self::from(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for Username {
|
||||||
|
type Target = String;
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl sqlx::FromRow<'_, SqliteRow> for Username {
|
||||||
|
fn from_row(row: &SqliteRow) -> sqlx::Result<Self> {
|
||||||
|
let name: String = row.try_get("username")?;
|
||||||
|
Ok(Username::from(name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq)]
|
||||||
|
pub struct AuthToken(String);
|
||||||
|
|
||||||
|
impl From<String> for AuthToken {
|
||||||
|
fn from(s: String) -> Self {
|
||||||
|
Self(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for AuthToken {
|
||||||
|
fn from(s: &str) -> Self {
|
||||||
|
Self(s.to_owned())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<AuthToken> for PathBuf {
|
||||||
|
fn from(s: AuthToken) -> Self {
|
||||||
|
Self::from(&s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&AuthToken> for PathBuf {
|
||||||
|
fn from(s: &AuthToken) -> Self {
|
||||||
|
let AuthToken(s) = s;
|
||||||
|
Self::from(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for AuthToken {
|
||||||
|
type Target = String;
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq)]
|
||||||
|
pub struct SessionToken(String);
|
||||||
|
|
||||||
|
impl From<String> for SessionToken {
|
||||||
|
fn from(s: String) -> Self {
|
||||||
|
Self(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for SessionToken {
|
||||||
|
fn from(s: &str) -> Self {
|
||||||
|
Self(s.to_owned())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SessionToken> for PathBuf {
|
||||||
|
fn from(s: SessionToken) -> Self {
|
||||||
|
Self::from(&s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&SessionToken> for PathBuf {
|
||||||
|
fn from(s: &SessionToken) -> Self {
|
||||||
|
let SessionToken(s) = s;
|
||||||
|
Self::from(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for SessionToken {
|
||||||
|
type Target = String;
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AuthDB {
|
||||||
|
pool: SqlitePool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AuthDB {
|
||||||
|
pub async fn new(path: PathBuf) -> Result<Self, sqlx::Error> {
|
||||||
|
let migrator = sqlx::migrate!("./migrations");
|
||||||
|
let pool = SqlitePool::connect(&format!("sqlite://{}", path.to_str().unwrap())).await?;
|
||||||
|
migrator.run(&pool).await?;
|
||||||
|
Ok(Self { pool })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn add_user(&self, username: Username) -> Result<AuthToken, AuthError> {
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(Uuid::new_v4().hyphenated().to_string());
|
||||||
|
hasher.update(username.to_string());
|
||||||
|
let auth_token = Base64::encode_string(&hasher.finalize());
|
||||||
|
|
||||||
|
let _ = sqlx::query("INSERT INTO users (username, token) VALUES ($1, $2)")
|
||||||
|
.bind(username.to_string())
|
||||||
|
.bind(auth_token.clone())
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(AuthToken::from(auth_token))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_users(&self) -> Result<Vec<Username>, AuthError> {
|
||||||
|
let usernames = sqlx::query_as::<_, Username>("SELECT (username) FROM users")
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(usernames)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn authenticate(&self, token: AuthToken) -> Result<Option<SessionToken>, AuthError> {
|
||||||
|
let results = sqlx::query("SELECT * FROM users WHERE token = $1")
|
||||||
|
.bind(token.to_string())
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if results.len() > 1 {
|
||||||
|
return Err(AuthError::DuplicateAuthToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
if results.is_empty() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let user_id: i64 = results[0].try_get("id")?;
|
||||||
|
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(Uuid::new_v4().hyphenated().to_string());
|
||||||
|
hasher.update(token.to_string());
|
||||||
|
let session_token = Base64::encode_string(&hasher.finalize());
|
||||||
|
|
||||||
|
let _ = sqlx::query("INSERT INTO sessions (token, user_id) VALUES ($1, $2)")
|
||||||
|
.bind(session_token.clone())
|
||||||
|
.bind(user_id)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(Some(SessionToken::from(session_token)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn validate_session(
|
||||||
|
&self,
|
||||||
|
token: SessionToken,
|
||||||
|
) -> Result<Option<Username>, AuthError> {
|
||||||
|
let rows = sqlx::query(
|
||||||
|
"SELECT users.username FROM sessions INNER JOIN users ON sessions.user_id = users.id WHERE sessions.token = $1",
|
||||||
|
)
|
||||||
|
.bind(token.to_string())
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await?;
|
||||||
|
if rows.len() > 1 {
|
||||||
|
return Err(AuthError::DuplicateSessionToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
if rows.is_empty() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let username: String = rows[0].try_get("username")?;
|
||||||
|
Ok(Some(Username::from(username)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use cool_asserts::assert_matches;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn can_create_and_list_users() {
|
||||||
|
let db = AuthDB::new(PathBuf::from(":memory:"))
|
||||||
|
.await
|
||||||
|
.expect("a memory-only database will be created");
|
||||||
|
let _ = db
|
||||||
|
.add_user(Username::from("savanni"))
|
||||||
|
.await
|
||||||
|
.expect("user to be created");
|
||||||
|
assert_matches!(db.list_users().await, Ok(names) => {
|
||||||
|
let names = names.into_iter().collect::<HashSet<Username>>();
|
||||||
|
assert!(names.contains(&Username::from("savanni")));
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn unknown_auth_token_returns_nothing() {
|
||||||
|
let db = AuthDB::new(PathBuf::from(":memory:"))
|
||||||
|
.await
|
||||||
|
.expect("a memory-only database will be created");
|
||||||
|
let _ = db
|
||||||
|
.add_user(Username::from("savanni"))
|
||||||
|
.await
|
||||||
|
.expect("user to be created");
|
||||||
|
|
||||||
|
let token = AuthToken::from("0000000000");
|
||||||
|
|
||||||
|
assert_matches!(db.authenticate(token).await, Ok(None));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn auth_token_becomes_session_token() {
|
||||||
|
let db = AuthDB::new(PathBuf::from(":memory:"))
|
||||||
|
.await
|
||||||
|
.expect("a memory-only database will be created");
|
||||||
|
let token = db
|
||||||
|
.add_user(Username::from("savanni"))
|
||||||
|
.await
|
||||||
|
.expect("user to be created");
|
||||||
|
|
||||||
|
assert_matches!(db.authenticate(token).await, Ok(_));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn can_validate_session_token() {
|
||||||
|
let db = AuthDB::new(PathBuf::from(":memory:"))
|
||||||
|
.await
|
||||||
|
.expect("a memory-only database will be created");
|
||||||
|
let token = db
|
||||||
|
.add_user(Username::from("savanni"))
|
||||||
|
.await
|
||||||
|
.expect("user to be created");
|
||||||
|
let session = db
|
||||||
|
.authenticate(token)
|
||||||
|
.await
|
||||||
|
.expect("token authentication should succeed")
|
||||||
|
.expect("session token should be found");
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
db.validate_session(session).await,
|
||||||
|
Ok(Some(username)) => {
|
||||||
|
assert_eq!(username, Username::from("savanni"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
[build]
|
||||||
|
target = "thumbv6m-none-eabi"
|
||||||
|
|
||||||
|
[target.thumbv6m-none-eabi]
|
||||||
|
rustflags = [
|
||||||
|
"-C", "link-arg=--nmagic",
|
||||||
|
"-C", "link-arg=-Tlink.x",
|
||||||
|
"-C", "inline-threshold=5",
|
||||||
|
"-C", "no-vectorize-loops",
|
||||||
|
]
|
||||||
|
|
||||||
|
runner = "elf2uf2-rs -d"
|
|
@ -0,0 +1,18 @@
|
||||||
|
[package]
|
||||||
|
name = "bike"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
az = { version = "1" }
|
||||||
|
cortex-m-rt = { version = "0.7.3" }
|
||||||
|
cortex-m = { version = "0.7.7" }
|
||||||
|
embedded-alloc = { version = "0.5.1" }
|
||||||
|
embedded-hal = { version = "0.2.7" }
|
||||||
|
fixed = { version = "1" }
|
||||||
|
fugit = { version = "0.3.7" }
|
||||||
|
lights-core = { path = "../core" }
|
||||||
|
panic-halt = { version = "0.2.0" }
|
||||||
|
rp-pico = { version = "0.8.0" }
|
|
@ -0,0 +1,241 @@
|
||||||
|
#![no_main]
|
||||||
|
#![no_std]
|
||||||
|
|
||||||
|
extern crate alloc;
|
||||||
|
|
||||||
|
use alloc::boxed::Box;
|
||||||
|
use az::*;
|
||||||
|
use core::cell::RefCell;
|
||||||
|
use cortex_m::delay::Delay;
|
||||||
|
use embedded_alloc::Heap;
|
||||||
|
use embedded_hal::{blocking::spi::Write, digital::v2::InputPin, digital::v2::OutputPin};
|
||||||
|
use fixed::types::I16F16;
|
||||||
|
use fugit::RateExtU32;
|
||||||
|
use lights_core::{App, BodyPattern, DashboardPattern, Event, Instant, FPS, UI};
|
||||||
|
use panic_halt as _;
|
||||||
|
use rp_pico::{
|
||||||
|
entry,
|
||||||
|
hal::{
|
||||||
|
clocks::init_clocks_and_plls,
|
||||||
|
gpio::{FunctionSio, Pin, PinId, PullDown, PullUp, SioInput, SioOutput},
|
||||||
|
pac::{CorePeripherals, Peripherals},
|
||||||
|
spi::{Enabled, Spi, SpiDevice, ValidSpiPinout},
|
||||||
|
watchdog::Watchdog,
|
||||||
|
Clock, Sio,
|
||||||
|
},
|
||||||
|
Pins,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[global_allocator]
|
||||||
|
static HEAP: Heap = Heap::empty();
|
||||||
|
|
||||||
|
const LIGHT_SCALE: I16F16 = I16F16::lit("256.0");
|
||||||
|
const DASHBOARD_BRIGHTESS: u8 = 1;
|
||||||
|
const BODY_BRIGHTNESS: u8 = 8;
|
||||||
|
|
||||||
|
struct DebouncedButton<P: PinId> {
|
||||||
|
debounce: Instant,
|
||||||
|
pin: Pin<P, FunctionSio<SioInput>, PullUp>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P: PinId> DebouncedButton<P> {
|
||||||
|
fn new(pin: Pin<P, FunctionSio<SioInput>, PullUp>) -> Self {
|
||||||
|
Self {
|
||||||
|
debounce: Instant((0 as u32).into()),
|
||||||
|
pin,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_low(&self, time: Instant) -> bool {
|
||||||
|
if time <= self.debounce {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.pin.is_low().unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_debounce(&mut self, time: Instant) {
|
||||||
|
self.debounce = time + Instant((250 as u32).into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct BikeUI<
|
||||||
|
D: SpiDevice,
|
||||||
|
P: ValidSpiPinout<D>,
|
||||||
|
LeftId: PinId,
|
||||||
|
RightId: PinId,
|
||||||
|
PreviousId: PinId,
|
||||||
|
NextId: PinId,
|
||||||
|
BrakeId: PinId,
|
||||||
|
> {
|
||||||
|
spi: RefCell<Spi<Enabled, D, P, 8>>,
|
||||||
|
left_blinker_button: DebouncedButton<LeftId>,
|
||||||
|
right_blinker_button: DebouncedButton<RightId>,
|
||||||
|
previous_animation_button: DebouncedButton<PreviousId>,
|
||||||
|
next_animation_button: DebouncedButton<NextId>,
|
||||||
|
brake_sensor: Pin<BrakeId, FunctionSio<SioInput>, PullUp>,
|
||||||
|
|
||||||
|
brake_enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<
|
||||||
|
D: SpiDevice,
|
||||||
|
P: ValidSpiPinout<D>,
|
||||||
|
LeftId: PinId,
|
||||||
|
RightId: PinId,
|
||||||
|
PreviousId: PinId,
|
||||||
|
NextId: PinId,
|
||||||
|
BrakeId: PinId,
|
||||||
|
> BikeUI<D, P, LeftId, RightId, PreviousId, NextId, BrakeId>
|
||||||
|
{
|
||||||
|
fn new(
|
||||||
|
spi: Spi<Enabled, D, P, 8>,
|
||||||
|
left_blinker_button: Pin<LeftId, FunctionSio<SioInput>, PullUp>,
|
||||||
|
right_blinker_button: Pin<RightId, FunctionSio<SioInput>, PullUp>,
|
||||||
|
previous_animation_button: Pin<PreviousId, FunctionSio<SioInput>, PullUp>,
|
||||||
|
next_animation_button: Pin<NextId, FunctionSio<SioInput>, PullUp>,
|
||||||
|
brake_sensor: Pin<BrakeId, FunctionSio<SioInput>, PullUp>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
spi: RefCell::new(spi),
|
||||||
|
left_blinker_button: DebouncedButton::new(left_blinker_button),
|
||||||
|
right_blinker_button: DebouncedButton::new(right_blinker_button),
|
||||||
|
previous_animation_button: DebouncedButton::new(previous_animation_button),
|
||||||
|
next_animation_button: DebouncedButton::new(next_animation_button),
|
||||||
|
brake_sensor,
|
||||||
|
|
||||||
|
brake_enabled: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<
|
||||||
|
D: SpiDevice,
|
||||||
|
P: ValidSpiPinout<D>,
|
||||||
|
LeftId: PinId,
|
||||||
|
RightId: PinId,
|
||||||
|
PreviousId: PinId,
|
||||||
|
NextId: PinId,
|
||||||
|
BrakeId: PinId,
|
||||||
|
> UI for BikeUI<D, P, LeftId, RightId, PreviousId, NextId, BrakeId>
|
||||||
|
{
|
||||||
|
fn check_event(&mut self, current_time: Instant) -> Option<Event> {
|
||||||
|
if self.brake_sensor.is_high().unwrap_or(true) && !self.brake_enabled {
|
||||||
|
self.brake_enabled = true;
|
||||||
|
Some(Event::Brake)
|
||||||
|
} else if self.brake_sensor.is_low().unwrap_or(false) && self.brake_enabled {
|
||||||
|
self.brake_enabled = false;
|
||||||
|
Some(Event::BrakeRelease)
|
||||||
|
} else if self.left_blinker_button.is_low(current_time) {
|
||||||
|
self.left_blinker_button.set_debounce(current_time);
|
||||||
|
Some(Event::LeftBlinker)
|
||||||
|
} else if self.right_blinker_button.is_low(current_time) {
|
||||||
|
self.right_blinker_button.set_debounce(current_time);
|
||||||
|
Some(Event::RightBlinker)
|
||||||
|
} else if self.previous_animation_button.is_low(current_time) {
|
||||||
|
self.previous_animation_button.set_debounce(current_time);
|
||||||
|
Some(Event::PreviousPattern)
|
||||||
|
} else if self.next_animation_button.is_low(current_time) {
|
||||||
|
self.next_animation_button.set_debounce(current_time);
|
||||||
|
Some(Event::NextPattern)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_lights(&self, dashboard_lights: DashboardPattern, body_lights: BodyPattern) {
|
||||||
|
let mut lights: [u8; 260] = [0; 260];
|
||||||
|
lights[256] = 0xff;
|
||||||
|
lights[257] = 0xff;
|
||||||
|
lights[258] = 0xff;
|
||||||
|
lights[259] = 0xff;
|
||||||
|
for (idx, rgb) in dashboard_lights.iter().enumerate() {
|
||||||
|
lights[(idx + 1) * 4 + 0] = 0xe0 + DASHBOARD_BRIGHTESS;
|
||||||
|
lights[(idx + 1) * 4 + 1] = (I16F16::from(rgb.r) * LIGHT_SCALE).saturating_as();
|
||||||
|
lights[(idx + 1) * 4 + 2] = (I16F16::from(rgb.b) * LIGHT_SCALE).saturating_as();
|
||||||
|
lights[(idx + 1) * 4 + 3] = (I16F16::from(rgb.g) * LIGHT_SCALE).saturating_as();
|
||||||
|
}
|
||||||
|
for (idx, rgb) in body_lights.iter().enumerate() {
|
||||||
|
lights[(idx + 4) * 4 + 0] = 0xe0 + BODY_BRIGHTNESS;
|
||||||
|
lights[(idx + 4) * 4 + 1] = (I16F16::from(rgb.b) * LIGHT_SCALE).saturating_as();
|
||||||
|
lights[(idx + 4) * 4 + 2] = (I16F16::from(rgb.g) * LIGHT_SCALE).saturating_as();
|
||||||
|
lights[(idx + 4) * 4 + 3] = (I16F16::from(rgb.r) * LIGHT_SCALE).saturating_as();
|
||||||
|
}
|
||||||
|
let mut spi = self.spi.borrow_mut();
|
||||||
|
spi.write(lights.as_slice());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[entry]
|
||||||
|
fn main() -> ! {
|
||||||
|
{
|
||||||
|
use core::mem::MaybeUninit;
|
||||||
|
const HEAP_SIZE: usize = 8096;
|
||||||
|
static mut HEAP_MEM: [MaybeUninit<u8>; HEAP_SIZE] = [MaybeUninit::uninit(); HEAP_SIZE];
|
||||||
|
unsafe { HEAP.init(HEAP_MEM.as_ptr() as usize, HEAP_SIZE) }
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut pac = Peripherals::take().unwrap();
|
||||||
|
let core = CorePeripherals::take().unwrap();
|
||||||
|
let sio = Sio::new(pac.SIO);
|
||||||
|
let mut watchdog = Watchdog::new(pac.WATCHDOG);
|
||||||
|
|
||||||
|
let pins = Pins::new(
|
||||||
|
pac.IO_BANK0,
|
||||||
|
pac.PADS_BANK0,
|
||||||
|
sio.gpio_bank0,
|
||||||
|
&mut pac.RESETS,
|
||||||
|
);
|
||||||
|
|
||||||
|
let clocks = init_clocks_and_plls(
|
||||||
|
12_000_000u32,
|
||||||
|
pac.XOSC,
|
||||||
|
pac.CLOCKS,
|
||||||
|
pac.PLL_SYS,
|
||||||
|
pac.PLL_USB,
|
||||||
|
&mut pac.RESETS,
|
||||||
|
&mut watchdog,
|
||||||
|
)
|
||||||
|
.ok()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut delay = Delay::new(core.SYST, clocks.system_clock.freq().to_Hz());
|
||||||
|
let mut spi_clk = pins.gpio10.into_function();
|
||||||
|
let mut spi_sdo = pins.gpio11.into_function();
|
||||||
|
let spi = Spi::<_, _, _, 8>::new(pac.SPI1, (spi_sdo, spi_clk));
|
||||||
|
let mut spi = spi.init(
|
||||||
|
&mut pac.RESETS,
|
||||||
|
clocks.peripheral_clock.freq(),
|
||||||
|
1_u32.MHz(),
|
||||||
|
embedded_hal::spi::MODE_1,
|
||||||
|
);
|
||||||
|
|
||||||
|
let left_blinker_button = pins.gpio16.into_pull_up_input();
|
||||||
|
let right_blinker_button = pins.gpio17.into_pull_up_input();
|
||||||
|
let previous_animation_button = pins.gpio27.into_pull_up_input();
|
||||||
|
let next_animation_button = pins.gpio26.into_pull_up_input();
|
||||||
|
let brake_sensor = pins.gpio18.into_pull_up_input();
|
||||||
|
|
||||||
|
let mut led_pin = pins.led.into_push_pull_output();
|
||||||
|
|
||||||
|
let ui = BikeUI::new(
|
||||||
|
spi,
|
||||||
|
left_blinker_button,
|
||||||
|
right_blinker_button,
|
||||||
|
previous_animation_button,
|
||||||
|
next_animation_button,
|
||||||
|
brake_sensor,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut app = App::new(Box::new(ui));
|
||||||
|
|
||||||
|
led_pin.set_high();
|
||||||
|
|
||||||
|
let mut time = Instant::default();
|
||||||
|
let delay_ms = 1000 / (FPS as u32);
|
||||||
|
loop {
|
||||||
|
app.tick(time);
|
||||||
|
|
||||||
|
delay.delay_ms(delay_ms);
|
||||||
|
time = time + Instant(delay_ms.into());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,158 @@
|
||||||
|
$fn = 50;
|
||||||
|
threshold = 0.1;
|
||||||
|
half_threshold = threshold / 2;
|
||||||
|
bevel = 0.5;
|
||||||
|
|
||||||
|
wire_radius = 1;
|
||||||
|
|
||||||
|
wall_thickness = 2;
|
||||||
|
cutout_threshold = 1;
|
||||||
|
|
||||||
|
battery_length = 71;
|
||||||
|
battery_width = 18.75;
|
||||||
|
|
||||||
|
cell_holder_length = battery_length + wall_thickness * 2;
|
||||||
|
cell_holder_width = battery_width + wall_thickness * 2;
|
||||||
|
cell_holder_height = battery_width + wall_thickness;
|
||||||
|
|
||||||
|
battery_contact_thickness = .6;
|
||||||
|
// battery_contact_thickness = 1;
|
||||||
|
battery_contact_width = 11;
|
||||||
|
battery_contact_length = 12.8;
|
||||||
|
battery_contact_spring_height = 10.5;
|
||||||
|
battery_contact_flange_height = 1.9;
|
||||||
|
|
||||||
|
converter_width = 11.25;
|
||||||
|
converter_length = 22.25;
|
||||||
|
converter_height = 5;
|
||||||
|
|
||||||
|
|
||||||
|
include <./common.scad>;
|
||||||
|
|
||||||
|
// box(20, 10, 10);
|
||||||
|
// color("blue", 0.5) cube([10, 20, 10], center = true);
|
||||||
|
|
||||||
|
module cell_cradle(width, height) {
|
||||||
|
difference() {
|
||||||
|
translate([0, 0, -height / 2]) cube([width,
|
||||||
|
wall_thickness,
|
||||||
|
height],
|
||||||
|
center = true);
|
||||||
|
color("red", 1) translate([0, 0, 0])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
cylinder(h = wall_thickness + cutout_threshold,
|
||||||
|
r = width / 2,
|
||||||
|
center = true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module cell_box() {
|
||||||
|
union() {
|
||||||
|
channel(cell_holder_length, cell_holder_width, cell_holder_height);
|
||||||
|
translate([0, -battery_length / 6, wall_thickness]) cell_cradle(cell_holder_width, cell_holder_height / 2);
|
||||||
|
translate([0, battery_length / 6, wall_thickness]) cell_cradle(cell_holder_width, cell_holder_height / 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module contact_box() {
|
||||||
|
contact_thickness = battery_contact_flange_height * .75;
|
||||||
|
cutout_width = battery_contact_width * .8;
|
||||||
|
// box_thickness = contact_thickness_ + wall_thickness * 2;
|
||||||
|
// box_height = width + wall_thickness;
|
||||||
|
|
||||||
|
difference() {
|
||||||
|
box(wall_thickness * 2 + contact_thickness, cell_holder_width, cell_holder_height);
|
||||||
|
translate([0, contact_thickness, wall_thickness * 2])
|
||||||
|
cube([battery_contact_width,
|
||||||
|
wall_thickness * 2,
|
||||||
|
battery_contact_length + threshold],
|
||||||
|
center = true);
|
||||||
|
|
||||||
|
color("red", 1) translate([0,
|
||||||
|
-(wall_thickness + contact_thickness + threshold) / 2,
|
||||||
|
cell_holder_height / 2])
|
||||||
|
cube([5, wall_thickness + threshold * 2, cell_holder_height], center = true);
|
||||||
|
|
||||||
|
translate([0,
|
||||||
|
-(wall_thickness + contact_thickness + threshold) / 2 - wire_radius,
|
||||||
|
0])
|
||||||
|
rotate([0, 90, 0])
|
||||||
|
cylinder(h = cell_holder_width, r = wire_radius, center = true);
|
||||||
|
|
||||||
|
color("green", 1) translate([-cell_holder_width / 2, 0, cell_holder_height / 2])
|
||||||
|
rotate([0, 90, 0])
|
||||||
|
cylinder(h = 5, r = contact_thickness / 2, center = true);
|
||||||
|
|
||||||
|
color("green", 1) translate([cell_holder_width / 2, 0, cell_holder_height / 2])
|
||||||
|
rotate([0, 90, 0])
|
||||||
|
cylinder(h = 5, r = contact_thickness / 2, center = true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module battery_slot() {
|
||||||
|
difference() {
|
||||||
|
union() {
|
||||||
|
translate([0, -cell_holder_length / 2, 0]) contact_box();
|
||||||
|
translate([0, wall_thickness, 0]) cell_box();
|
||||||
|
translate([0, cell_holder_length / 2 + wall_thickness * 2, 0])
|
||||||
|
rotate([0, 0, 180])
|
||||||
|
contact_box();
|
||||||
|
}
|
||||||
|
translate([cell_holder_width / 2, 1, 0]) rotate([90, 0, 0]) cylinder(h = cell_holder_length + wall_thickness * 4 + battery_contact_flange_height * 2, r = wire_radius, center = true);
|
||||||
|
translate([-cell_holder_width / 2, 1, 0]) rotate([90, 0, 0]) cylinder(h = cell_holder_length + wall_thickness * 4 + battery_contact_flange_height * 2, r = wire_radius, center = true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module converter_box() {
|
||||||
|
box_length = wall_thickness * 2 + converter_height;
|
||||||
|
box_width = cell_holder_width * 2 - wall_thickness;
|
||||||
|
difference() {
|
||||||
|
box(box_length, box_width, cell_holder_height);
|
||||||
|
|
||||||
|
translate([cell_holder_width - wire_radius, 0, 0])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
cylinder(h = box_length, r = wire_radius, center = true);
|
||||||
|
translate([cell_holder_width - wire_radius * 2, 0, 0])
|
||||||
|
rotate([0, 90, 0])
|
||||||
|
cylinder(h = wall_thickness + threshold, r = wire_radius, center = true);
|
||||||
|
|
||||||
|
translate([-cell_holder_width + wire_radius, 0, 0])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
cylinder(h = box_length, r = wire_radius, center = true);
|
||||||
|
translate([-cell_holder_width + wire_radius * 2, 0, 0])
|
||||||
|
rotate([0, 90, 0])
|
||||||
|
cylinder(h = wall_thickness + threshold, r = wire_radius, center = true);
|
||||||
|
|
||||||
|
translate([0, -box_length / 2, 0])
|
||||||
|
rotate([0, 90, 0])
|
||||||
|
cylinder(h = cell_holder_width * 2 + wall_thickness, r = wire_radius, center = true);
|
||||||
|
|
||||||
|
translate([-cell_holder_width * .75, (-box_length + wall_thickness) / 2, 0])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
cylinder(h = wall_thickness * 2, r = wire_radius, center = true);
|
||||||
|
|
||||||
|
translate([cell_holder_width * .75, (-box_length + wall_thickness) / 2, 0])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
cylinder(h = wall_thickness * 2, r = wire_radius, center = true);
|
||||||
|
|
||||||
|
color("red", 1) translate([-box_width / 4, -(converter_height + wall_thickness) / 2, cell_holder_height / 2])
|
||||||
|
cube([5, wall_thickness + threshold * 2, cell_holder_height], center = true);
|
||||||
|
color("red", 1) translate([box_width / 4, -(converter_height + wall_thickness) / 2, cell_holder_height / 2])
|
||||||
|
cube([5, wall_thickness + threshold * 2, cell_holder_height], center = true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module battery_case() {
|
||||||
|
union() {
|
||||||
|
translate([-cell_holder_width / 2, 0, 0]) battery_slot();
|
||||||
|
translate([cell_holder_width / 2 - wall_thickness, 0, 0]) battery_slot();
|
||||||
|
translate([-wall_thickness / 2,
|
||||||
|
cell_holder_length / 2 + wall_thickness * 2 + battery_contact_flange_height + wall_thickness * 2 + wall_thickness / 2,
|
||||||
|
0])
|
||||||
|
converter_box();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
battery_case();
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
module pill(length, bevel) {
|
||||||
|
hull() {
|
||||||
|
translate([0, 0, (-length / 2) + bevel]) sphere(r = bevel);
|
||||||
|
translate([0, 0, (length / 2) - bevel]) sphere(r = bevel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module box_face(dimensions, bevel = 0) {
|
||||||
|
x = dimensions[0];
|
||||||
|
y = dimensions[1];
|
||||||
|
z = dimensions[2];
|
||||||
|
|
||||||
|
if (bevel > 0) {
|
||||||
|
translate([0, 0, z / 2])
|
||||||
|
hull() {
|
||||||
|
pill(z, bevel);
|
||||||
|
translate([x, 0, 0])
|
||||||
|
pill(z, bevel);
|
||||||
|
translate([x, y, 0])
|
||||||
|
pill(z, bevel);
|
||||||
|
translate([0, y, 0])
|
||||||
|
pill(z, bevel);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cube(dimensions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module channel(length, width, height, bevel) {
|
||||||
|
union() {
|
||||||
|
box_face([length, width, wall_thickness], bevel);
|
||||||
|
|
||||||
|
translate([0, wall_thickness - bevel, bevel])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
box_face([length, height, wall_thickness], bevel);
|
||||||
|
|
||||||
|
translate([0, width + bevel, bevel])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
box_face([length, height, wall_thickness], bevel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module box(length, width, height, bevel = 0) {
|
||||||
|
union() {
|
||||||
|
channel(length, width, height, bevel);
|
||||||
|
|
||||||
|
translate([-bevel, 0, bevel])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
rotate([0, 90, 0])
|
||||||
|
box_face([width, height, wall_thickness], bevel);
|
||||||
|
translate([length - wall_thickness + bevel, 0, bevel])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
rotate([0, 90, 0])
|
||||||
|
box_face([width, height, wall_thickness], bevel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,95 @@
|
||||||
|
$fn = 50;
|
||||||
|
threshold = 0.1;
|
||||||
|
|
||||||
|
board_length = 92;
|
||||||
|
board_width = 72;
|
||||||
|
board_height = 21.5;
|
||||||
|
wall_thickness = 2;
|
||||||
|
bevel = 0.5;
|
||||||
|
|
||||||
|
hinge_radius = 2.5;
|
||||||
|
|
||||||
|
case_width = board_width + wall_thickness * 2;
|
||||||
|
case_length = board_length + wall_thickness * 2;
|
||||||
|
case_height = board_height + wall_thickness;
|
||||||
|
|
||||||
|
include <./common.scad>;
|
||||||
|
|
||||||
|
module hinge(length) {
|
||||||
|
difference() {
|
||||||
|
union() {
|
||||||
|
cylinder(h = length, r = hinge_radius);
|
||||||
|
translate([0, -hinge_radius, 0])
|
||||||
|
cube([hinge_radius, hinge_radius * 2, length]);
|
||||||
|
}
|
||||||
|
translate([0, 0, -threshold / 2]) cylinder(h = length + threshold, r = 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module main_case() {
|
||||||
|
hinge_length = board_length / 4;
|
||||||
|
hinge_y_offset = board_width + wall_thickness + hinge_radius;
|
||||||
|
hinge_z_offset = board_height;
|
||||||
|
|
||||||
|
difference() {
|
||||||
|
union() {
|
||||||
|
box(case_length,
|
||||||
|
case_width,
|
||||||
|
case_height,
|
||||||
|
bevel);
|
||||||
|
|
||||||
|
translate([0, -hinge_radius - bevel + threshold, hinge_z_offset + bevel])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
rotate([0, 90, 0])
|
||||||
|
hinge(case_length / 4);
|
||||||
|
|
||||||
|
translate([case_length - hinge_length, -hinge_radius - bevel + threshold, hinge_z_offset + bevel])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
rotate([0, 90, 0])
|
||||||
|
hinge(case_length / 4);
|
||||||
|
|
||||||
|
translate([43, case_width, wall_thickness + 8])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
rotate([0, 180, 0])
|
||||||
|
linear_extrude(1)
|
||||||
|
text("lights", size = 3);
|
||||||
|
|
||||||
|
translate([67, case_width, wall_thickness + 8])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
rotate([0, 180, 0])
|
||||||
|
linear_extrude(1)
|
||||||
|
text("left", size = 3);
|
||||||
|
|
||||||
|
translate([55, case_width, wall_thickness + 8])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
rotate([0, 180, 0])
|
||||||
|
linear_extrude(1)
|
||||||
|
text("right", size = 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
color("green", 1) translate([8.5 + wall_thickness, case_width - wall_thickness - threshold, wall_thickness])
|
||||||
|
cube([60, wall_thickness * 2, 7]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module lid() {
|
||||||
|
lid_width = case_width + hinge_radius * 2 + wall_thickness;
|
||||||
|
union() {
|
||||||
|
difference() {
|
||||||
|
box_face([case_length,
|
||||||
|
lid_width,
|
||||||
|
wall_thickness],
|
||||||
|
bevel);
|
||||||
|
translate([(case_length - 60) / 2, 14 + hinge_radius * 2, -threshold / 2])
|
||||||
|
cube([60, 16, wall_thickness + threshold]);
|
||||||
|
}
|
||||||
|
|
||||||
|
translate([case_length / 4 + 1, hinge_radius - 0.4, -hinge_radius])
|
||||||
|
rotate([180, 0, 0])
|
||||||
|
rotate([0, 90, 0]) hinge(case_length / 2 - 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// main_case();
|
||||||
|
// color("red", 1) translate([0, 0, case_height + wall_thickness / 2]) lid();
|
||||||
|
// color("red", 1) translate([0, 0, 40]) lid();
|
|
@ -0,0 +1,4 @@
|
||||||
|
|
||||||
|
include <./control_panel.scad>
|
||||||
|
|
||||||
|
main_case();
|
|
@ -0,0 +1,5 @@
|
||||||
|
|
||||||
|
include <./control_panel.scad>
|
||||||
|
|
||||||
|
lid();
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
[package]
|
||||||
|
name = "lights-core"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
az = { version = "1" }
|
||||||
|
fixed = { version = "1" }
|
|
@ -0,0 +1,481 @@
|
||||||
|
#![no_std]
|
||||||
|
|
||||||
|
extern crate alloc;
|
||||||
|
use alloc::boxed::Box;
|
||||||
|
use az::*;
|
||||||
|
use core::{
|
||||||
|
clone::Clone,
|
||||||
|
cmp::PartialEq,
|
||||||
|
default::Default,
|
||||||
|
ops::{Add, Sub},
|
||||||
|
option::Option,
|
||||||
|
};
|
||||||
|
use fixed::types::{I48F16, I8F8, U128F0, U16F0};
|
||||||
|
|
||||||
|
mod patterns;
|
||||||
|
pub use patterns::*;
|
||||||
|
|
||||||
|
mod types;
|
||||||
|
pub use types::{BodyPattern, DashboardPattern, RGB};
|
||||||
|
|
||||||
|
fn calculate_frames(starting_time: U128F0, now: U128F0) -> U16F0 {
|
||||||
|
let frames_128 = (now - starting_time) / U128F0::from(FPS);
|
||||||
|
(frames_128 % U128F0::from(U16F0::MAX)).cast()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calculate_slope(start: I8F8, end: I8F8, frames: U16F0) -> I8F8 {
|
||||||
|
let slope_i16f16 = (I48F16::from(end) - I48F16::from(start)) / I48F16::from(frames);
|
||||||
|
slope_i16f16.saturating_as()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn linear_ease(value: I8F8, frames: U16F0, slope: I8F8) -> I8F8 {
|
||||||
|
let value_i16f16 = I48F16::from(value) + I48F16::from(frames) * I48F16::from(slope);
|
||||||
|
value_i16f16.saturating_as()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialOrd, Ord, PartialEq, Eq)]
|
||||||
|
pub struct Instant(pub U128F0);
|
||||||
|
|
||||||
|
impl Default for Instant {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(U128F0::from(0 as u8))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Add for Instant {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn add(self, r: Self) -> Self::Output {
|
||||||
|
Self(self.0 + r.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sub for Instant {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn sub(self, r: Self) -> Self::Output {
|
||||||
|
Self(self.0 - r.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const FPS: u8 = 30;
|
||||||
|
|
||||||
|
pub trait UI {
|
||||||
|
fn check_event(&mut self, current_time: Instant) -> Option<Event>;
|
||||||
|
fn update_lights(&self, dashboard_lights: DashboardPattern, body_lights: BodyPattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Animation {
|
||||||
|
fn tick(&mut self, time: Instant) -> (DashboardPattern, BodyPattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
pub struct DefaultAnimation {}
|
||||||
|
|
||||||
|
impl Animation for DefaultAnimation {
|
||||||
|
fn tick(&mut self, _: Instant) -> (DashboardPattern, BodyPattern) {
|
||||||
|
(WATER_DASHBOARD, WATER_BODY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
pub struct Fade {
|
||||||
|
starting_dashboard: DashboardPattern,
|
||||||
|
starting_lights: BodyPattern,
|
||||||
|
|
||||||
|
start_time: Instant,
|
||||||
|
dashboard_slope: [RGB<I8F8>; 3],
|
||||||
|
body_slope: [RGB<I8F8>; 60],
|
||||||
|
frames: U16F0,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Fade {
|
||||||
|
fn new(
|
||||||
|
dashboard: DashboardPattern,
|
||||||
|
lights: BodyPattern,
|
||||||
|
ending_dashboard: DashboardPattern,
|
||||||
|
ending_lights: BodyPattern,
|
||||||
|
frames: U16F0,
|
||||||
|
time: Instant,
|
||||||
|
) -> Self {
|
||||||
|
let mut dashboard_slope = [Default::default(); 3];
|
||||||
|
let mut body_slope = [Default::default(); 60];
|
||||||
|
for i in 0..3 {
|
||||||
|
let slope = RGB {
|
||||||
|
r: calculate_slope(dashboard[i].r, ending_dashboard[i].r, frames),
|
||||||
|
g: calculate_slope(dashboard[i].g, ending_dashboard[i].g, frames),
|
||||||
|
b: calculate_slope(dashboard[i].b, ending_dashboard[i].b, frames),
|
||||||
|
};
|
||||||
|
dashboard_slope[i] = slope;
|
||||||
|
}
|
||||||
|
|
||||||
|
for i in 0..60 {
|
||||||
|
let slope = RGB {
|
||||||
|
r: calculate_slope(lights[i].r, ending_lights[i].r, frames),
|
||||||
|
g: calculate_slope(lights[i].g, ending_lights[i].g, frames),
|
||||||
|
b: calculate_slope(lights[i].b, ending_lights[i].b, frames),
|
||||||
|
};
|
||||||
|
body_slope[i] = slope;
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
starting_dashboard: dashboard,
|
||||||
|
starting_lights: lights,
|
||||||
|
start_time: time,
|
||||||
|
dashboard_slope,
|
||||||
|
body_slope,
|
||||||
|
frames,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Animation for Fade {
|
||||||
|
fn tick(&mut self, time: Instant) -> (DashboardPattern, BodyPattern) {
|
||||||
|
let mut frames = calculate_frames(self.start_time.0, time.0);
|
||||||
|
if frames > self.frames {
|
||||||
|
frames = self.frames
|
||||||
|
}
|
||||||
|
let mut dashboard_pattern: DashboardPattern = OFF_DASHBOARD;
|
||||||
|
let mut body_pattern: BodyPattern = OFF_BODY;
|
||||||
|
|
||||||
|
for i in 0..3 {
|
||||||
|
dashboard_pattern[i].r = linear_ease(
|
||||||
|
self.starting_dashboard[i].r,
|
||||||
|
frames,
|
||||||
|
self.dashboard_slope[i].r,
|
||||||
|
);
|
||||||
|
dashboard_pattern[i].g = linear_ease(
|
||||||
|
self.starting_dashboard[i].g,
|
||||||
|
frames,
|
||||||
|
self.dashboard_slope[i].g,
|
||||||
|
);
|
||||||
|
dashboard_pattern[i].b = linear_ease(
|
||||||
|
self.starting_dashboard[i].b,
|
||||||
|
frames,
|
||||||
|
self.dashboard_slope[i].b,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for i in 0..60 {
|
||||||
|
body_pattern[i].r =
|
||||||
|
linear_ease(self.starting_lights[i].r, frames, self.body_slope[i].r);
|
||||||
|
body_pattern[i].g =
|
||||||
|
linear_ease(self.starting_lights[i].g, frames, self.body_slope[i].g);
|
||||||
|
body_pattern[i].b =
|
||||||
|
linear_ease(self.starting_lights[i].b, frames, self.body_slope[i].b);
|
||||||
|
}
|
||||||
|
|
||||||
|
(dashboard_pattern, body_pattern)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum FadeDirection {
|
||||||
|
Transition,
|
||||||
|
FadeIn,
|
||||||
|
FadeOut,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum BlinkerDirection {
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Blinker {
|
||||||
|
transition: Fade,
|
||||||
|
fade_in: Fade,
|
||||||
|
fade_out: Fade,
|
||||||
|
direction: FadeDirection,
|
||||||
|
|
||||||
|
start_time: Instant,
|
||||||
|
frames: U16F0,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Blinker {
|
||||||
|
fn new(
|
||||||
|
starting_dashboard: DashboardPattern,
|
||||||
|
starting_body: BodyPattern,
|
||||||
|
direction: BlinkerDirection,
|
||||||
|
time: Instant,
|
||||||
|
) -> Self {
|
||||||
|
let mut ending_dashboard = OFF_DASHBOARD.clone();
|
||||||
|
|
||||||
|
match direction {
|
||||||
|
BlinkerDirection::Left => {
|
||||||
|
ending_dashboard[0].r = LEFT_BLINKER_DASHBOARD[0].r;
|
||||||
|
ending_dashboard[0].g = LEFT_BLINKER_DASHBOARD[0].g;
|
||||||
|
ending_dashboard[0].b = LEFT_BLINKER_DASHBOARD[0].b;
|
||||||
|
}
|
||||||
|
BlinkerDirection::Right => {
|
||||||
|
ending_dashboard[2].r = RIGHT_BLINKER_DASHBOARD[2].r;
|
||||||
|
ending_dashboard[2].g = RIGHT_BLINKER_DASHBOARD[2].g;
|
||||||
|
ending_dashboard[2].b = RIGHT_BLINKER_DASHBOARD[2].b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut ending_body = OFF_BODY.clone();
|
||||||
|
match direction {
|
||||||
|
BlinkerDirection::Left => {
|
||||||
|
for i in 0..30 {
|
||||||
|
ending_body[i].r = LEFT_BLINKER_BODY[i].r;
|
||||||
|
ending_body[i].g = LEFT_BLINKER_BODY[i].g;
|
||||||
|
ending_body[i].b = LEFT_BLINKER_BODY[i].b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BlinkerDirection::Right => {
|
||||||
|
for i in 30..60 {
|
||||||
|
ending_body[i].r = RIGHT_BLINKER_BODY[i].r;
|
||||||
|
ending_body[i].g = RIGHT_BLINKER_BODY[i].g;
|
||||||
|
ending_body[i].b = RIGHT_BLINKER_BODY[i].b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Blinker {
|
||||||
|
transition: Fade::new(
|
||||||
|
starting_dashboard.clone(),
|
||||||
|
starting_body.clone(),
|
||||||
|
ending_dashboard.clone(),
|
||||||
|
ending_body.clone(),
|
||||||
|
BLINKER_FRAMES,
|
||||||
|
time,
|
||||||
|
),
|
||||||
|
fade_in: Fade::new(
|
||||||
|
OFF_DASHBOARD.clone(),
|
||||||
|
OFF_BODY.clone(),
|
||||||
|
ending_dashboard.clone(),
|
||||||
|
ending_body.clone(),
|
||||||
|
BLINKER_FRAMES,
|
||||||
|
time,
|
||||||
|
),
|
||||||
|
fade_out: Fade::new(
|
||||||
|
ending_dashboard.clone(),
|
||||||
|
ending_body.clone(),
|
||||||
|
OFF_DASHBOARD.clone(),
|
||||||
|
OFF_BODY.clone(),
|
||||||
|
BLINKER_FRAMES,
|
||||||
|
time,
|
||||||
|
),
|
||||||
|
direction: FadeDirection::Transition,
|
||||||
|
start_time: time,
|
||||||
|
frames: BLINKER_FRAMES,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Animation for Blinker {
|
||||||
|
fn tick(&mut self, time: Instant) -> (DashboardPattern, BodyPattern) {
|
||||||
|
let frames = calculate_frames(self.start_time.0, time.0);
|
||||||
|
if frames > self.frames {
|
||||||
|
match self.direction {
|
||||||
|
FadeDirection::Transition => {
|
||||||
|
self.direction = FadeDirection::FadeOut;
|
||||||
|
self.fade_out.start_time = time;
|
||||||
|
}
|
||||||
|
FadeDirection::FadeIn => {
|
||||||
|
self.direction = FadeDirection::FadeOut;
|
||||||
|
self.fade_out.start_time = time;
|
||||||
|
}
|
||||||
|
FadeDirection::FadeOut => {
|
||||||
|
self.direction = FadeDirection::FadeIn;
|
||||||
|
self.fade_in.start_time = time;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.start_time = time;
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.direction {
|
||||||
|
FadeDirection::Transition => self.transition.tick(time),
|
||||||
|
FadeDirection::FadeIn => self.fade_in.tick(time),
|
||||||
|
FadeDirection::FadeOut => self.fade_out.tick(time),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum Event {
|
||||||
|
Brake,
|
||||||
|
BrakeRelease,
|
||||||
|
LeftBlinker,
|
||||||
|
NextPattern,
|
||||||
|
PreviousPattern,
|
||||||
|
RightBlinker,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum Pattern {
|
||||||
|
Water,
|
||||||
|
GayPride,
|
||||||
|
TransPride,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Pattern {
|
||||||
|
fn previous(&self) -> Pattern {
|
||||||
|
match self {
|
||||||
|
Pattern::Water => Pattern::TransPride,
|
||||||
|
Pattern::GayPride => Pattern::Water,
|
||||||
|
Pattern::TransPride => Pattern::GayPride,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next(&self) -> Pattern {
|
||||||
|
match self {
|
||||||
|
Pattern::Water => Pattern::GayPride,
|
||||||
|
Pattern::GayPride => Pattern::TransPride,
|
||||||
|
Pattern::TransPride => Pattern::Water,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dashboard(&self) -> DashboardPattern {
|
||||||
|
match self {
|
||||||
|
Pattern::Water => WATER_DASHBOARD,
|
||||||
|
Pattern::GayPride => PRIDE_DASHBOARD,
|
||||||
|
Pattern::TransPride => TRANS_PRIDE_DASHBOARD,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn body(&self) -> BodyPattern {
|
||||||
|
match self {
|
||||||
|
Pattern::Water => OFF_BODY,
|
||||||
|
Pattern::GayPride => PRIDE_BODY,
|
||||||
|
Pattern::TransPride => TRANS_PRIDE_BODY,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub enum State {
|
||||||
|
Pattern(Pattern),
|
||||||
|
Brake,
|
||||||
|
LeftBlinker,
|
||||||
|
RightBlinker,
|
||||||
|
BrakeLeftBlinker,
|
||||||
|
BrakeRightBlinker,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct App {
|
||||||
|
ui: Box<dyn UI>,
|
||||||
|
state: State,
|
||||||
|
home_pattern: Pattern,
|
||||||
|
current_animation: Box<dyn Animation>,
|
||||||
|
dashboard_lights: DashboardPattern,
|
||||||
|
lights: BodyPattern,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
pub fn new(ui: Box<dyn UI>) -> Self {
|
||||||
|
let pattern = Pattern::Water;
|
||||||
|
Self {
|
||||||
|
ui,
|
||||||
|
state: State::Pattern(pattern),
|
||||||
|
home_pattern: pattern,
|
||||||
|
current_animation: Box::new(Fade::new(
|
||||||
|
OFF_DASHBOARD,
|
||||||
|
OFF_BODY,
|
||||||
|
pattern.dashboard(),
|
||||||
|
pattern.body(),
|
||||||
|
DEFAULT_FRAMES,
|
||||||
|
Instant((0 as u32).into()),
|
||||||
|
)),
|
||||||
|
dashboard_lights: OFF_DASHBOARD,
|
||||||
|
lights: OFF_BODY,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_animation(&mut self, time: Instant) {
|
||||||
|
match self.state {
|
||||||
|
State::Pattern(ref pattern) => {
|
||||||
|
self.current_animation = Box::new(Fade::new(
|
||||||
|
self.dashboard_lights.clone(),
|
||||||
|
self.lights.clone(),
|
||||||
|
pattern.dashboard(),
|
||||||
|
pattern.body(),
|
||||||
|
DEFAULT_FRAMES,
|
||||||
|
time,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
State::Brake => {
|
||||||
|
self.current_animation = Box::new(Fade::new(
|
||||||
|
self.dashboard_lights.clone(),
|
||||||
|
self.lights.clone(),
|
||||||
|
BRAKES_DASHBOARD,
|
||||||
|
BRAKES_BODY,
|
||||||
|
BRAKES_FRAMES,
|
||||||
|
time,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
State::LeftBlinker => {
|
||||||
|
self.current_animation = Box::new(Blinker::new(
|
||||||
|
self.dashboard_lights.clone(),
|
||||||
|
self.lights.clone(),
|
||||||
|
BlinkerDirection::Left,
|
||||||
|
time,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
State::RightBlinker => {
|
||||||
|
self.current_animation = Box::new(Blinker::new(
|
||||||
|
self.dashboard_lights.clone(),
|
||||||
|
self.lights.clone(),
|
||||||
|
BlinkerDirection::Right,
|
||||||
|
time,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
State::BrakeLeftBlinker => (),
|
||||||
|
State::BrakeRightBlinker => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_state(&mut self, event: Event) {
|
||||||
|
match event {
|
||||||
|
Event::Brake => {
|
||||||
|
if self.state == State::Brake {
|
||||||
|
self.state = State::Pattern(self.home_pattern);
|
||||||
|
} else {
|
||||||
|
self.state = State::Brake;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::BrakeRelease => self.state = State::Pattern(self.home_pattern),
|
||||||
|
Event::LeftBlinker => match self.state {
|
||||||
|
State::Brake => self.state = State::BrakeLeftBlinker,
|
||||||
|
State::BrakeLeftBlinker => self.state = State::Brake,
|
||||||
|
State::LeftBlinker => self.state = State::Pattern(self.home_pattern),
|
||||||
|
_ => self.state = State::LeftBlinker,
|
||||||
|
},
|
||||||
|
Event::NextPattern => match self.state {
|
||||||
|
State::Pattern(ref pattern) => {
|
||||||
|
self.home_pattern = pattern.next();
|
||||||
|
self.state = State::Pattern(self.home_pattern);
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
},
|
||||||
|
Event::PreviousPattern => match self.state {
|
||||||
|
State::Pattern(ref pattern) => {
|
||||||
|
self.home_pattern = pattern.previous();
|
||||||
|
self.state = State::Pattern(self.home_pattern);
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
},
|
||||||
|
Event::RightBlinker => match self.state {
|
||||||
|
State::Brake => self.state = State::BrakeRightBlinker,
|
||||||
|
State::BrakeRightBlinker => self.state = State::Brake,
|
||||||
|
State::RightBlinker => self.state = State::Pattern(self.home_pattern),
|
||||||
|
_ => self.state = State::RightBlinker,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tick(&mut self, time: Instant) {
|
||||||
|
match self.ui.check_event(time) {
|
||||||
|
Some(event) => {
|
||||||
|
self.update_state(event);
|
||||||
|
self.update_animation(time);
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
};
|
||||||
|
|
||||||
|
let (dashboard, lights) = self.current_animation.tick(time);
|
||||||
|
self.dashboard_lights = dashboard.clone();
|
||||||
|
self.lights = lights.clone();
|
||||||
|
self.ui.update_lights(dashboard, lights);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,333 @@
|
||||||
|
use crate::{BodyPattern, DashboardPattern, RGB};
|
||||||
|
use fixed::types::{I8F8, U16F0};
|
||||||
|
|
||||||
|
pub const RGB_OFF: RGB<I8F8> = RGB {
|
||||||
|
r: I8F8::lit("0"),
|
||||||
|
g: I8F8::lit("0"),
|
||||||
|
b: I8F8::lit("0"),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const RGB_WHITE: RGB<I8F8> = RGB {
|
||||||
|
r: I8F8::lit("1"),
|
||||||
|
g: I8F8::lit("1"),
|
||||||
|
b: I8F8::lit("1"),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const BRAKES_RED: RGB<I8F8> = RGB {
|
||||||
|
r: I8F8::lit("1"),
|
||||||
|
g: I8F8::lit("0"),
|
||||||
|
b: I8F8::lit("0"),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const BLINKER_AMBER: RGB<I8F8> = RGB {
|
||||||
|
r: I8F8::lit("1"),
|
||||||
|
g: I8F8::lit("0.15"),
|
||||||
|
b: I8F8::lit("0"),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const PRIDE_RED: RGB<I8F8> = RGB {
|
||||||
|
r: I8F8::lit("0.95"),
|
||||||
|
g: I8F8::lit("0.00"),
|
||||||
|
b: I8F8::lit("0.00"),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const PRIDE_ORANGE: RGB<I8F8> = RGB {
|
||||||
|
r: I8F8::lit("1.0"),
|
||||||
|
g: I8F8::lit("0.25"),
|
||||||
|
b: I8F8::lit("0"),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const PRIDE_YELLOW: RGB<I8F8> = RGB {
|
||||||
|
r: I8F8::lit("1.0"),
|
||||||
|
g: I8F8::lit("0.85"),
|
||||||
|
b: I8F8::lit("0"),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const PRIDE_GREEN: RGB<I8F8> = RGB {
|
||||||
|
r: I8F8::lit("0"),
|
||||||
|
g: I8F8::lit("0.95"),
|
||||||
|
b: I8F8::lit("0.05"),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const PRIDE_INDIGO: RGB<I8F8> = RGB {
|
||||||
|
r: I8F8::lit("0.04"),
|
||||||
|
g: I8F8::lit("0.15"),
|
||||||
|
b: I8F8::lit("0.55"),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const PRIDE_VIOLET: RGB<I8F8> = RGB {
|
||||||
|
r: I8F8::lit("0.75"),
|
||||||
|
g: I8F8::lit("0.0"),
|
||||||
|
b: I8F8::lit("0.80"),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const TRANS_BLUE: RGB<I8F8> = RGB {
|
||||||
|
r: I8F8::lit("0.06"),
|
||||||
|
g: I8F8::lit("0.41"),
|
||||||
|
b: I8F8::lit("0.98"),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const TRANS_PINK: RGB<I8F8> = RGB {
|
||||||
|
r: I8F8::lit("0.96"),
|
||||||
|
g: I8F8::lit("0.16"),
|
||||||
|
b: I8F8::lit("0.32"),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const WATER_1: RGB<I8F8> = RGB {
|
||||||
|
r: I8F8::lit("0.0"),
|
||||||
|
g: I8F8::lit("0.0"),
|
||||||
|
b: I8F8::lit("0.75"),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const WATER_2: RGB<I8F8> = RGB {
|
||||||
|
r: I8F8::lit("0.8"),
|
||||||
|
g: I8F8::lit("0.8"),
|
||||||
|
b: I8F8::lit("0.8"),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const WATER_3: RGB<I8F8> = RGB {
|
||||||
|
r: I8F8::lit("0.00"),
|
||||||
|
g: I8F8::lit("0.75"),
|
||||||
|
b: I8F8::lit("0.75"),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const OFF_DASHBOARD: DashboardPattern = [RGB_OFF; 3];
|
||||||
|
pub const OFF_BODY: BodyPattern = [RGB_OFF; 60];
|
||||||
|
|
||||||
|
pub const DEFAULT_FRAMES: U16F0 = U16F0::lit("30");
|
||||||
|
|
||||||
|
pub const WATER_DASHBOARD: DashboardPattern = [WATER_1, WATER_2, WATER_3];
|
||||||
|
|
||||||
|
pub const WATER_BODY: BodyPattern = [RGB_OFF; 60];
|
||||||
|
|
||||||
|
pub const PRIDE_DASHBOARD: DashboardPattern = [PRIDE_RED, PRIDE_GREEN, PRIDE_INDIGO];
|
||||||
|
|
||||||
|
pub const PRIDE_BODY: BodyPattern = [
|
||||||
|
// Left Side
|
||||||
|
// Red
|
||||||
|
PRIDE_RED,
|
||||||
|
PRIDE_RED,
|
||||||
|
PRIDE_RED,
|
||||||
|
PRIDE_RED,
|
||||||
|
PRIDE_RED,
|
||||||
|
// Orange
|
||||||
|
PRIDE_ORANGE,
|
||||||
|
PRIDE_ORANGE,
|
||||||
|
PRIDE_ORANGE,
|
||||||
|
PRIDE_ORANGE,
|
||||||
|
PRIDE_ORANGE,
|
||||||
|
// Yellow
|
||||||
|
PRIDE_YELLOW,
|
||||||
|
PRIDE_YELLOW,
|
||||||
|
PRIDE_YELLOW,
|
||||||
|
PRIDE_YELLOW,
|
||||||
|
PRIDE_YELLOW,
|
||||||
|
// Green
|
||||||
|
PRIDE_GREEN,
|
||||||
|
PRIDE_GREEN,
|
||||||
|
PRIDE_GREEN,
|
||||||
|
PRIDE_GREEN,
|
||||||
|
PRIDE_GREEN,
|
||||||
|
// Indigo
|
||||||
|
PRIDE_INDIGO,
|
||||||
|
PRIDE_INDIGO,
|
||||||
|
PRIDE_INDIGO,
|
||||||
|
PRIDE_INDIGO,
|
||||||
|
PRIDE_INDIGO,
|
||||||
|
// Violet
|
||||||
|
PRIDE_VIOLET,
|
||||||
|
PRIDE_VIOLET,
|
||||||
|
PRIDE_VIOLET,
|
||||||
|
PRIDE_VIOLET,
|
||||||
|
PRIDE_VIOLET,
|
||||||
|
// Right Side
|
||||||
|
// Violet
|
||||||
|
PRIDE_VIOLET,
|
||||||
|
PRIDE_VIOLET,
|
||||||
|
PRIDE_VIOLET,
|
||||||
|
PRIDE_VIOLET,
|
||||||
|
PRIDE_VIOLET,
|
||||||
|
// Indigo
|
||||||
|
PRIDE_INDIGO,
|
||||||
|
PRIDE_INDIGO,
|
||||||
|
PRIDE_INDIGO,
|
||||||
|
PRIDE_INDIGO,
|
||||||
|
PRIDE_INDIGO,
|
||||||
|
// Green
|
||||||
|
PRIDE_GREEN,
|
||||||
|
PRIDE_GREEN,
|
||||||
|
PRIDE_GREEN,
|
||||||
|
PRIDE_GREEN,
|
||||||
|
PRIDE_GREEN,
|
||||||
|
// Yellow
|
||||||
|
PRIDE_YELLOW,
|
||||||
|
PRIDE_YELLOW,
|
||||||
|
PRIDE_YELLOW,
|
||||||
|
PRIDE_YELLOW,
|
||||||
|
PRIDE_YELLOW,
|
||||||
|
// Orange
|
||||||
|
PRIDE_ORANGE,
|
||||||
|
PRIDE_ORANGE,
|
||||||
|
PRIDE_ORANGE,
|
||||||
|
PRIDE_ORANGE,
|
||||||
|
PRIDE_ORANGE,
|
||||||
|
// Red
|
||||||
|
PRIDE_RED,
|
||||||
|
PRIDE_RED,
|
||||||
|
PRIDE_RED,
|
||||||
|
PRIDE_RED,
|
||||||
|
PRIDE_RED,
|
||||||
|
];
|
||||||
|
|
||||||
|
pub const TRANS_PRIDE_DASHBOARD: DashboardPattern = [TRANS_BLUE, RGB_WHITE, TRANS_PINK];
|
||||||
|
|
||||||
|
pub const TRANS_PRIDE_BODY: BodyPattern = [
|
||||||
|
// Left Side
|
||||||
|
TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_PINK, TRANS_PINK,
|
||||||
|
TRANS_PINK, TRANS_PINK, TRANS_PINK, TRANS_PINK, RGB_WHITE, RGB_WHITE, RGB_WHITE, RGB_WHITE,
|
||||||
|
RGB_WHITE, RGB_WHITE, TRANS_PINK, TRANS_PINK, TRANS_PINK, TRANS_PINK, TRANS_PINK, TRANS_PINK,
|
||||||
|
TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE,
|
||||||
|
// Right side
|
||||||
|
TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_PINK, TRANS_PINK,
|
||||||
|
TRANS_PINK, TRANS_PINK, TRANS_PINK, TRANS_PINK, RGB_WHITE, RGB_WHITE, RGB_WHITE, RGB_WHITE,
|
||||||
|
RGB_WHITE, RGB_WHITE, TRANS_PINK, TRANS_PINK, TRANS_PINK, TRANS_PINK, TRANS_PINK, TRANS_PINK,
|
||||||
|
TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE,
|
||||||
|
];
|
||||||
|
|
||||||
|
pub const BRAKES_FRAMES: U16F0 = U16F0::lit("15");
|
||||||
|
|
||||||
|
pub const BRAKES_DASHBOARD: DashboardPattern = [BRAKES_RED; 3];
|
||||||
|
|
||||||
|
pub const BRAKES_BODY: BodyPattern = [BRAKES_RED; 60];
|
||||||
|
|
||||||
|
pub const BLINKER_FRAMES: U16F0 = U16F0::lit("10");
|
||||||
|
|
||||||
|
pub const LEFT_BLINKER_DASHBOARD: DashboardPattern = [BLINKER_AMBER, RGB_OFF, RGB_OFF];
|
||||||
|
|
||||||
|
pub const LEFT_BLINKER_BODY: BodyPattern = [
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
];
|
||||||
|
|
||||||
|
pub const RIGHT_BLINKER_DASHBOARD: DashboardPattern = [RGB_OFF, RGB_OFF, BLINKER_AMBER];
|
||||||
|
|
||||||
|
pub const RIGHT_BLINKER_BODY: BodyPattern = [
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
RGB_OFF,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
BLINKER_AMBER,
|
||||||
|
];
|
|
@ -0,0 +1,17 @@
|
||||||
|
use core::default::Default;
|
||||||
|
use fixed::types::I8F8;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Default, Debug)]
|
||||||
|
pub struct RGB<T> {
|
||||||
|
pub r: T,
|
||||||
|
pub g: T,
|
||||||
|
pub b: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
const DASHBOARD_LIGHT_COUNT: usize = 3;
|
||||||
|
|
||||||
|
pub type DashboardPattern = [RGB<I8F8>; DASHBOARD_LIGHT_COUNT];
|
||||||
|
|
||||||
|
const BODY_LIGHT_COUNT: usize = 60;
|
||||||
|
|
||||||
|
pub type BodyPattern = [RGB<I8F8>; BODY_LIGHT_COUNT];
|
|
@ -0,0 +1,16 @@
|
||||||
|
[package]
|
||||||
|
name = "simulator"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
adw = { version = "0.5", package = "libadwaita", features = [ "v1_2" ] }
|
||||||
|
cairo-rs = { version = "0.18" }
|
||||||
|
fixed = { version = "1" }
|
||||||
|
gio = { version = "0.18" }
|
||||||
|
glib = { version = "0.18" }
|
||||||
|
gtk = { version = "0.7", package = "gtk4", features = [ "v4_8" ] }
|
||||||
|
lights-core = { path = "../core" }
|
||||||
|
pango = { version = "*" }
|
|
@ -0,0 +1,288 @@
|
||||||
|
use adw::prelude::*;
|
||||||
|
use fixed::types::{I8F8, U128F0};
|
||||||
|
use glib::{Object, Sender};
|
||||||
|
use gtk::subclass::prelude::*;
|
||||||
|
use lights_core::{
|
||||||
|
App, BodyPattern, DashboardPattern, Event, Instant, FPS, OFF_BODY, OFF_DASHBOARD, RGB, UI,
|
||||||
|
};
|
||||||
|
use std::{
|
||||||
|
cell::RefCell,
|
||||||
|
env,
|
||||||
|
rc::Rc,
|
||||||
|
sync::mpsc::{Receiver, TryRecvError},
|
||||||
|
};
|
||||||
|
|
||||||
|
const WIDTH: i32 = 640;
|
||||||
|
const HEIGHT: i32 = 480;
|
||||||
|
|
||||||
|
pub struct Update {
|
||||||
|
dashboard: DashboardPattern,
|
||||||
|
lights: BodyPattern,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DashboardLightsPrivate {
|
||||||
|
lights: Rc<RefCell<DashboardPattern>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[glib::object_subclass]
|
||||||
|
impl ObjectSubclass for DashboardLightsPrivate {
|
||||||
|
const NAME: &'static str = "DashboardLights";
|
||||||
|
type Type = DashboardLights;
|
||||||
|
type ParentType = gtk::DrawingArea;
|
||||||
|
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
lights: Rc::new(RefCell::new(OFF_DASHBOARD)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjectImpl for DashboardLightsPrivate {}
|
||||||
|
impl WidgetImpl for DashboardLightsPrivate {}
|
||||||
|
impl DrawingAreaImpl for DashboardLightsPrivate {}
|
||||||
|
|
||||||
|
glib::wrapper! {
|
||||||
|
pub struct DashboardLights(ObjectSubclass<DashboardLightsPrivate>) @extends gtk::DrawingArea, gtk::Widget;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DashboardLights {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let s: Self = Object::builder().build();
|
||||||
|
|
||||||
|
s.set_width_request(WIDTH);
|
||||||
|
s.set_height_request(100);
|
||||||
|
|
||||||
|
s.set_draw_func({
|
||||||
|
let s = s.clone();
|
||||||
|
move |_, context, width, _| {
|
||||||
|
let start = width as f64 / 2. - 150.;
|
||||||
|
let lights = s.imp().lights.borrow();
|
||||||
|
for i in 0..3 {
|
||||||
|
context.set_source_rgb(
|
||||||
|
lights[i].r.into(),
|
||||||
|
lights[i].g.into(),
|
||||||
|
lights[i].b.into(),
|
||||||
|
);
|
||||||
|
context.rectangle(start + 100. * i as f64, 10., 80., 80.);
|
||||||
|
let _ = context.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_lights(&self, lights: DashboardPattern) {
|
||||||
|
*self.imp().lights.borrow_mut() = lights;
|
||||||
|
self.queue_draw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct BikeLightsPrivate {
|
||||||
|
lights: Rc<RefCell<BodyPattern>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[glib::object_subclass]
|
||||||
|
impl ObjectSubclass for BikeLightsPrivate {
|
||||||
|
const NAME: &'static str = "BikeLights";
|
||||||
|
type Type = BikeLights;
|
||||||
|
type ParentType = gtk::DrawingArea;
|
||||||
|
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
lights: Rc::new(RefCell::new(OFF_BODY)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjectImpl for BikeLightsPrivate {}
|
||||||
|
impl WidgetImpl for BikeLightsPrivate {}
|
||||||
|
impl DrawingAreaImpl for BikeLightsPrivate {}
|
||||||
|
|
||||||
|
glib::wrapper! {
|
||||||
|
pub struct BikeLights(ObjectSubclass<BikeLightsPrivate>) @extends gtk::DrawingArea, gtk::Widget;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BikeLights {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let s: Self = Object::builder().build();
|
||||||
|
|
||||||
|
s.set_width_request(WIDTH);
|
||||||
|
s.set_height_request(640);
|
||||||
|
|
||||||
|
let center = WIDTH as f64 / 2.;
|
||||||
|
|
||||||
|
s.set_draw_func({
|
||||||
|
let s = s.clone();
|
||||||
|
move |_, context, _, _| {
|
||||||
|
let lights = s.imp().lights.borrow();
|
||||||
|
for i in 0..30 {
|
||||||
|
context.set_source_rgb(
|
||||||
|
lights[i].r.into(),
|
||||||
|
lights[i].g.into(),
|
||||||
|
lights[i].b.into(),
|
||||||
|
);
|
||||||
|
context.rectangle(center - 45., 5. + 20. * i as f64, 15., 15.);
|
||||||
|
let _ = context.fill();
|
||||||
|
}
|
||||||
|
for i in 0..30 {
|
||||||
|
context.set_source_rgb(
|
||||||
|
lights[i + 30].r.into(),
|
||||||
|
lights[i + 30].g.into(),
|
||||||
|
lights[i + 30].b.into(),
|
||||||
|
);
|
||||||
|
context.rectangle(center + 15., 5. + 20. * (30. - (i + 1) as f64), 15., 15.);
|
||||||
|
let _ = context.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_lights(&self, lights: [RGB<I8F8>; 60]) {
|
||||||
|
*self.imp().lights.borrow_mut() = lights;
|
||||||
|
self.queue_draw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GTKUI {
|
||||||
|
tx: Sender<Update>,
|
||||||
|
rx: Receiver<Event>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UI for GTKUI {
|
||||||
|
fn check_event(&mut self, _: Instant) -> Option<Event> {
|
||||||
|
match self.rx.try_recv() {
|
||||||
|
Ok(event) => Some(event),
|
||||||
|
Err(TryRecvError::Empty) => None,
|
||||||
|
Err(TryRecvError::Disconnected) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_lights(&self, dashboard_lights: DashboardPattern, lights: BodyPattern) {
|
||||||
|
self.tx
|
||||||
|
.send(Update {
|
||||||
|
dashboard: dashboard_lights,
|
||||||
|
lights,
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let adw_app = adw::Application::builder()
|
||||||
|
.application_id("com.luminescent-dreams.bike-light-simulator")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
adw_app.connect_activate(move |adw_app| {
|
||||||
|
let (update_tx, update_rx) =
|
||||||
|
gtk::glib::MainContext::channel::<Update>(gtk::glib::Priority::DEFAULT);
|
||||||
|
let (event_tx, event_rx) = std::sync::mpsc::channel();
|
||||||
|
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let mut bike_app = App::new(Box::new(GTKUI {
|
||||||
|
tx: update_tx,
|
||||||
|
rx: event_rx,
|
||||||
|
}));
|
||||||
|
loop {
|
||||||
|
bike_app.tick(Instant(U128F0::from(
|
||||||
|
std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_millis(),
|
||||||
|
)));
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(1000 / (FPS as u64)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let window = adw::ApplicationWindow::builder()
|
||||||
|
.application(adw_app)
|
||||||
|
.default_width(WIDTH)
|
||||||
|
.default_height(HEIGHT)
|
||||||
|
.build();
|
||||||
|
let layout = gtk::Box::builder()
|
||||||
|
.orientation(gtk::Orientation::Vertical)
|
||||||
|
.build();
|
||||||
|
let controls = gtk::Box::builder()
|
||||||
|
.orientation(gtk::Orientation::Horizontal)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let dashboard_lights = DashboardLights::new();
|
||||||
|
let bike_lights = BikeLights::new();
|
||||||
|
|
||||||
|
let left_button = gtk::Button::builder().label("L").build();
|
||||||
|
let brake_button = gtk::Button::builder().label("Brakes").build();
|
||||||
|
let right_button = gtk::Button::builder().label("R").build();
|
||||||
|
|
||||||
|
left_button.connect_clicked({
|
||||||
|
let event_tx = event_tx.clone();
|
||||||
|
move |_| {
|
||||||
|
let _ = event_tx.send(Event::LeftBlinker);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
brake_button.connect_clicked({
|
||||||
|
let event_tx = event_tx.clone();
|
||||||
|
move |_| {
|
||||||
|
let _ = event_tx.send(Event::Brake);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
right_button.connect_clicked({
|
||||||
|
let event_tx = event_tx.clone();
|
||||||
|
move |_| {
|
||||||
|
let _ = event_tx.send(Event::RightBlinker);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
controls.append(&left_button);
|
||||||
|
controls.append(&brake_button);
|
||||||
|
controls.append(&right_button);
|
||||||
|
layout.append(&controls);
|
||||||
|
|
||||||
|
let pattern_controls = gtk::Box::builder()
|
||||||
|
.orientation(gtk::Orientation::Horizontal)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let previous_pattern = gtk::Button::builder().label("Previous").build();
|
||||||
|
let next_pattern = gtk::Button::builder().label("Next").build();
|
||||||
|
|
||||||
|
previous_pattern.connect_clicked({
|
||||||
|
let event_tx = event_tx.clone();
|
||||||
|
move |_| {
|
||||||
|
let _ = event_tx.send(Event::PreviousPattern);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
next_pattern.connect_clicked({
|
||||||
|
let event_tx = event_tx.clone();
|
||||||
|
move |_| {
|
||||||
|
let _ = event_tx.send(Event::NextPattern);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
pattern_controls.append(&previous_pattern);
|
||||||
|
pattern_controls.append(&next_pattern);
|
||||||
|
layout.append(&pattern_controls);
|
||||||
|
|
||||||
|
layout.append(&dashboard_lights);
|
||||||
|
layout.append(&bike_lights);
|
||||||
|
|
||||||
|
update_rx.attach(None, {
|
||||||
|
let dashboard_lights = dashboard_lights.clone();
|
||||||
|
let bike_lights = bike_lights.clone();
|
||||||
|
move |Update { dashboard, lights }| {
|
||||||
|
dashboard_lights.set_lights(dashboard);
|
||||||
|
bike_lights.set_lights(lights);
|
||||||
|
glib::ControlFlow::Continue
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.set_content(Some(&layout));
|
||||||
|
window.present();
|
||||||
|
});
|
||||||
|
|
||||||
|
let args: Vec<String> = env::args().collect();
|
||||||
|
ApplicationExtManual::run_with_args(&adw_app, &args);
|
||||||
|
}
|
|
@ -3,7 +3,6 @@ name = "changeset"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
license-file = "../COPYING"
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
|
|
||||||
dev:
|
|
||||||
cargo watch -x build
|
|
||||||
|
|
||||||
test:
|
|
||||||
cargo watch -x test
|
|
||||||
|
|
||||||
test-once:
|
|
||||||
cargo test
|
|
|
@ -26,7 +26,7 @@ pub enum Change<Key: Eq + Hash, Value> {
|
||||||
NewRecord(Value),
|
NewRecord(Value),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub struct Changeset<Key: Clone + Eq + Hash, Value> {
|
pub struct Changeset<Key: Clone + Eq + Hash, Value> {
|
||||||
delete: HashSet<Key>,
|
delete: HashSet<Key>,
|
||||||
update: HashMap<Key, Value>,
|
update: HashMap<Key, Value>,
|
||||||
|
@ -34,14 +34,6 @@ pub struct Changeset<Key: Clone + Eq + Hash, Value> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Key: Clone + Constructable + Eq + Hash, Value> Changeset<Key, Value> {
|
impl<Key: Clone + Constructable + Eq + Hash, Value> Changeset<Key, Value> {
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
delete: HashSet::new(),
|
|
||||||
update: HashMap::new(),
|
|
||||||
new: HashMap::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add(&mut self, r: Value) -> Key {
|
pub fn add(&mut self, r: Value) -> Key {
|
||||||
let k = Key::new();
|
let k = Key::new();
|
||||||
self.new.insert(k.clone(), r);
|
self.new.insert(k.clone(), r);
|
||||||
|
@ -90,7 +82,7 @@ impl<Key: Clone + Eq + Hash, Value> From<Changeset<Key, Value>> for Vec<Change<K
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(k, v)| Change::UpdateRecord((k, v))),
|
.map(|(k, v)| Change::UpdateRecord((k, v))),
|
||||||
)
|
)
|
||||||
.chain(new.into_iter().map(|(_, v)| Change::NewRecord(v)))
|
.chain(new.into_values().map(|v| Change::NewRecord(v)))
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -100,7 +92,7 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Hash)]
|
#[derive(Clone, PartialEq, Eq, Hash, Default)]
|
||||||
struct Id(Uuid);
|
struct Id(Uuid);
|
||||||
impl Constructable for Id {
|
impl Constructable for Id {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
|
@ -110,7 +102,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn it_generates_a_new_record() {
|
fn it_generates_a_new_record() {
|
||||||
let mut set: Changeset<Id, String> = Changeset::new();
|
let mut set: Changeset<Id, String> = Changeset::default();
|
||||||
set.add("efgh".to_string());
|
set.add("efgh".to_string());
|
||||||
let changes = Vec::from(set.clone());
|
let changes = Vec::from(set.clone());
|
||||||
assert_eq!(changes.len(), 1);
|
assert_eq!(changes.len(), 1);
|
||||||
|
@ -125,7 +117,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn it_generates_a_delete_record() {
|
fn it_generates_a_delete_record() {
|
||||||
let mut set: Changeset<Id, String> = Changeset::new();
|
let mut set: Changeset<Id, String> = Changeset::default();
|
||||||
let id1 = Id::new();
|
let id1 = Id::new();
|
||||||
set.delete(id1.clone());
|
set.delete(id1.clone());
|
||||||
let changes = Vec::from(set.clone());
|
let changes = Vec::from(set.clone());
|
||||||
|
@ -142,7 +134,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn update_unrelated_records() {
|
fn update_unrelated_records() {
|
||||||
let mut set: Changeset<Id, String> = Changeset::new();
|
let mut set: Changeset<Id, String> = Changeset::default();
|
||||||
let id1 = Id::new();
|
let id1 = Id::new();
|
||||||
let id2 = Id::new();
|
let id2 = Id::new();
|
||||||
set.update(id1.clone(), "abcd".to_owned());
|
set.update(id1.clone(), "abcd".to_owned());
|
||||||
|
@ -155,7 +147,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn delete_cancels_new() {
|
fn delete_cancels_new() {
|
||||||
let mut set: Changeset<Id, String> = Changeset::new();
|
let mut set: Changeset<Id, String> = Changeset::default();
|
||||||
let key = set.add("efgh".to_string());
|
let key = set.add("efgh".to_string());
|
||||||
set.delete(key);
|
set.delete(key);
|
||||||
let changes = Vec::from(set);
|
let changes = Vec::from(set);
|
||||||
|
@ -164,7 +156,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn delete_cancels_update() {
|
fn delete_cancels_update() {
|
||||||
let mut set: Changeset<Id, String> = Changeset::new();
|
let mut set: Changeset<Id, String> = Changeset::default();
|
||||||
let id = Id::new();
|
let id = Id::new();
|
||||||
set.update(id.clone(), "efgh".to_owned());
|
set.update(id.clone(), "efgh".to_owned());
|
||||||
set.delete(id.clone());
|
set.delete(id.clone());
|
||||||
|
@ -175,7 +167,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn update_atop_new_is_new() {
|
fn update_atop_new_is_new() {
|
||||||
let mut set: Changeset<Id, String> = Changeset::new();
|
let mut set: Changeset<Id, String> = Changeset::default();
|
||||||
let key = set.add("efgh".to_owned());
|
let key = set.add("efgh".to_owned());
|
||||||
set.update(key, "wxyz".to_owned());
|
set.update(key, "wxyz".to_owned());
|
||||||
let changes = Vec::from(set);
|
let changes = Vec::from(set);
|
||||||
|
@ -185,7 +177,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn updates_get_squashed() {
|
fn updates_get_squashed() {
|
||||||
let mut set: Changeset<Id, String> = Changeset::new();
|
let mut set: Changeset<Id, String> = Changeset::default();
|
||||||
let id1 = Id::new();
|
let id1 = Id::new();
|
||||||
let id2 = Id::new();
|
let id2 = Id::new();
|
||||||
set.update(id1.clone(), "efgh".to_owned());
|
set.update(id1.clone(), "efgh".to_owned());
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
[package]
|
||||||
|
name = "config-derive"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
proc-macro = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
quote = { version = "1" }
|
||||||
|
syn = { version = "1", features = [ "extra-traits" ] }
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
extern crate proc_macro;
|
||||||
|
|
||||||
|
use proc_macro::TokenStream;
|
||||||
|
use quote::quote;
|
||||||
|
|
||||||
|
use syn::{parse_macro_input, DeriveInput};
|
||||||
|
|
||||||
|
#[proc_macro_derive(ConfigOption)]
|
||||||
|
pub fn derive(input: TokenStream) -> TokenStream {
|
||||||
|
let DeriveInput { ident, .. } = parse_macro_input!(input as DeriveInput);
|
||||||
|
|
||||||
|
let result = quote! {
|
||||||
|
impl From<&Config> for Option<#ident> {
|
||||||
|
fn from(config: &Config) -> Self {
|
||||||
|
match config.values.get(&ConfigName::#ident) {
|
||||||
|
Some(ConfigOption::#ident(val)) => Some(val.clone()),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
result.into()
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
[package]
|
||||||
|
name = "config"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
config-derive = { path = "../config-derive" }
|
||||||
|
serde_json = { version = "1" }
|
||||||
|
serde = { version = "1", features = [ "derive" ] }
|
||||||
|
thiserror = { version = "1" }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
cool_asserts = { version = "2" }
|
|
@ -0,0 +1,160 @@
|
||||||
|
/*
|
||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
fs::File,
|
||||||
|
hash::Hash,
|
||||||
|
io::{ErrorKind, Read},
|
||||||
|
path::PathBuf,
|
||||||
|
};
|
||||||
|
*/
|
||||||
|
|
||||||
|
pub use config_derive::ConfigOption;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum ConfigReadError {
|
||||||
|
#[error("Cannot read the configuration file: {0}")]
|
||||||
|
CannotRead(std::io::Error),
|
||||||
|
#[error("Cannot open the configuration file for reading: {0}")]
|
||||||
|
CannotOpen(std::io::Error),
|
||||||
|
#[error("Invalid json data found in the configurationfile: {0}")]
|
||||||
|
InvalidJSON(serde_json::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! define_config {
|
||||||
|
($($name:ident($struct:ident),)+) => (
|
||||||
|
#[derive(Clone, Debug, Hash, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub enum ConfigName {
|
||||||
|
$($name),+
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum ConfigOption {
|
||||||
|
$($name($struct)),+
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct Config {
|
||||||
|
values: std::collections::HashMap<ConfigName, ConfigOption>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
values: std::collections::HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_path(config_path: std::path::PathBuf) -> Result<Self, $crate::ConfigReadError> {
|
||||||
|
let mut settings = config_path.clone();
|
||||||
|
settings.push("config");
|
||||||
|
|
||||||
|
match std::fs::File::open(settings) {
|
||||||
|
Ok(mut file) => {
|
||||||
|
let mut buf = String::new();
|
||||||
|
std::io::Read::read_to_string(&mut file, &mut buf)
|
||||||
|
.map_err(|err| $crate::ConfigReadError::CannotRead(err))?;
|
||||||
|
let values = serde_json::from_str(buf.as_ref())
|
||||||
|
.map_err(|err| $crate::ConfigReadError::InvalidJSON(err))?;
|
||||||
|
Ok(Self {
|
||||||
|
values,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Err(io_err) => {
|
||||||
|
match io_err.kind() {
|
||||||
|
std::io::ErrorKind::NotFound => {
|
||||||
|
/* create the path and an empty file */
|
||||||
|
Ok(Self {
|
||||||
|
values: std::collections::HashMap::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => Err($crate::ConfigReadError::CannotOpen(io_err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set(&mut self, val: ConfigOption) {
|
||||||
|
let _ = match val {
|
||||||
|
$(ConfigOption::$struct(_) => self.values.insert(ConfigName::$name, val)),+
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get<'a, T>(&'a self) -> Option<T>
|
||||||
|
where
|
||||||
|
Option<T>: From<&'a Self>,
|
||||||
|
{
|
||||||
|
self.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use cool_asserts::assert_matches;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
define_config! {
|
||||||
|
DatabasePath(DatabasePath),
|
||||||
|
Me(Me),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, ConfigOption)]
|
||||||
|
pub struct DatabasePath(PathBuf);
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
enum Rank {
|
||||||
|
Kyu(i8),
|
||||||
|
Dan(i8),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, ConfigOption)]
|
||||||
|
pub struct Me {
|
||||||
|
name: String,
|
||||||
|
rank: Option<Rank>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_can_set_and_get_options() {
|
||||||
|
let mut config: Config = Config::new();
|
||||||
|
config.set(ConfigOption::DatabasePath(DatabasePath(PathBuf::from(
|
||||||
|
"./fixtures/five_games",
|
||||||
|
))));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
Some(DatabasePath(PathBuf::from("./fixtures/five_games"))),
|
||||||
|
config.get()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_can_serialize_and_deserialize() {
|
||||||
|
let mut config = Config::new();
|
||||||
|
config.set(ConfigOption::DatabasePath(DatabasePath(PathBuf::from(
|
||||||
|
"fixtures/five_games",
|
||||||
|
))));
|
||||||
|
config.set(ConfigOption::Me(Me {
|
||||||
|
name: "Savanni".to_owned(),
|
||||||
|
rank: Some(Rank::Kyu(10)),
|
||||||
|
}));
|
||||||
|
let s = serde_json::to_string(&config.values).unwrap();
|
||||||
|
println!("{}", s);
|
||||||
|
let values: HashMap<ConfigName, ConfigOption> = serde_json::from_str(s.as_ref()).unwrap();
|
||||||
|
println!("options: {:?}", values);
|
||||||
|
|
||||||
|
assert_matches!(values.get(&ConfigName::DatabasePath),
|
||||||
|
Some(ConfigOption::DatabasePath(ref db_path)) =>
|
||||||
|
assert_eq!(Some(db_path.clone()), config.get())
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_matches!(values.get(&ConfigName::Me), Some(ConfigOption::Me(val)) =>
|
||||||
|
assert_eq!(Some(val.clone()), config.get())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,6 @@ name = "coordinates"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
license-file = "../COPYING"
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
|
|
||||||
dev:
|
|
||||||
cargo watch -x build
|
|
||||||
|
|
||||||
test:
|
|
||||||
cargo watch -x test
|
|
||||||
|
|
||||||
test-once:
|
|
||||||
cargo test
|
|
|
@ -33,12 +33,12 @@ fn main() {
|
||||||
|
|
||||||
let filename = args
|
let filename = args
|
||||||
.next()
|
.next()
|
||||||
.map(|p| PathBuf::from(p))
|
.map(PathBuf::from)
|
||||||
.expect("A filename is required");
|
.expect("A filename is required");
|
||||||
let size = args
|
let size = args
|
||||||
.next()
|
.next()
|
||||||
.and_then(|s| s.parse::<usize>().ok())
|
.and_then(|s| s.parse::<usize>().ok())
|
||||||
.unwrap_or(3);
|
.unwrap_or(3);
|
||||||
let map: hex_map::Map<MapVal> = hex_map::Map::new_hexagonal(size);
|
let map: hex_map::Map<MapVal> = hex_map::Map::new_hexagonal(size);
|
||||||
hex_map::write_file(filename, map);
|
hex_map::write_file(filename, map).expect("to write file");
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,10 +10,9 @@ Luminescent Dreams Tools is distributed in the hope that it will be useful, but
|
||||||
You should have received a copy of the GNU General Public License along with Lumeto. If not, see <https://www.gnu.org/licenses/>.
|
You should have received a copy of the GNU General Public License along with Lumeto. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/// Ĉi-tiu modulo enhavas la elementojn por kub-koordinato.
|
/// This module contains the elements of cube coordinates.
|
||||||
///
|
///
|
||||||
/// This code is based on https://www.redblobgames.com/grids/hexagons/
|
/// This code is based on https://www.redblobgames.com/grids/hexagons/
|
||||||
use crate::Error;
|
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
/// An address within the hex coordinate system
|
/// An address within the hex coordinate system
|
||||||
|
@ -62,7 +61,7 @@ impl AxialAddr {
|
||||||
pub fn is_adjacent(&self, dest: &AxialAddr) -> bool {
|
pub fn is_adjacent(&self, dest: &AxialAddr) -> bool {
|
||||||
dest.adjacencies()
|
dest.adjacencies()
|
||||||
.collect::<Vec<AxialAddr>>()
|
.collect::<Vec<AxialAddr>>()
|
||||||
.contains(&self)
|
.contains(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Measure the distance to a destination
|
/// Measure the distance to a destination
|
||||||
|
@ -79,7 +78,7 @@ impl AxialAddr {
|
||||||
|
|
||||||
positions.push(item);
|
positions.push(item);
|
||||||
|
|
||||||
while positions.len() > 0 {
|
while !positions.is_empty() {
|
||||||
let elem = positions.remove(0);
|
let elem = positions.remove(0);
|
||||||
for adj in elem.adjacencies() {
|
for adj in elem.adjacencies() {
|
||||||
if self.distance(&adj) <= distance && !results.contains(&adj) {
|
if self.distance(&adj) <= distance && !results.contains(&adj) {
|
||||||
|
|
|
@ -14,7 +14,6 @@ use crate::{hex::AxialAddr, Error};
|
||||||
use nom::{
|
use nom::{
|
||||||
bytes::complete::tag,
|
bytes::complete::tag,
|
||||||
character::complete::alphanumeric1,
|
character::complete::alphanumeric1,
|
||||||
error::ParseError,
|
|
||||||
multi::many1,
|
multi::many1,
|
||||||
sequence::{delimited, separated_pair},
|
sequence::{delimited, separated_pair},
|
||||||
Finish, IResult, Parser,
|
Finish, IResult, Parser,
|
||||||
|
@ -81,7 +80,7 @@ pub fn parse_data<'a, A: Default + From<String>>(
|
||||||
}
|
}
|
||||||
|
|
||||||
let cells = data
|
let cells = data
|
||||||
.map(|line| parse_line::<A>(&line).unwrap())
|
.map(|line| parse_line::<A>(line).unwrap())
|
||||||
.collect::<Vec<(AxialAddr, A)>>();
|
.collect::<Vec<(AxialAddr, A)>>();
|
||||||
let cells = cells.into_iter().collect::<HashMap<AxialAddr, A>>();
|
let cells = cells.into_iter().collect::<HashMap<AxialAddr, A>>();
|
||||||
Map { cells }
|
Map { cells }
|
||||||
|
|
|
@ -9,9 +9,9 @@ Lumeto is distributed in the hope that it will be useful, but WITHOUT ANY WARRAN
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License along with Lumeto. If not, see <https://www.gnu.org/licenses/>.
|
You should have received a copy of the GNU General Public License along with Lumeto. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
use thiserror;
|
use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error("IO error on reading or writing: {0}")]
|
#[error("IO error on reading or writing: {0}")]
|
||||||
IO(std::io::Error),
|
IO(std::io::Error),
|
||||||
|
|
|
@ -0,0 +1,519 @@
|
||||||
|
{
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#addr2line@0.21.0": "1jx0k3iwyqr8klqbzk6kjvr496yd94aspis10vwsj5wy7gib4c4a",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#adler32@1.2.0": "0d7jq7jsjyhsgbhnfq5fvrlh9j0i9g1fqrl2735ibv5f75yjgqda",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#adler@1.0.2": "1zim79cvzd5yrkzl3nyfx0avijwgk9fqv3yrscdy1cc79ih02qpj",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#ahash@0.8.6": "0yn9i8nc6mmv28ig9w3dga571q09vg9f1f650mi5z8phx42r6hli",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#aho-corasick@1.1.2": "1w510wnixvlgimkx1zjbvlxh6xps2vjgfqgwf5a6adlbjp5rv5mj",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#allocator-api2@0.2.16": "1iayppgq4wqbfbfcqmsbwgamj0s65012sskfvyx07pxavk3gyhh9",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#android-tzdata@0.1.1": "1w7ynjxrfs97xg3qlcdns4kgfpwcdv824g611fq32cag4cdr96g9",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#android_system_properties@0.1.5": "04b3wrz12837j7mdczqd95b732gw5q7q66cv4yn4646lvccp57l1",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#annotate-snippets@0.9.2": "07p8r6jzb7nqydq0kr5pllckqcdxlyld2g275v425axnzffpxbyc",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#anstream@0.6.5": "1dm1mdbs1x6y3m3pz0qlamgiskb50i4q859676kx0pz8r8pajr6n",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#anstyle-parse@0.2.3": "134jhzrz89labrdwxxnjxqjdg06qvaflj1wkfnmyapwyldfwcnn7",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#anstyle-query@1.0.2": "0j3na4b1nma39g4x7cwvj009awxckjf3z2vkwhldgka44hqj72g2",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#anstyle-wincon@3.0.2": "19v0fv400bmp4niqpzxnhg83vz12mmqv7l2l8vi80qcdxj0lpm8w",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#anstyle@1.0.4": "11yxw02b6parn29s757z96rgiqbn8qy0fk9a3p3bhczm85dhfybh",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#anyhow@1.0.75": "1rmcjkim91c5mw7h9wn8nv0k6x118yz0xg0z1q18svgn42mqqrm4",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#arrayvec@0.7.4": "04b7n722jij0v3fnm3qk072d5ysc2q30rl9fz33zpfhzah30mlwn",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#async-channel@1.9.0": "0dbdlkzlncbibd3ij6y6jmvjd0cmdn48ydcfdpfhw09njd93r5c1",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#async-channel@2.1.1": "1337ywc1paw03rdlwh100kh8pa0zyp0nrlya8bpsn6zdqi5kz8qw",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#async-executor@1.8.0": "0z7rpayidhdqs4sdzjhh26z5155c1n94fycqni9793n4zjz5xbhp",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#async-global-executor@2.4.1": "1762s45cc134d38rrv0hyp41hv4iv6nmx59vswid2p0il8rvdc85",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#async-io@2.3.1": "0rggn074kbqxxajci1aq14b17gp75rw9l6rpbazcv9q0bc6ap5wg",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#async-lock@3.3.0": "0yxflkfw46rad4lv86f59b5z555dlfmg1riz1n8830rgi0qb8d6h",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#async-std@1.13.0": "059nbiyijwbndyrz0050skvlvzhds0dmnl0biwmxwbw055glfd66",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#async-task@4.7.0": "16975vx6aqy5yf16fs9xz5vx1zq8mwkzfmykvcilc1j7b6c6xczv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#async-trait@0.1.77": "1adf1jh2yg39rkpmqjqyr9xyd6849p0d95425i6imgbhx0syx069",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#atoi@2.0.0": "0a05h42fggmy7h0ajjv6m7z72l924i7igbx13hk9d8pyign9k3gj",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#atomic-waker@1.1.2": "1h5av1lw56m0jf0fd3bchxq8a30xv0b4wv8s4zkp4s0i7mfvs18m",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#atomic-write-file@0.1.2": "0dl4x0srdwjxm3zz3fj1c7m44i3b7mjiad550fqklj1n4bfbxkgd",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#autocfg@0.1.8": "0y4vw4l4izdxq1v0rrhvmlbqvalrqrmk60v1z0dqlgnlbzkl7phd",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#autocfg@1.1.0": "1ylp3cb47ylzabimazvbz9ms6ap784zhb6syaz6c1jqpmcmq0s6l",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#az@1.2.1": "0ww9k1w3al7x5qmb7f13v3s9c2pg1pdxbs8xshqy6zyrchj4qzkv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#backtrace@0.3.69": "0dsq23dhw4pfndkx2nsa1ml2g31idm7ss7ljxp8d57avygivg290",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#bare-metal@0.2.5": "1cy5pbb92fznnri72y6drfpjxj4qdmd62f0rrlgy70dxlppn9ssx",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#base64@0.21.5": "1y8x2xs9nszj5ix7gg4ycn5a6wy7ca74zxwqri3bdqzdjha6lqrm",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#base64@0.9.3": "0hs62r35bgxslawyrn1vp9rmvrkkm76fqv0vqcwd048vs876r7a8",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#base64ct@1.6.0": "0nvdba4jb8aikv60az40x2w1y96sjdq8z3yp09rwzmkhiwv1lg4c",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#bindgen@0.69.4": "18194611hn3k1dkxlha7a52sr8vmfhl9blc54xhj08cahd8wh3d0",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#bit-set@0.5.3": "1wcm9vxi00ma4rcxkl3pzzjli6ihrpn9cfdi0c5b4cvga2mxs007",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#bit-vec@0.6.3": "1ywqjnv60cdh1slhz67psnp422md6jdliji6alq0gmly2xm9p7rl",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#bit_field@0.10.2": "0qav5rpm4hqc33vmf4vc4r0mh51yjx5vmd9zhih26n9yjs3730nw",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#bitfield@0.13.2": "06g7jb5r2b856vnhx76081fg90jvmy61kjqcfjysgmd5hclvvbs6",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#bitflags@1.3.2": "12ki6w8gn1ldq7yz9y680llwk5gmrhrzszaa17g1sbrw2r2qvwxy",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#bitflags@2.4.1": "01ryy3kd671b0ll4bhdvhsz67vwz1lz53fz504injrd7wpv64xrj",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#block-buffer@0.10.4": "0w9sa2ypmrsqqvc20nhwr75wbb5cjr4kkyhpjm1z1lv2kdicfy1h",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#blocking@1.5.1": "064i3d6b8ln34fgdw49nmx9m36bwi3r3nv8c9xhcrpf4ilz92dva",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#build_html@2.4.0": "188nibbsv33vgjjiq9cn2irsgdb75gxfipavcavnyydcwxpzw21i",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#bumpalo@3.14.0": "1v4arnv9kwk54v5d0qqpv4vyw2sgr660nk0w3apzixi1cm3yfc3z",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#bytemuck@1.14.0": "1ik1ma5n3bg700skkzhx50zjk7kj7mbsphi773if17l04pn2hk9p",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#byteorder@1.5.0": "0jzncxyf404mwqdbspihyzpkndfgda450l0893pz5xj685cg5l0z",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#bytes@1.5.0": "08w2i8ac912l8vlvkv3q51cd4gr09pwlg3sjsjffcizlrb0i5gd2",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#cairo-rs@0.18.3": "18d80lk853bjhx36rjaj78clzfjrmlgi01863drnmshdgxi16dpk",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#cairo-sys-rs@0.18.2": "0lfsxl7ylw3phbnwmz3k58j1gnqi6kc2hdc7g3bb7f4hwnl9yp38",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#cc@1.0.83": "1l643zidlb5iy1dskc5ggqs4wqa29a02f44piczqc8zcnsq4y5zi",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#cexpr@0.6.0": "0rl77bwhs5p979ih4r0202cn5jrfsrbgrksp40lkfz5vk1x3ib3g",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#cfg-expr@0.15.5": "1cqicd9qi8mzzgh63dw03zhbdihqfl3lbiklrkynyzkq67s5m483",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#cfg-if@1.0.0": "1za0vb97n4brpzpv8lsbnzmq5r8f2b0cpqqr0sy8h5bn751xxwds",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#chrono-tz-build@0.2.1": "03rmzd69cn7fp0fgkjr5042b3g54s2l941afjm3001ls7kqkjgj3",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#chrono-tz@0.8.4": "0xhd3dsfs72im0sbc7w889lfy7bxgjlbvqhj5a1yvxhxwb08acg2",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#chrono@0.4.31": "0f6vg67pipm8cziad2yms6a639pssnvysk1m05dd9crymmdnhb3z",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#clang-sys@1.8.1": "1x1r9yqss76z8xwpdanw313ss6fniwc1r7dzb5ycjn0ph53kj0hb",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#clap@4.4.11": "1wj5gb2fnqls00zfahg3490bdfc36d9cwpl80qjacb5jyrqzdbxz",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#clap_builder@4.4.11": "1fxdsmw1ilgswz3lg2hjlvsdyyz04k78scjirlbd7c9bc83ba5m2",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#clap_derive@4.4.7": "0hk4hcxl56qwqsf4hmf7c0gr19r9fbxk0ah2bgkr36pmmaph966g",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#clap_lex@0.6.0": "1l8bragdvim7mva9flvd159dskn2bdkpl0jqrr41wnjfn8pcfbvh",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#cloudabi@0.0.3": "0kxcg83jlihy0phnd2g8c2c303px3l2p3pkjz357ll6llnd5pz6x",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#color_quant@1.1.0": "12q1n427h2bbmmm1mnglr57jaz2dj9apk0plcxw7nwqiai7qjyrx",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#colorchoice@1.0.0": "1ix7w85kwvyybwi2jdkl3yva2r2bvdcc3ka2grjfzfgrapqimgxc",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#concurrent-queue@2.4.0": "0qvk23ynj311adb4z7v89wk3bs65blps4n24q8rgl23vjk6lhq6i",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#const-oid@0.9.6": "1y0jnqaq7p2wvspnx7qj76m7hjcqpz73qzvr9l2p9n2s51vr6if2",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#convert_case@0.6.0": "1jn1pq6fp3rri88zyw6jlhwwgf6qiyc08d6gjv0qypgkl862n67c",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#cookie-factory@0.3.3": "18mka6fk3843qq3jw1fdfvzyv05kx7kcmirfbs2vg2kbw9qzm1cq",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#cookie@0.17.0": "096c52jg9iq4lfcps2psncswv33fc30mmnaa2sbzzcfcw71kgyvy",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#cool_asserts@2.0.3": "1v18dg7ifx41k2f82j3gsnpm1fg9wk5s4zv7sf42c7pnad72b7zf",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#core-foundation-sys@0.8.6": "13w6sdf06r0hn7bx2b45zxsg1mm2phz34jikm6xc5qrbr6djpsh6",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#core-foundation@0.9.4": "13zvbbj07yk3b61b8fhwfzhy35535a583irf23vlcg59j7h9bqci",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#cortex-m-rt-macros@0.7.0": "1iyki0wq8pj0qbjhw1mbq5njraihhyr7ydcbqzdzwg10dziz7xph",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#cortex-m-rt@0.7.3": "1cfxg502gvcmaczmaij5maxbvaxnda5w6gp14cbin44ksl9yi17f",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#cortex-m@0.7.7": "1fbca698v4gv57mv5fc48jrz8wcy6sv675n6fsrsah4qykc11ilf",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#cpufeatures@0.2.11": "1l0gzsyy576n017g9bf0vkv5hhg9cpz1h1libxyfdlzcgbh0yhnf",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#crc-any@2.5.0": "0wzs26q5cf29fhfnrkrjsr8dpai0rlm4im8b53by8rbrbzzwjbm6",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#crc-catalog@2.4.0": "1xg7sz82w3nxp1jfn425fvn1clvbzb3zgblmxsyqpys0dckp9lqr",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#crc32fast@1.3.2": "03c8f29yx293yf43xar946xbls1g60c207m9drf8ilqhr25vsh5m",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#crc@3.0.1": "1zkx87a5x06xfd6xm5956w4vmdfs0wcxpsn7iwj5jbp2rcapmv46",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#critical-section@1.1.2": "05pj0pvkdyc9r30xxabam4n8zxdbzxcddr0gdypajcbqjgwgynbh",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#crossbeam-deque@0.8.4": "0la7fx9n1vbx3h23va0xmcy36hziql1pkik08s3j3asv4479ma7w",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#crossbeam-epoch@0.9.16": "1anr32r8px0vb65cgwbwp3zhqz69scz5dgq9bmx54w5qa59yjbrd",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#crossbeam-queue@0.3.9": "0lz17pgydh29w8brld8dysi1m4n5bxfpnj8w9bxk0q6xpyyzbg5r",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#crossbeam-utils@0.8.17": "13y7wh993i7q71kg6wcfj65w3rlmizzrz7cqgz1l9whlgw9rcvf0",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#crunchy@0.2.2": "1dx9mypwd5mpfbbajm78xcrg5lirqk7934ik980mmaffg3hdm0bs",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#crypto-common@0.1.6": "1cvby95a6xg7kxdz5ln3rl9xh66nz66w46mm3g56ri1z5x815yqv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#data-encoding@2.5.0": "1rcbnwfmfxhlshzbn3r7srm3azqha3mn33yxyqxkzz2wpqcjm5ky",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#debug-helper@0.3.13": "0bhnpzpgmg8dkdr27g2b49slf6ca79m4idcb01z2krs0qkifhy7m",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#deflate@0.8.6": "0x6iqlayg129w63999kz97m279m0jj4x4sm6gkqlvmp73y70yxvk",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#der@0.7.8": "070bwiyr80800h31c5zd96ckkgagfjgnrrdmz3dzg2lccsd3dypz",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#deranged@0.3.10": "1p4i64nkadamksa943d6gk39sl1kximz0xr69n408fvsl1q0vcwf",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#digest@0.10.7": "14p2n6ih29x81akj097lvz7wi9b6b9hvls0lwrv7b6xwyy0s5ncy",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#dimensioned@0.7.0": "09ky8s3higkf677lmyqg30hmj66gpg7hx907s6hfvbk2a9av05r5",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#dimensioned@0.8.0": "15s3j4ry943xqlac63bp81sgdk9s3yilysabzww35j9ibmnaic50",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#displaydoc@0.2.4": "0p8pyg10csc782qlwx3znr6qx46ni96m1qh597kmyrf6s3s8axa8",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#dotenvy@0.15.7": "16s3n973n5aqym02692i1npb079n5mb0fwql42ikmwn8wnrrbbqs",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#either@1.9.0": "01qy3anr7jal5lpc20791vxrw0nl6vksb5j7x56q2fycgcyy8sm2",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#embedded-alloc@0.5.1": "05gqqv9nyr33vbd0i8ab2bmfcc5kwgk0msk4pk7w5fncba8igbnx",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#embedded-dma@0.2.0": "0ijld5jblcka4b95s1lwxd9k109nyaap34h44g122ddjbidpwkwr",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#embedded-hal@0.2.7": "1zv6pkgg2yl0mzvh3jp326rhryqfnv4l27h78v7p7maag629i51m",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#encoding_rs@0.8.33": "1qa5k4a0ipdrxq4xg9amms9r9pnnfn7nfh2i9m3mw0ka563b6s3j",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#env_logger@0.10.1": "1kmy9xmfjaqfvd4wkxr1f7d16ld3h9b487vqs2q9r0s8f3kg7cwm",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#equivalent@1.0.1": "1malmx5f4lkfvqasz319lq6gb3ddg19yzf9s8cykfsgzdmyq0hsl",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#errno@0.3.8": "0ia28ylfsp36i27g1qih875cyyy4by2grf80ki8vhgh6vinf8n52",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#etcetera@0.8.0": "0hxrsn75dirbjhwgkdkh0pnpqrnq17ypyhjpjaypgax1hd91nv8k",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#event-listener-strategy@0.4.0": "1lwprdjqp2ibbxhgm9khw7s7y7k4xiqj5i5yprqiks6mnrq4v3lm",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#event-listener@2.5.3": "1q4w3pndc518crld6zsqvvpy9lkzwahp2zgza9kbzmmqh9gif1h2",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#event-listener@4.0.1": "04k7qbi5kgs36s905gxijj41kcr78xs2s6cp6vbg50254z7wvwl4",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#exr@1.71.0": "1a58k179b0h8zpf1cfgc2vl60j2syg7cdgdzp9j6cgmb6lgpcal3",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#fastrand@2.0.1": "19flpv5zbzpf0rk4x77z4zf25in0brg8l7m304d3yrf47qvwxjr5",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#fdeflate@0.3.1": "0s5885wdsih2hqx3hsl7l8cl3666fgsgiwvglifzy229hpydmmk4",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#field-offset@0.3.6": "0zq5sssaa2ckmcmxxbly8qgz3sxpb8g1lwv90sdh1z74qif2gqiq",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#finl_unicode@1.2.0": "1ipdx778849czik798sjbgk5yhwxqybydac18d2g9jb20dxdrkwg",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#fixed@1.24.0": "0zbfwzk4mrfbawpx2ahz533bkb97jzihv7fxiyhpmwf0wzkrrih2",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#flate2@1.0.28": "03llhsh4gqdirnfxxb9g2w9n0721dyn4yjir3pz7z4vjaxb3yc26",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#fluent-bundle@0.15.2": "1zbzm13rfz7fay7bps7jd4j1pdnlxmdzzfymyq2iawf9vq0wchp2",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#fluent-langneg@0.13.0": "152yxplc11vmxkslvmaqak9x86xnavnhdqyhrh38ym37jscd0jic",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#fluent-syntax@0.11.0": "0y6ac7z7sbv51nsa6km5z8rkjj4nvqk91vlghq1ck5c3cjbyvay0",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#fluent@0.16.0": "19s7z0gw95qdsp9hhc00xcy11nwhnx93kknjmdvdnna435w97xk1",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#flume@0.11.0": "10girdbqn77wi802pdh55lwbmymy437k7kklnvj12aaiwaflbb2m",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#fnv@1.0.7": "1hc2mcqha06aibcaza94vbi81j6pr9a1bbxrxjfhc91zin8yr7iz",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#foreign-types-shared@0.1.1": "0jxgzd04ra4imjv8jgkmdq59kj8fsz6w4zxsbmlai34h26225c00",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#foreign-types@0.3.2": "1cgk0vyd7r45cj769jym4a6s7vwshvd0z4bqrb92q1fwibmkkwzn",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#form_urlencoded@1.2.1": "0milh8x7nl4f450s3ddhg57a3flcv6yq8hlkyk6fyr3mcb128dp1",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#frunk@0.4.2": "11v242h7zjka0lckxcffn5pjgr3jzxyljy7ffr0ppy8jkssm38qi",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#frunk_core@0.4.2": "1mjqnn7dclwn8d5g0mrfkg360cgn70a7mm8arx6fc1xxn3x6j95g",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#frunk_derives@0.4.2": "0blsy6aq6rbvxcc0337g15083w24s8539fmv8rwp1qan2qprkymh",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#frunk_proc_macro_helpers@0.1.2": "0b1xl4cfrfai7qi5cb4h9x0967miv3dvwvnsmr1vg4ljhgflmd9m",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#fuchsia-cprng@0.1.1": "1fnkqrbz7ixxzsb04bsz9p0zzazanma8znfdqjvh39n14vapfvx0",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#fugit@0.3.7": "1rzp49521akq49vs9m8llgmdkk08zb77rry10a7srm9797b6l60p",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#futures-channel@0.3.29": "1jxsifvrbqzdadk0svbax71cba5d3qg3wgjq8i160mxmd1kdckgz",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#futures-core@0.3.29": "1308bpj0g36nhx2y6bl4mm6f1gnh9xyvvw2q2wpdgnb6dv3247gb",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#futures-executor@0.3.29": "1g4pjni0sw28djx6mlcfz584abm2lpifz86cmng0kkxh7mlvhkqg",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#futures-intrusive@0.5.0": "0vwm08d1pli6bdaj0i7xhk3476qlx4pll6i0w03gzdnh7lh0r4qx",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#futures-io@0.3.29": "1ajsljgny3zfxwahba9byjzclrgvm1ypakca8z854k2w7cb4mwwb",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#futures-lite@2.2.0": "1flj85i6xm0rjicxixmajrp6rhq8i4bnbzffmrd6h23ln8jshns4",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#futures-macro@0.3.29": "1nwd18i8kvpkdfwm045hddjli0n96zi7pn6f99zi9c74j7ym7cak",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#futures-sink@0.3.29": "05q8jykqddxzp8nwf00wjk5m5mqi546d7i8hsxma7hiqxrw36vg3",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#futures-task@0.3.29": "1qmsss8rb5ppql4qvd4r70h9gpfcpd0bg2b3qilxrnhdkc397lgg",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#futures-util@0.3.29": "0141rkqh0psj4h8x8lgsl1p29dhqr7z2wcixkcbs60z74kb2d5d1",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#futures@0.3.29": "0dak2ilpcmyjrb1j54fzy9hlw6vd10vqljq9gd59pbrq9dqr00ns",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#gcd@2.3.0": "06l4fib4dh4m6gazdrzzzinhvcpcfh05r4i4gzscl03vnjhqnx8x",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#gdk-pixbuf-sys@0.18.0": "1xya543c4ffd2n7aiwwrdxsyc9casdbasafi6ixcknafckm3k61z",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#gdk-pixbuf@0.18.3": "0b68ssdyapvq3bgsna9frabbzhjkvvzz8jld4mxkphr29nvk4vs4",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#gdk4-sys@0.7.2": "1w7yvir565sjrrw828lss07749hfpfsr19jdjzwivkx36brl7ayv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#gdk4@0.7.3": "1xiacc63p73apr033gjrb9dsk0y4yxnsljwfxbwfry41snd03nvy",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#generic-array@0.11.2": "0a7w8w0rg47nmcinnfzv443lcyb8mplwc251p1jyr5xj2yh6wzv6",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#generic-array@0.14.7": "16lyyrzrljfq424c3n8kfwkqihlimmsg5nhshbbp48np3yjrqr45",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#getrandom@0.2.11": "03q7120cc2kn7ry013i67zmjl2g9q73h1ks5z08hq5v9syz0d47y",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#gif@0.11.4": "01hbw3isapzpzff8l6aw55jnaqx2bcscrbwyf3rglkbbfp397p9y",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#gif@0.12.0": "0ibhjyrslfv9qm400gp4hd50v9ibva01j4ab9bwiq1aycy9jayc0",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#gimli@0.28.1": "0lv23wc8rxvmjia3mcxc6hj9vkqnv1bqq0h8nzjcgf71mrxx6wa2",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#gio-sys@0.18.1": "1lip8z35iy9d184x2qwjxlbxi64q9cpayy7v1p5y9xdsa3w6smip",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#gio@0.18.4": "0wsc6mnx057s4ailacg99dwgna38dbqli5x7a6y9rdw75x9qzz6l",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#glib-build-tools@0.16.3": "1z73bl10zmxwrv16v4f5wcky1f3z5a2v0hknca54al4k2p5ka695",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#glib-build-tools@0.17.10": "05p7ab2vn8962cbchi7a6hndhvw64nqk4w5kpg5z53iizsgdfrbs",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#glib-build-tools@0.18.0": "0p5c2ayiam5bkp9wvq9f9ihwp06nqs5j801npjlwnhrl8rpwac9l",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#glib-macros@0.18.3": "19crnw5a57w02njpbsmdqwbkncl6hw6g3mv554y8dqzcrri3jybj",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#glib-sys@0.18.1": "164qhsfmlzd5mhyxs8123jzbdfldwxbikfpq5cysj3lddbmy4g06",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#glib@0.18.4": "0kjws6ns6dym48nzxz9skhipk55flc2hy5q5kzg4w12wvizvs6wm",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#glob@0.3.1": "16zca52nglanv23q5qrwd5jinw3d3as5ylya6y1pbx47vkxvrynj",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#gloo-timers@0.3.0": "1519157n7xppkk6pdw5w52vy1llzn5iljkqd7q1h5609jv7l7cdv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#gobject-sys@0.18.0": "0i6fhp3m6vs3wkzyc22rk2cqj68qvgddxmpaai34l72da5xi4l08",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#graphene-rs@0.18.1": "00f4q1ra4haap5i7lazwhkdgnb49fs8adk2nm6ki6mjhl76jh8iv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#graphene-sys@0.18.1": "0n8zlg7z26lwpnvlqp1hjlgrs671skqwagdpm7r8i1zwx3748hfc",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#grid@0.9.0": "0iswdcxggyxp9m1rz0m7bfg4xacinvn78zp2fgfp0l0079x10d06",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#gsk4-sys@0.7.3": "0mbdlm9qi1hql48rr29vsj9vlqwc7gxg67wg1q19z67azwz9xg8j",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#gsk4@0.7.3": "0zhzs2dkgiinhgc11akpn2harq3x5n1iq21dnc4h689g3lsqx58d",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#gtk4-macros@0.7.2": "0bw3cchiycf7dw1bw4p8946gv38azxy05a5w0ndgcmxnz6fc8znm",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#gtk4-sys@0.7.3": "1f2ylskyqkjdik9fij2m46pra4jagnif5xyalbxfk3334fmc9n2l",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#gtk4@0.7.3": "0hh8nzglmz94v1m1h6vy8z12m6fr7ia467ry0md5fa4p7sm53sss",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#h2@0.3.22": "0y41jlflvw8niifdirgng67zdmic62cjf5m2z69hzrpn5qr50qjd",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#half@2.2.1": "1l1gdlzxgm7wc8xl5fxas20kfi1j35iyb7vfjkghbdzijcvazd02",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#hashbrown@0.14.3": "012nywlg0lj9kwanh69my5x67vjlfmzfi9a0rq4qvis2j8fil3r9",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#hashlink@0.8.4": "1xy8agkyp0llbqk9fcffc1xblayrrywlyrm2a7v93x8zygm4y2g8",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#headers-core@0.2.0": "0ab469xfpd411mc3dhmjhmzrhqikzyj8a17jn5bkj9zfpy0n9xp7",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#headers@0.3.9": "0w62gnwh2p1lml0zqdkrx9dp438881nhz32zrzdy61qa0a9kns06",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#heck@0.4.1": "1a7mqsnycv5z4z5vnv1k34548jzmc0ajic7c1j8jsaspnhw5ql4m",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#hermit-abi@0.3.9": "092hxjbjnq5fmz66grd9plxd0sh6ssg5fhgwwwqbrzgzkjwdycfj",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#hex-string@0.1.0": "02sgrgrbp693jv0v5iga7z47y6aj93cq0ia39finby9x17fw53l4",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#hex@0.4.3": "0w1a4davm1lgzpamwnba907aysmlrnygbqmfis2mqjx5m552a93z",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#hkdf@0.12.4": "1xxxzcarz151p1b858yn5skmhyrvn8fs4ivx5km3i1kjmnr8wpvv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#hmac@0.12.1": "0pmbr069sfg76z7wsssfk5ddcqd9ncp79fyz6zcm6yn115yc6jbc",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#home@0.5.9": "19grxyg35rqfd802pcc9ys1q3lafzlcjcv2pl2s5q8xpyr5kblg3",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#http-body@0.4.6": "1lmyjfk6bqk6k9gkn1dxq770sb78pqbqshga241hr5p995bb5skw",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#http@0.2.11": "1fwz3mhh86h5kfnr5767jlx9agpdggclq7xsqx930fflzakb2iw9",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#http@1.0.0": "1sllw565jn8r5w7h928nsfqq33x586pyasdfr7vid01scwwgsamk",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#httparse@1.8.0": "010rrfahm1jss3p022fqf3j3jmm72vhn4iqhykahb9ynpaag75yq",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#httpdate@1.0.3": "1aa9rd2sac0zhjqh24c9xvir96g188zldkx0hr6dnnlx5904cfyz",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#humantime@2.1.0": "1r55pfkkf5v0ji1x6izrjwdq9v6sc7bv99xj6srywcar37xmnfls",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#hyper-tls@0.5.0": "01crgy13102iagakf6q4mb75dprzr7ps1gj0l5hxm1cvm7gks66n",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#hyper@0.10.16": "0wwjh9p3mzvg3fss2lqz5r7ddcgl1fh9w6my2j69d6k0lbcm41ha",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#hyper@0.14.28": "107gkvqx4h9bl17d602zkm2dgpfq86l2dr36yzfsi8l3xcsy35mz",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#iana-time-zone-haiku@0.1.2": "17r6jmj31chn7xs9698r122mapq85mfnv98bb4pg6spm0si2f67k",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#iana-time-zone@0.1.58": "081vcr8z8ddhl5r1ywif6grnswk01b2ac4nks2bhn8zzdimvh9l3",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#idna@0.1.5": "0kl4gs5kaydn4v07c6ka33spm9qdh2np0x7iw7g5zd8z1c7rxw1q",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#idna@0.5.0": "1xhjrcjqq0l5bpzvdgylvpkgk94panxgsirzhjnnqfdgc4a9nkb3",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#image@0.23.14": "18gn2f7xp30pf9aqka877knlq308khxqiwjvsccvzaa4f9zcpzr4",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#image@0.24.7": "04d7f25b8nlszfv9a474n4a0al4m2sv9gqj3yiphhqr0syyzsgbg",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#indent_write@2.2.0": "1hqjp80argdskrhd66g9sh542yxy8qi77j6rc69qd0l7l52rdzhc",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#indexmap@2.1.0": "07rxrqmryr1xfnmhrjlz8ic6jw28v6h5cig3ws2c9d0wifhy2c6m",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#intl-memoizer@0.5.1": "0vx6cji8ifw77zrgipwmvy1i3v43dcm58hwjxpb1h29i98z46463",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#intl_pluralrules@7.0.2": "0wprd3h6h8nfj62d8xk71h178q7zfn3srxm787w4sawsqavsg3h7",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#ipnet@2.9.0": "1hzrcysgwf0knf83ahb3535hrkw63mil88iqc6kjaryfblrqylcg",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#iron@0.6.1": "1s4mf8395f693nhwsr0znw3j5frzn56gzllypyl50il85p50ily6",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#is-terminal@0.4.9": "12xgvc7nsrp3pn8hcxajfhbli2l5wnh3679y2fmky88nhj4qj26b",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#itertools@0.10.5": "0ww45h7nxx5kj6z2y6chlskxd1igvs4j507anr6dzg99x1h25zdh",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#itertools@0.12.0": "1c07gzdlc6a1c8p8jrvvw3gs52bss3y58cs2s21d9i978l36pnr5",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#itoa@1.0.10": "0k7xjfki7mnv6yzjrbnbnjllg86acmbnk4izz2jmm1hx2wd6v95i",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#jpeg-decoder@0.1.22": "1wnh0bmmswpgwhgmlizz545x8334nlbmkq8imy9k224ri3am7792",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#jpeg-decoder@0.3.0": "0gkv0zx95i4fr40fj1a10d70lqi6lfyia8r5q8qjxj8j4pj0005w",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#js-sys@0.3.66": "1ji9la5ydg0vy17q54i7dnwc0wwb9zkx662w1583pblylm6wdsff",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#kv-log-macro@1.0.7": "0zwp4bxkkp87rl7xy2dain77z977rvcry1gmr5bssdbn541v7s0d",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#language-tags@0.2.2": "16hrjdpa827carq5x4b8zhas24d8kg4s16m6nmmn1kb7cr5qh7d9",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#lazy_static@1.4.0": "0in6ikhw8mgl33wjv6q6xfrb5b9jr16q8ygjy803fay4zcisvaz2",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#lazycell@1.3.0": "0m8gw7dn30i0zjjpjdyf6pc16c34nl71lpv461mix50x3p70h3c3",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#lebe@0.5.2": "1j2l6chx19qpa5gqcw434j83gyskq3g2cnffrbl3842ymlmpq203",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#libadwaita-sys@0.5.3": "16n6xsy6jhbj0jbpz8yvql6c9b89a99v9vhdz5s37mg1inisl42y",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#libadwaita@0.5.3": "174pzn9dwsk8ikvrhx13vkh0zrpvb3rhg9yd2q5d2zjh0q6fgrrg",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#libc@0.2.151": "1x28f0zgp4zcwr891p8n9ag9w371sbib30vp4y6hi2052frplb9h",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#libloading@0.8.5": "194dvczq4sifwkzllfmw0qkgvilpha7m5xy90gd6i446vcpz4ya9",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#libm@0.2.8": "0n4hk1rs8pzw8hdfmwn96c4568s93kfxqgcqswr7sajd2diaihjf",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#libspa-sys@0.8.0": "07yh4i5grzbxkchg6dnxlwbdw2wm5jnd7ffbhl77jr0388b9f3dz",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#libspa@0.8.0": "044qs48yl0llp2dmrgwxj9y1pgfy09i6fhq661zqqb9a3fwa9wv5",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#libsqlite3-sys@0.27.0": "05pp60ncrmyjlxxjj187808jkvpxm06w5lvvdwwvxd2qrmnj4kng",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#linked_list_allocator@0.10.5": "11k2dv6v5kq45kbvahll434f9iwfw0vsyaycp76q3vh5ahzldyls",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#linux-raw-sys@0.4.12": "0mhlla3gk1jgn6mrq9s255rvvq8a1w3yk2vpjiwsd6hmmy1imkf4",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#lock_api@0.4.11": "0iggx0h4jx63xm35861106af3jkxq06fpqhpkhgw0axi2n38y5iw",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#log@0.3.9": "0jq23hhn5h35k7pa8r7wqnsywji6x3wn1q5q7lif5q536if8v7p1",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#log@0.4.20": "13rf7wphnwd61vazpxr7fiycin6cb1g8fmvgqg18i464p0y1drmm",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#logger@0.4.0": "14xlxvkspcfnspjil0xi63qj5cybxn1hjmr5gq8m4v1g9k5p54bc",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#matches@0.1.10": "1994402fq4viys7pjhzisj4wcw894l53g798kkm2y74laxk0jci5",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#md-5@0.10.6": "1kvq5rnpm4fzwmyv5nmnxygdhhb2369888a06gdc9pxyrzh7x7nq",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#memchr@2.6.4": "0rq1ka8790ns41j147npvxcqcl2anxyngsdimy85ag2api0fwrgn",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#memoffset@0.9.0": "0v20ihhdzkfw1jx00a7zjpk2dcp5qjq6lz302nyqamd9c4f4nqss",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#mime@0.2.6": "1q1s1ax1gaz8ld3513nvhidfwnik5asbs1ma3hp6inp5dn56nqms",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#mime@0.3.17": "16hkibgvb9klh0w0jk5crr5xv90l3wlf77ggymzjmvl1818vnxv8",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#mime_guess@1.8.8": "18qcd5aa3363mb742y7lf39j7ha88pkzbv9ff2qidlsdxsjjjs91",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#mime_guess@2.0.4": "1vs28rxnbfwil6f48hh58lfcx90klcvg68gxdc60spwa4cy2d4j1",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#minimal-lexical@0.2.1": "16ppc5g84aijpri4jzv14rvcnslvlpphbszc7zzp6vfkddf4qdb8",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#miniz_oxide@0.3.7": "0dblrhgbm0wa8jjl8cjp81akaj36yna92df4z1h9b26n3spal7br",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#miniz_oxide@0.4.4": "0jsfv00hl5rmx1nijn59sr9jmjd4rjnjhh4kdjy8d187iklih9d9",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#miniz_oxide@0.7.1": "1ivl3rbbdm53bzscrd01g60l46lz5krl270487d8lhjvwl5hx0g7",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#mio@1.0.2": "1v1cnnn44awxbcfm4zlavwgkvbyg7gp5zzjm8mqf1apkrwflvq40",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#modifier@0.1.0": "0n3fmgli1nsskl0whrfzm1gk0rmwwl6pw1q4nb9sqqmn5h8wkxa1",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#multer@2.1.0": "1hjiphaypj3phqaj5igrzcia9xfmf4rr4ddigbh8zzb96k1bvb01",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#nary_tree@0.4.3": "1iqray1a716995l9mmvz5sfqrwg9a235bvrkpcn8bcqwjnwfv1pv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#native-tls@0.2.11": "0bmrlg0fmzxaycjpkgkchi93av07v2yf9k33gc12ca9gqdrn28h7",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#nb@0.1.3": "0vyh31pbwrg21f8hz1ipb9i20qwnfwx47gz92i9frdhk0pd327c0",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#nb@1.1.0": "179kbn9l6vhshncycagis7f8mfjppz4fhvgnmcikqz30mp23jm4d",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#nix@0.27.1": "0ly0kkmij5f0sqz35lx9czlbk6zpihb7yh1bsy4irzwfd2f4xc1f",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#no-std-compat@0.4.1": "132vrf710zsdp40yp1z3kgc2ss8pi0z4gmihsz3y7hl4dpd56f5r",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#nom@7.1.3": "0jha9901wxam390jcf5pfa0qqfrgh8li787jx2ip0yk5b8y9hwyj",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#num-bigint-dig@0.8.4": "0lb12df24wgxxbspz4gw1sf1kdqwvpdcpwq4fdlwg4gj41c1k16w",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#num-integer@0.1.45": "1ncwavvwdmsqzxnn65phv6c6nn72pnv9xhpmjd6a429mzf4k6p92",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#num-iter@0.1.43": "0lp22isvzmmnidbq9n5kbdh8gj0zm3yhxv1ddsn5rp65530fc0vx",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#num-rational@0.3.2": "01sgiwny9iflyxh2xz02sak71v2isc3x608hfdpwwzxi3j5l5b0j",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#num-rational@0.4.1": "1c0rb8x4avxy3jvvzv764yk7afipzxncfnqlb10r3h53s34s2f06",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#num-traits@0.2.17": "0z16bi5zwgfysz6765v3rd6whfbjpihx3mhsn4dg8dzj2c221qrr",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#num_cpus@1.16.0": "0hra6ihpnh06dvfvz9ipscys0xfqa9ca9hzp384d5m02ssvgqqa1",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#num_enum@0.5.11": "1japmqhcxwn1d3k7q8jw58y7xfby51s16nzd6dkj483cj2pnqr0z",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#num_enum_derive@0.5.11": "16f7r4jila0ckcgdnfgqyhhb90w9m2pdbwayyqmwcci0j6ygkgyw",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#object@0.32.1": "1c02x4kvqpnl3wn7gz9idm4jrbirbycyqjgiw6lm1g9k77fzkxcw",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#once_cell@1.19.0": "14kvw7px5z96dk4dwdm1r9cqhhy2cyj1l5n5b29mynbb8yr15nrz",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#openssl-macros@0.1.1": "173xxvfc63rr5ybwqwylsir0vq6xsj4kxiv4hmg4c3vscdmncj59",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#openssl-probe@0.1.5": "1kq18qm48rvkwgcggfkqq6pm948190czqc94d6bm2sir5hq1l0gz",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#openssl-sys@0.9.97": "02s670ir38fsavphdna07144y41dkvrcfkwnjzg82zfrrlsavsn3",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#openssl@0.10.61": "0idv3n9n9f2sxq8cqzxvq44633vg5sx4n9q1p3g6dn66ikf1k13b",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#pango-sys@0.18.0": "1iaxalcaaj59cl9n10svh4g50v8jrc1a36kd7n9yahx8j7ikfrs3",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#pango@0.18.3": "1r5ygq7036sv7w32kp8yxr6vgggd54iaavh3yckanmq4xg0px8kw",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#panic-halt@0.2.0": "04nqaa97ph20ppyy58grwr23hrbw83pn0gf7apf73rdx1q7595ny",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#parking@2.2.0": "1blwbkq6im1hfxp5wlbr475mw98rsyc0bbr2d5n16m38z253p0dv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#parking_lot@0.12.1": "13r2xk7mnxfc5g0g6dkdxqdqad99j7s7z8zhzz4npw5r0g0v4hip",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#parking_lot_core@0.9.9": "13h0imw1aq86wj28gxkblhkzx6z1gk8q18n0v76qmmj6cliajhjc",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#parse-zoneinfo@0.3.0": "0h8g6jy4kckn2gk8sd5adaws180n1ip65xhzw5jxlq4w8ibg41f7",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#paste@1.0.14": "0k7d54zz8zrz0623l3xhvws61z5q2wd3hkwim6gylk8212placfy",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#pem-rfc7468@0.7.0": "04l4852scl4zdva31c1z6jafbak0ni5pi0j38ml108zwzjdrrcw8",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#percent-encoding@1.0.1": "0cgq08v1fvr6bs5fvy390cz830lq4fak8havdasdacxcw790s09i",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#percent-encoding@2.3.1": "0gi8wgx0dcy8rnv1kywdv98lwcx67hz0a0zwpib5v2i08r88y573",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#phf@0.11.2": "1p03rsw66l7naqhpgr1a34r9yzi1gv9jh16g3fsk6wrwyfwdiqmd",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#phf@0.7.24": "066xwv4dr6056a9adlkarwp4n94kbpwngbmd47ngm3cfbyw49nmk",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#phf_codegen@0.11.2": "0nia6h4qfwaypvfch3pnq1nd2qj64dif4a6kai3b7rjrsf49dlz8",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#phf_codegen@0.7.24": "0zjiblicfm0nrmr2xxrs6pnf6zz2394wgch6dcbd8jijkq98agmh",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#phf_generator@0.11.2": "1c14pjyxbcpwkdgw109f7581cc5fa3fnkzdq1ikvx7mdq9jcrr28",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#phf_generator@0.7.24": "0qi62gxk3x3whrmw5c4i71406icqk11qmpgln438p6qm7k4lqdh9",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#phf_shared@0.11.2": "0azphb0a330ypqx3qvyffal5saqnks0xvl8rj73jlk3qxxgbkz4h",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#phf_shared@0.7.24": "18371fla0vsj7d6d5rlfb747xbr2in11ar9vgv5qna72bnhp2kr3",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#pin-project-internal@1.1.3": "01a4l3vb84brv9v7wl71chzxra2kynm6yvcjca66xv3ij6fgsna3",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#pin-project-lite@0.2.13": "0n0bwr5qxlf0mhn2xkl36sy55118s9qmvx2yl5f3ixkb007lbywa",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#pin-project@1.1.3": "08k4cpy8q3j93qqgnrbzkcgpn7g0a88l4a9nm33kyghpdhffv97x",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#pin-utils@0.1.0": "117ir7vslsl2z1a7qzhws4pd01cg2d3338c47swjyvqv2n60v1wb",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#pio@0.2.1": "1qvq03nbx6vjix7spr5fcxcbxw39flm1y72kxl1g728gnna9dq3n",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#piper@0.2.1": "1m45fkdq7q5l9mv3b0ra10qwm0kb67rjp2q8y91958gbqjqk33b6",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#pipewire-sys@0.8.0": "04hiy3rl8v3j2dfzp04gr7r8l5azzqqsvqdzwa7sipdij27ii7l4",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#pipewire@0.8.0": "1nldg1hz4v0qr26lzdxqpvrac4zbc3pb6436sl392425bjx4brh8",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#pkcs1@0.7.5": "0zz4mil3nchnxljdfs2k5ab1cjqn7kq5lqp62n9qfix01zqvkzy8",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#pkcs8@0.10.2": "1dx7w21gvn07azszgqd3ryjhyphsrjrmq5mmz1fbxkj5g0vv4l7r",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#pkg-config@0.3.27": "0r39ryh1magcq4cz5g9x88jllsnxnhcqr753islvyk4jp9h2h1r6",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#plugin@0.2.6": "1q7nghkpvxxr168y2jnzh3w7qc9vfrby9n7ygy3xpj0bj71hsshs",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#png@0.16.8": "1ipl44q3vy4kvx6j296vk7d4v8gvcg203lrkvvixwixq1j98fciw",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#png@0.17.10": "0r5a8a25ad0jq2pkp2zbab3wwhpgp6jmdg6d0ybjnw6kilnvyxfx",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#polling@3.4.0": "052am20b5r03nwhpnjw86rv3dwsdabvb07anv3fqxfbs65r4w19h",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#powerfmt@0.2.0": "14ckj2xdpkhv3h6l5sdmb9f1d57z8hbfpdldjc2vl5givq2y77j3",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#ppv-lite86@0.2.17": "1pp6g52aw970adv3x2310n7glqnji96z0a9wiamzw89ibf0ayh2v",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#pretty_env_logger@0.5.0": "076w9dnvcpx6d3mdbkqad8nwnsynb7c8haxmscyrz7g3vga28mw6",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#proc-macro-crate@1.3.1": "069r1k56bvgk0f58dm5swlssfcp79im230affwk6d9ck20g04k3z",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#proc-macro-crate@2.0.1": "06jbv5w6s04dbjbwq0iv7zil12ildf3w8dvvb4pqvhig4gm5zp4p",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#proc-macro-error-attr@1.0.4": "0sgq6m5jfmasmwwy8x4mjygx5l7kp8s4j60bv25ckv2j1qc41gm1",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#proc-macro-error@1.0.4": "1373bhxaf0pagd8zkyd03kkx6bchzf6g0dkwrwzsnal9z47lj9fs",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.78": "1bjak27pqdn4f4ih1c9nr3manzyavsgqmf76ygw9k76q8pb2lhp2",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#proptest@1.4.0": "1gzmw40pgmwzb7x6jsyr88z5w151snv5rp1g0dlcp1iw3h9pdd1i",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#qoi@0.4.1": "00c0wkb112annn2wl72ixyd78mf56p4lxkhlmsggx65l3v3n8vbz",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#quick-error@1.2.3": "1q6za3v78hsspisc197bg3g7rpc989qycy8ypr8ap8igv10ikl51",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#quote@1.0.35": "1vv8r2ncaz4pqdr78x7f138ka595sp2ncr1sa2plm4zxbsmwj7i9",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rand@0.3.23": "0v679h38pjjqj5h4md7v2slsvj6686qgcn7p9fbw3h43iwnk1b34",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rand@0.4.6": "14qjfv3gggzhnma20k0sc1jf8y6pplsaq7n1j9ls5c8kf2wl0a2m",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rand@0.6.5": "1jl4449jcl4wgmzld6ffwqj5gwxrp8zvx8w573g1z368qg6xlwbd",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rand@0.8.5": "013l6931nn7gkc23jz5mm3qdhf93jjf0fg64nz2lp4i51qd8vbrl",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rand_chacha@0.1.1": "1vxwyzs4fy1ffjc8l00fsyygpiss135irjf7nyxgq2v0lqf3lvam",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rand_chacha@0.3.1": "123x2adin558xbhvqb8w4f6syjsdkmqff8cxwhmjacpsl1ihmhg6",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rand_core@0.3.1": "0jzdgszfa4bliigiy4hi66k7fs3gfwi2qxn8vik84ph77fwdwvvs",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rand_core@0.4.2": "1p09ynysrq1vcdlmcqnapq4qakl2yd1ng3kxh3qscpx09k2a6cww",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rand_core@0.6.4": "0b4j2v4cb5krak1pv6kakv4sz6xcwbrmy2zckc32hsigbrwy82zc",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rand_hc@0.1.0": "1i0vl8q5ddvvy0x8hf1zxny393miyzxkwqnw31ifg6p0gdy6fh3v",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rand_isaac@0.1.1": "027flpjr4znx2csxk7gxb7vrf9c7y5mydmvg5az2afgisp4rgnfy",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rand_jitter@0.1.4": "16z387y46bfz3csc42zxbjq89vcr1axqacncvv8qhyy93p4xarhi",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rand_os@0.1.3": "0wahppm0s64gkr2vmhcgwc0lij37in1lgfxg5rbgqlz0l5vgcxbv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rand_pcg@0.1.2": "0i0bdla18a8x4jn1w0fxsbs3jg7ajllz6azmch1zw33r06dv1ydb",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rand_xorshift@0.1.1": "0p2x8nr00hricpi2m6ca5vysiha7ybnghz79yqhhx6sl4gkfkxyb",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rand_xorshift@0.3.0": "13vcag7gmqspzyabfl1gr9ykvxd2142q2agrj8dkyjmfqmgg4nyj",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rayon-core@1.12.0": "1vaq0q71yfvcwlmia0iqf6ixj2fibjcf2xjy92n1m1izv1mgpqsw",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rayon@1.8.0": "1cfdnvchf7j4cpha5jkcrrsr61li9i9lp5ak7xdq6d3pvc1xn9ww",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rdrand@0.4.0": "1cjq0kwx1bk7jx3kzyciiish5gqsj7620dm43dc52sr8fzmm9037",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#redox_syscall@0.4.1": "1aiifyz5dnybfvkk4cdab9p2kmphag1yad6iknc7aszlxxldf8j7",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#regex-automata@0.4.3": "0gs8q9yhd3kcg4pr00ag4viqxnh5l7jpyb9fsfr8hzh451w4r02z",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#regex-syntax@0.8.2": "17rd2s8xbiyf6lb4aj2nfi44zqlj98g2ays8zzj2vfs743k79360",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#regex@1.10.2": "0hxkd814n4irind8im5c9am221ri6bprx49nc7yxv02ykhd9a2rq",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#remove_dir_all@0.5.3": "1rzqbsgkmr053bxxl04vmvsd1njyz0nxvly97aip6aa2cmb15k9s",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#reqwest@0.11.23": "0hgvzb7r46656r9vqhl5qk1kbr2xzjb91yr2cb321160ka6sxc9p",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rp-pico@0.8.0": "0mmx8dyl0q1a9fgz12hrvwd7civqbd1j7g1w5c5i6pcfdwg7fhb3",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rp2040-boot2@0.3.0": "08dv9ndvdzyjz4wdlxcikf1m1s6wwi80027ldkihx59zyr2g74kw",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rp2040-hal-macros@0.1.0": "0piaczzlbrfdhidnqkg04xs1rzal3w3zjplrh6pf3vwpwiir0iw6",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rp2040-hal@0.9.2": "1jk725cf6nx6rhn06swbx47yaq3j134m0hpnv47p5mkdgspbkwhz",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rp2040-pac@0.5.0": "0k3fm4fww6gcy7w7zwbmmqn9wzz4sih13l1m93sl7x8mb0vxin8j",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rsa@0.9.6": "1z0d1aavfm0v4pv8jqmqhhvvhvblla1ydzlvwykpc3mkzhj523jx",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rustc-demangle@0.1.23": "0xnbk2bmyzshacjm2g1kd4zzv2y2az14bw3sjccq5qkpmsfvn9nn",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rustc-hash@1.1.0": "1qkc5khrmv5pqi5l5ca9p5nl5hs742cagrndhbrlk3dhlrx3zm08",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rustc_version@0.2.3": "02h3x57lcr8l2pm0a645s9whdh33pn5cnrwvn5cb57vcrc53x3hk",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rustc_version@0.4.0": "0rpk9rcdk405xhbmgclsh4pai0svn49x35aggl4nhbkd4a2zb85z",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rustix@0.38.28": "05m3vacvbqbg6r6ksmx9k5afpi0lppjdv712crrpsrfax2jp5rbj",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rusty-fork@0.3.0": "0kxwq5c480gg6q0j3bg4zzyfh2kwmc3v2ba94jw8ncjc8mpcqgfb",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#ryu@1.0.16": "0k7b90xr48ag5bzmfjp82rljasw2fx28xr3bg1lrpx7b5sljm3gr",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#safemem@0.3.3": "0wp0d2b2284lw11xhybhaszsczpbq1jbdklkxgifldcknmy3nw7g",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#schannel@0.1.22": "126zy5jb95fc5hvzyjwiq6lc81r08rdcn6affn00ispp9jzk6dqc",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#scoped-tls@1.0.1": "15524h04mafihcvfpgxd8f4bgc3k95aclz8grjkg9a0rxcvn9kz1",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#scoped_threadpool@0.1.9": "1a26d3lk40s9mrf4imhbik7caahmw2jryhhb6vqv6fplbbgzal8x",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#scopeguard@1.2.0": "0jcz9sd47zlsgcnm1hdw0664krxwb5gczlif4qngj2aif8vky54l",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#security-framework-sys@2.9.1": "0yhciwlsy9dh0ps1gw3197kvyqx1bvc4knrhiznhid6kax196cp9",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#security-framework@2.9.2": "1pplxk15s5yxvi2m1sz5xfmjibp96cscdcl432w9jzbk0frlzdh5",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#self_cell@0.10.3": "0pci3zh23b7dg6jmlxbn8k4plb7hcg5jprd1qiz0rp04p1ilskp1",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#self_cell@1.0.2": "1rmdglwnd77wcw2gv76finpgzjhkynx422d0jpahrf2fsqn37273",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#semver-parser@0.7.0": "18vhypw6zgccnrlm5ps1pwa0khz7ry927iznpr88b87cagr1v2iq",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#semver@0.9.0": "00q4lkcj0rrgbhviv9sd4p6qmdsipkwkbra7rh11jrhq5kpvjzhx",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#semver@1.0.20": "140hmbfa743hbmah1zjf07s8apavhvn04204qjigjiz5w6iscvw3",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#serde@0.9.15": "1bsla8l5xr9pp5sirkal6mngxcq6q961km88jvf339j5ff8j7dil",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#serde@1.0.209": "029yqqbb3c8v3gc720fhxn49dhgvb88zbyprdg5621riwzzy1z4r",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#serde_derive@1.0.209": "0w114ksg1ymnmqdisd0g1j3g8jgz6pam45xg6yb47dfpkybip0x5",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#serde_json@1.0.127": "1b99lgg1d986gwz5fbmmzmvjmqg5bx0lzmhy6rqp5gc2kxnw0hw0",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#serde_spanned@0.6.5": "1hgh6s3jjwyzhfk3xwb6pnnr1misq9nflwq0f026jafi37s24dpb",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#serde_urlencoded@0.7.1": "1zgklbdaysj3230xivihs30qi5vkhigg323a9m62k8jwf4a1qjfk",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#sha1@0.10.6": "1fnnxlfg08xhkmwf2ahv634as30l1i3xhlhkvxflmasi5nd85gz3",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#sha2@0.10.8": "1j1x78zk9il95w9iv46dh9wm73r6xrgj32y6lzzw7bxws9dbfgbr",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#shlex@1.3.0": "0r1y6bv26c1scpxvhg2cabimrmwgbp4p3wy6syj9n0c4s3q2znhg",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#signal-hook-registry@1.4.1": "18crkkw5k82bvcx088xlf5g4n3772m24qhzgfan80nda7d3rn8nq",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#signature@2.2.0": "1pi9hd5vqfr3q3k49k37z06p7gs5si0in32qia4mmr1dancr6m3p",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#simd-adler32@0.3.7": "1zkq40c3iajcnr5936gjp9jjh1lpzhy44p3dq3fiw75iwr1w2vfn",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#siphasher@0.2.3": "1b53m53l24lyhr505lwqzrpjyq5qfnic71mynrcfvm43rybf938b",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#siphasher@0.3.11": "03axamhmwsrmh0psdw3gf7c0zc4fyl5yjxfifz9qfka6yhkqid9q",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#slab@0.4.9": "0rxvsgir0qw5lkycrqgb1cxsvxzjv9bmx73bk5y42svnzfba94lg",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#smallvec@1.11.2": "0w79x38f7c0np7hqfmzrif9zmn0avjvvm31b166zdk9d1aad1k2d",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#snowflake@1.3.0": "1wadr7bxdxbmkbqkqsvzan6q1h3mxqpxningi3ss3v9jaav7n817",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#socket2@0.5.5": "1sgq315f1njky114ip7wcy83qlphv9qclprfjwvxcpfblmcsqpvv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#spin@0.5.2": "0b84m6dbzrwf2kxylnw82d3dr8w06av7rfkr8s85fb5f43rwyqvf",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#spin@0.9.8": "0rvam5r0p3a6qhc18scqpvpgb3ckzyqxpgdfyjnghh8ja7byi039",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#spki@0.7.3": "17fj8k5fmx4w9mp27l970clrh5qa7r5sjdvbsln987xhb34dc7nr",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#sqlformat@0.2.3": "0v0p70wjdshj18zgjjac9xlx8hmpx33xhq7g8x9rg4s4gjyvg0ff",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#sqlx-core@0.7.3": "1gdz44yb9qwxv4xl4hv6w4vbqx0zzdlzsf9j9gcj1qir6wy0ljyq",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#sqlx-macros-core@0.7.3": "0h88wahkxa6nam536lhwr1y0yxlr6la8b1x0hs0n88v790clbgfh",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#sqlx-macros@0.7.3": "19gjwisiym07q7ibkp9nkvvbywjh0r5rc572msvzyzadvh01r5l9",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#sqlx-mysql@0.7.3": "190ygz5a3pqcd9vvqjv2i4r1xh8vi53j4272yrld07zpblwrawg3",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#sqlx-postgres@0.7.3": "090wm9s6mm53ggn1xwr183cnn8yxly8rgcksdk4hrlfcnz1hmb6n",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#sqlx-sqlite@0.7.3": "143laha7wf8dmi0xwycwqmvxdcnb25dq7jnqrsgvmis8v6vpc291",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#sqlx@0.7.3": "1kv3hyx7izmmsjqh3l47zrfhjlcblpg20cvnk7pr8dm7klkkr86v",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#stable_deref_trait@1.2.0": "1lxjr8q2n534b2lhkxd6l6wcddzjvnksi58zv11f9y0jjmr15wd8",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#stringprep@0.1.4": "1rkfsf7riynsmqj3hbldfrvmna0i9chx2sz39qdpl40s4d7dfhdv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#strsim@0.10.0": "08s69r4rcrahwnickvi0kq49z524ci50capybln83mg6b473qivk",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#subtle@2.5.0": "1g2yjs7gffgmdvkkq0wrrh0pxds3q0dv6dhkw9cdpbib656xdkc1",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#syn@1.0.109": "0ds2if4600bd59wsv7jjgfkayfzy3hnazs394kz6zdkmna8l3dkj",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#syn@2.0.48": "0gqgfygmrxmp8q32lia9p294kdd501ybn6kn2h4gqza0irik2d8g",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#system-configuration-sys@0.5.0": "1jckxvdr37bay3i9v52izgy52dg690x5xfg3hd394sv2xf4b2px7",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#system-configuration@0.5.1": "1rz0r30xn7fiyqay2dvzfy56cvaa3km74hnbz2d72p97bkf3lfms",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#system-deps@6.2.0": "0c836abhh3k8yn5ymg8wx383ay7n731gkrbbp3gma352yq7mhb9a",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#target-lexicon@0.12.12": "02lk65ik5ffb8vl9qzq02v0df8kxrp16zih78a33mji49789zhql",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tempdir@0.3.7": "1n5n86zxpgd85y0mswrp5cfdisizq2rv3la906g6ipyc03xvbwhm",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tempfile@3.8.1": "1r88v07zdafzf46y63vs39rmzwl4vqd4g2c5qarz9mqa8nnavwby",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#termcolor@1.4.0": "0jfllflbxxffghlq6gx4csv0bv0qv77943dcx01h9zssy39w66zz",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#thiserror-impl@1.0.51": "1ps9ylhlk2vn19fv3cxp40j3wcg1xmb117g2z2fbf4vmg2bj4x01",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#thiserror@1.0.51": "1drvyim21w5sga3izvnvivrdp06l2c24xwbhp0vg1mhn2iz2277i",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tiff@0.6.1": "0ds48vs919ccxa3fv1www7788pzkvpg434ilqkq7sjb5dmqg8lws",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tiff@0.9.0": "04b2fd3clxm0pmdlfip8xj594zyrsfwmh641i6x1gfiz9l7jn5vd",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#time-core@0.1.2": "1wx3qizcihw6z151hywfzzyd1y5dl804ydyxci6qm07vbakpr4pg",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#time-macros@0.2.16": "0gx4ngf5g7ydqa8lf7kh9sy72rd4dhvpi31y1jvswi0288rpw696",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#time@0.1.45": "0nl0pzv9yf56djy8y5dx25nka5pr2q1ivlandb3d24pksgx7ly8v",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#time@0.3.31": "0gjqcdsdbh0r5vi4c2vrj5a6prdviapx731wwn07cvpqqd1blmzn",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tinystr@0.7.5": "1khf3j95bwwksj2hw76nlvwlwpwi4d1j421lj6x35arqqprjph43",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tinyvec@1.6.0": "0l6bl2h62a5m44jdnpn7lmj14rd44via8180i7121fvm73mmrk47",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tinyvec_macros@0.1.1": "081gag86208sc3y6sdkshgw3vysm5d34p431dzw0bshz66ncng0z",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tokio-macros@2.4.0": "0lnpg14h1v3fh2jvnc8cz7cjf0m7z1xgkwfpcyy632g829imjgb9",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tokio-native-tls@0.3.1": "1wkfg6zn85zckmv4im7mv20ca6b1vmlib5xwz9p7g19wjfmpdbmv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tokio-stream@0.1.14": "0hi8hcwavh5sdi1ivc9qc4yvyr32f153c212dpd7sb366y6rhz1r",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tokio-tungstenite@0.21.0": "0f5wj0crsx74rlll97lhw0wk6y12nhdnqvmnjx002hjn08fmcfy8",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tokio-util@0.7.10": "058y6x4mf0fsqji9rfyb77qbfyc50y4pk2spqgj6xsyr693z66al",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tokio@1.39.3": "1xgzhj7bxqqpjaabjkgsx8hi0f600bzj4iyp9f0a9gr3k6dwkawv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#toml@0.8.2": "0g9ysjaqvm2mv8q85xpqfn7hi710hj24sd56k49wyddvvyq8lp8q",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#toml_datetime@0.6.3": "0jsy7v8bdvmzsci6imj8fzgd255fmy5fzp6zsri14yrry7i77nkw",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#toml_edit@0.19.15": "08bl7rp5g6jwmfpad9s8jpw8wjrciadpnbaswgywpr9hv9qbfnqv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#toml_edit@0.20.2": "0f7k5svmxw98fhi28jpcyv7ldr2s3c867pjbji65bdxjpd44svir",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tower-service@0.3.2": "0lmfzmmvid2yp2l36mbavhmqgsvzqf7r2wiwz73ml4xmwaf1rg5n",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tracing-attributes@0.1.27": "1rvb5dn9z6d0xdj14r403z0af0bbaqhg02hq4jc97g5wds6lqw1l",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tracing-core@0.1.32": "0m5aglin3cdwxpvbg6kz0r9r0k31j48n0kcfwsp6l49z26k3svf0",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tracing@0.1.40": "1vv48dac9zgj9650pg2b4d0j3w6f3x9gbggf43scq5hrlysklln3",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#traitobject@0.1.0": "0yb0n8822mr59j200fyr2fxgzzgqljyxflx9y8bdy3rlaqngilgg",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#try-lock@0.2.5": "0jqijrrvm1pyq34zn1jmy2vihd4jcrjlvsh4alkjahhssjnsn8g4",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#tungstenite@0.21.0": "1qaphb5kgwgid19p64grhv2b9kxy7f1059yy92l9kwrlx90sdwcy",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#type-map@0.4.0": "0ilsqq7pcl3k9ggxv2x5fbxxfd6x7ljsndrhc38jmjwnbr63dlxn",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#typeable@0.1.2": "11w8dywgnm32hb291izjvh4zjd037ccnkk77ahk63l913zwzc40l",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#typemap@0.3.3": "1xm1gbvz9qisj1l6d36hrl9pw8imr8ngs6qyanjnsad3h0yfcfv5",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#typenum@1.17.0": "09dqxv69m9lj9zvv6xw5vxaqx15ps0vxyy5myg33i0kbqvq0pzs2",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#typeshare-annotation@1.0.2": "1adpfhyz3lqjjbq2ym69mv62ymqyd5651gxlqdy8aa446l70srzw",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#typeshare@1.0.1": "1mi7snkx2b4g84x8vx38v1myg5r6g48c865j0nz5zcsc8lpilkgl",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#unarray@0.1.4": "154smf048k84prsdgh09nkm2n0w0336v84jd4zikyn6v6jrqbspa",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#unic-langid-impl@0.9.4": "1ijvqmsrg6qw3b1h9bh537pvwk2jn2kl6ck3z3qlxspxcch5mmab",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#unic-langid@0.9.4": "05pm5p3j29c9jw9a4dr3v64g3x6g3zh37splj47i7vclszk251r3",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#unicase@1.4.2": "0cwazh4qsmm9msckjk86zc1z35xg7hjxjykrgjalzdv367w6aivz",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#unicase@2.7.0": "12gd74j79f94k4clxpf06l99wiv4p30wjr0qm04ihqk9zgdd9lpp",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#unicode-bidi@0.3.14": "05i4ps31vskq1wdp8yf315fxivyh1frijly9d4gb5clygbr2h9bg",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#unicode-ident@1.0.12": "0jzf1znfpb2gx8nr8mvmyqs1crnv79l57nxnbiszc7xf7ynbjm1k",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#unicode-normalization@0.1.22": "08d95g7b1irc578b2iyhzv4xhsa4pfvwsqxcl9lbcpabzkq16msw",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#unicode-segmentation@1.10.1": "0dky2hm5k51xy11hc3nk85p533rvghd462b6i0c532b7hl4j9mhx",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#unicode-width@0.1.13": "0p92vl8n7qc8mxz45xn6qbgi0259z96n32a158l6vj5bywwdadh3",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#unicode_categories@0.1.1": "0kp1d7fryxxm7hqywbk88yb9d1avsam9sg76xh36k5qx2arj9v1r",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#unsafe-any@0.4.2": "0zwwphsqkw5qaiqmjwngnfpv9ym85qcsyj7adip9qplzjzbn00zk",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#url@1.7.2": "0nim1c90mxpi9wgdw2xh8dqd72vlklwlzam436akcrhjac6pqknx",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#url@2.5.0": "0cs65961miawncdg2z20171w0vqrmraswv2ihdpd8lxp7cp31rii",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#urlencoding@2.1.3": "1nj99jp37k47n0hvaz5fvz7z6jd0sb4ppvfy3nphr1zbnyixpy6s",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#usb-device@0.2.9": "0205a850jhw9gb96scwfx1k4iwpjvighvz3m80mjkda9r2nw6v0z",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#utf-8@0.7.6": "1a9ns3fvgird0snjkd3wbdhwd3zdpc2h5gpyybrfr6ra5pkqxk09",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#utf8parse@0.2.1": "02ip1a0az0qmc2786vxk2nqwsgcwf17d3a38fkf0q7hrmwh9c6vi",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#uuid@0.4.0": "0cdj2v6v2yy3zyisij69waksd17cyir1n58kwyk1n622105wbzkw",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#uuid@0.8.2": "1dy4ldcp7rnzjy56dxh7d2sgrcvn4q77y0a8r0a48946h66zjp5w",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#uuid@1.6.1": "0q45jxahvysldn3iy04m8xmr8hgig80855y9gq9di8x72v7myfay",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#value-bag@1.7.0": "02r8wccrzi3bzlkrslkcfw9pwp8kwif9szif2i9arn9dzqx44vhj",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#vcell@0.1.3": "00n0ss2z3rh0ihig6d4w7xp72g58f7g1m6s5v4h3nc6jacdrqhvp",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#vcpkg@0.2.15": "09i4nf5y8lig6xgj3f7fyrvzd3nlaw4znrihw8psidvv5yk4xkdc",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#version-compare@0.1.1": "0acg4pmjdbmclg0m7yhijn979mdy66z3k8qrcnvn634f1gy456jp",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#version_check@0.1.5": "1pf91pvj8n6akh7w6j5ypka6aqz08b3qpzgs0ak2kjf4frkiljwi",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#version_check@0.9.4": "0gs8grwdlgh0xq660d7wr80x14vxbizmd8dbp29p2pdncx8lp1s9",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#void@1.0.2": "0zc8f0ksxvmhvgx4fdg0zyn6vdnbxd2xv9hfx4nhzg6kbs4f80ka",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#volatile-register@0.2.2": "1k0rkm81qyhn4r8f03z0sch2kyikkgjjfalpaami9c08c8m7whyy",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#wait-timeout@0.2.0": "1xpkk0j5l9pfmjfh1pi0i89invlavfrd9av5xp0zhxgb29dhy84z",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#want@0.3.1": "03hbfrnvqqdchb5kgxyavb9jabwza0dmh2vw5kg0dq8rxl57d9xz",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#warp@0.3.7": "07137zd13lchy5hxpspd0hs6sl19b0fv2zc1chf02nwnzw1d4y23",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#wasi@0.10.0+wasi-snapshot-preview1": "07y3l8mzfzzz4cj09c8y90yak4hpsi9g7pllyzpr6xvwrabka50s",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#wasi@0.11.0+wasi-snapshot-preview1": "08z4hxwkpdpalxjps1ai9y7ihin26y9f476i53dv98v45gkqg3cw",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen-backend@0.2.89": "09l8lyylsdssz993h4fzja69zpvpykaw84fivs210fjgwqjzcmhv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen-futures@0.4.39": "04lsxpw4jqfwh7c0crzx0smj52nvwp1w3bh4098sq90149da2dmc",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen-macro-support@0.2.89": "10sj1gr2naxv5q116yjb929hhpvz45dxbkvyk8hyc2lknzy85szh",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen-macro@0.2.89": "1cl2w7k5jn2jbd5kx613c8k8vjvda22hfgcgx7y2mk93fbrxnqh1",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen-shared@0.2.89": "17s5rppad113c6ggkaq8c3cg7a3zz15i78wxcg6mcl1n15iv7fbs",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen@0.2.89": "0kh6akdldy13z9xqj0skz6b4npq1d98bjkgzb8ccq59hibvd9l0f",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#web-sys@0.3.66": "03q1z22djv5ncqkyydcvnchmdsl5gvnyzcyixkxnifw6xi24mhjh",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#weezl@0.1.7": "1frdbq6y5jn2j93i20hc80swpkj30p1wffwxj1nr4fp09m6id4wi",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#whoami@1.4.1": "0l6ca9pl92wmngsn1dh9ih716v216nmn2zvcn94k04x9p1b3gz12",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#winapi-i686-pc-windows-gnu@0.4.0": "1dmpa6mvcvzz16zg6d5vrfy4bxgg541wxrcip7cnshi06v38ffxc",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#winapi-util@0.1.6": "15i5lm39wd44004i9d5qspry2cynkrpvwzghr6s2c3dsk28nz7pj",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#winapi-x86_64-pc-windows-gnu@0.4.0": "0gqq64czqb64kskjryj8isp62m2sgvx25yyj3kpc2myh85w24bki",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#winapi@0.3.9": "06gl025x418lchw1wxj64ycr7gha83m44cjr5sarhynd9xkrm0sw",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows-core@0.51.1": "0r1f57hsshsghjyc7ypp2s0i78f7b1vr93w68sdb8baxyf2czy7i",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows-sys@0.48.0": "1aan23v5gs7gya1lc46hqn9mdh8yph3fhxmhxlw36pn6pqc28zb7",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows-sys@0.52.0": "0gd3v4ji88490zgb6b5mq5zgbvwv7zx1ibn8v3x83rwcdbryaar8",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows-targets@0.48.5": "034ljxqshifs1lan89xwpcy1hp0lhdh4b5n0d2z4fwjx2piacbws",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows-targets@0.52.0": "1kg7a27ynzw8zz3krdgy6w5gbqcji27j1sz4p7xk2j5j8082064a",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows_aarch64_gnullvm@0.48.5": "1n05v7qblg1ci3i567inc7xrkmywczxrs1z3lj3rkkxw18py6f1b",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows_aarch64_gnullvm@0.52.0": "1shmn1kbdc0bpphcxz0vlph96bxz0h1jlmh93s9agf2dbpin8xyb",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows_aarch64_msvc@0.48.5": "1g5l4ry968p73g6bg6jgyvy9lb8fyhcs54067yzxpcpkf44k2dfw",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows_aarch64_msvc@0.52.0": "1vvmy1ypvzdvxn9yf0b8ygfl85gl2gpcyvsvqppsmlpisil07amv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows_i686_gnu@0.48.5": "0gklnglwd9ilqx7ac3cn8hbhkraqisd0n83jxzf9837nvvkiand7",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows_i686_gnu@0.52.0": "04zkglz4p3pjsns5gbz85v4s5aw102raz4spj4b0lmm33z5kg1m2",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows_i686_msvc@0.48.5": "01m4rik437dl9rdf0ndnm2syh10hizvq0dajdkv2fjqcywrw4mcg",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows_i686_msvc@0.52.0": "16kvmbvx0vr0zbgnaz6nsks9ycvfh5xp05bjrhq65kj623iyirgz",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows_x86_64_gnu@0.48.5": "13kiqqcvz2vnyxzydjh73hwgigsdr2z1xpzx313kxll34nyhmm2k",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows_x86_64_gnu@0.52.0": "1zdy4qn178sil5sdm63lm7f0kkcjg6gvdwmcprd2yjmwn8ns6vrx",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows_x86_64_gnullvm@0.48.5": "1k24810wfbgz8k48c2yknqjmiigmql6kk3knmddkv8k8g1v54yqb",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows_x86_64_gnullvm@0.52.0": "17lllq4l2k1lqgcnw1cccphxp9vs7inq99kjlm2lfl9zklg7wr8s",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows_x86_64_msvc@0.48.5": "0f4mdp895kkjh9zv8dxvn4pc10xr7839lf5pa9l0193i2pkgr57d",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#windows_x86_64_msvc@0.52.0": "012wfq37f18c09ij5m6rniw7xxn5fcvrxbqd0wd8vgnl3hfn9yfz",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#winnow@0.5.30": "1ifj9vnqna5qp0d7nb9mrinzf8j7zi1m0gv75870vm91jyw3sp4v",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#winreg@0.50.0": "1cddmp929k882mdh6i9f2as848f13qqna6czwsqzkh1pqnr5fkjj",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#yansi-term@0.1.2": "1w8vjlvxba6yvidqdvxddx3crl6z66h39qxj8xi6aqayw2nk0p7y",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#zerocopy-derive@0.7.31": "06k0zk4x4n9s1blgxmxqb1g81y8q334aayx61gyy6v9y1dajkhdk",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#zerocopy@0.7.31": "0gcfyrmlrhmsz16qxjp2qzr6vixyaw1p04zl28f08lxkvfz62h0w",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#zeroize@1.7.0": "0bfvby7k9pdp6623p98yz2irqnamcyzpn7zh20nqmdn68b0lwnsj",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#zune-inflate@0.2.54": "00kg24jh3zqa3i6rg6yksnb71bch9yi1casqydl00s7nw8pk7avk"
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -2,11 +2,12 @@
|
||||||
name = "cyberpunk-splash"
|
name = "cyberpunk-splash"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
license = "GPL-3.0-only"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
cairo-rs = { version = "0.17" }
|
cairo-rs = { version = "0.18" }
|
||||||
gio = { version = "0.17" }
|
gio = { version = "0.18" }
|
||||||
glib = { version = "0.17" }
|
glib = { version = "0.18" }
|
||||||
gtk = { version = "0.6", package = "gtk4" }
|
gtk = { version = "0.7", package = "gtk4" }
|
||||||
|
|
|
@ -2,8 +2,8 @@ use cairo::{
|
||||||
Context, FontSlant, FontWeight, Format, ImageSurface, LineCap, LinearGradient, Pattern,
|
Context, FontSlant, FontWeight, Format, ImageSurface, LineCap, LinearGradient, Pattern,
|
||||||
TextExtents,
|
TextExtents,
|
||||||
};
|
};
|
||||||
use glib::{GString, Object};
|
use glib::Object;
|
||||||
use gtk::{gdk::Key, prelude::*, subclass::prelude::*, EventControllerKey};
|
use gtk::{prelude::*, subclass::prelude::*, EventControllerKey};
|
||||||
use std::{
|
use std::{
|
||||||
cell::RefCell,
|
cell::RefCell,
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
|
@ -14,12 +14,6 @@ use std::{
|
||||||
const WIDTH: i32 = 1600;
|
const WIDTH: i32 = 1600;
|
||||||
const HEIGHT: i32 = 600;
|
const HEIGHT: i32 = 600;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
|
||||||
enum Event {
|
|
||||||
Frames(u8),
|
|
||||||
Time(Duration),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub enum State {
|
pub enum State {
|
||||||
Running {
|
Running {
|
||||||
|
@ -50,7 +44,7 @@ impl State {
|
||||||
*self = Self::Running {
|
*self = Self::Running {
|
||||||
last_update: Instant::now(),
|
last_update: Instant::now(),
|
||||||
deadline: Instant::now() + *time_remaining,
|
deadline: Instant::now() + *time_remaining,
|
||||||
timeout: timeout.clone(),
|
timeout: *timeout,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -62,7 +56,7 @@ impl State {
|
||||||
{
|
{
|
||||||
*self = Self::Paused {
|
*self = Self::Paused {
|
||||||
time_remaining: *deadline - Instant::now(),
|
time_remaining: *deadline - Instant::now(),
|
||||||
timeout: timeout.clone(),
|
timeout: *timeout,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -108,13 +102,13 @@ impl TimeoutAnimation {
|
||||||
fn tick(&mut self, frames_elapsed: u8) {
|
fn tick(&mut self, frames_elapsed: u8) {
|
||||||
let step_size = 1. / (self.duration * 60.);
|
let step_size = 1. / (self.duration * 60.);
|
||||||
if self.ascending {
|
if self.ascending {
|
||||||
self.intensity = self.intensity + step_size * frames_elapsed as f64;
|
self.intensity += step_size * frames_elapsed as f64;
|
||||||
if self.intensity > 1. {
|
if self.intensity > 1. {
|
||||||
self.intensity = 1.0;
|
self.intensity = 1.0;
|
||||||
self.ascending = false;
|
self.ascending = false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.intensity = self.intensity - step_size * frames_elapsed as f64;
|
self.intensity -= step_size * frames_elapsed as f64;
|
||||||
if self.intensity < 0. {
|
if self.intensity < 0. {
|
||||||
self.intensity = 0.0;
|
self.intensity = 0.0;
|
||||||
self.ascending = true;
|
self.ascending = true;
|
||||||
|
@ -143,6 +137,14 @@ impl SplashPrivate {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn redraw_background(&self) {
|
fn redraw_background(&self) {
|
||||||
|
let pen = GlowPen::new(
|
||||||
|
*self.width.borrow(),
|
||||||
|
*self.height.borrow(),
|
||||||
|
2.,
|
||||||
|
8.,
|
||||||
|
(0.7, 0., 1.),
|
||||||
|
);
|
||||||
|
|
||||||
let background =
|
let background =
|
||||||
ImageSurface::create(Format::Rgb24, *self.width.borrow(), *self.height.borrow())
|
ImageSurface::create(Format::Rgb24, *self.width.borrow(), *self.height.borrow())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -152,6 +154,43 @@ impl SplashPrivate {
|
||||||
let _ = context.paint();
|
let _ = context.paint();
|
||||||
|
|
||||||
context.select_font_face("Alegreya Sans SC", FontSlant::Normal, FontWeight::Bold);
|
context.select_font_face("Alegreya Sans SC", FontSlant::Normal, FontWeight::Bold);
|
||||||
|
|
||||||
|
{
|
||||||
|
context.set_source_rgb(0.7, 0., 1.);
|
||||||
|
|
||||||
|
let hashtag = "#CodingTogether";
|
||||||
|
context.set_font_size(64.);
|
||||||
|
let extents = context.text_extents(hashtag).unwrap();
|
||||||
|
|
||||||
|
context.move_to(20., extents.height() + 40.);
|
||||||
|
let _ = context.show_text(hashtag);
|
||||||
|
|
||||||
|
AsymLine {
|
||||||
|
orientation: gtk::Orientation::Horizontal,
|
||||||
|
start_x: 10.,
|
||||||
|
start_y: extents.height() + 10.,
|
||||||
|
start_length: 0.,
|
||||||
|
height: extents.height() / 2.,
|
||||||
|
total_length: extents.width() + extents.height() / 2.,
|
||||||
|
invert: false,
|
||||||
|
}
|
||||||
|
.draw(&pen);
|
||||||
|
pen.stroke();
|
||||||
|
|
||||||
|
AsymLine {
|
||||||
|
orientation: gtk::Orientation::Horizontal,
|
||||||
|
start_x: 20.,
|
||||||
|
start_y: extents.height() + 60.,
|
||||||
|
start_length: extents.width(),
|
||||||
|
height: extents.height() / 2.,
|
||||||
|
total_length: extents.width() + extents.height() / 2.,
|
||||||
|
invert: false,
|
||||||
|
}
|
||||||
|
.draw(&pen);
|
||||||
|
pen.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
context.set_font_size(128.);
|
context.set_font_size(128.);
|
||||||
|
|
||||||
let center_x = *self.width.borrow() as f64 / 2.;
|
let center_x = *self.width.borrow() as f64 / 2.;
|
||||||
|
@ -175,11 +214,8 @@ impl SplashPrivate {
|
||||||
invert: false,
|
invert: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
context.set_line_cap(LineCap::Round);
|
title_cutout.draw(&pen);
|
||||||
context.set_source_rgb(0.7, 0., 1.);
|
pen.stroke();
|
||||||
context.set_line_width(2.);
|
|
||||||
title_cutout.draw(&context);
|
|
||||||
let _ = context.stroke();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -198,9 +234,9 @@ impl SplashPrivate {
|
||||||
let _ = context.set_source(gradient);
|
let _ = context.set_source(gradient);
|
||||||
let _ = context.show_text(&self.text.borrow());
|
let _ = context.show_text(&self.text.borrow());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
context.set_source_rgb(0.7, 0., 1.);
|
|
||||||
AsymLine {
|
AsymLine {
|
||||||
orientation: gtk::Orientation::Horizontal,
|
orientation: gtk::Orientation::Horizontal,
|
||||||
start_x: 100.,
|
start_x: 100.,
|
||||||
|
@ -210,8 +246,8 @@ impl SplashPrivate {
|
||||||
total_length: 650.,
|
total_length: 650.,
|
||||||
invert: true,
|
invert: true,
|
||||||
}
|
}
|
||||||
.draw(&context);
|
.draw(&pen);
|
||||||
let _ = context.stroke();
|
pen.stroke();
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -225,10 +261,14 @@ impl SplashPrivate {
|
||||||
total_length: 650.,
|
total_length: 650.,
|
||||||
invert: false,
|
invert: false,
|
||||||
}
|
}
|
||||||
.draw(&context);
|
.draw(&pen);
|
||||||
let _ = context.stroke();
|
pen.stroke();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let tracery = pen.finish();
|
||||||
|
let _ = context.set_source(tracery);
|
||||||
|
let _ = context.paint();
|
||||||
|
|
||||||
let background = context.pop_group().unwrap();
|
let background = context.pop_group().unwrap();
|
||||||
|
|
||||||
*self.background.borrow_mut() = background;
|
*self.background.borrow_mut() = background;
|
||||||
|
@ -286,7 +326,7 @@ impl Splash {
|
||||||
let _ = context.set_source(&*background);
|
let _ = context.set_source(&*background);
|
||||||
let _ = context.paint();
|
let _ = context.paint();
|
||||||
|
|
||||||
let state = s.imp().state.borrow().clone();
|
let state = *s.imp().state.borrow();
|
||||||
|
|
||||||
let time = match state {
|
let time = match state {
|
||||||
State::Running { deadline, .. } => deadline - Instant::now(),
|
State::Running { deadline, .. } => deadline - Instant::now(),
|
||||||
|
@ -312,7 +352,7 @@ impl Splash {
|
||||||
|
|
||||||
let mut saved_extents = s.imp().time_extents.borrow_mut();
|
let mut saved_extents = s.imp().time_extents.borrow_mut();
|
||||||
if saved_extents.is_none() {
|
if saved_extents.is_none() {
|
||||||
*saved_extents = Some(time_extents.clone());
|
*saved_extents = Some(time_extents);
|
||||||
}
|
}
|
||||||
|
|
||||||
let time_baseline_x = center_x - time_extents.width() / 2.;
|
let time_baseline_x = center_x - time_extents.width() / 2.;
|
||||||
|
@ -325,8 +365,8 @@ impl Splash {
|
||||||
time_baseline_y,
|
time_baseline_y,
|
||||||
);
|
);
|
||||||
let (running, timeout_animation) = match state {
|
let (running, timeout_animation) = match state {
|
||||||
State::Running { timeout, .. } => (true, timeout.clone()),
|
State::Running { timeout, .. } => (true, timeout),
|
||||||
State::Paused { timeout, .. } => (false, timeout.clone()),
|
State::Paused { timeout, .. } => (false, timeout),
|
||||||
};
|
};
|
||||||
match timeout_animation {
|
match timeout_animation {
|
||||||
Some(ref animation) => {
|
Some(ref animation) => {
|
||||||
|
@ -348,8 +388,7 @@ impl Splash {
|
||||||
let _ = context.show_text(&time);
|
let _ = context.show_text(&time);
|
||||||
};
|
};
|
||||||
|
|
||||||
match *s.imp().time_extents.borrow() {
|
if let Some(extents) = *s.imp().time_extents.borrow() {
|
||||||
Some(extents) => {
|
|
||||||
context.set_source_rgb(0.7, 0.0, 1.0);
|
context.set_source_rgb(0.7, 0.0, 1.0);
|
||||||
let time_meter = SlashMeter {
|
let time_meter = SlashMeter {
|
||||||
orientation: gtk::Orientation::Horizontal,
|
orientation: gtk::Orientation::Horizontal,
|
||||||
|
@ -360,9 +399,7 @@ impl Splash {
|
||||||
height: 60.,
|
height: 60.,
|
||||||
length: 100.,
|
length: 100.,
|
||||||
};
|
};
|
||||||
time_meter.draw(&context);
|
time_meter.draw(context);
|
||||||
}
|
|
||||||
None => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -394,7 +431,7 @@ struct AsymLineCutout {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsymLineCutout {
|
impl AsymLineCutout {
|
||||||
fn draw(&self, context: &Context) {
|
fn draw(&self, pen: &impl Pen) {
|
||||||
let dodge = if self.invert {
|
let dodge = if self.invert {
|
||||||
self.height
|
self.height
|
||||||
} else {
|
} else {
|
||||||
|
@ -402,17 +439,17 @@ impl AsymLineCutout {
|
||||||
};
|
};
|
||||||
match self.orientation {
|
match self.orientation {
|
||||||
gtk::Orientation::Horizontal => {
|
gtk::Orientation::Horizontal => {
|
||||||
context.move_to(self.start_x, self.start_y);
|
pen.move_to(self.start_x, self.start_y);
|
||||||
context.line_to(self.start_x + self.start_length, self.start_y);
|
pen.line_to(self.start_x + self.start_length, self.start_y);
|
||||||
context.line_to(
|
pen.line_to(
|
||||||
self.start_x + self.start_length + self.height,
|
self.start_x + self.start_length + self.height,
|
||||||
self.start_y + dodge,
|
self.start_y + dodge,
|
||||||
);
|
);
|
||||||
context.line_to(
|
pen.line_to(
|
||||||
self.start_x + self.start_length + self.height + self.cutout_length,
|
self.start_x + self.start_length + self.height + self.cutout_length,
|
||||||
self.start_y + dodge,
|
self.start_y + dodge,
|
||||||
);
|
);
|
||||||
context.line_to(
|
pen.line_to(
|
||||||
self.start_x
|
self.start_x
|
||||||
+ self.start_length
|
+ self.start_length
|
||||||
+ self.height
|
+ self.height
|
||||||
|
@ -420,20 +457,20 @@ impl AsymLineCutout {
|
||||||
+ (self.height / 2.),
|
+ (self.height / 2.),
|
||||||
self.start_y + dodge / 2.,
|
self.start_y + dodge / 2.,
|
||||||
);
|
);
|
||||||
context.line_to(self.total_length, self.start_y + dodge / 2.);
|
pen.line_to(self.total_length, self.start_y + dodge / 2.);
|
||||||
}
|
}
|
||||||
gtk::Orientation::Vertical => {
|
gtk::Orientation::Vertical => {
|
||||||
context.move_to(self.start_x, self.start_y);
|
pen.move_to(self.start_x, self.start_y);
|
||||||
context.line_to(self.start_x, self.start_y + self.start_length);
|
pen.line_to(self.start_x, self.start_y + self.start_length);
|
||||||
context.line_to(
|
pen.line_to(
|
||||||
self.start_x + dodge,
|
self.start_x + dodge,
|
||||||
self.start_y + self.start_length + self.height,
|
self.start_y + self.start_length + self.height,
|
||||||
);
|
);
|
||||||
context.line_to(
|
pen.line_to(
|
||||||
self.start_x + dodge,
|
self.start_x + dodge,
|
||||||
self.start_y + self.start_length + self.height + self.cutout_length,
|
self.start_y + self.start_length + self.height + self.cutout_length,
|
||||||
);
|
);
|
||||||
context.line_to(
|
pen.line_to(
|
||||||
self.start_x + dodge / 2.,
|
self.start_x + dodge / 2.,
|
||||||
self.start_y
|
self.start_y
|
||||||
+ self.start_length
|
+ self.start_length
|
||||||
|
@ -441,7 +478,7 @@ impl AsymLineCutout {
|
||||||
+ self.cutout_length
|
+ self.cutout_length
|
||||||
+ (self.height / 2.),
|
+ (self.height / 2.),
|
||||||
);
|
);
|
||||||
context.line_to(self.start_x + dodge / 2., self.total_length);
|
pen.line_to(self.start_x + dodge / 2., self.total_length);
|
||||||
}
|
}
|
||||||
_ => panic!("unknown orientation"),
|
_ => panic!("unknown orientation"),
|
||||||
}
|
}
|
||||||
|
@ -459,7 +496,7 @@ struct AsymLine {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsymLine {
|
impl AsymLine {
|
||||||
fn draw(&self, context: &Context) {
|
fn draw(&self, pen: &impl Pen) {
|
||||||
let dodge = if self.invert {
|
let dodge = if self.invert {
|
||||||
self.height
|
self.height
|
||||||
} else {
|
} else {
|
||||||
|
@ -467,13 +504,13 @@ impl AsymLine {
|
||||||
};
|
};
|
||||||
match self.orientation {
|
match self.orientation {
|
||||||
gtk::Orientation::Horizontal => {
|
gtk::Orientation::Horizontal => {
|
||||||
context.move_to(self.start_x, self.start_y);
|
pen.move_to(self.start_x, self.start_y);
|
||||||
context.line_to(self.start_x + self.start_length, self.start_y);
|
pen.line_to(self.start_x + self.start_length, self.start_y);
|
||||||
context.line_to(
|
pen.line_to(
|
||||||
self.start_x + self.start_length + self.height,
|
self.start_x + self.start_length + self.height,
|
||||||
self.start_y + dodge,
|
self.start_y + dodge,
|
||||||
);
|
);
|
||||||
context.line_to(self.start_x + self.total_length, self.start_y + dodge);
|
pen.line_to(self.start_x + self.total_length, self.start_y + dodge);
|
||||||
}
|
}
|
||||||
gtk::Orientation::Vertical => {}
|
gtk::Orientation::Vertical => {}
|
||||||
_ => panic!("unknown orientation"),
|
_ => panic!("unknown orientation"),
|
||||||
|
@ -481,36 +518,6 @@ impl AsymLine {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct RoundedRectangle {
|
|
||||||
x: f64,
|
|
||||||
y: f64,
|
|
||||||
width: f64,
|
|
||||||
height: f64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RoundedRectangle {
|
|
||||||
fn draw(&self, context: &Context) {
|
|
||||||
context.arc(
|
|
||||||
self.x,
|
|
||||||
self.y - self.height / 2.,
|
|
||||||
self.height / 2.,
|
|
||||||
0.5 * std::f64::consts::PI,
|
|
||||||
1.5 * std::f64::consts::PI,
|
|
||||||
);
|
|
||||||
let _ = context.fill();
|
|
||||||
context.arc(
|
|
||||||
self.x + self.width,
|
|
||||||
self.y - self.height / 2.,
|
|
||||||
self.height / 2.,
|
|
||||||
1.5 * std::f64::consts::PI,
|
|
||||||
0.5 * std::f64::consts::PI,
|
|
||||||
);
|
|
||||||
let _ = context.fill();
|
|
||||||
context.rectangle(self.x, self.y, self.width, -self.height);
|
|
||||||
let _ = context.fill();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct SlashMeter {
|
struct SlashMeter {
|
||||||
orientation: gtk::Orientation,
|
orientation: gtk::Orientation,
|
||||||
start_x: f64,
|
start_x: f64,
|
||||||
|
@ -527,7 +534,7 @@ impl SlashMeter {
|
||||||
gtk::Orientation::Horizontal => {
|
gtk::Orientation::Horizontal => {
|
||||||
let angle: f64 = 0.8;
|
let angle: f64 = 0.8;
|
||||||
let run = self.height / angle.tan();
|
let run = self.height / angle.tan();
|
||||||
let width = self.length as f64 / (self.count as f64 * 2.);
|
let width = self.length / (self.count as f64 * 2.);
|
||||||
|
|
||||||
for c in 0..self.count {
|
for c in 0..self.count {
|
||||||
context.set_line_width(1.);
|
context.set_line_width(1.);
|
||||||
|
@ -551,6 +558,74 @@ impl SlashMeter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
trait Pen {
|
||||||
|
fn move_to(&self, x: f64, y: f64);
|
||||||
|
fn line_to(&self, x: f64, y: f64);
|
||||||
|
fn stroke(&self);
|
||||||
|
|
||||||
|
fn finish(self) -> Pattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GlowPen {
|
||||||
|
blur_context: Context,
|
||||||
|
draw_context: Context,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GlowPen {
|
||||||
|
fn new(
|
||||||
|
width: i32,
|
||||||
|
height: i32,
|
||||||
|
line_width: f64,
|
||||||
|
blur_line_width: f64,
|
||||||
|
color: (f64, f64, f64),
|
||||||
|
) -> Self {
|
||||||
|
let blur_context =
|
||||||
|
Context::new(ImageSurface::create(Format::Rgb24, width, height).unwrap()).unwrap();
|
||||||
|
blur_context.set_line_width(blur_line_width);
|
||||||
|
blur_context.set_source_rgba(color.0, color.1, color.2, 0.5);
|
||||||
|
blur_context.push_group();
|
||||||
|
blur_context.set_line_cap(LineCap::Round);
|
||||||
|
|
||||||
|
let draw_context =
|
||||||
|
Context::new(ImageSurface::create(Format::Rgb24, width, height).unwrap()).unwrap();
|
||||||
|
draw_context.set_line_width(line_width);
|
||||||
|
draw_context.set_source_rgb(color.0, color.1, color.2);
|
||||||
|
draw_context.push_group();
|
||||||
|
draw_context.set_line_cap(LineCap::Round);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
blur_context,
|
||||||
|
draw_context,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Pen for GlowPen {
|
||||||
|
fn move_to(&self, x: f64, y: f64) {
|
||||||
|
self.blur_context.move_to(x, y);
|
||||||
|
self.draw_context.move_to(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn line_to(&self, x: f64, y: f64) {
|
||||||
|
self.blur_context.line_to(x, y);
|
||||||
|
self.draw_context.line_to(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stroke(&self) {
|
||||||
|
self.blur_context.stroke().expect("to draw the blur line");
|
||||||
|
self.draw_context
|
||||||
|
.stroke()
|
||||||
|
.expect("to draw the regular line");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish(self) -> Pattern {
|
||||||
|
let foreground = self.draw_context.pop_group().unwrap();
|
||||||
|
self.blur_context.set_source(foreground).unwrap();
|
||||||
|
self.blur_context.paint().unwrap();
|
||||||
|
self.blur_context.pop_group().unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let app = gtk::Application::builder()
|
let app = gtk::Application::builder()
|
||||||
.application_id("com.luminescent-dreams.cyberpunk-splash")
|
.application_id("com.luminescent-dreams.cyberpunk-splash")
|
||||||
|
@ -590,7 +665,7 @@ fn main() {
|
||||||
let countdown = match options.lookup::<String>("countdown") {
|
let countdown = match options.lookup::<String>("countdown") {
|
||||||
Ok(Some(countdown_str)) => {
|
Ok(Some(countdown_str)) => {
|
||||||
let parts = countdown_str.split(':').collect::<Vec<&str>>();
|
let parts = countdown_str.split(':').collect::<Vec<&str>>();
|
||||||
let duration = match parts.len() {
|
match parts.len() {
|
||||||
2 => {
|
2 => {
|
||||||
let minutes = parts[0].parse::<u64>().unwrap();
|
let minutes = parts[0].parse::<u64>().unwrap();
|
||||||
let seconds = parts[1].parse::<u64>().unwrap();
|
let seconds = parts[1].parse::<u64>().unwrap();
|
||||||
|
@ -601,8 +676,7 @@ fn main() {
|
||||||
Duration::from_secs(seconds)
|
Duration::from_secs(seconds)
|
||||||
}
|
}
|
||||||
_ => Duration::from_secs(300),
|
_ => Duration::from_secs(300),
|
||||||
};
|
}
|
||||||
duration
|
|
||||||
}
|
}
|
||||||
_ => Duration::from_secs(300),
|
_ => Duration::from_secs(300),
|
||||||
};
|
};
|
||||||
|
@ -629,12 +703,12 @@ fn main() {
|
||||||
|
|
||||||
app.connect_activate(move |app| {
|
app.connect_activate(move |app| {
|
||||||
let (gtk_tx, gtk_rx) =
|
let (gtk_tx, gtk_rx) =
|
||||||
gtk::glib::MainContext::channel::<State>(gtk::glib::PRIORITY_DEFAULT);
|
gtk::glib::MainContext::channel::<State>(gtk::glib::Priority::DEFAULT);
|
||||||
|
|
||||||
let window = gtk::ApplicationWindow::new(app);
|
let window = gtk::ApplicationWindow::new(app);
|
||||||
window.present();
|
window.present();
|
||||||
|
|
||||||
let splash = Splash::new(title.read().unwrap().clone(), state.read().unwrap().clone());
|
let splash = Splash::new(title.read().unwrap().clone(), *state.read().unwrap());
|
||||||
|
|
||||||
window.set_child(Some(&splash));
|
window.set_child(Some(&splash));
|
||||||
|
|
||||||
|
@ -662,7 +736,7 @@ fn main() {
|
||||||
|
|
||||||
gtk_rx.attach(None, move |state| {
|
gtk_rx.attach(None, move |state| {
|
||||||
splash.set_state(state);
|
splash.set_state(state);
|
||||||
Continue(true)
|
glib::ControlFlow::Continue
|
||||||
});
|
});
|
||||||
|
|
||||||
std::thread::spawn({
|
std::thread::spawn({
|
||||||
|
@ -672,7 +746,7 @@ fn main() {
|
||||||
loop {
|
loop {
|
||||||
std::thread::sleep(Duration::from_millis(1000 / 60));
|
std::thread::sleep(Duration::from_millis(1000 / 60));
|
||||||
state.write().unwrap().run(Instant::now());
|
state.write().unwrap().run(Instant::now());
|
||||||
let _ = gtk_tx.send(state.read().unwrap().clone());
|
let _ = gtk_tx.send(*state.read().unwrap());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,31 @@
|
||||||
|
[package]
|
||||||
|
name = "dashboard"
|
||||||
|
version = "0.1.3"
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
adw = { version = "0.5", package = "libadwaita", features = [ "v1_2" ] }
|
||||||
|
async-std = { version = "1.13" }
|
||||||
|
cairo-rs = { version = "0.18" }
|
||||||
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
|
fluent-ergonomics = { path = "../fluent-ergonomics/" }
|
||||||
|
fluent = { version = "0.16" }
|
||||||
|
futures = { version = "0.3" }
|
||||||
|
geo-types = { path = "../geo-types/" }
|
||||||
|
gio = { version = "0.18" }
|
||||||
|
glib = { version = "0.18" }
|
||||||
|
gdk = { version = "0.7", package = "gdk4" }
|
||||||
|
gtk = { version = "0.7", package = "gtk4" }
|
||||||
|
lazy_static = { version = "1.4" }
|
||||||
|
memorycache = { path = "../memorycache/" }
|
||||||
|
reqwest = { version = "0.11", features = ["json"] }
|
||||||
|
serde_json = { version = "1" }
|
||||||
|
serde = { version = "1", features = [ "derive" ] }
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
unic-langid = { version = "0.9" }
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
glib-build-tools = "0.18"
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
fn main() {
|
||||||
|
glib_build_tools::compile_resources(
|
||||||
|
&["resources"],
|
||||||
|
"gresources.xml",
|
||||||
|
"com.luminescent-dreams.dashboard.gresource",
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Version=1.0
|
||||||
|
Name=dashboard
|
||||||
|
Comment=My personal system dashboard
|
||||||
|
Exec=dashboard
|
|
@ -0,0 +1,12 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
VERSION=`cat Cargo.toml | grep "^version =" | sed -r 's/^version = "(.+)"$/\1/'`
|
||||||
|
|
||||||
|
mkdir -p dist
|
||||||
|
cp dashboard.desktop dist
|
||||||
|
cp ../target/release/dashboard dist
|
||||||
|
strip dist/dashboard
|
||||||
|
tar -czf dashboard-${VERSION}.tgz dist/
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<gresources>
|
||||||
|
<gresource prefix="/com/luminescent-dreams/dashboard/">
|
||||||
|
<file>style.css</file>
|
||||||
|
</gresource>
|
||||||
|
</gresources>
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
label {
|
||||||
|
font-size: 200%;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight {
|
||||||
|
color: @accent_fg_color;
|
||||||
|
background-color: @accent_bg_color;
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
use crate::{
|
||||||
|
components::{Date, Events, TransitCard, TransitClock},
|
||||||
|
types::State,
|
||||||
|
};
|
||||||
|
use adw::prelude::AdwApplicationWindowExt;
|
||||||
|
use gio::resources_lookup_data;
|
||||||
|
use gtk::{prelude::*, STYLE_PROVIDER_PRIORITY_USER};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ApplicationWindow {
|
||||||
|
pub window: adw::ApplicationWindow,
|
||||||
|
pub date_label: Date,
|
||||||
|
pub events: Events,
|
||||||
|
pub transit_card: TransitCard,
|
||||||
|
pub transit_clock: TransitClock,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ApplicationWindow {
|
||||||
|
pub fn new(app: &adw::Application) -> Self {
|
||||||
|
let window = adw::ApplicationWindow::new(app);
|
||||||
|
|
||||||
|
let stylesheet = String::from_utf8(
|
||||||
|
resources_lookup_data(
|
||||||
|
"/com/luminescent-dreams/dashboard/style.css",
|
||||||
|
gio::ResourceLookupFlags::NONE,
|
||||||
|
)
|
||||||
|
.expect("stylesheet should just be available")
|
||||||
|
.to_vec(),
|
||||||
|
)
|
||||||
|
.expect("to parse stylesheet");
|
||||||
|
|
||||||
|
let provider = gtk::CssProvider::new();
|
||||||
|
provider.load_from_data(&stylesheet);
|
||||||
|
let context = window.style_context();
|
||||||
|
context.add_provider(&provider, STYLE_PROVIDER_PRIORITY_USER);
|
||||||
|
|
||||||
|
let layout = gtk::Box::builder()
|
||||||
|
.orientation(gtk::Orientation::Vertical)
|
||||||
|
.hexpand(true)
|
||||||
|
.vexpand(true)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let date_label = Date::default();
|
||||||
|
let header = adw::HeaderBar::builder()
|
||||||
|
.title_widget(&date_label)
|
||||||
|
.build();
|
||||||
|
layout.append(&header);
|
||||||
|
|
||||||
|
let events = Events::default();
|
||||||
|
layout.append(&events);
|
||||||
|
|
||||||
|
let transit_card = TransitCard::default();
|
||||||
|
layout.append(&transit_card);
|
||||||
|
|
||||||
|
let transit_clock = TransitClock::default();
|
||||||
|
layout.append(&transit_clock);
|
||||||
|
|
||||||
|
window.set_content(Some(&layout));
|
||||||
|
|
||||||
|
Self {
|
||||||
|
window,
|
||||||
|
date_label,
|
||||||
|
events,
|
||||||
|
transit_card,
|
||||||
|
transit_clock,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_state(&self, state: State) {
|
||||||
|
self.date_label.update_date(state.date);
|
||||||
|
self.events.set_events(state.events, state.next_event);
|
||||||
|
if let Some(transit) = state.transit {
|
||||||
|
self.transit_card.update_transit(&transit);
|
||||||
|
self.transit_clock.update_transit(transit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
|
use chrono::NaiveDate;
|
||||||
|
use glib::Object;
|
||||||
|
use gtk::{prelude::*, subclass::prelude::*};
|
||||||
|
|
||||||
|
pub struct DatePrivate {
|
||||||
|
date: Rc<RefCell<NaiveDate>>,
|
||||||
|
label: Rc<RefCell<gtk::Label>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DatePrivate {
|
||||||
|
fn default() -> Self {
|
||||||
|
let date = chrono::Local::now().date_naive();
|
||||||
|
Self {
|
||||||
|
date: Rc::new(RefCell::new(date)),
|
||||||
|
label: Rc::new(RefCell::new(gtk::Label::new(None))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[glib::object_subclass]
|
||||||
|
impl ObjectSubclass for DatePrivate {
|
||||||
|
const NAME: &'static str = "Date";
|
||||||
|
type Type = Date;
|
||||||
|
type ParentType = gtk::Box;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjectImpl for DatePrivate {}
|
||||||
|
impl WidgetImpl for DatePrivate {}
|
||||||
|
impl BoxImpl for DatePrivate {}
|
||||||
|
|
||||||
|
glib::wrapper! {
|
||||||
|
pub struct Date(ObjectSubclass<DatePrivate>) @extends gtk::Box, gtk::Widget;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Date {
|
||||||
|
fn default() -> Self {
|
||||||
|
let s: Self = Object::builder().build();
|
||||||
|
s.set_margin_bottom(8);
|
||||||
|
s.set_margin_top(8);
|
||||||
|
s.set_margin_start(8);
|
||||||
|
s.set_margin_end(8);
|
||||||
|
|
||||||
|
s.append(&*s.imp().label.borrow());
|
||||||
|
|
||||||
|
s.redraw();
|
||||||
|
s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Date {
|
||||||
|
pub fn update_date(&self, date: NaiveDate) {
|
||||||
|
*self.imp().date.borrow_mut() = date;
|
||||||
|
self.redraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn redraw(&self) {
|
||||||
|
let date = self.imp().date.borrow();
|
||||||
|
self.imp()
|
||||||
|
.label
|
||||||
|
.borrow_mut()
|
||||||
|
.set_text(&date.format("%Y %B %d").to_string());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
use crate::{
|
||||||
|
components::Date,
|
||||||
|
solstices::{self, YearlyEvents},
|
||||||
|
};
|
||||||
|
use glib::Object;
|
||||||
|
use gtk::{prelude::*, subclass::prelude::*};
|
||||||
|
|
||||||
|
/*
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
pub enum UpcomingEvent {
|
||||||
|
SpringEquinox,
|
||||||
|
SummerSolstice,
|
||||||
|
AutumnEquinox,
|
||||||
|
WinterSolstice,
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct EventsPrivate {
|
||||||
|
spring_equinox: Date,
|
||||||
|
summer_solstice: Date,
|
||||||
|
autumn_equinox: Date,
|
||||||
|
winter_solstice: Date,
|
||||||
|
// next: UpcomingEvent,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[glib::object_subclass]
|
||||||
|
impl ObjectSubclass for EventsPrivate {
|
||||||
|
const NAME: &'static str = "Events";
|
||||||
|
type Type = Events;
|
||||||
|
type ParentType = gtk::Box;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjectImpl for EventsPrivate {}
|
||||||
|
impl WidgetImpl for EventsPrivate {}
|
||||||
|
impl BoxImpl for EventsPrivate {}
|
||||||
|
|
||||||
|
glib::wrapper! {
|
||||||
|
pub struct Events(ObjectSubclass<EventsPrivate>) @extends gtk::Widget, gtk::Box, @implements gtk::Orientable;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Events {
|
||||||
|
fn default() -> Self {
|
||||||
|
let s: Self = Object::builder().build();
|
||||||
|
s.set_orientation(gtk::Orientation::Horizontal);
|
||||||
|
s.set_spacing(8);
|
||||||
|
|
||||||
|
s.append(&s.imp().spring_equinox);
|
||||||
|
s.append(&s.imp().summer_solstice);
|
||||||
|
s.append(&s.imp().autumn_equinox);
|
||||||
|
s.append(&s.imp().winter_solstice);
|
||||||
|
|
||||||
|
s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Events {
|
||||||
|
pub fn set_events(&self, events: YearlyEvents, next_event: solstices::Event) {
|
||||||
|
self.imp()
|
||||||
|
.spring_equinox
|
||||||
|
.update_date(events.spring_equinox.date_naive());
|
||||||
|
|
||||||
|
self.imp()
|
||||||
|
.summer_solstice
|
||||||
|
.update_date(events.summer_solstice.date_naive());
|
||||||
|
|
||||||
|
self.imp()
|
||||||
|
.autumn_equinox
|
||||||
|
.update_date(events.autumn_equinox.date_naive());
|
||||||
|
|
||||||
|
self.imp()
|
||||||
|
.winter_solstice
|
||||||
|
.update_date(events.winter_solstice.date_naive());
|
||||||
|
|
||||||
|
self.imp().spring_equinox.remove_css_class("highlight");
|
||||||
|
self.imp().summer_solstice.remove_css_class("highlight");
|
||||||
|
self.imp().autumn_equinox.remove_css_class("highlight");
|
||||||
|
self.imp().winter_solstice.remove_css_class("highlight");
|
||||||
|
|
||||||
|
match next_event {
|
||||||
|
solstices::Event::SpringEquinox(_) => {
|
||||||
|
self.imp().spring_equinox.add_css_class("highlight")
|
||||||
|
}
|
||||||
|
solstices::Event::SummerSolstice(_) => {
|
||||||
|
self.imp().summer_solstice.add_css_class("highlight")
|
||||||
|
}
|
||||||
|
solstices::Event::AutumnEquinox(_) => {
|
||||||
|
self.imp().autumn_equinox.add_css_class("highlight")
|
||||||
|
}
|
||||||
|
solstices::Event::WinterSolstice(_) => {
|
||||||
|
self.imp().winter_solstice.add_css_class("highlight")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
use glib::Object;
|
||||||
|
use gtk::{prelude::*, subclass::prelude::*};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct LabelPrivate {
|
||||||
|
label: gtk::Label,
|
||||||
|
icon: gtk::Image,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[glib::object_subclass]
|
||||||
|
impl ObjectSubclass for LabelPrivate {
|
||||||
|
const NAME: &'static str = "Label";
|
||||||
|
type Type = Label;
|
||||||
|
type ParentType = gtk::Box;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjectImpl for LabelPrivate {}
|
||||||
|
impl WidgetImpl for LabelPrivate {}
|
||||||
|
impl BoxImpl for LabelPrivate {}
|
||||||
|
|
||||||
|
glib::wrapper! {
|
||||||
|
pub struct Label(ObjectSubclass<LabelPrivate>) @extends gtk::Box, gtk::Widget,
|
||||||
|
@implements gtk::Orientable;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Label {
|
||||||
|
pub fn new(text: Option<&str>, icon: Option<gio::ThemedIcon>) -> Self {
|
||||||
|
let s: Self = Object::builder().build();
|
||||||
|
s.set_orientation(gtk::Orientation::Horizontal);
|
||||||
|
s.set_spacing(8);
|
||||||
|
s.set_margin_bottom(8);
|
||||||
|
s.set_margin_top(8);
|
||||||
|
s.set_margin_start(8);
|
||||||
|
s.set_margin_end(8);
|
||||||
|
|
||||||
|
s.append(&s.imp().icon);
|
||||||
|
s.append(&s.imp().label);
|
||||||
|
|
||||||
|
if let Some(text) = text {
|
||||||
|
s.set_text(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(icon) = icon {
|
||||||
|
s.set_icon(icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_text(&self, text: &str) {
|
||||||
|
self.imp().label.set_text(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_icon(&self, icon: gio::ThemedIcon) {
|
||||||
|
self.imp().icon.set_from_gicon(&icon);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
mod date;
|
||||||
|
pub use date::Date;
|
||||||
|
|
||||||
|
mod events;
|
||||||
|
pub use events::Events;
|
||||||
|
|
||||||
|
mod label;
|
||||||
|
pub use label::Label;
|
||||||
|
|
||||||
|
mod transit_card;
|
||||||
|
pub use transit_card::TransitCard;
|
||||||
|
|
||||||
|
mod transit_clock;
|
||||||
|
pub use transit_clock::TransitClock;
|
|
@ -0,0 +1,74 @@
|
||||||
|
use crate::{components::Label, soluna_client::SunMoon};
|
||||||
|
use glib::Object;
|
||||||
|
use gtk::{prelude::*, subclass::prelude::*};
|
||||||
|
|
||||||
|
pub struct TransitCardPrivate {
|
||||||
|
sunrise: Label,
|
||||||
|
sunset: Label,
|
||||||
|
moonrise: Label,
|
||||||
|
moonset: Label,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TransitCardPrivate {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
sunrise: Label::new(None, Some(gio::ThemedIcon::new("daytime-sunrise-symbolic"))),
|
||||||
|
sunset: Label::new(None, Some(gio::ThemedIcon::new("daytime-sunset-symbolic"))),
|
||||||
|
moonrise: Label::new(None, Some(gio::ThemedIcon::new("moon-outline-symbolic"))),
|
||||||
|
moonset: Label::new(None, Some(gio::ThemedIcon::new("moon-outline-symbolic"))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[glib::object_subclass]
|
||||||
|
impl ObjectSubclass for TransitCardPrivate {
|
||||||
|
const NAME: &'static str = "TransitCard";
|
||||||
|
type Type = TransitCard;
|
||||||
|
type ParentType = gtk::Grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjectImpl for TransitCardPrivate {}
|
||||||
|
impl WidgetImpl for TransitCardPrivate {}
|
||||||
|
impl GridImpl for TransitCardPrivate {}
|
||||||
|
|
||||||
|
glib::wrapper! {
|
||||||
|
pub struct TransitCard(ObjectSubclass<TransitCardPrivate>) @extends gtk::Grid, gtk::Widget;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TransitCard {
|
||||||
|
fn default() -> Self {
|
||||||
|
let s: Self = Object::builder().build();
|
||||||
|
s.add_css_class("card");
|
||||||
|
s.set_column_homogeneous(true);
|
||||||
|
|
||||||
|
s.attach(&s.imp().sunrise, 0, 0, 1, 1);
|
||||||
|
s.attach(&s.imp().sunset, 0, 1, 1, 1);
|
||||||
|
s.attach(&s.imp().moonrise, 1, 0, 1, 1);
|
||||||
|
s.attach(&s.imp().moonset, 1, 1, 1, 1);
|
||||||
|
|
||||||
|
s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TransitCard {
|
||||||
|
pub fn update_transit(&self, transit_info: &SunMoon) {
|
||||||
|
self.imp()
|
||||||
|
.sunrise
|
||||||
|
.set_text(format!("{}", transit_info.sunrise.format("%H:%M")).as_ref());
|
||||||
|
self.imp()
|
||||||
|
.sunset
|
||||||
|
.set_text(format!("{}", transit_info.sunset.format("%H:%M")).as_ref());
|
||||||
|
self.imp().moonrise.set_text(
|
||||||
|
&transit_info
|
||||||
|
.moonrise
|
||||||
|
.map(|time| format!("{}", time.format("%H:%M")))
|
||||||
|
.unwrap_or("".to_owned()),
|
||||||
|
);
|
||||||
|
self.imp().moonset.set_text(
|
||||||
|
&transit_info
|
||||||
|
.moonset
|
||||||
|
.map(|time| format!("{}", time.format("%H:%M")))
|
||||||
|
.unwrap_or("".to_owned()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
use crate::{
|
||||||
|
drawing::{Color, PieChart, Wedge},
|
||||||
|
soluna_client::SunMoon,
|
||||||
|
};
|
||||||
|
use chrono::{Duration, NaiveTime};
|
||||||
|
use glib::Object;
|
||||||
|
use gtk::{prelude::*, subclass::prelude::*};
|
||||||
|
use std::{cell::RefCell, f64::consts::PI, rc::Rc};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct TransitClockPrivate {
|
||||||
|
info: Rc<RefCell<Option<SunMoon>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[glib::object_subclass]
|
||||||
|
impl ObjectSubclass for TransitClockPrivate {
|
||||||
|
const NAME: &'static str = "TransitClock";
|
||||||
|
type Type = TransitClock;
|
||||||
|
type ParentType = gtk::DrawingArea;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjectImpl for TransitClockPrivate {}
|
||||||
|
impl WidgetImpl for TransitClockPrivate {}
|
||||||
|
impl DrawingAreaImpl for TransitClockPrivate {}
|
||||||
|
|
||||||
|
glib::wrapper! {
|
||||||
|
pub struct TransitClock(ObjectSubclass<TransitClockPrivate>) @extends gtk::DrawingArea, gtk::Widget;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TransitClock {
|
||||||
|
fn default() -> Self {
|
||||||
|
let s: Self = Object::builder().build();
|
||||||
|
s.set_width_request(500);
|
||||||
|
s.set_height_request(500);
|
||||||
|
|
||||||
|
s.set_draw_func({
|
||||||
|
let s = s.clone();
|
||||||
|
move |_, context, width, height| {
|
||||||
|
let style_context = WidgetExt::style_context(&s);
|
||||||
|
let center_x = width as f64 / 2.;
|
||||||
|
let center_y = height as f64 / 2.;
|
||||||
|
let radius = width.min(height) as f64 / 2. * 0.9;
|
||||||
|
if let Some(ref info) = *s.imp().info.borrow() {
|
||||||
|
let full_day = Duration::days(1).num_seconds() as f64;
|
||||||
|
let sunrise = info.sunrise - NaiveTime::from_hms_opt(0, 0, 0).unwrap();
|
||||||
|
let sunset = info.sunset - NaiveTime::from_hms_opt(0, 0, 0).unwrap();
|
||||||
|
|
||||||
|
let night_color = style_context.lookup_color("dark_5").unwrap();
|
||||||
|
let day_color = style_context.lookup_color("blue_1").unwrap();
|
||||||
|
|
||||||
|
PieChart::new(&style_context)
|
||||||
|
.center(center_x, center_y)
|
||||||
|
.radius(radius)
|
||||||
|
.rotation(-PI / 2.)
|
||||||
|
.wedges(
|
||||||
|
vec![
|
||||||
|
Wedge {
|
||||||
|
start_angle: (PI * 2.) * sunset.num_seconds() as f64 / full_day,
|
||||||
|
end_angle: (PI * 2.) * sunrise.num_seconds() as f64 / full_day,
|
||||||
|
color: Color {
|
||||||
|
r: night_color.red() as f64,
|
||||||
|
g: night_color.green() as f64,
|
||||||
|
b: night_color.blue() as f64,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Wedge {
|
||||||
|
start_angle: (PI * 2.) * sunrise.num_seconds() as f64
|
||||||
|
/ full_day,
|
||||||
|
end_angle: (PI * 2.) * sunset.num_seconds() as f64 / full_day,
|
||||||
|
color: Color {
|
||||||
|
r: day_color.red() as f64,
|
||||||
|
g: day_color.green() as f64,
|
||||||
|
b: day_color.blue() as f64,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
.into_iter(),
|
||||||
|
)
|
||||||
|
.draw(context);
|
||||||
|
|
||||||
|
(0..24).for_each(|tick| {
|
||||||
|
context.set_source_rgb(0., 0., 0.);
|
||||||
|
context.translate(center_x, center_y);
|
||||||
|
context.rotate(tick as f64 * (PI / 12.));
|
||||||
|
context.move_to(radius - 5., 0.);
|
||||||
|
context.line_to(radius - 10., 0.);
|
||||||
|
let _ = context.stroke();
|
||||||
|
context.identity_matrix();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TransitClock {
|
||||||
|
pub fn update_transit(&self, transit_info: SunMoon) {
|
||||||
|
*self.imp().info.borrow_mut() = Some(transit_info);
|
||||||
|
self.queue_draw();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
mod pie_chart;
|
||||||
|
pub use pie_chart::{Color, PieChart, Wedge};
|
|
@ -0,0 +1,96 @@
|
||||||
|
use cairo::Context;
|
||||||
|
use gtk::{gdk::RGBA, prelude::*, StyleContext};
|
||||||
|
use std::f64::consts::PI;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Color {
|
||||||
|
pub r: f64,
|
||||||
|
pub g: f64,
|
||||||
|
pub b: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Wedge {
|
||||||
|
pub start_angle: f64,
|
||||||
|
pub end_angle: f64,
|
||||||
|
pub color: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PieChart {
|
||||||
|
rotation: f64,
|
||||||
|
wedges: Vec<Wedge>,
|
||||||
|
center_x: f64,
|
||||||
|
center_y: f64,
|
||||||
|
radius: f64,
|
||||||
|
|
||||||
|
border_color: RGBA,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PieChart {
|
||||||
|
pub fn new(style_context: &StyleContext) -> Self {
|
||||||
|
Self {
|
||||||
|
rotation: 0.,
|
||||||
|
wedges: vec![],
|
||||||
|
center_x: 0.,
|
||||||
|
center_y: 0.,
|
||||||
|
radius: 0.,
|
||||||
|
border_color: style_context.lookup_color("theme_fg_color").unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rotation(mut self, rotation: f64) -> Self {
|
||||||
|
self.rotation = rotation;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn wedges(mut self, wedge: impl Iterator<Item = Wedge>) -> Self {
|
||||||
|
let mut wedges: Vec<Wedge> = wedge.collect();
|
||||||
|
self.wedges.append(&mut wedges);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn center(mut self, center_x: f64, center_y: f64) -> Self {
|
||||||
|
self.center_x = center_x;
|
||||||
|
self.center_y = center_y;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn radius(mut self, radius: f64) -> Self {
|
||||||
|
self.radius = radius;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw(self, context: &Context) {
|
||||||
|
context.set_source_rgba(0., 0., 0., 0.);
|
||||||
|
let _ = context.paint();
|
||||||
|
|
||||||
|
context.set_line_width(2.);
|
||||||
|
|
||||||
|
self.wedges.iter().for_each(
|
||||||
|
|Wedge {
|
||||||
|
start_angle,
|
||||||
|
end_angle,
|
||||||
|
color,
|
||||||
|
}| {
|
||||||
|
context.move_to(self.center_x, self.center_y);
|
||||||
|
context.set_source_rgb(color.r, color.g, color.b);
|
||||||
|
context.arc(
|
||||||
|
self.center_x,
|
||||||
|
self.center_y,
|
||||||
|
self.radius,
|
||||||
|
start_angle + self.rotation,
|
||||||
|
end_angle + self.rotation,
|
||||||
|
);
|
||||||
|
let _ = context.fill();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
context.set_source_rgb(
|
||||||
|
self.border_color.red() as f64,
|
||||||
|
self.border_color.green() as f64,
|
||||||
|
self.border_color.blue() as f64,
|
||||||
|
);
|
||||||
|
context.arc(self.center_x, self.center_y, self.radius, 0., 2. * PI);
|
||||||
|
let _ = context.stroke();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,143 @@
|
||||||
|
use std::{
|
||||||
|
env,
|
||||||
|
sync::{Arc, RwLock},
|
||||||
|
};
|
||||||
|
|
||||||
|
use async_std::channel::Sender;
|
||||||
|
use chrono::{Datelike, Local, Utc};
|
||||||
|
use geo_types::{Latitude, Longitude};
|
||||||
|
use gtk::prelude::*;
|
||||||
|
|
||||||
|
mod app_window;
|
||||||
|
use app_window::ApplicationWindow;
|
||||||
|
|
||||||
|
mod components;
|
||||||
|
|
||||||
|
mod drawing;
|
||||||
|
|
||||||
|
mod soluna_client;
|
||||||
|
use soluna_client::SolunaClient;
|
||||||
|
|
||||||
|
mod solstices;
|
||||||
|
use solstices::EVENTS;
|
||||||
|
|
||||||
|
mod types;
|
||||||
|
use types::State;
|
||||||
|
|
||||||
|
/*
|
||||||
|
const EO_TEXT: &'static str = "
|
||||||
|
day = {$day ->
|
||||||
|
*[Sunday] Dimanĉo
|
||||||
|
[Monday] Lundo
|
||||||
|
[Tuesday] Mardo
|
||||||
|
[Wednesday] Merkredo
|
||||||
|
[Thursday] Ĵaŭdo
|
||||||
|
[Friday] Vendredo
|
||||||
|
[Saturday] Sabato
|
||||||
|
[LeapDay] Leap Day
|
||||||
|
[YearDay] Year Day
|
||||||
|
}
|
||||||
|
month = {$month ->
|
||||||
|
*[January] Januaro
|
||||||
|
[February] Februaro
|
||||||
|
[March] Marto
|
||||||
|
[April] Aprilo
|
||||||
|
[May] Mayo
|
||||||
|
[June] Junio
|
||||||
|
[Sol] Solo
|
||||||
|
[July] Julio
|
||||||
|
[August] Aŭgusto
|
||||||
|
[September] Septembro
|
||||||
|
[October] Oktobro
|
||||||
|
[November] Novembro
|
||||||
|
[December] Decembro
|
||||||
|
}
|
||||||
|
spring_equinox = Printempa Ekvinokso
|
||||||
|
summer_solstice = Somera Solstico
|
||||||
|
autumn_equinox = Aŭtuna Ekvinokso
|
||||||
|
winter_solstice = Vintra Solstico
|
||||||
|
";
|
||||||
|
*/
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum Message {
|
||||||
|
Refresh(State),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Core {
|
||||||
|
tx: Arc<RwLock<Option<Sender<Message>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main() {
|
||||||
|
gio::resources_register_include!("com.luminescent-dreams.dashboard.gresource")
|
||||||
|
.expect("Failed to register resources");
|
||||||
|
|
||||||
|
let app = adw::Application::builder()
|
||||||
|
.application_id("com.luminescent-dreams.dashboard")
|
||||||
|
.resource_base_path("/com/luminescent-dreams/dashboard")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let latitude = Latitude::from(41.78);
|
||||||
|
let longitude = Longitude::from(-71.41);
|
||||||
|
|
||||||
|
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let core = Core {
|
||||||
|
tx: Arc::new(RwLock::new(None)),
|
||||||
|
};
|
||||||
|
|
||||||
|
runtime.spawn({
|
||||||
|
let core = core.clone();
|
||||||
|
async move {
|
||||||
|
let soluna_client = SolunaClient::new();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let transit = soluna_client
|
||||||
|
.request(latitude.clone(), longitude.clone(), Local::now())
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let now = Local::now();
|
||||||
|
let state = State {
|
||||||
|
date: now.date_naive(),
|
||||||
|
next_event: EVENTS.next_event(now.with_timezone(&Utc)).unwrap(),
|
||||||
|
events: EVENTS.yearly_events(now.year()).unwrap(),
|
||||||
|
transit: Some(transit),
|
||||||
|
};
|
||||||
|
|
||||||
|
let gtk_tx = core.tx.read().unwrap().clone();
|
||||||
|
|
||||||
|
if let Some(gtk_tx) = gtk_tx {
|
||||||
|
let state = state.clone();
|
||||||
|
let _ = gtk_tx.send(Message::Refresh(state)).await;
|
||||||
|
std::thread::sleep(std::time::Duration::from_secs(60));
|
||||||
|
} else {
|
||||||
|
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.connect_activate(move |app| {
|
||||||
|
let (gtk_tx, gtk_rx) = async_std::channel::unbounded();
|
||||||
|
|
||||||
|
*core.tx.write().unwrap() = Some(gtk_tx);
|
||||||
|
|
||||||
|
let window = ApplicationWindow::new(app);
|
||||||
|
window.window.present();
|
||||||
|
|
||||||
|
glib::spawn_future_local(async move {
|
||||||
|
loop {
|
||||||
|
let Message::Refresh(state) = gtk_rx.recv().await.unwrap();
|
||||||
|
window.update_state(state);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let args: Vec<String> = env::args().collect();
|
||||||
|
ApplicationExtManual::run_with_args(&app, &args);
|
||||||
|
runtime.shutdown_background();
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
use svg::{
|
||||||
|
node::element::{Circle, Image},
|
||||||
|
Document,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn moon() -> Document {
|
||||||
|
/*
|
||||||
|
svg(width="100%", height="100%", xmlns="http://www.w3.org/2000/svg") {
|
||||||
|
circle(cx="50", cy="50", r="50", stroke="green", fill="none");
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
let img = Image::new().set("href", "/moon-small.png");
|
||||||
|
Document::new().add(img)
|
||||||
|
}
|
|
@ -0,0 +1,202 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use chrono::prelude::*;
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
// http://astropixels.com/ephemeris/soleq2001.html
|
||||||
|
const SOLSTICE_TEXT: &str = "
|
||||||
|
2001 Mar 20 13:31 Jun 21 07:38 Sep 22 23:05 Dec 21 19:22
|
||||||
|
2002 Mar 20 19:16 Jun 21 13:25 Sep 23 04:56 Dec 22 01:15
|
||||||
|
2003 Mar 21 01:00 Jun 21 19:11 Sep 23 10:47 Dec 22 07:04
|
||||||
|
2004 Mar 20 06:49 Jun 21 00:57 Sep 22 16:30 Dec 21 12:42
|
||||||
|
2005 Mar 20 12:34 Jun 21 06:46 Sep 22 22:23 Dec 21 18:35
|
||||||
|
2006 Mar 20 18:25 Jun 21 12:26 Sep 23 04:04 Dec 22 00:22
|
||||||
|
2007 Mar 21 00:07 Jun 21 18:06 Sep 23 09:51 Dec 22 06:08
|
||||||
|
2008 Mar 20 05:49 Jun 21 00:00 Sep 22 15:45 Dec 21 12:04
|
||||||
|
2009 Mar 20 11:44 Jun 21 05:45 Sep 22 21:18 Dec 21 17:47
|
||||||
|
2010 Mar 20 17:32 Jun 21 11:28 Sep 23 03:09 Dec 21 23:38
|
||||||
|
|
||||||
|
2011 Mar 20 23:21 Jun 21 17:16 Sep 23 09:05 Dec 22 05:30
|
||||||
|
2012 Mar 20 05:15 Jun 20 23:08 Sep 22 14:49 Dec 21 11:12
|
||||||
|
2013 Mar 20 11:02 Jun 21 05:04 Sep 22 20:44 Dec 21 17:11
|
||||||
|
2014 Mar 20 16:57 Jun 21 10:52 Sep 23 02:30 Dec 21 23:03
|
||||||
|
2015 Mar 20 22:45 Jun 21 16:38 Sep 23 08:20 Dec 22 04:48
|
||||||
|
2016 Mar 20 04:31 Jun 20 22:35 Sep 22 14:21 Dec 21 10:45
|
||||||
|
2017 Mar 20 10:29 Jun 21 04:25 Sep 22 20:02 Dec 21 16:29
|
||||||
|
2018 Mar 20 16:15 Jun 21 10:07 Sep 23 01:54 Dec 21 22:22
|
||||||
|
2019 Mar 20 21:58 Jun 21 15:54 Sep 23 07:50 Dec 22 04:19
|
||||||
|
2020 Mar 20 03:50 Jun 20 21:43 Sep 22 13:31 Dec 21 10:03
|
||||||
|
|
||||||
|
2021 Mar 20 09:37 Jun 21 03:32 Sep 22 19:21 Dec 21 15:59
|
||||||
|
2022 Mar 20 15:33 Jun 21 09:14 Sep 23 01:04 Dec 21 21:48
|
||||||
|
2023 Mar 20 21:25 Jun 21 14:58 Sep 23 06:50 Dec 22 03:28
|
||||||
|
2024 Mar 20 03:07 Jun 20 20:51 Sep 22 12:44 Dec 21 09:20
|
||||||
|
2025 Mar 20 09:02 Jun 21 02:42 Sep 22 18:20 Dec 21 15:03
|
||||||
|
2026 Mar 20 14:46 Jun 21 08:25 Sep 23 00:06 Dec 21 20:50
|
||||||
|
2027 Mar 20 20:25 Jun 21 14:11 Sep 23 06:02 Dec 22 02:43
|
||||||
|
2028 Mar 20 02:17 Jun 20 20:02 Sep 22 11:45 Dec 21 08:20
|
||||||
|
2029 Mar 20 08:01 Jun 21 01:48 Sep 22 17:37 Dec 21 14:14
|
||||||
|
2030 Mar 20 13:51 Jun 21 07:31 Sep 22 23:27 Dec 21 20:09
|
||||||
|
|
||||||
|
2031 Mar 20 19:41 Jun 21 13:17 Sep 23 05:15 Dec 22 01:56
|
||||||
|
2032 Mar 20 01:23 Jun 20 19:09 Sep 22 11:11 Dec 21 07:57
|
||||||
|
2033 Mar 20 07:23 Jun 21 01:01 Sep 22 16:52 Dec 21 13:45
|
||||||
|
2034 Mar 20 13:18 Jun 21 06:45 Sep 22 22:41 Dec 21 19:35
|
||||||
|
2035 Mar 20 19:03 Jun 21 12:33 Sep 23 04:39 Dec 22 01:31
|
||||||
|
2036 Mar 20 01:02 Jun 20 18:31 Sep 22 10:23 Dec 21 07:12
|
||||||
|
2037 Mar 20 06:50 Jun 21 00:22 Sep 22 16:13 Dec 21 13:08
|
||||||
|
2038 Mar 20 12:40 Jun 21 06:09 Sep 22 22:02 Dec 21 19:01
|
||||||
|
2039 Mar 20 18:32 Jun 21 11:58 Sep 23 03:50 Dec 22 00:41
|
||||||
|
2040 Mar 20 00:11 Jun 20 17:46 Sep 22 09:44 Dec 21 06:33
|
||||||
|
|
||||||
|
2041 Mar 20 06:07 Jun 20 23:37 Sep 22 15:27 Dec 21 12:19
|
||||||
|
2042 Mar 20 11:53 Jun 21 05:16 Sep 22 21:11 Dec 21 18:04
|
||||||
|
2043 Mar 20 17:29 Jun 21 10:59 Sep 23 03:07 Dec 22 00:02
|
||||||
|
2044 Mar 19 23:20 Jun 20 16:50 Sep 22 08:47 Dec 21 05:43
|
||||||
|
2045 Mar 20 05:08 Jun 20 22:34 Sep 22 14:33 Dec 21 11:36
|
||||||
|
2046 Mar 20 10:58 Jun 21 04:15 Sep 22 20:22 Dec 21 17:28
|
||||||
|
2047 Mar 20 16:52 Jun 21 10:02 Sep 23 02:07 Dec 21 23:07
|
||||||
|
2048 Mar 19 22:34 Jun 20 15:54 Sep 22 08:01 Dec 21 05:02
|
||||||
|
2049 Mar 20 04:28 Jun 20 21:47 Sep 22 13:42 Dec 21 10:51
|
||||||
|
2050 Mar 20 10:20 Jun 21 03:33 Sep 22 19:29 Dec 21 16:39
|
||||||
|
";
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct YearlyEvents {
|
||||||
|
pub year: i32,
|
||||||
|
pub spring_equinox: chrono::DateTime<chrono::Utc>,
|
||||||
|
pub summer_solstice: chrono::DateTime<chrono::Utc>,
|
||||||
|
pub autumn_equinox: chrono::DateTime<chrono::Utc>,
|
||||||
|
pub winter_solstice: chrono::DateTime<chrono::Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
|
||||||
|
pub enum Event {
|
||||||
|
SpringEquinox(chrono::DateTime<chrono::Utc>),
|
||||||
|
SummerSolstice(chrono::DateTime<chrono::Utc>),
|
||||||
|
AutumnEquinox(chrono::DateTime<chrono::Utc>),
|
||||||
|
WinterSolstice(chrono::DateTime<chrono::Utc>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Event {
|
||||||
|
pub fn date(&self) -> chrono::DateTime<chrono::Utc> {
|
||||||
|
match *self {
|
||||||
|
Event::SpringEquinox(d) => d,
|
||||||
|
Event::SummerSolstice(d) => d,
|
||||||
|
Event::AutumnEquinox(d) => d,
|
||||||
|
Event::WinterSolstice(d) => d,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_time<'a>(
|
||||||
|
year: &str,
|
||||||
|
iter: impl Iterator<Item = &'a str>,
|
||||||
|
) -> chrono::DateTime<chrono::Utc> {
|
||||||
|
let parts = iter.collect::<Vec<&str>>();
|
||||||
|
let p = format!("{} {} {} {}", year, parts[0], parts[1], parts[2]);
|
||||||
|
NaiveDateTime::parse_from_str(&p, "%Y %b %d %H:%M")
|
||||||
|
.unwrap()
|
||||||
|
.and_utc()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_line(year: &str, rest: &[&str]) -> YearlyEvents {
|
||||||
|
let spring = parse_time(year, rest.iter().take(3).cloned());
|
||||||
|
let summer = parse_time(year, rest.iter().skip(3).take(3).cloned());
|
||||||
|
let autumn = parse_time(year, rest.iter().skip(6).take(3).cloned());
|
||||||
|
let winter = parse_time(year, rest.iter().skip(9).take(3).cloned());
|
||||||
|
YearlyEvents {
|
||||||
|
year: year.parse::<i32>().unwrap(),
|
||||||
|
spring_equinox: spring,
|
||||||
|
summer_solstice: summer,
|
||||||
|
autumn_equinox: autumn,
|
||||||
|
winter_solstice: winter,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_events() -> Vec<Option<YearlyEvents>> {
|
||||||
|
SOLSTICE_TEXT
|
||||||
|
.lines()
|
||||||
|
.map(|line| {
|
||||||
|
match line
|
||||||
|
.split(' ')
|
||||||
|
.filter(|elem| !elem.is_empty())
|
||||||
|
.collect::<Vec<&str>>()
|
||||||
|
.as_slice()
|
||||||
|
{
|
||||||
|
[year, rest @ ..] => Some(parse_line(year, rest)),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Solstices(HashMap<i32, YearlyEvents>);
|
||||||
|
|
||||||
|
impl Solstices {
|
||||||
|
pub fn yearly_events(&self, year: i32) -> Option<YearlyEvents> {
|
||||||
|
self.0.get(&year).copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next_event(&self, date: chrono::DateTime<chrono::Utc>) -> Option<Event> {
|
||||||
|
let year_events = self.0.get(&date.year());
|
||||||
|
match year_events {
|
||||||
|
Some(year_events) => {
|
||||||
|
if date <= year_events.spring_equinox {
|
||||||
|
Some(Event::SpringEquinox(year_events.spring_equinox))
|
||||||
|
} else if date <= year_events.summer_solstice {
|
||||||
|
Some(Event::SummerSolstice(year_events.summer_solstice))
|
||||||
|
} else if date <= year_events.autumn_equinox {
|
||||||
|
Some(Event::AutumnEquinox(year_events.autumn_equinox))
|
||||||
|
} else if date <= year_events.winter_solstice {
|
||||||
|
Some(Event::WinterSolstice(year_events.winter_solstice))
|
||||||
|
} else {
|
||||||
|
self.0
|
||||||
|
.get(&(date.year() + 1))
|
||||||
|
.map(|_| Event::SpringEquinox(year_events.spring_equinox))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Vec<Option<YearlyEvents>>> for Solstices {
|
||||||
|
fn from(event_list: Vec<Option<YearlyEvents>>) -> Self {
|
||||||
|
Solstices(event_list.iter().fold(HashMap::new(), |mut m, record| {
|
||||||
|
match record {
|
||||||
|
Some(record) => {
|
||||||
|
m.insert(record.year, *record);
|
||||||
|
}
|
||||||
|
None => (),
|
||||||
|
}
|
||||||
|
m
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
pub static ref EVENTS: Solstices = Solstices::from(parse_events());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use chrono::{NaiveDate, NaiveDateTime};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_can_parse_a_solstice_time() {
|
||||||
|
let p = "2001 Mar 20 13:31".to_owned();
|
||||||
|
let parsed_date = NaiveDateTime::parse_from_str(&p, "%Y %b %d %H:%M")
|
||||||
|
.unwrap()
|
||||||
|
.and_utc();
|
||||||
|
assert_eq!(
|
||||||
|
parsed_date,
|
||||||
|
NaiveDate::from_ymd_opt(2001, 03, 20)
|
||||||
|
.unwrap()
|
||||||
|
.and_hms_opt(13, 31, 0)
|
||||||
|
.unwrap()
|
||||||
|
.and_utc()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,154 @@
|
||||||
|
// 41.78, -71.41
|
||||||
|
// https://api.solunar.org/solunar/41.78,-71.41,20211029,-4
|
||||||
|
|
||||||
|
use chrono::{DateTime, Duration, Local, NaiveTime, Offset, TimeZone, Timelike, Utc};
|
||||||
|
use geo_types::{Latitude, Longitude};
|
||||||
|
use memorycache::MemoryCache;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
const ENDPOINT: &str = "https://api.solunar.org/solunar";
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct SunMoon {
|
||||||
|
pub sunrise: NaiveTime,
|
||||||
|
pub sunset: NaiveTime,
|
||||||
|
pub moonrise: Option<NaiveTime>,
|
||||||
|
pub moonset: Option<NaiveTime>,
|
||||||
|
pub moon_phase: LunarPhase,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SunMoon {
|
||||||
|
fn from_js(val: SunMoonJs) -> Self {
|
||||||
|
fn parse_time(val: String) -> Option<NaiveTime> {
|
||||||
|
NaiveTime::parse_from_str(&val, "%H:%M").ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
let sunrise = parse_time(val.sunrise).unwrap();
|
||||||
|
let sunset = parse_time(val.sunset).unwrap();
|
||||||
|
let moonrise = val.moonrise.and_then(parse_time);
|
||||||
|
let moonset = val.moonset.and_then(parse_time);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
sunrise,
|
||||||
|
sunset,
|
||||||
|
moonrise,
|
||||||
|
moonset,
|
||||||
|
moon_phase: val.moon_phase,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
pub(crate) struct SunMoonJs {
|
||||||
|
#[serde(alias = "sunRise")]
|
||||||
|
sunrise: String,
|
||||||
|
#[serde(alias = "sunSet")]
|
||||||
|
sunset: String,
|
||||||
|
#[serde(alias = "moonRise")]
|
||||||
|
moonrise: Option<String>,
|
||||||
|
#[serde(alias = "moonSet")]
|
||||||
|
moonset: Option<String>,
|
||||||
|
#[serde(alias = "moonPhase")]
|
||||||
|
moon_phase: LunarPhase,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, PartialEq)]
|
||||||
|
pub enum LunarPhase {
|
||||||
|
#[serde(alias = "New Moon")]
|
||||||
|
NewMoon,
|
||||||
|
#[serde(alias = "Waxing Crescent")]
|
||||||
|
WaxingCrescent,
|
||||||
|
#[serde(alias = "First Quarter")]
|
||||||
|
FirstQuarter,
|
||||||
|
#[serde(alias = "Waxing Gibbous")]
|
||||||
|
WaxingGibbous,
|
||||||
|
#[serde(alias = "Full Moon")]
|
||||||
|
FullMoon,
|
||||||
|
#[serde(alias = "Waning Gibbous")]
|
||||||
|
WaningGibbous,
|
||||||
|
#[serde(alias = "Last Quarter")]
|
||||||
|
LastQuarter,
|
||||||
|
#[serde(alias = "Waning Crescent")]
|
||||||
|
WaningCrescent,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SolunaClient {
|
||||||
|
client: reqwest::Client,
|
||||||
|
memory_cache: MemoryCache<SunMoonJs>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SolunaClient {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
client: reqwest::Client::new(),
|
||||||
|
memory_cache: MemoryCache::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn request<Tz: TimeZone>(
|
||||||
|
&self,
|
||||||
|
latitude: Latitude,
|
||||||
|
longitude: Longitude,
|
||||||
|
day: DateTime<Tz>,
|
||||||
|
) -> SunMoon {
|
||||||
|
let date = day.date_naive().format("%Y%m%d");
|
||||||
|
let url = format!(
|
||||||
|
"{}/{},{},{},{}",
|
||||||
|
ENDPOINT,
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
|
date,
|
||||||
|
day.offset().fix().local_minus_utc() / 3600
|
||||||
|
);
|
||||||
|
let js = self
|
||||||
|
.memory_cache
|
||||||
|
.find(&url, async {
|
||||||
|
let response = self.client.get(&url).send().await.unwrap();
|
||||||
|
let expiration = response
|
||||||
|
.headers()
|
||||||
|
.get(reqwest::header::EXPIRES)
|
||||||
|
.and_then(|header| header.to_str().ok())
|
||||||
|
.and_then(|expiration| DateTime::parse_from_rfc2822(expiration).ok())
|
||||||
|
.map(DateTime::<Utc>::from)
|
||||||
|
.unwrap_or(
|
||||||
|
Local::now()
|
||||||
|
.with_hour(0)
|
||||||
|
.and_then(|dt| dt.with_minute(0))
|
||||||
|
.and_then(|dt| dt.with_second(0))
|
||||||
|
.and_then(|dt| dt.with_nanosecond(0))
|
||||||
|
.map(|dt| dt.with_timezone(&Utc))
|
||||||
|
.unwrap()
|
||||||
|
+ Duration::days(1),
|
||||||
|
);
|
||||||
|
let soluna: SunMoonJs = response.json().await.unwrap();
|
||||||
|
(expiration, soluna)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
SunMoon::from_js(js)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use serde_json;
|
||||||
|
|
||||||
|
const EXAMPLE: &str = "{\"sunRise\":\"7:15\",\"sunTransit\":\"12:30\",\"sunSet\":\"17:45\",\"moonRise\":null,\"moonTransit\":\"7:30\",\"moonUnder\":\"19:54\",\"moonSet\":\"15:02\",\"moonPhase\":\"Waning Crescent\",\"moonIllumination\":0.35889454647387764,\"sunRiseDec\":7.25,\"sunTransitDec\":12.5,\"sunSetDec\":17.75,\"moonRiseDec\":null,\"moonSetDec\":15.033333333333333,\"moonTransitDec\":7.5,\"moonUnderDec\":19.9,\"minor1Start\":null,\"minor1Stop\":null,\"minor2StartDec\":14.533333333333333,\"minor2Start\":\"14:32\",\"minor2StopDec\":15.533333333333333,\"minor2Stop\":\"15:32\",\"major1StartDec\":6.5,\"major1Start\":\"06:30\",\"major1StopDec\":8.5,\"major1Stop\":\"08:30\",\"major2StartDec\":18.9,\"major2Start\":\"18:54\",\"major2StopDec\":20.9,\"major2Stop\":\"20:54\",\"dayRating\":1,\"hourlyRating\":{\"0\":20,\"1\":20,\"2\":0,\"3\":0,\"4\":0,\"5\":0,\"6\":20,\"7\":40,\"8\":40,\"9\":20,\"10\":0,\"11\":0,\"12\":0,\"13\":0,\"14\":0,\"15\":20,\"16\":20,\"17\":20,\"18\":40,\"19\":20,\"20\":20,\"21\":20,\"22\":0,\"23\":0}}";
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_parses_a_response() {
|
||||||
|
let sun_moon_js: SunMoonJs = serde_json::from_str(EXAMPLE).unwrap();
|
||||||
|
let sun_moon = SunMoon::from_js(sun_moon_js);
|
||||||
|
assert_eq!(
|
||||||
|
sun_moon,
|
||||||
|
SunMoon {
|
||||||
|
sunrise: NaiveTime::from_hms_opt(7, 15, 0).unwrap(),
|
||||||
|
sunset: NaiveTime::from_hms_opt(17, 45, 0).unwrap(),
|
||||||
|
moonrise: None,
|
||||||
|
moonset: Some(NaiveTime::from_hms_opt(15, 02, 0).unwrap()),
|
||||||
|
moon_phase: LunarPhase::WaningCrescent,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
use crate::{
|
||||||
|
solstices::{Event, YearlyEvents},
|
||||||
|
soluna_client::SunMoon,
|
||||||
|
};
|
||||||
|
use chrono::NaiveDate;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct State {
|
||||||
|
pub date: NaiveDate,
|
||||||
|
pub next_event: Event,
|
||||||
|
pub events: YearlyEvents,
|
||||||
|
pub transit: Option<SunMoon>,
|
||||||
|
}
|
|
@ -36,9 +36,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono-tz"
|
name = "chrono-tz"
|
||||||
version = "0.6.1"
|
version = "0.8.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "58549f1842da3080ce63002102d5bc954c7bc843d4f47818e642abdc36253552"
|
checksum = "cf9cc2b23599e6d7479755f3594285efb3f74a1bdca7a7374948bc831e23a552"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"chrono-tz-build",
|
"chrono-tz-build",
|
||||||
|
@ -48,9 +48,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono-tz-build"
|
name = "chrono-tz-build"
|
||||||
version = "0.0.2"
|
version = "0.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "db058d493fb2f65f41861bfed7e3fe6335264a9f0f92710cab5bdf01fef09069"
|
checksum = "d9998fb9f7e9b2111641485bf8beb32f92945f97f92a3d061f744cfef335f751"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"parse-zoneinfo",
|
"parse-zoneinfo",
|
||||||
"phf",
|
"phf",
|
||||||
|
@ -71,7 +71,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "emseries"
|
name = "emseries"
|
||||||
version = "0.5.1"
|
version = "0.6.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"chrono-tz",
|
"chrono-tz",
|
||||||
|
@ -82,7 +82,6 @@ dependencies = [
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"uuid",
|
"uuid",
|
||||||
"yaml-rust",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -135,12 +134,6 @@ version = "0.2.124"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "21a41fed9d98f27ab1c6d161da622a4fa35e8a54a8adc24bbf3ddd0ef70b0e50"
|
checksum = "21a41fed9d98f27ab1c6d161da622a4fa35e8a54a8adc24bbf3ddd0ef70b0e50"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "linked-hash-map"
|
|
||||||
version = "0.5.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-integer"
|
name = "num-integer"
|
||||||
version = "0.1.44"
|
version = "0.1.44"
|
||||||
|
@ -171,18 +164,18 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "phf"
|
name = "phf"
|
||||||
version = "0.10.1"
|
version = "0.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259"
|
checksum = "928c6535de93548188ef63bb7c4036bd415cd8f36ad25af44b9789b2ee72a48c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"phf_shared",
|
"phf_shared",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "phf_codegen"
|
name = "phf_codegen"
|
||||||
version = "0.10.0"
|
version = "0.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd"
|
checksum = "a56ac890c5e3ca598bbdeaa99964edb5b0258a583a9eb6ef4e89fc85d9224770"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"phf_generator",
|
"phf_generator",
|
||||||
"phf_shared",
|
"phf_shared",
|
||||||
|
@ -190,9 +183,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "phf_generator"
|
name = "phf_generator"
|
||||||
version = "0.10.0"
|
version = "0.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6"
|
checksum = "b1181c94580fa345f50f19d738aaa39c0ed30a600d95cb2d3e23f94266f14fbf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"phf_shared",
|
"phf_shared",
|
||||||
"rand",
|
"rand",
|
||||||
|
@ -200,20 +193,13 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "phf_shared"
|
name = "phf_shared"
|
||||||
version = "0.10.0"
|
version = "0.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096"
|
checksum = "e1fb5f6f826b772a8d4c0394209441e7d37cbbb967ae9c7e0e8134365c9ee676"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"siphasher",
|
"siphasher",
|
||||||
"uncased",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ppv-lite86"
|
|
||||||
version = "0.2.16"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.37"
|
version = "1.0.37"
|
||||||
|
@ -238,18 +224,6 @@ version = "0.8.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
|
||||||
"rand_chacha",
|
|
||||||
"rand_core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rand_chacha"
|
|
||||||
version = "0.3.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
|
||||||
dependencies = [
|
|
||||||
"ppv-lite86",
|
|
||||||
"rand_core",
|
"rand_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -258,9 +232,6 @@ name = "rand_core"
|
||||||
version = "0.6.3"
|
version = "0.6.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
|
checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
|
||||||
dependencies = [
|
|
||||||
"getrandom",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
|
@ -392,18 +363,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "typenum"
|
||||||
version = "1.15.0"
|
version = "1.16.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
|
checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "uncased"
|
|
||||||
version = "0.9.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5baeed7327e25054889b9bd4f975f32e5f4c5d434042d59ab6cd4142c0a76ed0"
|
|
||||||
dependencies = [
|
|
||||||
"version_check",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-xid"
|
name = "unicode-xid"
|
||||||
|
@ -421,12 +383,6 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "version_check"
|
|
||||||
version = "0.9.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
version = "0.10.2+wasi-snapshot-preview1"
|
version = "0.10.2+wasi-snapshot-preview1"
|
||||||
|
@ -454,12 +410,3 @@ name = "winapi-x86_64-pc-windows-gnu"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "yaml-rust"
|
|
||||||
version = "0.4.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
|
|
||||||
dependencies = [
|
|
||||||
"linked-hash-map",
|
|
||||||
]
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
[package]
|
[package]
|
||||||
name = "emseries"
|
name = "emseries"
|
||||||
version = "0.5.1"
|
version = "0.6.0"
|
||||||
authors = ["Savanni D'Gerinel <savanni@luminescent-dreams.com>"]
|
authors = ["Savanni D'Gerinel <savanni@luminescent-dreams.com>"]
|
||||||
description = "an Embedded Time Series database"
|
description = "an Embedded Time Series database"
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
license-file = "../COPYING"
|
# license-file = "../COPYING"
|
||||||
documentation = "https://docs.rs/emseries"
|
documentation = "https://docs.rs/emseries"
|
||||||
homepage = "https://github.com/luminescent-dreams/emseries"
|
homepage = "https://github.com/luminescent-dreams/emseries"
|
||||||
repository = "https://github.com/luminescent-dreams/emseries"
|
repository = "https://github.com/luminescent-dreams/emseries"
|
||||||
|
@ -18,14 +18,13 @@ include = [
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
chrono-tz = { version = "0.6", features = ["serde"] }
|
chrono-tz = { version = "0.8", features = ["serde"] }
|
||||||
dimensioned = { version = "0.7", features = ["serde"] }
|
|
||||||
serde = "1"
|
serde = "1"
|
||||||
serde_derive = "1"
|
serde_derive = "1"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
uuid = { version = "0.8", features = ["v4", "serde"] }
|
uuid = { version = "0.8", features = ["v4", "serde"] }
|
||||||
yaml-rust = "0.4"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3.1"
|
tempfile = "3.1"
|
||||||
|
dimensioned = { version = "0.7", features = ["serde"] }
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
|
|
||||||
dev:
|
|
||||||
cargo watch -x build
|
|
||||||
|
|
||||||
test:
|
|
||||||
cargo watch -x test
|
|
||||||
|
|
||||||
test-once:
|
|
||||||
cargo test
|
|
|
@ -1,2 +0,0 @@
|
||||||
{"data":{"weight":77.79109,"date":"2003-11-10T06:00:00.000000000000Z"},"id":"3330c5b0-783f-4919-b2c4-8169c38f65ff"}
|
|
||||||
{"data":{"weight":77.56429,"date":"2003-11-11T06:00:00.000000000000Z"},"id":"54c10502-030e-43d2-9ca6-df2f9a5a5ddf"}
|
|
|
@ -10,8 +10,7 @@ Luminescent Dreams Tools is distributed in the hope that it will be useful, but
|
||||||
You should have received a copy of the GNU General Public License along with Lumeto. If not, see <https://www.gnu.org/licenses/>.
|
You should have received a copy of the GNU General Public License along with Lumeto. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use date_time_tz::DateTimeTz;
|
use types::{Recordable, Timestamp};
|
||||||
use types::Recordable;
|
|
||||||
|
|
||||||
/// This trait is used for constructing queries for searching the database.
|
/// This trait is used for constructing queries for searching the database.
|
||||||
pub trait Criteria {
|
pub trait Criteria {
|
||||||
|
@ -45,7 +44,7 @@ pub struct Or<A: Criteria, B: Criteria> {
|
||||||
/// Specify the starting time for a search. This consists of a UTC timestamp and a specifier as to
|
/// Specify the starting time for a search. This consists of a UTC timestamp and a specifier as to
|
||||||
/// whether the exact time is included in the search criteria.
|
/// whether the exact time is included in the search criteria.
|
||||||
pub struct StartTime {
|
pub struct StartTime {
|
||||||
pub time: DateTimeTz,
|
pub time: Timestamp,
|
||||||
pub incl: bool,
|
pub incl: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,7 +61,7 @@ impl Criteria for StartTime {
|
||||||
/// Specify the ending time for a search. This consists of a UTC timestamp and a specifier as to
|
/// Specify the ending time for a search. This consists of a UTC timestamp and a specifier as to
|
||||||
/// whether the exact time is included in the search criteria.
|
/// whether the exact time is included in the search criteria.
|
||||||
pub struct EndTime {
|
pub struct EndTime {
|
||||||
pub time: DateTimeTz,
|
pub time: Timestamp,
|
||||||
pub incl: bool,
|
pub incl: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,7 +88,7 @@ impl Criteria for Tags {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Specify a criteria that searches for records matching an exact time.
|
/// Specify a criteria that searches for records matching an exact time.
|
||||||
pub fn exact_time(time: DateTimeTz) -> And<StartTime, EndTime> {
|
pub fn exact_time(time: Timestamp) -> And<StartTime, EndTime> {
|
||||||
And {
|
And {
|
||||||
lside: StartTime {
|
lside: StartTime {
|
||||||
time: time.clone(),
|
time: time.clone(),
|
||||||
|
@ -101,9 +100,9 @@ pub fn exact_time(time: DateTimeTz) -> And<StartTime, EndTime> {
|
||||||
|
|
||||||
/// Specify a criteria that searches for all records within a time range.
|
/// Specify a criteria that searches for all records within a time range.
|
||||||
pub fn time_range(
|
pub fn time_range(
|
||||||
start: DateTimeTz,
|
start: Timestamp,
|
||||||
start_incl: bool,
|
start_incl: bool,
|
||||||
end: DateTimeTz,
|
end: Timestamp,
|
||||||
end_incl: bool,
|
end_incl: bool,
|
||||||
) -> And<StartTime, EndTime> {
|
) -> And<StartTime, EndTime> {
|
||||||
And {
|
And {
|
||||||
|
|
|
@ -1,166 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2020-2023, Savanni D'Gerinel <savanni@luminescent-dreams.com>
|
|
||||||
|
|
||||||
This file is part of the Luminescent Dreams Tools.
|
|
||||||
|
|
||||||
Luminescent Dreams Tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
|
||||||
|
|
||||||
Luminescent Dreams Tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License along with Lumeto. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
extern crate chrono;
|
|
||||||
extern crate chrono_tz;
|
|
||||||
|
|
||||||
use chrono::SecondsFormat;
|
|
||||||
use chrono_tz::Etc::UTC;
|
|
||||||
use serde::de::{self, Deserialize, Deserializer, Visitor};
|
|
||||||
use serde::ser::{Serialize, Serializer};
|
|
||||||
use std::fmt;
|
|
||||||
|
|
||||||
/// This is a wrapper around date time objects, using timezones from the chroon-tz database and
|
|
||||||
/// providing string representation and parsing of the form "<RFC3339> <Timezone Name>", i.e.,
|
|
||||||
/// "2019-05-15T14:30:00Z US/Central". The to_string method, and serde serialization will
|
|
||||||
/// produce a string of this format. The parser will accept an RFC3339-only string of the forms
|
|
||||||
/// "2019-05-15T14:30:00Z", "2019-05-15T14:30:00+00:00", and also an "RFC3339 Timezone Name"
|
|
||||||
/// string.
|
|
||||||
///
|
|
||||||
/// The function here is to generate as close to unambiguous time/date strings, (for earth's
|
|
||||||
/// gravitational frame of reference), as possible. Clumping together the time, offset from UTC,
|
|
||||||
/// and the named time zone allows future parsers to know the exact interpretation of the time in
|
|
||||||
/// the frame of reference of the original recording.
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
|
||||||
pub struct DateTimeTz(pub chrono::DateTime<chrono_tz::Tz>);
|
|
||||||
|
|
||||||
impl DateTimeTz {
|
|
||||||
pub fn map<F>(&self, f: F) -> DateTimeTz
|
|
||||||
where
|
|
||||||
F: FnOnce(chrono::DateTime<chrono_tz::Tz>) -> chrono::DateTime<chrono_tz::Tz>,
|
|
||||||
{
|
|
||||||
DateTimeTz(f(self.0))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_string(&self) -> String {
|
|
||||||
if self.0.timezone() == UTC {
|
|
||||||
self.0.to_rfc3339_opts(SecondsFormat::Secs, true)
|
|
||||||
} else {
|
|
||||||
format!(
|
|
||||||
"{} {}",
|
|
||||||
self.0
|
|
||||||
.with_timezone(&chrono_tz::Etc::UTC)
|
|
||||||
.to_rfc3339_opts(SecondsFormat::Secs, true,),
|
|
||||||
self.0.timezone().name()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_str(s: &str) -> Result<DateTimeTz, chrono::ParseError> {
|
|
||||||
let v: Vec<&str> = s.split_terminator(" ").collect();
|
|
||||||
if v.len() == 2 {
|
|
||||||
let tz = v[1].parse::<chrono_tz::Tz>().unwrap();
|
|
||||||
chrono::DateTime::parse_from_rfc3339(v[0]).map(|ts| DateTimeTz(ts.with_timezone(&tz)))
|
|
||||||
} else {
|
|
||||||
chrono::DateTime::parse_from_rfc3339(v[0]).map(|ts| DateTimeTz(ts.with_timezone(&UTC)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct DateTimeTzVisitor;
|
|
||||||
|
|
||||||
impl<'de> Visitor<'de> for DateTimeTzVisitor {
|
|
||||||
type Value = DateTimeTz;
|
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
formatter.write_str("a string date time representation that can be parsed")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_str<E: de::Error>(self, s: &str) -> Result<Self::Value, E> {
|
|
||||||
DateTimeTz::from_str(s).or(Err(E::custom(format!(
|
|
||||||
"string is not a parsable datetime representation"
|
|
||||||
))))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Serialize for DateTimeTz {
|
|
||||||
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
|
||||||
serializer.serialize_str(&self.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for DateTimeTz {
|
|
||||||
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
|
||||||
deserializer.deserialize_str(DateTimeTzVisitor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
extern crate serde_json;
|
|
||||||
|
|
||||||
use chrono::TimeZone;
|
|
||||||
use chrono_tz::America::Phoenix;
|
|
||||||
use chrono_tz::Etc::UTC;
|
|
||||||
use chrono_tz::US::{Arizona, Central};
|
|
||||||
use date_time_tz::DateTimeTz;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn it_creates_timestamp_with_z() {
|
|
||||||
let t = DateTimeTz(UTC.ymd(2019, 5, 15).and_hms(12, 0, 0));
|
|
||||||
assert_eq!(t.to_string(), "2019-05-15T12:00:00Z");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn it_parses_utc_rfc3339_z() {
|
|
||||||
let t = DateTimeTz::from_str("2019-05-15T12:00:00Z").unwrap();
|
|
||||||
assert_eq!(t, DateTimeTz(UTC.ymd(2019, 5, 15).and_hms(12, 0, 0)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn it_parses_rfc3339_with_offset() {
|
|
||||||
let t = DateTimeTz::from_str("2019-05-15T12:00:00-06:00").unwrap();
|
|
||||||
assert_eq!(t, DateTimeTz(UTC.ymd(2019, 5, 15).and_hms(18, 0, 0)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn it_parses_rfc3339_with_tz() {
|
|
||||||
let t = DateTimeTz::from_str("2019-06-15T19:00:00Z US/Arizona").unwrap();
|
|
||||||
assert_eq!(t, DateTimeTz(UTC.ymd(2019, 6, 15).and_hms(19, 0, 0)));
|
|
||||||
assert_eq!(t, DateTimeTz(Arizona.ymd(2019, 6, 15).and_hms(12, 0, 0)));
|
|
||||||
assert_eq!(t, DateTimeTz(Central.ymd(2019, 6, 15).and_hms(14, 0, 0)));
|
|
||||||
assert_eq!(t.to_string(), "2019-06-15T19:00:00Z US/Arizona");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct DemoStruct {
|
|
||||||
id: String,
|
|
||||||
dt: DateTimeTz,
|
|
||||||
}
|
|
||||||
|
|
||||||
// I used Arizona here specifically because large parts of Arizona do not honor DST, and so
|
|
||||||
// that adds in more ambiguity of the -0700 offset with Pacific time.
|
|
||||||
#[test]
|
|
||||||
fn it_json_serializes() {
|
|
||||||
let t = DateTimeTz::from_str("2019-06-15T19:00:00Z America/Phoenix").unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
serde_json::to_string(&t).unwrap(),
|
|
||||||
"\"2019-06-15T19:00:00Z America/Phoenix\""
|
|
||||||
);
|
|
||||||
|
|
||||||
let demo = DemoStruct {
|
|
||||||
id: String::from("abcdefg"),
|
|
||||||
dt: t,
|
|
||||||
};
|
|
||||||
assert_eq!(
|
|
||||||
serde_json::to_string(&demo).unwrap(),
|
|
||||||
"{\"id\":\"abcdefg\",\"dt\":\"2019-06-15T19:00:00Z America/Phoenix\"}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn it_json_parses() {
|
|
||||||
let t =
|
|
||||||
serde_json::from_str::<DateTimeTz>("\"2019-06-15T19:00:00Z America/Phoenix\"").unwrap();
|
|
||||||
assert_eq!(t, DateTimeTz(Phoenix.ymd(2019, 6, 15).and_hms(12, 0, 0)));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -71,11 +71,9 @@ extern crate thiserror;
|
||||||
extern crate uuid;
|
extern crate uuid;
|
||||||
|
|
||||||
mod criteria;
|
mod criteria;
|
||||||
mod date_time_tz;
|
|
||||||
mod series;
|
mod series;
|
||||||
mod types;
|
mod types;
|
||||||
|
|
||||||
pub use criteria::*;
|
pub use criteria::*;
|
||||||
pub use date_time_tz::DateTimeTz;
|
|
||||||
pub use series::Series;
|
pub use series::Series;
|
||||||
pub use types::{EmseriesReadError, EmseriesWriteError, Recordable, UniqueId};
|
pub use types::{EmseriesReadError, EmseriesWriteError, Record, RecordId, Recordable, Timestamp};
|
||||||
|
|
|
@ -18,13 +18,51 @@ use serde::de::DeserializeOwned;
|
||||||
use serde::ser::Serialize;
|
use serde::ser::Serialize;
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::convert::TryFrom;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::fs::OpenOptions;
|
use std::fs::OpenOptions;
|
||||||
use std::io::{BufRead, BufReader, LineWriter, Write};
|
use std::io::{BufRead, BufReader, LineWriter, Write};
|
||||||
use std::iter::Iterator;
|
use std::iter::Iterator;
|
||||||
|
|
||||||
use criteria::Criteria;
|
use criteria::Criteria;
|
||||||
use types::{EmseriesReadError, EmseriesWriteError, Record, Recordable, UniqueId};
|
use types::{EmseriesReadError, EmseriesWriteError, Record, RecordId, Recordable};
|
||||||
|
|
||||||
|
// A RecordOnDisk, a private data structure, is useful for handling all of the on-disk
|
||||||
|
// representations of a record. Unlike [Record], this one can accept an empty data value to
|
||||||
|
// represent that the data may have been deleted. This is not made public because, so far as the
|
||||||
|
// user is concerned, any record in the system must have data associated with it.
|
||||||
|
#[derive(Clone, Deserialize, Serialize)]
|
||||||
|
struct RecordOnDisk<T: Clone + Recordable> {
|
||||||
|
id: RecordId,
|
||||||
|
data: Option<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
impl<T> FromStr for RecordOnDisk<T>
|
||||||
|
where
|
||||||
|
T: Clone + Recordable + DeserializeOwned + Serialize,
|
||||||
|
{
|
||||||
|
type Err = EmseriesReadError;
|
||||||
|
|
||||||
|
fn from_str(line: &str) -> Result<Self, Self::Err> {
|
||||||
|
serde_json::from_str(line).map_err(EmseriesReadError::JSONParseError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
impl<T: Clone + Recordable> TryFrom<RecordOnDisk<T>> for Record<T> {
|
||||||
|
type Error = EmseriesReadError;
|
||||||
|
|
||||||
|
fn try_from(disk_record: RecordOnDisk<T>) -> Result<Self, Self::Error> {
|
||||||
|
match disk_record.data {
|
||||||
|
Some(data) => Ok(Record {
|
||||||
|
id: disk_record.id,
|
||||||
|
data,
|
||||||
|
}),
|
||||||
|
None => Err(Self::Error::RecordDeleted(disk_record.id)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// An open time series database.
|
/// An open time series database.
|
||||||
///
|
///
|
||||||
|
@ -33,7 +71,7 @@ use types::{EmseriesReadError, EmseriesWriteError, Record, Recordable, UniqueId}
|
||||||
pub struct Series<T: Clone + Recordable + DeserializeOwned + Serialize> {
|
pub struct Series<T: Clone + Recordable + DeserializeOwned + Serialize> {
|
||||||
//path: String,
|
//path: String,
|
||||||
writer: LineWriter<File>,
|
writer: LineWriter<File>,
|
||||||
records: HashMap<UniqueId, T>,
|
records: HashMap<RecordId, Record<T>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Series<T>
|
impl<T> Series<T>
|
||||||
|
@ -42,12 +80,12 @@ where
|
||||||
{
|
{
|
||||||
/// Open a time series database at the specified path. `path` is the full path and filename for
|
/// Open a time series database at the specified path. `path` is the full path and filename for
|
||||||
/// the database.
|
/// the database.
|
||||||
pub fn open(path: &str) -> Result<Series<T>, EmseriesReadError> {
|
pub fn open<P: AsRef<std::path::Path>>(path: P) -> Result<Series<T>, EmseriesReadError> {
|
||||||
let f = OpenOptions::new()
|
let f = OpenOptions::new()
|
||||||
.read(true)
|
.read(true)
|
||||||
.append(true)
|
.append(true)
|
||||||
.create(true)
|
.create(true)
|
||||||
.open(&path)
|
.open(path)
|
||||||
.map_err(EmseriesReadError::IOError)?;
|
.map_err(EmseriesReadError::IOError)?;
|
||||||
|
|
||||||
let records = Series::load_file(&f)?;
|
let records = Series::load_file(&f)?;
|
||||||
|
@ -62,20 +100,18 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load a file and return all of the records in it.
|
/// Load a file and return all of the records in it.
|
||||||
fn load_file(f: &File) -> Result<HashMap<UniqueId, T>, EmseriesReadError> {
|
fn load_file(f: &File) -> Result<HashMap<RecordId, Record<T>>, EmseriesReadError> {
|
||||||
let mut records: HashMap<UniqueId, T> = HashMap::new();
|
let mut records: HashMap<RecordId, Record<T>> = HashMap::new();
|
||||||
let reader = BufReader::new(f);
|
let reader = BufReader::new(f);
|
||||||
for line in reader.lines() {
|
for line in reader.lines() {
|
||||||
match line {
|
match line {
|
||||||
Ok(line_) => {
|
Ok(line_) => {
|
||||||
/* Can't create a JSONParseError because I can't actually create the underlying error.
|
match serde_json::from_str::<RecordOnDisk<T>>(line_.as_ref())
|
||||||
fail_point!("parse-line", Err(Error::JSONParseError()))
|
.map_err(EmseriesReadError::JSONParseError)
|
||||||
*/
|
.and_then(Record::try_from)
|
||||||
match line_.parse::<Record<T>>() {
|
{
|
||||||
Ok(record) => match record.data {
|
Ok(record) => records.insert(record.id, record.clone()),
|
||||||
Some(val) => records.insert(record.id.clone(), val),
|
Err(EmseriesReadError::RecordDeleted(id)) => records.remove(&id),
|
||||||
None => records.remove(&record.id.clone()),
|
|
||||||
},
|
|
||||||
Err(err) => return Err(err),
|
Err(err) => return Err(err),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -87,18 +123,20 @@ where
|
||||||
|
|
||||||
/// Put a new record into the database. A unique id will be assigned to the record and
|
/// Put a new record into the database. A unique id will be assigned to the record and
|
||||||
/// returned.
|
/// returned.
|
||||||
pub fn put(&mut self, entry: T) -> Result<UniqueId, EmseriesWriteError> {
|
pub fn put(&mut self, entry: T) -> Result<RecordId, EmseriesWriteError> {
|
||||||
let uuid = UniqueId::new();
|
let id = RecordId::default();
|
||||||
self.update(uuid.clone(), entry).and_then(|_| Ok(uuid))
|
let record = Record { id, data: entry };
|
||||||
|
self.update(record)?;
|
||||||
|
Ok(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update an existing record. The `UniqueId` of the record passed into this function must match
|
/// Update an existing record. The [RecordId] of the record passed into this function must match
|
||||||
/// the `UniqueId` of a record already in the database.
|
/// the [RecordId] of a record already in the database.
|
||||||
pub fn update(&mut self, uuid: UniqueId, entry: T) -> Result<(), EmseriesWriteError> {
|
pub fn update(&mut self, record: Record<T>) -> Result<(), EmseriesWriteError> {
|
||||||
self.records.insert(uuid.clone(), entry.clone());
|
self.records.insert(record.id, record.clone());
|
||||||
let write_res = match serde_json::to_string(&Record {
|
let write_res = match serde_json::to_string(&RecordOnDisk {
|
||||||
id: uuid,
|
id: record.id,
|
||||||
data: Some(entry),
|
data: Some(record.data),
|
||||||
}) {
|
}) {
|
||||||
Ok(rec_str) => self
|
Ok(rec_str) => self
|
||||||
.writer
|
.writer
|
||||||
|
@ -118,14 +156,14 @@ where
|
||||||
/// Future note: while this deletes a record from the view, it only adds an entry to the
|
/// Future note: while this deletes a record from the view, it only adds an entry to the
|
||||||
/// database that indicates `data: null`. If record histories ever become important, the record
|
/// database that indicates `data: null`. If record histories ever become important, the record
|
||||||
/// and its entire history (including this delete) will still be available.
|
/// and its entire history (including this delete) will still be available.
|
||||||
pub fn delete(&mut self, uuid: &UniqueId) -> Result<(), EmseriesWriteError> {
|
pub fn delete(&mut self, uuid: &RecordId) -> Result<(), EmseriesWriteError> {
|
||||||
if !self.records.contains_key(uuid) {
|
if !self.records.contains_key(uuid) {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
self.records.remove(uuid);
|
self.records.remove(uuid);
|
||||||
|
|
||||||
let rec: Record<T> = Record {
|
let rec: RecordOnDisk<T> = RecordOnDisk {
|
||||||
id: uuid.clone(),
|
id: *uuid,
|
||||||
data: None,
|
data: None,
|
||||||
};
|
};
|
||||||
match serde_json::to_string(&rec) {
|
match serde_json::to_string(&rec) {
|
||||||
|
@ -138,8 +176,8 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get all of the records in the database.
|
/// Get all of the records in the database.
|
||||||
pub fn records<'s>(&'s self) -> impl Iterator<Item = (&'s UniqueId, &'s T)> + 's {
|
pub fn records(&self) -> impl Iterator<Item = &Record<T>> {
|
||||||
self.records.iter()
|
self.records.values()
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The point of having Search is so that a lot of internal optimizations can happen once the
|
/* The point of having Search is so that a lot of internal optimizations can happen once the
|
||||||
|
@ -148,29 +186,29 @@ where
|
||||||
pub fn search<'s>(
|
pub fn search<'s>(
|
||||||
&'s self,
|
&'s self,
|
||||||
criteria: impl Criteria + 's,
|
criteria: impl Criteria + 's,
|
||||||
) -> impl Iterator<Item = (&'s UniqueId, &'s T)> + 's {
|
) -> impl Iterator<Item = &'s Record<T>> + 's {
|
||||||
self.records().filter(move |&tr| criteria.apply(tr.1))
|
self.records().filter(move |&tr| criteria.apply(&tr.data))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Perform a search and sort the resulting records based on the comparison.
|
/// Perform a search and sort the resulting records based on the comparison.
|
||||||
pub fn search_sorted<'s, C, CMP>(&'s self, criteria: C, compare: CMP) -> Vec<(&UniqueId, &T)>
|
pub fn search_sorted<'s, C, CMP>(&'s self, criteria: C, compare: CMP) -> Vec<&'s Record<T>>
|
||||||
where
|
where
|
||||||
C: Criteria + 's,
|
C: Criteria + 's,
|
||||||
CMP: FnMut(&(&UniqueId, &T), &(&UniqueId, &T)) -> Ordering,
|
CMP: FnMut(&&Record<T>, &&Record<T>) -> Ordering,
|
||||||
{
|
{
|
||||||
let search_iter = self.search(criteria);
|
let search_iter = self.search(criteria);
|
||||||
let mut records: Vec<(&UniqueId, &T)> = search_iter.collect();
|
let mut records: Vec<&Record<T>> = search_iter.collect();
|
||||||
records.sort_by(compare);
|
records.sort_by(compare);
|
||||||
records
|
records
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get an exact record from the database based on unique id.
|
/// Get an exact record from the database based on unique id.
|
||||||
pub fn get(&self, uuid: &UniqueId) -> Option<T> {
|
pub fn get(&self, uuid: &RecordId) -> Option<Record<T>> {
|
||||||
self.records.get(uuid).map(|v| v.clone())
|
self.records.get(uuid).cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
pub fn remove(&self, uuid: UniqueId) -> Result<(), EmseriesError> {
|
pub fn remove(&self, uuid: RecordId) -> Result<(), EmseriesError> {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -10,10 +10,8 @@ Luminescent Dreams Tools is distributed in the hope that it will be useful, but
|
||||||
You should have received a copy of the GNU General Public License along with Lumeto. If not, see <https://www.gnu.org/licenses/>.
|
You should have received a copy of the GNU General Public License along with Lumeto. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use date_time_tz::DateTimeTz;
|
use chrono::{DateTime, FixedOffset, NaiveDate};
|
||||||
use serde::de::DeserializeOwned;
|
use std::{cmp::Ordering, fmt, io, str};
|
||||||
use serde::ser::Serialize;
|
|
||||||
use std::{fmt, io, str};
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
@ -27,6 +25,9 @@ pub enum EmseriesReadError {
|
||||||
#[error("Error parsing JSON: {0}")]
|
#[error("Error parsing JSON: {0}")]
|
||||||
JSONParseError(serde_json::error::Error),
|
JSONParseError(serde_json::error::Error),
|
||||||
|
|
||||||
|
#[error("Record was deleted")]
|
||||||
|
RecordDeleted(RecordId),
|
||||||
|
|
||||||
/// Indicates a general IO error
|
/// Indicates a general IO error
|
||||||
#[error("IO Error: {0}")]
|
#[error("IO Error: {0}")]
|
||||||
IOError(io::Error),
|
IOError(io::Error),
|
||||||
|
@ -43,11 +44,74 @@ pub enum EmseriesWriteError {
|
||||||
JSONWriteError(serde_json::error::Error),
|
JSONWriteError(serde_json::error::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
/// A Timestamp, stored with reference to human reckoning. This could be either a Naive Date or a
|
||||||
|
/// date and a time with a timezone. The idea of the "human reckoning" is that, no matter what
|
||||||
|
/// timezone the record was created in, we want to group things based on the date that the human
|
||||||
|
/// was perceiving at the time it was recorded.
|
||||||
|
pub enum Timestamp {
|
||||||
|
DateTime(DateTime<FixedOffset>),
|
||||||
|
Date(NaiveDate),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum TimestampJS {
|
||||||
|
DateTime(String),
|
||||||
|
Date(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Timestamp> for TimestampJS {
|
||||||
|
fn from(s: Timestamp) -> TimestampJS {
|
||||||
|
match s {
|
||||||
|
Timestamp::DateTime(ts) => TimestampJS::DateTime(ts.to_rfc3339()),
|
||||||
|
Timestamp::Date(ts) => TimestampJS::Date(ts.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TimestampJS> for Timestamp {
|
||||||
|
fn from(s: TimestampJS) -> Timestamp {
|
||||||
|
match s {
|
||||||
|
TimestampJS::DateTime(ts) => {
|
||||||
|
Timestamp::DateTime(DateTime::parse_from_rfc3339(&ts).unwrap())
|
||||||
|
}
|
||||||
|
TimestampJS::Date(ts) => Timestamp::Date(ts.parse::<NaiveDate>().unwrap()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl str::FromStr for Timestamp {
|
||||||
|
type Err = chrono::ParseError;
|
||||||
|
fn from_str(line: &str) -> Result<Self, Self::Err> {
|
||||||
|
DateTime::parse_from_rfc3339(line)
|
||||||
|
.map(Timestamp::DateTime)
|
||||||
|
.or(NaiveDate::from_str(line).map(Timestamp::Date))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for Timestamp {
|
||||||
|
fn partial_cmp(&self, other: &Timestamp) -> Option<Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for Timestamp {
|
||||||
|
fn cmp(&self, other: &Timestamp) -> Ordering {
|
||||||
|
match (self, other) {
|
||||||
|
(Timestamp::DateTime(dt1), Timestamp::DateTime(dt2)) => dt1.cmp(dt2),
|
||||||
|
(Timestamp::DateTime(dt1), Timestamp::Date(dt2)) => dt1.date_naive().cmp(dt2),
|
||||||
|
(Timestamp::Date(dt1), Timestamp::DateTime(dt2)) => dt1.cmp(&dt2.date_naive()),
|
||||||
|
(Timestamp::Date(dt1), Timestamp::Date(dt2)) => dt1.cmp(dt2),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Any element to be put into the database needs to be Recordable. This is the common API that
|
/// Any element to be put into the database needs to be Recordable. This is the common API that
|
||||||
/// will aid in searching and later in indexing records.
|
/// will aid in searching and later in indexing records.
|
||||||
pub trait Recordable {
|
pub trait Recordable {
|
||||||
/// The timestamp for the record.
|
/// The timestamp for the record.
|
||||||
fn timestamp(&self) -> DateTimeTz;
|
fn timestamp(&self) -> Timestamp;
|
||||||
|
|
||||||
/// A list of string tags that can be used for indexing. This list defined per-type.
|
/// A list of string tags that can be used for indexing. This list defined per-type.
|
||||||
fn tags(&self) -> Vec<String>;
|
fn tags(&self) -> Vec<String>;
|
||||||
|
@ -56,78 +120,88 @@ pub trait Recordable {
|
||||||
/// Uniquely identifies a record.
|
/// Uniquely identifies a record.
|
||||||
///
|
///
|
||||||
/// This is a wrapper around a basic uuid with some extra convenience methods.
|
/// This is a wrapper around a basic uuid with some extra convenience methods.
|
||||||
#[derive(Clone, Debug, Eq, Hash, PartialEq, Deserialize, Serialize)]
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Deserialize, Serialize)]
|
||||||
pub struct UniqueId(Uuid);
|
pub struct RecordId(Uuid);
|
||||||
|
|
||||||
impl UniqueId {
|
impl Default for RecordId {
|
||||||
/// Create a new V4 UUID (this is the most common type in use these days).
|
fn default() -> Self {
|
||||||
pub fn new() -> UniqueId {
|
Self(Uuid::new_v4())
|
||||||
let id = Uuid::new_v4();
|
|
||||||
UniqueId(id)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl str::FromStr for UniqueId {
|
impl str::FromStr for RecordId {
|
||||||
type Err = EmseriesReadError;
|
type Err = EmseriesReadError;
|
||||||
|
|
||||||
/// Parse a UniqueId from a string. Raise UUIDParseError if the parsing fails.
|
/// Parse a RecordId from a string. Raise UUIDParseError if the parsing fails.
|
||||||
fn from_str(val: &str) -> Result<Self, Self::Err> {
|
fn from_str(val: &str) -> Result<Self, Self::Err> {
|
||||||
Uuid::parse_str(val)
|
Uuid::parse_str(val)
|
||||||
.map(UniqueId)
|
.map(RecordId)
|
||||||
.map_err(|err| EmseriesReadError::UUIDParseError(err))
|
.map_err(EmseriesReadError::UUIDParseError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for UniqueId {
|
impl fmt::Display for RecordId {
|
||||||
/// Convert to a hyphenated string
|
/// Convert to a hyphenated string
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||||
write!(f, "{}", self.0.to_hyphenated().to_string())
|
write!(f, "{}", self.0.to_hyphenated())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Every record contains a unique ID and then the primary data, which itself must implementd the
|
/// A record represents data that actually exists in the database. Users cannot make the record
|
||||||
/// Recordable trait.
|
/// directly, as the database will create them.
|
||||||
#[derive(Clone, Deserialize, Serialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||||
pub struct Record<T: Clone + Recordable> {
|
pub struct Record<T: Clone + Recordable> {
|
||||||
pub id: UniqueId,
|
pub id: RecordId,
|
||||||
pub data: Option<T>,
|
pub data: T,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> str::FromStr for Record<T>
|
impl<T: Clone + Recordable> Record<T> {
|
||||||
where
|
pub fn date(&self) -> NaiveDate {
|
||||||
T: Clone + Recordable + DeserializeOwned + Serialize,
|
match self.data.timestamp() {
|
||||||
{
|
Timestamp::DateTime(dt) => dt.date_naive(),
|
||||||
type Err = EmseriesReadError;
|
Timestamp::Date(dt) => dt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn from_str(line: &str) -> Result<Self, Self::Err> {
|
pub fn timestamp(&self) -> Timestamp {
|
||||||
serde_json::from_str(&line).map_err(|err| EmseriesReadError::JSONParseError(err))
|
self.data.timestamp()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn map<Map, U>(self, map: Map) -> Record<U>
|
||||||
|
where
|
||||||
|
Map: Fn(T) -> U,
|
||||||
|
U: Clone + Recordable,
|
||||||
|
{
|
||||||
|
Record {
|
||||||
|
id: self.id,
|
||||||
|
data: map(self.data),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
extern crate dimensioned;
|
extern crate dimensioned;
|
||||||
|
|
||||||
extern crate serde_json;
|
extern crate serde_json;
|
||||||
|
|
||||||
use self::dimensioned::si::{Kilogram, KG};
|
use self::dimensioned::si::{Kilogram, KG};
|
||||||
use super::{Record, Recordable};
|
use super::*;
|
||||||
use chrono::TimeZone;
|
use chrono::TimeZone;
|
||||||
use chrono_tz::Etc::UTC;
|
use chrono_tz::Etc::UTC;
|
||||||
use chrono_tz::US::Central;
|
|
||||||
use date_time_tz::DateTimeTz;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
|
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
|
||||||
pub struct Weight(Kilogram<f64>);
|
pub struct Weight(Kilogram<f64>);
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
|
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
|
||||||
pub struct WeightRecord {
|
pub struct WeightRecord {
|
||||||
pub date: DateTimeTz,
|
pub date: NaiveDate,
|
||||||
pub weight: Weight,
|
pub weight: Weight,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Recordable for WeightRecord {
|
impl Recordable for WeightRecord {
|
||||||
fn timestamp(&self) -> DateTimeTz {
|
fn timestamp(&self) -> Timestamp {
|
||||||
self.date.clone()
|
Timestamp::Date(self.date)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tags(&self) -> Vec<String> {
|
fn tags(&self) -> Vec<String> {
|
||||||
|
@ -135,10 +209,31 @@ mod test {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const WEIGHT_ENTRY: &str = "{\"data\":{\"weight\":77.79109,\"date\":\"2003-11-10T06:00:00.000000000000Z\"},\"id\":\"3330c5b0-783f-4919-b2c4-8169c38f65ff\"}";
|
#[test]
|
||||||
|
fn timestamp_parses_utc_time() {
|
||||||
|
assert_eq!(
|
||||||
|
"2003-11-10T06:00:00Z".parse::<Timestamp>().unwrap(),
|
||||||
|
Timestamp::DateTime(
|
||||||
|
UTC.with_ymd_and_hms(2003, 11, 10, 6, 0, 0)
|
||||||
|
.unwrap()
|
||||||
|
.with_timezone(&FixedOffset::east_opt(0).unwrap())
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
pub fn legacy_deserialization() {
|
fn timestamp_parses_date() {
|
||||||
|
assert_eq!(
|
||||||
|
"2023-11-10".parse::<Timestamp>().unwrap(),
|
||||||
|
Timestamp::Date(NaiveDate::from_ymd_opt(2023, 11, 10).unwrap())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
#[ignore]
|
||||||
|
fn v_alpha_serialization() {
|
||||||
|
const WEIGHT_ENTRY: &str = "{\"data\":{\"weight\":77.79109},\"date\":\"2003-11-10\",\"id\":\"3330c5b0-783f-4919-b2c4-8169c38f65ff\"}";
|
||||||
|
|
||||||
let rec: Record<WeightRecord> = WEIGHT_ENTRY
|
let rec: Record<WeightRecord> = WEIGHT_ENTRY
|
||||||
.parse()
|
.parse()
|
||||||
.expect("should successfully parse the record");
|
.expect("should successfully parse the record");
|
||||||
|
@ -148,31 +243,65 @@ mod test {
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
rec.data,
|
rec.data,
|
||||||
Some(WeightRecord {
|
WeightRecord {
|
||||||
date: DateTimeTz(UTC.ymd(2003, 11, 10).and_hms(6, 0, 0)),
|
date: NaiveDate::from_ymd_opt(2003, 11, 10).unwrap(),
|
||||||
weight: Weight(77.79109 * KG),
|
weight: Weight(77.79109 * KG),
|
||||||
})
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
pub fn serialization_output() {
|
fn serialization_output() {
|
||||||
let rec = WeightRecord {
|
let rec = WeightRecord {
|
||||||
date: DateTimeTz(UTC.ymd(2003, 11, 10).and_hms(6, 0, 0)),
|
date: NaiveDate::from_ymd_opt(2003, 11, 10).unwrap(),
|
||||||
weight: Weight(77.0 * KG),
|
weight: Weight(77.0 * KG),
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
serde_json::to_string(&rec).unwrap(),
|
serde_json::to_string(&rec).unwrap(),
|
||||||
"{\"date\":\"2003-11-10T06:00:00Z\",\"weight\":77.0}"
|
"{\"date\":\"2003-11-10\",\"weight\":77.0}"
|
||||||
);
|
);
|
||||||
|
|
||||||
let rec2 = WeightRecord {
|
let rec2 = WeightRecord {
|
||||||
date: DateTimeTz(Central.ymd(2003, 11, 10).and_hms(0, 0, 0)),
|
date: NaiveDate::from_ymd_opt(2003, 11, 10).unwrap(),
|
||||||
weight: Weight(77.0 * KG),
|
weight: Weight(77.0 * KG),
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
serde_json::to_string(&rec2).unwrap(),
|
serde_json::to_string(&rec2).unwrap(),
|
||||||
"{\"date\":\"2003-11-10T06:00:00Z US/Central\",\"weight\":77.0}"
|
"{\"date\":\"2003-11-10\",\"weight\":77.0}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn two_datetimes_can_be_compared() {
|
||||||
|
let time1 = Timestamp::DateTime(
|
||||||
|
UTC.with_ymd_and_hms(2003, 11, 10, 6, 0, 0)
|
||||||
|
.unwrap()
|
||||||
|
.with_timezone(&FixedOffset::east_opt(0).unwrap()),
|
||||||
|
);
|
||||||
|
let time2 = Timestamp::DateTime(
|
||||||
|
UTC.with_ymd_and_hms(2003, 11, 11, 6, 0, 0)
|
||||||
|
.unwrap()
|
||||||
|
.with_timezone(&FixedOffset::east_opt(0).unwrap()),
|
||||||
|
);
|
||||||
|
assert!(time1 < time2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn two_dates_can_be_compared() {
|
||||||
|
let time1: Timestamp = Timestamp::Date(NaiveDate::from_ymd_opt(2003, 11, 10).unwrap());
|
||||||
|
let time2: Timestamp = Timestamp::Date(NaiveDate::from_ymd_opt(2003, 11, 11).unwrap());
|
||||||
|
assert!(time1 < time2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn datetime_and_date_can_be_compared() {
|
||||||
|
let time1 = Timestamp::DateTime(
|
||||||
|
UTC.with_ymd_and_hms(2003, 11, 10, 6, 0, 0)
|
||||||
|
.unwrap()
|
||||||
|
.with_timezone(&FixedOffset::east_opt(0).unwrap()),
|
||||||
|
);
|
||||||
|
let time2 = Timestamp::Date(NaiveDate::from_ymd_opt(2003, 11, 11).unwrap());
|
||||||
|
assert!(time1 < time2)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,9 +20,9 @@ extern crate emseries;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use chrono::prelude::*;
|
use chrono::{prelude::*};
|
||||||
use chrono_tz::Etc::UTC;
|
use chrono_tz::Etc::UTC;
|
||||||
use dimensioned::si::{Kilogram, Meter, Second, KG, M, S};
|
use dimensioned::si::{Kilogram, Meter, Second, M, S};
|
||||||
|
|
||||||
use emseries::*;
|
use emseries::*;
|
||||||
|
|
||||||
|
@ -34,15 +34,15 @@ mod test {
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
|
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
|
||||||
struct BikeTrip {
|
struct BikeTrip {
|
||||||
datetime: DateTimeTz,
|
datetime: DateTime<FixedOffset>,
|
||||||
distance: Distance,
|
distance: Distance,
|
||||||
duration: Duration,
|
duration: Duration,
|
||||||
comments: String,
|
comments: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Recordable for BikeTrip {
|
impl Recordable for BikeTrip {
|
||||||
fn timestamp(&self) -> DateTimeTz {
|
fn timestamp(&self) -> Timestamp {
|
||||||
self.datetime.clone()
|
Timestamp::DateTime(self.datetime)
|
||||||
}
|
}
|
||||||
fn tags(&self) -> Vec<String> {
|
fn tags(&self) -> Vec<String> {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
|
@ -52,31 +52,46 @@ mod test {
|
||||||
fn mk_trips() -> [BikeTrip; 5] {
|
fn mk_trips() -> [BikeTrip; 5] {
|
||||||
[
|
[
|
||||||
BikeTrip {
|
BikeTrip {
|
||||||
datetime: DateTimeTz(UTC.ymd(2011, 10, 29).and_hms(0, 0, 0)),
|
datetime: UTC
|
||||||
|
.with_ymd_and_hms(2011, 10, 29, 0, 0, 0)
|
||||||
|
.unwrap()
|
||||||
|
.with_timezone(&FixedOffset::east_opt(0).unwrap()),
|
||||||
distance: Distance(58741.055 * M),
|
distance: Distance(58741.055 * M),
|
||||||
duration: Duration(11040.0 * S),
|
duration: Duration(11040.0 * S),
|
||||||
comments: String::from("long time ago"),
|
comments: String::from("long time ago"),
|
||||||
},
|
},
|
||||||
BikeTrip {
|
BikeTrip {
|
||||||
datetime: DateTimeTz(UTC.ymd(2011, 10, 31).and_hms(0, 0, 0)),
|
datetime: UTC
|
||||||
|
.with_ymd_and_hms(2011, 10, 31, 0, 0, 0)
|
||||||
|
.unwrap()
|
||||||
|
.with_timezone(&FixedOffset::east_opt(0).unwrap()),
|
||||||
distance: Distance(17702.0 * M),
|
distance: Distance(17702.0 * M),
|
||||||
duration: Duration(2880.0 * S),
|
duration: Duration(2880.0 * S),
|
||||||
comments: String::from("day 2"),
|
comments: String::from("day 2"),
|
||||||
},
|
},
|
||||||
BikeTrip {
|
BikeTrip {
|
||||||
datetime: DateTimeTz(UTC.ymd(2011, 11, 02).and_hms(0, 0, 0)),
|
datetime: UTC
|
||||||
|
.with_ymd_and_hms(2011, 11, 02, 0, 0, 0)
|
||||||
|
.unwrap()
|
||||||
|
.with_timezone(&FixedOffset::east_opt(0).unwrap()),
|
||||||
distance: Distance(41842.945 * M),
|
distance: Distance(41842.945 * M),
|
||||||
duration: Duration(7020.0 * S),
|
duration: Duration(7020.0 * S),
|
||||||
comments: String::from("Do Some Distance!"),
|
comments: String::from("Do Some Distance!"),
|
||||||
},
|
},
|
||||||
BikeTrip {
|
BikeTrip {
|
||||||
datetime: DateTimeTz(UTC.ymd(2011, 11, 04).and_hms(0, 0, 0)),
|
datetime: UTC
|
||||||
|
.with_ymd_and_hms(2011, 11, 04, 0, 0, 0)
|
||||||
|
.unwrap()
|
||||||
|
.with_timezone(&FixedOffset::east_opt(0).unwrap()),
|
||||||
distance: Distance(34600.895 * M),
|
distance: Distance(34600.895 * M),
|
||||||
duration: Duration(5580.0 * S),
|
duration: Duration(5580.0 * S),
|
||||||
comments: String::from("I did a lot of distance back then"),
|
comments: String::from("I did a lot of distance back then"),
|
||||||
},
|
},
|
||||||
BikeTrip {
|
BikeTrip {
|
||||||
datetime: DateTimeTz(UTC.ymd(2011, 11, 05).and_hms(0, 0, 0)),
|
datetime: UTC
|
||||||
|
.with_ymd_and_hms(2011, 11, 05, 0, 0, 0)
|
||||||
|
.unwrap()
|
||||||
|
.with_timezone(&FixedOffset::east_opt(0).unwrap()),
|
||||||
distance: Distance(6437.376 * M),
|
distance: Distance(6437.376 * M),
|
||||||
duration: Duration(960.0 * S),
|
duration: Duration(960.0 * S),
|
||||||
comments: String::from("day 5"),
|
comments: String::from("day 5"),
|
||||||
|
@ -84,7 +99,7 @@ mod test {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_test<T>(test: T) -> ()
|
fn run_test<T>(test: T)
|
||||||
where
|
where
|
||||||
T: FnOnce(tempfile::TempPath),
|
T: FnOnce(tempfile::TempPath),
|
||||||
{
|
{
|
||||||
|
@ -93,14 +108,14 @@ mod test {
|
||||||
test(tmp_path);
|
test(tmp_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run<T>(test: T) -> ()
|
fn run<T>(test: T)
|
||||||
where
|
where
|
||||||
T: FnOnce(Series<BikeTrip>),
|
T: FnOnce(Series<BikeTrip>),
|
||||||
{
|
{
|
||||||
let tmp_file = tempfile::NamedTempFile::new().expect("temporary path created");
|
let tmp_file = tempfile::NamedTempFile::new().expect("temporary path created");
|
||||||
let tmp_path = tmp_file.into_temp_path();
|
let tmp_path = tmp_file.into_temp_path();
|
||||||
let ts: Series<BikeTrip> = Series::open(&tmp_path.to_string_lossy())
|
let ts: Series<BikeTrip> =
|
||||||
.expect("the time series should open correctly");
|
Series::open(&tmp_path).expect("the time series should open correctly");
|
||||||
test(ts);
|
test(ts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,11 +137,15 @@ mod test {
|
||||||
Some(tr) => {
|
Some(tr) => {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tr.timestamp(),
|
tr.timestamp(),
|
||||||
DateTimeTz(UTC.ymd(2011, 10, 29).and_hms(0, 0, 0))
|
Timestamp::DateTime(
|
||||||
|
UTC.with_ymd_and_hms(2011, 10, 29, 0, 0, 0)
|
||||||
|
.unwrap()
|
||||||
|
.with_timezone(&FixedOffset::east_opt(0).unwrap())
|
||||||
|
)
|
||||||
);
|
);
|
||||||
assert_eq!(tr.duration, Duration(11040.0 * S));
|
assert_eq!(tr.data.duration, Duration(11040.0 * S));
|
||||||
assert_eq!(tr.comments, String::from("long time ago"));
|
assert_eq!(tr.data.comments, String::from("long time ago"));
|
||||||
assert_eq!(tr, trips[0]);
|
assert_eq!(tr.data, trips[0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -136,20 +155,22 @@ mod test {
|
||||||
pub fn can_search_for_an_entry_with_exact_time() {
|
pub fn can_search_for_an_entry_with_exact_time() {
|
||||||
run_test(|path| {
|
run_test(|path| {
|
||||||
let trips = mk_trips();
|
let trips = mk_trips();
|
||||||
let mut ts: Series<BikeTrip> = Series::open(&path.to_string_lossy())
|
let mut ts: Series<BikeTrip> =
|
||||||
.expect("expect the time series to open correctly");
|
Series::open(&path).expect("expect the time series to open correctly");
|
||||||
|
|
||||||
for trip in &trips[0..=4] {
|
for trip in &trips[0..=4] {
|
||||||
ts.put(trip.clone()).expect("expect a successful put");
|
ts.put(trip.clone()).expect("expect a successful put");
|
||||||
}
|
}
|
||||||
|
|
||||||
let v: Vec<(&UniqueId, &BikeTrip)> = ts
|
let v: Vec<&Record<BikeTrip>> = ts
|
||||||
.search(exact_time(DateTimeTz(
|
.search(exact_time(Timestamp::DateTime(
|
||||||
UTC.ymd(2011, 10, 31).and_hms(0, 0, 0),
|
UTC.with_ymd_and_hms(2011, 10, 31, 0, 0, 0)
|
||||||
|
.unwrap()
|
||||||
|
.with_timezone(&FixedOffset::east_opt(0).unwrap()),
|
||||||
)))
|
)))
|
||||||
.collect();
|
.collect();
|
||||||
assert_eq!(v.len(), 1);
|
assert_eq!(v.len(), 1);
|
||||||
assert_eq!(*v[0].1, trips[1]);
|
assert_eq!(v[0].data, trips[1]);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -157,26 +178,34 @@ mod test {
|
||||||
pub fn can_get_entries_in_time_range() {
|
pub fn can_get_entries_in_time_range() {
|
||||||
run_test(|path| {
|
run_test(|path| {
|
||||||
let trips = mk_trips();
|
let trips = mk_trips();
|
||||||
let mut ts: Series<BikeTrip> = Series::open(&path.to_string_lossy())
|
let mut ts: Series<BikeTrip> =
|
||||||
.expect("expect the time series to open correctly");
|
Series::open(&path).expect("expect the time series to open correctly");
|
||||||
|
|
||||||
for trip in &trips[0..=4] {
|
for trip in &trips[0..=4] {
|
||||||
ts.put(trip.clone()).expect("expect a successful put");
|
ts.put(trip.clone()).expect("expect a successful put");
|
||||||
}
|
}
|
||||||
|
|
||||||
let v: Vec<(&UniqueId, &BikeTrip)> = ts.search_sorted(
|
let v: Vec<&Record<BikeTrip>> = ts.search_sorted(
|
||||||
time_range(
|
time_range(
|
||||||
DateTimeTz(UTC.ymd(2011, 10, 31).and_hms(0, 0, 0)),
|
Timestamp::DateTime(
|
||||||
|
UTC.with_ymd_and_hms(2011, 10, 31, 0, 0, 0)
|
||||||
|
.unwrap()
|
||||||
|
.with_timezone(&FixedOffset::east_opt(0).unwrap()),
|
||||||
|
),
|
||||||
true,
|
true,
|
||||||
DateTimeTz(UTC.ymd(2011, 11, 04).and_hms(0, 0, 0)),
|
Timestamp::DateTime(
|
||||||
|
UTC.with_ymd_and_hms(2011, 11, 04, 0, 0, 0)
|
||||||
|
.unwrap()
|
||||||
|
.with_timezone(&FixedOffset::east_opt(0).unwrap()),
|
||||||
|
),
|
||||||
true,
|
true,
|
||||||
),
|
),
|
||||||
|l, r| l.1.timestamp().cmp(&r.1.timestamp()),
|
|l, r| l.timestamp().cmp(&r.timestamp()),
|
||||||
);
|
);
|
||||||
assert_eq!(v.len(), 3);
|
assert_eq!(v.len(), 3);
|
||||||
assert_eq!(*v[0].1, trips[1]);
|
assert_eq!(v[0].data, trips[1]);
|
||||||
assert_eq!(*v[1].1, trips[2]);
|
assert_eq!(v[1].data, trips[2]);
|
||||||
assert_eq!(*v[2].1, trips[3]);
|
assert_eq!(v[2].data, trips[3]);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -186,8 +215,8 @@ mod test {
|
||||||
let trips = mk_trips();
|
let trips = mk_trips();
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut ts: Series<BikeTrip> = Series::open(&path.to_string_lossy())
|
let mut ts: Series<BikeTrip> =
|
||||||
.expect("expect the time series to open correctly");
|
Series::open(&path).expect("expect the time series to open correctly");
|
||||||
|
|
||||||
for trip in &trips[0..=4] {
|
for trip in &trips[0..=4] {
|
||||||
ts.put(trip.clone()).expect("expect a successful put");
|
ts.put(trip.clone()).expect("expect a successful put");
|
||||||
|
@ -195,21 +224,29 @@ mod test {
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let ts: Series<BikeTrip> = Series::open(&path.to_string_lossy())
|
let ts: Series<BikeTrip> =
|
||||||
.expect("expect the time series to open correctly");
|
Series::open(&path).expect("expect the time series to open correctly");
|
||||||
let v: Vec<(&UniqueId, &BikeTrip)> = ts.search_sorted(
|
let v: Vec<&Record<BikeTrip>> = ts.search_sorted(
|
||||||
time_range(
|
time_range(
|
||||||
DateTimeTz(UTC.ymd(2011, 10, 31).and_hms(0, 0, 0)),
|
Timestamp::DateTime(
|
||||||
|
UTC.with_ymd_and_hms(2011, 10, 31, 0, 0, 0)
|
||||||
|
.unwrap()
|
||||||
|
.with_timezone(&FixedOffset::east_opt(0).unwrap()),
|
||||||
|
),
|
||||||
true,
|
true,
|
||||||
DateTimeTz(UTC.ymd(2011, 11, 04).and_hms(0, 0, 0)),
|
Timestamp::DateTime(
|
||||||
|
UTC.with_ymd_and_hms(2011, 11, 04, 0, 0, 0)
|
||||||
|
.unwrap()
|
||||||
|
.with_timezone(&FixedOffset::east_opt(0).unwrap()),
|
||||||
|
),
|
||||||
true,
|
true,
|
||||||
),
|
),
|
||||||
|l, r| l.1.timestamp().cmp(&r.1.timestamp()),
|
|l, r| l.timestamp().cmp(&r.timestamp()),
|
||||||
);
|
);
|
||||||
assert_eq!(v.len(), 3);
|
assert_eq!(v.len(), 3);
|
||||||
assert_eq!(*v[0].1, trips[1]);
|
assert_eq!(v[0].data, trips[1]);
|
||||||
assert_eq!(*v[1].1, trips[2]);
|
assert_eq!(v[1].data, trips[2]);
|
||||||
assert_eq!(*v[2].1, trips[3]);
|
assert_eq!(v[2].data, trips[3]);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -220,8 +257,8 @@ mod test {
|
||||||
let trips = mk_trips();
|
let trips = mk_trips();
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut ts: Series<BikeTrip> = Series::open(&path.to_string_lossy())
|
let mut ts: Series<BikeTrip> =
|
||||||
.expect("expect the time series to open correctly");
|
Series::open(&path).expect("expect the time series to open correctly");
|
||||||
|
|
||||||
for trip in &trips[0..=2] {
|
for trip in &trips[0..=2] {
|
||||||
ts.put(trip.clone()).expect("expect a successful put");
|
ts.put(trip.clone()).expect("expect a successful put");
|
||||||
|
@ -229,41 +266,57 @@ mod test {
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut ts: Series<BikeTrip> = Series::open(&path.to_string_lossy())
|
let mut ts: Series<BikeTrip> =
|
||||||
.expect("expect the time series to open correctly");
|
Series::open(&path).expect("expect the time series to open correctly");
|
||||||
let v: Vec<(&UniqueId, &BikeTrip)> = ts.search_sorted(
|
let v: Vec<&Record<BikeTrip>> = ts.search_sorted(
|
||||||
time_range(
|
time_range(
|
||||||
DateTimeTz(UTC.ymd(2011, 10, 31).and_hms(0, 0, 0)),
|
Timestamp::DateTime(
|
||||||
|
UTC.with_ymd_and_hms(2011, 10, 31, 0, 0, 0)
|
||||||
|
.unwrap()
|
||||||
|
.with_timezone(&FixedOffset::east_opt(0).unwrap()),
|
||||||
|
),
|
||||||
true,
|
true,
|
||||||
DateTimeTz(UTC.ymd(2011, 11, 04).and_hms(0, 0, 0)),
|
Timestamp::DateTime(
|
||||||
|
UTC.with_ymd_and_hms(2011, 11, 04, 0, 0, 0)
|
||||||
|
.unwrap()
|
||||||
|
.with_timezone(&FixedOffset::east_opt(0).unwrap()),
|
||||||
|
),
|
||||||
true,
|
true,
|
||||||
),
|
),
|
||||||
|l, r| l.1.timestamp().cmp(&r.1.timestamp()),
|
|l, r| l.timestamp().cmp(&r.timestamp()),
|
||||||
);
|
);
|
||||||
assert_eq!(v.len(), 2);
|
assert_eq!(v.len(), 2);
|
||||||
assert_eq!(*v[0].1, trips[1]);
|
assert_eq!(v[0].data, trips[1]);
|
||||||
assert_eq!(*v[1].1, trips[2]);
|
assert_eq!(v[1].data, trips[2]);
|
||||||
ts.put(trips[3].clone()).expect("expect a successful put");
|
ts.put(trips[3].clone()).expect("expect a successful put");
|
||||||
ts.put(trips[4].clone()).expect("expect a successful put");
|
ts.put(trips[4].clone()).expect("expect a successful put");
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let ts: Series<BikeTrip> = Series::open(&path.to_string_lossy())
|
let ts: Series<BikeTrip> =
|
||||||
.expect("expect the time series to open correctly");
|
Series::open(&path).expect("expect the time series to open correctly");
|
||||||
let v: Vec<(&UniqueId, &BikeTrip)> = ts.search_sorted(
|
let v: Vec<&Record<BikeTrip>> = ts.search_sorted(
|
||||||
time_range(
|
time_range(
|
||||||
DateTimeTz(UTC.ymd(2011, 10, 31).and_hms(0, 0, 0)),
|
Timestamp::DateTime(
|
||||||
|
UTC.with_ymd_and_hms(2011, 10, 31, 0, 0, 0)
|
||||||
|
.unwrap()
|
||||||
|
.with_timezone(&FixedOffset::east_opt(0).unwrap()),
|
||||||
|
),
|
||||||
true,
|
true,
|
||||||
DateTimeTz(UTC.ymd(2011, 11, 05).and_hms(0, 0, 0)),
|
Timestamp::DateTime(
|
||||||
|
UTC.with_ymd_and_hms(2011, 11, 05, 0, 0, 0)
|
||||||
|
.unwrap()
|
||||||
|
.with_timezone(&FixedOffset::east_opt(0).unwrap()),
|
||||||
|
),
|
||||||
true,
|
true,
|
||||||
),
|
),
|
||||||
|l, r| l.1.timestamp().cmp(&r.1.timestamp()),
|
|l, r| l.timestamp().cmp(&r.timestamp()),
|
||||||
);
|
);
|
||||||
assert_eq!(v.len(), 4);
|
assert_eq!(v.len(), 4);
|
||||||
assert_eq!(*v[0].1, trips[1]);
|
assert_eq!(v[0].data, trips[1]);
|
||||||
assert_eq!(*v[1].1, trips[2]);
|
assert_eq!(v[1].data, trips[2]);
|
||||||
assert_eq!(*v[2].1, trips[3]);
|
assert_eq!(v[2].data, trips[3]);
|
||||||
assert_eq!(*v[3].1, trips[4]);
|
assert_eq!(v[3].data, trips[4]);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -273,8 +326,8 @@ mod test {
|
||||||
run_test(|path| {
|
run_test(|path| {
|
||||||
let trips = mk_trips();
|
let trips = mk_trips();
|
||||||
|
|
||||||
let mut ts: Series<BikeTrip> = Series::open(&path.to_string_lossy())
|
let mut ts: Series<BikeTrip> =
|
||||||
.expect("expect the time series to open correctly");
|
Series::open(&path).expect("expect the time series to open correctly");
|
||||||
|
|
||||||
ts.put(trips[0].clone()).expect("expect a successful put");
|
ts.put(trips[0].clone()).expect("expect a successful put");
|
||||||
ts.put(trips[1].clone()).expect("expect a successful put");
|
ts.put(trips[1].clone()).expect("expect a successful put");
|
||||||
|
@ -283,9 +336,8 @@ mod test {
|
||||||
match ts.get(&trip_id) {
|
match ts.get(&trip_id) {
|
||||||
None => assert!(false, "record not found"),
|
None => assert!(false, "record not found"),
|
||||||
Some(mut trip) => {
|
Some(mut trip) => {
|
||||||
trip.distance = Distance(50000.0 * M);
|
trip.data.distance = Distance(50000.0 * M);
|
||||||
ts.update(trip_id.clone(), trip)
|
ts.update(trip).expect("expect record to update");
|
||||||
.expect("expect record to update");
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -293,12 +345,12 @@ mod test {
|
||||||
None => assert!(false, "record not found"),
|
None => assert!(false, "record not found"),
|
||||||
Some(trip) => {
|
Some(trip) => {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
trip.datetime,
|
trip.data.datetime,
|
||||||
DateTimeTz(UTC.ymd(2011, 11, 02).and_hms(0, 0, 0))
|
UTC.with_ymd_and_hms(2011, 11, 02, 0, 0, 0).unwrap()
|
||||||
);
|
);
|
||||||
assert_eq!(trip.distance, Distance(50000.0 * M));
|
assert_eq!(trip.data.distance, Distance(50000.0 * M));
|
||||||
assert_eq!(trip.duration, Duration(7020.0 * S));
|
assert_eq!(trip.data.duration, Duration(7020.0 * S));
|
||||||
assert_eq!(trip.comments, String::from("Do Some Distance!"));
|
assert_eq!(trip.data.comments, String::from("Do Some Distance!"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -310,8 +362,8 @@ mod test {
|
||||||
let trips = mk_trips();
|
let trips = mk_trips();
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut ts: Series<BikeTrip> = Series::open(&path.to_string_lossy())
|
let mut ts: Series<BikeTrip> =
|
||||||
.expect("expect the time series to open correctly");
|
Series::open(&path).expect("expect the time series to open correctly");
|
||||||
|
|
||||||
ts.put(trips[0].clone()).expect("expect a successful put");
|
ts.put(trips[0].clone()).expect("expect a successful put");
|
||||||
ts.put(trips[1].clone()).expect("expect a successful put");
|
ts.put(trips[1].clone()).expect("expect a successful put");
|
||||||
|
@ -320,32 +372,36 @@ mod test {
|
||||||
match ts.get(&trip_id) {
|
match ts.get(&trip_id) {
|
||||||
None => assert!(false, "record not found"),
|
None => assert!(false, "record not found"),
|
||||||
Some(mut trip) => {
|
Some(mut trip) => {
|
||||||
trip.distance = Distance(50000.0 * M);
|
trip.data.distance = Distance(50000.0 * M);
|
||||||
ts.update(trip_id, trip).expect("expect record to update");
|
ts.update(trip).expect("expect record to update");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let ts: Series<BikeTrip> = Series::open(&path.to_string_lossy())
|
let ts: Series<BikeTrip> =
|
||||||
.expect("expect the time series to open correctly");
|
Series::open(&path).expect("expect the time series to open correctly");
|
||||||
|
|
||||||
let trips: Vec<(&UniqueId, &BikeTrip)> = ts.records().collect();
|
let trips: Vec<&Record<BikeTrip>> = ts.records().collect();
|
||||||
assert_eq!(trips.len(), 3);
|
assert_eq!(trips.len(), 3);
|
||||||
|
|
||||||
let trips: Vec<(&UniqueId, &BikeTrip)> = ts
|
let trips: Vec<&Record<BikeTrip>> = ts
|
||||||
.search(exact_time(DateTimeTz(
|
.search(exact_time(Timestamp::DateTime(
|
||||||
UTC.ymd(2011, 11, 02).and_hms(0, 0, 0),
|
UTC.with_ymd_and_hms(2011, 11, 02, 0, 0, 0)
|
||||||
|
.unwrap()
|
||||||
|
.with_timezone(&FixedOffset::east_opt(0).unwrap()),
|
||||||
)))
|
)))
|
||||||
.collect();
|
.collect();
|
||||||
assert_eq!(trips.len(), 1);
|
assert_eq!(trips.len(), 1);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
trips[0].1.datetime,
|
trips[0].data.datetime,
|
||||||
DateTimeTz(UTC.ymd(2011, 11, 02).and_hms(0, 0, 0))
|
UTC.with_ymd_and_hms(2011, 11, 02, 0, 0, 0)
|
||||||
|
.unwrap()
|
||||||
|
.with_timezone(&FixedOffset::east_opt(0).unwrap())
|
||||||
);
|
);
|
||||||
assert_eq!(trips[0].1.distance, Distance(50000.0 * M));
|
assert_eq!(trips[0].data.distance, Distance(50000.0 * M));
|
||||||
assert_eq!(trips[0].1.duration, Duration(7020.0 * S));
|
assert_eq!(trips[0].data.duration, Duration(7020.0 * S));
|
||||||
assert_eq!(trips[0].1.comments, String::from("Do Some Distance!"));
|
assert_eq!(trips[0].data.comments, String::from("Do Some Distance!"));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -356,22 +412,21 @@ mod test {
|
||||||
let trips = mk_trips();
|
let trips = mk_trips();
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut ts: Series<BikeTrip> = Series::open(&path.to_string_lossy())
|
let mut ts: Series<BikeTrip> =
|
||||||
.expect("expect the time series to open correctly");
|
Series::open(&path).expect("expect the time series to open correctly");
|
||||||
let trip_id = ts.put(trips[0].clone()).expect("expect a successful put");
|
let trip_id = ts.put(trips[0].clone()).expect("expect a successful put");
|
||||||
ts.put(trips[1].clone()).expect("expect a successful put");
|
ts.put(trips[1].clone()).expect("expect a successful put");
|
||||||
ts.put(trips[2].clone()).expect("expect a successful put");
|
ts.put(trips[2].clone()).expect("expect a successful put");
|
||||||
|
|
||||||
ts.delete(&trip_id).expect("successful delete");
|
ts.delete(&trip_id).expect("successful delete");
|
||||||
|
|
||||||
let recs: Vec<(&UniqueId, &BikeTrip)> = ts.records().collect();
|
let recs: Vec<&Record<BikeTrip>> = ts.records().collect();
|
||||||
assert_eq!(recs.len(), 2);
|
assert_eq!(recs.len(), 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let ts: Series<BikeTrip> = Series::open(&path.to_string_lossy())
|
let ts: Series<BikeTrip> =
|
||||||
.expect("expect the time series to open correctly");
|
Series::open(&path).expect("expect the time series to open correctly");
|
||||||
let recs: Vec<(&UniqueId, &BikeTrip)> = ts.records().collect();
|
let recs: Vec<&Record<BikeTrip>> = ts.records().collect();
|
||||||
assert_eq!(recs.len(), 2);
|
assert_eq!(recs.len(), 2);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -382,13 +437,13 @@ mod test {
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
|
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
|
||||||
pub struct WeightRecord {
|
pub struct WeightRecord {
|
||||||
pub date: DateTimeTz,
|
pub date: chrono::NaiveDate,
|
||||||
pub weight: Weight,
|
pub weight: Weight,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Recordable for WeightRecord {
|
impl Recordable for WeightRecord {
|
||||||
fn timestamp(&self) -> DateTimeTz {
|
fn timestamp(&self) -> Timestamp {
|
||||||
self.date.clone()
|
Timestamp::Date(self.date)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tags(&self) -> Vec<String> {
|
fn tags(&self) -> Vec<String> {
|
||||||
|
@ -396,6 +451,7 @@ mod test {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
#[test]
|
#[test]
|
||||||
pub fn legacy_file_load() {
|
pub fn legacy_file_load() {
|
||||||
let ts: Series<WeightRecord> =
|
let ts: Series<WeightRecord> =
|
||||||
|
@ -410,4 +466,5 @@ mod test {
|
||||||
Some(rec) => assert_eq!(rec.weight, Weight(77.79109 * KG)),
|
Some(rec) => assert_eq!(rec.weight, Weight(77.79109 * KG)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
fixtures
|
|
@ -0,0 +1,2 @@
|
||||||
|
fixtures
|
||||||
|
var
|
|
@ -0,0 +1,47 @@
|
||||||
|
[package]
|
||||||
|
name = "file-service"
|
||||||
|
version = "0.2.0"
|
||||||
|
authors = ["savanni@luminescent-dreams.com"]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "file_service"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "file-service"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[target.auth-cli.dependencies]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
authdb = { path = "../authdb/" }
|
||||||
|
base64ct = { version = "1", features = [ "alloc" ] }
|
||||||
|
build_html = { version = "2" }
|
||||||
|
bytes = { version = "1" }
|
||||||
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
|
clap = { version = "4", features = [ "derive" ] }
|
||||||
|
cookie = { version = "0.17" }
|
||||||
|
futures-util = { version = "0.3" }
|
||||||
|
hex-string = "0.1.0"
|
||||||
|
http = { version = "0.2" }
|
||||||
|
image = "0.23.5"
|
||||||
|
logger = "*"
|
||||||
|
log = { version = "0.4" }
|
||||||
|
mime = "0.3.16"
|
||||||
|
mime_guess = "2.0.3"
|
||||||
|
pretty_env_logger = { version = "0.5" }
|
||||||
|
serde_json = "*"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
sha2 = { version = "0.10" }
|
||||||
|
thiserror = { version = "1" }
|
||||||
|
tokio = { version = "1", features = [ "full" ] }
|
||||||
|
uuid = { version = "0.4", features = [ "serde", "v4" ] }
|
||||||
|
warp = { version = "0.3" }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
cool_asserts = { version = "2" }
|
||||||
|
tempdir = { version = "0.3" }
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
[{"jti":"ac3a46c6-3fa1-4d0a-af12-e7d3fefdc878","aud":"savanni","exp":1621351436,"iss":"savanni","iat":1589729036,"sub":"https://savanni.luminescent-dreams.com/file-service/","perms":["admin"]}]
|
|
@ -0,0 +1,13 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
VERSION=`cat Cargo.toml | grep "^version =" | sed -r 's/^version = "(.+)"$/\1/'`
|
||||||
|
|
||||||
|
mkdir -p dist
|
||||||
|
cp ../target/release/file-service dist
|
||||||
|
cp ../target/release/auth-cli dist
|
||||||
|
strip dist/file-service
|
||||||
|
strip dist/auth-cli
|
||||||
|
tar -czf file-service-${VERSION}.tgz dist/
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 23 KiB |
|
@ -0,0 +1,279 @@
|
||||||
|
use build_html::Html;
|
||||||
|
use bytes::Buf;
|
||||||
|
use file_service::WriteFileError;
|
||||||
|
use futures_util::StreamExt;
|
||||||
|
use http::{Error, StatusCode};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::io::Read;
|
||||||
|
use warp::{filters::multipart::FormData, http::Response, multipart::Part};
|
||||||
|
|
||||||
|
use crate::{pages, App, AuthToken, FileId, FileInfo, ReadFileError, SessionToken};
|
||||||
|
|
||||||
|
const CSS: &str = include_str!("../templates/style.css");
|
||||||
|
|
||||||
|
pub async fn handle_index(
|
||||||
|
app: App,
|
||||||
|
token: Option<SessionToken>,
|
||||||
|
) -> Result<Response<String>, Error> {
|
||||||
|
match token {
|
||||||
|
Some(token) => match app.validate_session(token).await {
|
||||||
|
Ok(_) => render_gallery_page(app).await,
|
||||||
|
Err(err) => render_auth_page(Some(format!("session expired: {:?}", err))),
|
||||||
|
},
|
||||||
|
None => render_auth_page(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_css() -> Result<Response<String>, Error> {
|
||||||
|
Response::builder()
|
||||||
|
.header("content-type", "text/css")
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.body(CSS.to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_auth_page(message: Option<String>) -> Result<Response<String>, Error> {
|
||||||
|
Response::builder()
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.body(pages::auth(message).to_html_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn render_gallery_page(app: App) -> Result<Response<String>, Error> {
|
||||||
|
match app.list_files().await {
|
||||||
|
Ok(ids) => {
|
||||||
|
let mut files = vec![];
|
||||||
|
for id in ids.into_iter() {
|
||||||
|
let file = app.get_file(&id).await;
|
||||||
|
files.push(file);
|
||||||
|
}
|
||||||
|
Response::builder()
|
||||||
|
.header("content-type", "text/html")
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.body(pages::gallery(files).to_html_string())
|
||||||
|
}
|
||||||
|
Err(_) => Response::builder()
|
||||||
|
.header("content-type", "text/html")
|
||||||
|
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
.body("".to_owned()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn thumbnail(
|
||||||
|
app: App,
|
||||||
|
id: String,
|
||||||
|
old_etags: Option<String>,
|
||||||
|
) -> Result<Response<Vec<u8>>, Error> {
|
||||||
|
match app.get_file(&FileId::from(id)).await {
|
||||||
|
Ok(file) => serve_file(file.info.clone(), || file.thumbnail(), old_etags),
|
||||||
|
Err(_err) => Response::builder()
|
||||||
|
.status(StatusCode::NOT_FOUND)
|
||||||
|
.body(vec![]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn file(
|
||||||
|
app: App,
|
||||||
|
id: String,
|
||||||
|
old_etags: Option<String>,
|
||||||
|
) -> Result<Response<Vec<u8>>, Error> {
|
||||||
|
match app.get_file(&FileId::from(id)).await {
|
||||||
|
Ok(file) => serve_file(file.info.clone(), || file.content(), old_etags),
|
||||||
|
Err(_err) => Response::builder()
|
||||||
|
.status(StatusCode::NOT_FOUND)
|
||||||
|
.body(vec![]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_auth(
|
||||||
|
app: App,
|
||||||
|
form: HashMap<String, String>,
|
||||||
|
) -> Result<http::Response<String>, Error> {
|
||||||
|
match form.get("password") {
|
||||||
|
Some(token) => match app.authenticate(AuthToken::from(token.clone())).await {
|
||||||
|
Ok(Some(session_token)) => Response::builder()
|
||||||
|
.header("location", "/")
|
||||||
|
.header(
|
||||||
|
"set-cookie",
|
||||||
|
format!(
|
||||||
|
"session={}; Secure; HttpOnly; SameSite=Strict",
|
||||||
|
*session_token
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.status(StatusCode::SEE_OTHER)
|
||||||
|
.body("".to_owned()),
|
||||||
|
Ok(None) => render_auth_page(Some("no user found".to_owned())),
|
||||||
|
Err(_) => render_auth_page(Some("invalid auth token".to_owned())),
|
||||||
|
},
|
||||||
|
None => render_auth_page(Some("no token available".to_owned())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_upload(
|
||||||
|
app: App,
|
||||||
|
token: SessionToken,
|
||||||
|
form: FormData,
|
||||||
|
) -> Result<http::Response<String>, Error> {
|
||||||
|
match app.validate_session(token).await {
|
||||||
|
Ok(Some(_)) => match process_file_upload(app, form).await {
|
||||||
|
Ok(_) => Response::builder()
|
||||||
|
.header("location", "/")
|
||||||
|
.status(StatusCode::SEE_OTHER)
|
||||||
|
.body("".to_owned()),
|
||||||
|
Err(UploadError::FilenameMissing) => Response::builder()
|
||||||
|
.status(StatusCode::BAD_REQUEST)
|
||||||
|
.body("filename is required for all files".to_owned()),
|
||||||
|
Err(UploadError::WriteFileError(err)) => Response::builder()
|
||||||
|
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
.body(format!("could not write to the file system: {:?}", err)),
|
||||||
|
Err(UploadError::WarpError(err)) => Response::builder()
|
||||||
|
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
.body(format!("error with the app framework: {:?}", err)),
|
||||||
|
},
|
||||||
|
_ => Response::builder()
|
||||||
|
.status(StatusCode::UNAUTHORIZED)
|
||||||
|
.body("".to_owned()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_delete(
|
||||||
|
app: App,
|
||||||
|
token: SessionToken,
|
||||||
|
id: FileId,
|
||||||
|
) -> Result<http::Response<String>, Error> {
|
||||||
|
match app.validate_session(token).await {
|
||||||
|
Ok(Some(_)) => match app.delete_file(id).await {
|
||||||
|
Ok(_) => Response::builder()
|
||||||
|
.header("location", "/")
|
||||||
|
.status(StatusCode::SEE_OTHER)
|
||||||
|
.body("".to_owned()),
|
||||||
|
Err(_) => unimplemented!(),
|
||||||
|
},
|
||||||
|
_ => Response::builder()
|
||||||
|
.status(StatusCode::UNAUTHORIZED)
|
||||||
|
.body("".to_owned()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serve_file<F>(
|
||||||
|
info: FileInfo,
|
||||||
|
file: F,
|
||||||
|
old_etags: Option<String>,
|
||||||
|
) -> http::Result<http::Response<Vec<u8>>>
|
||||||
|
where
|
||||||
|
F: FnOnce() -> Result<Vec<u8>, ReadFileError>,
|
||||||
|
{
|
||||||
|
match old_etags {
|
||||||
|
Some(old_etags) if old_etags != info.hash => Response::builder()
|
||||||
|
.header("content-type", info.file_type)
|
||||||
|
.status(StatusCode::NOT_MODIFIED)
|
||||||
|
.body(vec![]),
|
||||||
|
_ => match file() {
|
||||||
|
Ok(content) => Response::builder()
|
||||||
|
.header("content-type", info.file_type)
|
||||||
|
.header("etag", info.hash)
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.body(content),
|
||||||
|
Err(_) => Response::builder()
|
||||||
|
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
.body(vec![]),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn collect_multipart(
|
||||||
|
mut stream: warp::filters::multipart::FormData,
|
||||||
|
) -> Result<Vec<(Option<String>, Option<String>, Vec<u8>)>, warp::Error> {
|
||||||
|
let mut content: Vec<(Option<String>, Option<String>, Vec<u8>)> = Vec::new();
|
||||||
|
|
||||||
|
while let Some(part) = stream.next().await {
|
||||||
|
match part {
|
||||||
|
Ok(part) => content.push(collect_content(part).await.unwrap()),
|
||||||
|
Err(err) => return Err(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn collect_content(
|
||||||
|
mut part: Part,
|
||||||
|
) -> Result<(Option<String>, Option<String>, Vec<u8>), String> {
|
||||||
|
let mut content: Vec<u8> = Vec::new();
|
||||||
|
|
||||||
|
while let Some(Ok(data)) = part.data().await {
|
||||||
|
let mut reader = data.reader();
|
||||||
|
reader.read_to_end(&mut content).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
part.content_type().map(|s| s.to_owned()),
|
||||||
|
part.filename().map(|s| s.to_owned()),
|
||||||
|
content,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
async fn handle_upload(
|
||||||
|
form: warp::filters::multipart::FormData,
|
||||||
|
app: App,
|
||||||
|
) -> warp::http::Result<warp::http::Response<String>> {
|
||||||
|
let files = collect_multipart(form).await;
|
||||||
|
match files {
|
||||||
|
Ok(files) => {
|
||||||
|
for (_, filename, content) in files {
|
||||||
|
match filename {
|
||||||
|
Some(filename) => {
|
||||||
|
app.add_file(filename, content).unwrap();
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
return warp::http::Response::builder()
|
||||||
|
.status(StatusCode::BAD_REQUEST)
|
||||||
|
.body("".to_owned())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_err) => {
|
||||||
|
return warp::http::Response::builder()
|
||||||
|
.status(StatusCode::BAD_REQUEST)
|
||||||
|
.body("".to_owned())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// println!("file length: {:?}", files.map(|f| f.len()));
|
||||||
|
warp::http::Response::builder()
|
||||||
|
.header("location", "/")
|
||||||
|
.status(StatusCode::SEE_OTHER)
|
||||||
|
.body("".to_owned())
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
enum UploadError {
|
||||||
|
FilenameMissing,
|
||||||
|
WriteFileError(WriteFileError),
|
||||||
|
WarpError(warp::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<WriteFileError> for UploadError {
|
||||||
|
fn from(err: WriteFileError) -> Self {
|
||||||
|
Self::WriteFileError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<warp::Error> for UploadError {
|
||||||
|
fn from(err: warp::Error) -> Self {
|
||||||
|
Self::WarpError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn process_file_upload(app: App, form: FormData) -> Result<(), UploadError> {
|
||||||
|
let files = collect_multipart(form).await?;
|
||||||
|
for (_, filename, content) in files {
|
||||||
|
match filename {
|
||||||
|
Some(filename) => {
|
||||||
|
app.add_file(filename, content).await?;
|
||||||
|
}
|
||||||
|
None => return Err(UploadError::FilenameMissing),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -0,0 +1,208 @@
|
||||||
|
use build_html::{self, Html, HtmlContainer};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub struct Attributes(Vec<(String, String)>);
|
||||||
|
|
||||||
|
/*
|
||||||
|
impl FromIterator<(String, String)> for Attributes {
|
||||||
|
fn from_iter<T>(iter: T) -> Self
|
||||||
|
where
|
||||||
|
T: IntoIterator<Item = (String, String)>,
|
||||||
|
{
|
||||||
|
Attributes(iter.collect::<Vec<(String, String)>>())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromIterator<(&str, &str)> for Attributes {
|
||||||
|
fn from_iter<T>(iter: T) -> Self
|
||||||
|
where
|
||||||
|
T: IntoIterator<Item = (&str, &str)>,
|
||||||
|
{
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
impl ToString for Attributes {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
self.0
|
||||||
|
.iter()
|
||||||
|
.map(|(key, value)| format!("{}=\"{}\"", key, value))
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(" ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Form {
|
||||||
|
path: String,
|
||||||
|
method: String,
|
||||||
|
encoding: Option<String>,
|
||||||
|
elements: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Form {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
path: "/".to_owned(),
|
||||||
|
method: "get".to_owned(),
|
||||||
|
encoding: None,
|
||||||
|
elements: "".to_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_path(mut self, path: &str) -> Self {
|
||||||
|
self.path = path.to_owned();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_method(mut self, method: &str) -> Self {
|
||||||
|
self.method = method.to_owned();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_encoding(mut self, encoding: &str) -> Self {
|
||||||
|
self.encoding = Some(encoding.to_owned());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Html for Form {
|
||||||
|
fn to_html_string(&self) -> String {
|
||||||
|
let encoding = match self.encoding {
|
||||||
|
Some(ref encoding) => format!("enctype=\"{encoding}\"", encoding = encoding),
|
||||||
|
None => "".to_owned(),
|
||||||
|
};
|
||||||
|
format!(
|
||||||
|
"<form action=\"{path}\" method=\"{method}\" {encoding}>\n{elements}\n</form>\n",
|
||||||
|
path = self.path,
|
||||||
|
method = self.method,
|
||||||
|
encoding = encoding,
|
||||||
|
elements = self.elements.to_html_string(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HtmlContainer for Form {
|
||||||
|
fn add_html<H: Html>(&mut self, html: H) {
|
||||||
|
self.elements.push_str(&html.to_html_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Input {
|
||||||
|
ty: String,
|
||||||
|
name: String,
|
||||||
|
id: Option<String>,
|
||||||
|
value: Option<String>,
|
||||||
|
attributes: Attributes,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Html for Input {
|
||||||
|
fn to_html_string(&self) -> String {
|
||||||
|
let id = match self.id {
|
||||||
|
Some(ref id) => format!("id=\"{}\"", id),
|
||||||
|
None => "".to_owned(),
|
||||||
|
};
|
||||||
|
let value = match self.value {
|
||||||
|
Some(ref value) => format!("value=\"{}\"", value),
|
||||||
|
None => "".to_owned(),
|
||||||
|
};
|
||||||
|
let attrs = self.attributes.to_string();
|
||||||
|
|
||||||
|
format!(
|
||||||
|
"<input type=\"{ty}\" name=\"{name}\" {id} {value} {attrs} />\n",
|
||||||
|
ty = self.ty,
|
||||||
|
name = self.name,
|
||||||
|
id = id,
|
||||||
|
value = value,
|
||||||
|
attrs = attrs,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Input {
|
||||||
|
pub fn new(ty: &str, name: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
ty: ty.to_owned(),
|
||||||
|
name: name.to_owned(),
|
||||||
|
id: None,
|
||||||
|
value: None,
|
||||||
|
attributes: Attributes::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_id(mut self, val: &str) -> Self {
|
||||||
|
self.id = Some(val.to_owned());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_attributes<'a>(
|
||||||
|
mut self,
|
||||||
|
values: impl IntoIterator<Item = (&'a str, &'a str)>,
|
||||||
|
) -> Self {
|
||||||
|
self.attributes = Attributes(
|
||||||
|
values
|
||||||
|
.into_iter()
|
||||||
|
.map(|(a, b)| (a.to_owned(), b.to_owned()))
|
||||||
|
.collect::<Vec<(String, String)>>(),
|
||||||
|
);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Button {
|
||||||
|
ty: Option<String>,
|
||||||
|
name: Option<String>,
|
||||||
|
label: String,
|
||||||
|
attributes: Attributes,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Button {
|
||||||
|
pub fn new(label: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
ty: None,
|
||||||
|
name: None,
|
||||||
|
label: label.to_owned(),
|
||||||
|
attributes: Attributes::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_type(mut self, ty: &str) -> Self {
|
||||||
|
self.ty = Some(ty.to_owned());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_attributes<'a>(
|
||||||
|
mut self,
|
||||||
|
values: impl IntoIterator<Item = (&'a str, &'a str)>,
|
||||||
|
) -> Self {
|
||||||
|
self.attributes = Attributes(
|
||||||
|
values
|
||||||
|
.into_iter()
|
||||||
|
.map(|(a, b)| (a.to_owned(), b.to_owned()))
|
||||||
|
.collect::<Vec<(String, String)>>(),
|
||||||
|
);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Html for Button {
|
||||||
|
fn to_html_string(&self) -> String {
|
||||||
|
let ty = match self.ty {
|
||||||
|
Some(ref ty) => format!("type={}", ty),
|
||||||
|
None => "".to_owned(),
|
||||||
|
};
|
||||||
|
let name = match self.name {
|
||||||
|
Some(ref name) => format!("name={}", name),
|
||||||
|
None => "".to_owned(),
|
||||||
|
};
|
||||||
|
format!(
|
||||||
|
"<button {ty} {name} {attrs}>{label}</button>",
|
||||||
|
name = name,
|
||||||
|
label = self.label,
|
||||||
|
attrs = self.attributes.to_string()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
mod store;
|
||||||
|
|
||||||
|
pub use store::{
|
||||||
|
DeleteFileError, FileHandle, FileId, FileInfo, ReadFileError, Store, WriteFileError,
|
||||||
|
};
|
|
@ -0,0 +1,174 @@
|
||||||
|
extern crate log;
|
||||||
|
|
||||||
|
use cookie::Cookie;
|
||||||
|
use handlers::{file, handle_auth, handle_css, handle_delete, handle_upload, thumbnail};
|
||||||
|
use std::{
|
||||||
|
collections::{HashMap, HashSet},
|
||||||
|
convert::Infallible,
|
||||||
|
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||||
|
path::PathBuf,
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
use warp::{Filter, Rejection};
|
||||||
|
|
||||||
|
mod handlers;
|
||||||
|
mod html;
|
||||||
|
mod pages;
|
||||||
|
|
||||||
|
const MAX_UPLOAD: u64 = 15 * 1024 * 1024;
|
||||||
|
|
||||||
|
use authdb::{AuthDB, AuthError, AuthToken, SessionToken, Username};
|
||||||
|
|
||||||
|
use file_service::{
|
||||||
|
DeleteFileError, FileHandle, FileId, FileInfo, ReadFileError, Store, WriteFileError,
|
||||||
|
};
|
||||||
|
pub use handlers::handle_index;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct App {
|
||||||
|
authdb: Arc<RwLock<AuthDB>>,
|
||||||
|
store: Arc<RwLock<Store>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
pub fn new(authdb: AuthDB, store: Store) -> Self {
|
||||||
|
Self {
|
||||||
|
authdb: Arc::new(RwLock::new(authdb)),
|
||||||
|
store: Arc::new(RwLock::new(store)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn authenticate(&self, token: AuthToken) -> Result<Option<SessionToken>, AuthError> {
|
||||||
|
self.authdb.read().await.authenticate(token).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn validate_session(
|
||||||
|
&self,
|
||||||
|
token: SessionToken,
|
||||||
|
) -> Result<Option<Username>, AuthError> {
|
||||||
|
self.authdb.read().await.validate_session(token).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_files(&self) -> Result<HashSet<FileId>, ReadFileError> {
|
||||||
|
self.store.read().await.list_files()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_file(&self, id: &FileId) -> Result<FileHandle, ReadFileError> {
|
||||||
|
self.store.read().await.get_file(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn add_file(
|
||||||
|
&self,
|
||||||
|
filename: String,
|
||||||
|
content: Vec<u8>,
|
||||||
|
) -> Result<FileHandle, WriteFileError> {
|
||||||
|
self.store.write().await.add_file(filename, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_file(&self, id: FileId) -> Result<(), DeleteFileError> {
|
||||||
|
self.store.write().await.delete_file(&id)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_app(app: App) -> impl Filter<Extract = (App,), Error = Infallible> + Clone {
|
||||||
|
warp::any().map(move || app.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_cookies(cookie_str: &str) -> Result<HashMap<String, String>, cookie::ParseError> {
|
||||||
|
Cookie::split_parse(cookie_str)
|
||||||
|
.map(|c| c.map(|c| (c.name().to_owned(), c.value().to_owned())))
|
||||||
|
.collect::<Result<HashMap<String, String>, cookie::ParseError>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_session_token(cookies: HashMap<String, String>) -> Option<SessionToken> {
|
||||||
|
cookies.get("session").cloned().map(SessionToken::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn maybe_with_session() -> impl Filter<Extract = (Option<SessionToken>,), Error = Rejection> + Copy
|
||||||
|
{
|
||||||
|
warp::any()
|
||||||
|
.and(warp::header::optional::<String>("cookie"))
|
||||||
|
.map(|cookie_str: Option<String>| match cookie_str {
|
||||||
|
Some(cookie_str) => parse_cookies(&cookie_str).ok().and_then(get_session_token),
|
||||||
|
None => None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_session() -> impl Filter<Extract = (SessionToken,), Error = Rejection> + Copy {
|
||||||
|
warp::any()
|
||||||
|
.and(warp::header::<String>("cookie"))
|
||||||
|
.and_then(|cookie_str: String| async move {
|
||||||
|
match parse_cookies(&cookie_str).ok().and_then(get_session_token) {
|
||||||
|
Some(session_token) => Ok(session_token),
|
||||||
|
None => Err(warp::reject()),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
pub async fn main() {
|
||||||
|
pretty_env_logger::init();
|
||||||
|
|
||||||
|
let authdb = AuthDB::new(PathBuf::from(&std::env::var("AUTHDB").unwrap()))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let store = Store::new(PathBuf::from(&std::env::var("FILE_SHARE_DIR").unwrap()));
|
||||||
|
|
||||||
|
let app = App::new(authdb, store);
|
||||||
|
|
||||||
|
let log = warp::log("file_service");
|
||||||
|
let root = warp::path!()
|
||||||
|
.and(warp::get())
|
||||||
|
.and(with_app(app.clone()))
|
||||||
|
.and(maybe_with_session())
|
||||||
|
.then(handle_index);
|
||||||
|
|
||||||
|
let styles = warp::path!("css").and(warp::get()).then(handle_css);
|
||||||
|
|
||||||
|
let auth = warp::path!("auth")
|
||||||
|
.and(warp::post())
|
||||||
|
.and(with_app(app.clone()))
|
||||||
|
.and(warp::filters::body::form())
|
||||||
|
.then(handle_auth);
|
||||||
|
|
||||||
|
let upload_via_form = warp::path!("upload")
|
||||||
|
.and(warp::post())
|
||||||
|
.and(with_app(app.clone()))
|
||||||
|
.and(with_session())
|
||||||
|
.and(warp::multipart::form().max_length(MAX_UPLOAD))
|
||||||
|
.then(handle_upload);
|
||||||
|
|
||||||
|
let delete_via_form = warp::path!("delete" / String)
|
||||||
|
.and(warp::post())
|
||||||
|
.and(with_app(app.clone()))
|
||||||
|
.and(with_session())
|
||||||
|
.then(|id, app, token| handle_delete(app, token, FileId::from(id)));
|
||||||
|
|
||||||
|
let thumbnail = warp::path!(String / "tn")
|
||||||
|
.and(warp::get())
|
||||||
|
.and(warp::header::optional::<String>("if-none-match"))
|
||||||
|
.and(with_app(app.clone()))
|
||||||
|
.then(move |id, old_etags, app: App| thumbnail(app, id, old_etags));
|
||||||
|
|
||||||
|
let file = warp::path!(String)
|
||||||
|
.and(warp::get())
|
||||||
|
.and(warp::header::optional::<String>("if-none-match"))
|
||||||
|
.and(with_app(app.clone()))
|
||||||
|
.then(move |id, old_etags, app: App| file(app, id, old_etags));
|
||||||
|
|
||||||
|
let server = warp::serve(
|
||||||
|
root.or(styles)
|
||||||
|
.or(auth)
|
||||||
|
.or(upload_via_form)
|
||||||
|
.or(delete_via_form)
|
||||||
|
.or(thumbnail)
|
||||||
|
.or(file)
|
||||||
|
.with(log),
|
||||||
|
);
|
||||||
|
|
||||||
|
server
|
||||||
|
.run(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 8002))
|
||||||
|
.await;
|
||||||
|
}
|
|
@ -0,0 +1,114 @@
|
||||||
|
use crate::html::*;
|
||||||
|
use build_html::{self, Container, ContainerType, Html, HtmlContainer};
|
||||||
|
use file_service::{FileHandle, FileInfo, ReadFileError};
|
||||||
|
|
||||||
|
pub fn auth(_message: Option<String>) -> build_html::HtmlPage {
|
||||||
|
build_html::HtmlPage::new()
|
||||||
|
.with_title("Sign In")
|
||||||
|
.with_stylesheet("/css")
|
||||||
|
.with_container(
|
||||||
|
Container::new(ContainerType::Div)
|
||||||
|
.with_attributes([("class", "authentication-page")])
|
||||||
|
.with_container(auth_form()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn auth_form() -> Container {
|
||||||
|
Container::default()
|
||||||
|
.with_attributes([("class", "card authentication-form")])
|
||||||
|
.with_html(
|
||||||
|
Form::new()
|
||||||
|
.with_path("/auth")
|
||||||
|
.with_method("post")
|
||||||
|
.with_container(
|
||||||
|
Container::new(ContainerType::Div)
|
||||||
|
.with_html(
|
||||||
|
Input::new("password", "password")
|
||||||
|
.with_id("for-token-input")
|
||||||
|
.with_attributes([
|
||||||
|
("size", "50"),
|
||||||
|
("class", "authentication-form__input"),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
.with_html(
|
||||||
|
Button::new("Sign In")
|
||||||
|
.with_attributes([("class", "authentication-form__button")]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn gallery(handles: Vec<Result<FileHandle, ReadFileError>>) -> build_html::HtmlPage {
|
||||||
|
let mut page = build_html::HtmlPage::new()
|
||||||
|
.with_title("Gallery")
|
||||||
|
.with_stylesheet("/css")
|
||||||
|
.with_container(
|
||||||
|
Container::new(ContainerType::Div)
|
||||||
|
.with_attributes([("class", "gallery-page")])
|
||||||
|
.with_header(1, "Gallery")
|
||||||
|
.with_html(upload_form()),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut gallery = Container::new(ContainerType::Div).with_attributes([("class", "gallery")]);
|
||||||
|
for handle in handles {
|
||||||
|
let container = match handle {
|
||||||
|
Ok(ref handle) => thumbnail(&handle.info),
|
||||||
|
Err(err) => Container::new(ContainerType::Div)
|
||||||
|
.with_attributes(vec![("class", "file")])
|
||||||
|
.with_paragraph(format!("{:?}", err)),
|
||||||
|
};
|
||||||
|
gallery.add_container(container);
|
||||||
|
}
|
||||||
|
page.add_container(gallery);
|
||||||
|
page
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn upload_form() -> Form {
|
||||||
|
Form::new()
|
||||||
|
.with_path("/upload")
|
||||||
|
.with_method("post")
|
||||||
|
.with_encoding("multipart/form-data")
|
||||||
|
.with_container(
|
||||||
|
Container::new(ContainerType::Div)
|
||||||
|
.with_attributes([("class", "card upload-form")])
|
||||||
|
.with_html(Input::new("file", "file").with_attributes([
|
||||||
|
("id", "for-selector-input"),
|
||||||
|
("placeholder", "select file"),
|
||||||
|
("class", "upload-form__selector"),
|
||||||
|
]))
|
||||||
|
.with_html(
|
||||||
|
Button::new("Upload file")
|
||||||
|
.with_attributes([("class", "upload-form__button")])
|
||||||
|
.with_type("submit"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn thumbnail(info: &FileInfo) -> Container {
|
||||||
|
Container::new(ContainerType::Div)
|
||||||
|
.with_attributes(vec![("class", "card thumbnail")])
|
||||||
|
.with_html(
|
||||||
|
Container::new(ContainerType::Div).with_link(
|
||||||
|
format!("/{}", *info.id),
|
||||||
|
Container::default()
|
||||||
|
.with_attributes([("class", "thumbnail")])
|
||||||
|
.with_image(format!("{}/tn", *info.id), "test data")
|
||||||
|
.to_html_string(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.with_html(
|
||||||
|
Container::new(ContainerType::Div)
|
||||||
|
.with_html(
|
||||||
|
Container::new(ContainerType::UnorderedList)
|
||||||
|
.with_attributes(vec![("class", "thumbnail__metadata")])
|
||||||
|
.with_html(info.name.clone())
|
||||||
|
.with_html(format!("{}", info.created.format("%Y-%m-%d"))),
|
||||||
|
)
|
||||||
|
.with_html(
|
||||||
|
Form::new()
|
||||||
|
.with_path(&format!("/delete/{}", *info.id))
|
||||||
|
.with_method("post")
|
||||||
|
.with_html(Button::new("Delete")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,299 @@
|
||||||
|
use super::{fileinfo::FileInfo, FileId, ReadFileError, WriteFileError};
|
||||||
|
use chrono::prelude::*;
|
||||||
|
use hex_string::HexString;
|
||||||
|
use image::imageops::FilterType;
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use std::{
|
||||||
|
convert::TryFrom,
|
||||||
|
io::{Read, Write},
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
use thiserror::Error;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum PathError {
|
||||||
|
#[error("path cannot be derived from input")]
|
||||||
|
InvalidPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct PathResolver {
|
||||||
|
base: PathBuf,
|
||||||
|
id: FileId,
|
||||||
|
extension: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PathResolver {
|
||||||
|
pub fn new(base: &Path, id: FileId, extension: String) -> Self {
|
||||||
|
Self {
|
||||||
|
base: base.to_owned(),
|
||||||
|
id,
|
||||||
|
extension,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn metadata_path_by_id(base: &Path, id: FileId) -> PathBuf {
|
||||||
|
let mut path = base.to_path_buf();
|
||||||
|
path.push(PathBuf::from(id.clone()));
|
||||||
|
path.set_extension("json");
|
||||||
|
path
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn id(&self) -> FileId {
|
||||||
|
self.id.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn file_path(&self) -> PathBuf {
|
||||||
|
let mut path = self.base.clone();
|
||||||
|
path.push(PathBuf::from(self.id.clone()));
|
||||||
|
path.set_extension(self.extension.clone());
|
||||||
|
path
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn metadata_path(&self) -> PathBuf {
|
||||||
|
let mut path = self.base.clone();
|
||||||
|
path.push(PathBuf::from(self.id.clone()));
|
||||||
|
path.set_extension("json");
|
||||||
|
path
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn thumbnail_path(&self) -> PathBuf {
|
||||||
|
let mut path = self.base.clone();
|
||||||
|
path.push(PathBuf::from(self.id.clone()));
|
||||||
|
path.set_extension(format!("tn.{}", self.extension));
|
||||||
|
path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<String> for PathResolver {
|
||||||
|
type Error = PathError;
|
||||||
|
fn try_from(s: String) -> Result<Self, Self::Error> {
|
||||||
|
PathResolver::try_from(s.as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&str> for PathResolver {
|
||||||
|
type Error = PathError;
|
||||||
|
fn try_from(s: &str) -> Result<Self, Self::Error> {
|
||||||
|
PathResolver::try_from(Path::new(s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<PathBuf> for PathResolver {
|
||||||
|
type Error = PathError;
|
||||||
|
fn try_from(path: PathBuf) -> Result<Self, Self::Error> {
|
||||||
|
PathResolver::try_from(path.as_path())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&Path> for PathResolver {
|
||||||
|
type Error = PathError;
|
||||||
|
fn try_from(path: &Path) -> Result<Self, Self::Error> {
|
||||||
|
Ok(Self {
|
||||||
|
base: path
|
||||||
|
.parent()
|
||||||
|
.map(|s| s.to_owned())
|
||||||
|
.ok_or(PathError::InvalidPath)?,
|
||||||
|
id: path
|
||||||
|
.file_stem()
|
||||||
|
.and_then(|s| s.to_str().map(FileId::from))
|
||||||
|
.ok_or(PathError::InvalidPath)?,
|
||||||
|
extension: path
|
||||||
|
.extension()
|
||||||
|
.and_then(|s| s.to_str().map(|s| s.to_owned()))
|
||||||
|
.ok_or(PathError::InvalidPath)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One file in the database, complete with the path of the file and information about the
|
||||||
|
/// thumbnail of the file.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct FileHandle {
|
||||||
|
pub id: FileId,
|
||||||
|
pub path: PathResolver,
|
||||||
|
pub info: FileInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileHandle {
|
||||||
|
/// Create a new entry in the database
|
||||||
|
pub fn new(filename: String, root: PathBuf) -> Result<Self, WriteFileError> {
|
||||||
|
let id = FileId::from(Uuid::new_v4().hyphenated().to_string());
|
||||||
|
let path = PathBuf::from(filename);
|
||||||
|
|
||||||
|
let name = path
|
||||||
|
.file_stem()
|
||||||
|
.and_then(|s| s.to_str().map(|s| s.to_owned()))
|
||||||
|
.ok_or(WriteFileError::InvalidPath)?;
|
||||||
|
let extension = path
|
||||||
|
.extension()
|
||||||
|
.and_then(|s| s.to_str().map(|s| s.to_owned()))
|
||||||
|
.ok_or(WriteFileError::InvalidPath)?;
|
||||||
|
let path = PathResolver {
|
||||||
|
base: root.clone(),
|
||||||
|
id: id.clone(),
|
||||||
|
extension: extension.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let file_type = mime_guess::from_ext(&extension)
|
||||||
|
.first_or_text_plain()
|
||||||
|
.essence_str()
|
||||||
|
.to_owned();
|
||||||
|
|
||||||
|
let info = FileInfo {
|
||||||
|
id: id.clone(),
|
||||||
|
name,
|
||||||
|
size: 0,
|
||||||
|
created: Utc::now(),
|
||||||
|
file_type,
|
||||||
|
hash: "".to_owned(),
|
||||||
|
extension,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut md_file = std::fs::File::create(path.metadata_path())?;
|
||||||
|
let _ = md_file.write(&serde_json::to_vec(&info)?)?;
|
||||||
|
|
||||||
|
Ok(Self { id, path, info })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load(id: &FileId, root: &Path) -> Result<Self, ReadFileError> {
|
||||||
|
let info = FileInfo::load(PathResolver::metadata_path_by_id(root, id.clone()))?;
|
||||||
|
let resolver = PathResolver::new(root, id.clone(), info.extension.clone());
|
||||||
|
Ok(Self {
|
||||||
|
id: info.id.clone(),
|
||||||
|
path: resolver,
|
||||||
|
info,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_content(&mut self, content: Vec<u8>) -> Result<(), WriteFileError> {
|
||||||
|
let mut content_file = std::fs::File::create(self.path.file_path())?;
|
||||||
|
let byte_count = content_file.write(&content)?;
|
||||||
|
self.info.size = byte_count;
|
||||||
|
self.info.hash = self.hash_content(&content).as_string();
|
||||||
|
|
||||||
|
let mut md_file = std::fs::File::create(self.path.metadata_path())?;
|
||||||
|
let _ = md_file.write(&serde_json::to_vec(&self.info)?)?;
|
||||||
|
|
||||||
|
self.write_thumbnail()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn content(&self) -> Result<Vec<u8>, ReadFileError> {
|
||||||
|
load_content(&self.path.file_path())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn thumbnail(&self) -> Result<Vec<u8>, ReadFileError> {
|
||||||
|
load_content(&self.path.thumbnail_path())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hash_content(&self, data: &Vec<u8>) -> HexString {
|
||||||
|
HexString::from_bytes(&Sha256::digest(data).to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_thumbnail(&self) -> Result<(), WriteFileError> {
|
||||||
|
let img = image::open(self.path.file_path())?;
|
||||||
|
let tn = img.resize(640, 640, FilterType::Nearest);
|
||||||
|
tn.save(self.path.thumbnail_path())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete(self) {
|
||||||
|
let _ = std::fs::remove_file(self.path.thumbnail_path());
|
||||||
|
let _ = std::fs::remove_file(self.path.file_path());
|
||||||
|
let _ = std::fs::remove_file(self.path.metadata_path());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_content(path: &Path) -> Result<Vec<u8>, ReadFileError> {
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
let mut file = std::fs::File::open(path)?;
|
||||||
|
file.read_to_end(&mut buf)?;
|
||||||
|
Ok(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use std::{convert::TryFrom, path::PathBuf};
|
||||||
|
use tempdir::TempDir;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn paths() {
|
||||||
|
let resolver = PathResolver::try_from("path/82420255-d3c8-4d90-a582-f94be588c70c.png")
|
||||||
|
.expect("to have a valid path");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
resolver.file_path(),
|
||||||
|
PathBuf::from("path/82420255-d3c8-4d90-a582-f94be588c70c.png")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resolver.metadata_path(),
|
||||||
|
PathBuf::from("path/82420255-d3c8-4d90-a582-f94be588c70c.json")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resolver.thumbnail_path(),
|
||||||
|
PathBuf::from("path/82420255-d3c8-4d90-a582-f94be588c70c.tn.png")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_creates_file_info() {
|
||||||
|
let tmp = TempDir::new("var").unwrap();
|
||||||
|
let handle =
|
||||||
|
FileHandle::new("rawr.png".to_owned(), PathBuf::from(tmp.path())).expect("to succeed");
|
||||||
|
assert_eq!(handle.info.name, "rawr");
|
||||||
|
assert_eq!(handle.info.size, 0);
|
||||||
|
assert_eq!(handle.info.file_type, "image/png");
|
||||||
|
assert_eq!(handle.info.extension, "png");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_opens_a_file() {
|
||||||
|
let tmp = TempDir::new("var").unwrap();
|
||||||
|
FileHandle::new("rawr.png".to_owned(), PathBuf::from(tmp.path())).expect("to succeed");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_deletes_a_file() {
|
||||||
|
let tmp = TempDir::new("var").unwrap();
|
||||||
|
let f =
|
||||||
|
FileHandle::new("rawr.png".to_owned(), PathBuf::from(tmp.path())).expect("to succeed");
|
||||||
|
f.delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_can_return_a_thumbnail() {
|
||||||
|
let tmp = TempDir::new("var").unwrap();
|
||||||
|
let _ =
|
||||||
|
FileHandle::new("rawr.png".to_owned(), PathBuf::from(tmp.path())).expect("to succeed");
|
||||||
|
/*
|
||||||
|
assert_eq!(
|
||||||
|
f.thumbnail(),
|
||||||
|
Thumbnail {
|
||||||
|
id: String::from("rawr.png"),
|
||||||
|
root: PathBuf::from("var/"),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_can_return_a_file_stream() {
|
||||||
|
let tmp = TempDir::new("var").unwrap();
|
||||||
|
let _ =
|
||||||
|
FileHandle::new("rawr.png".to_owned(), PathBuf::from(tmp.path())).expect("to succeed");
|
||||||
|
// f.stream().expect("to succeed");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_raises_an_error_when_file_not_found() {
|
||||||
|
let tmp = TempDir::new("var").unwrap();
|
||||||
|
match FileHandle::load(&FileId::from("rawr"), tmp.path()) {
|
||||||
|
Err(ReadFileError::FileNotFound(_)) => assert!(true),
|
||||||
|
_ => assert!(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
use crate::FileId;
|
||||||
|
|
||||||
|
use super::{ReadFileError, WriteFileError};
|
||||||
|
use chrono::prelude::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::{
|
||||||
|
io::{Read, Write},
|
||||||
|
path::PathBuf,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct FileInfo {
|
||||||
|
pub id: FileId,
|
||||||
|
|
||||||
|
// Early versions of the application didn't support a name field, so it is possible that
|
||||||
|
// metadata won't contain the name. We can just default to an empty string when loading the
|
||||||
|
// metadata, as all future versions will require a filename when the file gets uploaded.
|
||||||
|
#[serde(default)]
|
||||||
|
pub name: String,
|
||||||
|
pub size: usize,
|
||||||
|
pub created: DateTime<Utc>,
|
||||||
|
pub file_type: String,
|
||||||
|
pub hash: String,
|
||||||
|
pub extension: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileInfo {
|
||||||
|
pub fn load(path: PathBuf) -> Result<Self, ReadFileError> {
|
||||||
|
let mut content: Vec<u8> = Vec::new();
|
||||||
|
let mut file =
|
||||||
|
std::fs::File::open(path.clone()).map_err(|_| ReadFileError::FileNotFound(path))?;
|
||||||
|
file.read_to_end(&mut content)?;
|
||||||
|
let js = serde_json::from_slice(&content)?;
|
||||||
|
|
||||||
|
Ok(js)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save(&self, path: PathBuf) -> Result<(), WriteFileError> {
|
||||||
|
let ser = serde_json::to_string(self).unwrap();
|
||||||
|
let mut file = std::fs::File::create(path)?;
|
||||||
|
let _ = file.write(ser.as_bytes())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use crate::store::FileId;
|
||||||
|
use tempdir::TempDir;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_saves_and_loads_metadata() {
|
||||||
|
let tmp = TempDir::new("var").unwrap();
|
||||||
|
let created = Utc::now();
|
||||||
|
|
||||||
|
let info = FileInfo {
|
||||||
|
id: FileId("temp-id".to_owned()),
|
||||||
|
name: "test-image".to_owned(),
|
||||||
|
size: 23777,
|
||||||
|
created,
|
||||||
|
file_type: "image/png".to_owned(),
|
||||||
|
hash: "abcdefg".to_owned(),
|
||||||
|
extension: "png".to_owned(),
|
||||||
|
};
|
||||||
|
let mut path = tmp.path().to_owned();
|
||||||
|
path.push(&PathBuf::from(info.id.clone()));
|
||||||
|
info.save(path.clone()).unwrap();
|
||||||
|
|
||||||
|
let info_ = FileInfo::load(path).unwrap();
|
||||||
|
assert_eq!(info_.size, 23777);
|
||||||
|
assert_eq!(info_.created, info.created);
|
||||||
|
assert_eq!(info_.file_type, "image/png");
|
||||||
|
assert_eq!(info_.hash, info.hash);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,269 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::{collections::HashSet, ops::Deref, path::PathBuf};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
mod filehandle;
|
||||||
|
mod fileinfo;
|
||||||
|
|
||||||
|
pub use filehandle::FileHandle;
|
||||||
|
pub use fileinfo::FileInfo;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum WriteFileError {
|
||||||
|
#[error("root file path does not exist")]
|
||||||
|
RootNotFound,
|
||||||
|
|
||||||
|
#[error("permission denied")]
|
||||||
|
PermissionDenied,
|
||||||
|
|
||||||
|
#[error("invalid path")]
|
||||||
|
InvalidPath,
|
||||||
|
|
||||||
|
#[error("no metadata available")]
|
||||||
|
NoMetadata,
|
||||||
|
|
||||||
|
#[error("file could not be loaded")]
|
||||||
|
LoadError(#[from] ReadFileError),
|
||||||
|
|
||||||
|
#[error("image conversion failed")]
|
||||||
|
ImageError(#[from] image::ImageError),
|
||||||
|
|
||||||
|
#[error("JSON error")]
|
||||||
|
JSONError(#[from] serde_json::error::Error),
|
||||||
|
|
||||||
|
#[error("IO error")]
|
||||||
|
IOError(#[from] std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum ReadFileError {
|
||||||
|
#[error("file not found")]
|
||||||
|
FileNotFound(PathBuf),
|
||||||
|
|
||||||
|
#[error("path is not a file")]
|
||||||
|
NotAFile,
|
||||||
|
|
||||||
|
#[error("permission denied")]
|
||||||
|
PermissionDenied,
|
||||||
|
|
||||||
|
#[error("JSON error")]
|
||||||
|
JSONError(#[from] serde_json::error::Error),
|
||||||
|
|
||||||
|
#[error("IO error")]
|
||||||
|
IOError(#[from] std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum DeleteFileError {
|
||||||
|
#[error("file not found")]
|
||||||
|
FileNotFound(PathBuf),
|
||||||
|
|
||||||
|
#[error("metadata path is not a file")]
|
||||||
|
NotAFile,
|
||||||
|
|
||||||
|
#[error("cannot read metadata")]
|
||||||
|
PermissionDenied,
|
||||||
|
|
||||||
|
#[error("invalid metadata path")]
|
||||||
|
MetadataParseError(serde_json::error::Error),
|
||||||
|
|
||||||
|
#[error("IO error")]
|
||||||
|
IOError(#[from] std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ReadFileError> for DeleteFileError {
|
||||||
|
fn from(err: ReadFileError) -> Self {
|
||||||
|
match err {
|
||||||
|
ReadFileError::FileNotFound(path) => DeleteFileError::FileNotFound(path),
|
||||||
|
ReadFileError::NotAFile => DeleteFileError::NotAFile,
|
||||||
|
ReadFileError::PermissionDenied => DeleteFileError::PermissionDenied,
|
||||||
|
ReadFileError::JSONError(err) => DeleteFileError::MetadataParseError(err),
|
||||||
|
ReadFileError::IOError(err) => DeleteFileError::IOError(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq)]
|
||||||
|
pub struct FileId(String);
|
||||||
|
|
||||||
|
impl From<String> for FileId {
|
||||||
|
fn from(s: String) -> Self {
|
||||||
|
Self(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for FileId {
|
||||||
|
fn from(s: &str) -> Self {
|
||||||
|
Self(s.to_owned())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<FileId> for PathBuf {
|
||||||
|
fn from(s: FileId) -> Self {
|
||||||
|
Self::from(&s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&FileId> for PathBuf {
|
||||||
|
fn from(s: &FileId) -> Self {
|
||||||
|
let FileId(s) = s;
|
||||||
|
Self::from(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for FileId {
|
||||||
|
type Target = String;
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait FileRoot {
|
||||||
|
fn root(&self) -> PathBuf;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Context(PathBuf);
|
||||||
|
|
||||||
|
impl FileRoot for Context {
|
||||||
|
fn root(&self) -> PathBuf {
|
||||||
|
self.0.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Store {
|
||||||
|
files_root: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Store {
|
||||||
|
pub fn new(files_root: PathBuf) -> Self {
|
||||||
|
Self { files_root }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_files(&self) -> Result<HashSet<FileId>, ReadFileError> {
|
||||||
|
let paths = std::fs::read_dir(&self.files_root)?;
|
||||||
|
let info_files = paths
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|path| {
|
||||||
|
let path_ = path.unwrap().path();
|
||||||
|
if path_.extension().and_then(|s| s.to_str()) == Some("json") {
|
||||||
|
let stem = path_.file_stem().and_then(|s| s.to_str()).unwrap();
|
||||||
|
Some(FileId::from(stem))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<HashSet<FileId>>();
|
||||||
|
Ok(info_files)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_file(
|
||||||
|
&mut self,
|
||||||
|
filename: String,
|
||||||
|
content: Vec<u8>,
|
||||||
|
) -> Result<FileHandle, WriteFileError> {
|
||||||
|
let mut file = FileHandle::new(filename, self.files_root.clone())?;
|
||||||
|
file.set_content(content)?;
|
||||||
|
Ok(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_file(&self, id: &FileId) -> Result<FileHandle, ReadFileError> {
|
||||||
|
FileHandle::load(id, &self.files_root)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_file(&mut self, id: &FileId) -> Result<(), DeleteFileError> {
|
||||||
|
let handle = FileHandle::load(id, &self.files_root)?;
|
||||||
|
handle.delete();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_metadata(&self, id: &FileId) -> Result<FileInfo, ReadFileError> {
|
||||||
|
let mut path = self.files_root.clone();
|
||||||
|
path.push(PathBuf::from(id));
|
||||||
|
path.set_extension("json");
|
||||||
|
FileInfo::load(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use cool_asserts::assert_matches;
|
||||||
|
use std::{collections::HashSet, io::Read};
|
||||||
|
use tempdir::TempDir;
|
||||||
|
|
||||||
|
fn with_file<F>(test_fn: F)
|
||||||
|
where
|
||||||
|
F: FnOnce(Store, FileId, TempDir),
|
||||||
|
{
|
||||||
|
let tmp = TempDir::new("var").unwrap();
|
||||||
|
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
let mut file = std::fs::File::open("fixtures/rawr.png").unwrap();
|
||||||
|
file.read_to_end(&mut buf).unwrap();
|
||||||
|
|
||||||
|
let mut store = Store::new(PathBuf::from(tmp.path()));
|
||||||
|
let file_record = store.add_file("rawr.png".to_owned(), buf).unwrap();
|
||||||
|
|
||||||
|
test_fn(store, file_record.id, tmp);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn adds_files() {
|
||||||
|
with_file(|store, id, tmp| {
|
||||||
|
let file = store.get_file(&id).expect("to retrieve the file");
|
||||||
|
|
||||||
|
assert_eq!(file.content().map(|file| file.len()).unwrap(), 23777);
|
||||||
|
|
||||||
|
assert!(tmp.path().join(&(*id)).with_extension("png").exists());
|
||||||
|
assert!(tmp.path().join(&(*id)).with_extension("json").exists());
|
||||||
|
assert!(tmp.path().join(&(*id)).with_extension("tn.png").exists());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sets_up_metadata_for_file() {
|
||||||
|
with_file(|store, id, tmp| {
|
||||||
|
assert!(tmp.path().join(&(*id)).with_extension("png").exists());
|
||||||
|
let info = store.get_metadata(&id).expect("to retrieve the metadata");
|
||||||
|
|
||||||
|
assert_matches!(info, FileInfo { size, file_type, hash, extension, .. } => {
|
||||||
|
assert_eq!(size, 23777);
|
||||||
|
assert_eq!(file_type, "image/png");
|
||||||
|
assert_eq!(hash, "b6cd35e113b95d62f53d9cbd27ccefef47d3e324aef01a2db6c0c6d3a43c89ee".to_owned());
|
||||||
|
assert_eq!(extension, "png".to_owned());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
#[test]
|
||||||
|
fn sets_up_thumbnail_for_file() {
|
||||||
|
with_file(|store, id| {
|
||||||
|
let (_, thumbnail) = store.get_thumbnail(&id).expect("to retrieve the thumbnail");
|
||||||
|
assert_eq!(thumbnail.content().map(|file| file.len()).unwrap(), 48869);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deletes_associated_files() {
|
||||||
|
with_file(|mut store, id, tmp| {
|
||||||
|
store.delete_file(&id).expect("file to be deleted");
|
||||||
|
|
||||||
|
assert!(!tmp.path().join(&(*id)).with_extension("png").exists());
|
||||||
|
assert!(!tmp.path().join(&(*id)).with_extension("json").exists());
|
||||||
|
assert!(!tmp.path().join(&(*id)).with_extension("tn.png").exists());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lists_files_in_the_db() {
|
||||||
|
with_file(|store, id, _| {
|
||||||
|
let resolvers = store.list_files().expect("file listing to succeed");
|
||||||
|
let ids = resolvers.into_iter().collect::<HashSet<FileId>>();
|
||||||
|
|
||||||
|
assert_eq!(ids.len(), 1);
|
||||||
|
assert!(ids.contains(&id));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
use super::{ReadFileError, WriteFileError};
|
||||||
|
use image::imageops::FilterType;
|
||||||
|
use std::{
|
||||||
|
fs::remove_file,
|
||||||
|
io::Read,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct Thumbnail {
|
||||||
|
pub path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Thumbnail {
|
||||||
|
pub fn open(
|
||||||
|
origin_path: PathBuf,
|
||||||
|
thumbnail_path: PathBuf,
|
||||||
|
) -> Result<Thumbnail, WriteFileError> {
|
||||||
|
let s = Thumbnail {
|
||||||
|
path: PathBuf::from(thumbnail_path),
|
||||||
|
};
|
||||||
|
|
||||||
|
if !s.path.exists() {
|
||||||
|
let img = image::open(&origin_path)?;
|
||||||
|
let tn = img.resize(640, 640, FilterType::Nearest);
|
||||||
|
tn.save(&s.path)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load(path: PathBuf) -> Result<Thumbnail, ReadFileError> {
|
||||||
|
let s = Thumbnail { path: path.clone() };
|
||||||
|
|
||||||
|
if !s.path.exists() {
|
||||||
|
return Err(ReadFileError::FileNotFound(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
pub fn from_path(path: &Path) -> Result<Thumbnail, ReadFileError> {
|
||||||
|
let id = path
|
||||||
|
.file_name()
|
||||||
|
.map(|s| String::from(s.to_string_lossy()))
|
||||||
|
.ok_or(ReadFileError::NotAnImage(PathBuf::from(path)))?;
|
||||||
|
|
||||||
|
let path = path
|
||||||
|
.parent()
|
||||||
|
.ok_or(ReadFileError::FileNotFound(PathBuf::from(path)))?;
|
||||||
|
|
||||||
|
Thumbnail::open(&id, root)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
pub fn stream(&self) -> Result<std::fs::File, ReadFileError> {
|
||||||
|
std::fs::File::open(self.path.clone()).map_err(|err| {
|
||||||
|
if err.kind() == std::io::ErrorKind::NotFound {
|
||||||
|
ReadFileError::FileNotFound
|
||||||
|
} else {
|
||||||
|
ReadFileError::from(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
pub fn delete(self) -> Result<(), WriteFileError> {
|
||||||
|
remove_file(self.path).map_err(WriteFileError::from)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use crate::store::utils::FileCleanup;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_creates_a_thumbnail_if_one_does_not_exist() {
|
||||||
|
let _ = FileCleanup(PathBuf::from("var/rawr.tn.png"));
|
||||||
|
let _ = Thumbnail::open(
|
||||||
|
PathBuf::from("fixtures/rawr.png"),
|
||||||
|
PathBuf::from("var/rawr.tn.png"),
|
||||||
|
)
|
||||||
|
.expect("thumbnail open must work");
|
||||||
|
assert!(Path::new("var/rawr.tn.png").is_file());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title> {{title}} </title>
|
||||||
|
<link href="/css" rel="stylesheet" type="text/css" media="screen" />
|
||||||
|
<script src="/script"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<a href="/file/{{id}}"><img src="/tn/{{id}}" /></a>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -0,0 +1,54 @@
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title> Admin list of files </title>
|
||||||
|
<link href="/css" rel="stylesheet" type="text/css" media="screen" />
|
||||||
|
<script src="/script"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h1> Admin list of files </h1>
|
||||||
|
|
||||||
|
<div class="uploadform">
|
||||||
|
<form action="/" method="post" enctype="multipart/form-data">
|
||||||
|
<div id="file-selector">
|
||||||
|
<input type="file" name="file" id="file-selector-input" />
|
||||||
|
<label for="file-selector-input" onclick="selectFile('file-selector')">Select a file</label>
|
||||||
|
</div>
|
||||||
|
<input type="submit" value="Upload file" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="files">
|
||||||
|
{{#files}}
|
||||||
|
<div class="file">
|
||||||
|
{{#error}}
|
||||||
|
<div>
|
||||||
|
<p> {{error}} </p>
|
||||||
|
</div>
|
||||||
|
{{/error}}
|
||||||
|
|
||||||
|
{{#file}}
|
||||||
|
<div class="thumbnail">
|
||||||
|
<a href="/file/{{id}}"><img src="/tn/{{id}}" /></a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<ul>
|
||||||
|
<li> {{date}} </li>
|
||||||
|
<li> {{type_}} </li>
|
||||||
|
<li> {{size}} </li>
|
||||||
|
</ul>
|
||||||
|
<div>
|
||||||
|
<form action="/{{id}}" method="post">
|
||||||
|
<input type="hidden" name="_method" value="delete" />
|
||||||
|
<input type="submit" value="Delete" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/file}}
|
||||||
|
</div>
|
||||||
|
{{/files}}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -0,0 +1,10 @@
|
||||||
|
const selectFile = (selectorId) => {
|
||||||
|
console.log("wide arrow functions work: " + selectorId);
|
||||||
|
const input = document.querySelector("#" + selectorId + " input[type='file']")
|
||||||
|
const label = document.querySelector("#" + selectorId + " label")
|
||||||
|
input.addEventListener("change", (e) => {
|
||||||
|
if (input.files.length > 0) {
|
||||||
|
label.innerHTML = input.files[0].name
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,186 @@
|
||||||
|
:root {
|
||||||
|
--main-bg-color: #e5f0fc;
|
||||||
|
--fg-color: #449dfc;
|
||||||
|
|
||||||
|
--space-small: 4px;
|
||||||
|
--space-medium: 8px;
|
||||||
|
--space-large: 12px;
|
||||||
|
|
||||||
|
--hover-low: 4px 4px 4px gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Ariel', sans-serif;
|
||||||
|
background-color: var(--main-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border: 1px solid black;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: var(--hover-low);
|
||||||
|
margin: var(--space-large);
|
||||||
|
padding: var(--space-medium);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.authentication-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 200px;
|
||||||
|
margin: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.authentication-form {
|
||||||
|
}
|
||||||
|
|
||||||
|
.authentication-form__label {
|
||||||
|
margin: var(--space-small);
|
||||||
|
}
|
||||||
|
|
||||||
|
.authentication-form__input {
|
||||||
|
margin: var(--space-small);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-form__selector {
|
||||||
|
margin: var(--space-small);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-form__button {
|
||||||
|
margin: var(--space-small);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnail {
|
||||||
|
width: 300px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnail__image {
|
||||||
|
max-width: 100%;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnail__metadata {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
[type="submit"] {
|
||||||
|
border-radius: 1em;
|
||||||
|
margin: 1em;
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadform {
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
[type="file"] {
|
||||||
|
border: 0;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
height: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0;
|
||||||
|
position: absolute !important;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[type="file"] + label {
|
||||||
|
background-color: rgb(0, 86, 112);
|
||||||
|
border-radius: 1em;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 1em;
|
||||||
|
margin: 1em;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
[type="file"]:focus + label,
|
||||||
|
[type="file"] + label:hover {
|
||||||
|
background-color: #67b0ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
[type="file"]:focus + label {
|
||||||
|
outline: 1px dotted #000;
|
||||||
|
outline: -webkit-focus-ring-color auto 5px;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
@media screen and (max-width: 1080px) { /* This is the screen width of a OnePlus 8 */
|
||||||
|
body {
|
||||||
|
font-size: xx-large;
|
||||||
|
}
|
||||||
|
|
||||||
|
.authentication-form {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.authentication-form__input {
|
||||||
|
font-size: x-large;
|
||||||
|
}
|
||||||
|
|
||||||
|
.authentication-form__button {
|
||||||
|
font-size: x-large;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-form__selector {
|
||||||
|
font-size: larger;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-form__button {
|
||||||
|
font-size: larger;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnail {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
[type="submit"] {
|
||||||
|
font-size: xx-large;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadform {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
[type="file"] + label {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnail {
|
||||||
|
max-width: 100%;
|
||||||
|
margin: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
[package]
|
||||||
|
name = "fitnesstrax"
|
||||||
|
version = "0.6.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
adw = { version = "0.5", package = "libadwaita", features = [ "v1_4" ] }
|
||||||
|
async-channel = { version = "2.1" }
|
||||||
|
async-trait = { version = "0.1" }
|
||||||
|
chrono = { version = "0.4" }
|
||||||
|
chrono-tz = { version = "0.8" }
|
||||||
|
dimensioned = { version = "0.8", features = [ "serde" ] }
|
||||||
|
emseries = { path = "../../emseries" }
|
||||||
|
ft-core = { path = "../core" }
|
||||||
|
gio = { version = "0.18" }
|
||||||
|
glib = { version = "0.18" }
|
||||||
|
gdk = { version = "0.7", package = "gdk4" }
|
||||||
|
gtk = { version = "0.7", package = "gtk4", features = [ "v4_10" ] }
|
||||||
|
thiserror = { version = "1.0" }
|
||||||
|
tokio = { version = "1.34", features = [ "full" ] }
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
glib-build-tools = "0.18"
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
fn main() {
|
||||||
|
glib_build_tools::compile_resources(
|
||||||
|
&["resources"],
|
||||||
|
"gresources.xml",
|
||||||
|
"com.luminescent-dreams.fitnesstrax.gresource",
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
VERSION=`cat Cargo.toml | grep "^version =" | sed -r 's/^version = "(.+)"$/\1/'`
|
||||||
|
|
||||||
|
mkdir -p dist
|
||||||
|
cp ../../target/release/fitnesstrax dist
|
||||||
|
cp resources/com.luminescent-dreams.fitnesstrax.gschema.xml resources/fitnesstrax.desktop dist
|
||||||
|
strip dist/fitnesstrax
|
||||||
|
tar -czf fitnesstrax-${VERSION}.tgz dist/
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<gresources>
|
||||||
|
<gresource prefix="/com/luminescent-dreams/fitnesstrax/">
|
||||||
|
<file>style.css</file>
|
||||||
|
</gresource>
|
||||||
|
|
||||||
|
<gresource prefix="/com/luminescent-dreams/fitnesstrax/icons/scalable/actions">
|
||||||
|
<file preprocess="xml-stripblanks">cycling-symbolic.svg</file>
|
||||||
|
</gresource>
|
||||||
|
|
||||||
|
<gresource prefix="/com/luminescent-dreams/fitnesstrax/icons/scalable/actions">
|
||||||
|
<file preprocess="xml-stripblanks">running-symbolic.svg</file>
|
||||||
|
</gresource>
|
||||||
|
|
||||||
|
<gresource prefix="/com/luminescent-dreams/fitnesstrax/icons/scalable/actions/">
|
||||||
|
<file preprocess="xml-stripblanks">walking-symbolic.svg</file>
|
||||||
|
</gresource>
|
||||||
|
</gresources>
|
|
@ -0,0 +1,14 @@
|
||||||
|
{ gtkNativeInputs }:
|
||||||
|
attrs: {
|
||||||
|
nativeBuildInputs = gtkNativeInputs;
|
||||||
|
postInstall = ''
|
||||||
|
install -Dt $out/share/applications resources/fitnesstrax.desktop
|
||||||
|
install -Dt $out/gsettings-schemas/${attrs.crateName}-${attrs.version}/glib-2.0/schemas resources/com.luminescent-dreams.fitnesstrax.gschema.xml
|
||||||
|
glib-compile-schemas $out/gsettings-schemas/${attrs.crateName}-${attrs.version}/glib-2.0/schemas
|
||||||
|
'';
|
||||||
|
preFixup = ''
|
||||||
|
gappsWrapperArgs+=(
|
||||||
|
--prefix XDG_DATA_DIRS : $out/gsettings-schemas/${attrs.crateName}-${attrs.version}
|
||||||
|
)
|
||||||
|
'';
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<schemalist>
|
||||||
|
<schema id="com.luminescent-dreams.fitnesstrax.dev" path="/com/luminescent-dreams/fitnesstrax/dev/">
|
||||||
|
<key name="series-path" type="s">
|
||||||
|
<default>""</default>
|
||||||
|
<summary>Path to the series</summary>
|
||||||
|
</key>
|
||||||
|
</schema>
|
||||||
|
</schemalist>
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<schemalist>
|
||||||
|
<schema id="com.luminescent-dreams.fitnesstrax" path="/com/luminescent-dreams/fitnesstrax/">
|
||||||
|
<key name="series-path" type="s">
|
||||||
|
<default>""</default>
|
||||||
|
<summary>Path to the series</summary>
|
||||||
|
</key>
|
||||||
|
</schema>
|
||||||
|
</schemalist>
|
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="16px" viewBox="0 0 16 16" width="16px"><filter id="a" height="100%" width="100%" x="0%" y="0%"><feColorMatrix color-interpolation-filters="sRGB" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/></filter><mask id="b"><g filter="url(#a)"><path d="m -1.6 -1.6 h 19.2 v 19.2 h -19.2 z" fill-opacity="0.5"/></g></mask><clipPath id="c"><path d="m 0 0 h 1600 v 1200 h -1600 z"/></clipPath><mask id="d"><g filter="url(#a)"><path d="m -1.6 -1.6 h 19.2 v 19.2 h -19.2 z" fill-opacity="0.7"/></g></mask><clipPath id="e"><path d="m 0 0 h 1600 v 1200 h -1600 z"/></clipPath><mask id="f"><g filter="url(#a)"><path d="m -1.6 -1.6 h 19.2 v 19.2 h -19.2 z" fill-opacity="0.35"/></g></mask><clipPath id="g"><path d="m 0 0 h 1600 v 1200 h -1600 z"/></clipPath><path d="m 9.5 2 c -0.828125 0 -1.5 0.671875 -1.5 1.5 s 0.671875 1.5 1.5 1.5 s 1.5 -0.671875 1.5 -1.5 s -0.671875 -1.5 -1.5 -1.5 z m 0 0"/><path d="m 4.285156 13 c 0 0.703125 -0.582031 1.285156 -1.285156 1.285156 s -1.285156 -0.582031 -1.285156 -1.285156 s 0.582031 -1.285156 1.285156 -1.285156 s 1.285156 0.582031 1.285156 1.285156 z m -4.285156 0 c 0 1.675781 1.324219 3 3 3 s 3 -1.324219 3 -3 s -1.324219 -3 -3 -3 s -3 1.324219 -3 3 z m 0 0"/><path d="m 8.992188 13.007812 v -3.003906 c 0 -0.359375 -0.1875 -0.6875 -0.5 -0.867187 l -2.558594 -1.476563 l 0.363281 1.363282 l 1.671875 -2.890626 l -1.367188 0.363282 l 0.910157 0.527344 l -0.40625 -0.4375 c 0.773437 1.621093 1.96875 1.933593 1.96875 1.933593 s 0.578125 0.242188 1.9375 0.429688 c 0.546875 0.074219 1.050781 -0.304688 1.128906 -0.851563 c 0.074219 -0.550781 -0.308594 -1.054687 -0.855469 -1.128906 c -1.179687 -0.164062 -1.601562 -0.355469 -1.601562 -0.355469 s -0.425782 -0.164062 -0.769532 -0.886719 c -0.089843 -0.183593 -0.226562 -0.335937 -0.402343 -0.4375 l -0.910157 -0.523437 c -0.476562 -0.277344 -1.089843 -0.113281 -1.363281 0.367187 l -1.671875 2.890626 c -0.277344 0.480468 -0.113281 1.089843 0.367188 1.367187 l 2.558594 1.480469 l -0.5 -0.867188 v 3.003906 c 0 0.550782 0.449218 1 1 1 c 0.554687 0 1 -0.449218 1 -1 z m 0 0"/><path d="m 14.285156 13 c 0 0.703125 -0.582031 1.285156 -1.285156 1.285156 s -1.285156 -0.582031 -1.285156 -1.285156 s 0.582031 -1.285156 1.285156 -1.285156 s 1.285156 0.582031 1.285156 1.285156 z m -4.285156 0 c 0 1.675781 1.324219 3 3 3 s 3 -1.324219 3 -3 s -1.324219 -3 -3 -3 s -3 1.324219 -3 3 z m 0 0"/><g mask="url(#b)"><g clip-path="url(#c)" transform="matrix(1 0 0 1 -600 -120)"><path d="m 550 182 c -0.351562 0.003906 -0.695312 0.101562 -1 0.28125 v 3.4375 c 0.304688 0.179688 0.648438 0.277344 1 0.28125 c 1.105469 0 2 -0.894531 2 -2 s -0.894531 -2 -2 -2 z m 0 5 c -0.339844 0 -0.679688 0.058594 -1 0.175781 v 6.824219 h 4 v -4 c 0 -1.65625 -1.34375 -3 -3 -3 z m 0 0"/></g></g><g mask="url(#d)"><g clip-path="url(#e)" transform="matrix(1 0 0 1 -600 -120)"><path d="m 569 182 v 4 c 1.105469 0 2 -0.894531 2 -2 s -0.894531 -2 -2 -2 z m 0 5 v 7 h 3 v -4 c 0 -1.65625 -1.34375 -3 -3 -3 z m 0 0"/></g></g><g mask="url(#f)"><g clip-path="url(#g)" transform="matrix(1 0 0 1 -600 -120)"><path d="m 573 182.269531 v 3.449219 c 0.613281 -0.355469 0.996094 -1.007812 1 -1.71875 c 0 -0.714844 -0.382812 -1.375 -1 -1.730469 z m 0 4.90625 v 6.824219 h 2 v -4 c 0 -1.269531 -0.800781 -2.402344 -2 -2.824219 z m 0 0"/></g></g></svg>
|
After Width: | Height: | Size: 3.3 KiB |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue