mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-06-13 12:15:09 +03:00
Compare commits
659 Commits
v2.3.1-pre2
...
v3.2.4
| Author | SHA1 | Date | |
|---|---|---|---|
| acaf165219 | |||
| bef7ede91f | |||
| 511dd121bd | |||
| d31c2bf034 | |||
| b7a884f26f | |||
| a477bf6829 | |||
| 1e8bf43e5f | |||
| 259d01d7ec | |||
| a13887a13a | |||
| b97c27aac4 | |||
| 8b8a1cc4d8 | |||
| 7a283c7608 | |||
| 9f586a1b97 | |||
| 789851579b | |||
| 7ea7bc7f9b | |||
| e267ba9135 | |||
| 44bb023b80 | |||
| 86b5567d4e | |||
| 8f41eb93ef | |||
| 670ae7d8a7 | |||
| 01ec1663bc | |||
| b840d75cb8 | |||
| e22ba20d13 | |||
| bff410d98b | |||
| 7188db9877 | |||
| 02e8640ad4 | |||
| e63983dba9 | |||
| cbea1518ed | |||
| 414493eed2 | |||
| 078a9cf33d | |||
| 6e357ceecc | |||
| e4f7546f99 | |||
| 4e5468a353 | |||
| 0b73882012 | |||
| b15392b5f2 | |||
| e646168e31 | |||
| 5ec4a4645d | |||
| b47ad69557 | |||
| efac7bc397 | |||
| ce2f98aa04 | |||
| ede8a18e9b | |||
| 8db3711cab | |||
| ffa3226077 | |||
| dc0e1cfdce | |||
| b3483897e0 | |||
| 543ee3d19e | |||
| 654957cb0c | |||
| 11e5236fa3 | |||
| c1dd1c7296 | |||
| 310dd700ac | |||
| 0c727d64d2 | |||
| 054517595d | |||
| ceb10fd7cf | |||
| 8e50886c48 | |||
| 76677fa877 | |||
| f6144f9634 | |||
| 79bb62a3c4 | |||
| 83f2c94712 | |||
| 25036b065f | |||
| 6bf21bf2ad | |||
| 4b47a8abeb | |||
| 43f22329e1 | |||
| 19e5d84928 | |||
| 86dea2d7c6 | |||
| 4c95484ccb | |||
| 8d2b252e6d | |||
| dbd7488d8f | |||
| cff3ca504d | |||
| 456ccf479a | |||
| 4f05a001fb | |||
| 43aa0497f4 | |||
| 1781c19c11 | |||
| 1ef3cb8307 | |||
| 5bc4dc684d | |||
| 4cff530b52 | |||
| 2f90a4fcf7 | |||
| 24fce1e093 | |||
| 97b12972ee | |||
| f69eb72df9 | |||
| 24215c0732 | |||
| e87ce625fb | |||
| 1284075d02 | |||
| d2c5aef58b | |||
| caac379b08 | |||
| c487a6e15a | |||
| 9df5f00068 | |||
| 508408ad5a | |||
| ef4ca204bd | |||
| 1d7af36a2b | |||
| 6b78e0ae9c | |||
| 1f5d5f9f1a | |||
| f7a47127a7 | |||
| 494f8dac5e | |||
| bcc14848c5 | |||
| 0e40374b10 | |||
| 81ffe11106 | |||
| d06494885a | |||
| a6e4bcf052 | |||
| c1f7ca7f20 | |||
| 6ae6f5ee67 | |||
| 016ed6e998 | |||
| 4842a599e0 | |||
| 927f912eb3 | |||
| 7c9b68c1dd | |||
| e5e75cc16e | |||
| 3656f541e6 | |||
| f2b859b409 | |||
| 93b3919325 | |||
| 7a161a5b83 | |||
| 1265672067 | |||
| b8acc849bc | |||
| 65b6883dcc | |||
| 166c7ea8a7 | |||
| ddc807340c | |||
| 7fc51c8b7d | |||
| 131f0961ff | |||
| 75aa1e4851 | |||
| dcfaf11696 | |||
| 3e50ce65a7 | |||
| adf5a76392 | |||
| e01a700669 | |||
| 4b23a800f6 | |||
| 7045cdea6b | |||
| ef0d449fb6 | |||
| a5ccc87a46 | |||
| 24c3d3c8f1 | |||
| 2000eaf399 | |||
| dc9f89afad | |||
| 88dfd476e5 | |||
| d634503ffa | |||
| 24e1a239cc | |||
| 88fed53a07 | |||
| 383225aa54 | |||
| 89c9751f04 | |||
| bf22cdb491 | |||
| 54b50fd25c | |||
| 1bccfe22ef | |||
| 68c6eedb81 | |||
| 7e0ec279e7 | |||
| 837ae838d4 | |||
| 5646168959 | |||
| 7043256232 | |||
| c1f01dcb21 | |||
| fb3de87921 | |||
| 846995e0d3 | |||
| f73aced452 | |||
| a45df8566e | |||
| 2b2a1d9e39 | |||
| 86df2e0fd9 | |||
| f5eba51c52 | |||
| 83972972f3 | |||
| 2c4edeb810 | |||
| b32fa7948f | |||
| 9a098ec942 | |||
| b378a25c8c | |||
| 0c96738530 | |||
| 9875c4ddbc | |||
| a12d9d2bdb | |||
| e266258fc4 | |||
| 725ebd5835 | |||
| 58cd6e9541 | |||
| 5697d98862 | |||
| 6cd4fb5553 | |||
| a3143c11a8 | |||
| d71fb7935b | |||
| 107b9abb3e | |||
| 0cfb41755a | |||
| 2f8b5df007 | |||
| 69e9f998e9 | |||
| a8d55785cf | |||
| d067fe6361 | |||
| c02a78ed6e | |||
| c2f8cba4af | |||
| c36f1985bd | |||
| fc52cf53ee | |||
| dcff7d146b | |||
| 13a38d82fd | |||
| b537968feb | |||
| 983fcf3014 | |||
| 61ad88db77 | |||
| 84577c8708 | |||
| 6bd3951d31 | |||
| 48e79f803a | |||
| 7f421f5bd4 | |||
| d6e71b23ef | |||
| 54aecee64e | |||
| 822b6bcaaa | |||
| 259aabf82f | |||
| 1de8c5915b | |||
| 4f92276147 | |||
| f11e34ab25 | |||
| 23944f7cbf | |||
| e6b1950bcb | |||
| 0c6c7b1b06 | |||
| 9c8db576f5 | |||
| a84a13c76d | |||
| 96cc72f2ca | |||
| 372771a228 | |||
| 675b6d5ef8 | |||
| 97b62ac72b | |||
| 0b1c069448 | |||
| 43b940e216 | |||
| 10e13571e5 | |||
| 14ea7b964f | |||
| 2a22f000c1 | |||
| 63f90d884e | |||
| a1a29616e5 | |||
| 6b06bae250 | |||
| 9a0a52f9e3 | |||
| 6f7dd26d56 | |||
| 28aca03f0d | |||
| f9ff518e6d | |||
| de9d2144a6 | |||
| 11b743a792 | |||
| 637e3c566e | |||
| 89a4a69f6d | |||
| a449181802 | |||
| 4696583680 | |||
| 6d2e9365c0 | |||
| 61cf80f8a2 | |||
| 39e752e4e2 | |||
| 992e2e2ad3 | |||
| c2ebc78836 | |||
| dc1697d049 | |||
| 35f6f041c1 | |||
| 7e3f9edacf | |||
| 22b732bab4 | |||
| 61b984bfa1 | |||
| 867ce63f7b | |||
| ee6122b97d | |||
| 091428f683 | |||
| 9db3f3f20e | |||
| 37d70c31ff | |||
| ee1627a3a1 | |||
| ecd8ef5998 | |||
| 8987076c5f | |||
| ec41c61219 | |||
| 21c7742f9a | |||
| e8add28797 | |||
| f5e902f741 | |||
| 063961d36a | |||
| 96ce599e34 | |||
| c5ea95a787 | |||
| db96121304 | |||
| 40c7c1b515 | |||
| 70104065e2 | |||
| 11a88186ba | |||
| 6522ebf0ca | |||
| 6042ca56b8 | |||
| 5190765ce1 | |||
| 3a38cf8074 | |||
| 4f40b97f53 | |||
| 5fd5b2f08c | |||
| 87d7e15951 | |||
| 98e2d1232c | |||
| 6cb545c78d | |||
| 41c62c3a34 | |||
| 31a6202da9 | |||
| 3565c3c9b4 | |||
| e2ade0d731 | |||
| 06690890fb | |||
| 8b0d1753ef | |||
| 70532c8d0b | |||
| 5e1e035bc2 | |||
| 34d1a34b10 | |||
| cfb6fe69e3 | |||
| 1921e7ec56 | |||
| 05b41b3410 | |||
| 18d89e76fd | |||
| 4a9b45b98e | |||
| 8f299a90dc | |||
| 5a633fdd82 | |||
| 8ce4a7679e | |||
| a0920cf0d0 | |||
| 7565a66d90 | |||
| e4b1efc44d | |||
| 85f4604bfd | |||
| f30309d153 | |||
| f48d774554 | |||
| 268311c482 | |||
| ed0f851d4d | |||
| 2f4e79a40e | |||
| f508d1dfce | |||
| 8dc6f19d2b | |||
| 321242b0d9 | |||
| 949c5e73c4 | |||
| da9217ac4a | |||
| 39f64ca666 | |||
| 2391af09eb | |||
| 0b01df7339 | |||
| c69ca5e821 | |||
| b155734dcf | |||
| 96ae502202 | |||
| b373c3114b | |||
| af6784272c | |||
| df08f611b9 | |||
| de1b24b8bc | |||
| fae09596a7 | |||
| e8f5ce8d5a | |||
| 81726acd51 | |||
| 5582457c58 | |||
| df755063c2 | |||
| 1949e9fcf1 | |||
| 44e6f03b06 | |||
| d00506d141 | |||
| ffe9cd8afe | |||
| a7669799bc | |||
| dacd2c19eb | |||
| 2f46e0dc3e | |||
| e363c677a0 | |||
| f94a1f97df | |||
| 28ca7b5c90 | |||
| feabb582b2 | |||
| 47e9708a20 | |||
| 934b1b35ad | |||
| 88250b3e20 | |||
| d9ba637cd9 | |||
| dcee5ca4e4 | |||
| dffa82a555 | |||
| 63f8cff341 | |||
| 836bcfbc03 | |||
| 64867369f9 | |||
| 315b2f9cda | |||
| 07c8ddc0b2 | |||
| 36c145bb3a | |||
| 19793d71e6 | |||
| 796b8b49f4 | |||
| 1b9d674a81 | |||
| 949244e8e6 | |||
| b7d4d57bc2 | |||
| 458fa6a66c | |||
| 06fccb792f | |||
| 6867feba85 | |||
| 45353c87c2 | |||
| 37b8c5264f | |||
| 5937971014 | |||
| a351c02ac1 | |||
| fc086db5e6 | |||
| ca1332334d | |||
| aebca54eac | |||
| 8fe8062950 | |||
| 11d731cf35 | |||
| bf83eabe6b | |||
| a7c358844c | |||
| e2d708a6af | |||
| 1e1e82ed71 | |||
| f53a3f383d | |||
| 407e647993 | |||
| 22d579512d | |||
| caf4d75a06 | |||
| a43e4328df | |||
| 0ede024771 | |||
| 193f97d9fe | |||
| 38c96ea96a | |||
| dd130dbbd7 | |||
| 9233495cac | |||
| e4001966e8 | |||
| c52f1cd038 | |||
| 2ba9bce3de | |||
| d4a76da78c | |||
| c7e4eb1b3f | |||
| 5460f95eac | |||
| 3ae4ceb431 | |||
| 7a725935fc | |||
| 9437d7ed99 | |||
| 34cf24f7c0 | |||
| 5a9959f70f | |||
| 96069d3299 | |||
| b4c90683aa | |||
| 6993516ccb | |||
| 37c1a0e964 | |||
| 6d086fb5d5 | |||
| 7c805e9b80 | |||
| 6b3f9e4a9e | |||
| dea08868d3 | |||
| 365866c35f | |||
| 3d6d69568d | |||
| 62a2107c06 | |||
| cf2f419942 | |||
| f657529ab5 | |||
| 8eae2d60e5 | |||
| 3e64e53cd7 | |||
| ef9afa8cbc | |||
| 143461d597 | |||
| ae887e10d6 | |||
| 186c70591a | |||
| 777e7893e3 | |||
| b2a1b172e2 | |||
| fc72ac5c63 | |||
| f1576c7798 | |||
| 388526d02d | |||
| cb65fb4b11 | |||
| b82c9cf6e6 | |||
| 04fff476d5 | |||
| 66bcdfd84e | |||
| 8494f2ec3e | |||
| 52f25708ad | |||
| 24b02114cb | |||
| 651215723d | |||
| ca9831be05 | |||
| f7fd7b712f | |||
| 3859c0e483 | |||
| 2db92536fd | |||
| a6ffc8f04d | |||
| 4df8cb5026 | |||
| a86d473280 | |||
| 4d226b00fc | |||
| 8b16b156ed | |||
| 4029a43637 | |||
| 57f4555350 | |||
| bd87cc852b | |||
| 30d7c9bb81 | |||
| 890599c7bb | |||
| b8e48d2146 | |||
| da1db5a053 | |||
| 60353df7ed | |||
| e8dfe5a87d | |||
| d0b7d1a4a6 | |||
| 13b6ed5ad9 | |||
| 89bbbf6fe4 | |||
| 9a05c228a0 | |||
| 00d9b03322 | |||
| 47bc910a84 | |||
| fdc556edee | |||
| 1c6270ccd6 | |||
| f87e794c3f | |||
| 8f17f2b0fe | |||
| 81b601c0e7 | |||
| 83e8c89c97 | |||
| ca107feb25 | |||
| 56719df827 | |||
| 3c11bb71f6 | |||
| 413cec9244 | |||
| e5277190ed | |||
| 935de1ad0c | |||
| 2e8e0fc7c1 | |||
| 8049417e03 | |||
| f4c0a8b5aa | |||
| d3584a9931 | |||
| 9542708c92 | |||
| b3c363c89f | |||
| 5f5e8ad5d7 | |||
| 6b6de2fcd5 | |||
| b27b625921 | |||
| c5229c6a62 | |||
| c631d40aa3 | |||
| d21b221575 | |||
| 010feddf36 | |||
| 5114bb5711 | |||
| 4e7001efd6 | |||
| 0c9d90df10 | |||
| 09963ef4bb | |||
| 2f24454b1e | |||
| 177ed35522 | |||
| c5aeb018db | |||
| 671120a517 | |||
| f306239b5f | |||
| 44859be657 | |||
| ee4c706529 | |||
| 5ca3015609 | |||
| 8b05fb6943 | |||
| e4ebbed477 | |||
| d13536955e | |||
| 37462548b3 | |||
| 74d06aaca6 | |||
| 1934d12aac | |||
| 70bd5adad3 | |||
| f9d5c4ba2a | |||
| cc5e6ac24d | |||
| d44ce0f74b | |||
| 687f7bf5db | |||
| 9cd219fab7 | |||
| 6e6f8caca8 | |||
| 3a83062438 | |||
| dfdad14ede | |||
| 852897c502 | |||
| 9e502f85fa | |||
| 368a1044da | |||
| 578b8b3ee2 | |||
| ede1067bb3 | |||
| bec1c61366 | |||
| 5d2cc1c133 | |||
| ef97921e30 | |||
| 0009a53800 | |||
| 3eff60e8aa | |||
| cbfa548f98 | |||
| 3f2d54ddbd | |||
| 0ecc9bf579 | |||
| 88b9984b0f | |||
| d6c025da1c | |||
| 7ba864136f | |||
| d5dda39add | |||
| 320f392ad3 | |||
| 47388b1083 | |||
| c1edb9f2e9 | |||
| d184e1c4af | |||
| 88a4c0c361 | |||
| ff1f902c91 | |||
| c1215072a9 | |||
| ad3fc990f5 | |||
| d3038efd45 | |||
| 92b953a22d | |||
| 9accb676e6 | |||
| ba9f12ffe7 | |||
| a4f8f5fcf5 | |||
| 4400226c2d | |||
| 6e6ef81e66 | |||
| c4a4249440 | |||
| a4e02b8201 | |||
| ea56b03b38 | |||
| 3fe1d44515 | |||
| 3932c69ad6 | |||
| 1b8fe4f82f | |||
| 67dc7e7893 | |||
| c14f7043bc | |||
| 6d47a2111b | |||
| aceeaeee07 | |||
| a77ad33ea0 | |||
| e2691592ba | |||
| 3d8629b803 | |||
| 4c5468ae97 | |||
| 81cb3dd0ec | |||
| f94866f473 | |||
| d4a4d2287a | |||
| 4cfeb8ef3e | |||
| 826e014456 | |||
| 9fb211316d | |||
| ba01ad6e13 | |||
| d0b897ff7f | |||
| dd3d313517 | |||
| 83faf6697b | |||
| 7bc2187c8c | |||
| 8a61ecc71a | |||
| 579d52982a | |||
| d895b7c0ef | |||
| a3f6004fd9 | |||
| 73c5a890ce | |||
| 0cb0fb7165 | |||
| 5376478b2d | |||
| c74cfc51e7 | |||
| 6b10c87648 | |||
| 8ab0b34a2e | |||
| b5aeee505a | |||
| d7f343aac4 | |||
| 1eacdc4bce | |||
| a2e2335dd9 | |||
| 6882de6027 | |||
| fccadefe47 | |||
| 70a4b26984 | |||
| 39206b782e | |||
| 3ca625d838 | |||
| 1042655eb9 | |||
| cad2babbca | |||
| dbd1ea9543 | |||
| 313f6db5fa | |||
| 3b58001abe | |||
| bf99bce579 | |||
| 9c02ccc537 | |||
| fedbfcb041 | |||
| 3c52b33e18 | |||
| d99f332523 | |||
| 0c83c9a2b5 | |||
| 6b14f08d59 | |||
| 089e6b086c | |||
| 08d5c942a4 | |||
| 35e7c80835 | |||
| caf37d681f | |||
| 93151e4e5e | |||
| d6f336d84b | |||
| 4777963b65 | |||
| fc193f98db | |||
| a07e4d6fe6 | |||
| 4ab7d97ab2 | |||
| 15a6f7fdb8 | |||
| 7d2cff8745 | |||
| ee5ed3faf0 | |||
| 0f36e87cce | |||
| fd325ef1cc | |||
| faf46ba533 | |||
| 94ec434eae | |||
| a223efd6f5 | |||
| 395e45167d | |||
| 0dd0d9f4bd | |||
| 3e5404abdd | |||
| 46abc5a266 | |||
| 88e1e9b869 | |||
| fc0089f7a5 | |||
| d335562328 | |||
| f9624a0859 | |||
| 97bb3fa4c8 | |||
| 611245aec9 | |||
| e1807e8c75 | |||
| b94cfe32d9 | |||
| f1e679c3fb | |||
| e1b138bcbd | |||
| c6be8f6c14 | |||
| ac086cf59e | |||
| 3e120ea312 | |||
| 0b718daa47 | |||
| 0929b9c5cb | |||
| 43b2269ea7 | |||
| 0c4e27cd34 | |||
| 36cf85b08a | |||
| 994da868af | |||
| 2e53fb217a | |||
| c2b76d9099 | |||
| c05e8f9ae0 | |||
| 2dce0646bd | |||
| 6569f1b268 | |||
| 3ebaac6752 | |||
| cbbd564860 | |||
| 635edf7f5b | |||
| 1b6e18bcb3 | |||
| 0ad0016c62 | |||
| a2716449f9 | |||
| 649eed89c9 | |||
| 83680fffa2 | |||
| d695b90baf | |||
| 5fdcc56409 | |||
| 915997d149 | |||
| e8b4c4a307 | |||
| e92b66068a | |||
| b72b327610 | |||
| b8fdff8093 | |||
| 880b4c2f8f | |||
| 34af340009 | |||
| 80b6115f6f | |||
| 3bed83697e | |||
| 0ffef6e4bf | |||
| 8810a014f3 | |||
| 385552451b | |||
| c2c9d11c66 | |||
| 0474095a40 | |||
| 346f80beb3 | |||
| 2a6bf75f02 | |||
| f73918c902 | |||
| ea35af2050 | |||
| 6232e3da58 | |||
| 35e46f9ccb | |||
| 2b9a80c151 | |||
| a90ed38c89 | |||
| 3653cf5640 | |||
| 0db45ca71e | |||
| 16681fabb5 | |||
| c210523a22 | |||
| 5d8601347a | |||
| 1e02fcf394 | |||
| f923306a7f | |||
| 478fe32527 | |||
| 50764600c8 | |||
| aec7063139 | |||
| c9ee98e0c0 | |||
| 093b064a4e | |||
| 2955681975 | |||
| de42639726 | |||
| 17742e947a | |||
| 898271b33d | |||
| b44896524f | |||
| db7bb236d8 |
@@ -0,0 +1,100 @@
|
||||
name: Bug report
|
||||
description: File a bug/issue
|
||||
title: "[BUG] <title>"
|
||||
labels: [bug]
|
||||
body:
|
||||
- type: checkboxes
|
||||
id: low-effort-checks
|
||||
attributes:
|
||||
label: Please confirm
|
||||
description: Issues without all checks may be ignored/closed.
|
||||
options:
|
||||
- label: I have searched existing issues
|
||||
- label: This issue is not a duplicate of an existing one
|
||||
- label: I have checked the [common issues section in the readme file](https://github.com/SteamDeckHomebrew/decky-loader#-common-issues)
|
||||
- label: I have attached logs to this bug report (failure to include logs will mean your issue may not be responded to).
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Bug Report Description
|
||||
description: A clear and concise description of what the bug is and if possible, the steps you used to get to the bug. If appropriate, include screenshots or videos.
|
||||
placeholder: |
|
||||
When I try to use ...
|
||||
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Expected Behaviour
|
||||
description: A brief description of the expected behavior.
|
||||
placeholder: It should be ...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: SteamOS version
|
||||
# description: Can be found with `uname -a`
|
||||
# placeholder: "Linux steamdeck 5.13.0-valve36-1-neptune #1 SMP PREEMPT Mon, 19 Dec 2022 23:39:41 +0000 x86_64 GNU/Linux"
|
||||
placeholder: "SteamOS 3.5.7 Stable"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Selected Update Channel
|
||||
description: Which branch of Decky are you on?
|
||||
multiple: false
|
||||
options:
|
||||
- Stable
|
||||
- Prerelease
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Decky Loader Version
|
||||
description: Specify the exact version of Decky.
|
||||
placeholder: v3.0.0-pre12
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Plugin Info
|
||||
description: "Include all plugins installed including their version. Helpful script here: https://github.com/SteamDeckHomebrew/decky-loader/blob/main/scripts/plugin-info.sh"
|
||||
placeholder: "If you don't want to collect this info manually you can download a helpful script linked in this item's description and place it into your home directory, chmod +x plugin-info.sh and then run it with ./plugin-info.sh"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Have you modified the read-only filesystem at any point?
|
||||
description: "Describe how here, if you haven't done anything you can leave this blank"
|
||||
placeholder: "Yes, I've installed neofetch via pacman."
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Backend Logs
|
||||
description: Please reboot your deck (if possible) when attempting to recreate the issue, then run ``cd ~ && journalctl -b0 -u plugin_loader.service > deckylog.txt``. This will save the log file to ``~`` aka ``/home/deck``. Please upload the file here.
|
||||
placeholder: deckylog.txt
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Frontend Logs
|
||||
description: Please copy from your deck ~/.steam/steam/logs/cef_log.txt and ~/.steam/steam/logs/cef_log.previous.txt. Make sure to scrub your Steam username as it may appear in these logs.
|
||||
placeholder: cef_log.txt
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Steam Deck Homebrew Discord Server
|
||||
url: https://decky.xyz/discord
|
||||
about: Please ask and answer questions here.
|
||||
@@ -0,0 +1,35 @@
|
||||
name: Feature request
|
||||
description: Request a new feature (NOT A PLUGIN)
|
||||
title: "[Request] <title>"
|
||||
labels: [feature request]
|
||||
body:
|
||||
- type: checkboxes
|
||||
id: low-effort-checks
|
||||
attributes:
|
||||
label: Please confirm
|
||||
description: Issues without all checks may be ignored/closed.
|
||||
options:
|
||||
- label: I have searched existing issues
|
||||
- label: This issue is not a duplicate of an existing one
|
||||
- label: This is not a request for a plugin
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Feature Request Description
|
||||
description: A clear and concise description of what the new feature.
|
||||
placeholder: |
|
||||
Decky plugins should be sortable in the quick access menu
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Further Description
|
||||
description: A further explanation of the feature. If appropriate, include screenshots or videos.
|
||||
placeholder: |
|
||||
This would help make the UI clearer and easier to use as there is less clutter in the QAM.
|
||||
It would also make it faster to access plugins that are used more.
|
||||
|
||||
This could be implemented by adding ...
|
||||
validations:
|
||||
required: false
|
||||
@@ -0,0 +1,13 @@
|
||||
Please tick as appropriate:
|
||||
- [ ] I have tested this code on a steam deck or on a PC
|
||||
- [ ] My changes generate no new errors/warnings
|
||||
- [ ] This is a bugfix/hotfix
|
||||
- [ ] This is a new feature
|
||||
|
||||
If you're wanting to update a translation or add a new one, please use the weblate page: https://weblate.werwolv.net/projects/decky/
|
||||
|
||||
# Description
|
||||
|
||||
This fixes issue: #
|
||||
|
||||
Please provide a clear and concise description of what the new feature is. If appropriate, include screenshots or videos.
|
||||
@@ -0,0 +1,67 @@
|
||||
name: Builder Win
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build-win:
|
||||
name: Build PluginLoader for Win
|
||||
runs-on: windows-2022
|
||||
|
||||
steps:
|
||||
- name: Checkout 🧰
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up NodeJS 20 💎
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Set up Python 3.11.7 🐍
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11.7"
|
||||
|
||||
- name: Install Poetry
|
||||
uses: snok/install-poetry@v1
|
||||
with:
|
||||
virtualenvs-create: false
|
||||
|
||||
- name: Install Python dependencies ⬇️
|
||||
working-directory: ./backend
|
||||
run: |
|
||||
C:\Users\runneradmin\.local\bin\poetry self add "poetry-dynamic-versioning[plugin]"
|
||||
C:\Users\runneradmin\.local\bin\poetry install --no-interaction
|
||||
|
||||
- name: Install JS dependencies ⬇️
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
npm i -g pnpm
|
||||
pnpm i --frozen-lockfile --dangerously-allow-all-builds
|
||||
|
||||
- name: Build JS Frontend 🛠️
|
||||
working-directory: ./frontend
|
||||
run: pnpm run build
|
||||
|
||||
- name: Build Python Backend 🛠️
|
||||
working-directory: ./backend
|
||||
run: |
|
||||
C:\Users\runneradmin\.local\bin\poetry dynamic-versioning
|
||||
C:\Users\runneradmin\.local\bin\poetry run pyinstaller pyinstaller.spec
|
||||
|
||||
- name: Build Python Backend (noconsole) 🛠️
|
||||
working-directory: ./backend
|
||||
run: $env:DECKY_NOCONSOLE = 1; C:\Users\runneradmin\.local\bin\poetry run pyinstaller pyinstaller.spec
|
||||
|
||||
- name: Upload package artifact ⬆️
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: PluginLoader Win
|
||||
path: |
|
||||
./backend/dist/PluginLoader.exe
|
||||
./backend/dist/PluginLoader_noconsole.exe
|
||||
|
||||
+30
-214
@@ -3,251 +3,67 @@ name: Builder
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
workflow_call:
|
||||
# schedule:
|
||||
# - cron: '0 13 * * *' # run at 1 PM UTC
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release:
|
||||
type: choice
|
||||
description: Release the asset
|
||||
default: 'none'
|
||||
options:
|
||||
- none
|
||||
- prerelease
|
||||
- release
|
||||
bump:
|
||||
type: choice
|
||||
description: Semver to bump
|
||||
default: 'none'
|
||||
options:
|
||||
- none
|
||||
- patch
|
||||
- minor
|
||||
- major
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build PluginLoader
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Print input
|
||||
run : |
|
||||
echo "release: ${{ github.event.inputs.release }}\n"
|
||||
echo "bump: ${{ github.event.inputs.bump }}\n"
|
||||
|
||||
- name: Checkout 🧰
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up NodeJS 18 💎
|
||||
uses: actions/setup-node@v3
|
||||
- name: Set up NodeJS 20 💎
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
|
||||
- name: Set up Python 3.10.2 🐍
|
||||
uses: actions/setup-python@v4
|
||||
- name: Set up Python 3.11.7 🐍
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.10.2"
|
||||
|
||||
python-version: "3.11.7"
|
||||
|
||||
- name: Install Poetry
|
||||
uses: snok/install-poetry@v1
|
||||
with:
|
||||
virtualenvs-create: false
|
||||
|
||||
- name: Install Python dependencies ⬇️
|
||||
working-directory: ./backend
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install pyinstaller==5.5
|
||||
[ -f requirements.txt ] && pip install -r requirements.txt
|
||||
poetry self add "poetry-dynamic-versioning[plugin]"
|
||||
poetry install --no-interaction
|
||||
|
||||
- name: Install JS dependencies ⬇️
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
npm i -g pnpm
|
||||
pnpm i --frozen-lockfile
|
||||
pnpm i --frozen-lockfile --dangerously-allow-all-builds
|
||||
|
||||
- name: Build JS Frontend 🛠️
|
||||
working-directory: ./frontend
|
||||
run: pnpm run build
|
||||
|
||||
- name: Build Python Backend 🛠️
|
||||
run: pyinstaller --noconfirm --onefile --name "PluginLoader" --add-data ./backend/static:/static --add-data ./backend/legacy:/legacy ./backend/*.py
|
||||
|
||||
working-directory: ./backend
|
||||
run: |
|
||||
poetry dynamic-versioning
|
||||
pyinstaller pyinstaller.spec
|
||||
|
||||
- name: Upload package artifact ⬆️
|
||||
if: ${{ !env.ACT }}
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: PluginLoader
|
||||
path: ./dist/PluginLoader
|
||||
path: ./backend/dist/PluginLoader
|
||||
|
||||
- name: Download package artifact locally
|
||||
if: ${{ env.ACT }}
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
path: ./dist/PluginLoader
|
||||
|
||||
release:
|
||||
name: Release stable version of the package
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.release == 'release' }}
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout 🧰
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install semver-tool asdf
|
||||
uses: asdf-vm/actions/install@v1
|
||||
with:
|
||||
tool_versions: |
|
||||
semver 3.3.0
|
||||
|
||||
- name: Fetch package artifact ⬇️
|
||||
uses: actions/download-artifact@v3
|
||||
if: ${{ !env.ACT }}
|
||||
with:
|
||||
name: PluginLoader
|
||||
path: dist
|
||||
|
||||
- name: Get latest release
|
||||
uses: rez0n/actions-github-release@main
|
||||
id: latest_release
|
||||
env:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
repository: "SteamDeckHomebrew/decky-loader"
|
||||
type: "nodraft"
|
||||
|
||||
- name: Prepare tag ⚙️
|
||||
id: ready_tag
|
||||
run: |
|
||||
export VERSION=${{ steps.latest_release.outputs.release }}
|
||||
echo "VERS: $VERSION"
|
||||
OUT="notsemver"
|
||||
if [[ "$VERSION" =~ "-pre" ]]; then
|
||||
printf "is prerelease, bumping to release\n"
|
||||
OUT=$(semver bump release "$VERSION")
|
||||
printf "OUT: ${OUT}\n"\
|
||||
printf "bumping by selected type.\n"
|
||||
if [[ "${{github.event.inputs.bump}}" != "none" ]]; then
|
||||
OUT=$(semver bump ${{github.event.inputs.bump}} "$OUT")
|
||||
printf "OUT: ${OUT}\n"
|
||||
else
|
||||
printf "no type selected, defaulting to patch.\n"
|
||||
OUT=$(semver bump patch "$OUT")
|
||||
printf "OUT: ${OUT}\n"
|
||||
fi
|
||||
elif [[ ! "$VERSION" =~ "-pre" ]]; then
|
||||
printf "previous tag is a release, bumping by selected type.\n"
|
||||
if [[ "${{github.event.inputs.bump}}" != "none" ]]; then
|
||||
OUT=$(semver bump ${{github.event.inputs.bump}} "$VERSION")
|
||||
printf "OUT: ${OUT}\n"
|
||||
else
|
||||
printf "previous tag is a release, but no bump selected. Defaulting to a patch bump.\n"
|
||||
OUT=$(semver bump patch "$VERSION")
|
||||
printf "OUT: ${OUT}\n"
|
||||
fi
|
||||
fi
|
||||
echo "vOUT: v$OUT"
|
||||
echo tag_name=v$OUT >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Push tag 📤
|
||||
uses: rickstaa/action-create-tag@v1.3.2
|
||||
if: ${{ steps.ready_tag.outputs.tag_name && github.event_name == 'workflow_dispatch' && !env.ACT }}
|
||||
with:
|
||||
tag: ${{ steps.ready_tag.outputs.tag_name }}
|
||||
message: Pre-release ${{ steps.ready_tag.outputs.tag_name }}
|
||||
|
||||
- name: Release 📦
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && !env.ACT }}
|
||||
with:
|
||||
name: Prerelease ${{ steps.ready_tag.outputs.tag_name }}
|
||||
tag_name: ${{ steps.ready_tag.outputs.tag_name }}
|
||||
files: ./dist/PluginLoader
|
||||
prerelease: false
|
||||
generate_release_notes: true
|
||||
|
||||
prerelease:
|
||||
name: Release the pre-release version of the package
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.release == 'prerelease' }}
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout 🧰
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install semver-tool asdf
|
||||
uses: asdf-vm/actions/install@v1
|
||||
with:
|
||||
tool_versions: |
|
||||
semver 3.3.0
|
||||
|
||||
- name: Fetch package artifact ⬇️
|
||||
uses: actions/download-artifact@v3
|
||||
if: ${{ !env.ACT }}
|
||||
with:
|
||||
name: PluginLoader
|
||||
path: dist
|
||||
|
||||
- name: Get latest release
|
||||
uses: rez0n/actions-github-release@main
|
||||
id: latest_release
|
||||
env:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
repository: "SteamDeckHomebrew/decky-loader"
|
||||
type: "nodraft"
|
||||
|
||||
- name: Prepare tag ⚙️
|
||||
id: ready_tag
|
||||
run: |
|
||||
export VERSION=${{ steps.latest_release.outputs.release }}
|
||||
echo "VERS: $VERSION"
|
||||
OUT=""
|
||||
if [[ ! "$VERSION" =~ "-pre" ]]; then
|
||||
printf "pre-release from release, bumping by selected type and prerel\n"
|
||||
if [[ ! ${{ github.event.inputs.bump }} == "none" ]]; then
|
||||
OUT=$(semver bump ${{github.event.inputs.bump}} "$VERSION")
|
||||
printf "OUT: ${OUT}\n"
|
||||
else
|
||||
printf "type not selected, defaulting to patch\n"
|
||||
OUT=$(semver bump patch "$VERSION")
|
||||
printf "OUT: ${OUT}\n"
|
||||
fi
|
||||
OUT="$OUT-pre"
|
||||
OUT=$(semver bump prerel "$OUT")
|
||||
printf "OUT: ${OUT}\n"
|
||||
elif [[ "$VERSION" =~ "-pre" ]]; then
|
||||
printf "pre-release to pre-release, bumping by selected type and or prerel version\n"
|
||||
if [[ ! ${{ github.event.inputs.bump }} == "none" ]]; then
|
||||
OUT=$(semver bump ${{github.event.inputs.bump}} "$VERSION")
|
||||
printf "OUT: ${OUT}\n"
|
||||
OUT="$OUT-pre"
|
||||
printf "OUT: ${OUT}\n"
|
||||
printf "bumping prerel\n"
|
||||
OUT=$(semver bump prerel "$OUT")
|
||||
printf "OUT: ${OUT}\n"
|
||||
else
|
||||
printf "type not selected, defaulting to new pre-release only\n"
|
||||
printf "bumping prerel\n"
|
||||
OUT=$(semver bump prerel "$VERSION")
|
||||
printf "OUT: ${OUT}\n"
|
||||
fi
|
||||
fi
|
||||
printf "vOUT: v${OUT}\n"
|
||||
echo tag_name=v$OUT >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Push tag 📤
|
||||
uses: rickstaa/action-create-tag@v1.3.2
|
||||
if: ${{ steps.ready_tag.outputs.tag_name && github.event_name == 'workflow_dispatch' && !env.ACT }}
|
||||
with:
|
||||
tag: ${{ steps.ready_tag.outputs.tag_name }}
|
||||
message: Pre-release ${{ steps.ready_tag.outputs.tag_name }}
|
||||
|
||||
- name: Release 📦
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && !env.ACT }}
|
||||
with:
|
||||
name: Prerelease ${{ steps.ready_tag.outputs.tag_name }}
|
||||
tag_name: ${{ steps.ready_tag.outputs.tag_name }}
|
||||
files: ./dist/PluginLoader
|
||||
prerelease: true
|
||||
generate_release_notes: true
|
||||
path: ./backend/dist/PluginLoader
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
name: Push Updated Plugin Stub to Template
|
||||
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
copy-stub:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@8230315d06ad95c617244d2f265d237a1682d445
|
||||
with:
|
||||
ref: ${{ github.sha }}
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@531f5f7d163941f0c1c04e0ff4d8bb243ac4366f
|
||||
with:
|
||||
separator: ","
|
||||
files: |
|
||||
backend/decky_loader/plugin/imports/decky.pyi
|
||||
|
||||
- name: Is stub changed
|
||||
id: changed-stub
|
||||
run: |
|
||||
STUB_CHANGED="false"
|
||||
PATHS=(backend backend/decky_loader/plugin/imports/decky.pyi)
|
||||
SHA=${{ github.sha }}
|
||||
SHA_PREV=HEAD^
|
||||
FILES=$(git diff $SHA_PREV..$SHA --name-only -- ${PATHS[@]} | jq -Rsc 'split("\n")[:-1] | join (",")')
|
||||
if [[ "$FILES" == *"backend/decky_loader/plugin/imports/decky.pyi"* ]]; then
|
||||
STUB_CHANGED="true"
|
||||
echo "Stub has changed, pushing updated stub"
|
||||
else
|
||||
echo "Stub has not changed, exiting."
|
||||
echo "has_changed=$STUB_CHANGED" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
echo "has_changed=$STUB_CHANGED" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Push updated stub
|
||||
if: github.ref == 'refs/heads/main' && steps.changed-stub.outputs.has_changed == true
|
||||
uses: dmnemec/copy_file_to_another_repo_action@bbebd3da22e4a37d04dca5f782edd5201cb97083
|
||||
env:
|
||||
API_TOKEN_GITHUB: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
source_file: 'backend/decky_loader/plugin/imports/decky.pyi'
|
||||
destination_repo: 'SteamDeckHomebrew/decky-plugin-template'
|
||||
user_email: '11465594+TrainDoctor@users.noreply.github.com'
|
||||
user_name: 'TrainDoctor'
|
||||
commit_message: 'Updated template with latest plugin stub changes'
|
||||
@@ -0,0 +1,23 @@
|
||||
name: Lint
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Run linters
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4 # Check out the repository first.
|
||||
|
||||
- name: Install TypeScript dependencies
|
||||
working-directory: frontend
|
||||
run: |
|
||||
npm i -g pnpm
|
||||
pnpm i --frozen-lockfile --dangerously-allow-all-builds
|
||||
|
||||
- name: Run prettier (TypeScript)
|
||||
working-directory: frontend
|
||||
run: pnpm run lint
|
||||
@@ -0,0 +1,156 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release:
|
||||
type: choice
|
||||
description: Release the asset
|
||||
default: 'none'
|
||||
options:
|
||||
- none
|
||||
- prerelease
|
||||
- release
|
||||
bump:
|
||||
type: choice
|
||||
description: Semver to bump
|
||||
default: 'none'
|
||||
options:
|
||||
- none
|
||||
- patch
|
||||
- minor
|
||||
- major
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
create_tag:
|
||||
name: Tag a new version of the package
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
tag_name: ${{ steps.ready_tag.outputs.tag_name }}
|
||||
|
||||
steps:
|
||||
- name: Checkout 🧰
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install semver-tool asdf
|
||||
uses: asdf-vm/actions/install@v4
|
||||
with:
|
||||
tool_versions: |
|
||||
semver 3.4.0
|
||||
|
||||
- name: Get latest release
|
||||
uses: rez0n/actions-github-release@main
|
||||
id: latest_release
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
repository: "SteamDeckHomebrew/decky-loader"
|
||||
type: "nodraft"
|
||||
|
||||
- name: Prepare tag ⚙️
|
||||
id: ready_tag
|
||||
run: |
|
||||
export VERSION=${{ steps.latest_release.outputs.release }}
|
||||
echo "VERS: $VERSION"
|
||||
if [[ ${{github.event.inputs.release}} == "release" ]]; then
|
||||
OUT="notsemver"
|
||||
if [[ "$VERSION" =~ "-pre" ]]; then
|
||||
printf "is prerelease, bumping to release\n"
|
||||
OUT=$(semver bump release "$VERSION")
|
||||
printf "OUT: ${OUT}\n"\
|
||||
printf "bumping by selected type.\n"
|
||||
if [[ "${{github.event.inputs.bump}}" != "none" ]]; then
|
||||
OUT=$(semver bump ${{github.event.inputs.bump}} "$OUT")
|
||||
printf "OUT: ${OUT}\n"
|
||||
else
|
||||
printf "no type selected, not bumping for release.\n"
|
||||
fi
|
||||
elif [[ ! "$VERSION" =~ "-pre" ]]; then
|
||||
printf "previous tag is a release, bumping by selected type.\n"
|
||||
if [[ "${{github.event.inputs.bump}}" != "none" ]]; then
|
||||
OUT=$(semver bump ${{github.event.inputs.bump}} "$VERSION")
|
||||
printf "OUT: ${OUT}\n"
|
||||
else
|
||||
printf "previous tag is a release, but no bump selected. Defaulting to a patch bump.\n"
|
||||
OUT=$(semver bump patch "$VERSION")
|
||||
printf "OUT: ${OUT}\n"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
OUT=""
|
||||
if [[ ! "$VERSION" =~ "-pre" ]]; then
|
||||
printf "pre-release from release, bumping by selected type and prerel\n"
|
||||
if [[ ! ${{ github.event.inputs.bump }} == "none" ]]; then
|
||||
OUT=$(semver bump ${{github.event.inputs.bump}} "$VERSION")
|
||||
printf "OUT: ${OUT}\n"
|
||||
else
|
||||
printf "type not selected, defaulting to patch\n"
|
||||
OUT=$(semver bump patch "$VERSION")
|
||||
printf "OUT: ${OUT}\n"
|
||||
fi
|
||||
OUT="$OUT-pre"
|
||||
OUT=$(semver bump prerel "$OUT")
|
||||
printf "OUT: ${OUT}\n"
|
||||
elif [[ "$VERSION" =~ "-pre" ]]; then
|
||||
printf "pre-release to pre-release, bumping by selected type and or prerel version\n"
|
||||
if [[ ! ${{ github.event.inputs.bump }} == "none" ]]; then
|
||||
OUT=$(semver bump ${{github.event.inputs.bump}} "$VERSION")
|
||||
printf "OUT: ${OUT}\n"
|
||||
OUT="$OUT-pre"
|
||||
printf "OUT: ${OUT}\n"
|
||||
printf "bumping prerel\n"
|
||||
OUT=$(semver bump prerel "$OUT")
|
||||
printf "OUT: ${OUT}\n"
|
||||
else
|
||||
printf "type not selected, defaulting to new pre-release only\n"
|
||||
printf "bumping prerel\n"
|
||||
OUT=$(semver bump prerel "$VERSION")
|
||||
printf "OUT: ${OUT}\n"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
echo "vOUT: v${OUT}"
|
||||
echo tag_name=v$OUT >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Push tag 📤
|
||||
uses: rickstaa/action-create-tag@v1.3.2
|
||||
if: ${{ steps.ready_tag.outputs.tag_name && !env.ACT }}
|
||||
with:
|
||||
tag: ${{ steps.ready_tag.outputs.tag_name }}
|
||||
message: Pre-release ${{ steps.ready_tag.outputs.tag_name }}
|
||||
|
||||
build:
|
||||
name: Build tagged artifact
|
||||
uses: ./.github/workflows/build.yml
|
||||
needs: [create_tag]
|
||||
|
||||
release:
|
||||
name: Release tagged artifact
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [create_tag, build]
|
||||
steps:
|
||||
- name: Fetch package artifact ⬇️
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: PluginLoader
|
||||
|
||||
- name: Pre-release 📦
|
||||
if: github.event.inputs.release == 'prerelease'
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
name: Prerelease ${{ needs.create_tag.outputs.tag_name }}
|
||||
tag_name: ${{ needs.create_tag.outputs.tag_name }}
|
||||
files: ./PluginLoader
|
||||
prerelease: true
|
||||
generate_release_notes: true
|
||||
- name: Release 📦
|
||||
if: github.event.inputs.release == 'release'
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
name: Release ${{ needs.create_tag.outputs.tag_name }}
|
||||
tag_name: ${{ needs.create_tag.outputs.tag_name }}
|
||||
files: ./PluginLoader
|
||||
prerelease: false
|
||||
generate_release_notes: true
|
||||
@@ -0,0 +1,49 @@
|
||||
name: Type Check
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
typecheck:
|
||||
name: Run type checkers
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4 # Check out the repository first.
|
||||
|
||||
- name: Set up NodeJS 20 💎
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Set up Python 3.11.7 🐍
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11.7"
|
||||
|
||||
- name: Install Poetry
|
||||
uses: snok/install-poetry@v1
|
||||
with:
|
||||
virtualenvs-create: false
|
||||
|
||||
- name: Install Python dependencies ⬇️
|
||||
working-directory: backend
|
||||
run: poetry install --no-interaction
|
||||
|
||||
- name: Install TypeScript dependencies
|
||||
working-directory: frontend
|
||||
run: |
|
||||
npm i -g pnpm
|
||||
pnpm i --frozen-lockfile --dangerously-allow-all-builds
|
||||
|
||||
- name: Run pyright (Python)
|
||||
uses: jakebailey/pyright-action@v1
|
||||
with:
|
||||
python-version: "3.10.6"
|
||||
no-comments: true
|
||||
working-directory: backend
|
||||
|
||||
- name: Run tsc (TypeScript)
|
||||
working-directory: frontend
|
||||
run: pnpm run typecheck
|
||||
+7
-4
@@ -29,7 +29,7 @@ MANIFEST
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
backend/dist/
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
@@ -126,6 +126,7 @@ venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
.direnv/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
@@ -152,13 +153,15 @@ dmypy.json
|
||||
cython_debug/
|
||||
|
||||
# static files are built
|
||||
backend/static
|
||||
backend/decky_loader/static
|
||||
|
||||
# ignore settings.json
|
||||
# prevents leaking login details
|
||||
.vscode/settings.json
|
||||
|
||||
# plugins folder for local launches
|
||||
plugins/*
|
||||
/plugins/*
|
||||
act/.directory
|
||||
act/artifacts/*
|
||||
act/artifacts/*
|
||||
bin/act
|
||||
/settings/
|
||||
|
||||
Vendored
+2
-1
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"deckip" : "0.0.0.0",
|
||||
"deckport" : "22",
|
||||
"deckuser" : "deck",
|
||||
"deckpass" : "ssap",
|
||||
"deckkey" : "-i ${env:HOME}/.ssh/id_rsa",
|
||||
"deckdir" : "/home/deck"
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+58
-11
@@ -14,7 +14,9 @@
|
||||
"label": "localrun",
|
||||
"type": "shell",
|
||||
"group": "none",
|
||||
"dependsOn" : ["buildall"],
|
||||
"dependsOn": [
|
||||
"buildall"
|
||||
],
|
||||
"detail": "Check for local runs, create a plugins folder",
|
||||
"command": "mkdir -p plugins",
|
||||
"problemMatcher": []
|
||||
@@ -35,8 +37,19 @@
|
||||
"label": "dependencies",
|
||||
"type": "shell",
|
||||
"group": "none",
|
||||
"dependsOn": [
|
||||
"deploy"
|
||||
],
|
||||
"detail": "Check for local runs, create a plugins folder",
|
||||
"command": "rsync -azp --rsh='ssh -p ${config:deckport} ${config:deckkey}' requirements.txt deck@${config:deckip}:${config:deckdir}/homebrew/dev/pluginloader/requirements.txt && ssh deck@${config:deckip} -p ${config:deckport} ${config:deckkey} 'python -m ensurepip && python -m pip install --upgrade pip && python -m pip install --upgrade setuptools && python -m pip install -r ${config:deckdir}/homebrew/dev/pluginloader/requirements.txt'",
|
||||
"command": "ssh ${config:deckuser}@${config:deckip} -p ${config:deckport} ${config:deckkey} 'python -m ensurepip --root / && python -m pip install --user --break-system-packages --upgrade poetry && cd ${config:deckdir}/homebrew/dev/pluginloader/backend && python -m poetry install'",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "extracttext",
|
||||
"type": "shell",
|
||||
"group": "none",
|
||||
"detail": "Check for new strings in the frontend source code and extract it into the corresponding json language files",
|
||||
"command": "cd frontend && ./node_modules/.bin/i18next --config ./i18next-parser.config.mjs",
|
||||
"problemMatcher": []
|
||||
},
|
||||
// BUILD
|
||||
@@ -48,6 +61,16 @@
|
||||
"command": "cd frontend && pnpm i",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"script": "watch",
|
||||
"type": "npm",
|
||||
"path": "frontend",
|
||||
"group": "build",
|
||||
"problemMatcher": [],
|
||||
"label": "watchfrontend",
|
||||
"detail": "rollup -c -w",
|
||||
"isBackground": true
|
||||
},
|
||||
{
|
||||
"label": "buildfrontend",
|
||||
"type": "npm",
|
||||
@@ -55,8 +78,7 @@
|
||||
"detail": "rollup -c",
|
||||
"script": "build",
|
||||
"path": "frontend",
|
||||
"problemMatcher": [],
|
||||
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "buildall",
|
||||
@@ -78,7 +100,7 @@
|
||||
"dependsOn": [
|
||||
"checkforsettings"
|
||||
],
|
||||
"command": "ssh deck@${config:deckip} -p ${config:deckport} ${config:deckkey} 'mkdir -p ${config:deckdir}/homebrew/dev/pluginloader && mkdir -p ${config:deckdir}/homebrew/dev/plugins'",
|
||||
"command": "ssh ${config:deckuser}@${config:deckip} -p ${config:deckport} ${config:deckkey} 'mkdir -p ${config:deckdir}/homebrew/dev/pluginloader && mkdir -p ${config:deckdir}/homebrew/plugins'",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
@@ -86,7 +108,7 @@
|
||||
"detail": "Deploy dev PluginLoader to deck",
|
||||
"type": "shell",
|
||||
"group": "none",
|
||||
"command": "rsync -azp --delete --rsh='ssh -p ${config:deckport} ${config:deckkey}' --exclude='.git/' --exclude='.github/' --exclude='.vscode/' --exclude='frontend/' --exclude='dist/' --exclude='contrib/' --exclude='*.log' --exclude='requirements.txt' --exclude='backend/__pycache__/' --exclude='.gitignore' . deck@${config:deckip}:${config:deckdir}/homebrew/dev/pluginloader",
|
||||
"command": "rsync -azp --delete --force --rsh='ssh -p ${config:deckport} ${config:deckkey}' --exclude='.git/' --exclude='.github/' --exclude='.vscode/' --exclude='frontend/' --exclude='dist/' --exclude='contrib/' --exclude='*.log' --exclude='**/__pycache__/' --exclude='.gitignore' . ${config:deckuser}@${config:deckip}:${config:deckdir}/homebrew/dev/pluginloader",
|
||||
"problemMatcher": []
|
||||
},
|
||||
// RUN
|
||||
@@ -95,8 +117,10 @@
|
||||
"detail": "Run indev PluginLoader on Deck",
|
||||
"type": "shell",
|
||||
"group": "none",
|
||||
"dependsOn" : ["checkforsettings"],
|
||||
"command": "ssh deck@${config:deckip} -p ${config:deckport} ${config:deckkey} 'export PLUGIN_PATH=${config:deckdir}/homebrew/dev/plugins; export CHOWN_PLUGIN_PATH=0; export LOG_LEVEL=DEBUG; cd ${config:deckdir}/homebrew/services; echo '${config:deckpass}' | sudo -SE python3 ${config:deckdir}/homebrew/dev/pluginloader/backend/main.py'",
|
||||
"dependsOn": [
|
||||
"checkforsettings"
|
||||
],
|
||||
"command": "ssh -t ${config:deckuser}@${config:deckip} -p ${config:deckport} ${config:deckkey} 'export PATH=${config:deckdir}/.local/bin:$PATH; export PLUGIN_PATH=${config:deckdir}/homebrew/plugins; export CHOWN_PLUGIN_PATH=0; export LOG_LEVEL=DEBUG; cd ${config:deckdir}/homebrew/dev/pluginloader/backend; echo '${config:deckpass}' | poetry run sh -c \"cd ${config:deckdir}/homebrew/services; sudo -SE env \"PATH=\\$PATH\" python3 ${config:deckdir}/homebrew/dev/pluginloader/backend/main.py\"'",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
@@ -108,6 +132,20 @@
|
||||
"problemMatcher": []
|
||||
},
|
||||
// ALL-IN-ONES
|
||||
{
|
||||
"label": "deployandrun",
|
||||
"detail": "Deploy and run, skipping JS build. Useful when combined with npm:watch",
|
||||
"dependsOrder": "sequence",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"dependsOn": [
|
||||
"deploy",
|
||||
"runpydeck"
|
||||
],
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "updateremote",
|
||||
"detail": "Build and deploy",
|
||||
@@ -115,7 +153,7 @@
|
||||
"group": "none",
|
||||
"dependsOn": [
|
||||
"buildall",
|
||||
"deploy",
|
||||
"deploy"
|
||||
],
|
||||
"problemMatcher": []
|
||||
},
|
||||
@@ -146,10 +184,19 @@
|
||||
"buildall",
|
||||
"createfolders",
|
||||
"dependencies",
|
||||
"deploy",
|
||||
// dependencies runs deploy already
|
||||
// "deploy",
|
||||
"runpydeck"
|
||||
],
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "act",
|
||||
"type": "shell",
|
||||
"group": "none",
|
||||
"detail": "Build release artifact using local CI",
|
||||
"command": "./act/run-act.sh release",
|
||||
"problemMatcher": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,37 @@
|
||||
<h1 align="center">
|
||||
<a name="logo" href="https://deckbrew.xyz/"><img src="https://deckbrew.xyz/logo.png" alt="Deckbrew logo" width="200"></a>
|
||||
<a name="logo" href="https://deckbrew.xyz/"><img src="https://deckbrew.xyz/static/icon-45ca1f5aea376a9ad37e92db906f283e.png" alt="Deckbrew logo" width="200"></a>
|
||||
<br>
|
||||
Decky Loader
|
||||
<br>
|
||||
<a name="download button" href="https://github.com/SteamDeckHomebrew/decky-installer/releases/latest/download/decky_installer.desktop"><img src="./docs/images/download_button.svg" alt="Download decky" width="150px" style="padding-top: 15px;"></a>
|
||||
</h1>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/SteamDeckHomebrew/decky-loader/releases"><img src="https://img.shields.io/github/downloads/SteamDeckHomebrew/decky-loader/total" /></a>
|
||||
<a href="https://github.com/SteamDeckHomebrew/decky-loader/stargazers"><img src="https://img.shields.io/github/stars/SteamDeckHomebrew/decky-loader" /></a>
|
||||
<a href="https://github.com/SteamDeckHomebrew/decky-loader/commits/main"><img src="https://img.shields.io/github/last-commit/SteamDeckHomebrew/decky-loader.svg" /></a>
|
||||
<a href="https://weblate.werwolv.net/engage/decky/"><img src="https://weblate.werwolv.net/widgets/decky/-/decky/svg-badge.svg" alt="Translation status" /></a>
|
||||
<a href="https://github.com/SteamDeckHomebrew/decky-loader/blob/main/LICENSE"><img src="https://img.shields.io/github/license/SteamDeckHomebrew/decky-loader" /></a>
|
||||
<a href="https://discord.gg/ZU74G2NJzk"><img src="https://img.shields.io/discord/960281551428522045?color=%235865F2&label=discord" /></a>
|
||||
<a href="https://deckbrew.xyz/discord"><img src="https://img.shields.io/discord/960281551428522045?color=%235865F2&label=discord" /></a>
|
||||
<br>
|
||||
<br>
|
||||
<img src="https://media.discordapp.net/attachments/966017112244125756/1012466063893610506/main.jpg" alt="Decky screenshot" width="80%">
|
||||
<!-- <img src="https://media.discordapp.net/attachments/966017112244125756/1012466063893610506/main.jpg" alt="Decky screenshot" width="80%">-->
|
||||
</p>
|
||||
|
||||
## 🩵 Backers and Sponsors
|
||||
|
||||
[Become a backer or sponsor](https://opencollective.com/steamdeckhomebrew) to support our work! Contributing to our collective effort will help Decky Loader developers cover the costs of web servers, acquire new development hardware, and more.
|
||||
|
||||
<!-- SPONSORS COMMENTED OUT UNTIL WE GET SOME SPONSORS TO AVOID BLANK SVG SPACE -->
|
||||
<a href="https://opencollective.com/steamdeckhomebrew"><img alt="Steam Deck Homebrew sponsors on Open Collective" src="https://opencollective.com/steamdeckhomebrew/sponsors.svg?button=true&avatarHeight=46&width=750"></a>
|
||||
|
||||
<a href="https://opencollective.com/steamdeckhomebrew"><img alt="Steam Deck Homebrew backers on Open Collective" src="https://opencollective.com/steamdeckhomebrew/backers.svg?button=false&avatarHeight=46&width=750"></a>
|
||||
|
||||
## 📖 About
|
||||
|
||||
Decky Loader is a homebrew plugin launcher for the Steam Deck. It can be used to [stylize your menus](https://github.com/suchmememanyskill/SDH-CssLoader), [change system sounds](https://github.com/EMERALD0874/SDH-AudioLoader), [adjust your screen saturation](https://github.com/libvibrant/vibrantDeck), [change additional system settings](https://github.com/NGnius/PowerTools), and [more](https://plugins.deckbrew.xyz/).
|
||||
|
||||
For more information about Decky Loader as well as documentation and development tools, please visit [our wiki](https://deckbrew.xyz).
|
||||
For more information about Decky Loader as well as documentation and development tools, please visit [our wiki](https://wiki.deckbrew.xyz).
|
||||
|
||||
### 🎨 Features
|
||||
|
||||
@@ -31,41 +43,44 @@ For more information about Decky Loader as well as documentation and development
|
||||
|
||||
### 🤔 Common Issues
|
||||
|
||||
- Crankshaft is incompatible with Decky Loader. If you are using Crankshaft, please uninstall it before installing Decky Loader.
|
||||
- Syncthing may use port 8080 on Steam Deck, which Decky Loader needs to function. If you are using Syncthing as a service, please change its port to something else.
|
||||
- 8384 is the recommended port for Syncthing.
|
||||
- If you are using any software that uses port 1337 or 8080, please change its port to something else or uninstall it.
|
||||
- Sometimes Decky will disappear on SteamOS updates. This can easily be fixed by just re-running the installer and installing the stable branch again. If this doesn't work, try installing the prerelease instead. If that doesn't work, then [check the existing issues](https://github.com/SteamDeckHomebrew/decky-loader/issues) and if there isn't one then you can [file a new issue](https://github.com/SteamDeckHomebrew/decky-loader/issues/new?assignees=&labels=bug&template=bug_report.yml&title=%5BBUG%5D+%3Ctitle%3E).
|
||||
|
||||
## 💾 Installation
|
||||
|
||||
1. Press the <img src="./docs/images/light/steam.svg#gh-dark-mode-only" height=16><img src="./docs/images/dark/steam.svg#gh-light-mode-only" height=16> button and open the Settings menu.
|
||||
1. Navigate to the System menu and scroll to the System Settings. Toggle "Enable Developer Mode" so it is enabled.
|
||||
1. Navigate to the Developer menu and scroll to Miscellaneous. Toggle "CEF Remote Debugging" so it is enabled.
|
||||
1. Select "Restart Now" to apply your changes.
|
||||
- This installation can be done without an admin/sudo password set.
|
||||
|
||||
1. Prepare a mouse and keyboard if possible.
|
||||
- Keyboards and mice can be connected to the Steam Deck via USB-C or Bluetooth.
|
||||
- Many Bluetooth keyboard and mouse apps are available for iOS and Android.
|
||||
- Many Bluetooth keyboard and mouse apps are available for iOS and Android. (KDE connect is preinstalled on the steam deck)
|
||||
- The Steam Link app is available on [Windows](https://media.steampowered.com/steamlink/windows/latest/SteamLink.zip), [macOS](https://apps.apple.com/us/app/steam-link/id1246969117), and [Linux](https://flathub.org/apps/details/com.valvesoftware.SteamLink). It works well as a remote desktop substitute.
|
||||
- If you have no other options, use the right trackpad as a mouse and press <img src="./docs/images/light/steam.svg#gh-dark-mode-only" height=16><img src="./docs/images/dark/steam.svg#gh-light-mode-only" height=16>+<img src="./docs/images/light/x.svg#gh-dark-mode-only" height=16><img src="./docs/images/dark/x.svg#gh-light-mode-only" height=16> to open the on-screen keyboard as needed.
|
||||
1. Press the <img src="./docs/images/light/steam.svg#gh-dark-mode-only" height=16><img src="./docs/images/dark/steam.svg#gh-light-mode-only" height=16> button and open the Power menu.
|
||||
1. Select "Switch to Desktop".
|
||||
1. Open the Konsole app and enter the command `passwd`. You can skip this step if you have already created a sudo password using this command. ([YouTube Guide](https://www.youtube.com/watch?v=1vOMYGj22rQ))
|
||||
1. You will be prompted to create a password. Your text will not be visible. After you press enter, you will need to type your password again to confirm.
|
||||
1. Choose the version of Decky Loader you want to install and paste the following command into the Konsole app.
|
||||
1. Navigate to this Github page on a browser of your choice.
|
||||
1. Download the [installer file](https://github.com/SteamDeckHomebrew/decky-installer/releases/latest/download/decky_installer.desktop). (If using firefox, it will be named `decky_installer.desktop.download`. Rename it to `decky_installer.desktop` before running it)
|
||||
1. Drag the file onto your desktop and double click it to run it.
|
||||
1. Either type your admin password or allow Decky to temporarily set your admin password to `Decky!` (this password will be removed after the installer finishes)
|
||||
1. Choose the version of Decky Loader you want to install.
|
||||
- **Latest Release**
|
||||
Intended for most users. This is the latest stable version of Decky Loader.
|
||||
`curl -L https://github.com/SteamDeckHomebrew/decky-loader/raw/main/dist/install_release.sh | sh`
|
||||
Intended for most users. This is the latest stable version of Decky Loader.
|
||||
- **Latest Pre-Release**
|
||||
Intended for plugin developers. Pre-releases are unlikely to be fully stable but contain the latest changes. For more information on plugin development, please consult [the wiki page](https://deckbrew.xyz/en/loader-dev/development).
|
||||
`curl -L https://github.com/SteamDeckHomebrew/decky-loader/raw/main/dist/install_prerelease.sh | sh`
|
||||
Intended for plugin developers. Pre-releases are unlikely to be fully stable but contain the latest changes. For more information on plugin development, please consult [the wiki page](https://wiki.deckbrew.xyz/en/loader-dev/development).
|
||||
1. Open the Return to Gaming Mode shortcut on your desktop.
|
||||
|
||||
- There is also a fast install for those who can use Konsole. Run `curl -L https://github.com/SteamDeckHomebrew/decky-installer/releases/latest/download/install_release.sh | sh` and type your password when prompted.
|
||||
|
||||
### 👋 Uninstallation
|
||||
|
||||
We are sorry to see you go! If you are considering uninstalling because you are having issues, please consider [opening an issue](https://github.com/SteamDeckHomebrew/decky-loader/issues) or [joining our Discord](https://discord.gg/ZU74G2NJzk) so we can help you and other users.
|
||||
We are sorry to see you go! If you are considering uninstalling because you are having issues, please consider [opening an issue](https://github.com/SteamDeckHomebrew/decky-loader/issues) or [joining our Discord](https://deckbrew.xyz/discord) so we can help you and other users.
|
||||
|
||||
1. Press the <img src="./docs/images/light/steam.svg#gh-dark-mode-only" height=16><img src="./docs/images/dark/steam.svg#gh-light-mode-only" height=16> button and open the Power menu.
|
||||
1. Select "Switch to Desktop".
|
||||
1. Open the Konsole app and run `curl -L https://github.com/SteamDeckHomebrew/decky-loader/raw/main/dist/uninstall.sh | sh`.
|
||||
1. Run the installer file again, and select `uninstall decky loader`.
|
||||
|
||||
- There is also a fast uninstall for those who can use Konsole. Run `curl -L https://github.com/SteamDeckHomebrew/decky-installer/releases/latest/download/uninstall.sh | sh` and type your password when prompted.
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
@@ -82,16 +97,17 @@ Now that you have Decky Loader installed, you can start using plugins. Each plug
|
||||
|
||||
### 🛠️ Plugin Development
|
||||
|
||||
There is no complete plugin development documentation yet. However a good starting point is the [plugin template repository](https://github.com/SteamDeckHomebrew/decky-plugin-template). Consider [joining our Discord](https://discord.gg/ZU74G2NJzk) if you have any questions.
|
||||
There is no complete plugin development documentation yet. However a good starting point is the [plugin template repository](https://github.com/SteamDeckHomebrew/decky-plugin-template). Consider [joining our Discord](https://deckbrew.xyz/discord) if you have any questions.
|
||||
|
||||
### 🤝 Contributing
|
||||
|
||||
Please consult [the wiki page regarding development](https://deckbrew.xyz/en/loader-dev/development) for more information on installing development versions of Decky Loader. You can also install the Steam Deck UI on a Windows or Linux computer for testing by following [this YouTube guide](https://youtu.be/1IAbZte8e7E?t=112).
|
||||
Please consult [the wiki page regarding development](https://wiki.deckbrew.xyz/en/loader-dev/development) for more information on installing development versions of Decky Loader. You can also install the Steam Deck UI on a Windows or Linux computer for testing by following [this YouTube guide](https://youtu.be/1IAbZte8e7E?t=112).
|
||||
|
||||
1. Clone the repository using the latest commit to main before starting your PR.
|
||||
1. In your clone of the repository, run these commands.
|
||||
```bash
|
||||
pnpm i
|
||||
cd frontend
|
||||
pnpm i # NOTE: you may need to approve esbuild's build script with pnpm approve-builds
|
||||
pnpm run build
|
||||
```
|
||||
1. If you are modifying the UI, these commands will need to be run before deploying the changes to your Steam Deck.
|
||||
@@ -101,7 +117,7 @@ Please consult [the wiki page regarding development](https://deckbrew.xyz/en/loa
|
||||
⚠️ If you are recieving build errors due to an out of date library, you should run this command inside of your repository.
|
||||
|
||||
```bash
|
||||
pnpm update decky-frontend-lib --latest
|
||||
pnpm update @decky/ui --latest
|
||||
```
|
||||
|
||||
Source control and deploying plugins are left to each respective contributor for the cloned repos in order to keep dependencies up to date.
|
||||
|
||||
+23
-20
@@ -1,44 +1,47 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
type=$1
|
||||
# bump=$2
|
||||
|
||||
oldartifactsdir="old"
|
||||
|
||||
parent_path=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P )
|
||||
cd "$parent_path"
|
||||
parent_path=$( cd "$(dirname "${BASH_SOURCE[0]}")" || exit ; pwd -P )
|
||||
cd "$parent_path" || exit
|
||||
|
||||
artifactfolders=$(find artifacts/ -maxdepth 1 -mindepth 1 -type d)
|
||||
if [[ ${#artifactfolders[@]} > 0 ]]; then
|
||||
for i in ${artifactfolders[@]}; do
|
||||
foldername=$(dirname $i)
|
||||
subfoldername=$(basename $i)
|
||||
out=$foldername/$oldartifactsdir/$subfoldername-$(date +'%s')
|
||||
if [[ ! "$subfoldername" =~ "$oldartifactsdir" ]]; then
|
||||
mkdir -p $out
|
||||
mv $i $out
|
||||
printf "Moved "${foldername}"/"${subfoldername}" to "${out}" \n"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
for i in artifacts/*; do
|
||||
if [[ ! -d "$i" ]]; then
|
||||
continue;
|
||||
fi
|
||||
subfoldername=$(basename "$i")
|
||||
|
||||
if [[ "$subfoldername" == "$oldartifactsdir" ]]; then
|
||||
continue;
|
||||
fi
|
||||
|
||||
out=artifacts/$oldartifactsdir/$subfoldername-$(date +'%s')
|
||||
mkdir -p "$out"
|
||||
mv "$i" "$out"
|
||||
echo "Moved artifacts/${subfoldername} to ${out}"
|
||||
done
|
||||
|
||||
cd ..
|
||||
|
||||
if [[ "$type" == "release" ]]; then
|
||||
printf "release!\n"
|
||||
act workflow_dispatch -e act/release.json --artifact-server-path act/artifacts
|
||||
act workflow_dispatch -e act/release.json --artifact-server-path act/artifacts --container-architecture linux/amd64 --platform ubuntu-22.04=catthehacker/ubuntu:act-22.04
|
||||
elif [[ "$type" == "prerelease" ]]; then
|
||||
printf "prerelease!\n"
|
||||
act workflow_dispatch -e act/prerelease.json --artifact-server-path act/artifacts
|
||||
act workflow_dispatch -e act/prerelease.json --artifact-server-path act/artifacts --container-architecture linux/amd64 --platform ubuntu-22.04=catthehacker/ubuntu:act-22.04
|
||||
else
|
||||
printf "Release type unspecified/badly specified.\n"
|
||||
printf "Options: 'release' or 'prerelease'\n"
|
||||
fi
|
||||
|
||||
cd act/artifacts
|
||||
cd act/artifacts || exit
|
||||
|
||||
if [[ -d "1" ]]; then
|
||||
cd "1/artifact"
|
||||
cd "1/artifact" || exit
|
||||
cp "PluginLoader.gz__" "PluginLoader.gz"
|
||||
gzip -d "PluginLoader.gz"
|
||||
chmod +x PluginLoader
|
||||
|
||||
@@ -1,184 +0,0 @@
|
||||
# Full imports
|
||||
import json
|
||||
|
||||
# Partial imports
|
||||
from aiohttp import ClientSession, web
|
||||
from asyncio import get_event_loop, sleep
|
||||
from concurrent.futures import ProcessPoolExecutor
|
||||
from hashlib import sha256
|
||||
from io import BytesIO
|
||||
from logging import getLogger
|
||||
from os import R_OK, W_OK, path, rename, listdir, access, mkdir
|
||||
from shutil import rmtree
|
||||
from subprocess import call
|
||||
from time import time
|
||||
from zipfile import ZipFile
|
||||
|
||||
# Local modules
|
||||
from helpers import get_ssl_context, get_user, get_user_group, download_remote_binary_to_path
|
||||
from injector import get_gamepadui_tab
|
||||
|
||||
logger = getLogger("Browser")
|
||||
|
||||
class PluginInstallContext:
|
||||
def __init__(self, artifact, name, version, hash) -> None:
|
||||
self.artifact = artifact
|
||||
self.name = name
|
||||
self.version = version
|
||||
self.hash = hash
|
||||
|
||||
class PluginBrowser:
|
||||
def __init__(self, plugin_path, plugins, loader) -> None:
|
||||
self.plugin_path = plugin_path
|
||||
self.plugins = plugins
|
||||
self.loader = loader
|
||||
self.install_requests = {}
|
||||
|
||||
def _unzip_to_plugin_dir(self, zip, name, hash):
|
||||
zip_hash = sha256(zip.getbuffer()).hexdigest()
|
||||
if hash and (zip_hash != hash):
|
||||
return False
|
||||
zip_file = ZipFile(zip)
|
||||
zip_file.extractall(self.plugin_path)
|
||||
plugin_dir = self.find_plugin_folder(name)
|
||||
code_chown = call(["chown", "-R", get_user()+":"+get_user_group(), plugin_dir])
|
||||
code_chmod = call(["chmod", "-R", "555", plugin_dir])
|
||||
if code_chown != 0 or code_chmod != 0:
|
||||
logger.error(f"chown/chmod exited with a non-zero exit code (chown: {code_chown}, chmod: {code_chmod})")
|
||||
return False
|
||||
return True
|
||||
|
||||
async def _download_remote_binaries_for_plugin_with_name(self, pluginBasePath):
|
||||
rv = False
|
||||
try:
|
||||
packageJsonPath = path.join(pluginBasePath, 'package.json')
|
||||
pluginBinPath = path.join(pluginBasePath, 'bin')
|
||||
|
||||
if access(packageJsonPath, R_OK):
|
||||
with open(packageJsonPath, 'r') as f:
|
||||
packageJson = json.load(f)
|
||||
if "remote_binary" in packageJson and len(packageJson["remote_binary"]) > 0:
|
||||
# create bin directory if needed.
|
||||
rc=call(["chmod", "-R", "777", pluginBasePath])
|
||||
if access(pluginBasePath, W_OK):
|
||||
|
||||
if not path.exists(pluginBinPath):
|
||||
mkdir(pluginBinPath)
|
||||
|
||||
if not access(pluginBinPath, W_OK):
|
||||
rc=call(["chmod", "-R", "777", pluginBinPath])
|
||||
|
||||
rv = True
|
||||
for remoteBinary in packageJson["remote_binary"]:
|
||||
# Required Fields. If any Remote Binary is missing these fail the install.
|
||||
binName = remoteBinary["name"]
|
||||
binURL = remoteBinary["url"]
|
||||
binHash = remoteBinary["sha256hash"]
|
||||
if not await download_remote_binary_to_path(binURL, binHash, path.join(pluginBinPath, binName)):
|
||||
rv = False
|
||||
raise Exception(f"Error Downloading Remote Binary {binName}@{binURL} with hash {binHash} to {path.join(pluginBinPath, binName)}")
|
||||
|
||||
code_chown = call(["chown", "-R", get_user()+":"+get_user_group(), self.plugin_path])
|
||||
rc=call(["chmod", "-R", "555", pluginBasePath])
|
||||
else:
|
||||
rv = True
|
||||
logger.debug(f"No Remote Binaries to Download")
|
||||
|
||||
except Exception as e:
|
||||
rv = False
|
||||
logger.debug(str(e))
|
||||
|
||||
return rv
|
||||
|
||||
def find_plugin_folder(self, name):
|
||||
for folder in listdir(self.plugin_path):
|
||||
try:
|
||||
with open(path.join(self.plugin_path, folder, 'plugin.json'), 'r') as f:
|
||||
plugin = json.load(f)
|
||||
|
||||
if plugin['name'] == name:
|
||||
return str(path.join(self.plugin_path, folder))
|
||||
except:
|
||||
logger.debug(f"skipping {folder}")
|
||||
|
||||
async def uninstall_plugin(self, name):
|
||||
if self.loader.watcher:
|
||||
self.loader.watcher.disabled = True
|
||||
tab = await get_gamepadui_tab()
|
||||
try:
|
||||
logger.info("uninstalling " + name)
|
||||
logger.info(" at dir " + self.find_plugin_folder(name))
|
||||
logger.debug("unloading %s" % str(name))
|
||||
await tab.evaluate_js(f"DeckyPluginLoader.unloadPlugin('{name}')")
|
||||
if self.plugins[name]:
|
||||
self.plugins[name].stop()
|
||||
del self.plugins[name]
|
||||
logger.debug("removing files %s" % str(name))
|
||||
rmtree(self.find_plugin_folder(name))
|
||||
except FileNotFoundError:
|
||||
logger.warning(f"Plugin {name} not installed, skipping uninstallation")
|
||||
except Exception as e:
|
||||
logger.error(f"Plugin {name} in {self.find_plugin_folder(name)} was not uninstalled")
|
||||
logger.error(f"Error at %s", exc_info=e)
|
||||
if self.loader.watcher:
|
||||
self.loader.watcher.disabled = False
|
||||
|
||||
async def _install(self, artifact, name, version, hash):
|
||||
isInstalled = False
|
||||
if self.loader.watcher:
|
||||
self.loader.watcher.disabled = True
|
||||
try:
|
||||
pluginFolderPath = self.find_plugin_folder(name)
|
||||
if pluginFolderPath:
|
||||
isInstalled = True
|
||||
except:
|
||||
logger.error(f"Failed to determine if {name} is already installed, continuing anyway.")
|
||||
logger.info(f"Installing {name} (Version: {version})")
|
||||
async with ClientSession() as client:
|
||||
logger.debug(f"Fetching {artifact}")
|
||||
res = await client.get(artifact, ssl=get_ssl_context())
|
||||
if res.status == 200:
|
||||
logger.debug("Got 200. Reading...")
|
||||
data = await res.read()
|
||||
logger.debug(f"Read {len(data)} bytes")
|
||||
res_zip = BytesIO(data)
|
||||
if isInstalled:
|
||||
try:
|
||||
logger.debug("Uninstalling existing plugin...")
|
||||
await self.uninstall_plugin(name)
|
||||
except:
|
||||
logger.error(f"Plugin {name} could not be uninstalled.")
|
||||
logger.debug("Unzipping...")
|
||||
ret = self._unzip_to_plugin_dir(res_zip, name, hash)
|
||||
if ret:
|
||||
plugin_dir = self.find_plugin_folder(name)
|
||||
ret = await self._download_remote_binaries_for_plugin_with_name(plugin_dir)
|
||||
if ret:
|
||||
logger.info(f"Installed {name} (Version: {version})")
|
||||
if name in self.loader.plugins:
|
||||
self.loader.plugins[name].stop()
|
||||
self.loader.plugins.pop(name, None)
|
||||
await sleep(1)
|
||||
self.loader.import_plugin(path.join(plugin_dir, "main.py"), plugin_dir)
|
||||
else:
|
||||
logger.fatal(f"Failed Downloading Remote Binaries")
|
||||
else:
|
||||
self.log.fatal(f"SHA-256 Mismatch!!!! {name} (Version: {version})")
|
||||
if self.loader.watcher:
|
||||
self.loader.watcher.disabled = False
|
||||
else:
|
||||
logger.fatal(f"Could not fetch from URL. {await res.text()}")
|
||||
|
||||
async def request_plugin_install(self, artifact, name, version, hash):
|
||||
request_id = str(time())
|
||||
self.install_requests[request_id] = PluginInstallContext(artifact, name, version, hash)
|
||||
tab = await get_gamepadui_tab()
|
||||
await tab.open_websocket()
|
||||
await tab.evaluate_js(f"DeckyPluginLoader.addPluginInstallPrompt('{name}', '{version}', '{request_id}', '{hash}')")
|
||||
|
||||
async def confirm_plugin_install(self, request_id):
|
||||
request = self.install_requests.pop(request_id)
|
||||
await self._install(request.artifact, request.name, request.version, request.hash)
|
||||
|
||||
def cancel_plugin_install(self, request_id):
|
||||
self.install_requests.pop(request_id)
|
||||
@@ -0,0 +1,355 @@
|
||||
# Full imports
|
||||
import json
|
||||
# import pprint
|
||||
# from pprint import pformat
|
||||
|
||||
# Partial imports
|
||||
from aiohttp import ClientSession
|
||||
from asyncio import sleep
|
||||
from hashlib import sha256
|
||||
from io import BytesIO
|
||||
from logging import getLogger
|
||||
from os import R_OK, W_OK, path, listdir, access, mkdir
|
||||
from re import sub
|
||||
from shutil import rmtree
|
||||
from time import time
|
||||
from zipfile import ZipFile
|
||||
from enum import IntEnum
|
||||
from typing import Dict, List, TypedDict
|
||||
|
||||
# Local modules
|
||||
from .localplatform.localplatform import chown, chmod, get_chown_plugin_path
|
||||
from .loader import Loader, Plugins
|
||||
from .helpers import get_ssl_context, download_remote_binary_to_path
|
||||
from .enums import UserType
|
||||
from .settings import SettingsManager
|
||||
|
||||
logger = getLogger("Browser")
|
||||
|
||||
class PluginInstallType(IntEnum):
|
||||
INSTALL = 0
|
||||
REINSTALL = 1
|
||||
UPDATE = 2
|
||||
DOWNGRADE = 3
|
||||
OVERWRITE = 4
|
||||
|
||||
class PluginInstallRequest(TypedDict):
|
||||
name: str
|
||||
artifact: str
|
||||
version: str
|
||||
hash: str
|
||||
install_type: PluginInstallType
|
||||
|
||||
class PluginInstallContext:
|
||||
def __init__(self, artifact: str, name: str, version: str, hash: str) -> None:
|
||||
self.artifact = artifact
|
||||
self.name = name
|
||||
self.version = version
|
||||
self.hash = hash
|
||||
|
||||
class PluginBrowser:
|
||||
def __init__(self, plugin_path: str, plugins: Plugins, loader: Loader, settings: SettingsManager) -> None:
|
||||
self.plugin_path = plugin_path
|
||||
self.plugins = plugins
|
||||
self.loader = loader
|
||||
self.settings = settings
|
||||
self.install_requests: Dict[str, PluginInstallContext | List[PluginInstallContext]] = {}
|
||||
|
||||
def _unzip_to_plugin_dir(self, zip: BytesIO, name: str, hash: str):
|
||||
zip_hash = sha256(zip.getbuffer()).hexdigest()
|
||||
if hash and (zip_hash != hash):
|
||||
return False
|
||||
zip_file = ZipFile(zip)
|
||||
zip_file.extractall(self.plugin_path)
|
||||
return True
|
||||
|
||||
async def _download_remote_binaries_for_plugin_with_name(self, pluginBasePath: str):
|
||||
rv = False
|
||||
try:
|
||||
packageJsonPath = path.join(pluginBasePath, 'package.json')
|
||||
pluginBinPath = path.join(pluginBasePath, 'bin')
|
||||
|
||||
logger.debug(f"Checking package.json at {packageJsonPath}")
|
||||
|
||||
if access(packageJsonPath, R_OK):
|
||||
with open(packageJsonPath, "r", encoding="utf-8") as f:
|
||||
packageJson = json.load(f)
|
||||
if "remote_binary" in packageJson and len(packageJson["remote_binary"]) > 0:
|
||||
# create bin directory if needed.
|
||||
chmod(pluginBasePath, 777)
|
||||
if access(pluginBasePath, W_OK):
|
||||
if not path.exists(pluginBinPath):
|
||||
logger.debug(f"Creating bin directory at {pluginBinPath}")
|
||||
mkdir(pluginBinPath)
|
||||
if not access(pluginBinPath, W_OK):
|
||||
chmod(pluginBinPath, 777)
|
||||
|
||||
rv = True
|
||||
for remoteBinary in packageJson["remote_binary"]:
|
||||
# Required Fields. If any Remote Binary is missing these fail the install.
|
||||
binName = remoteBinary["name"]
|
||||
binURL = remoteBinary["url"]
|
||||
binHash = remoteBinary["sha256hash"]
|
||||
logger.info(f"Attempting to download {binName} from {binURL}")
|
||||
if not await download_remote_binary_to_path(binURL, binHash, path.join(pluginBinPath, binName)):
|
||||
rv = False
|
||||
raise Exception(f"Error Downloading Remote Binary {binName}@{binURL} with hash {binHash} to {path.join(pluginBinPath, binName)}")
|
||||
|
||||
else:
|
||||
rv = True
|
||||
logger.info(f"No Remote Binaries to Download")
|
||||
|
||||
except Exception as e:
|
||||
rv = False
|
||||
logger.debug(str(e))
|
||||
|
||||
return rv
|
||||
|
||||
"""Return the filename (only) for the specified plugin"""
|
||||
def find_plugin_folder(self, name: str) -> str | None:
|
||||
for folder in listdir(self.plugin_path):
|
||||
try:
|
||||
with open(path.join(self.plugin_path, folder, 'plugin.json'), "r", encoding="utf-8") as f:
|
||||
plugin = json.load(f)
|
||||
|
||||
if plugin['name'] == name:
|
||||
return folder
|
||||
except:
|
||||
logger.debug(f"skipping {folder}")
|
||||
|
||||
def set_plugin_dir_permissions(self, plugin_dir: str) -> bool:
|
||||
plugin_json_path = path.join(plugin_dir, 'plugin.json')
|
||||
logger.debug(f"Checking plugin.json at {plugin_json_path}")
|
||||
|
||||
root_plugin = False
|
||||
|
||||
if access(plugin_json_path, R_OK):
|
||||
with open(plugin_json_path, "r", encoding="utf-8") as f:
|
||||
plugin_json = json.load(f)
|
||||
if "flags" in plugin_json and "root" in plugin_json["flags"]:
|
||||
root_plugin = True
|
||||
|
||||
logger.debug("root_plugin %d, dir %s", root_plugin, plugin_dir)
|
||||
if get_chown_plugin_path():
|
||||
return chown(plugin_dir, UserType.EFFECTIVE_USER if root_plugin else UserType.HOST_USER, True) and chown(plugin_dir, UserType.EFFECTIVE_USER, False) and chmod(plugin_dir, 755) and chown(plugin_json_path, UserType.EFFECTIVE_USER, False) and chmod(plugin_json_path, 755)
|
||||
else:
|
||||
logger.debug("chown disabled by environment")
|
||||
return True
|
||||
|
||||
async def uninstall_plugin(self, name: str):
|
||||
if self.loader.watcher:
|
||||
self.loader.watcher.disabled = True
|
||||
plugin_folder = self.find_plugin_folder(name)
|
||||
assert plugin_folder is not None
|
||||
plugin_dir = path.join(self.plugin_path, plugin_folder)
|
||||
try:
|
||||
logger.info("uninstalling " + name)
|
||||
logger.info(" at dir " + plugin_dir)
|
||||
logger.debug("calling frontend unload for %s" % str(name))
|
||||
await self.loader.ws.emit("loader/unload_plugin", name)
|
||||
# plugins_snapshot = self.plugins.copy()
|
||||
# snapshot_string = pformat(plugins_snapshot)
|
||||
# logger.debug("current plugins: %s", snapshot_string)
|
||||
|
||||
if name in self.plugins:
|
||||
logger.debug("Plugin %s was found", name)
|
||||
await self.plugins[name].stop(uninstall=True)
|
||||
logger.debug("Plugin %s was stopped", name)
|
||||
del self.plugins[name]
|
||||
logger.debug("Plugin %s was removed from the dictionary", name)
|
||||
self.cleanup_plugin_settings(name)
|
||||
logger.debug("removing files %s" % str(name))
|
||||
rmtree(plugin_dir)
|
||||
except FileNotFoundError:
|
||||
logger.warning(f"Plugin {name} not installed, skipping uninstallation")
|
||||
except Exception as e:
|
||||
logger.error(f"Plugin {name} in {plugin_dir} was not uninstalled")
|
||||
logger.error(f"Error at {str(e)}", exc_info=e)
|
||||
finally:
|
||||
if self.loader.watcher:
|
||||
self.loader.watcher.disabled = False
|
||||
|
||||
async def _install(self, artifact: str, name: str, version: str, hash: str):
|
||||
await self.loader.ws.emit("loader/plugin_download_start", name)
|
||||
await self.loader.ws.emit("loader/plugin_download_info", 5, "Store.download_progress_info.start")
|
||||
# Will be set later in code
|
||||
res_zip = None
|
||||
|
||||
# Check if plugin was already installed before this
|
||||
isInstalled = False
|
||||
|
||||
try:
|
||||
pluginFolderPath = self.find_plugin_folder(name)
|
||||
if pluginFolderPath:
|
||||
isInstalled = True
|
||||
except:
|
||||
logger.error(f"Failed to determine if {name} is already installed, continuing anyway.")
|
||||
|
||||
# Preserve plugin order before removing plugin (uninstall alters the order and removes the plugin from the list)
|
||||
current_plugin_order = self.settings.getSetting("pluginOrder")[:]
|
||||
if self.loader.watcher:
|
||||
self.loader.watcher.disabled = True
|
||||
|
||||
# Check if the file is a local file or a URL
|
||||
if artifact.startswith("file://"):
|
||||
logger.info(f"Installing {name} from local ZIP file (Version: {version})")
|
||||
await self.loader.ws.emit("loader/plugin_download_info", 10, "Store.download_progress_info.open_zip")
|
||||
res_zip = BytesIO(open(artifact[7:], "rb").read())
|
||||
else:
|
||||
logger.info(f"Installing {name} from URL (Version: {version})")
|
||||
await self.loader.ws.emit("loader/plugin_download_info", 10, "Store.download_progress_info.download_zip")
|
||||
|
||||
async with ClientSession() as client:
|
||||
logger.debug(f"Fetching {artifact}")
|
||||
res = await client.get(artifact, ssl=get_ssl_context())
|
||||
#TODO track progress of this download in chunks like with decky updates
|
||||
#TODO but squish with min 15 and max 75
|
||||
if res.status == 200:
|
||||
logger.debug("Got 200. Reading...")
|
||||
data = await res.read()
|
||||
logger.debug(f"Read {len(data)} bytes")
|
||||
res_zip = BytesIO(data)
|
||||
else:
|
||||
logger.fatal(f"Could not fetch from URL. {await res.text()}")
|
||||
|
||||
await self.loader.ws.emit("loader/plugin_download_info", 70, "Store.download_progress_info.increment_count")
|
||||
storeUrl = ""
|
||||
match self.settings.getSetting("store", 0):
|
||||
case 0: storeUrl = "https://plugins.deckbrew.xyz/plugins" # default
|
||||
case 1: storeUrl = "https://testing.deckbrew.xyz/plugins" # testing
|
||||
case 2: storeUrl = self.settings.getSetting("store-url", "https://plugins.deckbrew.xyz/plugins") # custom
|
||||
case _: storeUrl = "https://plugins.deckbrew.xyz/plugins"
|
||||
logger.info(f"Incrementing installs for {name} from URL {storeUrl} (version {version})")
|
||||
async with ClientSession() as client:
|
||||
res = await client.post(storeUrl+f"/{name}/versions/{version}/increment?isUpdate={isInstalled}", ssl=get_ssl_context())
|
||||
if res.status != 200:
|
||||
logger.error(f"Server did not accept install count increment request. code: {res.status}")
|
||||
|
||||
await self.loader.ws.emit("loader/plugin_download_info", 75, "Store.download_progress_info.parse_zip")
|
||||
if res_zip and version == "dev":
|
||||
with ZipFile(res_zip) as plugin_zip:
|
||||
plugin_json_list = [file for file in plugin_zip.namelist() if file.endswith("/plugin.json") and file.count("/") == 1]
|
||||
|
||||
if len(plugin_json_list) == 0:
|
||||
logger.fatal("No plugin.json found in plugin ZIP")
|
||||
return
|
||||
|
||||
elif len(plugin_json_list) > 1:
|
||||
logger.fatal("Multiple plugin.json found in plugin ZIP")
|
||||
return
|
||||
|
||||
else:
|
||||
plugin_json_file = plugin_json_list[0]
|
||||
name = sub(r"/.+$", "", plugin_json_file)
|
||||
try:
|
||||
with plugin_zip.open(plugin_json_file) as f:
|
||||
plugin_json_data = json.loads(f.read().decode('utf-8'))
|
||||
plugin_name_from_plugin_json = plugin_json_data.get('name')
|
||||
if plugin_name_from_plugin_json and plugin_name_from_plugin_json.strip():
|
||||
logger.info(f"Extracted plugin name from {plugin_json_file}: {plugin_name_from_plugin_json}")
|
||||
name = plugin_name_from_plugin_json
|
||||
else:
|
||||
logger.warning(f"Nonexistent or invalid 'name' key value in {plugin_json_file}. Falling back to extracting from path.")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to read or parse {plugin_json_file}: {str(e)}. Falling back to extracting from path.")
|
||||
|
||||
# Check to make sure we got the file
|
||||
if res_zip is None:
|
||||
logger.fatal(f"Could not fetch {artifact}")
|
||||
return
|
||||
|
||||
# If plugin is installed, uninstall it
|
||||
if isInstalled:
|
||||
await self.loader.ws.emit("loader/plugin_download_info", 80, "Store.download_progress_info.uninstalling_previous")
|
||||
try:
|
||||
logger.debug("Uninstalling existing plugin...")
|
||||
await self.uninstall_plugin(name)
|
||||
except:
|
||||
logger.error(f"Plugin {name} could not be uninstalled.")
|
||||
|
||||
|
||||
await self.loader.ws.emit("loader/plugin_download_info", 90, "Store.download_progress_info.installing_plugin")
|
||||
# Install the plugin
|
||||
logger.debug("Unzipping...")
|
||||
ret = self._unzip_to_plugin_dir(res_zip, name, hash)
|
||||
if ret:
|
||||
plugin_folder = self.find_plugin_folder(name)
|
||||
assert plugin_folder is not None
|
||||
plugin_dir = path.join(self.plugin_path, plugin_folder)
|
||||
await self.loader.ws.emit("loader/plugin_download_info", 95, "Store.download_progress_info.download_remote")
|
||||
ret = await self._download_remote_binaries_for_plugin_with_name(plugin_dir)
|
||||
chown_ret = self.set_plugin_dir_permissions(plugin_dir)
|
||||
if ret:
|
||||
logger.info(f"Installed {name} (Version: {version})")
|
||||
if name in self.loader.plugins:
|
||||
await self.loader.plugins[name].stop()
|
||||
self.loader.plugins.pop(name, None)
|
||||
await sleep(1)
|
||||
if not isInstalled:
|
||||
current_plugin_order = self.settings.getSetting("pluginOrder")
|
||||
current_plugin_order.append(name)
|
||||
self.settings.setSetting("pluginOrder", current_plugin_order)
|
||||
logger.debug("Plugin %s was added to the pluginOrder setting", name)
|
||||
await self.loader.import_plugin(path.join(plugin_dir, "main.py"), plugin_folder)
|
||||
elif not chown_ret:
|
||||
logger.error("Could not chown plugin")
|
||||
return
|
||||
else:
|
||||
logger.error("Could not download remote binaries")
|
||||
return
|
||||
else:
|
||||
logger.fatal(f"SHA-256 Mismatch!!!! {name} (Version: {version})")
|
||||
if self.loader.watcher:
|
||||
self.loader.watcher.disabled = False
|
||||
await self.loader.ws.emit("loader/plugin_download_finish", name)
|
||||
|
||||
async def request_plugin_install(self, artifact: str, name: str, version: str, hash: str, install_type: PluginInstallType):
|
||||
request_id = str(time())
|
||||
self.install_requests[request_id] = PluginInstallContext(artifact, name, version, hash)
|
||||
|
||||
await self.loader.ws.emit("loader/add_plugin_install_prompt", name, version, request_id, hash, install_type)
|
||||
|
||||
async def request_multiple_plugin_installs(self, requests: List[PluginInstallRequest]):
|
||||
request_id = str(time())
|
||||
self.install_requests[request_id] = [PluginInstallContext(req['artifact'], req['name'], req['version'], req['hash']) for req in requests]
|
||||
|
||||
await self.loader.ws.emit("loader/add_multiple_plugins_install_prompt", request_id, requests)
|
||||
|
||||
async def confirm_plugin_install(self, request_id: str):
|
||||
requestOrRequests = self.install_requests.pop(request_id)
|
||||
if isinstance(requestOrRequests, list):
|
||||
[await self._install(req.artifact, req.name, req.version, req.hash) for req in requestOrRequests]
|
||||
else:
|
||||
await self._install(requestOrRequests.artifact, requestOrRequests.name, requestOrRequests.version, requestOrRequests.hash)
|
||||
|
||||
def cancel_plugin_install(self, request_id: str):
|
||||
self.install_requests.pop(request_id)
|
||||
|
||||
def cleanup_plugin_settings(self, name: str):
|
||||
"""Removes any settings related to a plugin. Propably called when a plugin is uninstalled.
|
||||
|
||||
Args:
|
||||
name (string): The name of the plugin
|
||||
"""
|
||||
frozen_plugins = self.settings.getSetting("frozenPlugins", [])
|
||||
if name in frozen_plugins:
|
||||
frozen_plugins.remove(name)
|
||||
self.settings.setSetting("frozenPlugins", frozen_plugins)
|
||||
|
||||
hidden_plugins = self.settings.getSetting("hiddenPlugins", [])
|
||||
if name in hidden_plugins:
|
||||
hidden_plugins.remove(name)
|
||||
self.settings.setSetting("hiddenPlugins", hidden_plugins)
|
||||
|
||||
plugin_order = self.settings.getSetting("pluginOrder", [])
|
||||
|
||||
if name in plugin_order:
|
||||
plugin_order.remove(name)
|
||||
self.settings.setSetting("pluginOrder", plugin_order)
|
||||
|
||||
disabled_plugins: List[str] = self.settings.getSetting("disabled_plugins", [])
|
||||
if name in disabled_plugins:
|
||||
disabled_plugins.remove(name)
|
||||
self.settings.setSetting("disabled_plugins", disabled_plugins)
|
||||
|
||||
logger.debug("Removed any settings for plugin %s", name)
|
||||
@@ -0,0 +1,9 @@
|
||||
from enum import IntEnum
|
||||
|
||||
class UserType(IntEnum):
|
||||
HOST_USER = 1 # usually deck
|
||||
EFFECTIVE_USER = 2 # usually root
|
||||
|
||||
class PluginLoadType(IntEnum):
|
||||
LEGACY_EVAL_IIFE = 0 # legacy, uses legacy serverAPI
|
||||
ESMODULE_V1 = 1 # esmodule loading with modern @decky/backend apis
|
||||
@@ -0,0 +1,194 @@
|
||||
import re
|
||||
import ssl
|
||||
import uuid
|
||||
import os
|
||||
import subprocess
|
||||
from hashlib import sha256
|
||||
import importlib.metadata
|
||||
|
||||
import certifi
|
||||
from aiohttp.web import Request, Response, middleware
|
||||
from aiohttp.typedefs import Handler
|
||||
from aiohttp import ClientSession
|
||||
from .localplatform import localplatform
|
||||
from .enums import UserType
|
||||
from logging import getLogger
|
||||
from packaging.version import Version
|
||||
|
||||
SSHD_UNIT = "sshd.service"
|
||||
REMOTE_DEBUGGER_UNIT = "steam-web-debug-portforward.service"
|
||||
|
||||
# global vars
|
||||
csrf_token = str(uuid.uuid4())
|
||||
ssl_ctx = ssl.create_default_context(cafile=certifi.where())
|
||||
|
||||
assets_regex = re.compile("^/plugins/.*/assets/.*")
|
||||
data_regex = re.compile("^/plugins/.*/data/.*")
|
||||
dist_regex = re.compile("^/plugins/.*/dist/.*")
|
||||
frontend_regex = re.compile("^/frontend/.*")
|
||||
logger = getLogger("Main")
|
||||
|
||||
def get_ssl_context():
|
||||
return ssl_ctx
|
||||
|
||||
def get_csrf_token():
|
||||
return csrf_token
|
||||
|
||||
@middleware
|
||||
async def csrf_middleware(request: Request, handler: Handler):
|
||||
if str(request.method) == "OPTIONS" or \
|
||||
request.headers.get('X-Decky-Auth') == csrf_token or \
|
||||
str(request.rel_url) == "/auth/token" or \
|
||||
str(request.rel_url).startswith("/plugins/load_main/") or \
|
||||
str(request.rel_url).startswith("/static/") or \
|
||||
str(request.rel_url).startswith("/steam_resource/") or \
|
||||
str(request.rel_url).startswith("/frontend/") or \
|
||||
str(request.rel_url.path) == "/fetch" or \
|
||||
str(request.rel_url.path) == "/ws" or \
|
||||
assets_regex.match(str(request.rel_url)) or \
|
||||
data_regex.match(str(request.rel_url)) or \
|
||||
dist_regex.match(str(request.rel_url)) or \
|
||||
frontend_regex.match(str(request.rel_url)):
|
||||
|
||||
return await handler(request)
|
||||
return Response(text='Forbidden', status=403)
|
||||
|
||||
def create_inject_script(script: str) -> str:
|
||||
return "try{if (window.deckyHasLoaded){setTimeout(() => SteamClient.Browser.RestartJSContext(), 100)}else{window.deckyHasLoaded = true;(async()=>{try{await import('http://localhost:1337/frontend/%s?v=%s')}catch(e){console.error(e)};})();}}catch(e){console.error(e)}" % (script, get_loader_version(), )
|
||||
|
||||
# Get the default homebrew path unless a home_path is specified. home_path argument is deprecated
|
||||
def get_homebrew_path() -> str:
|
||||
return localplatform.get_unprivileged_path()
|
||||
|
||||
# Recursively create path and chown as user
|
||||
def mkdir_as_user(path: str):
|
||||
path = os.path.realpath(path)
|
||||
os.makedirs(path, exist_ok=True)
|
||||
localplatform.chown(path)
|
||||
|
||||
# Fetches the version of loader
|
||||
def get_loader_version() -> str:
|
||||
try:
|
||||
# Normalize Python-style version to conform to Decky style
|
||||
v = Version(importlib.metadata.version("decky_loader"))
|
||||
if v.major == 0 and v.minor == 0 and v.micro == 0:
|
||||
# We are probably running from source
|
||||
return "dev"
|
||||
|
||||
version_str = f'v{v.major}.{v.minor}.{v.micro}'
|
||||
|
||||
if v.pre:
|
||||
version_str += f'-pre{v.pre[1]}'
|
||||
|
||||
if v.post:
|
||||
version_str += f'-dev{v.post}'
|
||||
|
||||
return version_str
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to execute get_loader_version(): {str(e)}")
|
||||
return "unknown"
|
||||
|
||||
user_agent = f"Decky/{get_loader_version()} (https://decky.xyz)"
|
||||
|
||||
# returns the appropriate system python paths
|
||||
def get_system_pythonpaths() -> list[str]:
|
||||
try:
|
||||
# run as normal normal user if on linux to also include user python paths
|
||||
proc = subprocess.run(["python3" if localplatform.ON_LINUX else "python", "-c", "import sys; print('\\n'.join(x for x in sys.path if x))"],
|
||||
# TODO make this less insane
|
||||
capture_output=True, user=localplatform.localplatform._get_user_id() if localplatform.ON_LINUX else None, env={} if localplatform.ON_LINUX else None) # pyright: ignore [reportPrivateUsage]
|
||||
|
||||
proc.check_returncode()
|
||||
|
||||
versions = [x.strip() for x in proc.stdout.decode().strip().split("\n")]
|
||||
return [x for x in versions if x and not x.isspace()]
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to execute get_system_pythonpaths(): {str(e)}")
|
||||
return []
|
||||
|
||||
# Download Remote Binaries to local Plugin
|
||||
async def download_remote_binary_to_path(url: str, binHash: str, path: str) -> bool:
|
||||
rv = False
|
||||
try:
|
||||
if os.access(os.path.dirname(path), os.W_OK):
|
||||
async with ClientSession() as client:
|
||||
res = await client.get(url, ssl=get_ssl_context())
|
||||
if res.status == 200:
|
||||
logger.debug("Download attempt of URL: " + url)
|
||||
data = await res.read()
|
||||
remoteHash = sha256(data).hexdigest()
|
||||
if binHash == remoteHash:
|
||||
with open(path, 'wb') as f:
|
||||
f.write(data)
|
||||
rv = True
|
||||
else:
|
||||
raise Exception(f"Fatal Error: Hash Mismatch for remote binary {path}@{url}")
|
||||
else:
|
||||
rv = False
|
||||
except Exception as e:
|
||||
logger.error("Error during download " + str(e))
|
||||
rv = False
|
||||
|
||||
return rv
|
||||
|
||||
# Deprecated
|
||||
def set_user():
|
||||
pass
|
||||
|
||||
# Deprecated
|
||||
def set_user_group() -> str:
|
||||
return get_user_group()
|
||||
|
||||
#########
|
||||
# Below is legacy code, provided for backwards compatibility. This will break on windows
|
||||
#########
|
||||
|
||||
# Get the user id hosting the plugin loader
|
||||
def get_user_id() -> int:
|
||||
return localplatform.localplatform._get_user_id() # pyright: ignore [reportPrivateUsage]
|
||||
|
||||
# Get the user hosting the plugin loader
|
||||
def get_user() -> str:
|
||||
return localplatform.localplatform._get_user() # pyright: ignore [reportPrivateUsage]
|
||||
|
||||
# Get the effective user id of the running process
|
||||
def get_effective_user_id() -> int:
|
||||
return localplatform.localplatform._get_effective_user_id() # pyright: ignore [reportPrivateUsage]
|
||||
|
||||
# Get the effective user of the running process
|
||||
def get_effective_user() -> str:
|
||||
return localplatform.localplatform._get_effective_user() # pyright: ignore [reportPrivateUsage]
|
||||
|
||||
# Get the effective user group id of the running process
|
||||
def get_effective_user_group_id() -> int:
|
||||
return localplatform.localplatform._get_effective_user_group_id() # pyright: ignore [reportPrivateUsage]
|
||||
|
||||
# Get the effective user group of the running process
|
||||
def get_effective_user_group() -> str:
|
||||
return localplatform.localplatform._get_effective_user_group() # pyright: ignore [reportPrivateUsage]
|
||||
|
||||
# Get the user owner of the given file path.
|
||||
def get_user_owner(file_path: str) -> str:
|
||||
return localplatform.localplatform._get_user_owner(file_path) # pyright: ignore [reportPrivateUsage]
|
||||
|
||||
# Get the user group of the given file path, or the user group hosting the plugin loader
|
||||
def get_user_group(file_path: str | None = None) -> str:
|
||||
return localplatform.localplatform._get_user_group(file_path) # pyright: ignore [reportPrivateUsage]
|
||||
|
||||
# Get the group id of the user hosting the plugin loader
|
||||
def get_user_group_id() -> int:
|
||||
return localplatform.localplatform._get_user_group_id() # pyright: ignore [reportPrivateUsage]
|
||||
|
||||
# Get the default home path unless a user is specified
|
||||
def get_home_path(username: str | None = None) -> str:
|
||||
# TODO hardcoded root is kinda a hack
|
||||
return localplatform.get_home_path(UserType.EFFECTIVE_USER if username == "root" else UserType.HOST_USER)
|
||||
|
||||
async def is_systemd_unit_active(unit_name: str) -> bool:
|
||||
return await localplatform.service_active(unit_name)
|
||||
|
||||
async def stop_systemd_unit(unit_name: str) -> bool:
|
||||
return await localplatform.service_stop(unit_name)
|
||||
|
||||
async def start_systemd_unit(unit_name: str) -> bool:
|
||||
return await localplatform.service_start(unit_name)
|
||||
@@ -0,0 +1,438 @@
|
||||
# Injector code from https://github.com/SteamDeckHomebrew/steamdeck-ui-inject. More info on how it works there.
|
||||
|
||||
from asyncio import sleep
|
||||
from logging import getLogger
|
||||
from typing import Any, Callable, List, TypedDict, Dict
|
||||
|
||||
from aiohttp import ClientSession
|
||||
from aiohttp.client_exceptions import ClientConnectorError, ClientOSError
|
||||
from asyncio.exceptions import TimeoutError
|
||||
import uuid
|
||||
|
||||
BASE_ADDRESS = "http://localhost:8080"
|
||||
|
||||
logger = getLogger("Injector")
|
||||
|
||||
class _TabResponse(TypedDict):
|
||||
title: str
|
||||
id: str
|
||||
url: str
|
||||
webSocketDebuggerUrl: str
|
||||
|
||||
class Tab:
|
||||
cmd_id = 0
|
||||
|
||||
def __init__(self, res: _TabResponse) -> None:
|
||||
self.title: str = res["title"]
|
||||
self.id: str = res["id"]
|
||||
self.url: str = res["url"]
|
||||
self.ws_url: str = res["webSocketDebuggerUrl"]
|
||||
|
||||
self.websocket = None
|
||||
self.client = None
|
||||
|
||||
async def open_websocket(self):
|
||||
self.client = ClientSession()
|
||||
self.websocket = await self.client.ws_connect(self.ws_url)
|
||||
|
||||
async def close_websocket(self):
|
||||
if self.websocket:
|
||||
await self.websocket.close()
|
||||
if self.client:
|
||||
await self.client.close()
|
||||
|
||||
async def listen_for_message(self):
|
||||
if self.websocket:
|
||||
async for message in self.websocket:
|
||||
data = message.json()
|
||||
yield data
|
||||
logger.warning(f"The Tab {self.title} socket has been disconnected while listening for messages.")
|
||||
await self.close_websocket()
|
||||
|
||||
async def _send_devtools_cmd(self, dc: Dict[str, Any], receive: bool = True):
|
||||
if self.websocket:
|
||||
self.cmd_id += 1
|
||||
dc["id"] = self.cmd_id
|
||||
await self.websocket.send_json(dc)
|
||||
if receive:
|
||||
async for msg in self.listen_for_message():
|
||||
if "id" in msg and msg["id"] == dc["id"]:
|
||||
return msg
|
||||
return None
|
||||
raise RuntimeError("Websocket not opened")
|
||||
|
||||
async def evaluate_js(self, js: str, run_async: bool | None = False, manage_socket: bool | None = True, get_result: bool = True):
|
||||
try:
|
||||
if manage_socket:
|
||||
await self.open_websocket()
|
||||
|
||||
res = await self._send_devtools_cmd({
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {
|
||||
"expression": js,
|
||||
"userGesture": True,
|
||||
"awaitPromise": run_async
|
||||
}
|
||||
}, get_result)
|
||||
|
||||
finally:
|
||||
if manage_socket:
|
||||
await self.close_websocket()
|
||||
return res
|
||||
|
||||
async def has_global_var(self, var_name: str, manage_socket: bool = True):
|
||||
res = await self.evaluate_js(f"window['{var_name}'] !== null && window['{var_name}'] !== undefined", False, manage_socket)
|
||||
assert res is not None
|
||||
|
||||
if not "result" in res or not "result" in res["result"] or not "value" in res["result"]["result"]:
|
||||
return False
|
||||
|
||||
return res["result"]["result"]["value"]
|
||||
|
||||
async def close(self, manage_socket: bool = True):
|
||||
try:
|
||||
if manage_socket:
|
||||
await self.open_websocket()
|
||||
|
||||
res = await self._send_devtools_cmd({
|
||||
"method": "Page.close",
|
||||
}, False)
|
||||
|
||||
finally:
|
||||
if manage_socket:
|
||||
await self.close_websocket()
|
||||
return res
|
||||
|
||||
async def enable(self):
|
||||
"""
|
||||
Enables page domain notifications.
|
||||
"""
|
||||
await self._send_devtools_cmd({
|
||||
"method": "Page.enable",
|
||||
}, False)
|
||||
|
||||
async def disable(self):
|
||||
"""
|
||||
Disables page domain notifications.
|
||||
"""
|
||||
await self._send_devtools_cmd({
|
||||
"method": "Page.disable",
|
||||
}, False)
|
||||
|
||||
async def refresh(self, manage_socket: bool = True):
|
||||
try:
|
||||
if manage_socket:
|
||||
await self.open_websocket()
|
||||
|
||||
await self._send_devtools_cmd({
|
||||
"method": "Page.reload",
|
||||
}, False)
|
||||
|
||||
finally:
|
||||
if manage_socket:
|
||||
await self.close_websocket()
|
||||
|
||||
return
|
||||
async def reload_and_evaluate(self, js: str, manage_socket: bool = True):
|
||||
"""
|
||||
Reloads the current tab, with JS to run on load via debugger
|
||||
"""
|
||||
try:
|
||||
if manage_socket:
|
||||
await self.open_websocket()
|
||||
|
||||
await self._send_devtools_cmd({
|
||||
"method": "Debugger.enable"
|
||||
}, True)
|
||||
|
||||
await self._send_devtools_cmd({
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {
|
||||
"expression": "location.reload();",
|
||||
"userGesture": True,
|
||||
"awaitPromise": False
|
||||
}
|
||||
}, False)
|
||||
|
||||
breakpoint_res = await self._send_devtools_cmd({
|
||||
"method": "Debugger.setInstrumentationBreakpoint",
|
||||
"params": {
|
||||
"instrumentation": "beforeScriptExecution"
|
||||
}
|
||||
}, True)
|
||||
|
||||
assert breakpoint_res is not None
|
||||
|
||||
logger.info(breakpoint_res)
|
||||
|
||||
# Page finishes loading when breakpoint hits
|
||||
|
||||
for _ in range(20):
|
||||
# this works around 1/5 of the time, so just send it 8 times.
|
||||
# the js accounts for being injected multiple times allowing only one instance to run at a time anyway
|
||||
await self._send_devtools_cmd({
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {
|
||||
"expression": js,
|
||||
"userGesture": True,
|
||||
"awaitPromise": False
|
||||
}
|
||||
}, False)
|
||||
|
||||
await self._send_devtools_cmd({
|
||||
"method": "Debugger.removeBreakpoint",
|
||||
"params": {
|
||||
"breakpointId": breakpoint_res["result"]["breakpointId"]
|
||||
}
|
||||
}, False)
|
||||
|
||||
for _ in range(4):
|
||||
await self._send_devtools_cmd({
|
||||
"method": "Debugger.resume"
|
||||
}, False)
|
||||
|
||||
await self._send_devtools_cmd({
|
||||
"method": "Debugger.disable"
|
||||
}, True)
|
||||
|
||||
finally:
|
||||
if manage_socket:
|
||||
await self.close_websocket()
|
||||
return
|
||||
|
||||
async def add_script_to_evaluate_on_new_document(self, js: str, add_dom_wrapper: bool = True, manage_socket: bool = True, get_result: bool = True):
|
||||
"""
|
||||
How the underlying call functions is not particularly clear from the devtools docs, so stealing puppeteer's description:
|
||||
|
||||
Adds a function which would be invoked in one of the following scenarios:
|
||||
* whenever the page is navigated
|
||||
* whenever the child frame is attached or navigated. In this case, the
|
||||
function is invoked in the context of the newly attached frame.
|
||||
|
||||
The function is invoked after the document was created but before any of
|
||||
its scripts were run. This is useful to amend the JavaScript environment,
|
||||
e.g. to seed `Math.random`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
js : str
|
||||
The script to evaluate on new document
|
||||
add_dom_wrapper : bool
|
||||
True to wrap the script in a wait for the 'DOMContentLoaded' event.
|
||||
DOM will usually not exist when this execution happens,
|
||||
so it is necessary to delay til DOM is loaded if you are modifying it
|
||||
manage_socket : bool
|
||||
True to have this function handle opening/closing the websocket for this tab
|
||||
get_result : bool
|
||||
True to wait for the result of this call
|
||||
|
||||
Returns
|
||||
-------
|
||||
int or None
|
||||
The identifier of the script added, used to remove it later.
|
||||
(see remove_script_to_evaluate_on_new_document below)
|
||||
None is returned if `get_result` is False
|
||||
"""
|
||||
try:
|
||||
|
||||
wrappedjs = """
|
||||
function scriptFunc() {
|
||||
{js}
|
||||
}
|
||||
if (document.readyState === 'loading') {
|
||||
addEventListener('DOMContentLoaded', () => {
|
||||
scriptFunc();
|
||||
});
|
||||
} else {
|
||||
scriptFunc();
|
||||
}
|
||||
""".format(js=js) if add_dom_wrapper else js
|
||||
|
||||
if manage_socket:
|
||||
await self.open_websocket()
|
||||
|
||||
res = await self._send_devtools_cmd({
|
||||
"method": "Page.addScriptToEvaluateOnNewDocument",
|
||||
"params": {
|
||||
"source": wrappedjs
|
||||
}
|
||||
}, get_result)
|
||||
|
||||
finally:
|
||||
if manage_socket:
|
||||
await self.close_websocket()
|
||||
return res
|
||||
|
||||
async def remove_script_to_evaluate_on_new_document(self, script_id: str, manage_socket: bool = True):
|
||||
"""
|
||||
Removes a script from a page that was added with `add_script_to_evaluate_on_new_document`
|
||||
|
||||
Parameters
|
||||
----------
|
||||
script_id : int
|
||||
The identifier of the script to remove (returned from `add_script_to_evaluate_on_new_document`)
|
||||
"""
|
||||
|
||||
try:
|
||||
if manage_socket:
|
||||
await self.open_websocket()
|
||||
|
||||
await self._send_devtools_cmd({
|
||||
"method": "Page.removeScriptToEvaluateOnNewDocument",
|
||||
"params": {
|
||||
"identifier": script_id
|
||||
}
|
||||
}, False)
|
||||
|
||||
finally:
|
||||
if manage_socket:
|
||||
await self.close_websocket()
|
||||
|
||||
async def has_element(self, element_name: str, manage_socket: bool = True):
|
||||
res = await self.evaluate_js(f"document.getElementById('{element_name}') != null", False, manage_socket)
|
||||
assert res is not None
|
||||
|
||||
if not "result" in res or not "result" in res["result"] or not "value" in res["result"]["result"]:
|
||||
return False
|
||||
|
||||
return res["result"]["result"]["value"]
|
||||
|
||||
async def inject_css(self, style: str, manage_socket: bool = True):
|
||||
try:
|
||||
css_id = str(uuid.uuid4())
|
||||
|
||||
result = await self.evaluate_js(
|
||||
f"""
|
||||
(function() {{
|
||||
const style = document.createElement('style');
|
||||
style.id = "{css_id}";
|
||||
document.head.append(style);
|
||||
style.textContent = `{style}`;
|
||||
}})()
|
||||
""", False, manage_socket)
|
||||
|
||||
assert result is not None
|
||||
|
||||
if "exceptionDetails" in result["result"]:
|
||||
return {
|
||||
"success": False,
|
||||
"result": result["result"]
|
||||
}
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"result": css_id
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"result": e
|
||||
}
|
||||
|
||||
async def remove_css(self, css_id: str, manage_socket: bool = True):
|
||||
try:
|
||||
result = await self.evaluate_js(
|
||||
f"""
|
||||
(function() {{
|
||||
let style = document.getElementById("{css_id}");
|
||||
|
||||
if (style.nodeName.toLowerCase() == 'style')
|
||||
style.parentNode.removeChild(style);
|
||||
}})()
|
||||
""", False, manage_socket)
|
||||
|
||||
assert result is not None
|
||||
|
||||
if "exceptionDetails" in result["result"]:
|
||||
return {
|
||||
"success": False,
|
||||
"result": result
|
||||
}
|
||||
|
||||
return {
|
||||
"success": True
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"result": e
|
||||
}
|
||||
|
||||
async def get_steam_resource(self, url: str):
|
||||
res = await self.evaluate_js(f'(async function test() {{ return await (await fetch("{url}")).text() }})()', True)
|
||||
assert res is not None
|
||||
return res["result"]["result"]["value"]
|
||||
|
||||
def __repr__(self):
|
||||
return self.title
|
||||
|
||||
|
||||
async def get_tabs() -> List[Tab]:
|
||||
res = {}
|
||||
|
||||
na = False
|
||||
while True:
|
||||
try:
|
||||
async with ClientSession() as web:
|
||||
res = await web.get(f"{BASE_ADDRESS}/json", timeout=3)
|
||||
except ClientConnectorError:
|
||||
if not na:
|
||||
logger.debug("Steam isn't available yet. Wait for a moment...")
|
||||
na = True
|
||||
await sleep(5)
|
||||
except ClientOSError:
|
||||
logger.warning(f"The request to {BASE_ADDRESS}/json was reset")
|
||||
await sleep(1)
|
||||
except TimeoutError:
|
||||
logger.warning(f"The request to {BASE_ADDRESS}/json timed out")
|
||||
await sleep(1)
|
||||
else:
|
||||
break
|
||||
|
||||
if res.status == 200:
|
||||
r = await res.json()
|
||||
return [Tab(i) for i in r]
|
||||
else:
|
||||
raise Exception(f"/json did not return 200. {await res.text()}")
|
||||
|
||||
|
||||
async def get_tab(tab_name: str) -> Tab:
|
||||
tabs = await get_tabs()
|
||||
tab = next((i for i in tabs if i.title == tab_name), None)
|
||||
if not tab:
|
||||
raise ValueError(f"Tab {tab_name} not found")
|
||||
return tab
|
||||
|
||||
async def get_tab_lambda(test: Callable[[Tab], bool]) -> Tab:
|
||||
tabs = await get_tabs()
|
||||
tab = next((i for i in tabs if test(i)), None)
|
||||
if not tab:
|
||||
raise ValueError(f"Tab not found by lambda")
|
||||
return tab
|
||||
|
||||
SHARED_CTX_NAMES = ["SharedJSContext", "Steam Shared Context presented by Valve™", "Steam", "SP"]
|
||||
CLOSEABLE_URLS = ["about:blank", "data:text/html,%3Cbody%3E%3C%2Fbody%3E"] # Closing anything other than these *really* likes to crash Steam
|
||||
DO_NOT_CLOSE_URLS = ["Valve Steam Gamepad/default", "Valve%20Steam%20Gamepad"] # Steam Big Picture Mode tab
|
||||
|
||||
def tab_is_gamepadui(t: Tab) -> bool:
|
||||
return ("https://steamloopback.host/routes/" in t.url or "https://steamloopback.host/index.html" in t.url) and t.title in SHARED_CTX_NAMES
|
||||
|
||||
async def get_gamepadui_tab() -> Tab:
|
||||
tabs = await get_tabs()
|
||||
tab = next((i for i in tabs if tab_is_gamepadui(i)), None)
|
||||
if not tab:
|
||||
raise ValueError(f"GamepadUI Tab not found")
|
||||
return tab
|
||||
|
||||
async def inject_to_tab(tab_name: str, js: str, run_async: bool = False):
|
||||
tab = await get_tab(tab_name)
|
||||
|
||||
return await tab.evaluate_js(js, run_async)
|
||||
|
||||
async def close_old_tabs():
|
||||
tabs = await get_tabs()
|
||||
for t in tabs:
|
||||
if not t.title or (t.title not in SHARED_CTX_NAMES and any(url in t.url for url in CLOSEABLE_URLS) and not any(url in t.url for url in DO_NOT_CLOSE_URLS)):
|
||||
logger.debug("Closing tab: " + getattr(t, "title", "Untitled"))
|
||||
await t.close()
|
||||
await sleep(0.5)
|
||||
@@ -0,0 +1,233 @@
|
||||
from __future__ import annotations
|
||||
from asyncio import AbstractEventLoop, Queue, gather, sleep
|
||||
from logging import getLogger
|
||||
from os import listdir, path
|
||||
from pathlib import Path
|
||||
from traceback import print_exc, format_exc
|
||||
from typing import Any, Tuple, Dict, cast
|
||||
|
||||
from aiohttp import web
|
||||
from os.path import exists
|
||||
from decky_loader.helpers import get_homebrew_path
|
||||
from watchdog.events import RegexMatchingEventHandler, FileSystemEvent
|
||||
from watchdog.observers import Observer
|
||||
|
||||
from typing import TYPE_CHECKING, List
|
||||
if TYPE_CHECKING:
|
||||
from .main import PluginManager
|
||||
|
||||
from .plugin.plugin import PluginWrapper
|
||||
from .wsrouter import WSRouter
|
||||
from .enums import PluginLoadType
|
||||
|
||||
Plugins = dict[str, PluginWrapper]
|
||||
ReloadQueue = Queue[Tuple[str, str, bool | None] | Tuple[str, str]]
|
||||
|
||||
class FileChangeHandler(RegexMatchingEventHandler):
|
||||
def __init__(self, queue: ReloadQueue, plugin_path: str) -> None:
|
||||
super().__init__(regexes=[r'^.*?dist\/index\.js$', r'^.*?main\.py$']) # pyright: ignore [reportUnknownMemberType]
|
||||
self.logger = getLogger("file-watcher")
|
||||
self.plugin_path = plugin_path
|
||||
self.queue = queue
|
||||
self.disabled = True
|
||||
|
||||
def maybe_reload(self, src_path: str):
|
||||
if self.disabled:
|
||||
return
|
||||
plugin_dir = Path(path.relpath(src_path, self.plugin_path)).parts[0]
|
||||
if exists(path.join(self.plugin_path, plugin_dir, "plugin.json")):
|
||||
self.queue.put_nowait((path.join(self.plugin_path, plugin_dir, "main.py"), plugin_dir, True))
|
||||
|
||||
def on_created(self, event: FileSystemEvent):
|
||||
src_path = cast(str, event.src_path) #type: ignore # this is the correct type for this is in later versions of watchdog
|
||||
if "__pycache__" in src_path:
|
||||
return
|
||||
|
||||
# check to make sure this isn't a directory
|
||||
if path.isdir(src_path):
|
||||
return
|
||||
|
||||
# get the directory name of the plugin so that we can find its "main.py" and reload it; the
|
||||
# file that changed is not necessarily the one that needs to be reloaded
|
||||
self.logger.debug(f"file created: {src_path}")
|
||||
self.maybe_reload(src_path)
|
||||
|
||||
def on_modified(self, event: FileSystemEvent):
|
||||
src_path = cast(str, event.src_path) # type: ignore
|
||||
if "__pycache__" in src_path:
|
||||
return
|
||||
|
||||
# check to make sure this isn't a directory
|
||||
if path.isdir(src_path):
|
||||
return
|
||||
|
||||
# get the directory name of the plugin so that we can find its "main.py" and reload it; the
|
||||
# file that changed is not necessarily the one that needs to be reloaded
|
||||
self.logger.debug(f"file modified: {src_path}")
|
||||
self.maybe_reload(src_path)
|
||||
|
||||
class Loader:
|
||||
def __init__(self, server_instance: PluginManager, ws: WSRouter, plugin_path: str, loop: AbstractEventLoop, live_reload: bool = False) -> None:
|
||||
self.loop = loop
|
||||
self.logger = getLogger("Loader")
|
||||
self.ws = ws
|
||||
self.plugin_path = plugin_path
|
||||
self.logger.info(f"plugin_path: {self.plugin_path}")
|
||||
self.plugins: Plugins = {}
|
||||
self.watcher = None
|
||||
self.live_reload = live_reload
|
||||
self.reload_queue: ReloadQueue = Queue()
|
||||
self.loop.create_task(self.handle_reloads())
|
||||
self.context: PluginManager = server_instance
|
||||
|
||||
if live_reload:
|
||||
self.observer = Observer()
|
||||
self.watcher = FileChangeHandler(self.reload_queue, plugin_path)
|
||||
self.observer.schedule(self.watcher, self.plugin_path, recursive=True) # pyright: ignore [reportUnknownMemberType]
|
||||
self.observer.start()
|
||||
self.loop.create_task(self.enable_reload_wait())
|
||||
|
||||
server_instance.web_app.add_routes([
|
||||
web.get("/frontend/{path:.*}", self.handle_frontend_assets),
|
||||
web.get("/locales/{path:.*}", self.handle_frontend_locales),
|
||||
web.get("/plugins/{plugin_name}/frontend_bundle", self.handle_frontend_bundle),
|
||||
web.get("/plugins/{plugin_name}/dist/{path:.*}", self.handle_plugin_dist),
|
||||
web.get("/plugins/{plugin_name}/assets/{path:.*}", self.handle_plugin_frontend_assets),
|
||||
web.get("/plugins/{plugin_name}/data/{path:.*}", self.handle_plugin_frontend_assets_from_data),
|
||||
])
|
||||
|
||||
server_instance.ws.add_route("loader/get_plugins", self.get_plugins)
|
||||
server_instance.ws.add_route("loader/reload_plugin", self.handle_plugin_backend_reload)
|
||||
server_instance.ws.add_route("loader/call_plugin_method", self.handle_plugin_method_call)
|
||||
server_instance.ws.add_route("loader/call_legacy_plugin_method", self.handle_plugin_method_call_legacy)
|
||||
|
||||
async def shutdown_plugins(self):
|
||||
await gather(*[self.plugins[plugin_name].stop() for plugin_name in self.plugins])
|
||||
|
||||
async def enable_reload_wait(self):
|
||||
if self.live_reload:
|
||||
await sleep(10)
|
||||
if self.watcher and self.live_reload:
|
||||
self.logger.info("Hot reload enabled")
|
||||
self.watcher.disabled = False
|
||||
|
||||
async def disable_reload(self):
|
||||
if self.watcher:
|
||||
self.watcher.disabled = True
|
||||
self.live_reload = False
|
||||
|
||||
async def handle_frontend_assets(self, request: web.Request):
|
||||
file = Path(__file__).parent.joinpath("static").joinpath(request.match_info["path"])
|
||||
return web.FileResponse(file, headers={"Cache-Control": "no-cache"})
|
||||
|
||||
async def handle_frontend_locales(self, request: web.Request):
|
||||
req_lang = request.match_info["path"]
|
||||
file = Path(__file__).parent.joinpath("locales").joinpath(req_lang)
|
||||
if exists(file):
|
||||
return web.FileResponse(file, headers={"Cache-Control": "no-cache", "Content-Type": "application/json"})
|
||||
else:
|
||||
self.logger.info(f"Language {req_lang} not available, returning an empty dictionary")
|
||||
return web.json_response(data={}, headers={"Cache-Control": "no-cache"})
|
||||
|
||||
async def get_plugins(self):
|
||||
plugins = list(self.plugins.values())
|
||||
return [{"name": str(i), "version": i.version, "load_type": i.load_type, "disabled": i.disabled} for i in plugins]
|
||||
|
||||
async def handle_plugin_dist(self, request: web.Request):
|
||||
plugin = self.plugins[request.match_info["plugin_name"]]
|
||||
file = path.join(self.plugin_path, plugin.plugin_directory, "dist", request.match_info["path"])
|
||||
|
||||
return web.FileResponse(file, headers={"Cache-Control": "no-cache"})
|
||||
|
||||
async def handle_plugin_frontend_assets(self, request: web.Request):
|
||||
plugin = self.plugins[request.match_info["plugin_name"]]
|
||||
file = path.join(self.plugin_path, plugin.plugin_directory, "dist/assets", request.match_info["path"])
|
||||
|
||||
return web.FileResponse(file, headers={"Cache-Control": "no-cache"})
|
||||
|
||||
async def handle_plugin_frontend_assets_from_data(self, request: web.Request):
|
||||
plugin = self.plugins[request.match_info["plugin_name"]]
|
||||
home = get_homebrew_path()
|
||||
file = path.join(home, "data", plugin.plugin_directory, request.match_info["path"])
|
||||
|
||||
return web.FileResponse(file, headers={"Cache-Control": "no-cache"})
|
||||
|
||||
async def handle_frontend_bundle(self, request: web.Request):
|
||||
plugin = self.plugins[request.match_info["plugin_name"]]
|
||||
|
||||
with open(path.join(self.plugin_path, plugin.plugin_directory, "dist/index.js"), "r", encoding="utf-8") as bundle:
|
||||
return web.Response(text=bundle.read(), content_type="application/javascript")
|
||||
|
||||
async def import_plugin(self, file: str, plugin_directory: str, refresh: bool | None = False, batch: bool | None = False):
|
||||
try:
|
||||
async def plugin_emitted_event(event: str, args: Any):
|
||||
self.logger.debug(f"PLUGIN EMITTED EVENT: {event} with args {args}")
|
||||
await self.ws.emit(f"loader/plugin_event", {"plugin": plugin.name, "event": event, "args": args})
|
||||
|
||||
plugin = PluginWrapper(file, plugin_directory, self.plugin_path, plugin_emitted_event)
|
||||
if hasattr(self.context, "utilities") and plugin.name in await self.context.utilities.get_setting("disabled_plugins",[]):
|
||||
plugin.disabled = True
|
||||
self.plugins[plugin.name] = plugin
|
||||
return
|
||||
if plugin.name in self.plugins:
|
||||
if not "debug" in plugin.flags and refresh:
|
||||
self.logger.info(f"Plugin {plugin.name} is already loaded and has requested to not be re-loaded")
|
||||
return
|
||||
else:
|
||||
await self.plugins[plugin.name].stop()
|
||||
self.plugins.pop(plugin.name, None)
|
||||
if plugin.passive:
|
||||
self.logger.info(f"Plugin {plugin.name} is passive")
|
||||
|
||||
self.plugins[plugin.name] = plugin.start()
|
||||
self.logger.info(f"Loaded {plugin.name}")
|
||||
if not batch:
|
||||
self.loop.create_task(self.dispatch_plugin(plugin.name, plugin.version, plugin.load_type))
|
||||
except Exception as e:
|
||||
self.logger.error(f"Could not load {file}. {e}")
|
||||
print_exc()
|
||||
|
||||
async def dispatch_plugin(self, name: str, version: str | None, load_type: int = PluginLoadType.ESMODULE_V1.value):
|
||||
await self.ws.emit("loader/import_plugin", name, version, load_type, True, 15000)
|
||||
|
||||
async def import_plugins(self):
|
||||
self.logger.info(f"import plugins from {self.plugin_path}")
|
||||
|
||||
directories = [i for i in listdir(self.plugin_path) if path.isdir(path.join(self.plugin_path, i)) and path.isfile(path.join(self.plugin_path, i, "plugin.json"))]
|
||||
for directory in directories:
|
||||
self.logger.info(f"found plugin: {directory}")
|
||||
await self.import_plugin(path.join(self.plugin_path, directory, "main.py"), directory, False, True)
|
||||
|
||||
async def handle_reloads(self):
|
||||
while True:
|
||||
args = await self.reload_queue.get()
|
||||
await self.import_plugin(*args) # pyright: ignore [reportArgumentType]
|
||||
|
||||
async def handle_plugin_method_call_legacy(self, plugin_name: str, method_name: str, kwargs: Dict[Any, Any]):
|
||||
res: Dict[Any, Any] = {}
|
||||
plugin = self.plugins[plugin_name]
|
||||
try:
|
||||
if method_name.startswith("_"):
|
||||
raise RuntimeError(f"Plugin {plugin.name} tried to call private method {method_name}")
|
||||
res["result"] = await plugin.execute_legacy_method(method_name, kwargs)
|
||||
res["success"] = True
|
||||
except Exception as e:
|
||||
res["result"] = str(e)
|
||||
res["success"] = False
|
||||
return res
|
||||
|
||||
async def handle_plugin_method_call(self, plugin_name: str, method_name: str, *args: List[Any]):
|
||||
plugin = self.plugins[plugin_name]
|
||||
try:
|
||||
if method_name.startswith("_"):
|
||||
raise RuntimeError(f"Plugin {plugin.name} tried to call private method {method_name}")
|
||||
result = await plugin.execute_method(method_name, *args)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Method {method_name} of plugin {plugin.name} failed with the following exception:\n{format_exc()}")
|
||||
raise e # throw again to pass the error to the frontend
|
||||
return result
|
||||
|
||||
async def handle_plugin_backend_reload(self, plugin_name: str):
|
||||
plugin = self.plugins[plugin_name]
|
||||
|
||||
await self.reload_queue.put((plugin.file, plugin.plugin_directory))
|
||||
@@ -0,0 +1,156 @@
|
||||
{
|
||||
"BranchSelect": {
|
||||
"update_channel": {
|
||||
"label": "قناة التحديثات",
|
||||
"prerelease": "الإصدار التجريبي",
|
||||
"stable": "إصدار مستقر",
|
||||
"testing": "إصدار تحت الإختبار"
|
||||
}
|
||||
},
|
||||
"Developer": {
|
||||
"5secreload": "إعادة التحميل في 5 ثواني"
|
||||
},
|
||||
"DropdownMultiselect": {
|
||||
"button": {
|
||||
"back": "الخلف"
|
||||
}
|
||||
},
|
||||
"FilePickerIndex": {
|
||||
"file": {
|
||||
"select": "إختر هذا الملف"
|
||||
},
|
||||
"files": {
|
||||
"all_files": "جميع الملفات",
|
||||
"file_type": "نوع الملف",
|
||||
"show_hidden": "أظهر الملفات المخفية"
|
||||
},
|
||||
"filter": {
|
||||
"created_asce": "المنشئة (الأقدم)",
|
||||
"created_desc": "المنشئة (الأحدث)",
|
||||
"modified_asce": "المعدلة (الأقدم)",
|
||||
"modified_desc": "المعدلة (الأحدث)",
|
||||
"name_asce": "أ-ي",
|
||||
"name_desc": "أ-ي",
|
||||
"size_asce": "الحجم ( الأصغر)",
|
||||
"size_desc": "الحجم ( الأكبر)"
|
||||
},
|
||||
"folder": {
|
||||
"label": "المجلد",
|
||||
"select": "إستخدم هذا المجلد",
|
||||
"show_more": "أظهر المزيد من الملفات"
|
||||
}
|
||||
},
|
||||
"MultiplePluginsInstallModal": {
|
||||
"confirm": "هل أنت متأكد من التعديلات التالية؟",
|
||||
"description": {
|
||||
"install": "تنصيب {{name}} {{version}}",
|
||||
"reinstall": "إعادة تنصيب {{name}} {{version}}",
|
||||
"update": "تحديث {{name}} إلى {{version}}"
|
||||
},
|
||||
"ok_button": {
|
||||
"idle": "تأكيد"
|
||||
},
|
||||
"title": {
|
||||
"install_few": "",
|
||||
"install_many": "",
|
||||
"install_one": "",
|
||||
"install_other": "",
|
||||
"install_two": "",
|
||||
"install_zero": "",
|
||||
"mixed_few": "",
|
||||
"mixed_many": "",
|
||||
"mixed_one": "",
|
||||
"mixed_other": "",
|
||||
"mixed_two": "",
|
||||
"mixed_zero": "",
|
||||
"reinstall_few": "",
|
||||
"reinstall_many": "",
|
||||
"reinstall_one": "",
|
||||
"reinstall_other": "",
|
||||
"reinstall_two": "",
|
||||
"reinstall_zero": "",
|
||||
"update_few": "",
|
||||
"update_many": "",
|
||||
"update_one": "",
|
||||
"update_other": "",
|
||||
"update_two": "",
|
||||
"update_zero": ""
|
||||
}
|
||||
},
|
||||
"PluginCard": {
|
||||
"plugin_full_access": "هذه الإضافة لديها الصلاحية للوصول لمحتويات Steam Deck.",
|
||||
"plugin_install": "تنصيب",
|
||||
"plugin_no_desc": "لا يوجد وصف متاح.",
|
||||
"plugin_version_label": "رقم إصدار الإضافة"
|
||||
},
|
||||
"PluginInstallModal": {
|
||||
"install": {
|
||||
"button_idle": "تنصيب",
|
||||
"button_processing": "يتم التنصيب",
|
||||
"desc": "هل أنت متأكد من رغبتك في تنصيب {{artifact}} {{version}}؟",
|
||||
"title": "تنصيب {{artifact}}"
|
||||
},
|
||||
"reinstall": {
|
||||
"button_idle": "إعادة تنصيب",
|
||||
"button_processing": "تتم إعادة التنصيب",
|
||||
"desc": "هل أنت متأكد من رغبتك في إعادة تنصيب {{artifact}} {{version}}؟",
|
||||
"title": "إعادة تنصيب {{artifact}}"
|
||||
},
|
||||
"update": {
|
||||
"button_idle": "تحديث",
|
||||
"button_processing": "يتم التحديث",
|
||||
"desc": "هل أنت متأكد من رغبتك في تحديث {{artifact}} {{version}}؟",
|
||||
"title": "تحديث {{artifact}}"
|
||||
}
|
||||
},
|
||||
"PluginListIndex": {
|
||||
"hide": "إخفاء من قائمة الوصول السريع",
|
||||
"reinstall": "إعادة التنصيب",
|
||||
"reload": "إعادة التحميل",
|
||||
"show": "إظهار في قائمة الوصول السريع",
|
||||
"unfreeze": "السماح بالتحديثات",
|
||||
"uninstall": "إزالة التنصيب",
|
||||
"update_all_few": "",
|
||||
"update_all_many": "",
|
||||
"update_all_one": "",
|
||||
"update_all_other": "",
|
||||
"update_all_two": "",
|
||||
"update_all_zero": "",
|
||||
"update_to": "التحديث إلى {{name}}"
|
||||
},
|
||||
"PluginListLabel": {
|
||||
"hidden": "مخفي من قائمة الوصول السريع"
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
"error": "خطا",
|
||||
"plugin_uninstall": {
|
||||
"button": "إزالة التنصيب",
|
||||
"title": "إزالة التنصيب {{name}}"
|
||||
},
|
||||
"plugin_update_few": "",
|
||||
"plugin_update_many": "",
|
||||
"plugin_update_one": "",
|
||||
"plugin_update_other": "",
|
||||
"plugin_update_two": "",
|
||||
"plugin_update_zero": ""
|
||||
},
|
||||
"PluginView": {
|
||||
"hidden_few": "",
|
||||
"hidden_many": "",
|
||||
"hidden_one": "",
|
||||
"hidden_other": "",
|
||||
"hidden_two": "",
|
||||
"hidden_zero": ""
|
||||
},
|
||||
"SettingsDeveloperIndex": {
|
||||
"header": "أخرى",
|
||||
"react_devtools": {
|
||||
"ip_label": "عنوان الشبكة"
|
||||
},
|
||||
"third_party_plugins": {
|
||||
"button_install": "تنصيب",
|
||||
"button_zip": "تصفح"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
{
|
||||
"BranchSelect": {
|
||||
"update_channel": {
|
||||
"label": "Канал за обновления",
|
||||
"prerelease": "Предварителни издания",
|
||||
"stable": "Стабилен",
|
||||
"testing": "Тестване"
|
||||
}
|
||||
},
|
||||
"Developer": {
|
||||
"5secreload": "Презареждане след 5 секунди",
|
||||
"disabling": "Изключване на React DevTools",
|
||||
"enabling": "Включване на React DevTools"
|
||||
},
|
||||
"DropdownMultiselect": {
|
||||
"button": {
|
||||
"back": "Назад"
|
||||
}
|
||||
},
|
||||
"FilePickerError": {
|
||||
"errors": {
|
||||
"file_not_found": "Посоченият път е неправилен. Проверете го и го въведете правилно.",
|
||||
"perm_denied": "Нямате достъп до посочената папка. Проверете дали потребителят (deck на Steam Deck) има съответните правомощия за достъп до посочената папка/файл.",
|
||||
"unknown": "Възникна неизвестна грешка. Грешката в суров вид е: {{raw_error}}"
|
||||
}
|
||||
},
|
||||
"FilePickerIndex": {
|
||||
"file": {
|
||||
"select": "Избиране на този файл"
|
||||
},
|
||||
"files": {
|
||||
"all_files": "Всички файлове",
|
||||
"file_type": "Файлов тип",
|
||||
"show_hidden": "Показване на скритите файлове"
|
||||
},
|
||||
"filter": {
|
||||
"created_asce": "Дата на създаване (първо най-старите)",
|
||||
"created_desc": "Дата на създаване (първо най-новите)",
|
||||
"modified_asce": "Дата на промяна (първо най-старите)",
|
||||
"modified_desc": "Дата на промяна (първо най-новите)",
|
||||
"name_asce": "Я-А",
|
||||
"name_desc": "А-Я",
|
||||
"size_asce": "Размер (първо най-малките)",
|
||||
"size_desc": "Размер (първо най-големите)"
|
||||
},
|
||||
"folder": {
|
||||
"label": "Папка",
|
||||
"select": "Използване на тази папка",
|
||||
"show_more": "Показване на още файлове"
|
||||
}
|
||||
},
|
||||
"MultiplePluginsInstallModal": {
|
||||
"confirm": "Наистина ли искате да направите следните промени?",
|
||||
"description": {
|
||||
"install": "Инсталиране на {{name}} {{version}}",
|
||||
"reinstall": "Преинсталиране на {{name}} {{version}}",
|
||||
"update": "Обновяване на {{name}} до {{version}}"
|
||||
},
|
||||
"ok_button": {
|
||||
"idle": "Потвърждаване",
|
||||
"loading": "В процес на работа"
|
||||
},
|
||||
"title": {
|
||||
"install_one": "Инсталиране на 1 добавка",
|
||||
"install_other": "Инсталиране на {{count}} добавки",
|
||||
"mixed_one": "Промяна на {{count}} добавка",
|
||||
"mixed_other": "Промяна на {{count}} добавки",
|
||||
"reinstall_one": "Преинсталиране на 1 добавка",
|
||||
"reinstall_other": "Преинсталиране на {{count}} добавки",
|
||||
"update_one": "Обновяване на 1 добавка",
|
||||
"update_other": "Обновяване на {{count}} добавки"
|
||||
}
|
||||
},
|
||||
"PluginCard": {
|
||||
"plugin_full_access": "Тази добавка има пълен достъп до Вашия Steam Deck.",
|
||||
"plugin_install": "Инсталиране",
|
||||
"plugin_no_desc": "Няма описание.",
|
||||
"plugin_version_label": "Версия на добавката"
|
||||
},
|
||||
"PluginInstallModal": {
|
||||
"install": {
|
||||
"button_idle": "Инсталиране",
|
||||
"button_processing": "Инсталиране",
|
||||
"desc": "Наистина ли искате да инсталирате {{artifact}} {{version}}?",
|
||||
"title": "Инсталиране на {{artifact}}"
|
||||
},
|
||||
"no_hash": "Тази добавка няма хеш. Инсталирате я на свой собствен риск.",
|
||||
"reinstall": {
|
||||
"button_idle": "Преинсталиране",
|
||||
"button_processing": "Преинсталиране",
|
||||
"desc": "Наистина ли искате да преинсталирате {{artifact}} {{version}}?",
|
||||
"title": "Преинсталиране на {{artifact}}"
|
||||
},
|
||||
"update": {
|
||||
"button_idle": "Обновяване",
|
||||
"button_processing": "Обновяване",
|
||||
"desc": "Наистина ли искате да обновите {{artifact}} {{version}}?",
|
||||
"title": "Обновяване на {{artifact}}"
|
||||
}
|
||||
},
|
||||
"PluginListIndex": {
|
||||
"freeze": "Замразяване на актуализациите",
|
||||
"hide": "Бърз достъп: Скриване",
|
||||
"no_plugin": "Няма инсталирани добавки!",
|
||||
"plugin_actions": "Действия с добавката",
|
||||
"reinstall": "Преинсталиране",
|
||||
"reload": "Презареждане",
|
||||
"show": "Бърз достъп: Показване",
|
||||
"unfreeze": "Разрешаване на актуализациите",
|
||||
"uninstall": "Деинсталиране",
|
||||
"update_all_one": "Обновяване на 1 добавка",
|
||||
"update_all_other": "Обновяване на {{count}} добавки",
|
||||
"update_to": "Обновяване до {{name}}"
|
||||
},
|
||||
"PluginListLabel": {
|
||||
"hidden": "Скрито от менюто за бърз достъп"
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
"decky_update_available": "Има налично обновление до {{tag_name}}!",
|
||||
"error": "Грешка",
|
||||
"plugin_error_uninstall": "Зареждането на {{name}} предизвика грешка, както се вижда по-горе. Това обикновено означава, че добавката изисква обновяване на новата версия на SteamUI. Проверете дали има обновление или изберете да я премахнете в настройките на Decky, в раздела с добавките.",
|
||||
"plugin_load_error": {
|
||||
"message": "Грешка при зареждането на добавката {{name}}",
|
||||
"toast": "Грешка при зареждането на {{name}}"
|
||||
},
|
||||
"plugin_uninstall": {
|
||||
"button": "Деинсталиране",
|
||||
"desc": "Наистина ли искате да деинсталирате {{name}}?",
|
||||
"title": "Деинсталиране на {{name}}"
|
||||
},
|
||||
"plugin_update_one": "Има налично обновление за 1 добавка!",
|
||||
"plugin_update_other": "Има налични обновления за {{count}} добавки!"
|
||||
},
|
||||
"PluginView": {
|
||||
"hidden_one": "1 добавка е скрита от този списък",
|
||||
"hidden_other": "{{count}} добавки са скрити от този списък"
|
||||
},
|
||||
"RemoteDebugging": {
|
||||
"remote_cef": {
|
||||
"desc": "Разрешаване на достъп без удостоверяване до дебъгера на CEF на всеки от Вашата мрежа",
|
||||
"label": "Разрешаване на отдалеченото дебъгване на CEF"
|
||||
}
|
||||
},
|
||||
"SettingsDeveloperIndex": {
|
||||
"cef_console": {
|
||||
"button": "Отваряне на конзолата",
|
||||
"desc": "Отваря конзолата на CEF. Това има смисъл единствено за дебъгване. Нещата тук може да са опасни и трябва да бъдат използвани само ако Вие сте разработчик на добавка, или получавате насоки от такъв.",
|
||||
"label": "Конзола на CEF"
|
||||
},
|
||||
"header": "Други",
|
||||
"react_devtools": {
|
||||
"desc": "Включва свързването към компютър, на който работи React DevTools. Промяната на тази настройка ще презареди Steam. Задайте IP адреса преди да включите това.",
|
||||
"ip_label": "IP",
|
||||
"label": "Включване на React DevTools"
|
||||
},
|
||||
"third_party_plugins": {
|
||||
"button_install": "Инсталиране",
|
||||
"button_zip": "Разглеждане",
|
||||
"header": "Добавки от външен източник",
|
||||
"label_desc": "Адрес",
|
||||
"label_url": "Инсталиране на добавка от адрес в Интернет",
|
||||
"label_zip": "Инсталиране на добавка от файл ZIP"
|
||||
},
|
||||
"valve_internal": {
|
||||
"desc1": "Включва вътрешното меню за разработчици на Valve.",
|
||||
"desc2": "Не пипайте нищо в това меню, освен ако не знаете какво правите.",
|
||||
"label": "Включване на вътрешното меню на Valve"
|
||||
}
|
||||
},
|
||||
"SettingsGeneralIndex": {
|
||||
"about": {
|
||||
"decky_version": "Версия на Decky",
|
||||
"header": "Относно"
|
||||
},
|
||||
"beta": {
|
||||
"header": "Участие в бета-версии"
|
||||
},
|
||||
"developer_mode": {
|
||||
"label": "Режим за разработчици"
|
||||
},
|
||||
"notifications": {
|
||||
"decky_updates_label": "Има налично обновление на Decky",
|
||||
"header": "Известия",
|
||||
"plugin_updates_label": "Има налични обновления на добавките"
|
||||
},
|
||||
"other": {
|
||||
"header": "Други"
|
||||
},
|
||||
"updates": {
|
||||
"header": "Обновления"
|
||||
}
|
||||
},
|
||||
"SettingsIndex": {
|
||||
"developer_title": "Разработчик",
|
||||
"general_title": "Общи",
|
||||
"plugins_title": "Добавки",
|
||||
"testing_title": "Тестване"
|
||||
},
|
||||
"Store": {
|
||||
"download_progress_info": {
|
||||
"download_zip": "Изтегляне на плъгина",
|
||||
"increment_count": "Увеличаване на броя изтегляния",
|
||||
"installing_plugin": "Инсталиране на плъгина",
|
||||
"open_zip": "Отваряне на zip файла",
|
||||
"parse_zip": "Разглеждане на zip файла",
|
||||
"start": "Иницииране",
|
||||
"uninstalling_previous": "Деинсталиране на предишното копие"
|
||||
},
|
||||
"store_contrib": {
|
||||
"desc": "Ако искате да допринесете към магазина за добавки на Decky, разгледайте хранилището SteamDeckHomebrew/decky-plugin-template в GitHub. Може да намерите информация относно разработката и разпространението във файла README.",
|
||||
"label": "Допринасяне"
|
||||
},
|
||||
"store_filter": {
|
||||
"label": "Филтър",
|
||||
"label_def": "Всички"
|
||||
},
|
||||
"store_search": {
|
||||
"label": "Търсене"
|
||||
},
|
||||
"store_sort": {
|
||||
"label": "Подредба",
|
||||
"label_def": "Последно обновление (първо най-новите)"
|
||||
},
|
||||
"store_source": {
|
||||
"desc": "Целият изходен код е наличен в хранилището SteamDeckHomebrew/decky-plugin-database в GitHub.",
|
||||
"label": "Изходен код"
|
||||
},
|
||||
"store_tabs": {
|
||||
"about": "Относно",
|
||||
"alph_asce": "По азбучен ред (Я -> А)",
|
||||
"alph_desc": "По азбучен ред (А -> Я)",
|
||||
"date_asce": "Най-старият първи",
|
||||
"date_desc": "Най-новият първи",
|
||||
"downloads_asce": "Най-малко изтеглени първи",
|
||||
"downloads_desc": "Най-изтеглени първи",
|
||||
"title": "Разглеждане"
|
||||
},
|
||||
"store_testing_cta": "Помислете дали искате да тествате новите добавки, за да помогнете на екипа на Decky Loader!",
|
||||
"store_testing_warning": {
|
||||
"desc": "Можете да използвате този канал на магазина, за да тествате най-новите версии на плъгините. Не забравяйте да оставите обратна връзка в GitHub, за да може плъгинът да бъде актуализиран за всички потребители.",
|
||||
"label": "Добре дошли в канала на магазина за тестване"
|
||||
}
|
||||
},
|
||||
"StoreSelect": {
|
||||
"custom_store": {
|
||||
"label": "Персонализиран магазин",
|
||||
"url_label": "Адрес"
|
||||
},
|
||||
"store_channel": {
|
||||
"custom": "Персонализиран",
|
||||
"default": "По подразбиране",
|
||||
"label": "Канал за магазина",
|
||||
"testing": "Тестване"
|
||||
}
|
||||
},
|
||||
"Testing": {
|
||||
"download": "Изтегляне",
|
||||
"error": "Грешка при инсталирането на PR",
|
||||
"header": "Следните версии на Decky Loader са създадени от отворени заявки за изтегляне от трети страни. Екипът на Decky Loader не е проверявал тяхната функционалност или сигурност и е възможно те да са остарели.",
|
||||
"loading": "Зареждане на отворени заявки за изтегляне...",
|
||||
"start_download_toast": "Изтегляне на PR #{{id}}"
|
||||
},
|
||||
"TitleView": {
|
||||
"decky_store_desc": "Отворете Decky Store",
|
||||
"settings_desc": "Отворете настройките на Decky"
|
||||
},
|
||||
"Updater": {
|
||||
"decky_updates": "Обновления на Decky",
|
||||
"no_patch_notes_desc": "няма бележки за промените в тази версия",
|
||||
"patch_notes_desc": "Бележки за промените",
|
||||
"updates": {
|
||||
"check_button": "Проверка за обновления",
|
||||
"checking": "Проверяване",
|
||||
"cur_version": "Текуща версия: {{ver}}",
|
||||
"install_button": "Инсталиране на обновлението",
|
||||
"label": "Обновления",
|
||||
"lat_version": "Използвате най-новата версия: {{ver}}",
|
||||
"reloading": "Презареждане",
|
||||
"updating": "Обновяване"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
{
|
||||
"BranchSelect": {
|
||||
"update_channel": {
|
||||
"label": "Aktualizační kanál",
|
||||
"prerelease": "Předběžná vydání",
|
||||
"stable": "Stabilní",
|
||||
"testing": "Testování"
|
||||
}
|
||||
},
|
||||
"Developer": {
|
||||
"5secreload": "Znovu načtení za 5 vteřin",
|
||||
"disabling": "Vypínám React DevTools",
|
||||
"enabling": "Zapínám React DevTools"
|
||||
},
|
||||
"DropdownMultiselect": {
|
||||
"button": {
|
||||
"back": "Zpět"
|
||||
}
|
||||
},
|
||||
"FilePickerError": {
|
||||
"errors": {
|
||||
"file_not_found": "Zadaná cesta není platná. Zkontrolujte ji a zadejte znovu správně.",
|
||||
"perm_denied": "Nemáte přístup k zadanému adresáři. Zkontrolujte, zda jako uživatel (deck na Steam Decku) máte odpovídající oprávnění pro přístup k dané složce/souboru.",
|
||||
"unknown": "Nastala neznámá chyba. Nezpracovaná chyba je: {{raw_error}}"
|
||||
}
|
||||
},
|
||||
"FilePickerIndex": {
|
||||
"file": {
|
||||
"select": "Vybrat tento soubor"
|
||||
},
|
||||
"files": {
|
||||
"all_files": "Všechny soubory",
|
||||
"file_type": "Typ souboru",
|
||||
"show_hidden": "Zobrazit skryté soubory"
|
||||
},
|
||||
"filter": {
|
||||
"created_asce": "Vytvořeno (Nejstarší)",
|
||||
"created_desc": "Vytvořeno (Nejnovější)",
|
||||
"modified_asce": "Upraveno (Nejstarší)",
|
||||
"modified_desc": "Upraveno (Nejnovější)",
|
||||
"name_asce": "Z-A",
|
||||
"name_desc": "A-Z",
|
||||
"size_asce": "Velikost (Nejmenší)",
|
||||
"size_desc": "Velikost (Největší)"
|
||||
},
|
||||
"folder": {
|
||||
"label": "Složka",
|
||||
"select": "Použít tuto složku",
|
||||
"show_more": "Zobrazit více souborů"
|
||||
}
|
||||
},
|
||||
"MultiplePluginsInstallModal": {
|
||||
"confirm": "Jste si jisti, že chcete udělat následující úpravy?",
|
||||
"description": {
|
||||
"downgrade": "Downgradovat {{name}} na verzi {{version}}",
|
||||
"install": "Instalovat {{name}} {{version}}",
|
||||
"overwrite": "Přepsat {{name}} verzí {{version}}",
|
||||
"reinstall": "Přeinstalovat {{name}} {{version}}",
|
||||
"update": "Aktualizovat {{name}} na {{version}}"
|
||||
},
|
||||
"ok_button": {
|
||||
"idle": "Potvrdit",
|
||||
"loading": "Probíhá"
|
||||
},
|
||||
"title": {
|
||||
"downgrade_few": "Downgradovat {{count}} pluginy",
|
||||
"downgrade_one": "Downgradovat {{count}} plugin",
|
||||
"downgrade_other": "Downgradovat {{count}} pluginů",
|
||||
"install_few": "Instalovat {{count}} pluginů",
|
||||
"install_one": "Instalovat 1 plugin",
|
||||
"install_other": "Instalovat {{count}} pluginů",
|
||||
"mixed_few": "Upravit {{count}} pluginů",
|
||||
"mixed_one": "Upravit {{count}} plugin",
|
||||
"mixed_other": "Upravit {{count}} pluginů",
|
||||
"overwrite_few": "Přepsat {{count}} pluginy",
|
||||
"overwrite_one": "Přepsat {{count}} plugin",
|
||||
"overwrite_other": "Přepsat {{count}} pluginů",
|
||||
"reinstall_few": "Přeinstalovat {{count}} pluginů",
|
||||
"reinstall_one": "Přeinstalovat 1 plugin",
|
||||
"reinstall_other": "Přeinstalovat {{count}} pluginů",
|
||||
"update_few": "Aktualizovat {{count}} pluginů",
|
||||
"update_one": "Aktualizovat 1 plugin",
|
||||
"update_other": "Aktualizovat {{count}} pluginů"
|
||||
}
|
||||
},
|
||||
"PluginCard": {
|
||||
"plugin_downgrade": "Downgrade",
|
||||
"plugin_full_access": "Tento plugin má plný přístup k vašemu Steam Decku.",
|
||||
"plugin_install": "Instalovat",
|
||||
"plugin_no_desc": "Nebyl uveden žádný popis.",
|
||||
"plugin_overwrite": "Přepsat",
|
||||
"plugin_reinstall": "Přeinstalovat",
|
||||
"plugin_update": "Aktualizovat",
|
||||
"plugin_version_label": "Verze pluginu"
|
||||
},
|
||||
"PluginInstallModal": {
|
||||
"downgrade": {
|
||||
"button_idle": "Downgrade",
|
||||
"button_processing": "Downgradování",
|
||||
"desc": "Opravdu chcete downgradovat {{artifact}} na verzi {{version}}?",
|
||||
"title": "Downgradovat {{artifact}}"
|
||||
},
|
||||
"install": {
|
||||
"button_idle": "Instalovat",
|
||||
"button_processing": "Instalování",
|
||||
"desc": "Jste si jisti, že chcete nainstalovat {{artifact}} {{version}}?",
|
||||
"title": "Instalovat {{artifact}}"
|
||||
},
|
||||
"no_hash": "Tento plugin nemá hash, instalujete jej na vlastní nebezpečí.",
|
||||
"not_installed": "(nenainstalováno)",
|
||||
"overwrite": {
|
||||
"button_idle": "Přepsat",
|
||||
"button_processing": "Přepisování",
|
||||
"desc": "Opravdu chcete přepsat {{artifact}} na verzi {{version}}?",
|
||||
"title": "Přepsat {{artifact}}"
|
||||
},
|
||||
"reinstall": {
|
||||
"button_idle": "Přeinstalovat",
|
||||
"button_processing": "Přeinstalování",
|
||||
"desc": "Jste si jisti, že chcete přeinstalovat {{artifact}} {{version}}?",
|
||||
"title": "Přeinstalovat {{artifact}}"
|
||||
},
|
||||
"update": {
|
||||
"button_idle": "Aktualizovat",
|
||||
"button_processing": "Aktualizování",
|
||||
"desc": "Opravdu chcete aktualizovat {{artifact}} na verzi {{version}}?",
|
||||
"title": "Aktualizovat {{artifact}}"
|
||||
}
|
||||
},
|
||||
"PluginListIndex": {
|
||||
"freeze": "Pozastavit aktualizace",
|
||||
"hide": "Rychlý přístup: Skrýt",
|
||||
"no_plugin": "Nejsou nainstalovány žádné pluginy!",
|
||||
"plugin_actions": "Akce pluginu",
|
||||
"reinstall": "Přeinstalovat",
|
||||
"reload": "Znovu načíst",
|
||||
"show": "Rychlý přístup: Zobrazit",
|
||||
"unfreeze": "Povolit aktualizace",
|
||||
"uninstall": "Odinstalovat",
|
||||
"update_all_few": "Aktualizovat {{count}} pluginů",
|
||||
"update_all_one": "Aktualizovat 1 plugin",
|
||||
"update_all_other": "Aktualizovat {{count}} pluginů",
|
||||
"update_to": "Aktualizovat na {{name}}"
|
||||
},
|
||||
"PluginListLabel": {
|
||||
"hidden": "Skryto z nabídky rychlého přístupu"
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
"decky_update_available": "Aktualizace na {{tag_name}} dostupná!",
|
||||
"error": "Chyba",
|
||||
"plugin_error_uninstall": "Načítání {{name}} způsobilo chybu uvedenou výše. To obvykle znamená, že plugin vyžaduje aktualizaci SteamUI. Zkontrolujte, zda je aktualizace k dispozici, nebo zvažte odstranění pluginu v nastavení Decky v sekci Pluginy.",
|
||||
"plugin_load_error": {
|
||||
"message": "Chyba při načítání pluginu {{name}}",
|
||||
"toast": "Chyba při načítání {{name}}"
|
||||
},
|
||||
"plugin_uninstall": {
|
||||
"button": "Odinstalovat",
|
||||
"desc": "Opravdu chcete odinstalovat {{name}}?",
|
||||
"title": "Odinstalovat {{name}}"
|
||||
},
|
||||
"plugin_update_few": "Jsou dostupné aktualizace pro {{count}} pluginů!",
|
||||
"plugin_update_one": "Je dostupná aktualizace pro 1 plugin!",
|
||||
"plugin_update_other": "Jsou dostupné aktualizace pro {{count}} pluginů!"
|
||||
},
|
||||
"PluginView": {
|
||||
"hidden_few": "{{count}} pluginů je v tomto seznamu skryto",
|
||||
"hidden_one": "1 plugin je v tomto seznamu skrytý",
|
||||
"hidden_other": "{{count}} pluginů je v tomto seznamu skryto"
|
||||
},
|
||||
"RemoteDebugging": {
|
||||
"remote_cef": {
|
||||
"desc": "Umožní neověřený přístup k CEF ladění komukoli ve vaší síti",
|
||||
"label": "Povolit vzdálené CEF ladění"
|
||||
}
|
||||
},
|
||||
"SettingsDeveloperIndex": {
|
||||
"cef_console": {
|
||||
"button": "Otevřít konzoli",
|
||||
"desc": "Otevře CEF konzoli. Užitečné pouze pro účely ladění. Věci zde jsou potenciálně nebezpečné a měly by být používány pouze v případě, že jste vývojář pluginů, nebo vás sem nějaký nasměroval.",
|
||||
"label": "CEF konzole"
|
||||
},
|
||||
"header": "Ostatní",
|
||||
"react_devtools": {
|
||||
"desc": "Umožňuje připojení k počítači, na kterém běží React DevTools. Změnou tohoto nastavení se znovu načte Steam. Před povolením nastavte IP adresu.",
|
||||
"ip_label": "IP adresa",
|
||||
"label": "Zapnout React DevTools"
|
||||
},
|
||||
"third_party_plugins": {
|
||||
"button_install": "Instalovat",
|
||||
"button_zip": "Procházet",
|
||||
"header": "Pluginy třetí strany",
|
||||
"label_desc": "URL",
|
||||
"label_url": "Instalovat plugin z URL",
|
||||
"label_zip": "Instalovat plugin ze ZIP souboru"
|
||||
},
|
||||
"valve_internal": {
|
||||
"desc1": "Zapíná interní vývojářské menu Valve.",
|
||||
"desc2": "Nedotýkejte se ničeho v této nabídce, pokud nevíte, co děláte.",
|
||||
"label": "Zapnout Valve Internal"
|
||||
}
|
||||
},
|
||||
"SettingsGeneralIndex": {
|
||||
"about": {
|
||||
"decky_version": "Decky verze",
|
||||
"header": "O Decky"
|
||||
},
|
||||
"beta": {
|
||||
"header": "Účast v betě"
|
||||
},
|
||||
"developer_mode": {
|
||||
"label": "Vývojářský režim"
|
||||
},
|
||||
"notifications": {
|
||||
"decky_updates_label": "Dostupná aktualizace Decky",
|
||||
"header": "Notifikace",
|
||||
"plugin_updates_label": "Dostupná aktualizace pluginu"
|
||||
},
|
||||
"other": {
|
||||
"header": "Ostatní"
|
||||
},
|
||||
"updates": {
|
||||
"header": "Aktualizace"
|
||||
}
|
||||
},
|
||||
"SettingsIndex": {
|
||||
"developer_title": "Vývojář",
|
||||
"general_title": "Obecné",
|
||||
"plugins_title": "Pluginy",
|
||||
"testing_title": "Testování"
|
||||
},
|
||||
"Store": {
|
||||
"download_progress_info": {
|
||||
"download_remote": "Stahování externích knihoven",
|
||||
"download_zip": "Stahování pluginu",
|
||||
"increment_count": "Zvyšující se počet stahování",
|
||||
"installing_plugin": "Instalování pluginu",
|
||||
"open_zip": "Otevírání ZIP souboru",
|
||||
"parse_zip": "Analýza ZIP souboru",
|
||||
"start": "Inicializace",
|
||||
"uninstalling_previous": "Odinstalování předchozí kopie"
|
||||
},
|
||||
"store_contrib": {
|
||||
"desc": "Pokud byste chtěli přispět do obchodu Decky Plugin Store, podívejte se na repozitář SteamDeckHomebrew/decky-plugin-template na GitHubu. Informace o vývoji a distribuci jsou k dispozici v README.",
|
||||
"label": "Přispívání"
|
||||
},
|
||||
"store_filter": {
|
||||
"label": "Filtr",
|
||||
"label_def": "Vše"
|
||||
},
|
||||
"store_search": {
|
||||
"label": "Hledat"
|
||||
},
|
||||
"store_sort": {
|
||||
"label": "Seřadit",
|
||||
"label_def": "Naposledy aktualizováno (Nejnovější)"
|
||||
},
|
||||
"store_source": {
|
||||
"desc": "Veškerý zdrojový kód pluginu je dostupný v repozitáři SteamDeckHomebrew/decky-plugin-database na GitHubu.",
|
||||
"label": "Zdrojový kód"
|
||||
},
|
||||
"store_tabs": {
|
||||
"about": "O Decky Plugin Store",
|
||||
"alph_asce": "Abecedně (Z do A)",
|
||||
"alph_desc": "Abecedně (A do Z)",
|
||||
"date_asce": "Nejstarší",
|
||||
"date_desc": "Nejnovější",
|
||||
"downloads_asce": "Nejméně stažené",
|
||||
"downloads_desc": "Nejvíce stažené",
|
||||
"title": "Procházet"
|
||||
},
|
||||
"store_testing_cta": "Zvažte prosím testování nových pluginů, pomůžete tím týmu Decky Loader!",
|
||||
"store_testing_warning": {
|
||||
"desc": "Tento kanál obchodu můžete použít k testování nejnovějších verzí pluginů. Nezapomeňte zanechat zpětnou vazbu na GitHubu, aby bylo možné plugin aktualizovat pro všechny uživatele.",
|
||||
"label": "Vítejte na testovacím kanálu obchodu"
|
||||
}
|
||||
},
|
||||
"StoreSelect": {
|
||||
"custom_store": {
|
||||
"label": "Vlastní obchod",
|
||||
"url_label": "URL"
|
||||
},
|
||||
"store_channel": {
|
||||
"custom": "Vlastní",
|
||||
"default": "Výchozí",
|
||||
"label": "Kanál obchodu",
|
||||
"testing": "Testování"
|
||||
}
|
||||
},
|
||||
"Testing": {
|
||||
"download": "Stáhnout",
|
||||
"error": "Chyba při instalaci PR",
|
||||
"header": "Následující verze Decky Loaderu jsou vytvořeny z otevřených Pull Requestů třetích stran. Tým Decky Loaderu neověřil jejich funkčnost ani zabezpečení a mohou být zastaralé.",
|
||||
"loading": "Načítání Pull Requestů...",
|
||||
"start_download_toast": "Stahování PR #{{id}}"
|
||||
},
|
||||
"TitleView": {
|
||||
"decky_store_desc": "Otevřít obchod Decky",
|
||||
"settings_desc": "Otevřít nastavení Decky"
|
||||
},
|
||||
"Updater": {
|
||||
"decky_updates": "Aktualizace Decky",
|
||||
"no_patch_notes_desc": "žádné poznámky pro tuto verzi",
|
||||
"patch_notes_desc": "Poznámky k verzi",
|
||||
"updates": {
|
||||
"check_button": "Zkontrolovat aktualizace",
|
||||
"checking": "Kontrolování",
|
||||
"cur_version": "Aktuální verze: {{ver}}",
|
||||
"install_button": "Instalovat aktualizaci",
|
||||
"label": "Aktualizace",
|
||||
"lat_version": "Aktuální: běží na verzi {{ver}}",
|
||||
"reloading": "Znovu načítání",
|
||||
"updating": "Aktualizování"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
{
|
||||
"BranchSelect": {
|
||||
"update_channel": {
|
||||
"label": "Updatekanal",
|
||||
"prerelease": "Vorabveröffentlichung",
|
||||
"stable": "Standard",
|
||||
"testing": "Test"
|
||||
}
|
||||
},
|
||||
"Developer": {
|
||||
"5secreload": "Neu laden in 5 Sekunden",
|
||||
"disabling": "Deaktiviere React DevTools",
|
||||
"enabling": "Aktiviere React DevTools"
|
||||
},
|
||||
"DropdownMultiselect": {
|
||||
"button": {
|
||||
"back": "Zurück"
|
||||
}
|
||||
},
|
||||
"FilePickerError": {
|
||||
"errors": {
|
||||
"file_not_found": "Der Pfad ist ungültig. Bitte prüfen und erneut eingeben.",
|
||||
"perm_denied": "Kein Zugriff auf den angegebenen Dateipfad. Bitte prüfen, ob der Nutzer (deck auf dem Steam Deck) die entsprechenden Zugriffsrechte auf den angegebenen Ordner/die angegebene Datei hat.",
|
||||
"unknown": "Ein unbekannter Fehler ist aufgetreten. Die ursprüngliche Fehlermeldung ist: {{raw_error}}"
|
||||
}
|
||||
},
|
||||
"FilePickerIndex": {
|
||||
"file": {
|
||||
"select": "Diese Datei auswählen"
|
||||
},
|
||||
"files": {
|
||||
"all_files": "Alle Dateien",
|
||||
"file_type": "Dateityp",
|
||||
"show_hidden": "Versteckte Dateien anzeigen"
|
||||
},
|
||||
"filter": {
|
||||
"created_asce": "Erstellt (Älteste)",
|
||||
"created_desc": "Erstellt (Neuste)",
|
||||
"modified_asce": "Geändert (Älteste)",
|
||||
"modified_desc": "Geändert (Neuste)",
|
||||
"name_asce": "Z-A",
|
||||
"name_desc": "A-Z",
|
||||
"size_asce": "Größe (Kleinste)",
|
||||
"size_desc": "Größe (Größte)"
|
||||
},
|
||||
"folder": {
|
||||
"label": "Ordner",
|
||||
"select": "Diesen Ordner verwenden",
|
||||
"show_more": "Mehr Dateien anzeigen"
|
||||
}
|
||||
},
|
||||
"MultiplePluginsInstallModal": {
|
||||
"confirm": "Bist du sicher, dass du die folgenden Änderungen vornehmen möchtest?",
|
||||
"description": {
|
||||
"install": "{{name}} {{version}} installieren",
|
||||
"reinstall": "{{name}} {{version}} neu installieren",
|
||||
"update": "{{name}} auf {{version}} aktualisieren"
|
||||
},
|
||||
"ok_button": {
|
||||
"idle": "Bestätigen",
|
||||
"loading": "An der Arbeit"
|
||||
},
|
||||
"title": {
|
||||
"install_one": "{{count}} Plugin installieren",
|
||||
"install_other": "{{count}} Plugins installieren",
|
||||
"mixed_one": "{{count}} Plugin bearbeiten",
|
||||
"mixed_other": "{{count}} Plugins bearbeiten",
|
||||
"reinstall_one": "{{count}} Plugin neu installieren",
|
||||
"reinstall_other": "{{count}} Plugins neu installieren",
|
||||
"update_one": "{{count}} Plugin aktualisieren",
|
||||
"update_other": "{{count}} Plugins aktualisieren"
|
||||
}
|
||||
},
|
||||
"PluginCard": {
|
||||
"plugin_full_access": "Diese Erweiterung hat uneingeschränkten Zugriff auf dein Steam Deck.",
|
||||
"plugin_install": "Installieren",
|
||||
"plugin_no_desc": "Keine Beschreibung angegeben.",
|
||||
"plugin_version_label": "Erweiterungs Version"
|
||||
},
|
||||
"PluginInstallModal": {
|
||||
"install": {
|
||||
"button_idle": "Installieren",
|
||||
"button_processing": "Wird installiert",
|
||||
"desc": "Bist du dir sicher, dass du {{artifact}} {{version}} installieren willst?",
|
||||
"title": "Installiere {{artifact}}"
|
||||
},
|
||||
"no_hash": "Diese Erweiterung besitzt keine Prüfsumme, Installation auf eigene Gefahr.",
|
||||
"reinstall": {
|
||||
"button_idle": "Neu installieren",
|
||||
"button_processing": "Wird neu installiert",
|
||||
"desc": "Bist du dir sicher, dass du {{artifact}} {{version}} neu installieren willst?",
|
||||
"title": "Neu installation {{artifact}}"
|
||||
},
|
||||
"update": {
|
||||
"button_idle": "Aktualisieren",
|
||||
"button_processing": "Wird aktualisiert",
|
||||
"desc": "Bist du dir sicher, dass du {{artifact}} {{version}} aktualisieren willst?",
|
||||
"title": "Aktualisiere {{artifact}}"
|
||||
}
|
||||
},
|
||||
"PluginListIndex": {
|
||||
"freeze": "Updates einfrieren",
|
||||
"hide": "Schnellzugriff: Ausblenden",
|
||||
"no_plugin": "Keine Erweiterungen installiert!",
|
||||
"plugin_actions": "Erweiterungs Aktionen",
|
||||
"reinstall": "Neu installieren",
|
||||
"reload": "Neu laden",
|
||||
"show": "Schnellzugriff: Anzeigen",
|
||||
"unfreeze": "Updates erlauben",
|
||||
"uninstall": "Deinstallieren",
|
||||
"update_all_one": "{{count}} Plugin aktualisieren",
|
||||
"update_all_other": "{{count}} Plugins aktualisieren",
|
||||
"update_to": "Aktualisieren zu {{name}}"
|
||||
},
|
||||
"PluginListLabel": {
|
||||
"hidden": "Im Schnellzugriff-Menu ausgeblendet"
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
"decky_update_available": "Eine neue Version ({{tag_name}}) ist verfügbar!",
|
||||
"error": "Fehler",
|
||||
"plugin_error_uninstall": "Das Laden von {{name}} hat einen Fehler verursacht. Dies bedeutet normalerweise, dass die Erweiterung ein Update für die neue Version von SteamUI benötigt. Prüfe in den Decky-Einstellungen im Bereich Erweiterungen, ob ein Update vorhanden ist.",
|
||||
"plugin_load_error": {
|
||||
"message": "Fehler beim Laden von {{name}}",
|
||||
"toast": "Fehler beim Laden von {{name}}"
|
||||
},
|
||||
"plugin_uninstall": {
|
||||
"button": "Deinstallieren",
|
||||
"desc": "Bist du dir sicher, dass du {{name}} deinstallieren willst?",
|
||||
"title": "Deinstalliere {{name}}"
|
||||
},
|
||||
"plugin_update_one": "1 Erweiterung kann aktualisiert werden!",
|
||||
"plugin_update_other": "{{count}} Erweiterungen können aktualisiert werden!"
|
||||
},
|
||||
"PluginView": {
|
||||
"hidden_one": "{{count}} Plugin ist in dieser Liste ausgeblendet",
|
||||
"hidden_other": "{{count}} Plugins sind in dieser Liste ausgeblendet"
|
||||
},
|
||||
"RemoteDebugging": {
|
||||
"remote_cef": {
|
||||
"desc": "Erlaubt jedem aus dem Neztwerk unautorisierten Zugriff auf den CEF Debugger",
|
||||
"label": "Remote CEF Debugging Zugriff"
|
||||
}
|
||||
},
|
||||
"SettingsDeveloperIndex": {
|
||||
"cef_console": {
|
||||
"button": "Konsole öffnen",
|
||||
"desc": "Öffnet die CEF Konsole. Nur für Debugzwecke. Dinge hier sind potentiell gefährlich und sollten nur durch oder unter Anleitung von Pluginentwickler/innen geschehen.",
|
||||
"label": "CEF Konsole"
|
||||
},
|
||||
"header": "Sonstiges",
|
||||
"react_devtools": {
|
||||
"desc": "Erlaubt die Verbindung mit einem anderen Rechner, auf welchem React DevTools läuft. Eine Änderung startet Steam neu. Die IP Adresse muss vor Aktivierung ausgefüllt sein.",
|
||||
"ip_label": "IP",
|
||||
"label": "Aktiviere React DevTools"
|
||||
},
|
||||
"third_party_plugins": {
|
||||
"button_install": "Installieren",
|
||||
"button_zip": "Durchsuchen",
|
||||
"header": "Erweiterungen von Drittanbietern",
|
||||
"label_desc": "URL",
|
||||
"label_url": "Installiere Erweiterung via URL",
|
||||
"label_zip": "Installiere Erweiterung via ZIP Datei"
|
||||
},
|
||||
"valve_internal": {
|
||||
"desc1": "Aktiviert das Valve-interne Entwickler Menü.",
|
||||
"desc2": "Fasse in diesem Menü nichts an, es sei denn, du weißt was du tust.",
|
||||
"label": "Aktiviere Valve-internes Menü"
|
||||
}
|
||||
},
|
||||
"SettingsGeneralIndex": {
|
||||
"about": {
|
||||
"decky_version": "Decky Version",
|
||||
"header": "Über"
|
||||
},
|
||||
"beta": {
|
||||
"header": "Beta Teilnahme"
|
||||
},
|
||||
"developer_mode": {
|
||||
"label": "Entwickleroptionen"
|
||||
},
|
||||
"notifications": {
|
||||
"decky_updates_label": "Decky Update verfügbar",
|
||||
"header": "Benachrichtigungen",
|
||||
"plugin_updates_label": "Plugin Updates verfügbar"
|
||||
},
|
||||
"other": {
|
||||
"header": "Sonstiges"
|
||||
},
|
||||
"updates": {
|
||||
"header": "Aktualisierungen"
|
||||
}
|
||||
},
|
||||
"SettingsIndex": {
|
||||
"developer_title": "Entwickler",
|
||||
"general_title": "Allgemein",
|
||||
"plugins_title": "Erweiterungen",
|
||||
"testing_title": "Testen"
|
||||
},
|
||||
"Store": {
|
||||
"download_progress_info": {
|
||||
"download_zip": "Plugin herunterladen",
|
||||
"increment_count": "Erhöhen der Downloadanzahl",
|
||||
"installing_plugin": "Plugin installieren",
|
||||
"open_zip": "Öffnen der Zip-Datei",
|
||||
"parse_zip": "Parsen der Zip-Datei",
|
||||
"start": "Initialisieren",
|
||||
"uninstalling_previous": "Vorherige Kopie deinstallieren"
|
||||
},
|
||||
"store_contrib": {
|
||||
"desc": "Wenn du Erweiterungen im Decky Store veröffentlichen willst, besuche die SteamDeckHomebrew/decky-plugin-template Repository auf GitHub. Informationen rund um Entwicklung und Veröffentlichung findest du in der README.",
|
||||
"label": "Mitwirken"
|
||||
},
|
||||
"store_filter": {
|
||||
"label": "Filter",
|
||||
"label_def": "Alle"
|
||||
},
|
||||
"store_search": {
|
||||
"label": "Suche"
|
||||
},
|
||||
"store_sort": {
|
||||
"label": "Sortierung",
|
||||
"label_def": "Zuletzt aktualisiert"
|
||||
},
|
||||
"store_source": {
|
||||
"desc": "Jeder Erweiterungs Quellcode ist in der SteamDeckHomebrew/decky-plugin-database Repository auf GitHub verfügbar.",
|
||||
"label": "Quellcode"
|
||||
},
|
||||
"store_tabs": {
|
||||
"about": "Über",
|
||||
"alph_asce": "Alphabetisch (Z zu A)",
|
||||
"alph_desc": "Alphabetisch (A zu Z)",
|
||||
"date_asce": "Älteste Zuerst",
|
||||
"date_desc": "Neuste Zuerst",
|
||||
"downloads_asce": "Wenigste Downloads Zuerst",
|
||||
"downloads_desc": "Meiste Downloads Zuerst",
|
||||
"title": "Durchstöbern"
|
||||
},
|
||||
"store_testing_cta": "Unterstütze das Decky Loader Team mit dem Testen von neuen Erweiterungen!",
|
||||
"store_testing_warning": {
|
||||
"desc": "Du kannst diesen Store Kanal nutzen, um brandneue Testversionen von Plugins auszuprobieren. Denk daran Feedback auf GitHub zu hinterlassen, sodass das Plugin für alle Nutzer verbessert werden kann.",
|
||||
"label": "Willkommen zum Test Store Kanal"
|
||||
}
|
||||
},
|
||||
"StoreSelect": {
|
||||
"custom_store": {
|
||||
"label": "Benutzerdefiniertes Store",
|
||||
"url_label": "URL"
|
||||
},
|
||||
"store_channel": {
|
||||
"custom": "Benutzerdefiniert",
|
||||
"default": "Standard",
|
||||
"label": "Store Kanal",
|
||||
"testing": "Test"
|
||||
}
|
||||
},
|
||||
"Testing": {
|
||||
"download": "Download",
|
||||
"error": "Fehler beim Installieren von PR",
|
||||
"header": "Die folgenden Versionen von Decky Loader wurden aus offenen Pull Requests von Dritten erstellt. Das Decky Loader-Team hat ihre Funktionalität oder Sicherheit nicht überprüft, und sie können veraltet sein.",
|
||||
"loading": "Offene Pull Requests laden...",
|
||||
"start_download_toast": "Herunterladen von PR #{{id}}"
|
||||
},
|
||||
"TitleView": {
|
||||
"decky_store_desc": "Decky Store Öffnen",
|
||||
"settings_desc": "Decky Einstellungen Öffnen"
|
||||
},
|
||||
"Updater": {
|
||||
"decky_updates": "Decky Aktualisierungen",
|
||||
"no_patch_notes_desc": "Für diese Version gibt es keine Patchnotizen",
|
||||
"patch_notes_desc": "Patchnotizen",
|
||||
"updates": {
|
||||
"check_button": "Auf Aktualisierungen prüfen",
|
||||
"checking": "Wird überprüft",
|
||||
"cur_version": "Aktualle Version: {{ver}}",
|
||||
"install_button": "Aktualisierung installieren",
|
||||
"label": "Aktualisierungen",
|
||||
"lat_version": "{{ver}} ist die aktuellste",
|
||||
"reloading": "Lade neu",
|
||||
"updating": "Aktualisiere"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
{
|
||||
"BranchSelect": {
|
||||
"update_channel": {
|
||||
"label": "Κανάλι ενημερώσεων",
|
||||
"prerelease": "Προ-κυκλοφορία",
|
||||
"stable": "Σταθερό",
|
||||
"testing": "Δοκιμαστικό"
|
||||
}
|
||||
},
|
||||
"Developer": {
|
||||
"5secreload": "Γίνεται επαναφόρτωση σε 5 δευτερόλεπτα",
|
||||
"disabling": "Γίνεται απενεργοποίηση των React DevTools",
|
||||
"enabling": "Γίνεται ενεργοποίηση των React DevTools"
|
||||
},
|
||||
"DropdownMultiselect": {
|
||||
"button": {
|
||||
"back": "Πίσω"
|
||||
}
|
||||
},
|
||||
"FilePickerError": {
|
||||
"errors": {
|
||||
"file_not_found": "Η καθορισμένη διαδρομή δεν είναι έγκυρη. Παρακαλούμε ελέγξτε τη και εισάγετέ τη ξανά σωστά.",
|
||||
"perm_denied": "Δεν έχετε πρόσβαση στην καθορισμένη διαδρομή. Ελέγξτε εάν ο χρήστης σας (deck στο Steam Deck) έχει τα αντίστοιχα δικαιώματα πρόσβασης στον καθορισμένο φάκελο/αρχείο.",
|
||||
"unknown": "Παρουσιάστηκε άγνωστο σφάλμα. Το σφάλμα είναι: {{raw_error}}"
|
||||
}
|
||||
},
|
||||
"FilePickerIndex": {
|
||||
"file": {
|
||||
"select": "Επιλογή αυτού του αρχείου"
|
||||
},
|
||||
"files": {
|
||||
"all_files": "Όλα Τα Αρχεία",
|
||||
"file_type": "Τύπος Αρχείου",
|
||||
"show_hidden": "Εμφάνιση Κρυφών Αρχείων"
|
||||
},
|
||||
"filter": {
|
||||
"created_asce": "Δημιουργήθηκε (Παλαιότερο)",
|
||||
"created_desc": "Δημιουργήθηκε (Νεότερο)",
|
||||
"modified_asce": "Τροποποιήθηκε (Παλαιότερο)",
|
||||
"modified_desc": "Τροποποιήθηκε (Νεότερο)",
|
||||
"name_asce": "Z-A",
|
||||
"name_desc": "A-Z",
|
||||
"size_asce": "Μέγεθος (Μικρότερο)",
|
||||
"size_desc": "Μέγεθος (Μεγαλύτερο)"
|
||||
},
|
||||
"folder": {
|
||||
"label": "Φάκελος",
|
||||
"select": "Χρησιμοποιήστε αυτό το φάκελο",
|
||||
"show_more": "Εμφάνιση περισσότερων αρχείων"
|
||||
}
|
||||
},
|
||||
"MultiplePluginsInstallModal": {
|
||||
"confirm": "Είστε βέβαιοι ότι θέλετε να κάνετε τις ακόλουθες τροποποιήσεις;",
|
||||
"description": {
|
||||
"install": "Εγκατάσταση {{name}} {{version}}",
|
||||
"reinstall": "Επανεγκατάσταση {{name}} {{version}}",
|
||||
"update": "Ενημέρωση {{name}} to {{version}}"
|
||||
},
|
||||
"ok_button": {
|
||||
"idle": "Επιβεβαίωση",
|
||||
"loading": "Φόρτωση"
|
||||
},
|
||||
"title": {
|
||||
"install_one": "Εγκατάσταση 1 επέκτασης",
|
||||
"install_other": "Εγκατάσταση {{count}} επεκτάσεων",
|
||||
"mixed_one": "Τροποποίηση 1 επέκτασης",
|
||||
"mixed_other": "Τροποποίηση {{count}} επεκτάσεων",
|
||||
"reinstall_one": "Επανεγκατάσταση 1 επέκτασης",
|
||||
"reinstall_other": "Επανεγκατάσταση {{count}} επεκτάσεων",
|
||||
"update_one": "Ενημέρωση 1 επέκτασης",
|
||||
"update_other": "Ενημέρωση {{count}} επεκτάσεων"
|
||||
}
|
||||
},
|
||||
"PluginCard": {
|
||||
"plugin_full_access": "Αυτή η επέκταση έχει πλήρη πρόσβαση στο Steam Deck σας.",
|
||||
"plugin_install": "Εγκατάσταση",
|
||||
"plugin_no_desc": "Δεν υπάρχει περιγραφή.",
|
||||
"plugin_version_label": "Έκδοση επέκτασης"
|
||||
},
|
||||
"PluginInstallModal": {
|
||||
"install": {
|
||||
"button_idle": "Εγκατάσταση",
|
||||
"button_processing": "Γίνεται εγκατάσταση",
|
||||
"desc": "Σίγουρα θέλετε να εγκαταστήσετε το {{artifact}}{{version}};",
|
||||
"title": "Εγκατάσταση {{artifact}}"
|
||||
},
|
||||
"no_hash": "Αυτή η επέκταση δεν έχει υπογραφή, την εγκαθηστάτε με δικό σας ρίσκο.",
|
||||
"reinstall": {
|
||||
"button_idle": "Επανεγκατάσταση",
|
||||
"button_processing": "Γίνεται επανεγκατάσταση",
|
||||
"desc": "Σίγουρα θέλετε να επανεγκαταστήσετε το {{artifact}}{{version}};",
|
||||
"title": "Επανεγκατάσταση {{artifact}}"
|
||||
},
|
||||
"update": {
|
||||
"button_idle": "Ενημέρωση",
|
||||
"button_processing": "Γίνεται ενημέρωση",
|
||||
"desc": "Σίγουρα θέλετε να ενημερώσετε το {{artifact}} {{version}};",
|
||||
"title": "Ενημέρωση {{artifact}}"
|
||||
}
|
||||
},
|
||||
"PluginListIndex": {
|
||||
"hide": "Γρήγορη πρόσβαση: Απόκρυψη",
|
||||
"no_plugin": "Δεν υπάρχουν εγκατεστημένες επεκτάσεις!",
|
||||
"plugin_actions": "Ενέργειες επεκτάσεων",
|
||||
"reinstall": "Επανεγκατάσταση",
|
||||
"reload": "Επαναφόρτωση",
|
||||
"show": "Γρήγορη πρόσβαση: Εμφάνιση",
|
||||
"uninstall": "Απεγκατάσταση",
|
||||
"update_all_one": "Ενημέρωση 1 επέκτασης",
|
||||
"update_all_other": "Ενημέρωση {{count}} επεκτάσεων",
|
||||
"update_to": "Ενημέρωση σε {{name}}"
|
||||
},
|
||||
"PluginListLabel": {
|
||||
"hidden": "Κρυφό στο μενού γρήγορης πρόσβασης"
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
"decky_update_available": "Ενημέρωση σε {{tag_name}} διαθέσιμη!",
|
||||
"error": "Σφάλμα",
|
||||
"plugin_error_uninstall": "Η φόρτωση του {{name}} προκάλεσε το παραπάνω σφάλμα. Αυτό συνήθως σημαίνει ότι η επέκταση απαιτεί ενημέρωση για τη νέα έκδοση του SteamUI. Ελέγξτε εάν υπάρχει ενημέρωση ή αξιολογήστε την απεγκαταστήσετε της επέκτασης στις ρυθμίσεις του Decky, στην ενότητα Επεκτάσεις.",
|
||||
"plugin_load_error": {
|
||||
"message": "Σφάλμα στη φόρτωση της επέκτασης {{name}}",
|
||||
"toast": "Σφάλμα φόρτωσης {{name}}"
|
||||
},
|
||||
"plugin_uninstall": {
|
||||
"button": "Απεγκατάσταση",
|
||||
"desc": "Σίγουρα θέλετε να απεγκαταστήσετε το {{name}};",
|
||||
"title": "Απεγκατάσταση {{name}}"
|
||||
},
|
||||
"plugin_update_one": "Διαθέσιμη ενημέρωση για 1 επέκταση!",
|
||||
"plugin_update_other": "Διαθέσιμες ενημερώσεις για {{count}} επεκτάσεις!"
|
||||
},
|
||||
"PluginView": {
|
||||
"hidden_one": "1 επέκταση είναι κρυμμένη σε αυτήν τη λίστα",
|
||||
"hidden_other": "{{count}} επεκτάσεις είναι κρυμμένες σε αυτήν τη λίστα"
|
||||
},
|
||||
"RemoteDebugging": {
|
||||
"remote_cef": {
|
||||
"desc": "Να επιτρέπεται η ανεξέλεγκτη πρόσβαση στον CEF debugger σε οποιονδήποτε στο τοπικό δίκτυο",
|
||||
"label": "Να επιτρέπεται η απομακρυσμένη πρόσβαση στον CEF debugger"
|
||||
}
|
||||
},
|
||||
"SettingsDeveloperIndex": {
|
||||
"cef_console": {
|
||||
"button": "Άνοιγμα Κονσόλας",
|
||||
"desc": "Ανοίγει την Κονσόλα CEF. Χρήσιμο μόνο για εντοπισμό σφαλμάτων. Τα πράγματα εδώ είναι δυνητικά επικίνδυνα και θα πρέπει να χρησιμοποιηθεί μόνο εάν είστε προγραμματιστής επεκτάσεων, ή κατευθυνθήκατε εδώ από έναν προγραμματιστή.",
|
||||
"label": "Κονσόλα CEF"
|
||||
},
|
||||
"header": "Άλλα",
|
||||
"react_devtools": {
|
||||
"desc": "Επιτρέπει την σύνδεση με υπολογιστή που τρέχει React DevTools. Η αλλαγή αυτής της ρύθμισης θα προκαλέσει επαναφόρτωση του Steam. Ωρίστε την διεύθυνση IP πριν την ενεργοποιήσετε.",
|
||||
"ip_label": "IP",
|
||||
"label": "Ενεργοποίηση React DevTools"
|
||||
},
|
||||
"third_party_plugins": {
|
||||
"button_install": "Εγκατάσταση",
|
||||
"button_zip": "Περιήγηση",
|
||||
"header": "Επεκτάσεις τρίτων",
|
||||
"label_desc": "URL",
|
||||
"label_url": "Εγκατάσταση επέκτασης απο URL",
|
||||
"label_zip": "Εγκατάσταση επέκτασης από αρχείο ZIP"
|
||||
},
|
||||
"valve_internal": {
|
||||
"desc1": "Ενεργοποιεί το μενού προγραμματιστή της Valve.",
|
||||
"desc2": "Μην αγγίξετε τίποτα σε αυτό το μενού εκτός και αν ξέρετε τι κάνει.",
|
||||
"label": "Ενεργοποιήση εσωτερικού μενού Valve"
|
||||
}
|
||||
},
|
||||
"SettingsGeneralIndex": {
|
||||
"about": {
|
||||
"decky_version": "Έκδοση Decky",
|
||||
"header": "Σχετικά"
|
||||
},
|
||||
"beta": {
|
||||
"header": "Συμμετοχή στη Beta"
|
||||
},
|
||||
"developer_mode": {
|
||||
"label": "Λειτουργία προγραμματιστή"
|
||||
},
|
||||
"notifications": {
|
||||
"decky_updates_label": "Διαθέσιμη ενημέρωση του Decky",
|
||||
"header": "Ειδοποιήσεις",
|
||||
"plugin_updates_label": "Διαθέσιμες ενημερώσεις επεκτάσεων"
|
||||
},
|
||||
"other": {
|
||||
"header": "Άλλα"
|
||||
},
|
||||
"updates": {
|
||||
"header": "Ενημερώσεις"
|
||||
}
|
||||
},
|
||||
"SettingsIndex": {
|
||||
"developer_title": "Προγραμματιστής",
|
||||
"general_title": "Γενικά",
|
||||
"plugins_title": "Επεκτάσεις"
|
||||
},
|
||||
"Store": {
|
||||
"store_contrib": {
|
||||
"desc": "Αν θέλετε να συνεισφέρετε στο κατάστημα επεκτάσεων του Decky, τσεκάρετε το SteamDeckHomebrew/decky-plugin-template repository στο GitHub. Πληροφορίες σχετικά με τη δημιουργία και τη διανομή επεκτάσεων είναι διαθέσιμες στο README.",
|
||||
"label": "Συνεισφέροντας"
|
||||
},
|
||||
"store_filter": {
|
||||
"label": "Φίλτρο",
|
||||
"label_def": "Όλα"
|
||||
},
|
||||
"store_search": {
|
||||
"label": "Αναζήτηση"
|
||||
},
|
||||
"store_sort": {
|
||||
"label": "Ταξινόμηση",
|
||||
"label_def": "Τελευταία ενημέρωση (Νεότερα)"
|
||||
},
|
||||
"store_source": {
|
||||
"desc": "Ο πηγαίος κώδικας όλων των επεκτάσεων είναι διαθέσιμος στο SteamDeckHomebrew/decky-plugin-database repository στο GitHub.",
|
||||
"label": "Πηγαίος κώδικας"
|
||||
},
|
||||
"store_tabs": {
|
||||
"about": "Σχετικά",
|
||||
"alph_asce": "Αλφαβητικά (Ζ σε Α)",
|
||||
"alph_desc": "Αλφαβητικά (Α σε Ζ)",
|
||||
"title": "Περιήγηση"
|
||||
},
|
||||
"store_testing_cta": "Παρακαλώ σκεφτείτε να τεστάρετε νέες επεκτάσεις για να βοηθήσετε την ομάδα του Decky Loader!",
|
||||
"store_testing_warning": {
|
||||
"desc": "Μπορείτε να χρησιμοποιήσετε αυτό το κανάλι του καταστήματος για να δοκιμάσετε τις νεότερες εκδόσεις των επεκτάσεων. Φροντίστε να αφήσετε σχόλια στο GitHub, ώστε να βοηθήσετε στην ενημέρωση της εκάστοτε επέκταση για όλους τους χρήστες.",
|
||||
"label": "Καλώς ήρθατε στο Δοκιμαστικό Κανάλι τους Καταστήματος"
|
||||
}
|
||||
},
|
||||
"StoreSelect": {
|
||||
"custom_store": {
|
||||
"label": "Προσαρμοσμένο κατάστημα",
|
||||
"url_label": "URL"
|
||||
},
|
||||
"store_channel": {
|
||||
"custom": "Προσαρμοσμένο",
|
||||
"default": "Προεπιλεγμένο",
|
||||
"label": "Κανάλι καταστήματος",
|
||||
"testing": "Δοκιμαστικό"
|
||||
}
|
||||
},
|
||||
"TitleView": {
|
||||
"decky_store_desc": "Άνοιγμα Καταστήματος Decky",
|
||||
"settings_desc": "Άνοιγμα Ρυθμίσεων Decky"
|
||||
},
|
||||
"Updater": {
|
||||
"decky_updates": "Ενημερώσεις Decky",
|
||||
"no_patch_notes_desc": "Κανένα ενημερωτικό σημείωμα για αυτή την έκδοση",
|
||||
"patch_notes_desc": "Σημειώσεις ενημέρωσης",
|
||||
"updates": {
|
||||
"check_button": "Έλεγχος για ενημερώσεις",
|
||||
"checking": "Γίνεται έλεγχος",
|
||||
"cur_version": "Τρέχουσα έκδοση: {{ver}}",
|
||||
"install_button": "Εγκατάσταση ενημέρωσης",
|
||||
"label": "Ενημερώσεις",
|
||||
"lat_version": "Ενημερωμένο: τρέχουσα έκδοση {{ver}}",
|
||||
"reloading": "Γίνεται επαναφόρτωση",
|
||||
"updating": "Γίνεται ενημέρωση"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
{
|
||||
"BranchSelect": {
|
||||
"update_channel": {
|
||||
"label": "Update Channel",
|
||||
"prerelease": "Prerelease",
|
||||
"stable": "Stable",
|
||||
"testing": "Testing"
|
||||
}
|
||||
},
|
||||
"Developer": {
|
||||
"5secreload": "Reloading in 5 seconds",
|
||||
"disabling": "Disabling React DevTools",
|
||||
"enabling": "Enabling React DevTools"
|
||||
},
|
||||
"DropdownMultiselect": {
|
||||
"button": {
|
||||
"back": "Back"
|
||||
}
|
||||
},
|
||||
"FilePickerError": {
|
||||
"errors": {
|
||||
"file_not_found": "The path specified is not valid. Please check it and reenter it correctly.",
|
||||
"perm_denied": "You do not have access to the specified directory. Please check if your user (deck on Steam Deck) has the corresponding permission to access the specified folder/file.",
|
||||
"unknown": "An unknown error occurred. The raw error is: {{raw_error}}"
|
||||
}
|
||||
},
|
||||
"FilePickerIndex": {
|
||||
"file": {
|
||||
"select": "Select this file"
|
||||
},
|
||||
"files": {
|
||||
"all_files": "All Files",
|
||||
"file_type": "File Type",
|
||||
"show_hidden": "Show Hidden Files"
|
||||
},
|
||||
"filter": {
|
||||
"created_asce": "Created (Oldest)",
|
||||
"created_desc": "Created (Newest)",
|
||||
"modified_asce": "Modified (Oldest)",
|
||||
"modified_desc": "Modified (Newest)",
|
||||
"name_asce": "Z-A",
|
||||
"name_desc": "A-Z",
|
||||
"size_asce": "Size (Smallest)",
|
||||
"size_desc": "Size (Largest)"
|
||||
},
|
||||
"folder": {
|
||||
"label": "Folder",
|
||||
"select": "Use this folder",
|
||||
"show_more": "Show more files"
|
||||
}
|
||||
},
|
||||
"MultiplePluginsInstallModal": {
|
||||
"confirm": "Are you sure you want to make the following modifications?",
|
||||
"description": {
|
||||
"downgrade": "Downgrade {{name}} to {{version}}",
|
||||
"install": "Install {{name}} {{version}}",
|
||||
"overwrite": "Overwrite {{name}} with {{version}}",
|
||||
"reinstall": "Reinstall {{name}} {{version}}",
|
||||
"update": "Update {{name}} to {{version}}"
|
||||
},
|
||||
"ok_button": {
|
||||
"idle": "Confirm",
|
||||
"loading": "Working"
|
||||
},
|
||||
"title": {
|
||||
"downgrade_one": "Downgrade 1 plugin",
|
||||
"downgrade_other": "Downgrade {{count}} plugins",
|
||||
"install_one": "Install 1 plugin",
|
||||
"install_other": "Install {{count}} plugins",
|
||||
"mixed_one": "Modify {{count}} plugin",
|
||||
"mixed_other": "Modify {{count}} plugins",
|
||||
"overwrite_one": "Overwrite 1 plugin",
|
||||
"overwrite_other": "Overwrite {{count}} plugins",
|
||||
"reinstall_one": "Reinstall 1 plugin",
|
||||
"reinstall_other": "Reinstall {{count}} plugins",
|
||||
"update_one": "Update 1 plugin",
|
||||
"update_other": "Update {{count}} plugins"
|
||||
}
|
||||
},
|
||||
"PluginCard": {
|
||||
"plugin_downgrade": "Downgrade",
|
||||
"plugin_full_access": "This plugin has full access to your Steam Deck.",
|
||||
"plugin_install": "Install",
|
||||
"plugin_no_desc": "No description provided.",
|
||||
"plugin_overwrite": "Overwrite",
|
||||
"plugin_reinstall": "Reinstall",
|
||||
"plugin_update": "Update",
|
||||
"plugin_version_label": "Plugin Version"
|
||||
},
|
||||
"PluginInstallModal": {
|
||||
"downgrade": {
|
||||
"button_idle": "Downgrade",
|
||||
"button_processing": "Downgrading",
|
||||
"desc": "Are you sure you want to downgrade {{artifact}} to version {{version}}?",
|
||||
"title": "Downgrade {{artifact}}"
|
||||
},
|
||||
"install": {
|
||||
"button_idle": "Install",
|
||||
"button_processing": "Installing",
|
||||
"desc": "Are you sure you want to install {{artifact}} {{version}}?",
|
||||
"title": "Install {{artifact}}"
|
||||
},
|
||||
"no_hash": "This plugin does not have a hash, you are installing it at your own risk.",
|
||||
"not_installed": "(not installed)",
|
||||
"disabled": "The plugin will be re-enabled after installation",
|
||||
"overwrite": {
|
||||
"button_idle": "Overwrite",
|
||||
"button_processing": "Overwriting",
|
||||
"desc": "Are you sure you want to overwrite {{artifact}} with version {{version}}?",
|
||||
"title": "Overwrite {{artifact}}"
|
||||
},
|
||||
"reinstall": {
|
||||
"button_idle": "Reinstall",
|
||||
"button_processing": "Reinstalling",
|
||||
"desc": "Are you sure you want to reinstall {{artifact}} {{version}}?",
|
||||
"title": "Reinstall {{artifact}}"
|
||||
},
|
||||
"update": {
|
||||
"button_idle": "Update",
|
||||
"button_processing": "Updating",
|
||||
"desc": "Are you sure you want to update {{artifact}} to version {{version}}?",
|
||||
"title": "Update {{artifact}}"
|
||||
}
|
||||
},
|
||||
"PluginListIndex": {
|
||||
"freeze": "Freeze updates",
|
||||
"hide": "Quick access: Hide",
|
||||
"no_plugin": "No plugins installed!",
|
||||
"plugin_actions": "Plugin Actions",
|
||||
"reinstall": "Reinstall",
|
||||
"reload": "Reload",
|
||||
"show": "Quick access: Show",
|
||||
"unfreeze": "Allow updates",
|
||||
"uninstall": "Uninstall",
|
||||
"update_all_one": "Update 1 plugin",
|
||||
"update_all_other": "Update {{count}} plugins",
|
||||
"update_to": "Update to {{name}}",
|
||||
"disable": "Disable",
|
||||
"enable": "Enable"
|
||||
},
|
||||
"PluginListLabel": {
|
||||
"hidden": "Hidden from the quick access menu",
|
||||
"disabled": "Plugin disabled"
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
"decky_update_available": "Update to {{tag_name}} available!",
|
||||
"error": "Error",
|
||||
"plugin_error_uninstall": "Loading {{name}} caused an exception as shown above. This usually means that the plugin requires an update for the new version of SteamUI. Check if an update is present or evaluate its removal in the Decky settings, in the Plugins section.",
|
||||
"plugin_load_error": {
|
||||
"message": "Error loading plugin {{name}}",
|
||||
"toast": "Error loading {{name}}"
|
||||
},
|
||||
"plugin_uninstall": {
|
||||
"button": "Uninstall",
|
||||
"desc": "Are you sure you want to uninstall {{name}}?",
|
||||
"title": "Uninstall {{name}}"
|
||||
},
|
||||
"plugin_disable": {
|
||||
"button": "Disable",
|
||||
"desc": "Are you sure you want to disable {{name}}?",
|
||||
"title": "Disable {{name}}",
|
||||
"error": "Error disabling {{name}}"
|
||||
},
|
||||
"plugin_enable": {
|
||||
"error": "Error enabling {{name}}"
|
||||
},
|
||||
"plugin_update_one": "Updates available for 1 plugin!",
|
||||
"plugin_update_other": "Updates available for {{count}} plugins!"
|
||||
},
|
||||
"PluginView": {
|
||||
"hidden_one": "1 plugin is hidden from this list",
|
||||
"hidden_other": "{{count}} plugins are hidden from this list",
|
||||
"disabled_one": "1 plugin is disabled",
|
||||
"disabled_other": "{{count}} plugins are disabled"
|
||||
},
|
||||
"RemoteDebugging": {
|
||||
"remote_cef": {
|
||||
"desc": "Allow unauthenticated access to the CEF debugger to anyone in your network",
|
||||
"label": "Allow Remote CEF Debugging"
|
||||
}
|
||||
},
|
||||
"SettingsDeveloperIndex": {
|
||||
"cef_console": {
|
||||
"button": "Open Console",
|
||||
"desc": "Opens the CEF Console. Only useful for debugging purposes. Stuff here is potentially dangerous and should only be used if you are a plugin dev, or are directed here by one.",
|
||||
"label": "CEF Console"
|
||||
},
|
||||
"header": "Other",
|
||||
"react_devtools": {
|
||||
"desc": "Enables connection to a computer running React DevTools. Changing this setting will reload Steam. Set the IP address before enabling.",
|
||||
"ip_label": "IP",
|
||||
"label": "Enable React DevTools"
|
||||
},
|
||||
"third_party_plugins": {
|
||||
"button_install": "Install",
|
||||
"button_zip": "Browse",
|
||||
"header": "Third-Party Plugins",
|
||||
"label_desc": "URL",
|
||||
"label_url": "Install Plugin from URL",
|
||||
"label_zip": "Install Plugin from ZIP File"
|
||||
},
|
||||
"valve_internal": {
|
||||
"desc1": "Enables the Valve internal developer menu.",
|
||||
"desc2": "Do not touch anything in this menu unless you know what it does.",
|
||||
"label": "Enable Valve Internal"
|
||||
}
|
||||
},
|
||||
"SettingsGeneralIndex": {
|
||||
"about": {
|
||||
"decky_version": "Decky Version",
|
||||
"header": "About"
|
||||
},
|
||||
"beta": {
|
||||
"header": "Beta participation"
|
||||
},
|
||||
"developer_mode": {
|
||||
"label": "Developer mode"
|
||||
},
|
||||
"notifications": {
|
||||
"decky_updates_label": "Decky update available",
|
||||
"header": "Notifications",
|
||||
"plugin_updates_label": "Plugin updates available"
|
||||
},
|
||||
"other": {
|
||||
"header": "Other"
|
||||
},
|
||||
"updates": {
|
||||
"header": "Updates"
|
||||
}
|
||||
},
|
||||
"SettingsIndex": {
|
||||
"developer_title": "Developer",
|
||||
"general_title": "General",
|
||||
"plugins_title": "Plugins",
|
||||
"testing_title": "Testing"
|
||||
},
|
||||
"Store": {
|
||||
"download_progress_info": {
|
||||
"download_remote": "Downloading any external binaries",
|
||||
"download_zip": "Downloading plugin",
|
||||
"increment_count": "Incrementing download count",
|
||||
"installing_plugin": "Installing plugin",
|
||||
"open_zip": "Opening zip file",
|
||||
"parse_zip": "Parsing zip file",
|
||||
"start": "Initializing",
|
||||
"uninstalling_previous": "Uninstalling previous copy"
|
||||
},
|
||||
"store_contrib": {
|
||||
"desc": "If you would like to contribute to the Decky Plugin Store, check the SteamDeckHomebrew/decky-plugin-template repository on GitHub. Information on development and distribution is available in the README.",
|
||||
"label": "Contributing"
|
||||
},
|
||||
"store_filter": {
|
||||
"label": "Filter",
|
||||
"label_def": "All"
|
||||
},
|
||||
"store_search": {
|
||||
"label": "Search"
|
||||
},
|
||||
"store_sort": {
|
||||
"label": "Sort",
|
||||
"label_def": "Last Updated (Newest)"
|
||||
},
|
||||
"store_source": {
|
||||
"desc": "All plugin source code is available on SteamDeckHomebrew/decky-plugin-database repository on GitHub.",
|
||||
"label": "Source Code"
|
||||
},
|
||||
"store_tabs": {
|
||||
"about": "About",
|
||||
"alph_asce": "Alphabetical (Z to A)",
|
||||
"alph_desc": "Alphabetical (A to Z)",
|
||||
"date_asce": "Oldest First",
|
||||
"date_desc": "Newest First",
|
||||
"downloads_asce": "Least Downloaded First",
|
||||
"downloads_desc": "Most Downloaded First",
|
||||
"title": "Browse"
|
||||
},
|
||||
"store_testing_cta": "Please consider testing new plugins to help the Decky Loader team!",
|
||||
"store_testing_warning": {
|
||||
"desc": "You can use this store channel to test bleeding-edge plugin versions. Be sure to leave feedback on GitHub so the plugin can be updated for all users.",
|
||||
"label": "Welcome to the Testing Store Channel"
|
||||
}
|
||||
},
|
||||
"StoreSelect": {
|
||||
"custom_store": {
|
||||
"label": "Custom Store",
|
||||
"url_label": "URL"
|
||||
},
|
||||
"store_channel": {
|
||||
"custom": "Custom",
|
||||
"default": "Default",
|
||||
"label": "Store Channel",
|
||||
"testing": "Testing"
|
||||
}
|
||||
},
|
||||
"Testing": {
|
||||
"download": "Download",
|
||||
"error": "Error Installing PR",
|
||||
"header": "The following versions of Decky Loader are built from open third-party Pull Requests. The Decky Loader team has not verified their functionality or security, and they may be outdated.",
|
||||
"loading": "Loading open Pull Requests...",
|
||||
"start_download_toast": "Downloading PR #{{id}}"
|
||||
},
|
||||
"TitleView": {
|
||||
"decky_store_desc": "Open Decky Store",
|
||||
"settings_desc": "Open Decky Settings"
|
||||
},
|
||||
"Updater": {
|
||||
"decky_updates": "Decky Updates",
|
||||
"no_patch_notes_desc": "no patch notes for this version",
|
||||
"patch_notes_desc": "Patch Notes",
|
||||
"updates": {
|
||||
"check_button": "Check For Updates",
|
||||
"checking": "Checking",
|
||||
"cur_version": "Current version: {{ver}}",
|
||||
"install_button": "Install Update",
|
||||
"label": "Updates",
|
||||
"lat_version": "Up to date: running {{ver}}",
|
||||
"reloading": "Reloading",
|
||||
"updating": "Updating"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
{
|
||||
"BranchSelect": {
|
||||
"update_channel": {
|
||||
"label": "Canal de actualización",
|
||||
"prerelease": "Prelanzamiento",
|
||||
"stable": "Estable",
|
||||
"testing": "En pruebas"
|
||||
}
|
||||
},
|
||||
"Developer": {
|
||||
"5secreload": "Recargando en 5 segundos",
|
||||
"disabling": "Desactivando DevTools de React",
|
||||
"enabling": "Activando DevTools de React"
|
||||
},
|
||||
"DropdownMultiselect": {
|
||||
"button": {
|
||||
"back": "Volver"
|
||||
}
|
||||
},
|
||||
"FilePickerError": {
|
||||
"errors": {
|
||||
"file_not_found": "La ruta especificada no es válida. Por favor revísala e introdúcela correctamente.",
|
||||
"perm_denied": "No tienes acceso a la ruta especificada. Por favor revisa si tu usuario (deck en Steam Deck) tiene el permiso correspondiente para acceder a la ruta/archivo especificado.",
|
||||
"unknown": "Ha ocurrido un error desconocido. El error sin procesar es:{{raw_error}}"
|
||||
}
|
||||
},
|
||||
"FilePickerIndex": {
|
||||
"file": {
|
||||
"select": "Selecciona este archivo"
|
||||
},
|
||||
"files": {
|
||||
"all_files": "Todos los archivos",
|
||||
"file_type": "Tipo de archivo",
|
||||
"show_hidden": "Mostrar archivos ocultos"
|
||||
},
|
||||
"filter": {
|
||||
"created_asce": "Creado (Más antiguo)",
|
||||
"created_desc": "Creado (Más reciente)",
|
||||
"modified_asce": "Modificado (Más antiguo)",
|
||||
"modified_desc": "Modificado (Más reciente)",
|
||||
"name_asce": "Z-A",
|
||||
"name_desc": "A-Z",
|
||||
"size_asce": "Tamaño (Más pequeño)",
|
||||
"size_desc": "Tamaño (Más grande)"
|
||||
},
|
||||
"folder": {
|
||||
"label": "Carpeta",
|
||||
"select": "Usar esta carpeta",
|
||||
"show_more": "Mostrar más archivos"
|
||||
}
|
||||
},
|
||||
"MultiplePluginsInstallModal": {
|
||||
"confirm": "¿Estás seguro de que quieres hacer las siguientes modificaciones?",
|
||||
"description": {
|
||||
"install": "Instalar {{name}} {{version}}",
|
||||
"reinstall": "Reinstalar {{name}} {{version}}",
|
||||
"update": "Actualizar {{name}} a {{version}}"
|
||||
},
|
||||
"ok_button": {
|
||||
"idle": "Confirmar",
|
||||
"loading": "Trabajando"
|
||||
},
|
||||
"title": {
|
||||
"install_many": "Instalar {{count}} plugins",
|
||||
"install_one": "Instalar 1 plugin",
|
||||
"install_other": "Instalar {{count}} plugins",
|
||||
"mixed_many": "Modificar {{count}} plugins",
|
||||
"mixed_one": "Modificar 1 plugin",
|
||||
"mixed_other": "Modificar {{count}} plugins",
|
||||
"reinstall_many": "Reinstalar {{count}} plugins",
|
||||
"reinstall_one": "Reinstalar 1 plugin",
|
||||
"reinstall_other": "Reinstalar {{count}} plugins",
|
||||
"update_many": "Actualizar {{count}} plugins",
|
||||
"update_one": "Actualizar 1 plugin",
|
||||
"update_other": "Actualizar {{count}} plugins"
|
||||
}
|
||||
},
|
||||
"PluginCard": {
|
||||
"plugin_full_access": "Este plugin tiene acceso completo a tu Steam Deck.",
|
||||
"plugin_install": "Instalar",
|
||||
"plugin_no_desc": "No se ha proporcionado una descripción.",
|
||||
"plugin_version_label": "Versión de Plugin"
|
||||
},
|
||||
"PluginInstallModal": {
|
||||
"install": {
|
||||
"button_idle": "Instalar",
|
||||
"button_processing": "Instalando",
|
||||
"desc": "¿Estás seguro de que quieres instalar {{artifact}} {{version}}?",
|
||||
"title": "Instalar {{artifact}}"
|
||||
},
|
||||
"no_hash": "Este plugin no tiene un hash, lo estás instalando bajo tu propia responsabilidad.",
|
||||
"reinstall": {
|
||||
"button_idle": "Reinstalar",
|
||||
"button_processing": "Reinstalando",
|
||||
"desc": "¿Estás seguro de que quieres reinstalar {{artifact}} {{version}}?",
|
||||
"title": "Reinstalar {{artifact}}"
|
||||
},
|
||||
"update": {
|
||||
"button_idle": "Actualizar",
|
||||
"button_processing": "Actualizando",
|
||||
"desc": "¿Estás seguro de que quieres actualizar {{artifact}} {{version}}?",
|
||||
"title": "Actualizar {{artifact}}"
|
||||
}
|
||||
},
|
||||
"PluginListIndex": {
|
||||
"freeze": "Congelar actualizaciones",
|
||||
"hide": "Acceso rápido: Esconder",
|
||||
"no_plugin": "¡No hay plugins instalados!",
|
||||
"plugin_actions": "Acciones de Plugin",
|
||||
"reinstall": "Reinstalar",
|
||||
"reload": "Recargar",
|
||||
"show": "Acceso rápido: Mostrar",
|
||||
"unfreeze": "Permitir actualizaciones",
|
||||
"uninstall": "Desinstalar",
|
||||
"update_all_many": "Actualizar {{count}} plugins",
|
||||
"update_all_one": "Actualizar 1 plugin",
|
||||
"update_all_other": "Actualizar {{count}} plugins",
|
||||
"update_to": "Actualizar a {{name}}"
|
||||
},
|
||||
"PluginListLabel": {
|
||||
"hidden": "Escondido del menú de acceso rápido"
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
"decky_update_available": "¡Actualización para {{tag_name}} disponible!",
|
||||
"error": "Error",
|
||||
"plugin_error_uninstall": "Al cargar {{name}} se ha producido una excepción como se muestra arriba. Esto suele significar que el plugin requiere una actualización para la nueva versión de SteamUI. Comprueba si hay una actualización disponible o valora eliminarlo en los ajustes de Decky, en la sección Plugins.",
|
||||
"plugin_load_error": {
|
||||
"message": "Se ha producido un error al cargar el plugin {{name}}",
|
||||
"toast": "Se ha producido un error al cargar {{name}}"
|
||||
},
|
||||
"plugin_uninstall": {
|
||||
"button": "Desinstalar",
|
||||
"desc": "¿Estás seguro de que quieres desinstalar {{name}}?",
|
||||
"title": "Desinstalar {{name}}"
|
||||
},
|
||||
"plugin_update_many": "¡Actualizaciones disponibles para {{count}} plugins!",
|
||||
"plugin_update_one": "¡Actualización disponible para 1 plugin!",
|
||||
"plugin_update_other": "¡Actualizaciones disponibles para {{count}} plugins!"
|
||||
},
|
||||
"PluginView": {
|
||||
"hidden_many": "{{count}} plugins están escondidos de esta lista",
|
||||
"hidden_one": "1 plugin está escondido de esta lista",
|
||||
"hidden_other": "{{count}} plugins están escondidos de esta lista"
|
||||
},
|
||||
"RemoteDebugging": {
|
||||
"remote_cef": {
|
||||
"desc": "Permitir acceso no autenticado al depurador del CEF a cualquier persona en tu red",
|
||||
"label": "Permitir depuración remota del CEF"
|
||||
}
|
||||
},
|
||||
"SettingsDeveloperIndex": {
|
||||
"cef_console": {
|
||||
"button": "Abrir Consola",
|
||||
"desc": "Abre la consola del CEF. Solamente es útil para propósitos de depuración. Las cosas que hagas aquí pueden ser potencialmente peligrosas y solo se debería usar si eres un desarrollador de plugins, o uno te ha dirigido aquí.",
|
||||
"label": "Consola CEF"
|
||||
},
|
||||
"header": "Otros",
|
||||
"react_devtools": {
|
||||
"desc": "Permite la conexión a un ordenador ejecutando las DevTools de React. Cambiar este ajuste recargará Steam. Configura la dirección IP antes de activarlo.",
|
||||
"ip_label": "IP",
|
||||
"label": "Activar DevTools de React"
|
||||
},
|
||||
"third_party_plugins": {
|
||||
"button_install": "Instalar",
|
||||
"button_zip": "Navegar",
|
||||
"header": "Plugins de terceros",
|
||||
"label_desc": "URL",
|
||||
"label_url": "Instalar plugin desde URL",
|
||||
"label_zip": "Instalar plugin desde archivo ZIP"
|
||||
},
|
||||
"valve_internal": {
|
||||
"desc1": "Activa el menú interno de desarrollo de Valve.",
|
||||
"desc2": "No toques nada en este menú a menos que sepas lo que haces.",
|
||||
"label": "Activar menú interno de Valve"
|
||||
}
|
||||
},
|
||||
"SettingsGeneralIndex": {
|
||||
"about": {
|
||||
"decky_version": "Versión de Decky",
|
||||
"header": "Acerca de"
|
||||
},
|
||||
"beta": {
|
||||
"header": "Participación en la beta"
|
||||
},
|
||||
"developer_mode": {
|
||||
"label": "Modo desarrollador"
|
||||
},
|
||||
"notifications": {
|
||||
"decky_updates_label": "Actualización de Decky disponible",
|
||||
"header": "Notificaciones",
|
||||
"plugin_updates_label": "Actualizaciones de plugin disponibles"
|
||||
},
|
||||
"other": {
|
||||
"header": "Otros"
|
||||
},
|
||||
"updates": {
|
||||
"header": "Actualizaciones"
|
||||
}
|
||||
},
|
||||
"SettingsIndex": {
|
||||
"developer_title": "Desarrollador",
|
||||
"general_title": "General",
|
||||
"plugins_title": "Plugins",
|
||||
"testing_title": "En pruebas"
|
||||
},
|
||||
"Store": {
|
||||
"download_progress_info": {
|
||||
"download_zip": "Descargando plugin",
|
||||
"increment_count": "Incrementando el contador de descargas",
|
||||
"installing_plugin": "Instalando plugin",
|
||||
"open_zip": "Abriendo archivo zip",
|
||||
"parse_zip": "Analizando archivo zip",
|
||||
"start": "Iniciando",
|
||||
"uninstalling_previous": "Desinstalando copia previa"
|
||||
},
|
||||
"store_contrib": {
|
||||
"desc": "Si desea contribuir a la tienda de plugins de Decky, consulta el repositorio SteamDeckHomebrew/decky-plugin-template en GitHub. Hay información disponible acerca del desarrollo y distribución en el archivo README.",
|
||||
"label": "Contribuyendo"
|
||||
},
|
||||
"store_filter": {
|
||||
"label": "Filtrar",
|
||||
"label_def": "Todos"
|
||||
},
|
||||
"store_search": {
|
||||
"label": "Buscar"
|
||||
},
|
||||
"store_sort": {
|
||||
"label": "Ordenar",
|
||||
"label_def": "Actualizado por última vez (Más reciente)"
|
||||
},
|
||||
"store_source": {
|
||||
"desc": "El código fuente de los plugins está disponible en el repositiorio SteamDeckHomebrew/decky-plugin-database en GitHub.",
|
||||
"label": "Código fuente"
|
||||
},
|
||||
"store_tabs": {
|
||||
"about": "Información",
|
||||
"alph_asce": "Alfabéticamente (Z-A)",
|
||||
"alph_desc": "Alfabéticamente (A-Z)",
|
||||
"date_asce": "Más antiguo primero",
|
||||
"date_desc": "Más reciente primero",
|
||||
"downloads_asce": "Menos descargados primero",
|
||||
"downloads_desc": "Más descargados primero",
|
||||
"title": "Navegar"
|
||||
},
|
||||
"store_testing_cta": "¡Por favor considera probar plugins nuevos para ayudar al equipo de Decky Loader!",
|
||||
"store_testing_warning": {
|
||||
"desc": "Puedes usar este canal de la tienda para probar versiones inestables de plugins. Recuerda compartir tu experiencia en GitHub con el fin de poder actualizar el plugin para todos los usuarios.",
|
||||
"label": "Bienvenido al canal En Pruebas de la Tienda"
|
||||
}
|
||||
},
|
||||
"StoreSelect": {
|
||||
"custom_store": {
|
||||
"label": "Tienda personalizada",
|
||||
"url_label": "URL"
|
||||
},
|
||||
"store_channel": {
|
||||
"custom": "Personalizada",
|
||||
"default": "Por defecto",
|
||||
"label": "Canal de la Tienda",
|
||||
"testing": "En pruebas"
|
||||
}
|
||||
},
|
||||
"Testing": {
|
||||
"download": "Descargar",
|
||||
"error": "Error instalando PR",
|
||||
"header": "Las siguientes versiones de Decky Loader han sido compiladas de solicitudes Pull de terceros. El equipo de Decky Loader no ha verificado su funcionalidad o seguridad, y es posible que estén desactulizadas.",
|
||||
"loading": "Cargando abrir Solicitudes de Pull...",
|
||||
"start_download_toast": "Descargando PR #{{id}}"
|
||||
},
|
||||
"TitleView": {
|
||||
"decky_store_desc": "Abrir la tienda de Decky",
|
||||
"settings_desc": "Abrir los ajustes de Decky"
|
||||
},
|
||||
"Updater": {
|
||||
"decky_updates": "Actualizaciones de Decky",
|
||||
"no_patch_notes_desc": "No hay notas de parche para esta versión",
|
||||
"patch_notes_desc": "Notas de parche",
|
||||
"updates": {
|
||||
"check_button": "Buscar actualizaciones",
|
||||
"checking": "Buscando",
|
||||
"cur_version": "Versión actual: {{ver}}",
|
||||
"install_button": "Instalar actualización",
|
||||
"label": "Actualizaciones",
|
||||
"lat_version": "Actualizado: ejecutando {{ver}}",
|
||||
"reloading": "Recargando",
|
||||
"updating": "Actualizando"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
{
|
||||
"BranchSelect": {
|
||||
"update_channel": {
|
||||
"label": "Päivityskanava",
|
||||
"prerelease": "Esijulkaisu",
|
||||
"stable": "Vakaa versio",
|
||||
"testing": "Testiversio"
|
||||
}
|
||||
},
|
||||
"Developer": {
|
||||
"5secreload": "Uudelleenladataan 5 sekunin kuluttua",
|
||||
"disabling": "Poistetaan React DevTools käytöstä",
|
||||
"enabling": "Otetaan React DevTools käyttöön"
|
||||
},
|
||||
"DropdownMultiselect": {
|
||||
"button": {
|
||||
"back": "Takaisin"
|
||||
}
|
||||
},
|
||||
"FilePickerError": {
|
||||
"errors": {
|
||||
"file_not_found": "Määritetty polku ei kelpaa. Tarkista se ja kirjoita se uudelleen oikein.",
|
||||
"perm_denied": "Sinulla ei ole käyttöoikeutta määritettyyn hakemistoon. Tarkista, onko käyttäjälläsi (käyttäjä 'deck' Steam Deckillä) vastaavat oikeudet käyttää määritettyä kansiota/tiedostoa.",
|
||||
"unknown": "Tapahtui tuntematon virhe. Raaka virhe on: {{raw_error}}"
|
||||
}
|
||||
},
|
||||
"FilePickerIndex": {
|
||||
"file": {
|
||||
"select": "Valitse tämä tiedosto"
|
||||
},
|
||||
"files": {
|
||||
"all_files": "Kaikki tiedostot",
|
||||
"file_type": "Tiedostotyyppi",
|
||||
"show_hidden": "Näytä piilotetut tiedostot"
|
||||
},
|
||||
"filter": {
|
||||
"created_asce": "Luotu (vanhin ensin)",
|
||||
"created_desc": "Luotu (uusin ensin)",
|
||||
"modified_asce": "Muokattu (vanhin)",
|
||||
"modified_desc": "Muokattu (uusin)",
|
||||
"name_asce": "Z-A",
|
||||
"name_desc": "A-Z",
|
||||
"size_asce": "Koko (pienin ensin)",
|
||||
"size_desc": "Koko (suurin ensin)"
|
||||
},
|
||||
"folder": {
|
||||
"label": "Kansio",
|
||||
"select": "Käytä tätä kansiota",
|
||||
"show_more": "Näytä lisää tiedostoja"
|
||||
}
|
||||
},
|
||||
"MultiplePluginsInstallModal": {
|
||||
"confirm": "Haluatko varmasti tehdä seuraavat muutokset?",
|
||||
"description": {
|
||||
"install": "Asenna {{name}} {{version}}",
|
||||
"reinstall": "Uudelleenasenna {{name}} {{version}}",
|
||||
"update": "Päivitä {{name}} versioon {{version}}"
|
||||
},
|
||||
"ok_button": {
|
||||
"idle": "Vahvista",
|
||||
"loading": "Ladataan"
|
||||
},
|
||||
"title": {
|
||||
"install_one": "Asenna yksi laajennus",
|
||||
"install_other": "Asenna {{count}} laajennusta",
|
||||
"mixed_one": "Muuta yhtä laajennusta",
|
||||
"mixed_other": "Muuta {{count}} laajennusta",
|
||||
"reinstall_one": "Uudelleenasenna yksi laajennus",
|
||||
"reinstall_other": "Uudelleenasenna {{count}} laajennusta",
|
||||
"update_one": "Päivitä yksi laajennus",
|
||||
"update_other": "Päivitä {{count}} laajennusta"
|
||||
}
|
||||
},
|
||||
"PluginCard": {
|
||||
"plugin_full_access": "Tällä laajennuksella on täysi pääsy Steam Deckkiisi.",
|
||||
"plugin_install": "Asenna",
|
||||
"plugin_no_desc": "Ei kuvausta.",
|
||||
"plugin_version_label": "Laajennuksen versio"
|
||||
},
|
||||
"PluginInstallModal": {
|
||||
"install": {
|
||||
"button_idle": "Asenna",
|
||||
"button_processing": "Asennetaan",
|
||||
"desc": "Haluatko varmasti asentaa {{artifact}} {{version}}?",
|
||||
"title": "Asenna {{artifact}}"
|
||||
},
|
||||
"no_hash": "Tällä laajennuksella ei ole hashia, asennat sen omalla vastuullasi.",
|
||||
"reinstall": {
|
||||
"button_idle": "Uudelleenasenna",
|
||||
"button_processing": "Uudelleenasennetaan",
|
||||
"desc": "Haluatko varmasti uudelleenasentaa {{artifact}} {{version}}?",
|
||||
"title": "Uudelleenasenna {{artifact}}"
|
||||
},
|
||||
"update": {
|
||||
"button_idle": "Päivitä",
|
||||
"button_processing": "Päivitetään",
|
||||
"desc": "Haluatko varmasti päivittää {{artifact}} {{version}}?",
|
||||
"title": "Päivitä {{artifact}}"
|
||||
}
|
||||
},
|
||||
"PluginListIndex": {
|
||||
"hide": "Pikavalikko: Piilota",
|
||||
"no_plugin": "Ei asennettuja laajennuksia!",
|
||||
"plugin_actions": "Laajennustoiminnot",
|
||||
"reinstall": "Uudelleenasenna",
|
||||
"reload": "Lataa uudelleen",
|
||||
"show": "Pikavalikko: Näytä",
|
||||
"uninstall": "Poista asennus",
|
||||
"update_all_one": "Päivitä yksi laajennus",
|
||||
"update_all_other": "Päivitä {{count}} laajennusta",
|
||||
"update_to": "Päivitä versioon {{name}}"
|
||||
},
|
||||
"PluginListLabel": {
|
||||
"hidden": "Piilotettu pikavalikosta"
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
"decky_update_available": "Päivitys versioon {{tag_name}} on saatavilla!",
|
||||
"error": "Virhe",
|
||||
"plugin_error_uninstall": "{{name}} lataaminen aiheutti yllä olevan poikkeuksen. Tämä tarkoittaa yleensä sitä, että laajennus vaatii päivityksen uudelle SteamUI-versiolle. Tarkista, onko päivitystä saatavilla, tai harkitse laajennuksen poistoa Decky-asetuksista, laajennukset-osiosta.",
|
||||
"plugin_load_error": {
|
||||
"message": "Virhe ladattaessa {{name}}-laajennusta",
|
||||
"toast": "Virhe ladattaessa {{name}}"
|
||||
},
|
||||
"plugin_uninstall": {
|
||||
"button": "Poista asennus",
|
||||
"desc": "Haluatko varmasti poistaa {{name}} asennuksen?",
|
||||
"title": "Poista {{name}}"
|
||||
},
|
||||
"plugin_update_one": "Päivityksiä saatavilla yhdelle laajennukselle!",
|
||||
"plugin_update_other": "Päivityksiä saatavilla {{count}} laajennukselle!"
|
||||
},
|
||||
"PluginView": {
|
||||
"hidden_one": "Yksi laajennus on piilotettu tästä luettelosta",
|
||||
"hidden_other": "{{count}} laajennusta on piilotettu tästä luettelosta"
|
||||
},
|
||||
"RemoteDebugging": {
|
||||
"remote_cef": {
|
||||
"desc": "Salli todentamaton pääsy CEF-debuggeriin kenelle tahansa verkossasi",
|
||||
"label": "Salli CEF-etädebugaus"
|
||||
}
|
||||
},
|
||||
"SettingsDeveloperIndex": {
|
||||
"cef_console": {
|
||||
"button": "Avaa konsoli",
|
||||
"desc": "Avaa CEF-konsolin. Hyödyllinen vain debugaustarkoituksiin. Täällä olevat jutut ovat mahdollisesti vaarallisia, ja niitä tulisi käyttää vain, jos olet laajennuksen kehittäjä tai jos kehittäjä on ohjannut sinut tänne.",
|
||||
"label": "CEF-konsoli"
|
||||
},
|
||||
"header": "Muu",
|
||||
"react_devtools": {
|
||||
"desc": "Mahdollistaa yhteyden tietokoneeseen, jossa on käytössä React DevTools. Tämän asetuksen muuttaminen lataa Steamin uudelleen. Aseta IP-osoite ennen käyttöönottoa.",
|
||||
"ip_label": "IP-osoite",
|
||||
"label": "Ota React DevTools käyttöön"
|
||||
},
|
||||
"third_party_plugins": {
|
||||
"button_install": "Asenna",
|
||||
"button_zip": "Selaa",
|
||||
"header": "Kolmannen osapuolen laajennukset",
|
||||
"label_desc": "URL-osoite",
|
||||
"label_url": "Asenna laajennus URL-osoitteesta",
|
||||
"label_zip": "Asenna laajennus ZIP-tiedostosta"
|
||||
},
|
||||
"valve_internal": {
|
||||
"desc1": "Ottaa käyttöön Valven sisäisen kehittäjävalikon.",
|
||||
"desc2": "Älä koske mihinkään tässä valikossa, ellet tiedä mitä se tekee.",
|
||||
"label": "Ota Valve Internal käyttöön"
|
||||
}
|
||||
},
|
||||
"SettingsGeneralIndex": {
|
||||
"about": {
|
||||
"decky_version": "Decky-versio",
|
||||
"header": "Tietoja"
|
||||
},
|
||||
"beta": {
|
||||
"header": "Beta-osallistuminen"
|
||||
},
|
||||
"developer_mode": {
|
||||
"label": "Kehittäjätila"
|
||||
},
|
||||
"notifications": {
|
||||
"decky_updates_label": "Decky-päivitys saatavilla",
|
||||
"header": "Ilmoitukset",
|
||||
"plugin_updates_label": "Laajennuspäivityksiä saatavilla"
|
||||
},
|
||||
"other": {
|
||||
"header": "Muu"
|
||||
},
|
||||
"updates": {
|
||||
"header": "Päivitykset"
|
||||
}
|
||||
},
|
||||
"SettingsIndex": {
|
||||
"developer_title": "Kehittäjä",
|
||||
"general_title": "Yleinen",
|
||||
"plugins_title": "Laajennukset"
|
||||
},
|
||||
"Store": {
|
||||
"store_contrib": {
|
||||
"desc": "Mikäli haluat julkaista Decky Plugin Storeen, tarkista GitHubin SteamDeckHomebrew/decky-plugin-template -esimerkkitietovarasto. Tietoa kehityksestä ja jakelusta löytyy README:stä.",
|
||||
"label": "Osallistuminen"
|
||||
},
|
||||
"store_filter": {
|
||||
"label": "Suodin",
|
||||
"label_def": "Kaikki"
|
||||
},
|
||||
"store_search": {
|
||||
"label": "Hae"
|
||||
},
|
||||
"store_sort": {
|
||||
"label": "Järjestä",
|
||||
"label_def": "Viimeksi päivitetty (uusin ensin)"
|
||||
},
|
||||
"store_source": {
|
||||
"desc": "Kaikken laajennusten lähdekoodit ovat saatavilla SteamDeckHomebrew/decky-plugin-database -arkistosta GitHubissa.",
|
||||
"label": "Lähdekoodi"
|
||||
},
|
||||
"store_tabs": {
|
||||
"about": "Tietoja",
|
||||
"alph_asce": "Aakkosjärjestyksessä (Z–A)",
|
||||
"alph_desc": "Aakkosjärjestyksessä (A–Z)",
|
||||
"title": "Selaa"
|
||||
},
|
||||
"store_testing_cta": "Harkitse uusien lisäosien testaamista auttaaksesi Decky Loader -tiimiä!",
|
||||
"store_testing_warning": {
|
||||
"desc": "Voit käyttää tätä myymäläkanavaa testataksesi uusimpia laajennusversioita. Muista jättää palautetta GitHubissa, jotta laajennus voidaan päivittää kaikille käyttäjille.",
|
||||
"label": "Tervetuloa testausmyymälä-kanavalle"
|
||||
}
|
||||
},
|
||||
"StoreSelect": {
|
||||
"custom_store": {
|
||||
"label": "Mukautettu myymälä",
|
||||
"url_label": "URL-osoite"
|
||||
},
|
||||
"store_channel": {
|
||||
"custom": "Mukautettu",
|
||||
"default": "Oletus",
|
||||
"label": "Myymäläkanava",
|
||||
"testing": "Testaus"
|
||||
}
|
||||
},
|
||||
"TitleView": {
|
||||
"decky_store_desc": "Avaa Decky-myymälä",
|
||||
"settings_desc": "Avaa Decky-asetukset"
|
||||
},
|
||||
"Updater": {
|
||||
"decky_updates": "Decky-päivitykset",
|
||||
"no_patch_notes_desc": "tälle versiolle ei ole korjausmerkintöjä",
|
||||
"patch_notes_desc": "Korjausmerkinnät",
|
||||
"updates": {
|
||||
"check_button": "Tarkista päivitykset",
|
||||
"checking": "Tarkistetaan",
|
||||
"cur_version": "Nykyinen versio: {{ver}}",
|
||||
"install_button": "Asenna päivitys",
|
||||
"label": "Päivitykset",
|
||||
"lat_version": "Ajan tasalla: versio {{ver}}",
|
||||
"reloading": "Uudelleenladataan",
|
||||
"updating": "Päivitetään"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
{
|
||||
"BranchSelect": {
|
||||
"update_channel": {
|
||||
"label": "Canal de mise à jour",
|
||||
"prerelease": "Préliminaire",
|
||||
"stable": "Stable",
|
||||
"testing": "Test"
|
||||
}
|
||||
},
|
||||
"Developer": {
|
||||
"5secreload": "Rechargement dans 5 secondes",
|
||||
"disabling": "Désactivation des React DevTools",
|
||||
"enabling": "Activation des React DevTools"
|
||||
},
|
||||
"DropdownMultiselect": {
|
||||
"button": {
|
||||
"back": "Retour"
|
||||
}
|
||||
},
|
||||
"FilePickerError": {
|
||||
"errors": {
|
||||
"file_not_found": "Le chemin spécifié n'est pas valide. Veuillez vérifier et essayer à nouveau.",
|
||||
"perm_denied": "Vous n'avez pas accès au dossier spécifié. Veuillez vérifier que votre utilisateur (deck sur un Steam Deck) possède les permissions requises pour accéder au dossier/fichier spécifié.",
|
||||
"unknown": "Une erreur inconnue est survenue. L'erreur est : {{raw_error}}"
|
||||
}
|
||||
},
|
||||
"FilePickerIndex": {
|
||||
"file": {
|
||||
"select": "Sélectionner ce fichier"
|
||||
},
|
||||
"files": {
|
||||
"all_files": "Tout les fichiers",
|
||||
"file_type": "Type de fichier",
|
||||
"show_hidden": "Afficher les fichiers cachés"
|
||||
},
|
||||
"filter": {
|
||||
"created_asce": "Création (Plus vieux)",
|
||||
"created_desc": "Création (Plus récent)",
|
||||
"modified_asce": "Modifié (Plus vieux)",
|
||||
"modified_desc": "Modifié (Plus récent)",
|
||||
"name_asce": "Z-A",
|
||||
"name_desc": "A-Z",
|
||||
"size_asce": "Taille (Plus petit)",
|
||||
"size_desc": "Taille (Plus grand)"
|
||||
},
|
||||
"folder": {
|
||||
"label": "Dossier",
|
||||
"select": "Utiliser ce dossier",
|
||||
"show_more": "Afficher plus de fichiers"
|
||||
}
|
||||
},
|
||||
"MultiplePluginsInstallModal": {
|
||||
"confirm": "Êtes-vous sûr de vouloir apporter les modifications suivantes ?",
|
||||
"description": {
|
||||
"downgrade": "Rétrograder {{name}} en {{version}}",
|
||||
"install": "Installer {{name}} {{version}}",
|
||||
"overwrite": "Écraser {{name}} avec {{version}}",
|
||||
"reinstall": "Réinstaller {{name}} {{version}}",
|
||||
"update": "Mettre à jour {{name}} en {{version}}"
|
||||
},
|
||||
"ok_button": {
|
||||
"idle": "Confirmer",
|
||||
"loading": "En cours"
|
||||
},
|
||||
"title": {
|
||||
"downgrade_many": "Rétrograder {{count}} plugins",
|
||||
"downgrade_one": "Rétrograder 1 plugin",
|
||||
"downgrade_other": "Rétrograder {{count}} plugins",
|
||||
"install_many": "Installer {{count}} plugins",
|
||||
"install_one": "Installer 1 plugin",
|
||||
"install_other": "Installer {{count}} plugins",
|
||||
"mixed_many": "Modifier {{count}} plugins",
|
||||
"mixed_one": "Modifier {{count}} plugin",
|
||||
"mixed_other": "Modifier {{count}} plugins",
|
||||
"overwrite_many": "Écraser {{count}} plugins",
|
||||
"overwrite_one": "Écraser 1 plugin",
|
||||
"overwrite_other": "Écraser {{count}} plugins",
|
||||
"reinstall_many": "Réinstaller {{count}} plugins",
|
||||
"reinstall_one": "Réinstaller 1 plugin",
|
||||
"reinstall_other": "Réinstaller {{count}} plugins",
|
||||
"update_many": "Mettre à jour {{count}} plugins",
|
||||
"update_one": "Mettre à jour 1 plugin",
|
||||
"update_other": "Mettre à jour {{count}} plugins"
|
||||
}
|
||||
},
|
||||
"PluginCard": {
|
||||
"plugin_downgrade": "Rétrograder",
|
||||
"plugin_full_access": "Ce plugin a un accès complet à votre Steam Deck.",
|
||||
"plugin_install": "Installer",
|
||||
"plugin_no_desc": "Aucune description fournie.",
|
||||
"plugin_overwrite": "Écraser",
|
||||
"plugin_reinstall": "Réinstaller",
|
||||
"plugin_update": "Mettre à jour",
|
||||
"plugin_version_label": "Version du plugin"
|
||||
},
|
||||
"PluginInstallModal": {
|
||||
"downgrade": {
|
||||
"button_idle": "Rétrograder",
|
||||
"button_processing": "Rétrogradation",
|
||||
"desc": "Êtes-vous sûr de vouloir rétrograder {{artifact}} vers la version {{version}} ?",
|
||||
"title": "Rétrograder {{artifact}}"
|
||||
},
|
||||
"install": {
|
||||
"button_idle": "Installer",
|
||||
"button_processing": "Installation en cours",
|
||||
"desc": "Êtes-vous sûr de vouloir installer {{artifact}} {{version}} ?",
|
||||
"title": "Installer {{artifact}}"
|
||||
},
|
||||
"no_hash": "Ce plugin n'a pas de somme de contrôle, vous l'installez à vos risques et périls.",
|
||||
"not_installed": "(non installé)",
|
||||
"overwrite": {
|
||||
"button_idle": "Écraser",
|
||||
"button_processing": "Écrasement",
|
||||
"desc": "Êtes-vous sûr de vouloir remplacer {{artifact}} par la version {{version}} ?",
|
||||
"title": "Écraser {{artifact}}"
|
||||
},
|
||||
"reinstall": {
|
||||
"button_idle": "Réinstaller",
|
||||
"button_processing": "Réinstallation en cours",
|
||||
"desc": "Êtes-vous sûr de vouloir réinstaller {{artifact}} {{version}} ?",
|
||||
"title": "Réinstaller {{artifact}}"
|
||||
},
|
||||
"update": {
|
||||
"button_idle": "Mettre à jour",
|
||||
"button_processing": "Mise à jour en cours",
|
||||
"desc": "Êtes-vous sûr de vouloir mettre à jour {{artifact}} vers la version {{version}} ?",
|
||||
"title": "Mettre à jour {{artifact}}"
|
||||
}
|
||||
},
|
||||
"PluginListIndex": {
|
||||
"freeze": "Geler les mises à jour",
|
||||
"hide": "Accès rapide : Cacher",
|
||||
"no_plugin": "Aucun plugin installé !",
|
||||
"plugin_actions": "Plugin Actions",
|
||||
"reinstall": "Réinstaller",
|
||||
"reload": "Recharger",
|
||||
"show": "Accès Rapide : Afficher",
|
||||
"unfreeze": "Autoriser les mises à jour",
|
||||
"uninstall": "Désinstaller",
|
||||
"update_all_many": "Mettre à jour {{count}} plugins",
|
||||
"update_all_one": "Mettre à jour 1 plugin",
|
||||
"update_all_other": "Mettre à jour {{count}} plugins",
|
||||
"update_to": "Mettre à jour vers {{name}}"
|
||||
},
|
||||
"PluginListLabel": {
|
||||
"hidden": "Caché du menu d'accès rapide"
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
"decky_update_available": "Mise à jour vers {{tag_name}} disponible !",
|
||||
"error": "Erreur",
|
||||
"plugin_error_uninstall": "Le chargement de {{name}} a provoqué une exception comme indiqué ci-dessus. Cela signifie généralement que le plugin nécessite une mise à jour pour la nouvelle version de SteamUI. Vérifiez si une mise à jour est présente ou évaluez sa suppression dans les paramètres de Decky, dans la section Plugins.",
|
||||
"plugin_load_error": {
|
||||
"message": "Erreur lors du chargement du plugin {{name}}",
|
||||
"toast": "Erreur lors du chargement de {{name}}"
|
||||
},
|
||||
"plugin_uninstall": {
|
||||
"button": "Désinstaller",
|
||||
"desc": "Êtes-vous sûr.e de vouloir désinstaller {{name}} ?",
|
||||
"title": "Désinstaller {{name}}"
|
||||
},
|
||||
"plugin_update_many": "Mises à jour disponible pour {{count}} plugins !",
|
||||
"plugin_update_one": "Mise à jour disponible pour 1 plugin !",
|
||||
"plugin_update_other": "Mises à jour disponible pour {{count}} plugins !"
|
||||
},
|
||||
"PluginView": {
|
||||
"hidden_many": "{{count}} plugins sont masqués de cette liste",
|
||||
"hidden_one": "1 plugin est masqué dans cette liste",
|
||||
"hidden_other": "{{count}} plugins sont masqués de cette liste"
|
||||
},
|
||||
"RemoteDebugging": {
|
||||
"remote_cef": {
|
||||
"desc": "Autoriser l'accès non authentifié au débogueur CEF à toute personne de votre réseau",
|
||||
"label": "Autoriser le débogage CEF à distance"
|
||||
}
|
||||
},
|
||||
"SettingsDeveloperIndex": {
|
||||
"cef_console": {
|
||||
"button": "Ouvrir la console",
|
||||
"desc": "Ouvre la console CEF. Utile uniquement à des fins de débogage. Les éléments présentés ici sont potentiellement dangereux et ne doivent être utilisés que si vous êtes un développeur de plugins ou si vous êtes dirigé ici par un de ces développeurs.",
|
||||
"label": "Console CEF"
|
||||
},
|
||||
"header": "Autre",
|
||||
"react_devtools": {
|
||||
"desc": "Permet la connexion à un ordinateur exécutant React DevTools. Changer ce paramètre rechargera Steam. Définissez l'adresse IP avant l'activation.",
|
||||
"ip_label": "IP",
|
||||
"label": "Activer React DevTools"
|
||||
},
|
||||
"third_party_plugins": {
|
||||
"button_install": "Installer",
|
||||
"button_zip": "Parcourir",
|
||||
"header": "Plugins tiers",
|
||||
"label_desc": "URL",
|
||||
"label_url": "Installer le plugin à partir d'un URL",
|
||||
"label_zip": "Installer le plugin à partir d'un fichier ZIP"
|
||||
},
|
||||
"valve_internal": {
|
||||
"desc1": "Active le menu développeur interne de Valve.",
|
||||
"desc2": "Ne touchez à rien dans ce menu à moins que vous ne sachiez ce que ça fait.",
|
||||
"label": "Activer Valve Internal"
|
||||
}
|
||||
},
|
||||
"SettingsGeneralIndex": {
|
||||
"about": {
|
||||
"decky_version": "Version de Decky",
|
||||
"header": "À propos"
|
||||
},
|
||||
"beta": {
|
||||
"header": "Participation à la Bêta"
|
||||
},
|
||||
"developer_mode": {
|
||||
"label": "Mode développeur"
|
||||
},
|
||||
"notifications": {
|
||||
"decky_updates_label": "Mise à jour de Decky disponible",
|
||||
"header": "Notifications",
|
||||
"plugin_updates_label": "Mises à jour des plugins disponibles"
|
||||
},
|
||||
"other": {
|
||||
"header": "Autre"
|
||||
},
|
||||
"updates": {
|
||||
"header": "Mises à jour"
|
||||
}
|
||||
},
|
||||
"SettingsIndex": {
|
||||
"developer_title": "Développeur",
|
||||
"general_title": "Général",
|
||||
"plugins_title": "Plugins",
|
||||
"testing_title": "Expérimentations"
|
||||
},
|
||||
"Store": {
|
||||
"download_progress_info": {
|
||||
"download_remote": "Téléchargement des binaires externes",
|
||||
"download_zip": "Téléchargement du plugin",
|
||||
"increment_count": "Incrémentation du nombre de téléchargements",
|
||||
"installing_plugin": "Installation du plugin",
|
||||
"open_zip": "Ouverture du fichier zip",
|
||||
"parse_zip": "Analyse du fichier zip",
|
||||
"start": "Initialisation",
|
||||
"uninstalling_previous": "Désinstallation de la copie précédente"
|
||||
},
|
||||
"store_contrib": {
|
||||
"desc": "Si vous souhaitez contribuer au Decky Plugin Store, consultez le dépôt SteamDeckHomebrew/decky-plugin-template sur GitHub. Des informations sur le développement et la distribution sont disponibles dans le fichier README.",
|
||||
"label": "Contributions"
|
||||
},
|
||||
"store_filter": {
|
||||
"label": "Filtrer",
|
||||
"label_def": "Tous"
|
||||
},
|
||||
"store_search": {
|
||||
"label": "Rechercher"
|
||||
},
|
||||
"store_sort": {
|
||||
"label": "Trier",
|
||||
"label_def": "Mises à jour (Plus récentes)"
|
||||
},
|
||||
"store_source": {
|
||||
"desc": "Tout le code source des plugins est disponible sur le dépôt SteamDeckHomebrew/decky-plugin-database sur GitHub.",
|
||||
"label": "Code Source"
|
||||
},
|
||||
"store_tabs": {
|
||||
"about": "À propos",
|
||||
"alph_asce": "Alphabétique (Z à A)",
|
||||
"alph_desc": "Alphabétique (A à Z)",
|
||||
"date_asce": "Plus ancien en premier",
|
||||
"date_desc": "Le plus récent d'abord",
|
||||
"downloads_asce": "Le moins téléchargé en premier",
|
||||
"downloads_desc": "Les plus téléchargés en premier",
|
||||
"title": "Explorer"
|
||||
},
|
||||
"store_testing_cta": "Pensez à tester de nouveaux plugins pour aider l'équipe Decky Loader !",
|
||||
"store_testing_warning": {
|
||||
"desc": "Vous pouvez utiliser cette chaîne de magasin pour tester des versions de plugins. Assurez-vous de laisser des commentaires sur GitHub afin que le plugin puisse être mis à jour pour tous les utilisateurs.",
|
||||
"label": "Bienvenue sur le canal test de la boutique"
|
||||
}
|
||||
},
|
||||
"StoreSelect": {
|
||||
"custom_store": {
|
||||
"label": "Magasin personnalisé",
|
||||
"url_label": "URL"
|
||||
},
|
||||
"store_channel": {
|
||||
"custom": "Personnalisé",
|
||||
"default": "Par défaut",
|
||||
"label": "Canal magasin",
|
||||
"testing": "Test"
|
||||
}
|
||||
},
|
||||
"Testing": {
|
||||
"download": "Télécharger",
|
||||
"error": "Erreur d'installation de la PR",
|
||||
"header": "Les versions suivantes de Decky Loader sont construites à partir de Pull Requests ouvertes par des tiers. L'équipe de Decky Loader n'a pas vérifié leur fonctionnalité ou leur sécurité, et elles peuvent être obsolètes.",
|
||||
"loading": "Chargement des Pull Requests ouvertes...",
|
||||
"start_download_toast": "Téléchargement de la PR #{{id}}"
|
||||
},
|
||||
"TitleView": {
|
||||
"decky_store_desc": "Ouvrir le magasin Decky",
|
||||
"settings_desc": "Ouvrir les paramètres de Decky"
|
||||
},
|
||||
"Updater": {
|
||||
"decky_updates": "Mises à jour de Decky",
|
||||
"no_patch_notes_desc": "pas de notes de mise à jour pour cette version",
|
||||
"patch_notes_desc": "Notes de mise à jour",
|
||||
"updates": {
|
||||
"check_button": "Vérifier les mises à jour",
|
||||
"checking": "Recherche",
|
||||
"cur_version": "Version actuelle: {{ver}}",
|
||||
"install_button": "Installer la mise à jour",
|
||||
"label": "Mises à jour",
|
||||
"lat_version": "À jour: version {{ver}}",
|
||||
"reloading": "Rechargement",
|
||||
"updating": "Mise à jour en cours"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
{
|
||||
"BranchSelect": {
|
||||
"update_channel": {
|
||||
"label": "Canale di aggiornamento",
|
||||
"prerelease": "Prerilascio",
|
||||
"stable": "Stabile",
|
||||
"testing": "In prova"
|
||||
}
|
||||
},
|
||||
"Developer": {
|
||||
"5secreload": "Ricarico tra 5 secondi",
|
||||
"disabling": "Disabilito i tools di React",
|
||||
"enabling": "Abilito i tools di React"
|
||||
},
|
||||
"DropdownMultiselect": {
|
||||
"button": {
|
||||
"back": "Indietro"
|
||||
}
|
||||
},
|
||||
"FilePickerError": {
|
||||
"errors": {
|
||||
"file_not_found": "Il percorso specificato non è valido. Controllalo e prova a reinserirlo di nuovo.",
|
||||
"perm_denied": "Il tuo utente non ha accesso alla directory specificata. Verifica se l'utente corrente (è deck su Steam Deck di default) ha i permessi corrispondenti per accedere alla cartella/file desiderato.",
|
||||
"unknown": "È avvenuto un'errore sconosciuto. L'errore segnalato è {{raw_error}}"
|
||||
}
|
||||
},
|
||||
"FilePickerIndex": {
|
||||
"file": {
|
||||
"select": "Seleziona questo file"
|
||||
},
|
||||
"files": {
|
||||
"all_files": "Tutti i file",
|
||||
"file_type": "Tipo di file",
|
||||
"show_hidden": "Mostra nascosti"
|
||||
},
|
||||
"filter": {
|
||||
"created_asce": "Creazione (meno recente)",
|
||||
"created_desc": "Creazione (più recente)",
|
||||
"modified_asce": "Modifica (meno recente)",
|
||||
"modified_desc": "Modifica (più recente)",
|
||||
"name_asce": "Z-A",
|
||||
"name_desc": "A-Z",
|
||||
"size_asce": "Dimensione (più piccolo)",
|
||||
"size_desc": "Dimensione (più grande)"
|
||||
},
|
||||
"folder": {
|
||||
"label": "Cartella",
|
||||
"select": "Usa questa cartella",
|
||||
"show_more": "Mostra più file"
|
||||
}
|
||||
},
|
||||
"MultiplePluginsInstallModal": {
|
||||
"confirm": "Sei sicuro di voler effettuare le modifiche seguenti?",
|
||||
"description": {
|
||||
"downgrade": "Downgrada {{name}} a versione {{version}}",
|
||||
"install": "Installa {{name}} {{version}}",
|
||||
"overwrite": "Sovrascrive {{name}} con {{version}}",
|
||||
"reinstall": "Reinstalla {{name}} {{version}}",
|
||||
"update": "Aggiorna {{name}} alla versione {{version}}"
|
||||
},
|
||||
"ok_button": {
|
||||
"idle": "Conferma",
|
||||
"loading": "Elaboro"
|
||||
},
|
||||
"title": {
|
||||
"downgrade_many": "Downgrada {{count}} plugins",
|
||||
"downgrade_one": "Downgrada un plugin",
|
||||
"downgrade_other": "Downgrada {{count}} plugins",
|
||||
"install_many": "Installa {{count}} plugins",
|
||||
"install_one": "Installa un plugin",
|
||||
"install_other": "Installa {{count}} plugins",
|
||||
"mixed_many": "Modifica {{count}} plugins",
|
||||
"mixed_one": "Modifica un plugin",
|
||||
"mixed_other": "Modifica {{count}} plugins",
|
||||
"overwrite_many": "Sovrascrivi {{count}} plugins",
|
||||
"overwrite_one": "Sovrascrivi un plugin",
|
||||
"overwrite_other": "Sovrascrivi {{count}} plugins",
|
||||
"reinstall_many": "Reinstalla {{count}} plugins",
|
||||
"reinstall_one": "Reinstalla un plugin",
|
||||
"reinstall_other": "Reinstalla {{count}} plugins",
|
||||
"update_many": "Aggiorna {{count}} plugins",
|
||||
"update_one": "Aggiorna un plugin",
|
||||
"update_other": "Aggiorna {{count}} plugins"
|
||||
}
|
||||
},
|
||||
"PluginCard": {
|
||||
"plugin_downgrade": "Downgrada",
|
||||
"plugin_full_access": "Questo plugin ha accesso completo al tuo Steam Deck.",
|
||||
"plugin_install": "Installa",
|
||||
"plugin_no_desc": "Nessuna descrizione fornita.",
|
||||
"plugin_overwrite": "Sovrascrivi",
|
||||
"plugin_reinstall": "Reinstalla",
|
||||
"plugin_update": "Aggiorna",
|
||||
"plugin_version_label": "Versione Plugin"
|
||||
},
|
||||
"PluginInstallModal": {
|
||||
"downgrade": {
|
||||
"button_idle": "Downgrada",
|
||||
"button_processing": "Downgradando",
|
||||
"desc": "Sei sicuro di voler downgradare {{artifact}} alla versione {{version}}?",
|
||||
"title": "Downgrada {{artifact}}"
|
||||
},
|
||||
"install": {
|
||||
"button_idle": "Installa",
|
||||
"button_processing": "Installando",
|
||||
"desc": "Sei sicuro di voler installare {{artifact}} {{version}}?",
|
||||
"title": "Installa {{artifact}}"
|
||||
},
|
||||
"no_hash": "Questo plugin non ha un hash associato, lo stai installando a tuo rischio e pericolo.",
|
||||
"not_installed": "(non installato)",
|
||||
"overwrite": {
|
||||
"button_idle": "Sovrascrivi",
|
||||
"button_processing": "Sovrascrivendo",
|
||||
"desc": "Sei sicuro di voler sovrascrivere {{artifact}} con la versione {{version}}?",
|
||||
"title": "Sovrascrivi {{artifact}}"
|
||||
},
|
||||
"reinstall": {
|
||||
"button_idle": "Reinstalla",
|
||||
"button_processing": "Reinstallando",
|
||||
"desc": "Sei sicuro di voler reinstallare {{artifact}} {{version}}?",
|
||||
"title": "Reinstalla {{artifact}}"
|
||||
},
|
||||
"update": {
|
||||
"button_idle": "Aggiorna",
|
||||
"button_processing": "Aggiornando",
|
||||
"desc": "Sei sicuro di voler aggiornare {{artifact}} alla versione {{version}}?",
|
||||
"title": "Aggiorna {{artifact}}"
|
||||
}
|
||||
},
|
||||
"PluginListIndex": {
|
||||
"freeze": "Congela aggiornamenti",
|
||||
"hide": "Accesso rapido: Nascondi",
|
||||
"no_plugin": "Nessun plugin installato!",
|
||||
"plugin_actions": "Operazioni sui plugins",
|
||||
"reinstall": "Reinstalla",
|
||||
"reload": "Ricarica",
|
||||
"show": "Accesso rapido: Mostra",
|
||||
"unfreeze": "Permetti aggiornamenti",
|
||||
"uninstall": "Rimuovi",
|
||||
"update_all_many": "Aggiorna {{count}} plugins",
|
||||
"update_all_one": "Aggiorna un plugin",
|
||||
"update_all_other": "Aggiorna {{count}} plugins",
|
||||
"update_to": "Aggiorna a {{name}}"
|
||||
},
|
||||
"PluginListLabel": {
|
||||
"hidden": "Nascosto dal menu di accesso rapido"
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
"decky_update_available": "Disponibile aggiornamento a {{tag_name}}!",
|
||||
"error": "Errore",
|
||||
"plugin_error_uninstall": "Il plugin {{name}} ha causato un'eccezione che è descritta sopra. Questo tipicamente significa che il plugin deve essere aggiornato per funzionare sulla nuova versione di SteamUI. Controlla se è disponibile un aggiornamento o valutane la rimozione andando nelle impostazioni di Decky nella sezione Plugins.",
|
||||
"plugin_load_error": {
|
||||
"message": "Errore caricando il plugin {{name}}",
|
||||
"toast": "Errore caricando {{name}}"
|
||||
},
|
||||
"plugin_uninstall": {
|
||||
"button": "Rimuovi",
|
||||
"desc": "Sei sicuro di voler rimuovere {{name}}?",
|
||||
"title": "Rimuovi {{name}}"
|
||||
},
|
||||
"plugin_update_many": "Aggiornamenti disponibili per {{count}} plugins!",
|
||||
"plugin_update_one": "Aggiornamento disponibile per 1 plugin!",
|
||||
"plugin_update_other": "Aggiornamenti disponibili per {{count}} plugins!"
|
||||
},
|
||||
"PluginView": {
|
||||
"hidden_many": "Sono nascosti {{count}} plugin dalla lista",
|
||||
"hidden_one": "Un plugin è nascosto dalla lista",
|
||||
"hidden_other": "Sono nascosti {{count}} plugin dalla lista"
|
||||
},
|
||||
"RemoteDebugging": {
|
||||
"remote_cef": {
|
||||
"desc": "Permetti l'accesso non autenticato al debugger di CEF da tutti gli indirizzi sulla tua rete locale",
|
||||
"label": "Permetti il debug remoto di CEF"
|
||||
}
|
||||
},
|
||||
"SettingsDeveloperIndex": {
|
||||
"cef_console": {
|
||||
"button": "Apri la console",
|
||||
"desc": "Apri la console di CEF. Utile solamente per ragioni di debug. Questa opzione deve essere usata solo se sei uno sviluppatore di plugin o se uno di questi ti ha chiesto di farlo, visto che questa feature potrebbe essere potenzialmente pericolosa.",
|
||||
"label": "Console CEF"
|
||||
},
|
||||
"header": "Altro",
|
||||
"react_devtools": {
|
||||
"desc": "Abilita la connessione ad un computer che esegue i DevTools di React. Steam verrà ricaricato se lo stato cambia. Imposta il tuo indirizzo IP prima di abilitarlo.",
|
||||
"ip_label": "IP",
|
||||
"label": "Abilita i DevTools di React"
|
||||
},
|
||||
"third_party_plugins": {
|
||||
"button_install": "Installa",
|
||||
"button_zip": "Seleziona",
|
||||
"header": "Plugin di terze parti",
|
||||
"label_desc": "URL",
|
||||
"label_url": "Installa plugin da un'indirizzo web",
|
||||
"label_zip": "Installa plugin da un file ZIP"
|
||||
},
|
||||
"valve_internal": {
|
||||
"desc1": "Abilita il menu di sviluppo interno di Valve.",
|
||||
"desc2": "Non toccare nulla in questo menu se non sai quello che fa.",
|
||||
"label": "Abilita Menu Sviluppatore"
|
||||
}
|
||||
},
|
||||
"SettingsGeneralIndex": {
|
||||
"about": {
|
||||
"decky_version": "Versione di Decky",
|
||||
"header": "Riguardo a"
|
||||
},
|
||||
"beta": {
|
||||
"header": "Partecipazione alla beta"
|
||||
},
|
||||
"developer_mode": {
|
||||
"label": "Modalità sviluppatore"
|
||||
},
|
||||
"notifications": {
|
||||
"decky_updates_label": "Aggiornamenti di Decky",
|
||||
"header": "Notifiche",
|
||||
"plugin_updates_label": "Aggiornamenti dei plugins"
|
||||
},
|
||||
"other": {
|
||||
"header": "Altro"
|
||||
},
|
||||
"updates": {
|
||||
"header": "Aggiornamenti"
|
||||
}
|
||||
},
|
||||
"SettingsIndex": {
|
||||
"developer_title": "Sviluppatore",
|
||||
"general_title": "Generali",
|
||||
"plugins_title": "Plugins",
|
||||
"testing_title": "Testing"
|
||||
},
|
||||
"Store": {
|
||||
"download_progress_info": {
|
||||
"download_remote": "Scaricando qualunque binario esterno",
|
||||
"download_zip": "Scarico plugin",
|
||||
"increment_count": "Incremento il numero di download",
|
||||
"installing_plugin": "Installo il plugin",
|
||||
"open_zip": "Apro file zip",
|
||||
"parse_zip": "Analizzo file zip",
|
||||
"start": "Inizializzo",
|
||||
"uninstalling_previous": "Rimuovo la copia precedente"
|
||||
},
|
||||
"store_contrib": {
|
||||
"desc": "Se desideri contribuire allo store di Decky, puoi trovare un template caricato su GitHub all'indirizzo SteamDeckHomebrew/decky-plugin-template. Informazioni riguardo sviluppo e distribuzione sono disponibili nel README.",
|
||||
"label": "Contribuisci"
|
||||
},
|
||||
"store_filter": {
|
||||
"label": "Filtra",
|
||||
"label_def": "Tutto"
|
||||
},
|
||||
"store_search": {
|
||||
"label": "Cerca"
|
||||
},
|
||||
"store_sort": {
|
||||
"label": "Ordina",
|
||||
"label_def": "Ultimo aggiornato (Più recente)"
|
||||
},
|
||||
"store_source": {
|
||||
"desc": "Tutto il codice sorgente dei plugin è disponibile su GitHub all'indirizzo SteamDeckHomebrew/decky-plugin-database.",
|
||||
"label": "Codice Sorgente"
|
||||
},
|
||||
"store_tabs": {
|
||||
"about": "Riguardo a",
|
||||
"alph_asce": "Alfabetico (Z a A)",
|
||||
"alph_desc": "Alfabetico (A a Z)",
|
||||
"date_asce": "Per più vecchio",
|
||||
"date_desc": "Per più recente",
|
||||
"downloads_asce": "Per meno scaricato",
|
||||
"downloads_desc": "Per più scaricato",
|
||||
"title": "Sfoglia"
|
||||
},
|
||||
"store_testing_cta": "Valuta la possibilità di testare nuovi plugin per aiutare il team di Decky Loader!",
|
||||
"store_testing_warning": {
|
||||
"desc": "Puoi usare questo canale del negozio per testare versioni di plugin sperimentali. Assicurati di lasciare un feedback su Github dopo averlo testato in modo che il plugin possa essere promosso a stabile per tutti gli altri utenti o per permettere allo sviluppatore di plugin di correggere eventuali errori.",
|
||||
"label": "Benvenuto nel Negozio di Test dei Plugins"
|
||||
}
|
||||
},
|
||||
"StoreSelect": {
|
||||
"custom_store": {
|
||||
"label": "Negozio custom",
|
||||
"url_label": "URL"
|
||||
},
|
||||
"store_channel": {
|
||||
"custom": "Personalizzato",
|
||||
"default": "Default",
|
||||
"label": "Canale del negozio",
|
||||
"testing": "In prova"
|
||||
}
|
||||
},
|
||||
"Testing": {
|
||||
"download": "Scarica",
|
||||
"error": "Errore durante l'installazione della PR",
|
||||
"header": "Le versioni mostrate di Decky Loader sono create da Pull Request aperte da terze parti. Il team di Decky Loader non ha verificato la loro funzionalita o sicurezza, e potrebbero essere vecchie.",
|
||||
"loading": "Carico Pull Request aperte...",
|
||||
"start_download_toast": "Scarico PR #{{id}}"
|
||||
},
|
||||
"TitleView": {
|
||||
"decky_store_desc": "Apri lo store di Decky",
|
||||
"settings_desc": "Apri le impostazioni di Decky"
|
||||
},
|
||||
"Updater": {
|
||||
"decky_updates": "Aggiornamento di Decky",
|
||||
"no_patch_notes_desc": "nessuna patch notes per questa versione",
|
||||
"patch_notes_desc": "Cambiamenti",
|
||||
"updates": {
|
||||
"check_button": "Cerca aggiornamenti",
|
||||
"checking": "Controllando",
|
||||
"cur_version": "Versione attuale: {{ver}}",
|
||||
"install_button": "Installa aggiornamento",
|
||||
"label": "Aggiornamenti",
|
||||
"lat_version": "Aggiornato. Eseguendo {{ver}}",
|
||||
"reloading": "Ricaricando",
|
||||
"updating": "Aggiornando"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
{
|
||||
"BranchSelect": {
|
||||
"update_channel": {
|
||||
"label": "アップデートチャンネル",
|
||||
"prerelease": "プレリリース",
|
||||
"stable": "安定",
|
||||
"testing": "テスト"
|
||||
}
|
||||
},
|
||||
"Developer": {
|
||||
"5secreload": "5秒以内に再読み込みされます",
|
||||
"disabling": "React DevToolsを無効",
|
||||
"enabling": "React DevToolsを有効"
|
||||
},
|
||||
"DropdownMultiselect": {
|
||||
"button": {
|
||||
"back": "戻る"
|
||||
}
|
||||
},
|
||||
"FilePickerError": {
|
||||
"errors": {
|
||||
"file_not_found": "指定されたパスは無効です。 内容をご確認の上、正しく入力し直してください。",
|
||||
"perm_denied": "選択したパスへのアクセス権がありません。選択したフォルダ/ファイルのアクセス権がユーザー(Steam Deckのdeckユーザー)に合わせて正しく設定されていることを確認してください。",
|
||||
"unknown": "不明なエラーが発生しました。 エラー内容は次のとおりです: {{raw_error}}"
|
||||
}
|
||||
},
|
||||
"FilePickerIndex": {
|
||||
"file": {
|
||||
"select": "ファイルを選択"
|
||||
},
|
||||
"files": {
|
||||
"all_files": "すべてのファイル",
|
||||
"file_type": "ファイルタイプ",
|
||||
"show_hidden": "非表示ファイルを表示する"
|
||||
},
|
||||
"filter": {
|
||||
"created_asce": "作成日(古い順)",
|
||||
"created_desc": "作成日(新しい順)",
|
||||
"modified_asce": "更新日(古い順)",
|
||||
"modified_desc": "更新日(新しい順)",
|
||||
"name_asce": "Z-A",
|
||||
"name_desc": "A-Z",
|
||||
"size_asce": "サイズ(小さい順)",
|
||||
"size_desc": "サイズ(大きい順)"
|
||||
},
|
||||
"folder": {
|
||||
"label": "フォルダ",
|
||||
"select": "このフォルダを使用",
|
||||
"show_more": "その他のファイルを表示"
|
||||
}
|
||||
},
|
||||
"MultiplePluginsInstallModal": {
|
||||
"confirm": "以下の変更を加えてもよろしいですか?",
|
||||
"description": {
|
||||
"downgrade": "ダウングレード {{name}} {{version}}",
|
||||
"install": "インストール {{name}} {{version}}",
|
||||
"overwrite": "上書き {{name}} {{version}}",
|
||||
"reinstall": "再インストール {{name}} {{version}}",
|
||||
"update": "アップデート {{name}} {{version}}"
|
||||
},
|
||||
"ok_button": {
|
||||
"idle": "確認",
|
||||
"loading": "作業中"
|
||||
},
|
||||
"title": {
|
||||
"downgrade_other": "{{count}} 個のプラグインをダウングレード",
|
||||
"install_other": "{{count}} 個のプラグインをインストール",
|
||||
"mixed_other": "{{count}} 個のプラグインを修正",
|
||||
"overwrite_other": "{{count}} 個のプラグインを上書き",
|
||||
"reinstall_other": "{{count}} 個のプラグインを再インストール",
|
||||
"update_other": "{{count}} 個のプラグインをアップデート"
|
||||
}
|
||||
},
|
||||
"PluginCard": {
|
||||
"plugin_downgrade": "ダウングレード",
|
||||
"plugin_full_access": "このプラグインはSteam Deckの全てのアクセス権を持ちます。",
|
||||
"plugin_install": "インストール",
|
||||
"plugin_no_desc": "説明はありません。",
|
||||
"plugin_overwrite": "上書き",
|
||||
"plugin_reinstall": "再インストール",
|
||||
"plugin_update": "アップデート",
|
||||
"plugin_version_label": "プラグインバージョン"
|
||||
},
|
||||
"PluginInstallModal": {
|
||||
"downgrade": {
|
||||
"button_idle": "ダウングレード",
|
||||
"button_processing": "ダウングレード中",
|
||||
"desc": "{{artifact}}をVer {{version}} にダウングレードしてもよろしいですか?",
|
||||
"title": "{{artifact}}をダウングレード"
|
||||
},
|
||||
"install": {
|
||||
"button_idle": "インストール",
|
||||
"button_processing": "インストール中",
|
||||
"desc": "{{artifact}} {{version}} をインストールしてもよろしいですか?",
|
||||
"title": "{{artifact}} をインストール"
|
||||
},
|
||||
"no_hash": "このプラグインにはハッシュがありません。ご自身の責任でインストールしてください。",
|
||||
"not_installed": "(インストールされていません)",
|
||||
"overwrite": {
|
||||
"button_idle": "上書き",
|
||||
"button_processing": "上書き中",
|
||||
"desc": "{{artifact}}をVer {{version}} に上書きしてもよろしいですか?",
|
||||
"title": "{{artifact}}を上書き"
|
||||
},
|
||||
"reinstall": {
|
||||
"button_idle": "再インストール",
|
||||
"button_processing": "再インストール中",
|
||||
"desc": "{{artifact}} {{version}} を再インストールしてもよろしいですか?",
|
||||
"title": "{{artifact}} を再インストール"
|
||||
},
|
||||
"update": {
|
||||
"button_idle": "アップデート",
|
||||
"button_processing": "アップデート中",
|
||||
"desc": "{{artifact}}をVer {{version}} にアップデートしてもよろしいですか?",
|
||||
"title": "{{artifact}} をアップデート"
|
||||
}
|
||||
},
|
||||
"PluginListIndex": {
|
||||
"freeze": "アップデートを凍結",
|
||||
"hide": "クイックアクセス: 非表示",
|
||||
"no_plugin": "プラグインがインストールされていません!",
|
||||
"plugin_actions": "プラグインアクション",
|
||||
"reinstall": "再インストール",
|
||||
"reload": "再読み込み",
|
||||
"show": "クイックアクセス: 表示",
|
||||
"unfreeze": "アップデートを許可",
|
||||
"uninstall": "アンインストール",
|
||||
"update_all_other": "{{count}} 個のプラグインをアップデート",
|
||||
"update_to": "{{name}} を更新"
|
||||
},
|
||||
"PluginListLabel": {
|
||||
"hidden": "クイックアクセスメニューから表示されません"
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
"decky_update_available": "{{tag_name}} のアップデートが利用可能です!",
|
||||
"error": "エラー",
|
||||
"plugin_error_uninstall": "{{name}} プラグインを読み込む際に上記のような例外が発生しました。 これは通常、SteamUIの最新バージョンに合ったプラグインのアップデートが必要な場合に発生します。Decky設定のプラグインセクションでアップデートがあるかどうかを確認するか、アンインストールをお試しください。",
|
||||
"plugin_load_error": {
|
||||
"message": "プラグイン {{name}} の読み込みエラー",
|
||||
"toast": "{{name}} の読み込みエラー"
|
||||
},
|
||||
"plugin_uninstall": {
|
||||
"button": "アンインストール",
|
||||
"desc": "{{name}} をアンインストールしてもよろしいですか?",
|
||||
"title": "{{name}} をアンインストール"
|
||||
},
|
||||
"plugin_update_other": "{{count}} 個のプラグインのアップデートが利用可能です!"
|
||||
},
|
||||
"PluginView": {
|
||||
"hidden_other": "{{count}} 個のプラグインがこのリストから非表示になっています"
|
||||
},
|
||||
"RemoteDebugging": {
|
||||
"remote_cef": {
|
||||
"desc": "ネットワーク上のすべてのユーザーにCEFデバッガへの非認証アクセスを許可します",
|
||||
"label": "リモート CEF デバッグを許可する"
|
||||
}
|
||||
},
|
||||
"SettingsDeveloperIndex": {
|
||||
"cef_console": {
|
||||
"button": "コンソールを開く",
|
||||
"desc": "CEFコンソールを開きます。デバッグ目的でのみ使用してください。これらの項目は危険な可能性があるので、プラグイン開発者であるか、開発者のガイドに従う場合のみ使用する必要があります。",
|
||||
"label": "CEFコンソール"
|
||||
},
|
||||
"header": "その他",
|
||||
"react_devtools": {
|
||||
"desc": "React DevToolsを実行しているコンピューターへの接続を有効にします。この設定を変更すると、Steam が再ロードされます。有効にする前にIPアドレスを設定してください。",
|
||||
"ip_label": "IP",
|
||||
"label": "React DevTools を有効化"
|
||||
},
|
||||
"third_party_plugins": {
|
||||
"button_install": "インストール",
|
||||
"button_zip": "開く",
|
||||
"header": "サードパーティプラグイン",
|
||||
"label_desc": "URL",
|
||||
"label_url": "URLからプラグインをインストール",
|
||||
"label_zip": "ZIPファイルからプラグインをインストール"
|
||||
},
|
||||
"valve_internal": {
|
||||
"desc1": "Valveの内部開発者メニューを有効にします。",
|
||||
"desc2": "このメニューの機能が分からない場合、このメニューには触れないでください。",
|
||||
"label": "Valve Internalを有効"
|
||||
}
|
||||
},
|
||||
"SettingsGeneralIndex": {
|
||||
"about": {
|
||||
"decky_version": "Deckyバージョン",
|
||||
"header": "情報"
|
||||
},
|
||||
"beta": {
|
||||
"header": "ベータ版への参加"
|
||||
},
|
||||
"developer_mode": {
|
||||
"label": "開発者モード"
|
||||
},
|
||||
"notifications": {
|
||||
"decky_updates_label": "Deckyのアップデートが利用可能な場合に通知",
|
||||
"header": "通知",
|
||||
"plugin_updates_label": "プラグインのアップデートが利用可能な場合に通知"
|
||||
},
|
||||
"other": {
|
||||
"header": "その他"
|
||||
},
|
||||
"updates": {
|
||||
"header": "アップデート"
|
||||
}
|
||||
},
|
||||
"SettingsIndex": {
|
||||
"developer_title": "開発者",
|
||||
"general_title": "一般",
|
||||
"plugins_title": "プラグイン",
|
||||
"testing_title": "テスト"
|
||||
},
|
||||
"Store": {
|
||||
"download_progress_info": {
|
||||
"download_remote": "外部バイナリのダウンロード",
|
||||
"download_zip": "プラグインのダウンロード中",
|
||||
"increment_count": "ダウンロード数の増加",
|
||||
"installing_plugin": "プラグインのインストール中",
|
||||
"open_zip": "zipファイルを展開中",
|
||||
"parse_zip": "zipファイルの解析中",
|
||||
"start": "初期化中",
|
||||
"uninstalling_previous": "以前のコピーのアンインストール"
|
||||
},
|
||||
"store_contrib": {
|
||||
"desc": "Decky Plugin Storeに貢献したい場合は、GitHubのSteamDeckHomebrew/decky-plugin-templateリポジトリを確認してください。 開発と配布に関する情報は README で入手できます。",
|
||||
"label": "貢献"
|
||||
},
|
||||
"store_filter": {
|
||||
"label": "フィルター",
|
||||
"label_def": "すべて"
|
||||
},
|
||||
"store_search": {
|
||||
"label": "検索"
|
||||
},
|
||||
"store_sort": {
|
||||
"label": "並べ替え",
|
||||
"label_def": "直近のアップデート(新しい順)"
|
||||
},
|
||||
"store_source": {
|
||||
"desc": "すべてのプラグインのソース コードは、GitHubのSteamDeckHomebrew/decky-plugin-databaseリポジトリで入手できます。",
|
||||
"label": "ソースコード"
|
||||
},
|
||||
"store_tabs": {
|
||||
"about": "概要",
|
||||
"alph_asce": "アルファベット(Z to A)",
|
||||
"alph_desc": "アルファベット(A to Z)",
|
||||
"date_asce": "古い順",
|
||||
"date_desc": "新しい順",
|
||||
"downloads_asce": "ダウンロード数が少ない順",
|
||||
"downloads_desc": "ダウンロード数が多い順",
|
||||
"title": "閲覧"
|
||||
},
|
||||
"store_testing_cta": "Decky Loaderチームを支援するために、新しいプラグインのテストを検討してください!",
|
||||
"store_testing_warning": {
|
||||
"desc": "このストアチャンネルを使用して、最先端のプラグイン バージョンをテストできます。 すべてのユーザーがプラグインを更新できるように、必ずGitHubにフィードバックを残してください。",
|
||||
"label": "テストストア チャンネルへようこそ"
|
||||
}
|
||||
},
|
||||
"StoreSelect": {
|
||||
"custom_store": {
|
||||
"label": "カスタムストア",
|
||||
"url_label": "URL"
|
||||
},
|
||||
"store_channel": {
|
||||
"custom": "カスタム",
|
||||
"default": "デフォルト",
|
||||
"label": "ストアチャンネル",
|
||||
"testing": "テスト"
|
||||
}
|
||||
},
|
||||
"Testing": {
|
||||
"download": "ダウンロード",
|
||||
"error": "PRのインストールエラー",
|
||||
"header": "Decky Loaderの以下のバージョンは、公開されているサードパーティのPull Requestからビルドされたものです。 Decky Loaderチームはその機能や安全性を検証しておらず、内容も古い可能性があります。",
|
||||
"loading": "Pull Requestの読み込み中...",
|
||||
"start_download_toast": "PR #{{id}}のダウンロード中"
|
||||
},
|
||||
"TitleView": {
|
||||
"decky_store_desc": "Deckyストアを開く",
|
||||
"settings_desc": "Decky設定を開く"
|
||||
},
|
||||
"Updater": {
|
||||
"decky_updates": "Deckyアップデート",
|
||||
"no_patch_notes_desc": "このバージョンのパッチノートはありません",
|
||||
"patch_notes_desc": "パッチノート",
|
||||
"updates": {
|
||||
"check_button": "アップデートを確認",
|
||||
"checking": "確認中",
|
||||
"cur_version": "現在のバージョン: {{ver}}",
|
||||
"install_button": "アップデートをインストール",
|
||||
"label": "アップデート",
|
||||
"lat_version": "アップデート: {{ver}} を実行中",
|
||||
"reloading": "再読み込み中",
|
||||
"updating": "アップデート中"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -0,0 +1,276 @@
|
||||
{
|
||||
"BranchSelect": {
|
||||
"update_channel": {
|
||||
"label": "업데이트 배포 채널",
|
||||
"prerelease": "사전 출시",
|
||||
"stable": "안정",
|
||||
"testing": "테스트"
|
||||
}
|
||||
},
|
||||
"Developer": {
|
||||
"5secreload": "5초 내로 다시 로드 됩니다",
|
||||
"disabling": "React DevTools 비활성화",
|
||||
"enabling": "React DevTools 활성화"
|
||||
},
|
||||
"DropdownMultiselect": {
|
||||
"button": {
|
||||
"back": "뒤로"
|
||||
}
|
||||
},
|
||||
"FilePickerError": {
|
||||
"errors": {
|
||||
"file_not_found": "지정된 경로가 잘못되었습니다. 확인 후에 다시 입력해 주세요.",
|
||||
"perm_denied": "선택한 경로에 접근 할 수 없습니다. 선택한 폴더/파일 접근 권한이 유저(Steam Deck의 deck 유저)에 맞게 올바르게 설정 되었는지 확인하세요.",
|
||||
"unknown": "알 수 없는 오류가 발생했습니다. Raw 오류: {{raw_error}}"
|
||||
}
|
||||
},
|
||||
"FilePickerIndex": {
|
||||
"file": {
|
||||
"select": "이 파일 선택"
|
||||
},
|
||||
"files": {
|
||||
"all_files": "모든 파일",
|
||||
"file_type": "파일 형식",
|
||||
"show_hidden": "숨김 파일 표시"
|
||||
},
|
||||
"filter": {
|
||||
"created_asce": "만든 날짜 (오름차순)",
|
||||
"created_desc": "만든 날짜 (내림차 순)",
|
||||
"modified_asce": "수정한 날짜 (오름차순)",
|
||||
"modified_desc": "수정한 날짜 (내림차순)",
|
||||
"name_asce": "Z-A",
|
||||
"name_desc": "A-Z",
|
||||
"size_asce": "크기 (오름차순)",
|
||||
"size_desc": "크기 (내림차순)"
|
||||
},
|
||||
"folder": {
|
||||
"label": "폴더",
|
||||
"select": "이 폴더 사용",
|
||||
"show_more": "더 많은 파일 표시"
|
||||
}
|
||||
},
|
||||
"MultiplePluginsInstallModal": {
|
||||
"confirm": "해당 수정을 적용하겠습니까?",
|
||||
"description": {
|
||||
"install": "{{name}} {{version}} 플러그인 설치",
|
||||
"reinstall": "{{name}} {{version}} 재설치",
|
||||
"update": "{{name}}의 {{version}} 업데이트 설치"
|
||||
},
|
||||
"ok_button": {
|
||||
"idle": "확인",
|
||||
"loading": "작업 중"
|
||||
},
|
||||
"title": {
|
||||
"install_other": "플러그인 {{count}}개 설치",
|
||||
"mixed_other": "플러그인 {{count}}개 수정",
|
||||
"reinstall_other": "플러그인 {{count}}개 재설치",
|
||||
"update_other": "플러그인 {{count}}개 업데이트"
|
||||
}
|
||||
},
|
||||
"PluginCard": {
|
||||
"plugin_full_access": "이 플러그인은 Steam Deck의 모든 접근 권한을 가집니다.",
|
||||
"plugin_install": "설치",
|
||||
"plugin_no_desc": "플러그인 설명이 제공되지 않았습니다.",
|
||||
"plugin_version_label": "플러그인 버전"
|
||||
},
|
||||
"PluginInstallModal": {
|
||||
"install": {
|
||||
"button_idle": "설치",
|
||||
"button_processing": "설치 중",
|
||||
"desc": "{{artifact}} {{version}}을(를) 설치하겠습니까?",
|
||||
"title": "{{artifact}} 설치"
|
||||
},
|
||||
"no_hash": "이 플러그인은 해시 확인을 하지 않습니다, 설치에 따른 위험은 사용자가 감수해야 합니다.",
|
||||
"reinstall": {
|
||||
"button_idle": "재설치",
|
||||
"button_processing": "재설치 중",
|
||||
"desc": "{{artifact}} {{version}}을(를) 재설치하겠습니까?",
|
||||
"title": "{{artifact}} 재설치"
|
||||
},
|
||||
"update": {
|
||||
"button_idle": "업데이트",
|
||||
"button_processing": "업데이트 중",
|
||||
"desc": "{{artifact}} {{version}} 업데이트를 설치하겠습니까?",
|
||||
"title": "{{artifact}} 업데이트"
|
||||
}
|
||||
},
|
||||
"PluginListIndex": {
|
||||
"freeze": "업데이트 일시 중지",
|
||||
"hide": "빠른 액세스 메뉴: 숨김",
|
||||
"no_plugin": "설치된 플러그인이 없습니다!",
|
||||
"plugin_actions": "플러그인 동작",
|
||||
"reinstall": "재설치",
|
||||
"reload": "다시 로드",
|
||||
"show": "빠른 액세스 메뉴: 표시",
|
||||
"unfreeze": "업데이트 허용",
|
||||
"uninstall": "설치 제거",
|
||||
"update_all_other": "플러그인 {{count}}개 업데이트",
|
||||
"update_to": "{{name}}(으)로 업데이트"
|
||||
},
|
||||
"PluginListLabel": {
|
||||
"hidden": "빠른 액세스 메뉴에서 숨김"
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
"decky_update_available": "{{tag_name}} 업데이트를 설치할 수 있습니다!",
|
||||
"error": "오류",
|
||||
"plugin_error_uninstall": "{{name}} 플러그인을 불러올 때 위와 같은 예외가 발생했습니다. 이는 보통 SteamUI 최신 버전에 맞는 플러그인 업데이트가 필요할 때 발생합니다. Decky 설정의 플러그인 섹션에서 업데이트가 있는지 확인하거나 설치 제거를 시도 해 보세요.",
|
||||
"plugin_load_error": {
|
||||
"message": "{{name}} 플러그인 불러오기 오류",
|
||||
"toast": "{{name}} 불러오기 오류"
|
||||
},
|
||||
"plugin_uninstall": {
|
||||
"button": "설치 제거",
|
||||
"desc": "{{name}}을(를) 설치 제거하겠습니까?",
|
||||
"title": "{{name}} 설치 제거"
|
||||
},
|
||||
"plugin_update_other": "플러그인 {{count}}개를 업데이트 할 수 있습니다!"
|
||||
},
|
||||
"PluginView": {
|
||||
"hidden_other": "플러그인 {{count}}개 숨김"
|
||||
},
|
||||
"RemoteDebugging": {
|
||||
"remote_cef": {
|
||||
"desc": "네트워크의 모든 사용자에게 CEF 디버거에 대한 인증되지 않은 액세스 허용",
|
||||
"label": "리모트 CEF 디버그 허용"
|
||||
}
|
||||
},
|
||||
"SettingsDeveloperIndex": {
|
||||
"cef_console": {
|
||||
"button": "콘솔 열기",
|
||||
"desc": "CEF 콘솔을 엽니다. 디버그 전용입니다. 이 항목들은 위험 가능성이 있으므로 플러그인 개발자이거나 개발자의 가이드를 따를 경우에만 사용해야 합니다.",
|
||||
"label": "CEF 콘솔"
|
||||
},
|
||||
"header": "기타",
|
||||
"react_devtools": {
|
||||
"desc": "React DevTools를 실행하고 있는 컴퓨터에 연결을 활성화합니다. 이 설정을 변경하면 Steam이 다시 로드됩니다. 활성화하기 전에 IP 주소를 설정하세요.",
|
||||
"ip_label": "IP",
|
||||
"label": "React DevTools 활성화"
|
||||
},
|
||||
"third_party_plugins": {
|
||||
"button_install": "설치",
|
||||
"button_zip": "검색",
|
||||
"header": "서드파티 플러그인",
|
||||
"label_desc": "URL",
|
||||
"label_url": "URL에서 플러그인 설치",
|
||||
"label_zip": "ZIP 파일에서 플러그인 설치"
|
||||
},
|
||||
"valve_internal": {
|
||||
"desc1": "Valve 내부 개발자 메뉴를 활성화합니다.",
|
||||
"desc2": "이 메뉴의 기능을 모른다면 어떤 것도 건드리지 마세요.",
|
||||
"label": "Valve 내부 개발자 메뉴 활성화"
|
||||
}
|
||||
},
|
||||
"SettingsGeneralIndex": {
|
||||
"about": {
|
||||
"decky_version": "Decky 버전",
|
||||
"header": "정보"
|
||||
},
|
||||
"beta": {
|
||||
"header": "베타 참가"
|
||||
},
|
||||
"developer_mode": {
|
||||
"label": "개발자 모드"
|
||||
},
|
||||
"notifications": {
|
||||
"decky_updates_label": "Decky 업데이트 가능",
|
||||
"header": "알림",
|
||||
"plugin_updates_label": "플러그인 업데이트 가능"
|
||||
},
|
||||
"other": {
|
||||
"header": "기타"
|
||||
},
|
||||
"updates": {
|
||||
"header": "업데이트"
|
||||
}
|
||||
},
|
||||
"SettingsIndex": {
|
||||
"developer_title": "개발자",
|
||||
"general_title": "일반",
|
||||
"plugins_title": "플러그인",
|
||||
"testing_title": "테스트"
|
||||
},
|
||||
"Store": {
|
||||
"download_progress_info": {
|
||||
"download_zip": "플러그인 다운로드 중",
|
||||
"increment_count": "다운로드 횟수 증가",
|
||||
"installing_plugin": "플러그인 설치 중",
|
||||
"open_zip": "압축 파일 여는 중",
|
||||
"parse_zip": "압축 파일 파싱 중",
|
||||
"start": "초기화 중",
|
||||
"uninstalling_previous": "이전 복사본 설치 제거 중"
|
||||
},
|
||||
"store_contrib": {
|
||||
"desc": "Decky 플러그인 스토어에 기여하고 싶다면 SteamDeckHomebrew/decky-plugin-template Github 저장소를 확인하세요. 개발 및 배포에 대한 정보는 README에서 확인할 수 있습니다.",
|
||||
"label": "기여하기"
|
||||
},
|
||||
"store_filter": {
|
||||
"label": "필터",
|
||||
"label_def": "모두"
|
||||
},
|
||||
"store_search": {
|
||||
"label": "검색"
|
||||
},
|
||||
"store_sort": {
|
||||
"label": "정렬",
|
||||
"label_def": "최근 업데이트 순"
|
||||
},
|
||||
"store_source": {
|
||||
"desc": "모든 플러그인 소스 코드는 SteamDeckHomebrew/decky-plugin-database Github 저장소에서 확인할 수 있습니다.",
|
||||
"label": "소스 코드"
|
||||
},
|
||||
"store_tabs": {
|
||||
"about": "정보",
|
||||
"alph_asce": "알파벳순 (Z-A)",
|
||||
"alph_desc": "알파벳순 (A-Z)",
|
||||
"date_asce": "오래된 순",
|
||||
"date_desc": "최신 순",
|
||||
"downloads_asce": "다운로드 수 낮은 순",
|
||||
"downloads_desc": "다운로드 많은 순",
|
||||
"title": "검색"
|
||||
},
|
||||
"store_testing_cta": "새로운 플러그인을 테스트하여 Decky Loader 팀을 도와주세요!",
|
||||
"store_testing_warning": {
|
||||
"desc": "이 스토어 채널을 사용하여 가장 최신 버전의 플러그인을 테스트할 수 있습니다. GitHub에 피드백을 남겨서 모든 사용자가 업데이트 할 수 있게 해주세요.",
|
||||
"label": "테스트 스토어 채널에 오신 것을 환영합니다"
|
||||
}
|
||||
},
|
||||
"StoreSelect": {
|
||||
"custom_store": {
|
||||
"label": "사용자 지정 스토어",
|
||||
"url_label": "URL"
|
||||
},
|
||||
"store_channel": {
|
||||
"custom": "사용자 지정",
|
||||
"default": "기본",
|
||||
"label": "스토어 배포 채널",
|
||||
"testing": "테스트"
|
||||
}
|
||||
},
|
||||
"Testing": {
|
||||
"download": "다운로드",
|
||||
"error": "PR 설치 오류",
|
||||
"header": "다음 버전의 Decky Loader는 서드 파티 PR를 사용하여 빌드 되엇습니다. Decky Loader 팀은 해당 버전의 작동 여부와 안전성을 확인하지 않았으며 최신 버전이 아닐 수 도 있습니다.",
|
||||
"loading": "열린 PR 로딩 중...",
|
||||
"start_download_toast": "PR #{{id}} 다운로드 중"
|
||||
},
|
||||
"TitleView": {
|
||||
"decky_store_desc": "Decky 스토어 열기",
|
||||
"settings_desc": "Decky 설정 열기"
|
||||
},
|
||||
"Updater": {
|
||||
"decky_updates": "Decky 업데이트",
|
||||
"no_patch_notes_desc": "이 버전에는 패치 노트가 없습니다",
|
||||
"patch_notes_desc": "패치 노트",
|
||||
"updates": {
|
||||
"check_button": "업데이트 확인",
|
||||
"checking": "확인 중",
|
||||
"cur_version": "현재 버전: {{ver}}",
|
||||
"install_button": "업데이트 설치",
|
||||
"label": "업데이트",
|
||||
"lat_version": "최신 상태: {{ver}} 실행 중",
|
||||
"reloading": "다시 로드 중",
|
||||
"updating": "업데이트 중"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
{
|
||||
"BranchSelect": {
|
||||
"update_channel": {
|
||||
"label": "Updatekanaal",
|
||||
"prerelease": "Vooruitgave",
|
||||
"stable": "Stabiel",
|
||||
"testing": "Testen"
|
||||
}
|
||||
},
|
||||
"Developer": {
|
||||
"5secreload": "Bezig met herstarten in 5 seconden",
|
||||
"disabling": "Bezig met uitschakelen van React DevTools",
|
||||
"enabling": "Bezig met inschakelen van React DevTools"
|
||||
},
|
||||
"DropdownMultiselect": {
|
||||
"button": {
|
||||
"back": "Terug"
|
||||
}
|
||||
},
|
||||
"FilePickerError": {
|
||||
"errors": {
|
||||
"file_not_found": "Het opgegeven pad is niet geldig. Controleer het en voer het opnieuw correct in.",
|
||||
"perm_denied": "U heeft geen toegang tot de opgegeven map. Controleer of uw gebruiker (deck op Steam Deck) de juiste permissies heeft om toegang te krijgen tot de opgegeven map/het opgegeven bestand.",
|
||||
"unknown": "Er is een onbekende fout opgetreden. De foutmelding is: {{raw_error}}"
|
||||
}
|
||||
},
|
||||
"FilePickerIndex": {
|
||||
"file": {
|
||||
"select": "Dit bestand selecteren"
|
||||
},
|
||||
"files": {
|
||||
"all_files": "Alle bestanden",
|
||||
"file_type": "Bestandstype",
|
||||
"show_hidden": "Verborgen bestanden tonen"
|
||||
},
|
||||
"filter": {
|
||||
"created_asce": "Aanmaakdatum (oudste)",
|
||||
"created_desc": "Aanmaakdatum (nieuwste)",
|
||||
"modified_asce": "Gewijzigd op (oudste)",
|
||||
"modified_desc": "Gewijzigd op (nieuwste)",
|
||||
"name_asce": "Naam (Z-A)",
|
||||
"name_desc": "Naam (A-Z)",
|
||||
"size_asce": "Grootte (kleinste)",
|
||||
"size_desc": "Grootte (grootste)"
|
||||
},
|
||||
"folder": {
|
||||
"label": "Map",
|
||||
"select": "Deze map gebruiken",
|
||||
"show_more": "Meer bestanden tonen"
|
||||
}
|
||||
},
|
||||
"MultiplePluginsInstallModal": {
|
||||
"confirm": "Weet je zeker dat je de volgende wijzigingen wilt aanbrengen?",
|
||||
"description": {
|
||||
"install": "Installeer {{name}} {{version}}",
|
||||
"reinstall": "Installeer {{name}} {{version}} opnieuw",
|
||||
"update": "Werk {{name}} bij naar {{version}}"
|
||||
},
|
||||
"ok_button": {
|
||||
"idle": "Bevestigen",
|
||||
"loading": "Bezig"
|
||||
},
|
||||
"title": {
|
||||
"install_one": "Installeer 1 plug-in",
|
||||
"install_other": "Installeer {{count}} plug-ins",
|
||||
"mixed_one": "Wijzig 1 plug-in",
|
||||
"mixed_other": "Wijzig {{count}} plug-ins",
|
||||
"reinstall_one": "Installeer 1 plug-in opnieuw",
|
||||
"reinstall_other": "Installeer {{count}} plug-ins opnieuw",
|
||||
"update_one": "Werk 1 plug-in bij",
|
||||
"update_other": "Werk {{count}} plug-ins bij"
|
||||
}
|
||||
},
|
||||
"PluginCard": {
|
||||
"plugin_full_access": "Deze plug-in heeft volledige toegang tot uw Steam Deck.",
|
||||
"plugin_install": "Installeren",
|
||||
"plugin_no_desc": "Geen beschrijving gegeven.",
|
||||
"plugin_version_label": "Plug-inversie"
|
||||
},
|
||||
"PluginInstallModal": {
|
||||
"install": {
|
||||
"button_idle": "Installeren",
|
||||
"button_processing": "Bezig met installeren",
|
||||
"desc": "Weet je zeker dat je {{artifact}} {{version}} wilt installeren?",
|
||||
"title": "Installeer {{artifact}}"
|
||||
},
|
||||
"no_hash": "Deze plug-in heeft geen hash, je installeert deze op eigen risico.",
|
||||
"reinstall": {
|
||||
"button_idle": "Opnieuw installeren",
|
||||
"button_processing": "Bezig met opnieuw te installeren",
|
||||
"desc": "Weet je zeker dat je {{artifact}} {{version}} opnieuw wilt installeren?",
|
||||
"title": "Installeer {{artifact}} opnieuw"
|
||||
},
|
||||
"update": {
|
||||
"button_idle": "Bijwerken",
|
||||
"button_processing": "Bezig met bijwerken",
|
||||
"desc": "Weet je zeker dat je {{artifact}} {{version}} wilt bijwerken?",
|
||||
"title": "{{artifact}} bijwerken"
|
||||
}
|
||||
},
|
||||
"PluginListIndex": {
|
||||
"freeze": "Updates bevriezen",
|
||||
"hide": "Verberg in snelle toegang",
|
||||
"no_plugin": "Geen plug-ins geïnstalleerd!",
|
||||
"plugin_actions": "Plug-inacties",
|
||||
"reinstall": "Opnieuw installeren",
|
||||
"reload": "Herstarten",
|
||||
"show": "Toon in snelle toegang",
|
||||
"unfreeze": "Updates toestaan",
|
||||
"uninstall": "Verwijderen",
|
||||
"update_all_one": "Werk 1 plug-in bij",
|
||||
"update_all_other": "Werk {{count}} plug-ins bij",
|
||||
"update_to": "Bijwerken naar {{name}}"
|
||||
},
|
||||
"PluginListLabel": {
|
||||
"hidden": "Verborgen in snelle toegang"
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
"decky_update_available": "Update naar {{tag_name}} beschikbaar!",
|
||||
"error": "Fout",
|
||||
"plugin_error_uninstall": "Het laden van {{name}} veroorzaakte een fout zoals hierboven weergegeven. Dit betekent meestal dat de plug-in moet worden bijgewerkt voor de nieuwe versie van SteamUI. Controleer of er een update aanwezig is of evalueer de verwijdering ervan in de Decky-instellingen, in het gedeelte Plug-ins.",
|
||||
"plugin_load_error": {
|
||||
"message": "Fout bij het laden van plug-in {{name}}",
|
||||
"toast": "Fout bij het laden van {{name}}"
|
||||
},
|
||||
"plugin_uninstall": {
|
||||
"button": "Verwijderen",
|
||||
"desc": "Weet je zeker dat je {{name}} wilt verwijderen?",
|
||||
"title": "Verwijder {{name}}"
|
||||
},
|
||||
"plugin_update_one": "Updates beschikbaar voor 1 plug-in!",
|
||||
"plugin_update_other": "Updates beschikbaar voor {{count}} plug-ins!"
|
||||
},
|
||||
"PluginView": {
|
||||
"hidden_one": "1 plug-in is verborgen in deze lijst",
|
||||
"hidden_other": "{{count}} plug-ins zijn verborgen in deze lijst"
|
||||
},
|
||||
"RemoteDebugging": {
|
||||
"remote_cef": {
|
||||
"desc": "Sta ongeauthenticeerde toegang tot de CEF-debugger toe aan iedereen in uw netwerk",
|
||||
"label": "Externe CEF-debugging toestaan"
|
||||
}
|
||||
},
|
||||
"SettingsDeveloperIndex": {
|
||||
"cef_console": {
|
||||
"button": "Console openen",
|
||||
"desc": "Opent de CEF-console. Alleen nuttig voor foutopsporingsdoeleinden. Dingen hier zijn potentieel gevaarlijk en mogen alleen worden gebruikt als je een ontwikkelaar van plug-ins bent, of hier door een ontwikkelaar naartoe wordt geleid.",
|
||||
"label": "CEF-console"
|
||||
},
|
||||
"header": "Overige",
|
||||
"react_devtools": {
|
||||
"desc": "Maakt verbinding met een computer met React DevTools mogelijk. Als je deze instelling wijzigt, wordt Steam opnieuw geladen. Stel het IP-adres in voordat je het inschakelt.",
|
||||
"ip_label": "IP-adres",
|
||||
"label": "React DevTools inschakelen"
|
||||
},
|
||||
"third_party_plugins": {
|
||||
"button_install": "Installeren",
|
||||
"button_zip": "Bladeren",
|
||||
"header": "Plug-ins van derden",
|
||||
"label_desc": "URL",
|
||||
"label_url": "Installeer plug-in via een URL",
|
||||
"label_zip": "Installeer plug-in via een ZIP-bestand"
|
||||
},
|
||||
"valve_internal": {
|
||||
"desc1": "Schakelt het interne ontwikkelaarsmenu van Valve in.",
|
||||
"desc2": "Pas niets in dit menu aan, tenzij je weet wat het doet.",
|
||||
"label": "Valve Internal inschakelen"
|
||||
}
|
||||
},
|
||||
"SettingsGeneralIndex": {
|
||||
"about": {
|
||||
"decky_version": "Decky-versie",
|
||||
"header": "Over"
|
||||
},
|
||||
"beta": {
|
||||
"header": "Beta-deelname"
|
||||
},
|
||||
"developer_mode": {
|
||||
"label": "Ontwikkelaarsmodus"
|
||||
},
|
||||
"notifications": {
|
||||
"decky_updates_label": "Wanneer er een Decky-update beschikbaar is",
|
||||
"header": "Meldingen",
|
||||
"plugin_updates_label": "Wanneer er plug-in-updates beschikbaar zijn"
|
||||
},
|
||||
"other": {
|
||||
"header": "Overige"
|
||||
},
|
||||
"updates": {
|
||||
"header": "Bijwerkingen"
|
||||
}
|
||||
},
|
||||
"SettingsIndex": {
|
||||
"developer_title": "Ontwikkelaar",
|
||||
"general_title": "Algemeen",
|
||||
"plugins_title": "Plug-ins",
|
||||
"testing_title": "Testen"
|
||||
},
|
||||
"Store": {
|
||||
"download_progress_info": {
|
||||
"download_zip": "Plugin downloaden",
|
||||
"increment_count": "Aantal downloads verhogen",
|
||||
"installing_plugin": "Plugin installeren",
|
||||
"open_zip": "Zip-bestand openen",
|
||||
"parse_zip": "Zip-bestand parseren",
|
||||
"start": "Initialiseren",
|
||||
"uninstalling_previous": "Vorige kopie verwijderen"
|
||||
},
|
||||
"store_contrib": {
|
||||
"desc": "Als je wilt bijdragen aan de Decky Plugin Store, kijk dan in de SteamDeckHomebrew/decky-plugin-template repository op GitHub. Informatie over ontwikkeling en distributie is beschikbaar in de README.",
|
||||
"label": "Bijdragen"
|
||||
},
|
||||
"store_filter": {
|
||||
"label": "Filter",
|
||||
"label_def": "Alles"
|
||||
},
|
||||
"store_search": {
|
||||
"label": "Zoeken"
|
||||
},
|
||||
"store_sort": {
|
||||
"label": "Sorteren",
|
||||
"label_def": "Laatst bijgewerkt (nieuwste)"
|
||||
},
|
||||
"store_source": {
|
||||
"desc": "Alle broncode van de plug-in is beschikbaar in de SteamDeckHomebrew/decky-plugin-database-repository op GitHub.",
|
||||
"label": "Broncode"
|
||||
},
|
||||
"store_tabs": {
|
||||
"about": "Over",
|
||||
"alph_asce": "Alfabetisch (Z naar A)",
|
||||
"alph_desc": "Alfabetisch (A naar Z)",
|
||||
"date_asce": "Oudste eerst",
|
||||
"date_desc": "Nieuwste eerst",
|
||||
"downloads_asce": "Minste gedownload eerst",
|
||||
"downloads_desc": "Meeste gedownload eerst",
|
||||
"title": "Bladeren"
|
||||
},
|
||||
"store_testing_cta": "Overweeg om nieuwe plug-ins te testen om het Decky Loader-team te helpen!",
|
||||
"store_testing_warning": {
|
||||
"desc": "Je kunt dit winkelkanaal gebruiken om nog in ontwikkeling zijnde plug-inversies te testen. Zorg ervoor dat je feedback geeft op GitHub, zodat de plug-in voor alle gebruikers kan worden bijgewerkt.",
|
||||
"label": "Welkom bij het Testing-winkelkanaal"
|
||||
}
|
||||
},
|
||||
"StoreSelect": {
|
||||
"custom_store": {
|
||||
"label": "Aangepaste winkel",
|
||||
"url_label": "URL"
|
||||
},
|
||||
"store_channel": {
|
||||
"custom": "Aangepast",
|
||||
"default": "Standaard",
|
||||
"label": "Winkelkanaal",
|
||||
"testing": "Testen"
|
||||
}
|
||||
},
|
||||
"Testing": {
|
||||
"download": "Downloaden",
|
||||
"error": "Fout bij installatie van PR",
|
||||
"header": "De volgende versies van Decky Loader zijn gebouwd op basis van open Pull Requests van derden. Het Decky Loader-team heeft hun functionaliteit of veiligheid niet gecontroleerd en ze kunnen verouderd zijn.",
|
||||
"loading": "Openstaande Pull Requests laden...",
|
||||
"start_download_toast": "PR #{{id}} downloaden"
|
||||
},
|
||||
"TitleView": {
|
||||
"decky_store_desc": "Decky Store openen",
|
||||
"settings_desc": "Decky-instellingen openen"
|
||||
},
|
||||
"Updater": {
|
||||
"decky_updates": "Decky-updates",
|
||||
"no_patch_notes_desc": "geen patch-opmerkingen voor deze versie",
|
||||
"patch_notes_desc": "Patch-opmerkingen",
|
||||
"updates": {
|
||||
"check_button": "Op updates controleren",
|
||||
"checking": "Bezig met controleren op updates",
|
||||
"cur_version": "Huidige versie: {{ver}}",
|
||||
"install_button": "Bijwerken",
|
||||
"label": "Bijwerkingen",
|
||||
"lat_version": "Bijwerkt: versie {{ver}}",
|
||||
"reloading": "Bezig met herstarten",
|
||||
"updating": "Bezig met bijwerken"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
{
|
||||
"BranchSelect": {
|
||||
"update_channel": {
|
||||
"label": "Kanał aktualizacji",
|
||||
"prerelease": "Przedpremierowy",
|
||||
"stable": "Stabilny",
|
||||
"testing": "Testowy"
|
||||
}
|
||||
},
|
||||
"Developer": {
|
||||
"5secreload": "Ponowne załadowanie za 5 sekund",
|
||||
"disabling": "Wyłączanie React DevTools",
|
||||
"enabling": "Włączanie React DevTools"
|
||||
},
|
||||
"DropdownMultiselect": {
|
||||
"button": {
|
||||
"back": "Powrót"
|
||||
}
|
||||
},
|
||||
"FilePickerError": {
|
||||
"errors": {
|
||||
"file_not_found": "Podana ścieżka jest nieprawidłowa. Sprawdź ją i wprowadź ponownie poprawnie.",
|
||||
"perm_denied": "Nie masz dostępu do podanego katalogu. Sprawdź, czy twój użytkownik (deck na Steam Deck) ma odpowiednie uprawnienia dostępu do określonego katalogu/pliku.",
|
||||
"unknown": "Wystąpił nieznany błąd. Surowy błąd to {{raw_error}}"
|
||||
}
|
||||
},
|
||||
"FilePickerIndex": {
|
||||
"file": {
|
||||
"select": "Zaznacz ten plik"
|
||||
},
|
||||
"files": {
|
||||
"all_files": "Wszystkie pliki",
|
||||
"file_type": "Typ pliku",
|
||||
"show_hidden": "Pokaż ukryte pliki"
|
||||
},
|
||||
"filter": {
|
||||
"created_asce": "Utworzono (najstarszy)",
|
||||
"created_desc": "Utworzono (najnowszy)",
|
||||
"modified_asce": "Zmodyfikowany (najstarszy)",
|
||||
"modified_desc": "Zmodyfikowany (najnowszy)",
|
||||
"name_asce": "Z-A",
|
||||
"name_desc": "A-Z",
|
||||
"size_asce": "Rozmiar (najmniejszy)",
|
||||
"size_desc": "Rozmiar (największy)"
|
||||
},
|
||||
"folder": {
|
||||
"label": "Katalog",
|
||||
"select": "Użyj tego katalogu",
|
||||
"show_more": "Pokaż więcej plików"
|
||||
}
|
||||
},
|
||||
"MultiplePluginsInstallModal": {
|
||||
"confirm": "Czy na pewno chcesz wprowadzić następujące modyfikacje?",
|
||||
"description": {
|
||||
"install": "Zainstaluj {{name}} {{version}}",
|
||||
"reinstall": "Reinstaluj {{name}} {{version}}",
|
||||
"update": "Zaktualizuj {{name}} do {{version}}"
|
||||
},
|
||||
"ok_button": {
|
||||
"idle": "Potwierdź",
|
||||
"loading": "W toku"
|
||||
},
|
||||
"title": {
|
||||
"install_few": "Zainstaluj {{count}} pluginy",
|
||||
"install_many": "Zainstaluj {{count}} pluginów",
|
||||
"install_one": "Zainstaluj 1 plugin",
|
||||
"mixed_few": "Zmodyfikuj {{count}} pluginy",
|
||||
"mixed_many": "Zmodyfikuj {{count}} pluginów",
|
||||
"mixed_one": "Zmodyfikuj {{count}} plugin",
|
||||
"reinstall_few": "Reinstaluj {{count}} pluginy",
|
||||
"reinstall_many": "Reinstaluj {{count}} pluginów",
|
||||
"reinstall_one": "Reinstaluj 1 plugin",
|
||||
"update_few": "Zaktualizuj {{count}} pluginy",
|
||||
"update_many": "Zaktualizuj {{count}} pluginów",
|
||||
"update_one": "Zaktualizuj 1 plugin"
|
||||
}
|
||||
},
|
||||
"PluginCard": {
|
||||
"plugin_full_access": "Ten plugin ma pełny dostęp do twojego Steam Decka.",
|
||||
"plugin_install": "Zainstaluj",
|
||||
"plugin_no_desc": "Brak opisu.",
|
||||
"plugin_version_label": "Wersja pluginu"
|
||||
},
|
||||
"PluginInstallModal": {
|
||||
"install": {
|
||||
"button_idle": "Zainstaluj",
|
||||
"button_processing": "Instalowanie",
|
||||
"desc": "Czy na pewno chcesz zainstalować {{artifact}} {{version}}?",
|
||||
"title": "Zainstaluj {{artifact}}"
|
||||
},
|
||||
"no_hash": "Ten plugin nie ma hasha, instalujesz go na własne ryzyko.",
|
||||
"reinstall": {
|
||||
"button_idle": "Reinstaluj",
|
||||
"button_processing": "Reinstalowanie",
|
||||
"desc": "Czy na pewno chcesz ponownie zainstalować {{artifact}} {{version}}?",
|
||||
"title": "Reinstaluj {{artifact}}"
|
||||
},
|
||||
"update": {
|
||||
"button_idle": "Aktualizuj",
|
||||
"button_processing": "Aktualizowanie",
|
||||
"desc": "Czy na pewno chcesz zaktualizować {{artifact}} do wersji {{version}}?",
|
||||
"title": "Zaktualizuj {{artifact}}"
|
||||
}
|
||||
},
|
||||
"PluginListIndex": {
|
||||
"freeze": "Zablokuj aktualizacje",
|
||||
"hide": "Szybki dostęp: Ukryj",
|
||||
"no_plugin": "Brak zainstalowanych pluginów!",
|
||||
"plugin_actions": "Akcje pluginów",
|
||||
"reinstall": "Reinstalacja",
|
||||
"reload": "Załaduj ponownie",
|
||||
"show": "Szybki dostęp: Pokaż",
|
||||
"unfreeze": "Odblokuj aktualizacje",
|
||||
"uninstall": "Odinstaluj",
|
||||
"update_all_few": "Zaktualizuj {{count}} pluginy",
|
||||
"update_all_many": "Zaktualizuj {{count}} pluginów",
|
||||
"update_all_one": "Zaktualizuj 1 plugin",
|
||||
"update_to": "Zaktualizuj do {{name}}"
|
||||
},
|
||||
"PluginListLabel": {
|
||||
"hidden": "Ukryty w menu szybkiego dostępu"
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
"decky_update_available": "Aktualizacja do {{tag_name}} jest dostępna!",
|
||||
"error": "Błąd",
|
||||
"plugin_error_uninstall": "Ładowanie {{name}} spowodowało wyjątek, jak pokazano powyżej. Zwykle oznacza to, że plugin wymaga aktualizacji do nowej wersji SteamUI. Sprawdź, czy aktualizacja jest obecna lub rozważ usunięcie go w ustawieniach Decky, w sekcji Pluginy.",
|
||||
"plugin_load_error": {
|
||||
"message": "Błąd ładowania pluginu {{name}}",
|
||||
"toast": "Błąd ładowania {{name}}"
|
||||
},
|
||||
"plugin_uninstall": {
|
||||
"button": "Odinstaluj",
|
||||
"desc": "Czy na pewno chcesz odinstalować {{name}}?",
|
||||
"title": "Odinstaluj {{name}}"
|
||||
},
|
||||
"plugin_update_few": "Aktualizacje dostępne dla {{count}} pluginów!",
|
||||
"plugin_update_many": "Aktualizacje dostępne dla {{count}} pluginów!",
|
||||
"plugin_update_one": "Aktualizacje dostępne dla 1 pluginu!"
|
||||
},
|
||||
"PluginView": {
|
||||
"hidden_few": "{{count}} pluginy jest ukryty na tej liście",
|
||||
"hidden_many": "{{count}} pluginów jest ukryty na tej liście",
|
||||
"hidden_one": "1 plugin jest ukryty na tej liście"
|
||||
},
|
||||
"RemoteDebugging": {
|
||||
"remote_cef": {
|
||||
"desc": "Zezwalaj na nieuwierzytelniony dostęp do debugera CEF wszystkim osobom w Twojej sieci",
|
||||
"label": "Zezwól na zdalne debugowanie CEF"
|
||||
}
|
||||
},
|
||||
"SettingsDeveloperIndex": {
|
||||
"cef_console": {
|
||||
"button": "Otwórz konsolę",
|
||||
"desc": "Otwiera konsolę CEF. Przydatne tylko do celów debugowania. Rzeczy tutaj są potencjalnie niebezpieczne i powinny być używane tylko wtedy, gdy jesteś twórcą wtyczek lub zostałeś tu przez kogoś skierowany.",
|
||||
"label": "Konsola CEF"
|
||||
},
|
||||
"header": "Inne",
|
||||
"react_devtools": {
|
||||
"desc": "Umożliwia połączenie z komputerem z uruchomionym React DevTools. Zmiana tego ustawienia spowoduje ponowne załadowanie Steam. Ustaw adres IP przed włączeniem.",
|
||||
"ip_label": "IP",
|
||||
"label": "Włącz React DevTools"
|
||||
},
|
||||
"third_party_plugins": {
|
||||
"button_install": "Zainstaluj",
|
||||
"button_zip": "Przeglądaj",
|
||||
"header": "Pluginy zewnętrzne",
|
||||
"label_desc": "URL",
|
||||
"label_url": "Zainstaluj plugin z adresu URL",
|
||||
"label_zip": "Zainstaluj plugin z pliku ZIP"
|
||||
},
|
||||
"valve_internal": {
|
||||
"desc1": "Włącza wewnętrzne menu programisty Valve.",
|
||||
"desc2": "Nie dotykaj niczego w tym menu, chyba że wiesz, co robi.",
|
||||
"label": "Włącz Valve Internal"
|
||||
}
|
||||
},
|
||||
"SettingsGeneralIndex": {
|
||||
"about": {
|
||||
"decky_version": "Wersja Decky",
|
||||
"header": "Informacje"
|
||||
},
|
||||
"beta": {
|
||||
"header": "Udział w becie"
|
||||
},
|
||||
"developer_mode": {
|
||||
"label": "Tryb dewelopera"
|
||||
},
|
||||
"notifications": {
|
||||
"decky_updates_label": "Dostępna aktualizacja Decky",
|
||||
"header": "Powiadomienia",
|
||||
"plugin_updates_label": "Dostępne aktualizacje pluginów"
|
||||
},
|
||||
"other": {
|
||||
"header": "Inne"
|
||||
},
|
||||
"updates": {
|
||||
"header": "Aktualizacje"
|
||||
}
|
||||
},
|
||||
"SettingsIndex": {
|
||||
"developer_title": "Deweloper",
|
||||
"general_title": "Ogólne",
|
||||
"plugins_title": "Pluginy",
|
||||
"testing_title": "Testowanie"
|
||||
},
|
||||
"Store": {
|
||||
"download_progress_info": {
|
||||
"download_zip": "Pobieranie pluginu",
|
||||
"increment_count": "Zwiększanie liczby pobrań",
|
||||
"installing_plugin": "Instalowanie pluginu",
|
||||
"open_zip": "Otwieranie pliku zip",
|
||||
"parse_zip": "Analizowanie pliku zip",
|
||||
"start": "Inicjalizacja",
|
||||
"uninstalling_previous": "Odinstalowywanie poprzednich kopii"
|
||||
},
|
||||
"store_contrib": {
|
||||
"desc": "Jeśli chcesz przyczynić się do rozwoju Decky Plugin Store, sprawdź repozytorium SteamDeckHomebrew/decky-plugin-template na GitHub. Informacje na temat rozwoju i dystrybucji są dostępne w pliku README.",
|
||||
"label": "Współtworzenie"
|
||||
},
|
||||
"store_filter": {
|
||||
"label": "Filtr",
|
||||
"label_def": "Wszystko"
|
||||
},
|
||||
"store_search": {
|
||||
"label": "Szukaj"
|
||||
},
|
||||
"store_sort": {
|
||||
"label": "Sortowanie",
|
||||
"label_def": "Ostatnia aktualizacja (najnowsza)"
|
||||
},
|
||||
"store_source": {
|
||||
"desc": "Cały kod źródłowy pluginów jest dostępny w repozytorium SteamDeckHomebrew/decky-plugin-database na GitHub.",
|
||||
"label": "Kod źródłowy"
|
||||
},
|
||||
"store_tabs": {
|
||||
"about": "Informacje",
|
||||
"alph_asce": "Alfabetycznie (od Z do A)",
|
||||
"alph_desc": "Alfabetycznie (od A do Z)",
|
||||
"date_asce": "Od najstarszych",
|
||||
"date_desc": "Od najnowszych",
|
||||
"downloads_asce": "Najmniej pobrań",
|
||||
"downloads_desc": "Najwięcej pobrań",
|
||||
"title": "Przeglądaj"
|
||||
},
|
||||
"store_testing_cta": "Rozważ przetestowanie nowych pluginów, aby pomóc zespołowi Decky Loader!",
|
||||
"store_testing_warning": {
|
||||
"desc": "Możesz użyć tego kanału sklepu do testowania najnowszych wersji pluginów. Pamiętaj, aby zostawić opinię na GitHub, aby plugin mógł zostać zaktualizowany dla wszystkich użytkowników.",
|
||||
"label": "Witamy w Testowym Kanale Sklepu"
|
||||
}
|
||||
},
|
||||
"StoreSelect": {
|
||||
"custom_store": {
|
||||
"label": "Niestandardowy sklep",
|
||||
"url_label": "URL"
|
||||
},
|
||||
"store_channel": {
|
||||
"custom": "Niestandardowy",
|
||||
"default": "Domyślny",
|
||||
"label": "Kanał sklepu",
|
||||
"testing": "Testowy"
|
||||
}
|
||||
},
|
||||
"Testing": {
|
||||
"download": "Pobierz",
|
||||
"error": "Błąd instalowania PR",
|
||||
"header": "Następujące wersje Decky Loader są zrobione z open third-party Pull Requests. Zespół Decky Loader nie zweryfikował ich działania czy bezpieczeństwa, mogą też być nie aktualne.",
|
||||
"loading": "Ładowanie open Pull Requests...",
|
||||
"start_download_toast": "Pobieranie PR #{{id}}"
|
||||
},
|
||||
"TitleView": {
|
||||
"decky_store_desc": "Otwórz sklep Decky",
|
||||
"settings_desc": "Otwórz ustawienia Decky"
|
||||
},
|
||||
"Updater": {
|
||||
"decky_updates": "Aktualizacje Decky",
|
||||
"no_patch_notes_desc": "Brak informacji o poprawkach dla tej wersji",
|
||||
"patch_notes_desc": "Opis zmian",
|
||||
"updates": {
|
||||
"check_button": "Sprawdź aktualizacje",
|
||||
"checking": "Sprawdzanie",
|
||||
"cur_version": "Aktualna wersja: {{ver}}",
|
||||
"install_button": "Zainstaluj aktualizację",
|
||||
"label": "Aktualizacje",
|
||||
"lat_version": "Aktualizacje zainstalowane. Aktualna wersja: {{ver}}",
|
||||
"reloading": "Ponowne ładowanie",
|
||||
"updating": "Aktualizowanie"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
{
|
||||
"BranchSelect": {
|
||||
"update_channel": {
|
||||
"label": "Canal de Atualização",
|
||||
"prerelease": "Pré-lançamento",
|
||||
"stable": "Estável",
|
||||
"testing": "Em Teste"
|
||||
}
|
||||
},
|
||||
"Developer": {
|
||||
"5secreload": "Recarregando em 5 segundos",
|
||||
"disabling": "Desabilitando React DevTools",
|
||||
"enabling": "Habilitando React DevTools"
|
||||
},
|
||||
"DropdownMultiselect": {
|
||||
"button": {
|
||||
"back": "Voltar"
|
||||
}
|
||||
},
|
||||
"FilePickerError": {
|
||||
"errors": {
|
||||
"file_not_found": "O caminho especificado não é válido. Por favor, confira e reinsira corretamente.",
|
||||
"perm_denied": "Você não tem acesso à este diretório. Por favor, verifiquei se seu usuário (deck no Steam Deck) tem as permissões necessárias para acessar este arquivo/pasta.",
|
||||
"unknown": "Ocorreu um erro desconhecido. O erro completo é: {{raw_error}}"
|
||||
}
|
||||
},
|
||||
"FilePickerIndex": {
|
||||
"file": {
|
||||
"select": "Selecione este arquivo"
|
||||
},
|
||||
"files": {
|
||||
"all_files": "Todos os arquivos",
|
||||
"file_type": "Formato de arquivo",
|
||||
"show_hidden": "Mostrar Arquivos Ocultos"
|
||||
},
|
||||
"filter": {
|
||||
"created_asce": "Criado (Mais antigo)",
|
||||
"created_desc": "Criado (Mais recente)",
|
||||
"modified_asce": "Alterado (Mais antigo)",
|
||||
"modified_desc": "Alterado (Mais recente)",
|
||||
"name_asce": "Z-A",
|
||||
"name_desc": "A-Z",
|
||||
"size_asce": "Tamanho (Menor)",
|
||||
"size_desc": "Tamanho (Maior)"
|
||||
},
|
||||
"folder": {
|
||||
"label": "Pasta",
|
||||
"select": "Use esta pasta",
|
||||
"show_more": "Mostrar mais arquivos"
|
||||
}
|
||||
},
|
||||
"MultiplePluginsInstallModal": {
|
||||
"confirm": "Tem certeza que deseja fazer as seguintes modificações?",
|
||||
"description": {
|
||||
"install": "Instalar {{name}} {{version}}",
|
||||
"reinstall": "Reinstalar {{name}} {{version}}",
|
||||
"update": "Atualizar {{name}} para {{version}}"
|
||||
},
|
||||
"ok_button": {
|
||||
"idle": "Confirmar",
|
||||
"loading": "Carregando"
|
||||
},
|
||||
"title": {
|
||||
"install_many": "Instalar {{count}} plugins",
|
||||
"install_one": "Instalar 1 plugin",
|
||||
"install_other": "Instalar {{count}} plugins",
|
||||
"mixed_many": "Modificar {{count}} plugins",
|
||||
"mixed_one": "Modificar {{count}} plugin",
|
||||
"mixed_other": "Modificar {{count}} plugins",
|
||||
"reinstall_many": "Reinstalar {{count}} plugins",
|
||||
"reinstall_one": "Reinstalar 1 plugin",
|
||||
"reinstall_other": "Reinstalar {{count}} plugins",
|
||||
"update_many": "Atualizar {{count}} plugins",
|
||||
"update_one": "Atualizar 1 plugin",
|
||||
"update_other": "Atualizar {{count}} plugins"
|
||||
}
|
||||
},
|
||||
"PluginCard": {
|
||||
"plugin_full_access": "Este plugin tem acesso total ao seu Steam Deck.",
|
||||
"plugin_install": "Instalar",
|
||||
"plugin_no_desc": "Nenhuma descrição fornecida.",
|
||||
"plugin_version_label": "Versão do plugin"
|
||||
},
|
||||
"PluginInstallModal": {
|
||||
"install": {
|
||||
"button_idle": "Instalar",
|
||||
"button_processing": "Instalando",
|
||||
"desc": "Você tem certeza que deseja instalar {{artifact}} {{version}}?",
|
||||
"title": "Instalar {{artifact}}"
|
||||
},
|
||||
"no_hash": "Este plugin não tem um hash, você o está instalando por sua conta em risco.",
|
||||
"reinstall": {
|
||||
"button_idle": "Reinstalar",
|
||||
"button_processing": "Reinstalando",
|
||||
"desc": "Tem certeza que voce deseja reinstalar {{artifact}} {{version}}?",
|
||||
"title": "Reinstalar {{artifact}}"
|
||||
},
|
||||
"update": {
|
||||
"button_idle": "Atualizar",
|
||||
"button_processing": "Atualizando",
|
||||
"desc": "Tem certeza que voce deseja atualizar {{artifact}} {{version}}?",
|
||||
"title": "Atualizar {{artifact}}"
|
||||
}
|
||||
},
|
||||
"PluginListIndex": {
|
||||
"freeze": "Congelar updates",
|
||||
"hide": "Acesso Rápido: Ocultar",
|
||||
"no_plugin": "Nenhum plugin instalado!",
|
||||
"plugin_actions": "Ações do plugin",
|
||||
"reinstall": "Reinstalar",
|
||||
"reload": "Recarregar",
|
||||
"show": "Acesso Rápido: Mostrar",
|
||||
"unfreeze": "Permitir atualizações",
|
||||
"uninstall": "Desinstalar",
|
||||
"update_all_many": "Atualizar {{count}} plugins",
|
||||
"update_all_one": "Atualizar 1 plugin",
|
||||
"update_all_other": "Atualizar {{count}} plugins",
|
||||
"update_to": "Atualizar para {{name}}"
|
||||
},
|
||||
"PluginListLabel": {
|
||||
"hidden": "Oculto no menu de acesso rápido"
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
"decky_update_available": "Atualização para {{tag_name}} disponível!",
|
||||
"error": "Erro",
|
||||
"plugin_error_uninstall": "Um erro aconteceu ao carregar {{name}}, como mostrado acima. Isso normalmente significa que o plugin precisa de uma atualização para a nova versão do SteamUI. Confira se existe uma atualização ou avalie a remoção do plugin nas configurações do Decky, na sessão de plugins.",
|
||||
"plugin_load_error": {
|
||||
"message": "Erro ao carregar o plugin {{name}}",
|
||||
"toast": "Erro ao carregar {{name}}"
|
||||
},
|
||||
"plugin_uninstall": {
|
||||
"button": "Desinstalar",
|
||||
"desc": "Você tem certeza que deseja desinstalar {{name}}?",
|
||||
"title": "Desinstalar {{name}}"
|
||||
},
|
||||
"plugin_update_many": "Atualizações disponíveis para {{count}} plugins!",
|
||||
"plugin_update_one": "Atualização disponível para 1 plugin!",
|
||||
"plugin_update_other": "Atualizações disponíveis para {{count}} plugins!"
|
||||
},
|
||||
"PluginView": {
|
||||
"hidden_many": "{{count}} plugins estão ocultos nesta lista",
|
||||
"hidden_one": "1 plugin está oculto nesta lista",
|
||||
"hidden_other": "{{count}} plugins estão ocultos nesta lista"
|
||||
},
|
||||
"RemoteDebugging": {
|
||||
"remote_cef": {
|
||||
"desc": "Permitir acesso não autenticato ao depurador CEF para qualquer um na sua rede",
|
||||
"label": "Permitir Depuração CEF Demota"
|
||||
}
|
||||
},
|
||||
"SettingsDeveloperIndex": {
|
||||
"cef_console": {
|
||||
"button": "Abrir o Console",
|
||||
"desc": "Abre o Console CEF. Somente útil para fins de depuração. O material aqui é potencialmente perigoso e só deve ser usado se você for um desenvolvedor de plugin, ou direcionado até aqui por um.",
|
||||
"label": "Console CEF"
|
||||
},
|
||||
"header": "Outros",
|
||||
"react_devtools": {
|
||||
"desc": "Habilita a conexão a um computador executando React DevTools. Alterar essa configuração irá recarregar a Steam. Defina o endereço IP antes de habilitar.",
|
||||
"ip_label": "IP",
|
||||
"label": "Habilitar React DevTools"
|
||||
},
|
||||
"third_party_plugins": {
|
||||
"button_install": "Instalar",
|
||||
"button_zip": "Navegar",
|
||||
"header": "Plugins de terceiros",
|
||||
"label_desc": "URL",
|
||||
"label_url": "Instalar Plugin a partir da URL",
|
||||
"label_zip": "Instalar Plugin a partir de um arquivo ZIP"
|
||||
},
|
||||
"valve_internal": {
|
||||
"desc1": "Habilita o menu interno de desenvolvedor da Valve.",
|
||||
"desc2": "Não toque em nada neste menu, a não ser que você saiba o que está fazendo.",
|
||||
"label": "Habilitar Menu Interno da Valve"
|
||||
}
|
||||
},
|
||||
"SettingsGeneralIndex": {
|
||||
"about": {
|
||||
"decky_version": "Versão do Decky",
|
||||
"header": "Sobre"
|
||||
},
|
||||
"beta": {
|
||||
"header": "Participação no Beta"
|
||||
},
|
||||
"developer_mode": {
|
||||
"label": "Modo Deselvolvedor"
|
||||
},
|
||||
"notifications": {
|
||||
"decky_updates_label": "Atualização do Decky disponível",
|
||||
"header": "Noificações",
|
||||
"plugin_updates_label": "Atualizações de Plugin disponíveis"
|
||||
},
|
||||
"other": {
|
||||
"header": "Outros"
|
||||
},
|
||||
"updates": {
|
||||
"header": "Atualizações"
|
||||
}
|
||||
},
|
||||
"SettingsIndex": {
|
||||
"developer_title": "Desenvolvedor",
|
||||
"general_title": "Geral",
|
||||
"plugins_title": "Plugins",
|
||||
"testing_title": "Testando"
|
||||
},
|
||||
"Store": {
|
||||
"download_progress_info": {
|
||||
"download_zip": "Baixando Plugin",
|
||||
"increment_count": "Aumentando contagem de downloads",
|
||||
"installing_plugin": "Instalando Plugin",
|
||||
"open_zip": "Abrindo arquivo Zip",
|
||||
"parse_zip": "Analizando Arquivo ZIP",
|
||||
"start": "Iniciando",
|
||||
"uninstalling_previous": "Desinstalando Cópias Anteriores"
|
||||
},
|
||||
"store_contrib": {
|
||||
"desc": "Se você deseja contribuir para a Loja de Plugins para o Decky, confira o repositório SteamDeckHomebrew/decky-plugin-template no GitHub. Informações sobre o desenvolvimento e distribuição estão disponíveis no README.",
|
||||
"label": "Contribuindo"
|
||||
},
|
||||
"store_filter": {
|
||||
"label": "Filtros",
|
||||
"label_def": "Todos"
|
||||
},
|
||||
"store_search": {
|
||||
"label": "Buscar"
|
||||
},
|
||||
"store_sort": {
|
||||
"label": "Ordenar",
|
||||
"label_def": "Último atualizado (Mais recente)"
|
||||
},
|
||||
"store_source": {
|
||||
"desc": "Todos os códigos fonte dos plugins estão disponíveis no repositório SteamDeckHomebrew/decky-plugin-database no GitHub.",
|
||||
"label": "Código Fonte"
|
||||
},
|
||||
"store_tabs": {
|
||||
"about": "Sobre",
|
||||
"alph_asce": "Alfabética (Z - A)",
|
||||
"alph_desc": "Alfabética (A - Z)",
|
||||
"date_asce": "Antigos Primeiro",
|
||||
"date_desc": "Novos Primeiro",
|
||||
"downloads_asce": "Menos Baixados Primeiro",
|
||||
"downloads_desc": "Mais Baixados Primeiro",
|
||||
"title": "Navegar"
|
||||
},
|
||||
"store_testing_cta": "Por favor, considere testar os novos plugins para ajudar o time do Decky Loader!",
|
||||
"store_testing_warning": {
|
||||
"desc": "Você pode usar este canal da loja para testar versões avançadas do plugin. Certifique-se de deixar feedback no GitHub para que o plugin possa ser atualizado para todos os usuários.",
|
||||
"label": "Bem-vindo ao Canal de Testes da Loja"
|
||||
}
|
||||
},
|
||||
"StoreSelect": {
|
||||
"custom_store": {
|
||||
"label": "Loja Personalizada",
|
||||
"url_label": "URL"
|
||||
},
|
||||
"store_channel": {
|
||||
"custom": "Personalizada",
|
||||
"default": "Padrão",
|
||||
"label": "Canal da Loja",
|
||||
"testing": "Em Teste"
|
||||
}
|
||||
},
|
||||
"Testing": {
|
||||
"download": "Download",
|
||||
"error": "Erro ao instalar PR",
|
||||
"header": "As seguintes versões do Decky Loader são construídas a partir de Pull Requests de terceiros. A equipe do Decky Loader não verificou sua funcionalidade ou segurança, e elas podem estar desatualizadas.",
|
||||
"loading": "Carregando Pull Requests abertos...",
|
||||
"start_download_toast": "Baixando PR #{{id}}"
|
||||
},
|
||||
"TitleView": {
|
||||
"decky_store_desc": "Abrir Loja Decky",
|
||||
"settings_desc": "Abrir Definições Decky"
|
||||
},
|
||||
"Updater": {
|
||||
"decky_updates": "Atualizações do Decky",
|
||||
"no_patch_notes_desc": "nenhuma nota de alteração para esta versão",
|
||||
"patch_notes_desc": "Notas de alteração",
|
||||
"updates": {
|
||||
"check_button": "Buscar Atualizações",
|
||||
"checking": "Buscando",
|
||||
"cur_version": "Versão atual: {{ver}}",
|
||||
"install_button": "Instalar Atualização",
|
||||
"label": "Atualizações",
|
||||
"lat_version": "Atualizado: rodando {{ver}}",
|
||||
"reloading": "Recarregando",
|
||||
"updating": "Atualizando"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
{
|
||||
"BranchSelect": {
|
||||
"update_channel": {
|
||||
"label": "Canal de atualização",
|
||||
"prerelease": "Pré-lançamento",
|
||||
"stable": "Estável",
|
||||
"testing": "Em testes"
|
||||
}
|
||||
},
|
||||
"Developer": {
|
||||
"5secreload": "A recarregar em 5 segundos",
|
||||
"disabling": "Desativar React DevTools",
|
||||
"enabling": "Ativar React DevTools"
|
||||
},
|
||||
"DropdownMultiselect": {
|
||||
"button": {
|
||||
"back": "Voltar"
|
||||
}
|
||||
},
|
||||
"FilePickerError": {
|
||||
"errors": {
|
||||
"file_not_found": "O caminho especificado não é válido. Por favor, verifica e insere o caminho correto.",
|
||||
"perm_denied": "Não tens acesso ao diretório especificado. Por favor, verifica se o seu utilizador (deck na Steam Deck) tem a permissão correspondente para aceder à pasta/ficheiro especificada(o).",
|
||||
"unknown": "Ocorreu um erro desconhecido. O erro bruto é: {{raw_error}}"
|
||||
}
|
||||
},
|
||||
"FilePickerIndex": {
|
||||
"file": {
|
||||
"select": "Selecionar este ficheiro"
|
||||
},
|
||||
"files": {
|
||||
"all_files": "Todos os ficheiros",
|
||||
"file_type": "Tipo de ficheiro",
|
||||
"show_hidden": "Mostrar ficheiros ocultos"
|
||||
},
|
||||
"filter": {
|
||||
"created_asce": "Criado (mais antigo)",
|
||||
"created_desc": "Criado (mais recente)",
|
||||
"modified_asce": "Modificado (mais antigo)",
|
||||
"modified_desc": "Modificado (mais recente)",
|
||||
"name_asce": "Z-A",
|
||||
"name_desc": "A-Z",
|
||||
"size_asce": "Tamanho (mais pequeno)",
|
||||
"size_desc": "Tamanho (maior)"
|
||||
},
|
||||
"folder": {
|
||||
"label": "Pasta",
|
||||
"select": "Usar esta pasta",
|
||||
"show_more": "Mostrar mais ficheiros"
|
||||
}
|
||||
},
|
||||
"MultiplePluginsInstallModal": {
|
||||
"confirm": "Tens a certeza de que pretendes fazer as seguintes alterações?",
|
||||
"description": {
|
||||
"install": "Instalar {{name}} {{version}}",
|
||||
"reinstall": "Reinstalar {{name}} {{version}}",
|
||||
"update": "Atualizar {{name}} para {{version}}"
|
||||
},
|
||||
"ok_button": {
|
||||
"idle": "Confirmar",
|
||||
"loading": "Em curso"
|
||||
},
|
||||
"title": {
|
||||
"install_many": "Instalar {{count}} plugins",
|
||||
"install_one": "Instalar 1 plugin",
|
||||
"install_other": "Instalar {{count}} plugins",
|
||||
"mixed_many": "Alterar {{count}} plugins",
|
||||
"mixed_one": "Alterar 1 plugin",
|
||||
"mixed_other": "Alterar {{count}} plugins",
|
||||
"reinstall_many": "Reinstalar {{count}} plugins",
|
||||
"reinstall_one": "Reinstalar 1 plugin",
|
||||
"reinstall_other": "Reinstalar {{count}} plugins",
|
||||
"update_many": "Atualizar {{count}} plugins",
|
||||
"update_one": "Atualizar 1 plugin",
|
||||
"update_other": "Atualizar {{count}} plugins"
|
||||
}
|
||||
},
|
||||
"PluginCard": {
|
||||
"plugin_full_access": "Este plugin tem acesso total ao teu Steam Deck.",
|
||||
"plugin_install": "Instalar",
|
||||
"plugin_no_desc": "Sem descrição fornecida.",
|
||||
"plugin_version_label": "Versão do plugin"
|
||||
},
|
||||
"PluginInstallModal": {
|
||||
"install": {
|
||||
"button_idle": "Instalar",
|
||||
"button_processing": "Instalação em curso",
|
||||
"desc": "De certeza que queres instalar {{artifact}} {{version}}?",
|
||||
"title": "Instalar {{artifact}}"
|
||||
},
|
||||
"no_hash": "Este plugin não tem uma hash, estás a instalá-lo por tua conta e risco.",
|
||||
"reinstall": {
|
||||
"button_idle": "Reinstalar",
|
||||
"button_processing": "Reinstalação em curso",
|
||||
"desc": "De certeza que queres reinstalar {{artifact}} {{version}}?",
|
||||
"title": "Reinstalar {{artifact}}"
|
||||
},
|
||||
"update": {
|
||||
"button_idle": "Atualizar",
|
||||
"button_processing": "Atualização em curso",
|
||||
"desc": "De certeza que queres atualizar {{artifact}} {{version}}?",
|
||||
"title": "Atualizar {{artifact}}"
|
||||
}
|
||||
},
|
||||
"PluginListIndex": {
|
||||
"freeze": "Congelar atualizações",
|
||||
"hide": "Acesso rápido: Ocultar",
|
||||
"no_plugin": "Nenhum plugin instalado!",
|
||||
"plugin_actions": "Operações de plugin",
|
||||
"reinstall": "Reinstalar",
|
||||
"reload": "Recarregar",
|
||||
"show": "Acesso rápido: Mostrar",
|
||||
"unfreeze": "Permitir atualizações",
|
||||
"uninstall": "Desinstalar",
|
||||
"update_all_many": "Atualizar {{count}} plugins",
|
||||
"update_all_one": "Atualizar 1 plugin",
|
||||
"update_all_other": "Atualizar {{count}} plugins",
|
||||
"update_to": "Atualizar para {{name}}"
|
||||
},
|
||||
"PluginListLabel": {
|
||||
"hidden": "Oculto do menu de acesso rápido"
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
"decky_update_available": "Está disponível uma nova versão de {{tag_name}} !",
|
||||
"error": "Erro",
|
||||
"plugin_error_uninstall": "Ao carregar {{name}}, ocorreu uma exceção, como pode ser verificado acima. Pode ter sido porque o plugin requer a última versão do SteamUI. Verifica se há uma atualização disponível ou desinstala o plugin nas definições do Decky.",
|
||||
"plugin_load_error": {
|
||||
"message": "Erro ao carregar o plugin {{name}}",
|
||||
"toast": "Erro ao carregar {{name}}"
|
||||
},
|
||||
"plugin_uninstall": {
|
||||
"button": "Desinstalar",
|
||||
"desc": "De certeza que queres desinstalar {{name}}?",
|
||||
"title": "Desinstalar {{name}}"
|
||||
},
|
||||
"plugin_update_many": "{{count}} plugins têm actualizações disponíveis!",
|
||||
"plugin_update_one": "1 plugin tem atualizações disponíveis!",
|
||||
"plugin_update_other": "{{count}} plugins têm actualizações disponíveis!"
|
||||
},
|
||||
"PluginView": {
|
||||
"hidden_many": "{{count}} plugins estão ocultos desta lista",
|
||||
"hidden_one": "1 plugin está oculto desta lista",
|
||||
"hidden_other": "{{count}} plugins estão ocultos desta lista"
|
||||
},
|
||||
"RemoteDebugging": {
|
||||
"remote_cef": {
|
||||
"desc": "Permitir acesso não autenticado ao depurador do CEF a qualquer pessoa na tua rede",
|
||||
"label": "Permitir Depuração Remota do CEF"
|
||||
}
|
||||
},
|
||||
"SettingsDeveloperIndex": {
|
||||
"cef_console": {
|
||||
"button": "Abrir consola",
|
||||
"desc": "Abre a Consola CEF. Apenas útil para fins de depuração. O conteúdo aqui presente pode ser perigoso e só devem ser usadas se fores um desenvolvedor de plugins ou se fores direcionado para aqui por um.",
|
||||
"label": "Consola CEF"
|
||||
},
|
||||
"header": "Outros",
|
||||
"react_devtools": {
|
||||
"desc": "Permite a conexão a um computador a correr o React DevTools. Alterar esta definição irá recarregar o Steam. Define o endereço IP antes de ativar.",
|
||||
"ip_label": "IP",
|
||||
"label": "Ativar React DevTools"
|
||||
},
|
||||
"third_party_plugins": {
|
||||
"button_install": "Instalar",
|
||||
"button_zip": "Navegar",
|
||||
"header": "Plugins de terceiros",
|
||||
"label_desc": "URL",
|
||||
"label_url": "Instalar plugin a partir de um URL",
|
||||
"label_zip": "Instalar plugin a partir de um ficheiro ZIP"
|
||||
},
|
||||
"valve_internal": {
|
||||
"desc1": "Ativa o menu interno de programador da Valve.",
|
||||
"desc2": "Não toques em nada neste menu, a menos que saibas o que faz.",
|
||||
"label": "Cativar menu interno da Valve"
|
||||
}
|
||||
},
|
||||
"SettingsGeneralIndex": {
|
||||
"about": {
|
||||
"decky_version": "Versão do Decky",
|
||||
"header": "Sobre"
|
||||
},
|
||||
"beta": {
|
||||
"header": "Participação na versão Beta"
|
||||
},
|
||||
"developer_mode": {
|
||||
"label": "Modo de programador"
|
||||
},
|
||||
"notifications": {
|
||||
"decky_updates_label": "Atualização Decky disponível",
|
||||
"header": "Notificações",
|
||||
"plugin_updates_label": "Atualizações de plugins disponíveis"
|
||||
},
|
||||
"other": {
|
||||
"header": "Outros"
|
||||
},
|
||||
"updates": {
|
||||
"header": "Atualizações"
|
||||
}
|
||||
},
|
||||
"SettingsIndex": {
|
||||
"developer_title": "Programador",
|
||||
"general_title": "Geral",
|
||||
"plugins_title": "Plugins",
|
||||
"testing_title": "Testes"
|
||||
},
|
||||
"Store": {
|
||||
"download_progress_info": {
|
||||
"download_zip": "A transferir o plugin",
|
||||
"increment_count": "A incrementar a contagem de transferências",
|
||||
"installing_plugin": "A instalar o plugin",
|
||||
"open_zip": "A abrir o ficheiro zip",
|
||||
"parse_zip": "A processar o ficheiro zip",
|
||||
"start": "A inicializar",
|
||||
"uninstalling_previous": "A desinstalar a cópia anterior"
|
||||
},
|
||||
"store_contrib": {
|
||||
"desc": "Se quiseres contribuir para a Loja de Plugins do Decky, consulta o repositório SteamDeckHomebrew/decky-plugin-template no GitHub. Encontras informações sobre desenvolvimento e distribuição no README.",
|
||||
"label": "Contribuir"
|
||||
},
|
||||
"store_filter": {
|
||||
"label": "Filtro",
|
||||
"label_def": "Todos"
|
||||
},
|
||||
"store_search": {
|
||||
"label": "Procurar"
|
||||
},
|
||||
"store_sort": {
|
||||
"label": "Ordenar",
|
||||
"label_def": "Última atualização (mais recente)"
|
||||
},
|
||||
"store_source": {
|
||||
"desc": "Todo o código-fonte dos plugins está disponível no repositório SteamDeckHomebrew/decky-plugin-database no GitHub.",
|
||||
"label": "Código fonte"
|
||||
},
|
||||
"store_tabs": {
|
||||
"about": "Sobre",
|
||||
"alph_asce": "Alfabeticamente (Z-A)",
|
||||
"alph_desc": "Alfabeticamente (A-Z)",
|
||||
"date_asce": "Mais Antigos Primeiro",
|
||||
"date_desc": "Mais Recentes Primeiro",
|
||||
"downloads_asce": "Menos Transferidos Primeiro",
|
||||
"downloads_desc": "Mais Transferidos Primeiro",
|
||||
"title": "Navegar"
|
||||
},
|
||||
"store_testing_cta": "Testa novos plugins e ajuda a equipa do Decky Loader!",
|
||||
"store_testing_warning": {
|
||||
"desc": "Podes utilizar este canal da loja para testar versões de plugins de última geração. Não te esqueças de deixar o teufeedback no GitHub para que o plugin possa ser atualizado para todos os utilizadores.",
|
||||
"label": "Bem-vindo ao Canal de Testes da Loja"
|
||||
}
|
||||
},
|
||||
"StoreSelect": {
|
||||
"custom_store": {
|
||||
"label": "Loja personalizada",
|
||||
"url_label": "URL"
|
||||
},
|
||||
"store_channel": {
|
||||
"custom": "Personalizada",
|
||||
"default": "Padrão",
|
||||
"label": "Canal da loja",
|
||||
"testing": "Em testes"
|
||||
}
|
||||
},
|
||||
"Testing": {
|
||||
"download": "Transferir",
|
||||
"error": "Erro ao instalar PR",
|
||||
"header": "As seguintes versões do Decky Loader estão construidas a partir de Pull Requests de terceiros. A equipa do Decky Loader não verificou as sua funcionalidade ou segurança e as mesmas podem estar desatualizados.",
|
||||
"loading": "A carregar Pull Requests abertos...",
|
||||
"start_download_toast": "A descarregar PR #{{id}}"
|
||||
},
|
||||
"TitleView": {
|
||||
"decky_store_desc": "Abrir a Loja Decky",
|
||||
"settings_desc": "Abrir as Definições Decky"
|
||||
},
|
||||
"Updater": {
|
||||
"decky_updates": "Atualizações do Decky",
|
||||
"no_patch_notes_desc": "sem notas de atualizações para esta versão",
|
||||
"patch_notes_desc": "Notas de atualizações",
|
||||
"updates": {
|
||||
"check_button": "Procurar atualizações",
|
||||
"checking": "A verificar atualizações",
|
||||
"cur_version": "Versão atual: {{ver}}",
|
||||
"install_button": "Instalar Atualização",
|
||||
"label": "Atualizações",
|
||||
"lat_version": "Atualizado: a executar {{ver}}",
|
||||
"reloading": "Recarregando",
|
||||
"updating": "Atualização em curso"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
{
|
||||
"BranchSelect": {
|
||||
"update_channel": {
|
||||
"label": "Канал обновлений",
|
||||
"prerelease": "Предрелиз",
|
||||
"stable": "Стабильный",
|
||||
"testing": "Тестовый"
|
||||
}
|
||||
},
|
||||
"Developer": {
|
||||
"5secreload": "Перезагрузка через 5 секунд",
|
||||
"disabling": "Выключение React DevTools",
|
||||
"enabling": "Включение React DevTools"
|
||||
},
|
||||
"DropdownMultiselect": {
|
||||
"button": {
|
||||
"back": "Назад"
|
||||
}
|
||||
},
|
||||
"FilePickerError": {
|
||||
"errors": {
|
||||
"file_not_found": "Указан недействительный путь. Пожалуйста, проверьте его и повторите ввод.",
|
||||
"perm_denied": "У вас нет доступа к указанному каталогу.. Пожалуйста, проверьте имеет ли пользователь (deck на Steam Deck) соответствующие права доступа к указанной папке/файлу.",
|
||||
"unknown": "Произошла неизвестная ошибка. Текст ошибки: {{raw_error}}"
|
||||
}
|
||||
},
|
||||
"FilePickerIndex": {
|
||||
"file": {
|
||||
"select": "Выберите этот файл"
|
||||
},
|
||||
"files": {
|
||||
"all_files": "Все файлы",
|
||||
"file_type": "Тип файла",
|
||||
"show_hidden": "Показать скрытые файлы"
|
||||
},
|
||||
"filter": {
|
||||
"created_asce": "Создан (самый старый)",
|
||||
"created_desc": "Создан (самый новый)",
|
||||
"modified_asce": "Модифицирован (самый новый)",
|
||||
"modified_desc": "Модифицирован (самый старый)",
|
||||
"name_asce": "Z-A",
|
||||
"name_desc": "A-Z",
|
||||
"size_asce": "Размер (самый малый)",
|
||||
"size_desc": "Размер (самый большой)"
|
||||
},
|
||||
"folder": {
|
||||
"label": "Папка",
|
||||
"select": "Использовать этот каталог",
|
||||
"show_more": "Показать больше файлов"
|
||||
}
|
||||
},
|
||||
"MultiplePluginsInstallModal": {
|
||||
"confirm": "Вы уверены, что хотите внести следующие изменения?",
|
||||
"description": {
|
||||
"downgrade": "Откатить {{name}} до {{version}}",
|
||||
"install": "Установить {{name}} {{version}}",
|
||||
"overwrite": "Заменить {{name}} на {{version}}",
|
||||
"reinstall": "Переустановить {{name}} {{version}}",
|
||||
"update": "Обновить с {{name}} на {{version}}"
|
||||
},
|
||||
"ok_button": {
|
||||
"idle": "Подтвердить",
|
||||
"loading": "В процессе"
|
||||
},
|
||||
"title": {
|
||||
"downgrade_few": "Откатить {{count}} плагинов",
|
||||
"downgrade_many": "Откатить {{count}} плагинов",
|
||||
"downgrade_one": "Откатить 1 плагин",
|
||||
"install_few": "Установить {{count}} плагинов",
|
||||
"install_many": "Установить {{count}} плагинов",
|
||||
"install_one": "Установить {{count}} плагин",
|
||||
"mixed_few": "Изменить {{count}} плагинов",
|
||||
"mixed_many": "Изменить {{count}} плагинов",
|
||||
"mixed_one": "Изменить {{count}} плагин",
|
||||
"reinstall_few": "Переустановить {{count}} плагинов",
|
||||
"reinstall_many": "Переустановить {{count}} плагинов",
|
||||
"reinstall_one": "Переустановить {{count}} плагин",
|
||||
"update_few": "Переустановить {{count}} плагинов",
|
||||
"update_many": "Переустановить {{count}} плагинов",
|
||||
"update_one": "Переустановить {{count}} плагин"
|
||||
}
|
||||
},
|
||||
"PluginCard": {
|
||||
"plugin_downgrade": "Откат",
|
||||
"plugin_full_access": "Этот плагин имеет полный доступ к вашему Steam Deck.",
|
||||
"plugin_install": "Установить",
|
||||
"plugin_no_desc": "Нет описания.",
|
||||
"plugin_overwrite": "Замена",
|
||||
"plugin_reinstall": "Переустановка",
|
||||
"plugin_update": "Обновление",
|
||||
"plugin_version_label": "Версия плагина"
|
||||
},
|
||||
"PluginInstallModal": {
|
||||
"downgrade": {
|
||||
"button_idle": "Откат",
|
||||
"desc": "Вы уверенны, что хотите откатить {{artifact}} до версии {{version}}?",
|
||||
"title": "Откатить {{artifact}}"
|
||||
},
|
||||
"install": {
|
||||
"button_idle": "Установить",
|
||||
"button_processing": "Установка",
|
||||
"desc": "Вы уверены, что хотите установить {{artifact}} {{version}}?",
|
||||
"title": "Установить {{artifact}}"
|
||||
},
|
||||
"no_hash": "У данного плагина отсутствует хэш, устанавливайте на свой страх и риск.",
|
||||
"reinstall": {
|
||||
"button_idle": "Переустановить",
|
||||
"button_processing": "Переустановка",
|
||||
"desc": "Вы уверены, что хотите переустановить {{artifact}} {{version}}?",
|
||||
"title": "Переустановить {{artifact}}"
|
||||
},
|
||||
"update": {
|
||||
"button_idle": "Обновить",
|
||||
"button_processing": "Обновление",
|
||||
"desc": "Вы уверены, что хотите обновить {{artifact}} до версии {{version}}?",
|
||||
"title": "Обновить {{artifact}}"
|
||||
}
|
||||
},
|
||||
"PluginListIndex": {
|
||||
"freeze": "Остановить обновления",
|
||||
"hide": "Быстрый доступ: Скрыть",
|
||||
"no_plugin": "Не установлено ни одного плагина!",
|
||||
"plugin_actions": "Действия с плагинами",
|
||||
"reinstall": "Переустановить",
|
||||
"reload": "Перезагрузить",
|
||||
"show": "Быстрый доступ: Показать",
|
||||
"unfreeze": "Разрешить обновления",
|
||||
"uninstall": "Удалить",
|
||||
"update_all_few": "Обновить {{count}} плагинов",
|
||||
"update_all_many": "Обновить {{count}} плагинов",
|
||||
"update_all_one": "Обновить {{count}} плагин",
|
||||
"update_to": "Обновить на {{name}}"
|
||||
},
|
||||
"PluginListLabel": {
|
||||
"hidden": "Скрыто из меню быстрого доступа"
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
"decky_update_available": "Доступно обновление на {{tag_name}}!",
|
||||
"error": "Ошибка",
|
||||
"plugin_error_uninstall": "Загрузка {{name}} вызвала исключение, указанное выше. Обычно это означает, что плагин требует обновления для новой версии SteamUI. Проверьте наличие обновления или попробуйте его удалить в настройках Decky, в разделе Плагины.",
|
||||
"plugin_load_error": {
|
||||
"message": "Ошибка загрузки плагина {{name}}",
|
||||
"toast": "Ошибка загрузки {{name}}"
|
||||
},
|
||||
"plugin_uninstall": {
|
||||
"button": "Удалить",
|
||||
"desc": "Вы уверены, что хотите удалить {{name}}?",
|
||||
"title": "Удалить {{name}}"
|
||||
},
|
||||
"plugin_update_few": "Обновления доступны для {{count}} плагинов!",
|
||||
"plugin_update_many": "Обновления доступны для {{count}} плагинов!",
|
||||
"plugin_update_one": "Обновления доступны для {{count}} плагина!"
|
||||
},
|
||||
"PluginView": {
|
||||
"hidden_few": "{{count}} плагинов скрыт из списка",
|
||||
"hidden_many": "{{count}} плагинов скрыт из списка",
|
||||
"hidden_one": "{{count}} плагин скрыт из списка"
|
||||
},
|
||||
"RemoteDebugging": {
|
||||
"remote_cef": {
|
||||
"desc": "Разрешить неаутентифицированный доступ к отладчику CEF всем в вашей сети",
|
||||
"label": "Разрешить удаленную отладку CEF"
|
||||
}
|
||||
},
|
||||
"SettingsDeveloperIndex": {
|
||||
"cef_console": {
|
||||
"button": "Открыть консоль",
|
||||
"desc": "Открывает консоль CEF. Полезно только для целей отладки. Настройки здесь потенциально опасны и должны использоваться только в том случае, если вы являетесь разработчиком плагинов или направленны сюда одним из них.",
|
||||
"label": "CEF Консоль"
|
||||
},
|
||||
"header": "Другое",
|
||||
"react_devtools": {
|
||||
"desc": "Позволяет подключиться к компьютеру, на котором работает React DevTools. Изменение этого параметра приведет к перезагрузке Steam. Установите IP-адрес перед включением.",
|
||||
"ip_label": "IP",
|
||||
"label": "Включить React DevTools"
|
||||
},
|
||||
"third_party_plugins": {
|
||||
"button_install": "Установить",
|
||||
"button_zip": "Обзор",
|
||||
"header": "Сторонние плагины",
|
||||
"label_desc": "Ссылка",
|
||||
"label_url": "Установить плагин из URL",
|
||||
"label_zip": "Установить плагин из ZIP файла"
|
||||
},
|
||||
"valve_internal": {
|
||||
"desc1": "Включает внутреннее меню разработчика Valve.",
|
||||
"desc2": "Ничего не трогайте в этом меню, если не знаете, что оно делает.",
|
||||
"label": "Включить Valve Internal"
|
||||
}
|
||||
},
|
||||
"SettingsGeneralIndex": {
|
||||
"about": {
|
||||
"decky_version": "Версия Decky",
|
||||
"header": "Информация"
|
||||
},
|
||||
"beta": {
|
||||
"header": "Бета программа"
|
||||
},
|
||||
"developer_mode": {
|
||||
"label": "Режим разработчика"
|
||||
},
|
||||
"notifications": {
|
||||
"decky_updates_label": "Обновление Decky доступно",
|
||||
"header": "Уведомления",
|
||||
"plugin_updates_label": "Доступны обновления плагинов"
|
||||
},
|
||||
"other": {
|
||||
"header": "Другое"
|
||||
},
|
||||
"updates": {
|
||||
"header": "Обновления"
|
||||
}
|
||||
},
|
||||
"SettingsIndex": {
|
||||
"developer_title": "Разработчик",
|
||||
"general_title": "Общее",
|
||||
"plugins_title": "Плагины",
|
||||
"testing_title": "Тестирование"
|
||||
},
|
||||
"Store": {
|
||||
"download_progress_info": {
|
||||
"download_zip": "Скачивание плагина",
|
||||
"increment_count": "Увеличение количества загрузок",
|
||||
"installing_plugin": "Установка плагина",
|
||||
"open_zip": "Открытие zip файла",
|
||||
"parse_zip": "Распаковка zip файла",
|
||||
"start": "Инициализация",
|
||||
"uninstalling_previous": "Удаление предыдущей копии"
|
||||
},
|
||||
"store_contrib": {
|
||||
"desc": "Если вы хотите внести свой вклад в магазин плагинов Decky, проверьте репозиторий SteamDeckHomebrew/decky-plugin-template на GitHub. Информация о разработке и распространении доступна в README.",
|
||||
"label": "Помощь проекту"
|
||||
},
|
||||
"store_filter": {
|
||||
"label": "Фильтр",
|
||||
"label_def": "Все"
|
||||
},
|
||||
"store_search": {
|
||||
"label": "Поиск"
|
||||
},
|
||||
"store_sort": {
|
||||
"label": "Сортировка",
|
||||
"label_def": "Последнее обновление(самые новые)"
|
||||
},
|
||||
"store_source": {
|
||||
"desc": "Весь исходный код плагина доступен в репозитории SteamDeckHomebrew/decky-plugin-database на GitHub.",
|
||||
"label": "Исходный код"
|
||||
},
|
||||
"store_tabs": {
|
||||
"about": "Информация",
|
||||
"alph_asce": "По алфавиту (Z - A)",
|
||||
"alph_desc": "По алфавиту (A - Z)",
|
||||
"date_asce": "Сначала старые",
|
||||
"date_desc": "Сначала новые",
|
||||
"downloads_asce": "Наименее загружаемые сначала",
|
||||
"downloads_desc": "Наиболее загружаемые сначала",
|
||||
"title": "Обзор"
|
||||
},
|
||||
"store_testing_cta": "Пожалуйста, рассмотрите возможность тестирования новых плагинов, чтобы помочь команде Decky Loader!",
|
||||
"store_testing_warning": {
|
||||
"desc": "Вы можете использовать этот канал магазина для тестирования новейших версий плагинов. Не забудьте оставить отзыв на GitHub, чтобы плагин можно было обновить для всех пользователей.",
|
||||
"label": "Добро пожаловать в тестовый канал магазина"
|
||||
}
|
||||
},
|
||||
"StoreSelect": {
|
||||
"custom_store": {
|
||||
"label": "Сторонний магазин",
|
||||
"url_label": "URL"
|
||||
},
|
||||
"store_channel": {
|
||||
"custom": "Сторонний",
|
||||
"default": "По-умолчанию",
|
||||
"label": "Канал магазина",
|
||||
"testing": "Тестовый"
|
||||
}
|
||||
},
|
||||
"Testing": {
|
||||
"download": "Загрузить",
|
||||
"error": "Ошибка при установке PR",
|
||||
"header": "Данные версии Decky Loader созданы на основе сторонних pull requst. Команда Decky Loader не проверяла их функциональность и безопасность, и они могут быть устаревшими.",
|
||||
"loading": "Загрузка открытых pull requst'ов...",
|
||||
"start_download_toast": "Загрузка PR#{{id}}"
|
||||
},
|
||||
"TitleView": {
|
||||
"decky_store_desc": "Открыть магазин Decky",
|
||||
"settings_desc": "Открыть настройки Decky"
|
||||
},
|
||||
"Updater": {
|
||||
"decky_updates": "Обновления Decky",
|
||||
"no_patch_notes_desc": "нет примечаний к патчу для этой версии",
|
||||
"patch_notes_desc": "Примечания к патчу",
|
||||
"updates": {
|
||||
"check_button": "Проверить обновления",
|
||||
"checking": "Проверка",
|
||||
"cur_version": "Текущая версия: {{ver}}",
|
||||
"install_button": "Установить обновление",
|
||||
"label": "Обновления",
|
||||
"lat_version": "Обновлено: версия {{ver}}",
|
||||
"reloading": "Перезагрузка",
|
||||
"updating": "Обновление"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
{
|
||||
"BranchSelect": {
|
||||
"update_channel": {
|
||||
"label": "Kanali Përditësimet",
|
||||
"stable": "Fiksuar"
|
||||
}
|
||||
},
|
||||
"FilePickerIndex": {
|
||||
"folder": {
|
||||
"select": "Përdore këtë folder"
|
||||
}
|
||||
},
|
||||
"MultiplePluginsInstallModal": {
|
||||
"title": {
|
||||
"install_one": "",
|
||||
"install_other": "",
|
||||
"mixed_one": "",
|
||||
"mixed_other": "",
|
||||
"reinstall_one": "",
|
||||
"reinstall_other": "",
|
||||
"update_one": "",
|
||||
"update_other": ""
|
||||
}
|
||||
},
|
||||
"PluginCard": {
|
||||
"plugin_install": "Instalo",
|
||||
"plugin_version_label": "Versioni Shteses"
|
||||
},
|
||||
"PluginInstallModal": {
|
||||
"install": {
|
||||
"button_idle": "Instalo",
|
||||
"button_processing": "Instalohet",
|
||||
"desc": "Je i sigurt që don ta instalojsh {{artifact}} {{version}}?",
|
||||
"title": "Instalo {{artifact}}"
|
||||
},
|
||||
"no_hash": "Ky shtesë nuk ka hash, ti e instalon me rrezikun tuaj.",
|
||||
"reinstall": {
|
||||
"button_idle": "Riinstalo",
|
||||
"button_processing": "Riinstalohet",
|
||||
"desc": "Je i sigurt a don ta riinstalojsh {{artifact}} {{version}}?",
|
||||
"title": "Riinstalo {{artifact}}"
|
||||
},
|
||||
"update": {
|
||||
"button_processing": "Përditësohet",
|
||||
"desc": "Je i sigurt a don ta përditësojsh {{artifact}} {{version}}?",
|
||||
"title": "Përditëso {{artifact}}"
|
||||
}
|
||||
},
|
||||
"PluginListIndex": {
|
||||
"no_plugin": "Nuk ka shtesa të instaluar!",
|
||||
"uninstall": "Çinstalo",
|
||||
"update_all_one": "",
|
||||
"update_all_other": ""
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
"error": "Gabim",
|
||||
"plugin_error_uninstall": "Ju lutem shko nga {{name}} në Decky menu nëse don ta çinstalojsh këtë shtese.",
|
||||
"plugin_uninstall": {
|
||||
"button": "Çinstalo",
|
||||
"desc": "Je i sigurt që don ta çinstalojsh {{name}}?",
|
||||
"title": "Çinstalo {{name}}"
|
||||
},
|
||||
"plugin_update_one": "",
|
||||
"plugin_update_other": ""
|
||||
},
|
||||
"PluginView": {
|
||||
"hidden_one": "",
|
||||
"hidden_other": ""
|
||||
},
|
||||
"SettingsDeveloperIndex": {
|
||||
"react_devtools": {
|
||||
"ip_label": "IP",
|
||||
"label": "Aktivizo React DevTools"
|
||||
},
|
||||
"third_party_plugins": {
|
||||
"button_install": "Instalo",
|
||||
"button_zip": "Kërko",
|
||||
"header": "Shtesa të Huaj",
|
||||
"label_desc": "URL",
|
||||
"label_url": "Instalo Shtes Nga URL",
|
||||
"label_zip": "Instalo Shtes Nga ZIP"
|
||||
}
|
||||
},
|
||||
"SettingsGeneralIndex": {
|
||||
"about": {
|
||||
"decky_version": "Versioni Decky"
|
||||
},
|
||||
"other": {
|
||||
"header": "Të Tjera"
|
||||
},
|
||||
"updates": {
|
||||
"header": "Përmirësimet"
|
||||
}
|
||||
},
|
||||
"SettingsIndex": {
|
||||
"developer_title": "Zhvillues",
|
||||
"general_title": "Gjeneral"
|
||||
},
|
||||
"Store": {
|
||||
"store_contrib": {
|
||||
"label": "Kontributi"
|
||||
},
|
||||
"store_filter": {
|
||||
"label": "Filtro",
|
||||
"label_def": "Të Gjitha"
|
||||
},
|
||||
"store_search": {
|
||||
"label": "Kërko"
|
||||
},
|
||||
"store_sort": {
|
||||
"label": "Rendit"
|
||||
},
|
||||
"store_source": {
|
||||
"label": "Kodin Burimor"
|
||||
},
|
||||
"store_tabs": {
|
||||
"title": "Kërko"
|
||||
}
|
||||
},
|
||||
"StoreSelect": {
|
||||
"store_channel": {
|
||||
"label": "Kanali Dyqanit"
|
||||
}
|
||||
},
|
||||
"Updater": {
|
||||
"updates": {
|
||||
"cur_version": "Versioni e tanishëme: {{ver}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
{
|
||||
"BranchSelect": {
|
||||
"update_channel": {
|
||||
"label": "Uppdateringskanal",
|
||||
"prerelease": "Förhandsversion",
|
||||
"stable": "Stabil",
|
||||
"testing": "Testning"
|
||||
}
|
||||
},
|
||||
"Developer": {
|
||||
"5secreload": "Uppdaterar om 5 sekunder",
|
||||
"disabling": "Inaktiverar React DevTools",
|
||||
"enabling": "Aktiverar React DevTools"
|
||||
},
|
||||
"DropdownMultiselect": {
|
||||
"button": {
|
||||
"back": "Tillbaka"
|
||||
}
|
||||
},
|
||||
"FilePickerError": {
|
||||
"errors": {
|
||||
"file_not_found": "Den angivna sökvägen är inte giltig. Kontrollera den och ange den korrekt igen.",
|
||||
"perm_denied": "Du har inte tillgång till den angivna katalogen. Kontrollera om din användare (deck på Steam Deck) har motsvarande behörighet för att komma åt den angivna mappen/filen.",
|
||||
"unknown": "Ett okänt fel har inträffat. Det råa felet är: {{raw_error}}"
|
||||
}
|
||||
},
|
||||
"FilePickerIndex": {
|
||||
"file": {
|
||||
"select": "Välj denna fil"
|
||||
},
|
||||
"files": {
|
||||
"all_files": "Alla filer",
|
||||
"file_type": "Filtyp",
|
||||
"show_hidden": "Visa dolda filer"
|
||||
},
|
||||
"filter": {
|
||||
"created_asce": "Skapad (äldst)",
|
||||
"created_desc": "Skapad (nyast)",
|
||||
"modified_asce": "Modifierad (äldst)",
|
||||
"modified_desc": "Modifierad (nyaste)",
|
||||
"name_asce": "Z-A",
|
||||
"name_desc": "A-Z",
|
||||
"size_asce": "Storlek (minst)",
|
||||
"size_desc": "Storlek (störst)"
|
||||
},
|
||||
"folder": {
|
||||
"label": "Mapp",
|
||||
"select": "Använd denna mapp",
|
||||
"show_more": "Visa fler filer"
|
||||
}
|
||||
},
|
||||
"MultiplePluginsInstallModal": {
|
||||
"confirm": "Är du säker på att du vill göra följande ändringar?",
|
||||
"description": {
|
||||
"downgrade": "Nedgradera {{name}} till {{version}}",
|
||||
"install": "Installera {{name}} {{version}}",
|
||||
"overwrite": "Skriv över {{name}} med {{version}}",
|
||||
"reinstall": "Installera om {{name}} {{version}}",
|
||||
"update": "Uppdatera {{name}} {{version}}"
|
||||
},
|
||||
"ok_button": {
|
||||
"idle": "Bekräfta",
|
||||
"loading": "Arbetar"
|
||||
},
|
||||
"title": {
|
||||
"downgrade_one": "Nedgradera 1 insticksmodul",
|
||||
"downgrade_other": "Nedgradera {{count}} insticksmoduler",
|
||||
"install_one": "Install 1 tillägg",
|
||||
"install_other": "Installerar {{count}} tillägg",
|
||||
"mixed_one": "Ändra {{count}} insticksmodul",
|
||||
"mixed_other": "Ändra {{count}} insticksmoduler",
|
||||
"overwrite_one": "Skriv över 1 insticksmodul",
|
||||
"overwrite_other": "Skriv över {{count}} insticksmoduler",
|
||||
"reinstall_one": "Installera om 1 insticksmodul",
|
||||
"reinstall_other": "Installera om {{count}} insticksmoduler",
|
||||
"update_one": "Uppdatera 1 insticksmodul",
|
||||
"update_other": "Uppdatera {{count}} insticksmoduler"
|
||||
}
|
||||
},
|
||||
"PluginCard": {
|
||||
"plugin_downgrade": "Nedgradera",
|
||||
"plugin_full_access": "Denna insticksmodul har full åtkomst till din Steam Deck.",
|
||||
"plugin_install": "Installera",
|
||||
"plugin_no_desc": "Ingen beskrivning angavs.",
|
||||
"plugin_overwrite": "Skriv över",
|
||||
"plugin_reinstall": "Installera om",
|
||||
"plugin_update": "Uppdatera",
|
||||
"plugin_version_label": "Version av insticksmodul"
|
||||
},
|
||||
"PluginInstallModal": {
|
||||
"downgrade": {
|
||||
"button_idle": "Nedgradera",
|
||||
"button_processing": "Nedgraderar",
|
||||
"desc": "Är du säker på att du vill nedgradera {{artifact}} till version {{version}}?",
|
||||
"title": "Nedgradera {{artifact}}"
|
||||
},
|
||||
"install": {
|
||||
"button_idle": "Installera",
|
||||
"button_processing": "Installerar",
|
||||
"desc": "Är du säker på att du vill installera {{artifact}} {{version}}?",
|
||||
"title": "Installera {{artifact}}"
|
||||
},
|
||||
"no_hash": "Denna insticksmodul har inte någon kontrollsumma. Du installerar den på egen risk.",
|
||||
"not_installed": "(inte installerad)",
|
||||
"overwrite": {
|
||||
"button_idle": "Skriv över",
|
||||
"button_processing": "Skriver över",
|
||||
"desc": "Är du säker på att du vill skriva över {{artifact}} med version {{version}}?",
|
||||
"title": "Skriv över {{artifact}}"
|
||||
},
|
||||
"reinstall": {
|
||||
"button_idle": "Installera om",
|
||||
"button_processing": "Installerar om",
|
||||
"desc": "Är du säker på att du vill installera om {{artifact}} {{version}}?",
|
||||
"title": "Installera om {{artifact}}"
|
||||
},
|
||||
"update": {
|
||||
"button_idle": "Uppdatera",
|
||||
"button_processing": "Uppdaterar",
|
||||
"desc": "Är du säker på att du vill uppdatera {{artifact}} till version {{version}}?",
|
||||
"title": "Uppdatera {{artifact}}"
|
||||
}
|
||||
},
|
||||
"PluginListIndex": {
|
||||
"freeze": "Frys uppdateringar",
|
||||
"hide": "Snabbåtkomst: Dölj",
|
||||
"no_plugin": "Inga insticksmoduler installerade!",
|
||||
"plugin_actions": "Åtgärder för insticksmodul",
|
||||
"reinstall": "Installera om",
|
||||
"reload": "Uppdatera",
|
||||
"show": "Snabbåtkomst: Visa",
|
||||
"unfreeze": "Tillåt uppdateringar",
|
||||
"uninstall": "Avinstallera",
|
||||
"update_all_one": "Uppdatera 1 insticksmodul",
|
||||
"update_all_other": "Uppdatera {{count}} insticksmoduler",
|
||||
"update_to": "Uppdatera till {{name}}"
|
||||
},
|
||||
"PluginListLabel": {
|
||||
"hidden": "Dold från snabbåtkomstmenyn"
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
"decky_update_available": "Uppdatering till {{tag_name}} finns!",
|
||||
"error": "Fel",
|
||||
"plugin_error_uninstall": "Inläsning av {{name}} orsakade ett undantag som visas nedan. Detta betyder oftast att insticksmodulen kräver en uppdatering för den nya versionen av SteamUI. Kontrollera om en uppdatering finns eller fundera på att ta bort den i Decky-inställningarna, under insticksmoduler.",
|
||||
"plugin_load_error": {
|
||||
"message": "Fel vid inläsning av insticksmodulen {{name}}",
|
||||
"toast": "Fel vid inläsning {{name}}"
|
||||
},
|
||||
"plugin_uninstall": {
|
||||
"button": "Avinstallera",
|
||||
"desc": "Är du säker på att du vill avinstallera {{name}}?",
|
||||
"title": "Avinstallera {{name}}"
|
||||
},
|
||||
"plugin_update_one": "Uppdateringar tillgängliga för 1 insticksmodul!",
|
||||
"plugin_update_other": "Uppdateringar tillgängliga för {{count}} insticksmoduler!"
|
||||
},
|
||||
"PluginView": {
|
||||
"hidden_one": "1 insticksmodul är dold från denna lista",
|
||||
"hidden_other": "{{count}} insticksmoduler är dolda från denna lista"
|
||||
},
|
||||
"RemoteDebugging": {
|
||||
"remote_cef": {
|
||||
"desc": "Tillåt oautentiserad åtkomst till CEF-felsökaren till vem som helst i ditt nätverk",
|
||||
"label": "Tillåt fjärr-CEF-felsökning"
|
||||
}
|
||||
},
|
||||
"SettingsDeveloperIndex": {
|
||||
"cef_console": {
|
||||
"button": "Öppna konsoll",
|
||||
"desc": "Öppnar CEF-konsollen. Endast användbart för felsökningssyften. Saker här är potentiellt farliga och bör endast användas om du utvecklar insticksmoduler.",
|
||||
"label": "CEF-konsoll"
|
||||
},
|
||||
"header": "Övrigt",
|
||||
"react_devtools": {
|
||||
"desc": "Aktiverar anslutning till en dator som kör React DevTools. Ändring av denna inställning kommer att läsa om Steam. Ställ in IP-adressen innan du aktiverar.",
|
||||
"ip_label": "IP",
|
||||
"label": "Aktivera React DevTools"
|
||||
},
|
||||
"third_party_plugins": {
|
||||
"button_install": "Installera",
|
||||
"button_zip": "Bläddra",
|
||||
"header": "Insticksmoduler från tredjepart",
|
||||
"label_desc": "URL",
|
||||
"label_url": "Installera insticksmodul från URL",
|
||||
"label_zip": "Installera insticksmodul från ZIP-fil"
|
||||
},
|
||||
"valve_internal": {
|
||||
"desc1": "Aktiverar Valves interna utvecklarmeny.",
|
||||
"desc2": "Rör ingenting i denna meny såvida inte du vet vad du gör.",
|
||||
"label": "Aktivera Valves interna"
|
||||
}
|
||||
},
|
||||
"SettingsGeneralIndex": {
|
||||
"about": {
|
||||
"decky_version": "Decky-version",
|
||||
"header": "Om"
|
||||
},
|
||||
"beta": {
|
||||
"header": "Delta i betatestning"
|
||||
},
|
||||
"developer_mode": {
|
||||
"label": "Utvecklarläge"
|
||||
},
|
||||
"notifications": {
|
||||
"decky_updates_label": "Uppdatering till Decky finns tillgänglig",
|
||||
"header": "Aviseringar",
|
||||
"plugin_updates_label": "Uppdateringar till insticksmoduler tillgängliga"
|
||||
},
|
||||
"other": {
|
||||
"header": "Övrigt"
|
||||
},
|
||||
"updates": {
|
||||
"header": "Uppdateringar"
|
||||
}
|
||||
},
|
||||
"SettingsIndex": {
|
||||
"developer_title": "Utvecklare",
|
||||
"general_title": "Allmänt",
|
||||
"plugins_title": "Insticksmoduler",
|
||||
"testing_title": "Testning"
|
||||
},
|
||||
"Store": {
|
||||
"download_progress_info": {
|
||||
"download_remote": "Hämtar externa binärfiler",
|
||||
"download_zip": "Hämtar insticksmodul",
|
||||
"increment_count": "Ökar hämtningsantal",
|
||||
"installing_plugin": "Installerar insticksmodul",
|
||||
"open_zip": "Öppnar zip-fil",
|
||||
"parse_zip": "Tolkar zip-fil",
|
||||
"start": "Initierar",
|
||||
"uninstalling_previous": "Avinstallerar tidigare kopia"
|
||||
},
|
||||
"store_contrib": {
|
||||
"desc": "Om du vill bidra till Deckys insticksmoduler, titta in i förrådet SteamDeckHomebrew/decky-plugin-template på GitHub. Information om utveckling och distribution finns tillgänglig i filen README.",
|
||||
"label": "Bidra"
|
||||
},
|
||||
"store_filter": {
|
||||
"label": "Filtrera",
|
||||
"label_def": "Alla"
|
||||
},
|
||||
"store_search": {
|
||||
"label": "Sök"
|
||||
},
|
||||
"store_sort": {
|
||||
"label": "Sortera",
|
||||
"label_def": "Senast uppdaterad (Senaste)"
|
||||
},
|
||||
"store_source": {
|
||||
"desc": "All källkod för insticksmoduler finns tillgänglig i förrådet SteamDeckHomebrew/decky-plugin-database på GitHub.",
|
||||
"label": "Källkod"
|
||||
},
|
||||
"store_tabs": {
|
||||
"about": "Om",
|
||||
"alph_asce": "Alfabetisk (Z till A)",
|
||||
"alph_desc": "Alfabetisk (A till Z)",
|
||||
"date_asce": "Äldsta först",
|
||||
"date_desc": "Senaste först",
|
||||
"downloads_asce": "Minst hämtade först",
|
||||
"downloads_desc": "Mest hämtade först",
|
||||
"title": "Bläddra"
|
||||
},
|
||||
"store_testing_cta": "Överväg att testa nya insticksmoduler för att hjälpa Decky Loader-teamet!",
|
||||
"store_testing_warning": {
|
||||
"desc": "Du kan använda denna kanal för att testa de absolut senaste versionerna. Tänk på att ge återkoppling på GitHub så att insticksmodulen kan uppdateras för alla användare.",
|
||||
"label": "Välkommen till testkanalen"
|
||||
}
|
||||
},
|
||||
"StoreSelect": {
|
||||
"custom_store": {
|
||||
"label": "Anpassad affär",
|
||||
"url_label": "URL"
|
||||
},
|
||||
"store_channel": {
|
||||
"custom": "Anpassad",
|
||||
"default": "Standard",
|
||||
"label": "Affärskanal",
|
||||
"testing": "Testning"
|
||||
}
|
||||
},
|
||||
"Testing": {
|
||||
"download": "Hämta",
|
||||
"error": "Fel vid installation av PR",
|
||||
"header": "Följande versioner av Decky Loader byggs från öppnade Pull Requests från tredjepart. Decky Loader-teamet har inte verifierat deras funktionalitet eller säkerhet och de kan vara utdaterade.",
|
||||
"loading": "Läser in öppnade Pull Requests...",
|
||||
"start_download_toast": "Hämtar PR #{{id}}"
|
||||
},
|
||||
"TitleView": {
|
||||
"decky_store_desc": "Öppna Decky-affär",
|
||||
"settings_desc": "Öppna Decky-inställningar"
|
||||
},
|
||||
"Updater": {
|
||||
"decky_updates": "Decky-uppdateringar",
|
||||
"no_patch_notes_desc": "inga patch-anteckningar för denna version",
|
||||
"patch_notes_desc": "Patch-anteckningar",
|
||||
"updates": {
|
||||
"check_button": "Leta efter uppdateringar",
|
||||
"checking": "Letar",
|
||||
"cur_version": "Aktuell version: {{ver}}",
|
||||
"install_button": "Installera uppdatering",
|
||||
"label": "Uppdateringar",
|
||||
"lat_version": "Uppdaterad: kör {{ver}}",
|
||||
"reloading": "Läser om",
|
||||
"updating": "Uppdaterar"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
{
|
||||
"BranchSelect": {
|
||||
"update_channel": {
|
||||
"label": "Güncelleme Kanalı",
|
||||
"prerelease": "Önsürüm",
|
||||
"stable": "Stabil",
|
||||
"testing": "Test"
|
||||
}
|
||||
},
|
||||
"DropdownMultiselect": {
|
||||
"button": {
|
||||
"back": "Geri"
|
||||
}
|
||||
},
|
||||
"FilePickerError": {
|
||||
"errors": {
|
||||
"file_not_found": "Belirtilen yol geçerli değil. Lütfen yolu kontrol edin ve doğru şekilde yeniden girin.",
|
||||
"perm_denied": "Belirtilen dizine erişim izniniz yok. Lütfen kullanıcınızın (Steam Deck'te deck) belirtilen klasöre/dosyaya erişmek için ilgili izne sahip olup olmadığını kontrol edin.",
|
||||
"unknown": "Bilinmeyen bir hata oluştu. Raw hata: {{raw_error}}"
|
||||
}
|
||||
},
|
||||
"FilePickerIndex": {
|
||||
"file": {
|
||||
"select": "Bu dosyayı seçin"
|
||||
},
|
||||
"files": {
|
||||
"all_files": "Tüm Dosyalar",
|
||||
"file_type": "Dosya Türü",
|
||||
"show_hidden": "Gizli Dosyaları Göster"
|
||||
},
|
||||
"filter": {
|
||||
"created_asce": "Oluşturuldu (En Eski)",
|
||||
"created_desc": "Oluşturuldu (En Yeni)",
|
||||
"modified_asce": "Değiştirildi (En Eski)",
|
||||
"modified_desc": "Değiştirildi (En Yeni)",
|
||||
"name_asce": "Z-A",
|
||||
"name_desc": "A-Z",
|
||||
"size_asce": "Boyut (En Küçük)",
|
||||
"size_desc": "Boyut (En Büyük)"
|
||||
},
|
||||
"folder": {
|
||||
"label": "Klasör",
|
||||
"select": "Bu klasörü kullan",
|
||||
"show_more": "Daha fazla dosya göster"
|
||||
}
|
||||
},
|
||||
"MultiplePluginsInstallModal": {
|
||||
"confirm": "Aşağıdaki değişiklikleri yapmak istediğinizden emin misiniz?",
|
||||
"description": {
|
||||
"install": "Yükle {{name}} {{version}}",
|
||||
"reinstall": "Yeniden yükle {{name}} {{version}}",
|
||||
"update": "{{name}}'i {{version}}'ye güncelle"
|
||||
},
|
||||
"ok_button": {
|
||||
"idle": "Onayla",
|
||||
"loading": "Çalışıyor"
|
||||
},
|
||||
"title": {
|
||||
"install_one": "1 eklenti yükle",
|
||||
"install_other": "{{count}} eklenti yükle",
|
||||
"mixed_one": "{{count}} eklentiyi değiştir",
|
||||
"mixed_other": "{{count}} eklentiyi değiştir",
|
||||
"reinstall_one": "1 eklentiyi yeniden yükle",
|
||||
"reinstall_other": "{{count}} eklentiyi yeniden yükle",
|
||||
"update_one": "{{count}} eklentiyi güncelle",
|
||||
"update_other": "{{count}} eklentiyi güncelle"
|
||||
}
|
||||
},
|
||||
"PluginCard": {
|
||||
"plugin_full_access": "Bu eklenti Steam Deck'inize tam erişime sahiptir.",
|
||||
"plugin_install": "Yükle",
|
||||
"plugin_no_desc": "Açıklama bulunmuyor.",
|
||||
"plugin_version_label": "Eklenti Versiyonu"
|
||||
},
|
||||
"PluginInstallModal": {
|
||||
"install": {
|
||||
"button_idle": "Yükle",
|
||||
"button_processing": "Yükleniyor",
|
||||
"desc": "Yüklemek istediğinizden emin misiniz {{artifact}} {{version}}?",
|
||||
"title": "Yükle {{artifact}}"
|
||||
},
|
||||
"reinstall": {
|
||||
"button_idle": "Yeniden Yükle",
|
||||
"button_processing": "Yeniden Yükleniyor",
|
||||
"desc": "Yeniden yüklemek istediğinizden emin misiniz {{artifact}} {{version}}?",
|
||||
"title": "Yeniden Yükle {{artifact}}"
|
||||
},
|
||||
"update": {
|
||||
"button_idle": "Güncelle",
|
||||
"button_processing": "Güncelleniyor",
|
||||
"desc": "Güncellemek istediğinizden emin misiniz {{artifact}} {{version}}?",
|
||||
"title": "Güncelle {{artifact}}"
|
||||
}
|
||||
},
|
||||
"PluginListIndex": {
|
||||
"freeze": "Güncellemeleri durdur",
|
||||
"hide": "Hızlı erişim: Gizle",
|
||||
"no_plugin": "Yüklü eklenti yok!",
|
||||
"plugin_actions": "Eklenti İşlemleri",
|
||||
"reinstall": "Yeniden Yükle",
|
||||
"reload": "Yenile",
|
||||
"show": "Hızlı erişim: Göster",
|
||||
"unfreeze": "Güncellemelere izin ver",
|
||||
"uninstall": "Kaldır",
|
||||
"update_all_one": "1 eklentiyi güncelle",
|
||||
"update_all_other": "{{count}} eklentiyi güncelle"
|
||||
},
|
||||
"PluginListLabel": {
|
||||
"hidden": "Hızlı erişim menüsünden gizlenmiş"
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
"decky_update_available": "{{tag_name}} güncellemesi mevcut!",
|
||||
"error": "Hata",
|
||||
"plugin_load_error": {
|
||||
"message": "{{name}} eklentisi yüklenirken bir hata oluştu",
|
||||
"toast": "{{name}} yüklenirken hata oluştu"
|
||||
},
|
||||
"plugin_uninstall": {
|
||||
"button": "Kaldır",
|
||||
"desc": "{{name}} kaldırmak istediğinizden emin misiniz?",
|
||||
"title": "Kaldır {{name}}"
|
||||
},
|
||||
"plugin_update_one": "1 eklenti için güncelleme mevcut!",
|
||||
"plugin_update_other": "{{count}} eklenti için güncelleme mevcut!"
|
||||
},
|
||||
"PluginView": {
|
||||
"hidden_one": "1 eklenti bu listeden gizlenmiştir",
|
||||
"hidden_other": "{{count}} eklenti bu listeden gizlenmiştir"
|
||||
},
|
||||
"SettingsDeveloperIndex": {
|
||||
"cef_console": {
|
||||
"button": "Konsolu Aç"
|
||||
},
|
||||
"header": "Diğer",
|
||||
"react_devtools": {
|
||||
"ip_label": "IP"
|
||||
},
|
||||
"third_party_plugins": {
|
||||
"button_install": "Yükle",
|
||||
"button_zip": "Gözat",
|
||||
"header": "Üçüncü Parti Eklentiler",
|
||||
"label_desc": "URL",
|
||||
"label_url": "URL'den Eklenti Yükle",
|
||||
"label_zip": "ZIP Dosyasından Eklenti Yükle"
|
||||
}
|
||||
},
|
||||
"SettingsGeneralIndex": {
|
||||
"about": {
|
||||
"decky_version": "Decky Versiyonu",
|
||||
"header": "Hakkında"
|
||||
},
|
||||
"beta": {
|
||||
"header": "Betaya katılım"
|
||||
},
|
||||
"developer_mode": {
|
||||
"label": "Geliştirici modu"
|
||||
},
|
||||
"notifications": {
|
||||
"decky_updates_label": "Decky güncellemesi mevcut",
|
||||
"header": "Bildirimler",
|
||||
"plugin_updates_label": "Eklenti güncellemesi mevcut"
|
||||
},
|
||||
"other": {
|
||||
"header": "Diğer"
|
||||
},
|
||||
"updates": {
|
||||
"header": "Güncellemeler"
|
||||
}
|
||||
},
|
||||
"SettingsIndex": {
|
||||
"developer_title": "Geliştirici",
|
||||
"general_title": "Genel",
|
||||
"plugins_title": "Eklentiler"
|
||||
},
|
||||
"Store": {
|
||||
"store_contrib": {
|
||||
"label": "Katkıda Bulunma"
|
||||
},
|
||||
"store_filter": {
|
||||
"label": "Filtre",
|
||||
"label_def": "Tümü"
|
||||
},
|
||||
"store_search": {
|
||||
"label": "Ara"
|
||||
},
|
||||
"store_sort": {
|
||||
"label": "Sırala",
|
||||
"label_def": "Son Güncellenme (En Yeni)"
|
||||
},
|
||||
"store_source": {
|
||||
"label": "Kaynak Kodu"
|
||||
},
|
||||
"store_tabs": {
|
||||
"about": "Hakkında",
|
||||
"alph_asce": "Alfabetik (Z'den A'ya)",
|
||||
"alph_desc": "Alfabetik (A'dan Z'ye)",
|
||||
"date_asce": "Önce En Eski",
|
||||
"date_desc": "Önce En Yeni",
|
||||
"downloads_asce": "Önce En Az İndirilen",
|
||||
"downloads_desc": "Önce En Çok İndirilen",
|
||||
"title": "Gözat"
|
||||
}
|
||||
},
|
||||
"StoreSelect": {
|
||||
"custom_store": {
|
||||
"url_label": "URL"
|
||||
},
|
||||
"store_channel": {
|
||||
"custom": "Özel",
|
||||
"default": "Varsayılan"
|
||||
}
|
||||
},
|
||||
"Testing": {
|
||||
"download": "İndir"
|
||||
},
|
||||
"TitleView": {
|
||||
"decky_store_desc": "Decky Mağazasını Aç",
|
||||
"settings_desc": "Decky Ayarlarını Aç"
|
||||
},
|
||||
"Updater": {
|
||||
"decky_updates": "Decky Güncellemeleri",
|
||||
"no_patch_notes_desc": "bu sürüm için yama notları mevcut değil",
|
||||
"patch_notes_desc": "Yama Notları",
|
||||
"updates": {
|
||||
"check_button": "Güncellemeleri Kontrol Et",
|
||||
"checking": "Kontrol ediliyor",
|
||||
"cur_version": "Mevcut Versiyon: {{ver}}",
|
||||
"install_button": "Güncellemeyi Yükle",
|
||||
"label": "Güncellemeler",
|
||||
"updating": "Güncelleniyor"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
{
|
||||
"BranchSelect": {
|
||||
"update_channel": {
|
||||
"label": "Канал оновлень",
|
||||
"prerelease": "Передреліз",
|
||||
"stable": "Стабільний",
|
||||
"testing": "Тестовий"
|
||||
}
|
||||
},
|
||||
"Developer": {
|
||||
"5secreload": "Перезавантаження за 5 секунд",
|
||||
"disabling": "Вимкнення React DevTools",
|
||||
"enabling": "Увімкнення React DevTools"
|
||||
},
|
||||
"FilePickerIndex": {
|
||||
"folder": {
|
||||
"select": "Використовувати цю папку"
|
||||
}
|
||||
},
|
||||
"MultiplePluginsInstallModal": {
|
||||
"confirm": "Ви впевнені, що хочете застосувати такі модифікації?",
|
||||
"description": {
|
||||
"install": "Встановити {{name}} {{version}}",
|
||||
"reinstall": "Перевстановити {{name}} {{version}}",
|
||||
"update": "Оновити {{name}} до {{version}}"
|
||||
},
|
||||
"ok_button": {
|
||||
"idle": "Підтвердити",
|
||||
"loading": "Опрацювання"
|
||||
},
|
||||
"title": {
|
||||
"install_few": "Встановити {{count}} плагінів",
|
||||
"install_many": "Встановити {{count}} плагінів",
|
||||
"install_one": "Встановити 1 плагін",
|
||||
"mixed_few": "Модифікувати {{count}} плагінів",
|
||||
"mixed_many": "",
|
||||
"mixed_one": "Модифікувати 1 плагін",
|
||||
"reinstall_few": "Перевстановити {{count}} плагінів",
|
||||
"reinstall_many": "Перевстановити {{count}} плагінів",
|
||||
"reinstall_one": "Перевстановити 1 плагін",
|
||||
"update_few": "Оновити {{count}} плагінів",
|
||||
"update_many": "Оновити {{count}} плагінів",
|
||||
"update_one": "Оновити 1 плагін"
|
||||
}
|
||||
},
|
||||
"PluginCard": {
|
||||
"plugin_full_access": "Цей плагін має повний доступ до вашого Steam Deck.",
|
||||
"plugin_install": "Встановити",
|
||||
"plugin_no_desc": "Опис не надано.",
|
||||
"plugin_version_label": "Версія плагіна"
|
||||
},
|
||||
"PluginInstallModal": {
|
||||
"install": {
|
||||
"button_idle": "Встановити",
|
||||
"button_processing": "Встановлення",
|
||||
"desc": "Ви впевнені, що хочете встановити {{artifact}} {{version}}?",
|
||||
"title": "Встановити {{artifact}}"
|
||||
},
|
||||
"no_hash": "Цей плагін не має хешу, ви встановлюєте його на власний ризик.",
|
||||
"reinstall": {
|
||||
"button_idle": "Перевстановити",
|
||||
"button_processing": "Перевстановлення",
|
||||
"desc": "Ви впевнені, що хочете перевстановити {{artifact}} {{version}}?",
|
||||
"title": "Перевстановити {{artifact}}"
|
||||
},
|
||||
"update": {
|
||||
"button_idle": "Оновити",
|
||||
"button_processing": "Оновлення",
|
||||
"desc": "Ви впевнені, що хочете оновити {{artifact}} {{version}}?",
|
||||
"title": "Оновити {{artifact}}"
|
||||
}
|
||||
},
|
||||
"PluginListIndex": {
|
||||
"hide": "Швидкий доступ: Приховати",
|
||||
"no_plugin": "Плагінів не встановлено!",
|
||||
"plugin_actions": "Дії плагінів",
|
||||
"reinstall": "Перевстановити",
|
||||
"reload": "Перезавантажити",
|
||||
"show": "Швидкий доступ: Показати",
|
||||
"uninstall": "Видалити",
|
||||
"update_all_few": "Оновити {{count}} плагінів",
|
||||
"update_all_many": "Оновити {{count}} плагінів",
|
||||
"update_all_one": "Оновити 1 плагін",
|
||||
"update_to": "Оновити {{name}}"
|
||||
},
|
||||
"PluginListLabel": {
|
||||
"hidden": "Приховано з меню швидкого доступу"
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
"decky_update_available": "Доступне оновлення до {{tag_name}}!",
|
||||
"error": "Помилка",
|
||||
"plugin_error_uninstall": "Завантаження {{name}} спровокувало помилку показану вище. Зазвичай це означає, що плагін вимагає оновлення до нової версії SteamUI. Перевірте чи таке оновлення доступне або виконайте його видалення у налаштуваннях Decky, у секції Плагіни.",
|
||||
"plugin_load_error": {
|
||||
"message": "Помилка завантаження плагіна {{name}}",
|
||||
"toast": "Помилка завантаження {{name}}"
|
||||
},
|
||||
"plugin_uninstall": {
|
||||
"button": "Видалення",
|
||||
"desc": "Ви впевнені, що хочете видалити {{name}}?",
|
||||
"title": "Видалити {{name}}"
|
||||
},
|
||||
"plugin_update_few": "Доступне оновлення для {{count}} плагінів!",
|
||||
"plugin_update_many": "Доступне оновлення для {{count}} плагінів!",
|
||||
"plugin_update_one": "Доступне оновлення для 1 плагіна!"
|
||||
},
|
||||
"PluginView": {
|
||||
"hidden_few": "{{count}} плагінів приховано з цього списку",
|
||||
"hidden_many": "{{count}} плагінів приховано з цього списку",
|
||||
"hidden_one": "{{count}} плагін приховано з цього списку"
|
||||
},
|
||||
"RemoteDebugging": {
|
||||
"remote_cef": {
|
||||
"desc": "Дозволити доступ до CEF-дебагера без аутентифікації для будь-кого у вашій мережі",
|
||||
"label": "Дозволити віддалений CEF-дебагінг"
|
||||
}
|
||||
},
|
||||
"SettingsDeveloperIndex": {
|
||||
"cef_console": {
|
||||
"button": "Відкрити консоль",
|
||||
"desc": "Відкрити CEF-консоль. Корисно тільки для дебагу. Ця штука потенційно небезпечна і повинна використовувати виключно якщо ви розробник плагіна, або якщо розробник спрямував вас сюди.",
|
||||
"label": "CEF-консоль"
|
||||
},
|
||||
"header": "Інше",
|
||||
"react_devtools": {
|
||||
"desc": "Вмикає доступ до компʼютера із запущеним React DevTools. Зміна цього налаштування перезавантажить Steam. Вкажіть IP перед увімкненням.",
|
||||
"ip_label": "IP",
|
||||
"label": "Увімкнути React DevTools"
|
||||
},
|
||||
"third_party_plugins": {
|
||||
"button_install": "Встановити",
|
||||
"button_zip": "Огляд",
|
||||
"header": "Сторонні плагіни",
|
||||
"label_desc": "URL",
|
||||
"label_url": "Встановити плагін з URL",
|
||||
"label_zip": "Встановити плагін з ZIP-файлу"
|
||||
},
|
||||
"valve_internal": {
|
||||
"desc1": "Вмикає внутрішнє розробницьке меню Valve.",
|
||||
"desc2": "Нічого не торкайтесь у цьому меню, якщо не розумієте, що ви робите.",
|
||||
"label": "Увімкнути Valve Internal"
|
||||
}
|
||||
},
|
||||
"SettingsGeneralIndex": {
|
||||
"about": {
|
||||
"decky_version": "Версія Decky",
|
||||
"header": "Про нас"
|
||||
},
|
||||
"beta": {
|
||||
"header": "Участь у Beta"
|
||||
},
|
||||
"developer_mode": {
|
||||
"label": "Розробницький режим"
|
||||
},
|
||||
"other": {
|
||||
"header": "Інше"
|
||||
},
|
||||
"updates": {
|
||||
"header": "Оновлення"
|
||||
}
|
||||
},
|
||||
"SettingsIndex": {
|
||||
"developer_title": "Розробник",
|
||||
"general_title": "Загальне",
|
||||
"plugins_title": "Плагіни"
|
||||
},
|
||||
"Store": {
|
||||
"store_contrib": {
|
||||
"desc": "Якщо ви бажаєте додати щось у Decky Plugin Store, завітайте у репозиторій SteamDeckHomebrew/decky-plugin-template на GitHub. Інформація про розробку та поширення доступна у README.",
|
||||
"label": "Зробити внесок"
|
||||
},
|
||||
"store_filter": {
|
||||
"label": "Фільтр",
|
||||
"label_def": "Усе"
|
||||
},
|
||||
"store_search": {
|
||||
"label": "Пошук"
|
||||
},
|
||||
"store_sort": {
|
||||
"label": "Сортування",
|
||||
"label_def": "Востаннє оновлені (Найновіші)"
|
||||
},
|
||||
"store_source": {
|
||||
"desc": "Код усіх плагінів доступний у репозиторії SteamDeckHomebrew/decky-plugin-database на GitHub.",
|
||||
"label": "Вихідний код"
|
||||
},
|
||||
"store_tabs": {
|
||||
"about": "Інформація",
|
||||
"alph_asce": "За алфавітом (Z до A)",
|
||||
"alph_desc": "За алфавітом (A до Z)",
|
||||
"title": "Огляд"
|
||||
},
|
||||
"store_testing_cta": "Розгляньте можливість тестування нових плагінів, щоб допомогти команді Decky Loader!"
|
||||
},
|
||||
"StoreSelect": {
|
||||
"custom_store": {
|
||||
"label": "Власний магазин",
|
||||
"url_label": "URL"
|
||||
},
|
||||
"store_channel": {
|
||||
"custom": "Власний",
|
||||
"default": "За замовчуванням",
|
||||
"label": "Канал магазину",
|
||||
"testing": "Тестування"
|
||||
}
|
||||
},
|
||||
"Updater": {
|
||||
"decky_updates": "Оновлення Decky",
|
||||
"no_patch_notes_desc": "Немає нотаток до цієї версії",
|
||||
"patch_notes_desc": "Перелік змін",
|
||||
"updates": {
|
||||
"check_button": "Перевірити оновлення",
|
||||
"checking": "Перевірка",
|
||||
"cur_version": "Поточна версія: {{ver}}",
|
||||
"install_button": "Встановити оновлення",
|
||||
"label": "Оновлення",
|
||||
"lat_version": "Оновлено: використовується {{ver}}",
|
||||
"reloading": "Перезавантаження",
|
||||
"updating": "Оновлення"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
{
|
||||
"BranchSelect": {
|
||||
"update_channel": {
|
||||
"label": "更新通道",
|
||||
"prerelease": "发布候选",
|
||||
"stable": "稳定",
|
||||
"testing": "测试"
|
||||
}
|
||||
},
|
||||
"Developer": {
|
||||
"5secreload": "5 秒钟后重新加载",
|
||||
"disabling": "正在禁用 React DevTools",
|
||||
"enabling": "正在启用 React DevTools"
|
||||
},
|
||||
"DropdownMultiselect": {
|
||||
"button": {
|
||||
"back": "返回"
|
||||
}
|
||||
},
|
||||
"FilePickerError": {
|
||||
"errors": {
|
||||
"file_not_found": "指定路径无效。请检查并输入正确的路径。",
|
||||
"perm_denied": "你没有访问特定目录的权限。请检查你的用户(Steam Deck 中的 deck 账户)是否有权访问特定的文件夹或文件。",
|
||||
"unknown": "发生了一个未知错误。原始错误为:{{raw_error}}"
|
||||
}
|
||||
},
|
||||
"FilePickerIndex": {
|
||||
"file": {
|
||||
"select": "选择此文件"
|
||||
},
|
||||
"files": {
|
||||
"all_files": "全部文件",
|
||||
"file_type": "文件类型",
|
||||
"show_hidden": "显示隐藏文件"
|
||||
},
|
||||
"filter": {
|
||||
"created_asce": "创建日期(最旧)",
|
||||
"created_desc": "创建日期(最新)",
|
||||
"modified_asce": "修改日期(最旧)",
|
||||
"modified_desc": "修改日期(最新)",
|
||||
"name_asce": "字母降序",
|
||||
"name_desc": "字母升序",
|
||||
"size_asce": "大小(最小)",
|
||||
"size_desc": "大小(最大)"
|
||||
},
|
||||
"folder": {
|
||||
"label": "文件夹",
|
||||
"select": "使用这个文件夹",
|
||||
"show_more": "显示更多文件"
|
||||
}
|
||||
},
|
||||
"MultiplePluginsInstallModal": {
|
||||
"confirm": "确定要进行以下修改吗?",
|
||||
"description": {
|
||||
"install": "安装 {{name}} {{version}}",
|
||||
"reinstall": "重装 {{name}} {{version}}",
|
||||
"update": "更新 {{name}} to {{version}}"
|
||||
},
|
||||
"ok_button": {
|
||||
"idle": "确认",
|
||||
"loading": "工作中"
|
||||
},
|
||||
"title": {
|
||||
"install_other": "安装 {{count}} 个插件",
|
||||
"mixed_other": "更改 {{count}} 个插件",
|
||||
"reinstall_other": "重装 {{count}} 个插件",
|
||||
"update_other": "更新 {{count}} 个插件"
|
||||
}
|
||||
},
|
||||
"PluginCard": {
|
||||
"plugin_full_access": "此插件可以完全访问你的 Steam Deck。",
|
||||
"plugin_install": "安装",
|
||||
"plugin_no_desc": "无描述提供。",
|
||||
"plugin_version_label": "插件版本"
|
||||
},
|
||||
"PluginInstallModal": {
|
||||
"install": {
|
||||
"button_idle": "安装",
|
||||
"button_processing": "安装中",
|
||||
"desc": "你确定要安装 {{artifact}} {{version}} 吗?",
|
||||
"title": "安装 {{artifact}}"
|
||||
},
|
||||
"no_hash": "此插件没有哈希校验值,你需要自行承担安装风险。",
|
||||
"reinstall": {
|
||||
"button_idle": "重新安装",
|
||||
"button_processing": "正在重新安装",
|
||||
"desc": "你确定要重新安装 {{artifact}} {{version}} 吗?",
|
||||
"title": "重新安装 {{artifact}}"
|
||||
},
|
||||
"update": {
|
||||
"button_idle": "更新",
|
||||
"button_processing": "正在更新",
|
||||
"desc": "你确定要更新 {{artifact}} {{version}} 吗?",
|
||||
"title": "更新 {{artifact}}"
|
||||
}
|
||||
},
|
||||
"PluginListIndex": {
|
||||
"freeze": "暂停更新",
|
||||
"hide": "在快速访问菜单中隐藏",
|
||||
"no_plugin": "没有安装插件!",
|
||||
"plugin_actions": "插件操作",
|
||||
"reinstall": "重新安装",
|
||||
"reload": "重新加载",
|
||||
"show": "在快速访问菜单中显示",
|
||||
"unfreeze": "允许更新",
|
||||
"uninstall": "卸载",
|
||||
"update_all_other": "更新 {{count}} 个插件",
|
||||
"update_to": "更新 {{name}}"
|
||||
},
|
||||
"PluginListLabel": {
|
||||
"hidden": "在快速访问菜单中已隐藏"
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
"decky_update_available": "新版本 {{tag_name}} 可用!",
|
||||
"error": "错误",
|
||||
"plugin_error_uninstall": "加载 {{name}} 时引起了上述异常。这通常意味着插件需要更新以适应 SteamUI 的新版本。请检查插件是否有更新,或在 Decky 设置中的插件部分将其移除。",
|
||||
"plugin_load_error": {
|
||||
"message": "加载插件 {{name}} 错误",
|
||||
"toast": "加载插件 {{name}} 发生了错误"
|
||||
},
|
||||
"plugin_uninstall": {
|
||||
"button": "卸载",
|
||||
"desc": "你确定要卸载 {{name}} 吗?",
|
||||
"title": "卸载 {{name}}"
|
||||
},
|
||||
"plugin_update_other": "{{count}} 个插件有更新!"
|
||||
},
|
||||
"PluginView": {
|
||||
"hidden_other": "此列表隐藏了 {{count}} 个插件"
|
||||
},
|
||||
"RemoteDebugging": {
|
||||
"remote_cef": {
|
||||
"desc": "允许你的网络中的任何人无需身份验证即可访问 CEF 调试器",
|
||||
"label": "允许 CEF 远程调试"
|
||||
}
|
||||
},
|
||||
"SettingsDeveloperIndex": {
|
||||
"cef_console": {
|
||||
"button": "打开控制台",
|
||||
"desc": "打开 CEF 控制台。仅在调试目的下使用。这列选项均有风险,请仅在您是插件开发者或是在插件开发者指导时访问使用。",
|
||||
"label": "CEF 控制台"
|
||||
},
|
||||
"header": "其他",
|
||||
"react_devtools": {
|
||||
"desc": "允许连接到运行着 React DevTools 的计算机。更改此设置将重新加载 Steam。请在启用前设置 IP 地址。",
|
||||
"ip_label": "IP",
|
||||
"label": "启用 React DevTools"
|
||||
},
|
||||
"third_party_plugins": {
|
||||
"button_install": "安装",
|
||||
"button_zip": "浏览文件",
|
||||
"header": "第三方插件",
|
||||
"label_desc": "URL",
|
||||
"label_url": "从 URL 安装插件",
|
||||
"label_zip": "从 ZIP 压缩文件安装插件"
|
||||
},
|
||||
"valve_internal": {
|
||||
"desc1": "启用 Valve 内部开发者菜单。",
|
||||
"desc2": "除非你知道你在干什么,否则请不要修改此菜单中的任何内容。",
|
||||
"label": "启用 Valve 内部开发者"
|
||||
}
|
||||
},
|
||||
"SettingsGeneralIndex": {
|
||||
"about": {
|
||||
"decky_version": "Decky 版本",
|
||||
"header": "关于"
|
||||
},
|
||||
"beta": {
|
||||
"header": "参与测试"
|
||||
},
|
||||
"developer_mode": {
|
||||
"label": "开发者模式"
|
||||
},
|
||||
"notifications": {
|
||||
"decky_updates_label": "Decky 更新可用",
|
||||
"header": "通知",
|
||||
"plugin_updates_label": "插件更新可用"
|
||||
},
|
||||
"other": {
|
||||
"header": "其他"
|
||||
},
|
||||
"updates": {
|
||||
"header": "更新"
|
||||
}
|
||||
},
|
||||
"SettingsIndex": {
|
||||
"developer_title": "开发者",
|
||||
"general_title": "通用",
|
||||
"plugins_title": "插件",
|
||||
"testing_title": "测试"
|
||||
},
|
||||
"Store": {
|
||||
"download_progress_info": {
|
||||
"download_zip": "正在下载插件",
|
||||
"increment_count": "正在计入下载次数",
|
||||
"installing_plugin": "正在安装插件",
|
||||
"open_zip": "正在打开 ZIP 文件",
|
||||
"parse_zip": "正在解析 ZIP 文件",
|
||||
"start": "正在初始化",
|
||||
"uninstalling_previous": "正在卸载之前的版本"
|
||||
},
|
||||
"store_contrib": {
|
||||
"desc": "如果你想要提交你的插件到 Decky 插件商店,请访问 GitHub 上的 SteamDeckHomebrew/decky-plugin-template 存储库。有关开发和分发插件的信息,请查看 README 文件。",
|
||||
"label": "贡献"
|
||||
},
|
||||
"store_filter": {
|
||||
"label": "过滤器",
|
||||
"label_def": "全部"
|
||||
},
|
||||
"store_search": {
|
||||
"label": "搜索"
|
||||
},
|
||||
"store_sort": {
|
||||
"label": "排序",
|
||||
"label_def": "最后更新 (最新)"
|
||||
},
|
||||
"store_source": {
|
||||
"desc": "所有插件的源代码都可从 GitHub 上的 SteamDeckHomebrew/decky-plugin-database 存储库中获得。",
|
||||
"label": "源代码"
|
||||
},
|
||||
"store_tabs": {
|
||||
"about": "关于",
|
||||
"alph_asce": "字母排序 (Z 到 A)",
|
||||
"alph_desc": "字母排序 (A 到 Z)",
|
||||
"date_asce": "更新时间正序",
|
||||
"date_desc": "更新时间倒序",
|
||||
"downloads_asce": "下载量正序",
|
||||
"downloads_desc": "下载量倒序",
|
||||
"title": "浏览"
|
||||
},
|
||||
"store_testing_cta": "请考虑测试新插件以帮助 Decky Loader 团队!",
|
||||
"store_testing_warning": {
|
||||
"desc": "你可以使用该商店频道以体验最新版本的插件。 请在插件 Github 页面留言以使插件可以正式面向所有用户。",
|
||||
"label": "欢迎来到商店测试频道"
|
||||
}
|
||||
},
|
||||
"StoreSelect": {
|
||||
"custom_store": {
|
||||
"label": "自定义商店",
|
||||
"url_label": "URL"
|
||||
},
|
||||
"store_channel": {
|
||||
"custom": "自定义",
|
||||
"default": "默认",
|
||||
"label": "商店通道",
|
||||
"testing": "测试"
|
||||
}
|
||||
},
|
||||
"Testing": {
|
||||
"download": "下载",
|
||||
"error": "安装 PR 时出错",
|
||||
"header": "以下版本的 Decky Loader 是根据开放的第三方 Pull Request 构建的。Decky Loader 团队尚未验证这些版本的功能或安全性,且它们可能已经过期。",
|
||||
"loading": "正在加载尚未合并的 Pull Request ...",
|
||||
"start_download_toast": "正在下载 PR #{{id}}"
|
||||
},
|
||||
"TitleView": {
|
||||
"decky_store_desc": "打开 Decky 商店",
|
||||
"settings_desc": "打开 Decky 设置"
|
||||
},
|
||||
"Updater": {
|
||||
"decky_updates": "Decky 更新",
|
||||
"no_patch_notes_desc": "此版本没有补丁说明",
|
||||
"patch_notes_desc": "补丁说明",
|
||||
"updates": {
|
||||
"check_button": "检查更新",
|
||||
"checking": "检查中",
|
||||
"cur_version": "当前版本: {{ver}}",
|
||||
"install_button": "安装更新",
|
||||
"label": "更新",
|
||||
"lat_version": "已是最新版本: {{ver}} 运行中",
|
||||
"reloading": "重新加载中",
|
||||
"updating": "更新中"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
{
|
||||
"BranchSelect": {
|
||||
"update_channel": {
|
||||
"label": "更新頻道",
|
||||
"prerelease": "預發佈",
|
||||
"stable": "穩定版",
|
||||
"testing": "測試版"
|
||||
}
|
||||
},
|
||||
"Developer": {
|
||||
"5secreload": "5 秒後重新載入",
|
||||
"disabling": "正在停用 React DevTools",
|
||||
"enabling": "正在啟用 React DevTools"
|
||||
},
|
||||
"DropdownMultiselect": {
|
||||
"button": {
|
||||
"back": "返回"
|
||||
}
|
||||
},
|
||||
"FilePickerError": {
|
||||
"errors": {
|
||||
"file_not_found": "指定路徑無效。請檢查並輸入正確路徑。",
|
||||
"perm_denied": "您沒有瀏覽此目錄的權限。請檢查您的使用者(Steam Deck 中的 deck 帳號)有權限瀏覽特定的資料夾或檔案。",
|
||||
"unknown": "發生未知錯誤。錯誤詳細資料:{{raw_error}}"
|
||||
}
|
||||
},
|
||||
"FilePickerIndex": {
|
||||
"file": {
|
||||
"select": "選擇此檔案"
|
||||
},
|
||||
"files": {
|
||||
"all_files": "所有檔案",
|
||||
"file_type": "檔案類型",
|
||||
"show_hidden": "顯示隱藏檔"
|
||||
},
|
||||
"filter": {
|
||||
"created_asce": "建立日期(舊到新)",
|
||||
"created_desc": "建立日期(新到舊)",
|
||||
"modified_asce": "修改日期(舊到新)",
|
||||
"modified_desc": "修改日期(新到舊)",
|
||||
"name_asce": "子母排序(Z到A)",
|
||||
"name_desc": "子母排序(A到Z)",
|
||||
"size_asce": "檔案大小(小到大)",
|
||||
"size_desc": "檔案大小(大到小)"
|
||||
},
|
||||
"folder": {
|
||||
"label": "資料夾",
|
||||
"select": "使用此資料夾",
|
||||
"show_more": "顯示更多檔案"
|
||||
}
|
||||
},
|
||||
"MultiplePluginsInstallModal": {
|
||||
"confirm": "您確定要進行以下的修改嗎?",
|
||||
"description": {
|
||||
"install": "安裝 {{name}} {{version}}",
|
||||
"reinstall": "重新安裝 {{name}} {{version}}",
|
||||
"update": "更新 {{name}} 的版本到 {{version}}"
|
||||
},
|
||||
"ok_button": {
|
||||
"idle": "確定",
|
||||
"loading": "執行中"
|
||||
},
|
||||
"title": {
|
||||
"install_other": "安裝 {{count}} 個外掛程式",
|
||||
"mixed_other": "修改 {{count}} 個外掛程式",
|
||||
"reinstall_other": "重新安裝 {{count}} 個外掛程式",
|
||||
"update_other": "更新 {{count}} 個外掛程式"
|
||||
}
|
||||
},
|
||||
"PluginCard": {
|
||||
"plugin_full_access": "此外掛程式擁有您的 Steam Deck 的完整存取權。",
|
||||
"plugin_install": "安裝",
|
||||
"plugin_no_desc": "未提供描述。",
|
||||
"plugin_version_label": "外掛程式版本"
|
||||
},
|
||||
"PluginInstallModal": {
|
||||
"install": {
|
||||
"button_idle": "安裝",
|
||||
"button_processing": "正在安裝",
|
||||
"desc": "您確定要安裝 {{artifact}} {{version}} 嗎?",
|
||||
"title": "安裝 {{artifact}}"
|
||||
},
|
||||
"no_hash": "此外掛程式沒有提供 hash 驗證,安裝可能有風險。",
|
||||
"reinstall": {
|
||||
"button_idle": "重新安裝",
|
||||
"button_processing": "正在重新安裝",
|
||||
"desc": "您確定要重新安裝 {{artifact}} {{version}} 嗎?",
|
||||
"title": "重新安裝 {{artifact}}"
|
||||
},
|
||||
"update": {
|
||||
"button_idle": "更新",
|
||||
"button_processing": "正在更新",
|
||||
"desc": "您確定要更新 {{artifact}} {{version}} 嗎?",
|
||||
"title": "更新 {{artifact}}"
|
||||
}
|
||||
},
|
||||
"PluginListIndex": {
|
||||
"freeze": "禁止更新",
|
||||
"hide": "快速存取:隱藏",
|
||||
"no_plugin": "未安裝外掛程式!",
|
||||
"plugin_actions": "外掛程式操作",
|
||||
"reinstall": "重新安裝",
|
||||
"reload": "重新載入",
|
||||
"show": "快速存取:顯示",
|
||||
"unfreeze": "允許更新",
|
||||
"uninstall": "解除安裝",
|
||||
"update_all_other": "更新 {{count}} 個外掛程式",
|
||||
"update_to": "更新到 {{name}}"
|
||||
},
|
||||
"PluginListLabel": {
|
||||
"hidden": "已從快速存取選單中移除"
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
"decky_update_available": "可更新至版本 {{tag_name}}!",
|
||||
"error": "錯誤",
|
||||
"plugin_error_uninstall": "載入 {{name}} 導致上述異常。這通常意味著該外掛程式需要針對新版本的 SteamUI 進行更新。在 Decky 設定中檢查是否存在更新,或評估刪除此外掛程式。",
|
||||
"plugin_load_error": {
|
||||
"message": "載入外掛程式 {{name}} 發生錯誤",
|
||||
"toast": "{{name}} 載入出錯"
|
||||
},
|
||||
"plugin_uninstall": {
|
||||
"button": "解除安裝",
|
||||
"desc": "您確定要解除安裝 {{name}} 嗎?",
|
||||
"title": "解除安裝 {{name}}"
|
||||
},
|
||||
"plugin_update_other": "可更新 {{count}} 個外掛程式!"
|
||||
},
|
||||
"PluginView": {
|
||||
"hidden_other": "{{count}} 個外掛程式已隱藏"
|
||||
},
|
||||
"RemoteDebugging": {
|
||||
"remote_cef": {
|
||||
"desc": "允許您的網路中的任何人未經認證地存取 CEF 偵錯器",
|
||||
"label": "允許 CEF 遠端偵錯"
|
||||
}
|
||||
},
|
||||
"SettingsDeveloperIndex": {
|
||||
"cef_console": {
|
||||
"button": "開啟控制台",
|
||||
"desc": "開啟 CEF 控制台。僅用於偵錯。這裡的東西有潛在的風險,只有當您是一個外掛程式開發者或者被外掛程式開發者引導到這裡時,才應該使用。",
|
||||
"label": "CEF 控制台"
|
||||
},
|
||||
"header": "其他",
|
||||
"react_devtools": {
|
||||
"desc": "啟用與執行 React DevTools 的電腦的連接。改變這個設定將重新載入 Steam。啟用前必須設定 IP 位址。",
|
||||
"ip_label": "IP",
|
||||
"label": "啟用 React DevTools"
|
||||
},
|
||||
"third_party_plugins": {
|
||||
"button_install": "安裝",
|
||||
"button_zip": "瀏覽",
|
||||
"header": "第三方外掛程式",
|
||||
"label_desc": "網址",
|
||||
"label_url": "從網址安裝外掛程式",
|
||||
"label_zip": "從 ZIP 檔案安裝外掛程式"
|
||||
},
|
||||
"valve_internal": {
|
||||
"desc1": "啟用 Valve 內建開發人員選單。",
|
||||
"desc2": "除非您知道它的作用,否則不要碰這個選單中的任何東西。",
|
||||
"label": "啟用 Valve 內建"
|
||||
}
|
||||
},
|
||||
"SettingsGeneralIndex": {
|
||||
"about": {
|
||||
"decky_version": "Decky 版本",
|
||||
"header": "關於"
|
||||
},
|
||||
"beta": {
|
||||
"header": "參與測試"
|
||||
},
|
||||
"developer_mode": {
|
||||
"label": "開發人員模式"
|
||||
},
|
||||
"notifications": {
|
||||
"decky_updates_label": "Decky 可更新",
|
||||
"header": "通知",
|
||||
"plugin_updates_label": "外掛程式有更新"
|
||||
},
|
||||
"other": {
|
||||
"header": "其他"
|
||||
},
|
||||
"updates": {
|
||||
"header": "更新"
|
||||
}
|
||||
},
|
||||
"SettingsIndex": {
|
||||
"developer_title": "開發人員",
|
||||
"general_title": "一般",
|
||||
"plugins_title": "外掛程式",
|
||||
"testing_title": "測試"
|
||||
},
|
||||
"Store": {
|
||||
"store_contrib": {
|
||||
"desc": "如果您想為 Decky 外掛程式商店做貢獻,請查看 GitHub 上的 SteamDeckHomebrew/decky-plugin-template 儲存庫。README 中提供了有關開發和發佈的資訊。",
|
||||
"label": "貢獻"
|
||||
},
|
||||
"store_filter": {
|
||||
"label": "過濾",
|
||||
"label_def": "全部"
|
||||
},
|
||||
"store_search": {
|
||||
"label": "搜尋"
|
||||
},
|
||||
"store_sort": {
|
||||
"label": "排序",
|
||||
"label_def": "最後更新 (最新)"
|
||||
},
|
||||
"store_source": {
|
||||
"desc": "所有外掛程式原始碼可以在 GitHub 的 SteamDeckHomebrew/decky-plugin-database 儲存庫查看。",
|
||||
"label": "原始碼"
|
||||
},
|
||||
"store_tabs": {
|
||||
"about": "關於",
|
||||
"alph_asce": "依字母排序 (Z 到 A)",
|
||||
"alph_desc": "依字母排序 (A 到 Z)",
|
||||
"date_asce": "日期舊到新",
|
||||
"date_desc": "日期新到舊",
|
||||
"downloads_asce": "下載量低到高",
|
||||
"downloads_desc": "下載量高到低",
|
||||
"title": "瀏覽"
|
||||
},
|
||||
"store_testing_cta": "請考慮測試新的外掛程式來幫助 Decky Loader 團隊!",
|
||||
"store_testing_warning": {
|
||||
"desc": "您可以使用此商店頻道來體驗測試外掛版本。請務必在 GitHub 上留下回饋,以便為所有用戶更新該外掛程式。",
|
||||
"label": "歡迎來到測試頻道"
|
||||
}
|
||||
},
|
||||
"StoreSelect": {
|
||||
"custom_store": {
|
||||
"label": "自訂商店",
|
||||
"url_label": "網址"
|
||||
},
|
||||
"store_channel": {
|
||||
"custom": "自訂",
|
||||
"default": "預設",
|
||||
"label": "商店頻道",
|
||||
"testing": "測試"
|
||||
}
|
||||
},
|
||||
"Testing": {
|
||||
"download": "下載"
|
||||
},
|
||||
"TitleView": {
|
||||
"decky_store_desc": "開啟 Decky 商店",
|
||||
"settings_desc": "開啟 Decky 設定"
|
||||
},
|
||||
"Updater": {
|
||||
"decky_updates": "Decky 更新",
|
||||
"no_patch_notes_desc": "這個版本沒有更新日誌",
|
||||
"patch_notes_desc": "更新日誌",
|
||||
"updates": {
|
||||
"check_button": "檢查更新",
|
||||
"checking": "正在檢查",
|
||||
"cur_version": "目前版本:{{ver}}",
|
||||
"install_button": "安裝更新",
|
||||
"label": "更新",
|
||||
"lat_version": "已是最新:執行 {{ver}}",
|
||||
"reloading": "正在重新載入",
|
||||
"updating": "正在更新"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import platform, os
|
||||
|
||||
ON_WINDOWS = platform.system() == "Windows"
|
||||
ON_LINUX = not ON_WINDOWS
|
||||
|
||||
if ON_WINDOWS:
|
||||
from .localplatformwin import *
|
||||
from . import localplatformwin as localplatform
|
||||
else:
|
||||
from .localplatformlinux import *
|
||||
from . import localplatformlinux as localplatform
|
||||
|
||||
def get_privileged_path() -> str:
|
||||
'''Get path accessible by elevated user. Holds plugins, decky loader and decky loader configs'''
|
||||
return localplatform.get_privileged_path()
|
||||
|
||||
def get_unprivileged_path() -> str:
|
||||
'''Get path accessible by non-elevated user. Holds plugin configuration, plugin data and plugin logs. Externally referred to as the 'Homebrew' directory'''
|
||||
return localplatform.get_unprivileged_path()
|
||||
|
||||
def get_unprivileged_user() -> str:
|
||||
'''Get user that should own files made in unprivileged path'''
|
||||
return localplatform.get_unprivileged_user()
|
||||
|
||||
def get_chown_plugin_path() -> bool:
|
||||
return os.getenv("CHOWN_PLUGIN_PATH", "1") == "1"
|
||||
|
||||
def get_server_host() -> str:
|
||||
return os.getenv("SERVER_HOST", "127.0.0.1")
|
||||
|
||||
def get_server_port() -> int:
|
||||
return int(os.getenv("SERVER_PORT", "1337"))
|
||||
|
||||
def get_live_reload() -> bool:
|
||||
return os.getenv("LIVE_RELOAD", "1") == "1"
|
||||
|
||||
def get_keep_systemd_service() -> bool:
|
||||
return os.getenv("KEEP_SYSTEMD_SERVICE", "0") == "1"
|
||||
|
||||
def get_use_cef_close_workaround() -> bool:
|
||||
return ON_LINUX and os.getenv("USE_CEF_CLOSE_WORKAROUND", "1") == "1"
|
||||
|
||||
def get_log_level() -> int:
|
||||
return {"CRITICAL": 50, "ERROR": 40, "WARNING": 30, "INFO": 20, "DEBUG": 10}[
|
||||
os.getenv("LOG_LEVEL", "INFO")
|
||||
]
|
||||
|
||||
def get_selinux() -> bool:
|
||||
if ON_LINUX:
|
||||
from subprocess import check_output
|
||||
try:
|
||||
if (check_output("getenforce").decode("ascii").strip("\n") == "Enforcing"): return True
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
return False
|
||||
@@ -0,0 +1,279 @@
|
||||
from re import compile
|
||||
from asyncio import Lock, create_subprocess_exec
|
||||
from asyncio.subprocess import PIPE, DEVNULL, STDOUT, Process
|
||||
from subprocess import call as call_sync
|
||||
import os, pwd, grp, sys, logging
|
||||
from typing import IO, Any, Mapping
|
||||
from ..enums import UserType
|
||||
|
||||
logger = logging.getLogger("localplatform")
|
||||
|
||||
# subprocess._ENV
|
||||
ENV = Mapping[str, str]
|
||||
ProcessIO = int | IO[Any] | None
|
||||
async def run(args: list[str], stdin: ProcessIO = DEVNULL, stdout: ProcessIO = PIPE, stderr: ProcessIO = PIPE, env: ENV | None = {"LD_LIBRARY_PATH": ""}) -> tuple[Process, bytes | None, bytes | None]:
|
||||
proc = await create_subprocess_exec(args[0], *(args[1:]), stdin=stdin, stdout=stdout, stderr=stderr, env=env)
|
||||
proc_stdout, proc_stderr = await proc.communicate()
|
||||
return (proc, proc_stdout, proc_stderr)
|
||||
|
||||
# Get the user id hosting the plugin loader
|
||||
def _get_user_id() -> int:
|
||||
return pwd.getpwnam(_get_user()).pw_uid
|
||||
|
||||
# Get the user hosting the plugin loader
|
||||
def _get_user() -> str:
|
||||
return get_unprivileged_user()
|
||||
|
||||
# Get the effective user id of the running process
|
||||
def _get_effective_user_id() -> int:
|
||||
return os.geteuid()
|
||||
|
||||
# Get the effective user of the running process
|
||||
def _get_effective_user() -> str:
|
||||
return pwd.getpwuid(_get_effective_user_id()).pw_name
|
||||
|
||||
# Get the effective user group id of the running process
|
||||
def _get_effective_user_group_id() -> int:
|
||||
return os.getegid()
|
||||
|
||||
# Get the effective user group of the running process
|
||||
def _get_effective_user_group() -> str:
|
||||
return grp.getgrgid(_get_effective_user_group_id()).gr_name
|
||||
|
||||
# Get the user owner of the given file path.
|
||||
def _get_user_owner(file_path: str) -> str:
|
||||
return pwd.getpwuid(os.stat(file_path).st_uid).pw_name
|
||||
|
||||
# Get the user group of the given file path, or the user group hosting the plugin loader
|
||||
def _get_user_group(file_path: str | None = None) -> str:
|
||||
return grp.getgrgid(os.stat(file_path).st_gid if file_path is not None else _get_user_group_id()).gr_name
|
||||
|
||||
# Get the group id of the user hosting the plugin loader
|
||||
def _get_user_group_id() -> int:
|
||||
return pwd.getpwuid(_get_user_id()).pw_gid
|
||||
|
||||
def chown(path : str, user : UserType = UserType.HOST_USER, recursive : bool = True) -> bool:
|
||||
user_str = ""
|
||||
|
||||
if user == UserType.HOST_USER:
|
||||
user_str = _get_user()+":"+_get_user_group()
|
||||
elif user == UserType.EFFECTIVE_USER:
|
||||
user_str = _get_effective_user()+":"+_get_effective_user_group()
|
||||
else:
|
||||
raise Exception("Unknown User Type")
|
||||
|
||||
result = call_sync(["chown", "-R", user_str, path] if recursive else ["chown", user_str, path])
|
||||
return result == 0
|
||||
|
||||
def chmod(path : str, permissions : int, recursive : bool = True) -> bool:
|
||||
if _get_effective_user_id() != 0:
|
||||
return True
|
||||
|
||||
try:
|
||||
octal_permissions = int(str(permissions), 8)
|
||||
|
||||
if recursive:
|
||||
for root, dirs, files in os.walk(path):
|
||||
for d in dirs:
|
||||
os.chmod(os.path.join(root, d), octal_permissions)
|
||||
for d in files:
|
||||
os.chmod(os.path.join(root, d), octal_permissions)
|
||||
|
||||
os.chmod(path, octal_permissions)
|
||||
except:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def file_owner(path : str) -> UserType|None:
|
||||
user_owner = _get_user_owner(path)
|
||||
|
||||
if (user_owner == _get_user()):
|
||||
return UserType.HOST_USER
|
||||
|
||||
elif (user_owner == _get_effective_user()):
|
||||
return UserType.EFFECTIVE_USER
|
||||
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_home_path(user : UserType = UserType.HOST_USER) -> str:
|
||||
user_name = "root"
|
||||
|
||||
if user == UserType.HOST_USER:
|
||||
user_name = _get_user()
|
||||
elif user == UserType.EFFECTIVE_USER:
|
||||
user_name = _get_effective_user()
|
||||
else:
|
||||
raise Exception("Unknown User Type")
|
||||
|
||||
return pwd.getpwnam(user_name).pw_dir
|
||||
|
||||
def get_effective_username() -> str:
|
||||
return _get_effective_user()
|
||||
|
||||
def get_username() -> str:
|
||||
return _get_user()
|
||||
|
||||
def setgid(user : UserType = UserType.HOST_USER):
|
||||
host_user_group_id, effective_user_group_id = _get_user_group_id(), _get_effective_user_group_id()
|
||||
if host_user_group_id == effective_user_group_id:
|
||||
pass
|
||||
elif user == UserType.HOST_USER:
|
||||
os.setgid(host_user_group_id)
|
||||
elif user == UserType.EFFECTIVE_USER:
|
||||
os.setgid(effective_user_group_id)
|
||||
else:
|
||||
raise Exception("Unknown user type")
|
||||
|
||||
def setuid(user : UserType = UserType.HOST_USER):
|
||||
host_user_id, effective_user_id = _get_user_id(), _get_effective_user_id()
|
||||
if host_user_id == effective_user_id:
|
||||
pass
|
||||
elif user == UserType.HOST_USER:
|
||||
os.setuid(host_user_id)
|
||||
elif user == UserType.EFFECTIVE_USER:
|
||||
os.setuid(effective_user_id)
|
||||
else:
|
||||
raise Exception("Unknown user type")
|
||||
|
||||
async def service_active(service_name : str) -> bool:
|
||||
res, _, _ = await run(["systemctl", "is-active", service_name], stdout=DEVNULL, stderr=DEVNULL)
|
||||
return res.returncode == 0
|
||||
|
||||
async def service_restart(service_name : str, block : bool = True) -> bool:
|
||||
await run(["systemctl", "daemon-reload"])
|
||||
logger.info("Systemd reload done.")
|
||||
cmd = ["systemctl", "restart", service_name]
|
||||
|
||||
if not block:
|
||||
cmd.append("--no-block")
|
||||
|
||||
res, _, _ = await run(cmd, stdout=PIPE, stderr=STDOUT)
|
||||
return res.returncode == 0
|
||||
|
||||
async def service_stop(service_name : str) -> bool:
|
||||
if not await service_active(service_name):
|
||||
# Service isn't running. pretend we stopped it
|
||||
return True
|
||||
|
||||
cmd = ["systemctl", "stop", service_name]
|
||||
res, _, _ = await run(cmd, stdout=PIPE, stderr=STDOUT)
|
||||
return res.returncode == 0
|
||||
|
||||
async def service_start(service_name : str) -> bool:
|
||||
if await service_active(service_name):
|
||||
# Service is running. pretend we started it
|
||||
return True
|
||||
|
||||
cmd = ["systemctl", "start", service_name]
|
||||
res, _, _ = await run(cmd, stdout=PIPE, stderr=STDOUT)
|
||||
return res.returncode == 0
|
||||
|
||||
async def restart_webhelper() -> bool:
|
||||
logger.info("Restarting steamwebhelper")
|
||||
# TODO move to pkill
|
||||
res, _, _ = await run(["killall", "-s", "SIGTERM", "steamwebhelper"], stdout=DEVNULL, stderr=DEVNULL)
|
||||
return res.returncode == 0
|
||||
|
||||
def get_privileged_path() -> str:
|
||||
path = os.getenv("PRIVILEGED_PATH")
|
||||
|
||||
if path == None:
|
||||
path = get_unprivileged_path()
|
||||
|
||||
os.makedirs(path, exist_ok=True)
|
||||
|
||||
return path
|
||||
|
||||
def _parent_dir(path : str | None) -> str | None:
|
||||
if path == None:
|
||||
return None
|
||||
|
||||
if path.endswith('/'):
|
||||
path = path[:-1]
|
||||
|
||||
return os.path.dirname(path)
|
||||
|
||||
def get_unprivileged_path() -> str:
|
||||
path = os.getenv("UNPRIVILEGED_PATH")
|
||||
|
||||
if path == None:
|
||||
path = _parent_dir(os.getenv("PLUGIN_PATH"))
|
||||
|
||||
if path == None:
|
||||
logger.debug("Unprivileged path is not properly configured. Making something up!")
|
||||
|
||||
if hasattr(sys, 'frozen'):
|
||||
# Expected path of loader binary is /home/deck/homebrew/service/PluginLoader
|
||||
path = _parent_dir(_parent_dir(os.path.realpath(sys.argv[0])))
|
||||
else:
|
||||
# Expected path of this file is $src_root/backend/src/localplatformlinux.py
|
||||
path = _parent_dir(_parent_dir(_parent_dir(__file__)))
|
||||
|
||||
if path != None and not os.path.exists(path):
|
||||
path = None
|
||||
|
||||
if path == None:
|
||||
logger.warning("Unprivileged path is not properly configured. Defaulting to /home/deck/homebrew")
|
||||
path = "/home/deck/homebrew" # We give up
|
||||
|
||||
os.makedirs(path, exist_ok=True)
|
||||
|
||||
return path
|
||||
|
||||
|
||||
def get_unprivileged_user() -> str:
|
||||
user = os.getenv("UNPRIVILEGED_USER")
|
||||
|
||||
if user == None:
|
||||
# Lets hope we can extract it from the unprivileged dir
|
||||
dir = os.path.realpath(get_unprivileged_path())
|
||||
|
||||
pws = sorted(pwd.getpwall(), reverse=True, key=lambda pw: len(pw.pw_dir))
|
||||
for pw in pws:
|
||||
if dir.startswith(os.path.realpath(pw.pw_dir)):
|
||||
user = pw.pw_name
|
||||
break
|
||||
|
||||
if user == None:
|
||||
logger.warning("Unprivileged user is not properly configured. Defaulting to 'deck'")
|
||||
user = 'deck'
|
||||
|
||||
return user
|
||||
|
||||
# Works around the CEF debugger TCP socket not closing properly when Steam restarts
|
||||
# Group 1 is PID, group 2 is FD. this also filters for "steamwebhelper" in the process name.
|
||||
cef_socket_lsof_regex = compile(r"^p(\d+)(?:\s|.)+csteamwebhelper(?:\s|.)+f(\d+)(?:\s|.)+TST=LISTEN")
|
||||
close_cef_socket_lock = Lock()
|
||||
|
||||
async def close_cef_socket():
|
||||
async with close_cef_socket_lock:
|
||||
if _get_effective_user_id() != 0:
|
||||
logger.warning("Can't close CEF socket as Decky isn't running as root.")
|
||||
return
|
||||
# Look for anything listening TCP on port 8080
|
||||
lsof, stdout, _ = await run(["lsof", "-F", "-iTCP:8080", "-sTCP:LISTEN"], stdout=PIPE)
|
||||
if not stdout or lsof.returncode != 0 or len(stdout) < 1:
|
||||
logger.error(f"lsof call failed in close_cef_socket! return code: {str(lsof.returncode)}")
|
||||
return
|
||||
|
||||
lsof_data = cef_socket_lsof_regex.match(stdout.decode())
|
||||
|
||||
if not lsof_data:
|
||||
logger.error("lsof regex match failed in close_cef_socket!")
|
||||
return
|
||||
|
||||
pid = lsof_data.group(1)
|
||||
fd = lsof_data.group(2)
|
||||
|
||||
logger.info(f"Closing CEF socket with PID {pid} and FD {fd}")
|
||||
|
||||
# Use gdb to inject a close() call for the socket fd into steamwebhelper
|
||||
gdb_ret, _, _ = await run(["gdb", "--nx", "-p", pid, "--batch", "--eval-command", f"call (int)close({fd})"])
|
||||
|
||||
if gdb_ret.returncode != 0:
|
||||
logger.error(f"Failed to close CEF socket with gdb! return code: {str(gdb_ret.returncode)}", exc_info=True)
|
||||
return
|
||||
|
||||
logger.info("CEF socket closed")
|
||||
@@ -0,0 +1,64 @@
|
||||
from ..enums import UserType
|
||||
import os, sys
|
||||
|
||||
def chown(path : str, user : UserType = UserType.HOST_USER, recursive : bool = True) -> bool:
|
||||
return True # Stubbed
|
||||
|
||||
def chmod(path : str, permissions : int, recursive : bool = True) -> bool:
|
||||
return True # Stubbed
|
||||
|
||||
def file_owner(path : str) -> UserType|None:
|
||||
return UserType.HOST_USER # Stubbed
|
||||
|
||||
def get_home_path(user : UserType = UserType.HOST_USER) -> str:
|
||||
return os.path.expanduser("~") # Mostly stubbed
|
||||
|
||||
def setgid(user : UserType = UserType.HOST_USER):
|
||||
pass # Stubbed
|
||||
|
||||
def setuid(user : UserType = UserType.HOST_USER):
|
||||
pass # Stubbed
|
||||
|
||||
async def service_active(service_name : str) -> bool:
|
||||
return True # Stubbed
|
||||
|
||||
async def service_stop(service_name : str) -> bool:
|
||||
return True # Stubbed
|
||||
|
||||
async def service_start(service_name : str) -> bool:
|
||||
return True # Stubbed
|
||||
|
||||
async def service_restart(service_name : str, block : bool = True) -> bool:
|
||||
if service_name == "plugin_loader":
|
||||
sys.exit(42)
|
||||
|
||||
return True # Stubbed
|
||||
|
||||
def get_effective_username() -> str:
|
||||
return os.getlogin()
|
||||
|
||||
def get_username() -> str:
|
||||
return os.getlogin()
|
||||
|
||||
def get_privileged_path() -> str:
|
||||
'''On windows, privileged_path is equal to unprivileged_path'''
|
||||
return get_unprivileged_path()
|
||||
|
||||
def get_unprivileged_path() -> str:
|
||||
path = os.getenv("UNPRIVILEGED_PATH")
|
||||
|
||||
if path == None:
|
||||
path = os.getenv("PRIVILEGED_PATH", os.path.join(os.path.expanduser("~"), "homebrew"))
|
||||
|
||||
os.makedirs(path, exist_ok=True)
|
||||
|
||||
return path
|
||||
|
||||
def get_unprivileged_user() -> str:
|
||||
return os.getenv("UNPRIVILEGED_USER", os.getlogin())
|
||||
|
||||
async def restart_webhelper() -> bool:
|
||||
return True # Stubbed
|
||||
|
||||
async def close_cef_socket():
|
||||
return # Stubbed
|
||||
@@ -0,0 +1,166 @@
|
||||
import asyncio, time
|
||||
from typing import Any, Callable, Coroutine
|
||||
import random
|
||||
|
||||
from .localplatform import ON_WINDOWS
|
||||
|
||||
BUFFER_LIMIT = 2 ** 20 # 1 MiB
|
||||
|
||||
class UnixSocket:
|
||||
def __init__(self):
|
||||
'''
|
||||
on_new_message takes 1 string argument.
|
||||
It's return value gets used, if not None, to write data to the socket.
|
||||
Method should be async
|
||||
'''
|
||||
self.socket_addr = f"/tmp/plugin_socket_{time.time()}"
|
||||
self.on_new_message = None
|
||||
self.socket = None
|
||||
self.reader = None
|
||||
self.writer = None
|
||||
self.server_writer = None
|
||||
self.open_lock = asyncio.Lock()
|
||||
self.active = True
|
||||
|
||||
async def setup_server(self, on_new_message: Callable[[str], Coroutine[Any, Any, Any]]):
|
||||
try:
|
||||
self.on_new_message = on_new_message
|
||||
self.socket = await asyncio.start_unix_server(self._listen_for_method_call, path=self.socket_addr, limit=BUFFER_LIMIT)
|
||||
except asyncio.CancelledError:
|
||||
await self.close_socket_connection()
|
||||
raise
|
||||
|
||||
async def _open_socket_if_not_exists(self):
|
||||
if not self.reader:
|
||||
retries = 0
|
||||
while retries < 10:
|
||||
try:
|
||||
self.reader, self.writer = await asyncio.open_unix_connection(self.socket_addr, limit=BUFFER_LIMIT)
|
||||
return True
|
||||
except:
|
||||
await asyncio.sleep(2)
|
||||
retries += 1
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
async def get_socket_connection(self):
|
||||
async with self.open_lock:
|
||||
if not await self._open_socket_if_not_exists():
|
||||
return None, None
|
||||
|
||||
return self.reader, self.writer
|
||||
|
||||
async def close_socket_connection(self):
|
||||
if self.writer != None:
|
||||
self.writer.close()
|
||||
|
||||
self.reader = None
|
||||
|
||||
if self.socket:
|
||||
self.socket.close()
|
||||
await self.socket.wait_closed()
|
||||
|
||||
self.active = False
|
||||
|
||||
async def read_single_line(self) -> str|None:
|
||||
reader, _ = await self.get_socket_connection()
|
||||
|
||||
try:
|
||||
assert reader
|
||||
except AssertionError:
|
||||
return
|
||||
|
||||
return await self._read_single_line(reader)
|
||||
|
||||
async def write_single_line(self, message : str):
|
||||
_, writer = await self.get_socket_connection()
|
||||
|
||||
try:
|
||||
assert writer
|
||||
except AssertionError:
|
||||
return
|
||||
|
||||
await self._write_single_line(writer, message)
|
||||
|
||||
async def _read_single_line(self, reader: asyncio.StreamReader) -> str:
|
||||
line = bytearray()
|
||||
while self.active:
|
||||
try:
|
||||
line.extend(await reader.readuntil())
|
||||
except asyncio.LimitOverrunError:
|
||||
line.extend(await reader.read(reader._limit)) # pyright: ignore [reportUnknownMemberType, reportUnknownArgumentType, reportAttributeAccessIssue]
|
||||
continue
|
||||
except asyncio.IncompleteReadError as err:
|
||||
line.extend(err.partial)
|
||||
break
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
else:
|
||||
break
|
||||
|
||||
return line.decode("utf-8")
|
||||
|
||||
async def _write_single_line(self, writer: asyncio.StreamWriter, message : str):
|
||||
if not message.endswith("\n"):
|
||||
message += "\n"
|
||||
|
||||
writer.write(message.encode("utf-8"))
|
||||
await writer.drain()
|
||||
|
||||
async def write_single_line_server(self, message: str):
|
||||
if self.server_writer is None:
|
||||
return
|
||||
await self._write_single_line(self.server_writer, message)
|
||||
|
||||
async def _listen_for_method_call(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
|
||||
self.server_writer = writer
|
||||
while self.active and self.on_new_message:
|
||||
|
||||
def _(task: asyncio.Task[str|None]):
|
||||
res = task.result()
|
||||
if res is not None:
|
||||
asyncio.create_task(self._write_single_line(writer, res))
|
||||
|
||||
line = await self._read_single_line(reader)
|
||||
asyncio.create_task(self.on_new_message(line)).add_done_callback(_)
|
||||
|
||||
class PortSocket (UnixSocket):
|
||||
def __init__(self):
|
||||
'''
|
||||
on_new_message takes 1 string argument.
|
||||
It's return value gets used, if not None, to write data to the socket.
|
||||
Method should be async
|
||||
'''
|
||||
super().__init__()
|
||||
self.host = "127.0.0.1"
|
||||
self.port = random.sample(range(40000, 60000), 1)[0]
|
||||
|
||||
async def setup_server(self, on_new_message: Callable[[str], Coroutine[Any, Any, Any]]):
|
||||
try:
|
||||
self.on_new_message = on_new_message
|
||||
self.socket = await asyncio.start_server(self._listen_for_method_call, host=self.host, port=self.port, limit=BUFFER_LIMIT)
|
||||
except asyncio.CancelledError:
|
||||
await self.close_socket_connection()
|
||||
raise
|
||||
|
||||
async def _open_socket_if_not_exists(self):
|
||||
if not self.reader:
|
||||
retries = 0
|
||||
while retries < 10:
|
||||
try:
|
||||
self.reader, self.writer = await asyncio.open_connection(host=self.host, port=self.port, limit=BUFFER_LIMIT)
|
||||
return True
|
||||
except:
|
||||
await asyncio.sleep(2)
|
||||
retries += 1
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
if ON_WINDOWS:
|
||||
class LocalSocket (PortSocket): # type: ignore
|
||||
pass
|
||||
else:
|
||||
class LocalSocket (UnixSocket):
|
||||
pass
|
||||
@@ -0,0 +1,272 @@
|
||||
# Change PyInstaller files permissions
|
||||
import sys
|
||||
from typing import Any, Dict
|
||||
from .localplatform.localplatform import (chmod, chown, service_stop, service_start,
|
||||
ON_WINDOWS, ON_LINUX, get_log_level, get_live_reload,
|
||||
get_server_port, get_server_host, get_chown_plugin_path,
|
||||
get_privileged_path, restart_webhelper)
|
||||
if hasattr(sys, '_MEIPASS'):
|
||||
chmod(sys._MEIPASS, 755) # type: ignore
|
||||
|
||||
# Full imports
|
||||
import multiprocessing
|
||||
multiprocessing.freeze_support()
|
||||
from asyncio import AbstractEventLoop, CancelledError, Task, all_tasks, current_task, gather, new_event_loop, set_event_loop, sleep
|
||||
from logging import basicConfig, getLogger
|
||||
from os import path
|
||||
from traceback import format_exc
|
||||
from time import time
|
||||
import aiohttp_cors # pyright: ignore [reportMissingTypeStubs]
|
||||
|
||||
# Partial imports
|
||||
from aiohttp import client_exceptions
|
||||
from aiohttp.web import Application, Response, Request, get, run_app, static # pyright: ignore [reportUnknownVariableType]
|
||||
from aiohttp_jinja2 import setup as jinja_setup
|
||||
from setproctitle import getproctitle, setproctitle, setthreadtitle
|
||||
|
||||
# local modules
|
||||
from .browser import PluginBrowser
|
||||
from .helpers import (REMOTE_DEBUGGER_UNIT, create_inject_script, csrf_middleware, get_csrf_token, get_loader_version,
|
||||
mkdir_as_user, get_system_pythonpaths, get_effective_user_id)
|
||||
|
||||
from .injector import get_gamepadui_tab, Tab
|
||||
from .loader import Loader
|
||||
from .settings import SettingsManager
|
||||
from .updater import Updater
|
||||
from .utilities import Utilities
|
||||
from .enums import UserType
|
||||
from .wsrouter import WSRouter
|
||||
|
||||
|
||||
basicConfig(
|
||||
level=get_log_level(),
|
||||
format="[%(module)s][%(levelname)s]: %(message)s"
|
||||
)
|
||||
|
||||
logger = getLogger("Main")
|
||||
plugin_path = path.join(get_privileged_path(), "plugins")
|
||||
|
||||
def chown_plugin_dir():
|
||||
if not path.exists(plugin_path): # For safety, create the folder before attempting to do anything with it
|
||||
mkdir_as_user(plugin_path)
|
||||
|
||||
if not chown(plugin_path, UserType.EFFECTIVE_USER, False) or not chmod(plugin_path, 755, False):
|
||||
logger.error(f"chown/chmod exited with a non-zero exit code")
|
||||
|
||||
if get_chown_plugin_path() == True:
|
||||
chown_plugin_dir()
|
||||
|
||||
class PluginManager:
|
||||
def __init__(self, loop: AbstractEventLoop) -> None:
|
||||
self.loop = loop
|
||||
self.reinject: bool = True
|
||||
self.js_ctx_tab: Tab | None = None
|
||||
self.web_app = Application()
|
||||
self.web_app.middlewares.append(csrf_middleware)
|
||||
self.cors = aiohttp_cors.setup(self.web_app, defaults={
|
||||
"https://steamloopback.host": aiohttp_cors.ResourceOptions(
|
||||
expose_headers="*",
|
||||
allow_headers="*",
|
||||
allow_credentials=True
|
||||
)
|
||||
})
|
||||
self.ws = WSRouter(self.loop, self.web_app)
|
||||
self.plugin_loader = Loader(self, self.ws, plugin_path, self.loop, get_live_reload())
|
||||
self.settings = SettingsManager("loader", path.join(get_privileged_path(), "settings"))
|
||||
self.plugin_browser = PluginBrowser(plugin_path, self.plugin_loader.plugins, self.plugin_loader, self.settings)
|
||||
self.utilities = Utilities(self)
|
||||
self.updater = Updater(self)
|
||||
self.last_webhelper_exit: float = 0
|
||||
self.webhelper_crash_count: int = 0
|
||||
self.inject_fallback: bool = False
|
||||
|
||||
jinja_setup(self.web_app)
|
||||
|
||||
async def startup(_: Application):
|
||||
if self.settings.getSetting("cef_forward", False):
|
||||
self.loop.create_task(service_start(REMOTE_DEBUGGER_UNIT))
|
||||
else:
|
||||
self.loop.create_task(service_stop(REMOTE_DEBUGGER_UNIT))
|
||||
self.loop.create_task(self.loader_reinjector())
|
||||
self.loop.create_task(self.load_plugins())
|
||||
|
||||
self.web_app.on_startup.append(startup)
|
||||
self.web_app.on_shutdown.append(self.shutdown)
|
||||
|
||||
self.loop.set_exception_handler(self.exception_handler)
|
||||
self.web_app.add_routes([get("/auth/token", self.get_auth_token)])
|
||||
|
||||
for route in list(self.web_app.router.routes()):
|
||||
self.cors.add(route) # pyright: ignore [reportUnknownMemberType]
|
||||
self.web_app.add_routes([static("/static", path.join(path.dirname(__file__), 'static'))])
|
||||
|
||||
async def handle_crash(self):
|
||||
if not self.reinject:
|
||||
return
|
||||
new_time = time()
|
||||
if (new_time - self.last_webhelper_exit < 60):
|
||||
self.webhelper_crash_count += 1
|
||||
logger.warning(f"webhelper crashed within a minute from last crash! crash count: {self.webhelper_crash_count}")
|
||||
else:
|
||||
self.webhelper_crash_count = 0
|
||||
self.last_webhelper_exit = new_time
|
||||
|
||||
# should never happen
|
||||
if (self.webhelper_crash_count > 4):
|
||||
await self.updater.do_shutdown()
|
||||
# Give up
|
||||
exit(0)
|
||||
|
||||
async def shutdown(self, _: Application):
|
||||
try:
|
||||
logger.info(f"Shutting down...")
|
||||
logger.info("Disabling reload...")
|
||||
await self.plugin_loader.disable_reload()
|
||||
logger.info("Killing plugins...")
|
||||
await self.plugin_loader.shutdown_plugins()
|
||||
logger.info("Disconnecting from WS...")
|
||||
self.reinject = False
|
||||
await self.ws.disconnect()
|
||||
if self.js_ctx_tab:
|
||||
await self.js_ctx_tab.close_websocket()
|
||||
self.js_ctx_tab = None
|
||||
except:
|
||||
logger.info("Error during shutdown:\n" + format_exc())
|
||||
pass
|
||||
finally:
|
||||
logger.info("Cancelling tasks...")
|
||||
tasks = all_tasks()
|
||||
current = current_task()
|
||||
async def cancel_task(task: Task[Any]):
|
||||
name = task.get_coro().__qualname__
|
||||
logger.debug(f"Cancelling task {name}")
|
||||
try:
|
||||
task.cancel()
|
||||
try:
|
||||
await task
|
||||
except CancelledError:
|
||||
pass
|
||||
logger.debug(f"Task {name} finished")
|
||||
except:
|
||||
logger.warning(f"Failed to cancel task {name}:\n" + format_exc())
|
||||
pass
|
||||
if current:
|
||||
tasks.remove(current)
|
||||
await gather(*[cancel_task(task) for task in tasks])
|
||||
|
||||
logger.info("Shutdown finished.")
|
||||
|
||||
def exception_handler(self, loop: AbstractEventLoop, context: Dict[str, str]):
|
||||
if context["message"] == "Unclosed connection":
|
||||
return
|
||||
loop.default_exception_handler(context)
|
||||
|
||||
async def get_auth_token(self, request: Request):
|
||||
return Response(text=get_csrf_token())
|
||||
|
||||
async def load_plugins(self):
|
||||
# await self.wait_for_server()
|
||||
logger.debug("Loading plugins")
|
||||
await self.plugin_loader.import_plugins()
|
||||
if self.settings.getSetting("pluginOrder", None) == None:
|
||||
self.settings.setSetting("pluginOrder", list(self.plugin_loader.plugins.keys()))
|
||||
logger.debug("Did not find pluginOrder setting, set it to default")
|
||||
|
||||
async def loader_reinjector(self):
|
||||
while self.reinject:
|
||||
tab = None
|
||||
nf = False
|
||||
dc = False
|
||||
while not tab:
|
||||
if not self.reinject:
|
||||
return
|
||||
try:
|
||||
tab = await get_gamepadui_tab()
|
||||
except (client_exceptions.ClientConnectorError, client_exceptions.ServerDisconnectedError):
|
||||
if not dc:
|
||||
logger.debug("Couldn't connect to debugger, waiting...")
|
||||
dc = True
|
||||
pass
|
||||
except ValueError:
|
||||
if not nf:
|
||||
logger.debug("Couldn't find GamepadUI tab, waiting...")
|
||||
nf = True
|
||||
pass
|
||||
if not tab:
|
||||
await sleep(5)
|
||||
await tab.open_websocket()
|
||||
self.js_ctx_tab = tab
|
||||
await tab.enable()
|
||||
await self.inject_javascript(tab, True)
|
||||
try:
|
||||
async for msg in tab.listen_for_message():
|
||||
if msg.get("method", None) == "Page.domContentEventFired":
|
||||
if not await tab.has_global_var("deckyHasLoaded", False):
|
||||
await self.inject_javascript(tab)
|
||||
elif msg.get("method", None) == "Inspector.detached":
|
||||
if not self.reinject:
|
||||
return
|
||||
logger.info("CEF has requested that we detach.")
|
||||
await tab.close_websocket()
|
||||
self.js_ctx_tab = None
|
||||
break
|
||||
# If this is a forceful disconnect the loop will just stop without any failure message. In this case, injector.py will handle this for us so we don't need to close the socket.
|
||||
# This is because of https://github.com/aio-libs/aiohttp/blob/3ee7091b40a1bc58a8d7846e7878a77640e96996/aiohttp/client_ws.py#L321
|
||||
logger.info("CEF has disconnected...")
|
||||
await self.handle_crash()
|
||||
# At this point the loop starts again and we connect to the freshly started Steam client once it is ready.
|
||||
except Exception:
|
||||
if not self.reinject:
|
||||
return
|
||||
logger.error("Exception while reading page events " + format_exc())
|
||||
await tab.close_websocket()
|
||||
self.js_ctx_tab = None
|
||||
await self.handle_crash()
|
||||
pass
|
||||
# while True:
|
||||
# await sleep(5)
|
||||
# if not await tab.has_global_var("deckyHasLoaded", False):
|
||||
# logger.info("Plugin loader isn't present in Steam anymore, reinjecting...")
|
||||
# await self.inject_javascript(tab)
|
||||
|
||||
async def inject_javascript(self, tab: Tab, first: bool=False, request: Request|None=None):
|
||||
logger.info("Loading Decky frontend!")
|
||||
try:
|
||||
# if first:
|
||||
if ON_LINUX and await tab.has_global_var("deckyHasLoaded", False):
|
||||
await tab.close_websocket()
|
||||
self.js_ctx_tab = None
|
||||
await restart_webhelper()
|
||||
await sleep(1) # To give CEF enough time to close down the websocket
|
||||
return # We'll catch the next tab in the main loop
|
||||
await tab.evaluate_js(create_inject_script("index.js" if self.webhelper_crash_count < 3 else "fallback.js"), False, False, False)
|
||||
if self.webhelper_crash_count > 2:
|
||||
self.reinject = False
|
||||
await sleep(1)
|
||||
await self.updater.do_shutdown()
|
||||
except:
|
||||
logger.info("Failed to inject JavaScript into tab\n" + format_exc())
|
||||
pass
|
||||
|
||||
def run(self):
|
||||
run_app(self.web_app, host=get_server_host(), port=get_server_port(), loop=self.loop, access_log=None, handle_signals=True, shutdown_timeout=40)
|
||||
|
||||
def main():
|
||||
setproctitle(f"Decky Loader {get_loader_version()} ({getproctitle()})")
|
||||
setthreadtitle("Decky Loader")
|
||||
if ON_WINDOWS:
|
||||
# Fix windows/flask not recognising that .js means 'application/javascript'
|
||||
import mimetypes
|
||||
mimetypes.add_type('application/javascript', '.js')
|
||||
else:
|
||||
if get_effective_user_id() != 0:
|
||||
logger.warning(f"decky is running as an unprivileged user, this is not officially supported and may cause issues")
|
||||
|
||||
# Append the system and user python paths
|
||||
sys.path.extend(get_system_pythonpaths())
|
||||
|
||||
logger.info(f"Starting Decky version {get_loader_version()}")
|
||||
|
||||
loop = new_event_loop()
|
||||
set_event_loop(loop)
|
||||
PluginManager(loop).run()
|
||||
@@ -0,0 +1,225 @@
|
||||
"""
|
||||
This module exposes various constants and helpers useful for decky plugins.
|
||||
|
||||
* Plugin's settings and configurations should be stored under `DECKY_PLUGIN_SETTINGS_DIR`.
|
||||
* Plugin's runtime data should be stored under `DECKY_PLUGIN_RUNTIME_DIR`.
|
||||
* Plugin's persistent log files should be stored under `DECKY_PLUGIN_LOG_DIR`.
|
||||
|
||||
Avoid writing outside of `DECKY_HOME`, storing under the suggested paths is strongly recommended.
|
||||
|
||||
Some basic migration helpers are available: `migrate_any`, `migrate_settings`, `migrate_runtime`, `migrate_logs`.
|
||||
|
||||
A logging facility `logger` is available which writes to the recommended location.
|
||||
"""
|
||||
|
||||
__version__ = '1.0.0'
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import logging
|
||||
import time
|
||||
|
||||
from typing import Any
|
||||
|
||||
"""
|
||||
Constants
|
||||
"""
|
||||
|
||||
HOME: str = os.getenv("HOME", default="")
|
||||
"""
|
||||
The home directory of the effective user running the process.
|
||||
Environment variable: `HOME`.
|
||||
If `root` was specified in the plugin's flags it will be `/root` otherwise the user whose home decky resides in.
|
||||
e.g.: `/home/deck`
|
||||
"""
|
||||
|
||||
USER: str = os.getenv("USER", default="")
|
||||
"""
|
||||
The effective username running the process.
|
||||
Environment variable: `USER`.
|
||||
It would be `root` if `root` was specified in the plugin's flags otherwise the user whose home decky resides in.
|
||||
e.g.: `deck`
|
||||
"""
|
||||
|
||||
DECKY_VERSION: str = os.getenv("DECKY_VERSION", default="")
|
||||
"""
|
||||
The version of the decky loader.
|
||||
Environment variable: `DECKY_VERSION`.
|
||||
e.g.: `v2.5.0-pre1`
|
||||
"""
|
||||
|
||||
DECKY_USER: str = os.getenv("DECKY_USER", default="")
|
||||
"""
|
||||
The user whose home decky resides in.
|
||||
Environment variable: `DECKY_USER`.
|
||||
e.g.: `deck`
|
||||
"""
|
||||
|
||||
DECKY_USER_HOME: str = os.getenv("DECKY_USER_HOME", default="")
|
||||
"""
|
||||
The home of the user where decky resides in.
|
||||
Environment variable: `DECKY_USER_HOME`.
|
||||
e.g.: `/home/deck`
|
||||
"""
|
||||
|
||||
DECKY_HOME: str = os.getenv("DECKY_HOME", default="")
|
||||
"""
|
||||
The root of the decky folder.
|
||||
Environment variable: `DECKY_HOME`.
|
||||
e.g.: `/home/deck/homebrew`
|
||||
"""
|
||||
|
||||
DECKY_PLUGIN_SETTINGS_DIR: str = os.getenv(
|
||||
"DECKY_PLUGIN_SETTINGS_DIR", default="")
|
||||
"""
|
||||
The recommended path in which to store configuration files (created automatically).
|
||||
Environment variable: `DECKY_PLUGIN_SETTINGS_DIR`.
|
||||
e.g.: `/home/deck/homebrew/settings/decky-plugin-template`
|
||||
"""
|
||||
|
||||
DECKY_PLUGIN_RUNTIME_DIR: str = os.getenv(
|
||||
"DECKY_PLUGIN_RUNTIME_DIR", default="")
|
||||
"""
|
||||
The recommended path in which to store runtime data (created automatically).
|
||||
Environment variable: `DECKY_PLUGIN_RUNTIME_DIR`.
|
||||
e.g.: `/home/deck/homebrew/data/decky-plugin-template`
|
||||
"""
|
||||
|
||||
DECKY_PLUGIN_LOG_DIR: str = os.getenv("DECKY_PLUGIN_LOG_DIR", default="")
|
||||
"""
|
||||
The recommended path in which to store persistent logs (created automatically).
|
||||
Environment variable: `DECKY_PLUGIN_LOG_DIR`.
|
||||
e.g.: `/home/deck/homebrew/logs/decky-plugin-template`
|
||||
"""
|
||||
|
||||
DECKY_PLUGIN_DIR: str = os.getenv("DECKY_PLUGIN_DIR", default="")
|
||||
"""
|
||||
The root of the plugin's directory.
|
||||
Environment variable: `DECKY_PLUGIN_DIR`.
|
||||
e.g.: `/home/deck/homebrew/plugins/decky-plugin-template`
|
||||
"""
|
||||
|
||||
DECKY_PLUGIN_NAME: str = os.getenv("DECKY_PLUGIN_NAME", default="")
|
||||
"""
|
||||
The name of the plugin as specified in the 'plugin.json'.
|
||||
Environment variable: `DECKY_PLUGIN_NAME`.
|
||||
e.g.: `Example Plugin`
|
||||
"""
|
||||
|
||||
DECKY_PLUGIN_VERSION: str = os.getenv("DECKY_PLUGIN_VERSION", default="")
|
||||
"""
|
||||
The version of the plugin as specified in the 'package.json'.
|
||||
Environment variable: `DECKY_PLUGIN_VERSION`.
|
||||
e.g.: `0.0.1`
|
||||
"""
|
||||
|
||||
DECKY_PLUGIN_AUTHOR: str = os.getenv("DECKY_PLUGIN_AUTHOR", default="")
|
||||
"""
|
||||
The author of the plugin as specified in the 'plugin.json'.
|
||||
Environment variable: `DECKY_PLUGIN_AUTHOR`.
|
||||
e.g.: `John Doe`
|
||||
"""
|
||||
|
||||
__cur_time = time.strftime("%Y-%m-%d %H.%M.%S")
|
||||
DECKY_PLUGIN_LOG: str = os.path.join(DECKY_PLUGIN_LOG_DIR, f"{__cur_time}.log")
|
||||
"""
|
||||
The path to the plugin's main logfile.
|
||||
Environment variable: `DECKY_PLUGIN_LOG`.
|
||||
e.g.: `/home/deck/homebrew/logs/decky-plugin-template/plugin.log`
|
||||
"""
|
||||
|
||||
"""
|
||||
Migration helpers
|
||||
"""
|
||||
|
||||
|
||||
def migrate_any(target_dir: str, *files_or_directories: str) -> dict[str, str]:
|
||||
"""
|
||||
Migrate files and directories to a new location and remove old locations.
|
||||
Specified files will be migrated to `target_dir`.
|
||||
Specified directories will have their contents recursively migrated to `target_dir`.
|
||||
|
||||
Returns the mapping of old -> new location.
|
||||
"""
|
||||
file_map: dict[str, str] = {}
|
||||
for f in files_or_directories:
|
||||
if not os.path.exists(f):
|
||||
file_map[f] = ""
|
||||
continue
|
||||
if os.path.isdir(f):
|
||||
src_dir = f
|
||||
src_file = "."
|
||||
file_map[f] = target_dir
|
||||
else:
|
||||
src_dir = os.path.dirname(f)
|
||||
src_file = os.path.basename(f)
|
||||
file_map[f] = os.path.join(target_dir, src_file)
|
||||
subprocess.run(["sh", "-c", "mkdir -p \"$3\"; tar -cf - -C \"$1\" \"$2\" | tar -xf - -C \"$3\" && rm -rf \"$4\"",
|
||||
"_", src_dir, src_file, target_dir, f])
|
||||
return file_map
|
||||
|
||||
|
||||
def migrate_settings(*files_or_directories: str) -> dict[str, str]:
|
||||
"""
|
||||
Migrate files and directories relating to plugin settings to the recommended location and remove old locations.
|
||||
Specified files will be migrated to `DECKY_PLUGIN_SETTINGS_DIR`.
|
||||
Specified directories will have their contents recursively migrated to `DECKY_PLUGIN_SETTINGS_DIR`.
|
||||
|
||||
Returns the mapping of old -> new location.
|
||||
"""
|
||||
return migrate_any(DECKY_PLUGIN_SETTINGS_DIR, *files_or_directories)
|
||||
|
||||
|
||||
def migrate_runtime(*files_or_directories: str) -> dict[str, str]:
|
||||
"""
|
||||
Migrate files and directories relating to plugin runtime data to the recommended location and remove old locations
|
||||
Specified files will be migrated to `DECKY_PLUGIN_RUNTIME_DIR`.
|
||||
Specified directories will have their contents recursively migrated to `DECKY_PLUGIN_RUNTIME_DIR`.
|
||||
|
||||
Returns the mapping of old -> new location.
|
||||
"""
|
||||
return migrate_any(DECKY_PLUGIN_RUNTIME_DIR, *files_or_directories)
|
||||
|
||||
|
||||
def migrate_logs(*files_or_directories: str) -> dict[str, str]:
|
||||
"""
|
||||
Migrate files and directories relating to plugin logs to the recommended location and remove old locations.
|
||||
Specified files will be migrated to `DECKY_PLUGIN_LOG_DIR`.
|
||||
Specified directories will have their contents recursively migrated to `DECKY_PLUGIN_LOG_DIR`.
|
||||
|
||||
Returns the mapping of old -> new location.
|
||||
"""
|
||||
return migrate_any(DECKY_PLUGIN_LOG_DIR, *files_or_directories)
|
||||
|
||||
|
||||
"""
|
||||
Logging
|
||||
"""
|
||||
|
||||
try:
|
||||
for x in [entry.name for entry in sorted(os.scandir(DECKY_PLUGIN_LOG_DIR),key=lambda x: x.stat().st_mtime, reverse=True) if entry.name.endswith(".log")][4:]:
|
||||
os.unlink(os.path.join(DECKY_PLUGIN_LOG_DIR, x))
|
||||
except Exception as e:
|
||||
print(f"Failed to delete old logs: {str(e)}")
|
||||
|
||||
logging.basicConfig(filename=DECKY_PLUGIN_LOG,
|
||||
format='[%(asctime)s][%(levelname)s]: %(message)s',
|
||||
force=True)
|
||||
logger: logging.Logger = logging.getLogger()
|
||||
# Also log to stdout
|
||||
logger.addHandler(logging.StreamHandler())
|
||||
"""The main plugin logger writing to `DECKY_PLUGIN_LOG`."""
|
||||
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
"""
|
||||
Event handling
|
||||
"""
|
||||
# This is overriden with an actual implementation before being passed to any plugins
|
||||
# in ../sandboxed_plugin.py 's initialize function
|
||||
async def emit(event: str, *args: Any) -> None:
|
||||
"""
|
||||
Triggers all event listeners in the frontend waiting for `event`, passing the remaining `*args` as the arguments to each listener function.
|
||||
(Event listeners are set up in the frontend via the `addEventListener` function from `@decky/api`)
|
||||
"""
|
||||
pass
|
||||
@@ -0,0 +1,185 @@
|
||||
"""
|
||||
This module exposes various constants and helpers useful for decky plugins.
|
||||
|
||||
* Plugin's settings and configurations should be stored under `DECKY_PLUGIN_SETTINGS_DIR`.
|
||||
* Plugin's runtime data should be stored under `DECKY_PLUGIN_RUNTIME_DIR`.
|
||||
* Plugin's persistent log files should be stored under `DECKY_PLUGIN_LOG_DIR`.
|
||||
|
||||
Avoid writing outside of `DECKY_HOME`, storing under the suggested paths is strongly recommended.
|
||||
|
||||
Some basic migration helpers are available: `migrate_any`, `migrate_settings`, `migrate_runtime`, `migrate_logs`.
|
||||
|
||||
A logging facility `logger` is available which writes to the recommended location.
|
||||
"""
|
||||
|
||||
__version__ = '1.0.0'
|
||||
|
||||
import logging
|
||||
|
||||
from typing import Any
|
||||
|
||||
"""
|
||||
Constants
|
||||
"""
|
||||
|
||||
HOME: str
|
||||
"""
|
||||
The home directory of the effective user running the process.
|
||||
Environment variable: `HOME`.
|
||||
If `root` was specified in the plugin's flags it will be `/root` otherwise the user whose home decky resides in.
|
||||
e.g.: `/home/deck`
|
||||
"""
|
||||
|
||||
USER: str
|
||||
"""
|
||||
The effective username running the process.
|
||||
Environment variable: `USER`.
|
||||
It would be `root` if `root` was specified in the plugin's flags otherwise the user whose home decky resides in.
|
||||
e.g.: `deck`
|
||||
"""
|
||||
|
||||
DECKY_VERSION: str
|
||||
"""
|
||||
The version of the decky loader.
|
||||
Environment variable: `DECKY_VERSION`.
|
||||
e.g.: `v2.5.0-pre1`
|
||||
"""
|
||||
|
||||
DECKY_USER: str
|
||||
"""
|
||||
The user whose home decky resides in.
|
||||
Environment variable: `DECKY_USER`.
|
||||
e.g.: `deck`
|
||||
"""
|
||||
|
||||
DECKY_USER_HOME: str
|
||||
"""
|
||||
The home of the user where decky resides in.
|
||||
Environment variable: `DECKY_USER_HOME`.
|
||||
e.g.: `/home/deck`
|
||||
"""
|
||||
|
||||
DECKY_HOME: str
|
||||
"""
|
||||
The root of the decky folder.
|
||||
Environment variable: `DECKY_HOME`.
|
||||
e.g.: `/home/deck/homebrew`
|
||||
"""
|
||||
|
||||
DECKY_PLUGIN_SETTINGS_DIR: str
|
||||
"""
|
||||
The recommended path in which to store configuration files (created automatically).
|
||||
Environment variable: `DECKY_PLUGIN_SETTINGS_DIR`.
|
||||
e.g.: `/home/deck/homebrew/settings/decky-plugin-template`
|
||||
"""
|
||||
|
||||
DECKY_PLUGIN_RUNTIME_DIR: str
|
||||
"""
|
||||
The recommended path in which to store runtime data (created automatically).
|
||||
Environment variable: `DECKY_PLUGIN_RUNTIME_DIR`.
|
||||
e.g.: `/home/deck/homebrew/data/decky-plugin-template`
|
||||
"""
|
||||
|
||||
DECKY_PLUGIN_LOG_DIR: str
|
||||
"""
|
||||
The recommended path in which to store persistent logs (created automatically).
|
||||
Environment variable: `DECKY_PLUGIN_LOG_DIR`.
|
||||
e.g.: `/home/deck/homebrew/logs/decky-plugin-template`
|
||||
"""
|
||||
|
||||
DECKY_PLUGIN_DIR: str
|
||||
"""
|
||||
The root of the plugin's directory.
|
||||
Environment variable: `DECKY_PLUGIN_DIR`.
|
||||
e.g.: `/home/deck/homebrew/plugins/decky-plugin-template`
|
||||
"""
|
||||
|
||||
DECKY_PLUGIN_NAME: str
|
||||
"""
|
||||
The name of the plugin as specified in the 'plugin.json'.
|
||||
Environment variable: `DECKY_PLUGIN_NAME`.
|
||||
e.g.: `Example Plugin`
|
||||
"""
|
||||
|
||||
DECKY_PLUGIN_VERSION: str
|
||||
"""
|
||||
The version of the plugin as specified in the 'package.json'.
|
||||
Environment variable: `DECKY_PLUGIN_VERSION`.
|
||||
e.g.: `0.0.1`
|
||||
"""
|
||||
|
||||
DECKY_PLUGIN_AUTHOR: str
|
||||
"""
|
||||
The author of the plugin as specified in the 'plugin.json'.
|
||||
Environment variable: `DECKY_PLUGIN_AUTHOR`.
|
||||
e.g.: `John Doe`
|
||||
"""
|
||||
|
||||
DECKY_PLUGIN_LOG: str
|
||||
"""
|
||||
The path to the plugin's main logfile.
|
||||
Environment variable: `DECKY_PLUGIN_LOG`.
|
||||
e.g.: `/home/deck/homebrew/logs/decky-plugin-template/plugin.log`
|
||||
"""
|
||||
|
||||
"""
|
||||
Migration helpers
|
||||
"""
|
||||
|
||||
|
||||
def migrate_any(target_dir: str, *files_or_directories: str) -> dict[str, str]:
|
||||
"""
|
||||
Migrate files and directories to a new location and remove old locations.
|
||||
Specified files will be migrated to `target_dir`.
|
||||
Specified directories will have their contents recursively migrated to `target_dir`.
|
||||
|
||||
Returns the mapping of old -> new location.
|
||||
"""
|
||||
|
||||
|
||||
def migrate_settings(*files_or_directories: str) -> dict[str, str]:
|
||||
"""
|
||||
Migrate files and directories relating to plugin settings to the recommended location and remove old locations.
|
||||
Specified files will be migrated to `DECKY_PLUGIN_SETTINGS_DIR`.
|
||||
Specified directories will have their contents recursively migrated to `DECKY_PLUGIN_SETTINGS_DIR`.
|
||||
|
||||
Returns the mapping of old -> new location.
|
||||
"""
|
||||
|
||||
|
||||
def migrate_runtime(*files_or_directories: str) -> dict[str, str]:
|
||||
"""
|
||||
Migrate files and directories relating to plugin runtime data to the recommended location and remove old locations
|
||||
Specified files will be migrated to `DECKY_PLUGIN_RUNTIME_DIR`.
|
||||
Specified directories will have their contents recursively migrated to `DECKY_PLUGIN_RUNTIME_DIR`.
|
||||
|
||||
Returns the mapping of old -> new location.
|
||||
"""
|
||||
|
||||
|
||||
def migrate_logs(*files_or_directories: str) -> dict[str, str]:
|
||||
"""
|
||||
Migrate files and directories relating to plugin logs to the recommended location and remove old locations.
|
||||
Specified files will be migrated to `DECKY_PLUGIN_LOG_DIR`.
|
||||
Specified directories will have their contents recursively migrated to `DECKY_PLUGIN_LOG_DIR`.
|
||||
|
||||
Returns the mapping of old -> new location.
|
||||
"""
|
||||
|
||||
|
||||
"""
|
||||
Logging
|
||||
"""
|
||||
|
||||
logger: logging.Logger
|
||||
"""The main plugin logger writing to `DECKY_PLUGIN_LOG`."""
|
||||
|
||||
"""
|
||||
Event handling
|
||||
"""
|
||||
|
||||
async def emit(event: str, *args: Any) -> None:
|
||||
"""
|
||||
Triggers all event listeners in the frontend waiting for `event`, passing the remaining `*args` as the arguments to each listener function.
|
||||
(Event listeners are set up in the frontend via the `addEventListener` function from `@decky/api`)
|
||||
"""
|
||||
@@ -0,0 +1,36 @@
|
||||
from typing import Any, TypedDict
|
||||
from enum import IntEnum
|
||||
from uuid import uuid4
|
||||
from asyncio import Event
|
||||
|
||||
class SocketMessageType(IntEnum):
|
||||
CALL = 0
|
||||
RESPONSE = 1
|
||||
EVENT = 2
|
||||
|
||||
class SocketResponseDict(TypedDict):
|
||||
type: SocketMessageType
|
||||
id: str
|
||||
success: bool
|
||||
res: Any
|
||||
|
||||
class MethodCallResponse:
|
||||
def __init__(self, success: bool, result: Any) -> None:
|
||||
self.success = success
|
||||
self.result = result
|
||||
|
||||
class MethodCallRequest:
|
||||
def __init__(self) -> None:
|
||||
self.id = str(uuid4())
|
||||
self.event = Event()
|
||||
self.response: MethodCallResponse
|
||||
|
||||
def set_result(self, dc: SocketResponseDict):
|
||||
self.response = MethodCallResponse(dc["success"], dc["res"])
|
||||
self.event.set()
|
||||
|
||||
async def wait_for_result(self):
|
||||
await self.event.wait()
|
||||
if not self.response.success:
|
||||
raise Exception(self.response.result)
|
||||
return self.response.result
|
||||
@@ -0,0 +1,178 @@
|
||||
from asyncio import CancelledError, Task, create_task, sleep, wait
|
||||
from json import dumps, load, loads
|
||||
from logging import getLogger
|
||||
from os import path
|
||||
from multiprocessing import Process
|
||||
from time import time
|
||||
from traceback import format_exc
|
||||
|
||||
from .sandboxed_plugin import SandboxedPlugin
|
||||
from .messages import MethodCallRequest, SocketMessageType
|
||||
from ..enums import PluginLoadType, UserType
|
||||
from ..localplatform.localplatform import file_owner, chown, chmod, get_chown_plugin_path
|
||||
from ..localplatform.localsocket import LocalSocket
|
||||
from ..helpers import get_homebrew_path, mkdir_as_user
|
||||
|
||||
from typing import Any, Callable, Coroutine, Dict, List
|
||||
|
||||
EmittedEventCallbackType = Callable[[str, Any], Coroutine[Any, Any, Any]]
|
||||
|
||||
class PluginWrapper:
|
||||
def __init__(self, file: str, plugin_directory: str, plugin_path: str, emit_callback: EmittedEventCallbackType) -> None:
|
||||
self.file = file
|
||||
self.plugin_path = plugin_path
|
||||
self.plugin_directory = plugin_directory
|
||||
|
||||
self.version = None
|
||||
|
||||
self.load_type = PluginLoadType.LEGACY_EVAL_IIFE.value
|
||||
|
||||
plugin_dir_path = path.join(plugin_path, plugin_directory)
|
||||
plugin_json_path = path.join(plugin_dir_path, "plugin.json")
|
||||
|
||||
json = load(open(plugin_json_path, "r", encoding="utf-8"))
|
||||
if path.isfile(path.join(plugin_dir_path, "package.json")):
|
||||
package_json = load(open(path.join(plugin_dir_path, "package.json"), "r", encoding="utf-8"))
|
||||
self.version = package_json["version"]
|
||||
if ("type" in package_json and package_json["type"] == "module"):
|
||||
self.load_type = PluginLoadType.ESMODULE_V1.value
|
||||
|
||||
self.name = json["name"]
|
||||
self.author = json["author"]
|
||||
self.flags = json["flags"]
|
||||
self.api_version = json["api_version"] if "api_version" in json else 0
|
||||
self.disabled = False
|
||||
|
||||
self.passive = not path.isfile(self.file)
|
||||
|
||||
self.log = getLogger("plugin")
|
||||
|
||||
if get_chown_plugin_path():
|
||||
# ensure plugin folder ownership
|
||||
if file_owner(plugin_dir_path) != UserType.EFFECTIVE_USER:
|
||||
chown(plugin_dir_path, UserType.EFFECTIVE_USER if "root" in self.flags else UserType.HOST_USER, True)
|
||||
chown(plugin_dir_path, UserType.EFFECTIVE_USER, False)
|
||||
chmod(plugin_dir_path, 755, True)
|
||||
# fix plugin.json permissions
|
||||
if file_owner(plugin_json_path) != UserType.EFFECTIVE_USER:
|
||||
chown(plugin_json_path, UserType.EFFECTIVE_USER, False)
|
||||
chmod(plugin_json_path, 755, False)
|
||||
|
||||
self.sandboxed_plugin = SandboxedPlugin(self.name, self.passive, self.flags, self.file, self.plugin_directory, self.plugin_path, self.version, self.author, self.api_version)
|
||||
self.proc: Process | None = None
|
||||
self._socket = LocalSocket()
|
||||
self._listener_task: Task[Any]
|
||||
self._method_call_requests: Dict[str, MethodCallRequest] = {}
|
||||
|
||||
self.emitted_event_callback: EmittedEventCallbackType = emit_callback
|
||||
|
||||
# TODO enable this after websocket release
|
||||
self.legacy_method_warning = False
|
||||
|
||||
home = get_homebrew_path()
|
||||
mkdir_as_user(path.join(home, "settings", self.plugin_directory))
|
||||
# TODO maybe dont chown this?
|
||||
mkdir_as_user(path.join(home, "data"))
|
||||
mkdir_as_user(path.join(home, "data", self.plugin_directory))
|
||||
# TODO maybe dont chown this?
|
||||
mkdir_as_user(path.join(home, "logs"))
|
||||
mkdir_as_user(path.join(home, "logs", self.plugin_directory))
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
async def _response_listener(self):
|
||||
while self._socket.active:
|
||||
try:
|
||||
line = await self._socket.read_single_line()
|
||||
if line != None:
|
||||
res = loads(line)
|
||||
if res["type"] == SocketMessageType.EVENT.value:
|
||||
create_task(self.emitted_event_callback(res["event"], res["args"]))
|
||||
elif res["type"] == SocketMessageType.RESPONSE.value:
|
||||
self._method_call_requests.pop(res["id"]).set_result(res)
|
||||
except CancelledError:
|
||||
self.log.info(f"Stopping response listener for {self.name}")
|
||||
await self._socket.close_socket_connection()
|
||||
raise
|
||||
except:
|
||||
pass
|
||||
|
||||
async def execute_legacy_method(self, method_name: str, kwargs: Dict[Any, Any]):
|
||||
if not self.legacy_method_warning:
|
||||
self.legacy_method_warning = True
|
||||
self.log.warning(f"Plugin {self.name} is using legacy method calls. This will be removed in a future release.")
|
||||
if self.passive:
|
||||
raise RuntimeError("This plugin is passive (aka does not implement main.py)")
|
||||
|
||||
request = MethodCallRequest()
|
||||
await self._socket.get_socket_connection()
|
||||
await self._socket.write_single_line(dumps({ "type": SocketMessageType.CALL, "method": method_name, "args": kwargs, "id": request.id, "legacy": True }, ensure_ascii=False))
|
||||
self._method_call_requests[request.id] = request
|
||||
|
||||
return await request.wait_for_result()
|
||||
|
||||
async def execute_method(self, method_name: str, *args: List[Any]):
|
||||
if self.passive:
|
||||
raise RuntimeError("This plugin is passive (aka does not implement main.py)")
|
||||
|
||||
request = MethodCallRequest()
|
||||
await self._socket.get_socket_connection()
|
||||
await self._socket.write_single_line(dumps({ "type": SocketMessageType.CALL, "method": method_name, "args": args, "id": request.id }, ensure_ascii=False))
|
||||
self._method_call_requests[request.id] = request
|
||||
|
||||
return await request.wait_for_result()
|
||||
|
||||
def start(self):
|
||||
if self.passive:
|
||||
return self
|
||||
self.proc = Process(target=self.sandboxed_plugin.initialize, args=[self._socket])
|
||||
self.proc.start()
|
||||
self._listener_task = create_task(self._response_listener())
|
||||
return self
|
||||
|
||||
async def stop(self, uninstall: bool = False):
|
||||
try:
|
||||
start_time = time()
|
||||
if self.passive:
|
||||
return
|
||||
self.log.info(f"Shutting down {self.name}")
|
||||
|
||||
pending: set[Task[None]] | None = None;
|
||||
|
||||
if uninstall:
|
||||
_, pending = await wait([
|
||||
create_task(self._socket.write_single_line(dumps({ "uninstall": uninstall }, ensure_ascii=False)))
|
||||
], timeout=1)
|
||||
|
||||
self.terminate() # the plugin process will handle SIGTERM and shut down cleanly without a socket message
|
||||
|
||||
if hasattr(self, "_listener_task"):
|
||||
self._listener_task.cancel()
|
||||
|
||||
await self.kill_if_still_running()
|
||||
|
||||
if pending:
|
||||
for pending_task in pending:
|
||||
pending_task.cancel()
|
||||
|
||||
self.log.info(f"Plugin {self.name} has been stopped in {time() - start_time:.1f}s")
|
||||
except Exception as e:
|
||||
self.log.error(f"Error during shutdown for plugin {self.name}: {str(e)}\n{format_exc()}")
|
||||
|
||||
async def kill_if_still_running(self):
|
||||
start_time = time()
|
||||
while self.proc and self.proc.is_alive():
|
||||
elapsed_time = time() - start_time
|
||||
if elapsed_time >= 5:
|
||||
self.log.warning(f"Plugin {self.name} still alive 5 seconds after stop request! Sending SIGKILL!")
|
||||
self.terminate(True)
|
||||
await sleep(0.1)
|
||||
|
||||
|
||||
def terminate(self, kill: bool = False):
|
||||
if self.proc and self.proc.is_alive():
|
||||
if kill:
|
||||
self.proc.kill()
|
||||
else:
|
||||
self.proc.terminate()
|
||||
@@ -0,0 +1,208 @@
|
||||
import sys
|
||||
from os import path, environ
|
||||
from importlib.util import module_from_spec, spec_from_file_location
|
||||
from json import dumps, loads
|
||||
from logging import getLogger
|
||||
from traceback import format_exc
|
||||
from asyncio import (ensure_future, get_event_loop, new_event_loop,
|
||||
set_event_loop)
|
||||
from signal import SIGINT, SIGTERM
|
||||
from setproctitle import setproctitle, setthreadtitle
|
||||
|
||||
from .messages import SocketResponseDict, SocketMessageType
|
||||
from ..localplatform.localsocket import LocalSocket
|
||||
from ..localplatform.localplatform import setgid, setuid, get_username, get_home_path, ON_LINUX
|
||||
from ..enums import UserType
|
||||
from .. import helpers
|
||||
from .. import settings # pyright: ignore [reportUnusedImport]
|
||||
|
||||
from typing import List, TypeVar, Any
|
||||
|
||||
DataType = TypeVar("DataType")
|
||||
|
||||
class SandboxedPlugin:
|
||||
def __init__(self,
|
||||
name: str,
|
||||
passive: bool,
|
||||
flags: List[str],
|
||||
file: str,
|
||||
plugin_directory: str,
|
||||
plugin_path: str,
|
||||
version: str|None,
|
||||
author: str,
|
||||
api_version: int) -> None:
|
||||
self.name = name
|
||||
self.passive = passive
|
||||
self.flags = flags
|
||||
self.file = file
|
||||
self.plugin_path = plugin_path
|
||||
self.plugin_directory = plugin_directory
|
||||
self.version = version
|
||||
self.author = author
|
||||
self.api_version = api_version
|
||||
self.shutdown_running = False
|
||||
self.uninstalling = False
|
||||
|
||||
self.log = getLogger("sandboxed_plugin")
|
||||
|
||||
def initialize(self, socket: LocalSocket):
|
||||
self._socket = socket
|
||||
|
||||
try:
|
||||
setproctitle(f"{self.name} ({self.file})")
|
||||
setthreadtitle(self.name)
|
||||
|
||||
loop = new_event_loop()
|
||||
set_event_loop(loop)
|
||||
# When running Decky manually in a terminal, ctrl-c will trigger this, so we have to handle it properly
|
||||
if ON_LINUX:
|
||||
loop.add_signal_handler(SIGINT, lambda: ensure_future(self.shutdown()))
|
||||
loop.add_signal_handler(SIGTERM, lambda: ensure_future(self.shutdown()))
|
||||
|
||||
if self.passive:
|
||||
return
|
||||
|
||||
setgid(UserType.EFFECTIVE_USER if "root" in self.flags else UserType.HOST_USER)
|
||||
setuid(UserType.EFFECTIVE_USER if "root" in self.flags else UserType.HOST_USER)
|
||||
# export a bunch of environment variables to help plugin developers
|
||||
environ["HOME"] = get_home_path(UserType.EFFECTIVE_USER if "root" in self.flags else UserType.HOST_USER)
|
||||
environ["USER"] = "root" if "root" in self.flags else get_username()
|
||||
environ["DECKY_VERSION"] = helpers.get_loader_version()
|
||||
environ["DECKY_USER"] = get_username()
|
||||
environ["DECKY_USER_HOME"] = helpers.get_home_path()
|
||||
environ["DECKY_HOME"] = helpers.get_homebrew_path()
|
||||
environ["DECKY_PLUGIN_SETTINGS_DIR"] = path.join(environ["DECKY_HOME"], "settings", self.plugin_directory)
|
||||
environ["DECKY_PLUGIN_RUNTIME_DIR"] = path.join(environ["DECKY_HOME"], "data", self.plugin_directory)
|
||||
environ["DECKY_PLUGIN_LOG_DIR"] = path.join(environ["DECKY_HOME"], "logs", self.plugin_directory)
|
||||
environ["DECKY_PLUGIN_DIR"] = path.join(self.plugin_path, self.plugin_directory)
|
||||
environ["DECKY_PLUGIN_NAME"] = self.name
|
||||
if self.version:
|
||||
environ["DECKY_PLUGIN_VERSION"] = self.version
|
||||
environ["DECKY_PLUGIN_AUTHOR"] = self.author
|
||||
|
||||
# append the plugin's `py_modules` to the recognized python paths
|
||||
sys.path.append(path.join(environ["DECKY_PLUGIN_DIR"], "py_modules"))
|
||||
|
||||
#TODO: FIX IN A LESS CURSED WAY
|
||||
keys = [key for key in sys.modules if key.startswith("decky_loader.")]
|
||||
for key in keys:
|
||||
sys.modules[key.replace("decky_loader.", "")] = sys.modules[key]
|
||||
|
||||
from .imports import decky
|
||||
async def emit(event: str, *args: Any) -> None:
|
||||
await self._socket.write_single_line_server(dumps({
|
||||
"type": SocketMessageType.EVENT,
|
||||
"event": event,
|
||||
"args": args
|
||||
}))
|
||||
# copy the docstring over so we don't have to duplicate it
|
||||
emit.__doc__ = decky.emit.__doc__
|
||||
decky.emit = emit
|
||||
sys.modules["decky"] = decky
|
||||
# provided for compatibility
|
||||
sys.modules["decky_plugin"] = decky
|
||||
|
||||
spec = spec_from_file_location("_", self.file)
|
||||
assert spec is not None
|
||||
module = module_from_spec(spec)
|
||||
assert spec.loader is not None
|
||||
spec.loader.exec_module(module)
|
||||
# TODO fix self weirdness once plugin.json versioning is done. need this before WS release!
|
||||
if self.api_version > 0:
|
||||
self.Plugin = module.Plugin()
|
||||
else:
|
||||
self.Plugin = module.Plugin
|
||||
|
||||
if hasattr(self.Plugin, "_migration"):
|
||||
if self.api_version > 0:
|
||||
get_event_loop().run_until_complete(self.Plugin._migration())
|
||||
else:
|
||||
get_event_loop().run_until_complete(self.Plugin._migration(self.Plugin))
|
||||
if hasattr(self.Plugin, "_main"):
|
||||
if self.api_version > 0:
|
||||
get_event_loop().create_task(self.Plugin._main())
|
||||
else:
|
||||
get_event_loop().create_task(self.Plugin._main(self.Plugin))
|
||||
get_event_loop().create_task(socket.setup_server(self.on_new_message))
|
||||
except:
|
||||
self.log.error("Failed to start " + self.name + "!\n" + format_exc())
|
||||
sys.exit(0)
|
||||
try:
|
||||
get_event_loop().run_forever()
|
||||
except SystemExit:
|
||||
pass
|
||||
except:
|
||||
self.log.error("Loop exited for " + self.name + "!\n" + format_exc())
|
||||
finally:
|
||||
get_event_loop().close()
|
||||
|
||||
async def _unload(self):
|
||||
try:
|
||||
self.log.info("Attempting to unload with plugin " + self.name + "'s \"_unload\" function.\n")
|
||||
if hasattr(self.Plugin, "_unload"):
|
||||
if self.api_version > 0:
|
||||
await self.Plugin._unload()
|
||||
else:
|
||||
await self.Plugin._unload(self.Plugin)
|
||||
self.log.info("Unloaded " + self.name + "\n")
|
||||
else:
|
||||
self.log.info("Could not find \"_unload\" in " + self.name + "'s main.py" + "\n")
|
||||
except:
|
||||
self.log.error("Failed to unload " + self.name + "!\n" + format_exc())
|
||||
pass
|
||||
|
||||
async def _uninstall(self):
|
||||
try:
|
||||
self.log.info("Attempting to uninstall with plugin " + self.name + "'s \"_uninstall\" function.\n")
|
||||
if hasattr(self.Plugin, "_uninstall"):
|
||||
if self.api_version > 0:
|
||||
await self.Plugin._uninstall()
|
||||
else:
|
||||
await self.Plugin._uninstall(self.Plugin)
|
||||
self.log.info("Uninstalled " + self.name + "\n")
|
||||
else:
|
||||
self.log.info("Could not find \"_uninstall\" in " + self.name + "'s main.py" + "\n")
|
||||
except:
|
||||
self.log.error("Failed to uninstall " + self.name + "!\n" + format_exc())
|
||||
pass
|
||||
|
||||
async def shutdown(self):
|
||||
if not self.shutdown_running:
|
||||
self.shutdown_running = True
|
||||
self.log.info(f"Calling Loader unload function for {self.name}.")
|
||||
await self._unload()
|
||||
|
||||
if self.uninstalling:
|
||||
self.log.info("Calling Loader uninstall function.")
|
||||
await self._uninstall()
|
||||
|
||||
self.log.debug("Stopping event loop")
|
||||
|
||||
loop = get_event_loop()
|
||||
loop.call_soon_threadsafe(loop.stop)
|
||||
sys.exit(0)
|
||||
|
||||
async def on_new_message(self, message : str) -> str|None:
|
||||
data = loads(message)
|
||||
|
||||
if "uninstall" in data:
|
||||
self.uninstalling = data.get("uninstall")
|
||||
return
|
||||
|
||||
d: SocketResponseDict = {"type": SocketMessageType.RESPONSE, "res": None, "success": True, "id": data["id"]}
|
||||
try:
|
||||
if data.get("legacy"):
|
||||
if self.api_version > 0:
|
||||
raise Exception("Legacy methods may not be used on api_version > 0")
|
||||
# Legacy kwargs
|
||||
d["res"] = await getattr(self.Plugin, data["method"])(self.Plugin, **data["args"])
|
||||
else:
|
||||
if self.api_version < 1 :
|
||||
raise Exception("api_version 1 or newer is required to call methods with index-based arguments")
|
||||
# New args
|
||||
d["res"] = await getattr(self.Plugin, data["method"])(*data["args"])
|
||||
except Exception as e:
|
||||
d["res"] = str(e)
|
||||
d["success"] = False
|
||||
finally:
|
||||
return dumps(d, ensure_ascii=False)
|
||||
@@ -0,0 +1,60 @@
|
||||
from json import dump, load
|
||||
from os import mkdir, path, listdir, rename
|
||||
from typing import Any, Dict
|
||||
from .localplatform.localplatform import chown, file_owner, get_chown_plugin_path
|
||||
from .enums import UserType
|
||||
|
||||
from .helpers import get_homebrew_path
|
||||
|
||||
|
||||
class SettingsManager:
|
||||
def __init__(self, name: str, settings_directory: str | None = None) -> None:
|
||||
wrong_dir = get_homebrew_path()
|
||||
if settings_directory == None:
|
||||
settings_directory = path.join(wrong_dir, "settings")
|
||||
|
||||
self.path = path.join(settings_directory, name + ".json")
|
||||
|
||||
#Create the folder with the correct permission
|
||||
if not path.exists(settings_directory):
|
||||
mkdir(settings_directory)
|
||||
|
||||
#Copy all old settings file in the root directory to the correct folder
|
||||
for file in listdir(wrong_dir):
|
||||
if file.endswith(".json"):
|
||||
rename(path.join(wrong_dir,file),
|
||||
path.join(settings_directory, file))
|
||||
self.path = path.join(settings_directory, name + ".json")
|
||||
|
||||
|
||||
#If the owner of the settings directory is not the user, then set it as the user:
|
||||
expected_user = UserType.HOST_USER if get_chown_plugin_path() else UserType.EFFECTIVE_USER
|
||||
if file_owner(settings_directory) != expected_user:
|
||||
chown(settings_directory, expected_user, False)
|
||||
|
||||
self.settings: Dict[str, Any] = {}
|
||||
|
||||
try:
|
||||
open(self.path, "x", encoding="utf-8")
|
||||
except FileExistsError as _:
|
||||
self.read()
|
||||
pass
|
||||
|
||||
def read(self):
|
||||
try:
|
||||
with open(self.path, "r", encoding="utf-8") as file:
|
||||
self.settings = load(file)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
pass
|
||||
|
||||
def commit(self):
|
||||
with open(self.path, "w+", encoding="utf-8") as file:
|
||||
dump(self.settings, file, indent=4, ensure_ascii=False)
|
||||
|
||||
def getSetting(self, key: str, default: Any = None) -> Any:
|
||||
return self.settings.get(key, default)
|
||||
|
||||
def setSetting(self, key: str, value: Any) -> Any:
|
||||
self.settings[key] = value
|
||||
self.commit()
|
||||
@@ -0,0 +1,293 @@
|
||||
from __future__ import annotations
|
||||
from asyncio import sleep
|
||||
from logging import getLogger
|
||||
import os
|
||||
from os import getcwd, path, remove
|
||||
from typing import TYPE_CHECKING, List, TypedDict
|
||||
if TYPE_CHECKING:
|
||||
from .main import PluginManager
|
||||
from .localplatform.localplatform import chmod, service_restart, service_stop, ON_LINUX, ON_WINDOWS, get_keep_systemd_service, get_selinux
|
||||
import shutil
|
||||
from typing import List, TYPE_CHECKING, TypedDict
|
||||
import zipfile
|
||||
|
||||
from aiohttp import ClientSession
|
||||
|
||||
from . import helpers
|
||||
from .settings import SettingsManager
|
||||
if TYPE_CHECKING:
|
||||
from .main import PluginManager
|
||||
|
||||
|
||||
logger = getLogger("Updater")
|
||||
|
||||
class RemoteVerAsset(TypedDict):
|
||||
name: str
|
||||
size: int
|
||||
browser_download_url: str
|
||||
class RemoteVer(TypedDict):
|
||||
tag_name: str
|
||||
prerelease: bool
|
||||
assets: List[RemoteVerAsset]
|
||||
class TestingVersion(TypedDict):
|
||||
id: int
|
||||
name: str
|
||||
link: str
|
||||
head_sha: str
|
||||
|
||||
class Updater:
|
||||
def __init__(self, context: PluginManager) -> None:
|
||||
self.context = context
|
||||
self.settings = self.context.settings
|
||||
self.remoteVer: RemoteVer | None = None
|
||||
self.allRemoteVers: List[RemoteVer] = []
|
||||
self.localVer = helpers.get_loader_version()
|
||||
|
||||
try:
|
||||
self.currentBranch = self.get_branch(self.context.settings)
|
||||
except:
|
||||
self.currentBranch = 0
|
||||
logger.error("Current branch could not be determined, defaulting to \"Stable\"")
|
||||
|
||||
if context:
|
||||
context.ws.add_route("updater/get_version_info", self.get_version_info);
|
||||
context.ws.add_route("updater/check_for_updates", self.check_for_updates);
|
||||
context.ws.add_route("updater/do_restart", self.do_restart);
|
||||
context.ws.add_route("updater/do_shutdown", self.do_shutdown);
|
||||
context.ws.add_route("updater/do_update", self.do_update);
|
||||
context.ws.add_route("updater/get_testing_versions", self.get_testing_versions);
|
||||
context.ws.add_route("updater/download_testing_version", self.download_testing_version);
|
||||
context.loop.create_task(self.version_reloader())
|
||||
|
||||
def get_branch(self, manager: SettingsManager):
|
||||
ver = manager.getSetting("branch", -1)
|
||||
logger.debug("current branch: %i" % ver)
|
||||
if ver == -1:
|
||||
logger.info("Current branch is not set, determining branch from version...")
|
||||
if self.localVer.startswith("v") and "-pre" in self.localVer:
|
||||
logger.info("Current version determined to be pre-release")
|
||||
manager.setSetting('branch', 1)
|
||||
return 1
|
||||
else:
|
||||
logger.info("Current version determined to be stable")
|
||||
manager.setSetting('branch', 0)
|
||||
return 0
|
||||
return ver
|
||||
|
||||
async def _get_branch(self, manager: SettingsManager):
|
||||
return self.get_branch(manager)
|
||||
|
||||
# retrieve relevant service file's url for each branch
|
||||
def get_service_url(self):
|
||||
logger.debug("Getting service URL")
|
||||
branch = self.get_branch(self.context.settings)
|
||||
match branch:
|
||||
case 0:
|
||||
url = "https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/dist/plugin_loader-release.service"
|
||||
case 1 | 2:
|
||||
url = "https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/dist/plugin_loader-prerelease.service"
|
||||
case _:
|
||||
logger.error("You have an invalid branch set... Defaulting to prerelease service, please send the logs to the devs!")
|
||||
url = "https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/dist/plugin_loader-prerelease.service"
|
||||
return str(url)
|
||||
|
||||
async def get_version_info(self):
|
||||
return {
|
||||
"current": self.localVer,
|
||||
"remote": self.remoteVer,
|
||||
"all": self.allRemoteVers,
|
||||
"updatable": self.localVer != "unknown" and self.localVer != "dev"
|
||||
}
|
||||
|
||||
async def check_for_updates(self):
|
||||
logger.debug("checking for updates")
|
||||
selectedBranch = self.get_branch(self.context.settings)
|
||||
async with ClientSession() as web:
|
||||
async with web.request("GET", "https://api.github.com/repos/SteamDeckHomebrew/decky-loader/releases", headers={'X-GitHub-Api-Version': '2022-11-28'}, ssl=helpers.get_ssl_context()) as res:
|
||||
remoteVersions: List[RemoteVer] = await res.json()
|
||||
if selectedBranch == 0:
|
||||
logger.debug("release type: release")
|
||||
remoteVersions = list(filter(lambda ver: ver["tag_name"].startswith("v") and not ver["prerelease"] and not ver["tag_name"].find("-pre") > 0 and ver["tag_name"], remoteVersions))
|
||||
elif selectedBranch == 1:
|
||||
logger.debug("release type: pre-release")
|
||||
remoteVersions = list(filter(lambda ver:ver["tag_name"].startswith("v"), remoteVersions))
|
||||
else:
|
||||
logger.error("release type: NOT FOUND")
|
||||
raise ValueError("no valid branch found")
|
||||
self.allRemoteVers = remoteVersions
|
||||
logger.debug("determining release type to find, branch is %i" % selectedBranch)
|
||||
if selectedBranch == 0:
|
||||
logger.debug("release type: release")
|
||||
self.remoteVer = next(filter(lambda ver: ver["tag_name"].startswith("v") and not ver["prerelease"] and not ver["tag_name"].find("-pre") > 0 and ver["tag_name"], remoteVersions), None)
|
||||
elif selectedBranch == 1:
|
||||
logger.debug("release type: pre-release")
|
||||
self.remoteVer = next(filter(lambda ver:ver["tag_name"].startswith("v"), remoteVersions), None)
|
||||
else:
|
||||
logger.error("release type: NOT FOUND")
|
||||
raise ValueError("no valid branch found")
|
||||
logger.info("Updated remote version information")
|
||||
await self.context.ws.emit("loader/notify_updates")
|
||||
return await self.get_version_info()
|
||||
|
||||
async def version_reloader(self):
|
||||
await sleep(30)
|
||||
while True:
|
||||
try:
|
||||
await self.check_for_updates()
|
||||
except:
|
||||
pass
|
||||
await sleep(60 * 60 * 6) # 6 hours
|
||||
|
||||
async def download_decky_binary(self, download_url: str, version: str, is_zip: bool = False, size_in_bytes: int | None = None):
|
||||
download_filename = "PluginLoader" if ON_LINUX else "PluginLoader.exe"
|
||||
download_temp_filename = download_filename + ".new"
|
||||
|
||||
if size_in_bytes == None:
|
||||
size_in_bytes = 26214400 # 25MiB, a reasonable overestimate (19.6MiB as of 2024/02/25)
|
||||
|
||||
async with ClientSession() as web:
|
||||
logger.debug("Downloading binary")
|
||||
async with web.request("GET", download_url, ssl=helpers.get_ssl_context(), allow_redirects=True) as res:
|
||||
total = int(res.headers.get('content-length', size_in_bytes))
|
||||
if total == 0: total = 1
|
||||
with open(path.join(getcwd(), download_temp_filename), "wb") as out:
|
||||
progress = 0
|
||||
raw = 0
|
||||
async for c in res.content.iter_chunked(512):
|
||||
out.write(c)
|
||||
raw += len(c)
|
||||
new_progress = round((raw / total) * 100)
|
||||
if progress != new_progress:
|
||||
self.context.loop.create_task(self.context.ws.emit("updater/update_download_percentage", new_progress))
|
||||
progress = new_progress
|
||||
|
||||
with open(path.join(getcwd(), ".loader.version"), "w", encoding="utf-8") as out:
|
||||
out.write(version)
|
||||
|
||||
if ON_LINUX:
|
||||
remove(path.join(getcwd(), download_filename))
|
||||
if (is_zip):
|
||||
with zipfile.ZipFile(path.join(getcwd(), download_temp_filename), 'r') as file:
|
||||
file.getinfo(download_filename).filename = download_filename + ".unzipped"
|
||||
file.extract(download_filename)
|
||||
remove(path.join(getcwd(), download_temp_filename))
|
||||
shutil.move(path.join(getcwd(), download_filename + ".unzipped"), path.join(getcwd(), download_filename))
|
||||
else:
|
||||
shutil.move(path.join(getcwd(), download_temp_filename), path.join(getcwd(), download_filename))
|
||||
|
||||
chmod(path.join(getcwd(), download_filename), 777, False)
|
||||
if get_selinux():
|
||||
from asyncio.subprocess import create_subprocess_exec
|
||||
process = await create_subprocess_exec("chcon", "-t", "bin_t", path.join(getcwd(), download_filename))
|
||||
logger.info(f"Setting the executable flag with chcon returned {await process.wait()}")
|
||||
|
||||
logger.info("Updated loader installation.")
|
||||
await self.context.ws.emit("updater/finish_download")
|
||||
await self.do_restart()
|
||||
|
||||
async def do_update(self):
|
||||
logger.debug("Starting update.")
|
||||
try:
|
||||
assert self.remoteVer
|
||||
except AssertionError:
|
||||
logger.error("Unable to update as remoteVer is missing")
|
||||
return
|
||||
|
||||
version = self.remoteVer["tag_name"]
|
||||
download_url = None
|
||||
size_in_bytes = None
|
||||
download_filename = "PluginLoader" if ON_LINUX else "PluginLoader.exe"
|
||||
|
||||
for x in self.remoteVer["assets"]:
|
||||
if x["name"] == download_filename:
|
||||
download_url = x["browser_download_url"]
|
||||
size_in_bytes = x["size"]
|
||||
break
|
||||
|
||||
if download_url == None:
|
||||
raise Exception("Download url not found")
|
||||
|
||||
service_url = self.get_service_url()
|
||||
logger.debug("Retrieved service URL")
|
||||
|
||||
async with ClientSession() as web:
|
||||
if ON_LINUX and not get_keep_systemd_service():
|
||||
logger.debug("Downloading systemd service")
|
||||
# download the relevant systemd service depending upon branch
|
||||
async with web.request("GET", service_url, ssl=helpers.get_ssl_context(), allow_redirects=True) as res:
|
||||
logger.debug("Downloading service file")
|
||||
data = await res.content.read()
|
||||
logger.debug(str(data))
|
||||
service_file_path = path.join(getcwd(), "plugin_loader.service")
|
||||
try:
|
||||
with open(path.join(getcwd(), "plugin_loader.service"), "wb") as out:
|
||||
out.write(data)
|
||||
except Exception as e:
|
||||
logger.error(f"Error at %s", exc_info=e)
|
||||
with open(path.join(getcwd(), "plugin_loader.service"), "r", encoding="utf-8") as service_file:
|
||||
service_data = service_file.read()
|
||||
service_data = service_data.replace("${HOMEBREW_FOLDER}", helpers.get_homebrew_path())
|
||||
with open(path.join(getcwd(), "plugin_loader.service"), "w", encoding="utf-8") as service_file:
|
||||
service_file.write(service_data)
|
||||
|
||||
logger.debug("Saved service file")
|
||||
logger.debug("Copying service file over current file.")
|
||||
shutil.copy(service_file_path, "/etc/systemd/system/plugin_loader.service")
|
||||
if not os.path.exists(path.join(getcwd(), ".systemd")):
|
||||
os.mkdir(path.join(getcwd(), ".systemd"))
|
||||
shutil.move(service_file_path, path.join(getcwd(), ".systemd")+"/plugin_loader.service")
|
||||
|
||||
await self.download_decky_binary(download_url, version, size_in_bytes=size_in_bytes)
|
||||
|
||||
async def do_restart(self):
|
||||
logger.info("Restarting loader for update.")
|
||||
await service_restart("plugin_loader", block=False)
|
||||
|
||||
async def do_shutdown(self):
|
||||
await service_stop("plugin_loader")
|
||||
|
||||
async def get_testing_versions(self) -> List[TestingVersion]:
|
||||
result: List[TestingVersion] = []
|
||||
async with ClientSession() as web:
|
||||
async with web.request("GET", "https://api.github.com/repos/SteamDeckHomebrew/decky-loader/pulls",
|
||||
headers={'X-GitHub-Api-Version': '2022-11-28'}, params={'state':'open'}, ssl=helpers.get_ssl_context()) as res:
|
||||
open_prs = await res.json()
|
||||
for pr in open_prs:
|
||||
result.append({
|
||||
"id": int(pr['number']),
|
||||
"name": pr['title'],
|
||||
"link": pr['html_url'],
|
||||
"head_sha": pr['head']['sha'],
|
||||
})
|
||||
return result
|
||||
|
||||
async def download_testing_version(self, pr_id: int, sha_id: str):
|
||||
down_id = ''
|
||||
#Get all the associated workflow run for the given sha_id code hash
|
||||
async with ClientSession() as web:
|
||||
async with web.request("GET", "https://api.github.com/repos/SteamDeckHomebrew/decky-loader/actions/runs",
|
||||
headers={'X-GitHub-Api-Version': '2022-11-28'}, params={'head_sha': sha_id}, ssl=helpers.get_ssl_context()) as res:
|
||||
works = await res.json()
|
||||
#Iterate over the workflow_run to get the two builds if they exists
|
||||
for work in works['workflow_runs']:
|
||||
if ON_WINDOWS and work['name'] == 'Builder Win':
|
||||
down_id=work['id']
|
||||
break
|
||||
elif ON_LINUX and work['name'] == 'Builder':
|
||||
down_id=work['id']
|
||||
break
|
||||
if down_id != '':
|
||||
async with ClientSession() as web:
|
||||
async with web.request("GET", f"https://api.github.com/repos/SteamDeckHomebrew/decky-loader/actions/runs/{down_id}/artifacts",
|
||||
headers={'X-GitHub-Api-Version': '2022-11-28'}, ssl=helpers.get_ssl_context()) as res:
|
||||
jresp = await res.json()
|
||||
#If the request found at least one artifact to download...
|
||||
if int(jresp['total_count']) != 0:
|
||||
# this assumes that the artifact we want is the first one!
|
||||
artifact = jresp['artifacts'][0]
|
||||
down_link = f"https://nightly.link/SteamDeckHomebrew/decky-loader/actions/artifacts/{artifact['id']}.zip"
|
||||
#Then fetch it and restart itself
|
||||
await self.download_decky_binary(down_link, f'PR-{pr_id}', is_zip=True, size_in_bytes=artifact.get('size_in_bytes',None))
|
||||
else:
|
||||
logger.error("workflow run not found", str(works))
|
||||
raise Exception("Workflow run not found.")
|
||||
@@ -0,0 +1,516 @@
|
||||
from __future__ import annotations
|
||||
from os import path, stat_result
|
||||
import uuid
|
||||
from urllib.parse import unquote
|
||||
from json.decoder import JSONDecodeError
|
||||
from os.path import splitext
|
||||
import re
|
||||
from traceback import format_exc
|
||||
from stat import FILE_ATTRIBUTE_HIDDEN # pyright: ignore [reportAttributeAccessIssue, reportUnknownVariableType]
|
||||
|
||||
from asyncio import StreamReader, StreamWriter, sleep, start_server, gather, open_connection
|
||||
from aiohttp import ClientSession, hdrs
|
||||
from aiohttp.web import Request, StreamResponse, Response, json_response, post
|
||||
from typing import TYPE_CHECKING, Callable, Coroutine, Dict, Any, List, TypedDict
|
||||
|
||||
from logging import getLogger
|
||||
from pathlib import Path
|
||||
|
||||
from .browser import PluginInstallRequest, PluginInstallType
|
||||
if TYPE_CHECKING:
|
||||
from .main import PluginManager
|
||||
from .injector import inject_to_tab, get_gamepadui_tab, close_old_tabs, get_tab
|
||||
from . import helpers
|
||||
from .localplatform.localplatform import ON_WINDOWS, service_stop, service_start, get_home_path, get_username, get_use_cef_close_workaround, close_cef_socket, restart_webhelper
|
||||
|
||||
class FilePickerObj(TypedDict):
|
||||
file: Path
|
||||
filest: stat_result
|
||||
is_dir: bool
|
||||
|
||||
decky_header_regex = re.compile("X-Decky-(.*)")
|
||||
extra_header_regex = re.compile("X-Decky-Header-(.*)")
|
||||
|
||||
excluded_default_headers = ["Host", "Origin", "Sec-Fetch-Site", "Sec-Fetch-Mode", "Sec-Fetch-Dest"]
|
||||
|
||||
class Utilities:
|
||||
def __init__(self, context: PluginManager) -> None:
|
||||
self.context = context
|
||||
self.legacy_util_methods: Dict[str, Callable[..., Coroutine[Any, Any, Any]]] = {
|
||||
"ping": self.ping,
|
||||
"http_request": self.http_request_legacy,
|
||||
"execute_in_tab": self.execute_in_tab,
|
||||
"inject_css_into_tab": self.inject_css_into_tab,
|
||||
"remove_css_from_tab": self.remove_css_from_tab,
|
||||
"set_setting": self.set_setting,
|
||||
"get_setting": self.get_setting,
|
||||
"filepicker_ls": self.filepicker_ls,
|
||||
"get_tab_id": self.get_tab_id,
|
||||
"get_user_info": self.get_user_info,
|
||||
}
|
||||
|
||||
self.logger = getLogger("Utilities")
|
||||
|
||||
self.rdt_proxy_server = None
|
||||
self.rdt_script_id = None
|
||||
self.rdt_proxy_task = None
|
||||
|
||||
if context:
|
||||
context.ws.add_route("utilities/ping", self.ping)
|
||||
context.ws.add_route("utilities/settings/get", self.get_setting)
|
||||
context.ws.add_route("utilities/settings/set", self.set_setting)
|
||||
context.ws.add_route("utilities/install_plugin", self.install_plugin)
|
||||
context.ws.add_route("utilities/install_plugins", self.install_plugins)
|
||||
context.ws.add_route("utilities/cancel_plugin_install", self.cancel_plugin_install)
|
||||
context.ws.add_route("utilities/confirm_plugin_install", self.confirm_plugin_install)
|
||||
context.ws.add_route("utilities/uninstall_plugin", self.uninstall_plugin)
|
||||
context.ws.add_route("utilities/execute_in_tab", self.execute_in_tab)
|
||||
context.ws.add_route("utilities/inject_css_into_tab", self.inject_css_into_tab)
|
||||
context.ws.add_route("utilities/remove_css_from_tab", self.remove_css_from_tab)
|
||||
context.ws.add_route("utilities/allow_remote_debugging", self.allow_remote_debugging)
|
||||
context.ws.add_route("utilities/disallow_remote_debugging", self.disallow_remote_debugging)
|
||||
context.ws.add_route("utilities/start_ssh", self.allow_remote_debugging)
|
||||
context.ws.add_route("utilities/stop_ssh", self.allow_remote_debugging)
|
||||
context.ws.add_route("utilities/filepicker_ls", self.filepicker_ls)
|
||||
context.ws.add_route("utilities/disable_rdt", self.disable_rdt)
|
||||
context.ws.add_route("utilities/enable_rdt", self.enable_rdt)
|
||||
context.ws.add_route("utilities/get_tab_id", self.get_tab_id)
|
||||
context.ws.add_route("utilities/get_user_info", self.get_user_info)
|
||||
context.ws.add_route("utilities/http_request", self.http_request_legacy)
|
||||
context.ws.add_route("utilities/restart_webhelper", self.restart_webhelper)
|
||||
context.ws.add_route("utilities/close_cef_socket", self.close_cef_socket)
|
||||
context.ws.add_route("utilities/_call_legacy_utility", self._call_legacy_utility)
|
||||
context.ws.add_route("utilities/enable_plugin", self.enable_plugin)
|
||||
context.ws.add_route("utilities/disable_plugin", self.disable_plugin)
|
||||
context.ws.add_route("utilities/set_all_plugins_disabled", self.set_all_plugins_disabled)
|
||||
|
||||
context.web_app.add_routes([
|
||||
post("/methods/{method_name}", self._handle_legacy_server_method_call)
|
||||
])
|
||||
|
||||
for method in ('GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD'):
|
||||
context.web_app.router.add_route(method, "/fetch", self.http_request)
|
||||
|
||||
|
||||
async def _handle_legacy_server_method_call(self, request: Request) -> Response:
|
||||
method_name = request.match_info["method_name"]
|
||||
try:
|
||||
args = await request.json()
|
||||
except JSONDecodeError:
|
||||
args = {}
|
||||
res = {}
|
||||
try:
|
||||
r = await self.legacy_util_methods[method_name](**args)
|
||||
res["result"] = r
|
||||
res["success"] = True
|
||||
except Exception as e:
|
||||
res["result"] = str(e)
|
||||
res["success"] = False
|
||||
return json_response(res)
|
||||
|
||||
async def _call_legacy_utility(self, method_name: str, kwargs: Dict[Any, Any]) -> Any:
|
||||
self.logger.debug(f"Calling utility {method_name} with legacy kwargs");
|
||||
res: Dict[Any, Any] = {}
|
||||
try:
|
||||
r = await self.legacy_util_methods[method_name](**kwargs)
|
||||
res["result"] = r
|
||||
res["success"] = True
|
||||
except Exception as e:
|
||||
res["result"] = str(e)
|
||||
res["success"] = False
|
||||
return res
|
||||
|
||||
async def install_plugin(self, artifact: str="", name: str="No name", version: str="dev", hash: str="", install_type: PluginInstallType=PluginInstallType.INSTALL):
|
||||
return await self.context.plugin_browser.request_plugin_install(
|
||||
artifact=artifact,
|
||||
name=name,
|
||||
version=version,
|
||||
hash=hash,
|
||||
install_type=install_type
|
||||
)
|
||||
|
||||
async def install_plugins(self, requests: List[PluginInstallRequest]):
|
||||
return await self.context.plugin_browser.request_multiple_plugin_installs(
|
||||
requests=requests
|
||||
)
|
||||
|
||||
async def confirm_plugin_install(self, request_id: str):
|
||||
return await self.context.plugin_browser.confirm_plugin_install(request_id)
|
||||
|
||||
async def cancel_plugin_install(self, request_id: str):
|
||||
return self.context.plugin_browser.cancel_plugin_install(request_id)
|
||||
|
||||
async def uninstall_plugin(self, name: str):
|
||||
return await self.context.plugin_browser.uninstall_plugin(name)
|
||||
|
||||
# Loosely based on https://gist.github.com/mosquito/4dbfacd51e751827cda7ec9761273e95#file-proxy-py
|
||||
async def http_request(self, req: Request) -> StreamResponse:
|
||||
if req.query['auth'] != helpers.get_csrf_token():
|
||||
return Response(text='Forbidden', status=403)
|
||||
|
||||
url = unquote(req.query['fetch_url'])
|
||||
self.logger.info(f"Preparing {req.method} request to {url}")
|
||||
|
||||
headers = dict(req.headers)
|
||||
|
||||
headers["User-Agent"] = helpers.user_agent
|
||||
|
||||
for excluded_header in excluded_default_headers:
|
||||
if excluded_header in headers:
|
||||
self.logger.debug(f"Excluding default header {excluded_header}: {headers[excluded_header]}")
|
||||
del headers[excluded_header]
|
||||
|
||||
if "X-Decky-Fetch-Excluded-Headers" in req.headers:
|
||||
for excluded_header in req.headers["X-Decky-Fetch-Excluded-Headers"].split(", "):
|
||||
if excluded_header in headers:
|
||||
self.logger.debug(f"Excluding header {excluded_header}: {headers[excluded_header]}")
|
||||
del headers[excluded_header]
|
||||
|
||||
for header in req.headers:
|
||||
match = extra_header_regex.search(header)
|
||||
if match:
|
||||
header_name = match.group(1)
|
||||
header_value = req.headers[header]
|
||||
self.logger.debug(f"Adding extra header {header_name}: {header_value}")
|
||||
headers[header_name] = header_value
|
||||
|
||||
for header in list(headers.keys()):
|
||||
match = decky_header_regex.search(header)
|
||||
if match:
|
||||
self.logger.debug(f"Removing decky header {header} from request")
|
||||
del headers[header]
|
||||
|
||||
self.logger.debug(f"Final request headers: {headers}")
|
||||
|
||||
body = await req.read() # TODO can this also be streamed?
|
||||
|
||||
# We disable auto-decompress so that the body is completely forwarded to the
|
||||
# JS engine for it to do the decompression. Otherwise we need need to clear
|
||||
# the Content-Encoding header in the response headers, however that would
|
||||
# defeat the point of this proxy.
|
||||
async with ClientSession(auto_decompress=False) as web:
|
||||
async with web.request(req.method, url, headers=headers, data=body, ssl=helpers.get_ssl_context()) as web_res:
|
||||
# Whenever the aiohttp_cors is used, it expects a near complete control over whatever headers are needed
|
||||
# for `aiohttp_cors.ResourceOptions`. As a server, if you delegate CORS handling to aiohttp_cors,
|
||||
# the headers below must NOT be set. Otherwise they would be overwritten by aiohttp_cors and there would be
|
||||
# logic bugs, so it was probably a smart choice to assert if the headers are present.
|
||||
#
|
||||
# However, this request handler method does not act like our own local server, it always acts like a proxy
|
||||
# where we do not have control over the response headers. For responses that do not allow CORS, we add the support
|
||||
# via aiohttp_cors. For responses that allow CORS, we have to remove the conflicting headers to allow
|
||||
# aiohttp_cors handle it for us as if there was no CORS support.
|
||||
aiohttp_cors_compatible_headers = web_res.headers.copy()
|
||||
aiohttp_cors_compatible_headers.popall(hdrs.ACCESS_CONTROL_ALLOW_ORIGIN, default=None)
|
||||
aiohttp_cors_compatible_headers.popall(hdrs.ACCESS_CONTROL_ALLOW_CREDENTIALS, default=None)
|
||||
aiohttp_cors_compatible_headers.popall(hdrs.ACCESS_CONTROL_EXPOSE_HEADERS, default=None)
|
||||
|
||||
res = StreamResponse(headers=aiohttp_cors_compatible_headers, status=web_res.status)
|
||||
if web_res.headers.get('Transfer-Encoding', '').lower() == 'chunked':
|
||||
res.enable_chunked_encoding()
|
||||
|
||||
await res.prepare(req)
|
||||
self.logger.debug(f"Starting stream for {url}")
|
||||
async for data in web_res.content.iter_any():
|
||||
await res.write(data)
|
||||
self.logger.debug(f"Finished stream for {url}")
|
||||
return res
|
||||
|
||||
async def http_request_legacy(self, method: str, url: str, extra_opts: Any = {}, timeout: int | None = None):
|
||||
async with ClientSession() as web:
|
||||
res = await web.request(method, url, ssl=helpers.get_ssl_context(), timeout=timeout, **extra_opts) # type: ignore
|
||||
text = await res.text()
|
||||
return {
|
||||
"status": res.status,
|
||||
"headers": dict(res.headers),
|
||||
"body": text
|
||||
}
|
||||
|
||||
async def ping(self, **kwargs: Any):
|
||||
return "pong"
|
||||
|
||||
async def execute_in_tab(self, tab: str, run_async: bool, code: str):
|
||||
try:
|
||||
result = await inject_to_tab(tab, code, run_async)
|
||||
assert result
|
||||
if "exceptionDetails" in result["result"]:
|
||||
return {
|
||||
"success": False,
|
||||
"result": result["result"]
|
||||
}
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"result": result["result"]["result"].get("value")
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"result": e
|
||||
}
|
||||
|
||||
async def inject_css_into_tab(self, tab: str, style: str) -> str:
|
||||
css_id = str(uuid.uuid4())
|
||||
|
||||
result = await inject_to_tab(tab,
|
||||
f"""
|
||||
(function() {{
|
||||
const style = document.createElement('style');
|
||||
style.id = "{css_id}";
|
||||
document.head.append(style);
|
||||
style.textContent = `{style}`;
|
||||
}})()
|
||||
""", False)
|
||||
assert result is not None # TODO remove this once it has proper typings
|
||||
if "exceptionDetails" in result["result"]:
|
||||
raise result["result"]["exceptionDetails"]
|
||||
|
||||
return css_id
|
||||
|
||||
async def remove_css_from_tab(self, tab: str, css_id: str):
|
||||
result = await inject_to_tab(tab,
|
||||
f"""
|
||||
(function() {{
|
||||
let style = document.getElementById("{css_id}");
|
||||
|
||||
if (style.nodeName.toLowerCase() == 'style')
|
||||
style.parentNode.removeChild(style);
|
||||
}})()
|
||||
""", False)
|
||||
|
||||
assert result
|
||||
if "exceptionDetails" in result["result"]:
|
||||
raise result["result"]["exceptionDetails"]
|
||||
|
||||
return
|
||||
|
||||
async def get_setting(self, key: str, default: Any):
|
||||
return self.context.settings.getSetting(key, default)
|
||||
|
||||
async def set_setting(self, key: str, value: Any):
|
||||
return self.context.settings.setSetting(key, value)
|
||||
|
||||
async def allow_remote_debugging(self):
|
||||
await service_start(helpers.REMOTE_DEBUGGER_UNIT)
|
||||
return True
|
||||
|
||||
async def disallow_remote_debugging(self):
|
||||
await service_stop(helpers.REMOTE_DEBUGGER_UNIT)
|
||||
return True
|
||||
|
||||
async def start_ssh(self):
|
||||
await service_start(helpers.SSHD_UNIT)
|
||||
return True
|
||||
|
||||
async def stop_ssh(self):
|
||||
await service_stop(helpers.SSHD_UNIT)
|
||||
return True
|
||||
|
||||
async def close_cef_socket(self):
|
||||
if get_use_cef_close_workaround():
|
||||
await close_cef_socket()
|
||||
|
||||
async def restart_webhelper(self):
|
||||
await restart_webhelper()
|
||||
|
||||
async def filepicker_ls(self,
|
||||
path: str | None = None,
|
||||
include_files: bool = True,
|
||||
include_folders: bool = True,
|
||||
include_ext: list[str] | None = None,
|
||||
include_hidden: bool = False,
|
||||
order_by: str = "name_desc",
|
||||
filter_for: str | None = None,
|
||||
page: int = 1,
|
||||
max: int = 1000):
|
||||
|
||||
if path == None:
|
||||
path = get_home_path()
|
||||
|
||||
path_obj = Path(path).resolve()
|
||||
|
||||
files: List[FilePickerObj] = []
|
||||
folders: List[FilePickerObj] = []
|
||||
|
||||
#Resolving all files/folders in the requested directory
|
||||
for file in path_obj.iterdir():
|
||||
if file.exists():
|
||||
filest = file.stat()
|
||||
is_hidden = file.name.startswith('.')
|
||||
if ON_WINDOWS and not is_hidden:
|
||||
is_hidden = bool(filest.st_file_attributes & FILE_ATTRIBUTE_HIDDEN) # type: ignore
|
||||
if include_folders and file.is_dir():
|
||||
if (is_hidden and include_hidden) or not is_hidden:
|
||||
folders.append({"file": file, "filest": filest, "is_dir": True})
|
||||
elif include_files:
|
||||
# Handle requested extensions if present
|
||||
if include_ext == None or len(include_ext) == 0 or 'all_files' in include_ext \
|
||||
or splitext(file.name)[1].lstrip('.').upper() in (ext.upper() for ext in include_ext):
|
||||
if (is_hidden and include_hidden) or not is_hidden:
|
||||
files.append({"file": file, "filest": filest, "is_dir": False})
|
||||
# Filter logic
|
||||
if filter_for is not None:
|
||||
try:
|
||||
if re.compile(filter_for):
|
||||
files = list(filter(lambda file: re.search(filter_for, file["file"].name) != None, files))
|
||||
except re.error:
|
||||
files = list(filter(lambda file: file["file"].name.find(filter_for) != -1, files))
|
||||
|
||||
# Ordering logic
|
||||
ord_arg = order_by.split("_")
|
||||
ord = ord_arg[0]
|
||||
rev = True if ord_arg[1] == "asc" else False
|
||||
match ord:
|
||||
case 'name':
|
||||
files.sort(key=lambda x: x['file'].name.casefold(), reverse = rev)
|
||||
folders.sort(key=lambda x: x['file'].name.casefold(), reverse = rev)
|
||||
case 'modified':
|
||||
files.sort(key=lambda x: x['filest'].st_mtime, reverse = not rev)
|
||||
folders.sort(key=lambda x: x['filest'].st_mtime, reverse = not rev)
|
||||
case 'created':
|
||||
files.sort(key=lambda x: x['filest'].st_ctime, reverse = not rev)
|
||||
folders.sort(key=lambda x: x['filest'].st_ctime, reverse = not rev)
|
||||
case 'size':
|
||||
files.sort(key=lambda x: x['filest'].st_size, reverse = not rev)
|
||||
# Folders has no file size, order by name instead
|
||||
folders.sort(key=lambda x: x['file'].name.casefold())
|
||||
case _:
|
||||
files.sort(key=lambda x: x['file'].name.casefold(), reverse = rev)
|
||||
folders.sort(key=lambda x: x['file'].name.casefold(), reverse = rev)
|
||||
|
||||
#Constructing the final file list, folders first
|
||||
all = [{
|
||||
"isdir": x['is_dir'],
|
||||
"name": str(x['file'].name),
|
||||
"realpath": str(x['file']),
|
||||
"size": x['filest'].st_size,
|
||||
"modified": x['filest'].st_mtime,
|
||||
"created": x['filest'].st_ctime,
|
||||
} for x in folders + files ]
|
||||
|
||||
return {
|
||||
"realpath": str(path),
|
||||
"files": all[(page-1)*max:(page)*max],
|
||||
"total": len(all),
|
||||
}
|
||||
|
||||
# Based on https://stackoverflow.com/a/46422554/13174603
|
||||
def start_rdt_proxy(self, ip: str, port: int):
|
||||
async def pipe(reader: StreamReader, writer: StreamWriter):
|
||||
try:
|
||||
while not reader.at_eof():
|
||||
writer.write(await reader.read(2048))
|
||||
finally:
|
||||
writer.close()
|
||||
async def handle_client(local_reader: StreamReader, local_writer: StreamWriter):
|
||||
try:
|
||||
remote_reader, remote_writer = await open_connection(
|
||||
ip, port)
|
||||
pipe1 = pipe(local_reader, remote_writer)
|
||||
pipe2 = pipe(remote_reader, local_writer)
|
||||
await gather(pipe1, pipe2)
|
||||
finally:
|
||||
local_writer.close()
|
||||
|
||||
self.rdt_proxy_server = start_server(handle_client, "127.0.0.1", port)
|
||||
self.rdt_proxy_task = self.context.loop.create_task(self.rdt_proxy_server)
|
||||
|
||||
def stop_rdt_proxy(self):
|
||||
if self.rdt_proxy_server != None:
|
||||
self.rdt_proxy_server.close()
|
||||
if self.rdt_proxy_task:
|
||||
self.rdt_proxy_task.cancel()
|
||||
|
||||
async def _enable_rdt(self):
|
||||
# TODO un-hardcode port
|
||||
try:
|
||||
self.stop_rdt_proxy()
|
||||
ip = self.context.settings.getSetting("developer.rdt.ip", None)
|
||||
|
||||
if ip != None:
|
||||
self.logger.info("Connecting to React DevTools at " + ip)
|
||||
async with ClientSession() as web:
|
||||
res = await web.request("GET", "http://" + ip + ":8097", ssl=helpers.get_ssl_context())
|
||||
script = """
|
||||
if (!window.deckyHasConnectedRDT) {
|
||||
window.deckyHasConnectedRDT = true;
|
||||
// This fixes the overlay when hovering over an element in RDT
|
||||
Object.defineProperty(window, '__REACT_DEVTOOLS_TARGET_WINDOW__', {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
get: function() {
|
||||
return (GamepadNavTree?.m_context?.m_controller || FocusNavController)?.m_ActiveContext?.ActiveWindow || window;
|
||||
}
|
||||
});
|
||||
""" + await res.text() + "\n}"
|
||||
if res.status != 200:
|
||||
self.logger.error("Failed to connect to React DevTools at " + ip)
|
||||
return False
|
||||
self.start_rdt_proxy(ip, 8097)
|
||||
self.logger.info("Connected to React DevTools, loading script")
|
||||
tab = await get_gamepadui_tab()
|
||||
# RDT needs to load before React itself to work.
|
||||
await close_old_tabs()
|
||||
result = await tab.reload_and_evaluate(script)
|
||||
self.logger.info(result)
|
||||
|
||||
except Exception:
|
||||
self.logger.error("Failed to connect to React DevTools")
|
||||
self.logger.error(format_exc())
|
||||
|
||||
async def enable_rdt(self):
|
||||
self.context.loop.create_task(self._enable_rdt())
|
||||
|
||||
async def disable_rdt(self):
|
||||
self.logger.info("Disabling React DevTools")
|
||||
tab = await get_gamepadui_tab()
|
||||
self.rdt_script_id = None
|
||||
await close_old_tabs()
|
||||
await tab.evaluate_js("location.reload();", False, True, False)
|
||||
self.logger.info("React DevTools disabled")
|
||||
|
||||
async def get_user_info(self) -> Dict[str, str]:
|
||||
return {
|
||||
"username": get_username(),
|
||||
"path": get_home_path()
|
||||
}
|
||||
|
||||
async def get_tab_id(self, name: str):
|
||||
return (await get_tab(name)).id
|
||||
|
||||
async def disable_plugin(self, name: str):
|
||||
disabled_plugins: List[str] = await self.get_setting("disabled_plugins", [])
|
||||
if name not in disabled_plugins:
|
||||
disabled_plugins.append(name)
|
||||
await self.set_setting("disabled_plugins", disabled_plugins)
|
||||
|
||||
await self.context.plugin_loader.plugins[name].stop()
|
||||
await self.context.ws.emit("loader/disable_plugin", name)
|
||||
|
||||
async def enable_plugin(self, name: str):
|
||||
plugin_folder = self.context.plugin_browser.find_plugin_folder(name)
|
||||
assert plugin_folder is not None
|
||||
plugin_dir = path.join(self.context.plugin_browser.plugin_path, plugin_folder)
|
||||
|
||||
if name in self.context.plugin_loader.plugins:
|
||||
plugin = self.context.plugin_loader.plugins[name]
|
||||
if plugin.proc and plugin.proc.is_alive():
|
||||
await plugin.stop()
|
||||
self.context.plugin_loader.plugins.pop(name, None)
|
||||
await sleep(1)
|
||||
|
||||
disabled_plugins: List[str] = await self.get_setting("disabled_plugins", [])
|
||||
|
||||
if name in disabled_plugins:
|
||||
disabled_plugins.remove(name)
|
||||
await self.set_setting("disabled_plugins", disabled_plugins)
|
||||
|
||||
await self.context.plugin_loader.import_plugin(path.join(plugin_dir, "main.py"), plugin_folder)
|
||||
|
||||
async def set_all_plugins_disabled(self):
|
||||
disabled_plugins: List[str] = await self.get_setting("disabled_plugins", [])
|
||||
|
||||
for name, _ in self.context.plugin_loader.plugins.items():
|
||||
if name not in disabled_plugins:
|
||||
disabled_plugins.append(name)
|
||||
|
||||
await self.set_setting("disabled_plugins", disabled_plugins)
|
||||
@@ -0,0 +1,137 @@
|
||||
from logging import getLogger
|
||||
|
||||
from asyncio import AbstractEventLoop
|
||||
|
||||
from aiohttp import WSCloseCode, WSMsgType, WSMessage
|
||||
from aiohttp.web import Application, WebSocketResponse, Request, Response, get
|
||||
|
||||
from enum import IntEnum
|
||||
|
||||
from typing import Callable, Coroutine, Dict, Any, cast
|
||||
|
||||
from traceback import format_exc
|
||||
|
||||
from .helpers import get_csrf_token
|
||||
|
||||
class MessageType(IntEnum):
|
||||
ERROR = -1
|
||||
# Call-reply, Frontend -> Backend -> Frontend
|
||||
CALL = 0
|
||||
REPLY = 1
|
||||
# Pub/Sub, Backend -> Frontend
|
||||
EVENT = 3
|
||||
|
||||
# WSMessage with slightly better typings
|
||||
class WSMessageExtra(WSMessage):
|
||||
# TODO message typings here too
|
||||
data: Any # pyright: ignore [reportIncompatibleVariableOverride]
|
||||
type: WSMsgType # pyright: ignore [reportIncompatibleVariableOverride]
|
||||
|
||||
# see wsrouter.ts for typings
|
||||
|
||||
Route = Callable[..., Coroutine[Any, Any, Any]]
|
||||
|
||||
class WSRouter:
|
||||
def __init__(self, loop: AbstractEventLoop, server_instance: Application) -> None:
|
||||
self.loop = loop
|
||||
self.ws: WebSocketResponse | None = None
|
||||
self.instance_id = 0
|
||||
self.routes: Dict[str, Route] = {}
|
||||
# self.subscriptions: Dict[str, Callable[[Any]]] = {}
|
||||
self.logger = getLogger("WSRouter")
|
||||
|
||||
server_instance.add_routes([
|
||||
get("/ws", self.handle)
|
||||
])
|
||||
|
||||
async def write(self, data: Dict[str, Any]):
|
||||
if self.ws != None:
|
||||
await self.ws.send_json(data)
|
||||
else:
|
||||
self.logger.warning("Dropping message as there is no connected socket: %s", data)
|
||||
|
||||
def add_route(self, name: str, route: Route):
|
||||
self.routes[name] = route
|
||||
|
||||
def remove_route(self, name: str):
|
||||
del self.routes[name]
|
||||
|
||||
async def _call_route(self, route: str, args: ..., call_id: int):
|
||||
instance_id = self.instance_id
|
||||
error = None
|
||||
try:
|
||||
res = await self.routes[route](*args)
|
||||
except Exception as err:
|
||||
error = {"name":err.__class__.__name__, "message":str(err), "traceback":format_exc()}
|
||||
res = None
|
||||
|
||||
if instance_id != self.instance_id:
|
||||
try:
|
||||
self.logger.warning("Ignoring %s reply from stale instance %d with args %s and response %s", route, instance_id, args, res)
|
||||
except:
|
||||
self.logger.warning("Ignoring %s reply from stale instance %d (failed to log event data)", route, instance_id)
|
||||
finally:
|
||||
return
|
||||
|
||||
if error:
|
||||
await self.write({"type": MessageType.ERROR.value, "id": call_id, "error": error})
|
||||
else:
|
||||
await self.write({"type": MessageType.REPLY.value, "id": call_id, "result": res})
|
||||
|
||||
async def handle(self, request: Request):
|
||||
# Auth is a query param as JS WebSocket doesn't support headers
|
||||
if request.rel_url.query["auth"] != get_csrf_token():
|
||||
return Response(text='Forbidden', status=403)
|
||||
self.logger.debug('Websocket connection starting')
|
||||
ws = WebSocketResponse()
|
||||
await ws.prepare(request)
|
||||
self.instance_id += 1
|
||||
self.logger.debug('Websocket connection ready')
|
||||
|
||||
if self.ws != None:
|
||||
try:
|
||||
await self.ws.close()
|
||||
except:
|
||||
pass
|
||||
self.ws = None
|
||||
|
||||
self.ws = ws
|
||||
|
||||
try:
|
||||
async for msg in ws:
|
||||
msg = cast(WSMessageExtra, msg)
|
||||
|
||||
if msg.type == WSMsgType.TEXT:
|
||||
if msg.data == 'close':
|
||||
# TODO DO NOT RELY ON THIS!
|
||||
break
|
||||
else:
|
||||
data = msg.json()
|
||||
match data["type"]:
|
||||
case MessageType.CALL.value:
|
||||
# do stuff with the message
|
||||
if data["route"] in self.routes:
|
||||
self.logger.debug(f'Started PY call {data["route"]} ID {data["id"]}')
|
||||
self.loop.create_task(self._call_route(data["route"], data["args"], data["id"]))
|
||||
else:
|
||||
error = {"error":f'Route {data["route"]} does not exist.', "name": "RouteNotFoundError", "traceback": None}
|
||||
self.loop.create_task(self.write({"type": MessageType.ERROR.value, "id": data["id"], "error": error}))
|
||||
case _:
|
||||
self.logger.error("Unknown message type", data)
|
||||
finally:
|
||||
try:
|
||||
await ws.close()
|
||||
self.ws = None
|
||||
except:
|
||||
pass
|
||||
|
||||
self.logger.debug('Websocket connection closed')
|
||||
return ws
|
||||
|
||||
async def emit(self, event: str, *args: Any):
|
||||
self.logger.debug(f'Firing frontend event {event} with args {args}')
|
||||
await self.write({ "type": MessageType.EVENT.value, "event": event, "args": args })
|
||||
|
||||
async def disconnect(self):
|
||||
if self.ws:
|
||||
await self.ws.close(code=WSCloseCode.GOING_AWAY, message=b"Loader is shutting down")
|
||||
@@ -1,126 +0,0 @@
|
||||
import re
|
||||
import ssl
|
||||
import subprocess
|
||||
import uuid
|
||||
import os
|
||||
from subprocess import check_output
|
||||
from time import sleep
|
||||
from hashlib import sha256
|
||||
from io import BytesIO
|
||||
|
||||
import certifi
|
||||
from aiohttp.web import Response, middleware
|
||||
from aiohttp import ClientSession
|
||||
|
||||
REMOTE_DEBUGGER_UNIT = "steam-web-debug-portforward.service"
|
||||
|
||||
# global vars
|
||||
csrf_token = str(uuid.uuid4())
|
||||
ssl_ctx = ssl.create_default_context(cafile=certifi.where())
|
||||
user = None
|
||||
group = None
|
||||
|
||||
assets_regex = re.compile("^/plugins/.*/assets/.*")
|
||||
frontend_regex = re.compile("^/frontend/.*")
|
||||
|
||||
def get_ssl_context():
|
||||
return ssl_ctx
|
||||
|
||||
def get_csrf_token():
|
||||
return csrf_token
|
||||
|
||||
@middleware
|
||||
async def csrf_middleware(request, handler):
|
||||
if str(request.method) == "OPTIONS" or request.headers.get('Authentication') == csrf_token or str(request.rel_url) == "/auth/token" or str(request.rel_url).startswith("/plugins/load_main/") or str(request.rel_url).startswith("/static/") or str(request.rel_url).startswith("/legacy/") or str(request.rel_url).startswith("/steam_resource/") or str(request.rel_url).startswith("/frontend/") or assets_regex.match(str(request.rel_url)) or frontend_regex.match(str(request.rel_url)):
|
||||
return await handler(request)
|
||||
return Response(text='Forbidden', status='403')
|
||||
|
||||
# Get the user by checking for the first logged in user. As this is run
|
||||
# by systemd at startup the process is likely to start before the user
|
||||
# logs in, so we will wait here until they are available. Note that
|
||||
# other methods such as getenv wont work as there was no $SUDO_USER to
|
||||
# start the systemd service.
|
||||
def set_user():
|
||||
global user
|
||||
cmd = "who | awk '{print $1}' | sort | head -1"
|
||||
while user == None:
|
||||
name = check_output(cmd, shell=True).decode().strip()
|
||||
if name not in [None, '']:
|
||||
user = name
|
||||
sleep(0.1)
|
||||
|
||||
# Get the global user. get_user must be called first.
|
||||
def get_user() -> str:
|
||||
global user
|
||||
if user == None:
|
||||
raise ValueError("helpers.get_user method called before user variable was set. Run helpers.set_user first.")
|
||||
return user
|
||||
|
||||
# Set the global user group. get_user must be called first
|
||||
def set_user_group() -> str:
|
||||
global group
|
||||
global user
|
||||
if user == None:
|
||||
raise ValueError("helpers.set_user_dir method called before user variable was set. Run helpers.set_user first.")
|
||||
if group == None:
|
||||
group = check_output(["id", "-g", "-n", user]).decode().strip()
|
||||
|
||||
# Get the group of the global user. set_user_group must be called first.
|
||||
def get_user_group() -> str:
|
||||
global group
|
||||
if group == None:
|
||||
raise ValueError("helpers.get_user_group method called before group variable was set. Run helpers.set_user_group first.")
|
||||
return group
|
||||
|
||||
# Get the default home path unless a user is specified
|
||||
def get_home_path(username = None) -> str:
|
||||
if username == None:
|
||||
raise ValueError("Username not defined, no home path can be found.")
|
||||
else:
|
||||
return str("/home/"+username)
|
||||
|
||||
# Get the default homebrew path unless a user is specified
|
||||
def get_homebrew_path(home_path = None) -> str:
|
||||
if home_path == None:
|
||||
raise ValueError("Home path not defined, homebrew dir cannot be determined.")
|
||||
else:
|
||||
return str(home_path+"/homebrew")
|
||||
# return str(home_path+"/homebrew")
|
||||
|
||||
# Download Remote Binaries to local Plugin
|
||||
async def download_remote_binary_to_path(url, binHash, path) -> bool:
|
||||
rv = False
|
||||
try:
|
||||
if os.access(os.path.dirname(path), os.W_OK):
|
||||
async with ClientSession() as client:
|
||||
res = await client.get(url, ssl=get_ssl_context())
|
||||
if res.status == 200:
|
||||
data = BytesIO(await res.read())
|
||||
remoteHash = sha256(data.getbuffer()).hexdigest()
|
||||
if binHash == remoteHash:
|
||||
data.seek(0)
|
||||
with open(path, 'wb') as f:
|
||||
f.write(data.getbuffer())
|
||||
rv = True
|
||||
else:
|
||||
raise Exception(f"Fatal Error: Hash Mismatch for remote binary {path}@{url}")
|
||||
else:
|
||||
rv = False
|
||||
except:
|
||||
rv = False
|
||||
|
||||
return rv
|
||||
|
||||
async def is_systemd_unit_active(unit_name: str) -> bool:
|
||||
res = subprocess.run(["systemctl", "is-active", unit_name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
return res.returncode == 0
|
||||
|
||||
async def stop_systemd_unit(unit_name: str) -> subprocess.CompletedProcess:
|
||||
cmd = ["systemctl", "stop", unit_name]
|
||||
|
||||
return subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
|
||||
async def start_systemd_unit(unit_name: str) -> subprocess.CompletedProcess:
|
||||
cmd = ["systemctl", "start", unit_name]
|
||||
|
||||
return subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
@@ -1,388 +0,0 @@
|
||||
# Injector code from https://github.com/SteamDeckHomebrew/steamdeck-ui-inject. More info on how it works there.
|
||||
|
||||
from asyncio import sleep
|
||||
from logging import getLogger
|
||||
from traceback import format_exc
|
||||
from typing import List
|
||||
|
||||
from aiohttp import ClientSession
|
||||
from aiohttp.client_exceptions import ClientConnectorError
|
||||
from asyncio.exceptions import TimeoutError
|
||||
import uuid
|
||||
|
||||
BASE_ADDRESS = "http://localhost:8080"
|
||||
|
||||
logger = getLogger("Injector")
|
||||
|
||||
|
||||
class Tab:
|
||||
cmd_id = 0
|
||||
|
||||
def __init__(self, res) -> None:
|
||||
self.title = res["title"]
|
||||
self.id = res["id"]
|
||||
self.url = res["url"]
|
||||
self.ws_url = res["webSocketDebuggerUrl"]
|
||||
|
||||
self.websocket = None
|
||||
self.client = None
|
||||
|
||||
async def open_websocket(self):
|
||||
self.client = ClientSession()
|
||||
self.websocket = await self.client.ws_connect(self.ws_url)
|
||||
|
||||
async def close_websocket(self):
|
||||
await self.client.close()
|
||||
|
||||
async def listen_for_message(self):
|
||||
async for message in self.websocket:
|
||||
data = message.json()
|
||||
yield data
|
||||
|
||||
async def _send_devtools_cmd(self, dc, receive=True):
|
||||
if self.websocket:
|
||||
self.cmd_id += 1
|
||||
dc["id"] = self.cmd_id
|
||||
await self.websocket.send_json(dc)
|
||||
if receive:
|
||||
async for msg in self.listen_for_message():
|
||||
if "id" in msg and msg["id"] == dc["id"]:
|
||||
return msg
|
||||
return None
|
||||
raise RuntimeError("Websocket not opened")
|
||||
|
||||
async def evaluate_js(self, js, run_async=False, manage_socket=True, get_result=True):
|
||||
if manage_socket:
|
||||
await self.open_websocket()
|
||||
|
||||
res = await self._send_devtools_cmd({
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {
|
||||
"expression": js,
|
||||
"userGesture": True,
|
||||
"awaitPromise": run_async
|
||||
}
|
||||
}, get_result)
|
||||
|
||||
if manage_socket:
|
||||
await self.close_websocket()
|
||||
return res
|
||||
|
||||
async def has_global_var(self, var_name, manage_socket=True):
|
||||
res = await self.evaluate_js(f"window['{var_name}'] !== null && window['{var_name}'] !== undefined", False, manage_socket)
|
||||
|
||||
if not "result" in res or not "result" in res["result"] or not "value" in res["result"]["result"]:
|
||||
return False
|
||||
|
||||
return res["result"]["result"]["value"]
|
||||
|
||||
async def close(self, manage_socket=True):
|
||||
if manage_socket:
|
||||
await self.open_websocket()
|
||||
|
||||
res = await self._send_devtools_cmd({
|
||||
"method": "Page.close",
|
||||
}, False)
|
||||
|
||||
if manage_socket:
|
||||
await self.close_websocket()
|
||||
return res
|
||||
|
||||
async def enable(self):
|
||||
"""
|
||||
Enables page domain notifications.
|
||||
"""
|
||||
await self._send_devtools_cmd({
|
||||
"method": "Page.enable",
|
||||
}, False)
|
||||
|
||||
async def disable(self):
|
||||
"""
|
||||
Disables page domain notifications.
|
||||
"""
|
||||
await self._send_devtools_cmd({
|
||||
"method": "Page.disable",
|
||||
}, False)
|
||||
|
||||
async def refresh(self):
|
||||
if manage_socket:
|
||||
await self.open_websocket()
|
||||
|
||||
await self._send_devtools_cmd({
|
||||
"method": "Page.reload",
|
||||
}, False)
|
||||
|
||||
if manage_socket:
|
||||
await self.close_websocket()
|
||||
|
||||
return
|
||||
async def reload_and_evaluate(self, js, manage_socket=True):
|
||||
"""
|
||||
Reloads the current tab, with JS to run on load via debugger
|
||||
"""
|
||||
if manage_socket:
|
||||
await self.open_websocket()
|
||||
|
||||
await self._send_devtools_cmd({
|
||||
"method": "Debugger.enable"
|
||||
}, True)
|
||||
|
||||
await self._send_devtools_cmd({
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {
|
||||
"expression": "location.reload();",
|
||||
"userGesture": True,
|
||||
"awaitPromise": False
|
||||
}
|
||||
}, False)
|
||||
|
||||
breakpoint_res = await self._send_devtools_cmd({
|
||||
"method": "Debugger.setInstrumentationBreakpoint",
|
||||
"params": {
|
||||
"instrumentation": "beforeScriptExecution"
|
||||
}
|
||||
}, True)
|
||||
|
||||
logger.info(breakpoint_res)
|
||||
|
||||
# Page finishes loading when breakpoint hits
|
||||
|
||||
for x in range(20):
|
||||
# this works around 1/5 of the time, so just send it 8 times.
|
||||
# the js accounts for being injected multiple times allowing only one instance to run at a time anyway
|
||||
await self._send_devtools_cmd({
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {
|
||||
"expression": js,
|
||||
"userGesture": True,
|
||||
"awaitPromise": False
|
||||
}
|
||||
}, False)
|
||||
|
||||
await self._send_devtools_cmd({
|
||||
"method": "Debugger.removeBreakpoint",
|
||||
"params": {
|
||||
"breakpointId": breakpoint_res["result"]["breakpointId"]
|
||||
}
|
||||
}, False)
|
||||
|
||||
for x in range(4):
|
||||
await self._send_devtools_cmd({
|
||||
"method": "Debugger.resume"
|
||||
}, False)
|
||||
|
||||
await self._send_devtools_cmd({
|
||||
"method": "Debugger.disable"
|
||||
}, True)
|
||||
|
||||
if manage_socket:
|
||||
await self.close_websocket()
|
||||
return
|
||||
|
||||
async def add_script_to_evaluate_on_new_document(self, js, add_dom_wrapper=True, manage_socket=True, get_result=True):
|
||||
"""
|
||||
How the underlying call functions is not particularly clear from the devtools docs, so stealing puppeteer's description:
|
||||
|
||||
Adds a function which would be invoked in one of the following scenarios:
|
||||
* whenever the page is navigated
|
||||
* whenever the child frame is attached or navigated. In this case, the
|
||||
function is invoked in the context of the newly attached frame.
|
||||
|
||||
The function is invoked after the document was created but before any of
|
||||
its scripts were run. This is useful to amend the JavaScript environment,
|
||||
e.g. to seed `Math.random`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
js : str
|
||||
The script to evaluate on new document
|
||||
add_dom_wrapper : bool
|
||||
True to wrap the script in a wait for the 'DOMContentLoaded' event.
|
||||
DOM will usually not exist when this execution happens,
|
||||
so it is necessary to delay til DOM is loaded if you are modifying it
|
||||
manage_socket : bool
|
||||
True to have this function handle opening/closing the websocket for this tab
|
||||
get_result : bool
|
||||
True to wait for the result of this call
|
||||
|
||||
Returns
|
||||
-------
|
||||
int or None
|
||||
The identifier of the script added, used to remove it later.
|
||||
(see remove_script_to_evaluate_on_new_document below)
|
||||
None is returned if `get_result` is False
|
||||
"""
|
||||
|
||||
wrappedjs = """
|
||||
function scriptFunc() {
|
||||
{js}
|
||||
}
|
||||
if (document.readyState === 'loading') {
|
||||
addEventListener('DOMContentLoaded', () => {
|
||||
scriptFunc();
|
||||
});
|
||||
} else {
|
||||
scriptFunc();
|
||||
}
|
||||
""".format(js=js) if add_dom_wrapper else js
|
||||
|
||||
if manage_socket:
|
||||
await self.open_websocket()
|
||||
|
||||
res = await self._send_devtools_cmd({
|
||||
"method": "Page.addScriptToEvaluateOnNewDocument",
|
||||
"params": {
|
||||
"source": wrappedjs
|
||||
}
|
||||
}, get_result)
|
||||
|
||||
if manage_socket:
|
||||
await self.close_websocket()
|
||||
return res
|
||||
|
||||
async def remove_script_to_evaluate_on_new_document(self, script_id, manage_socket=True):
|
||||
"""
|
||||
Removes a script from a page that was added with `add_script_to_evaluate_on_new_document`
|
||||
|
||||
Parameters
|
||||
----------
|
||||
script_id : int
|
||||
The identifier of the script to remove (returned from `add_script_to_evaluate_on_new_document`)
|
||||
"""
|
||||
|
||||
if manage_socket:
|
||||
await self.open_websocket()
|
||||
|
||||
res = await self._send_devtools_cmd({
|
||||
"method": "Page.removeScriptToEvaluateOnNewDocument",
|
||||
"params": {
|
||||
"identifier": script_id
|
||||
}
|
||||
}, False)
|
||||
|
||||
if manage_socket:
|
||||
await self.close_websocket()
|
||||
|
||||
async def has_element(self, element_name, manage_socket=True):
|
||||
res = await self.evaluate_js(f"document.getElementById('{element_name}') != null", False, manage_socket)
|
||||
|
||||
if not "result" in res or not "result" in res["result"] or not "value" in res["result"]["result"]:
|
||||
return False
|
||||
|
||||
return res["result"]["result"]["value"]
|
||||
|
||||
async def inject_css(self, style, manage_socket=True):
|
||||
try:
|
||||
css_id = str(uuid.uuid4())
|
||||
|
||||
result = await self.evaluate_js(
|
||||
f"""
|
||||
(function() {{
|
||||
const style = document.createElement('style');
|
||||
style.id = "{css_id}";
|
||||
document.head.append(style);
|
||||
style.textContent = `{style}`;
|
||||
}})()
|
||||
""", False, manage_socket)
|
||||
|
||||
if "exceptionDetails" in result["result"]:
|
||||
return {
|
||||
"success": False,
|
||||
"result": result["result"]
|
||||
}
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"result": css_id
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"result": e
|
||||
}
|
||||
|
||||
async def remove_css(self, css_id, manage_socket=True):
|
||||
try:
|
||||
result = await self.evaluate_js(
|
||||
f"""
|
||||
(function() {{
|
||||
let style = document.getElementById("{css_id}");
|
||||
|
||||
if (style.nodeName.toLowerCase() == 'style')
|
||||
style.parentNode.removeChild(style);
|
||||
}})()
|
||||
""", False, manage_socket)
|
||||
|
||||
if "exceptionDetails" in result["result"]:
|
||||
return {
|
||||
"success": False,
|
||||
"result": result
|
||||
}
|
||||
|
||||
return {
|
||||
"success": True
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"result": e
|
||||
}
|
||||
|
||||
async def get_steam_resource(self, url):
|
||||
res = await self.evaluate_js(f'(async function test() {{ return await (await fetch("{url}")).text() }})()', True)
|
||||
return res["result"]["result"]["value"]
|
||||
|
||||
def __repr__(self):
|
||||
return self.title
|
||||
|
||||
|
||||
async def get_tabs() -> List[Tab]:
|
||||
async with ClientSession() as web:
|
||||
res = {}
|
||||
|
||||
while True:
|
||||
try:
|
||||
res = await web.get(f"{BASE_ADDRESS}/json", timeout=3)
|
||||
except ClientConnectorError:
|
||||
logger.debug("ClientConnectorError excepted.")
|
||||
logger.debug("Steam isn't available yet. Wait for a moment...")
|
||||
logger.error(format_exc())
|
||||
await sleep(5)
|
||||
except TimeoutError:
|
||||
logger.warn(f"The request to {BASE_ADDRESS}/json timed out")
|
||||
await sleep(1)
|
||||
else:
|
||||
break
|
||||
|
||||
if res.status == 200:
|
||||
r = await res.json()
|
||||
return [Tab(i) for i in r]
|
||||
else:
|
||||
raise Exception(f"/json did not return 200. {await res.text()}")
|
||||
|
||||
|
||||
async def get_tab(tab_name) -> Tab:
|
||||
tabs = await get_tabs()
|
||||
tab = next((i for i in tabs if i.title == tab_name), None)
|
||||
if not tab:
|
||||
raise ValueError(f"Tab {tab_name} not found")
|
||||
return tab
|
||||
|
||||
async def get_tab_lambda(test) -> Tab:
|
||||
tabs = await get_tabs()
|
||||
tab = next((i for i in tabs if test(i)), None)
|
||||
if not tab:
|
||||
raise ValueError(f"Tab not found by lambda")
|
||||
return tab
|
||||
|
||||
async def get_gamepadui_tab() -> Tab:
|
||||
tabs = await get_tabs()
|
||||
tab = next((i for i in tabs if ("https://steamloopback.host/routes/" in i.url and (i.title == "Steam" or i.title == "SP"))), None)
|
||||
if not tab:
|
||||
raise ValueError(f"GamepadUI Tab not found")
|
||||
return tab
|
||||
|
||||
async def inject_to_tab(tab_name, js, run_async=False):
|
||||
tab = await get_tab(tab_name)
|
||||
|
||||
return await tab.evaluate_js(js, run_async)
|
||||
@@ -1,84 +0,0 @@
|
||||
class PluginEventTarget extends EventTarget { }
|
||||
method_call_ev_target = new PluginEventTarget();
|
||||
|
||||
window.addEventListener("message", function(evt) {
|
||||
let ev = new Event(evt.data.call_id);
|
||||
ev.data = evt.data.result;
|
||||
method_call_ev_target.dispatchEvent(ev);
|
||||
}, false);
|
||||
|
||||
async function call_server_method(method_name, arg_object={}) {
|
||||
const token = await fetch("http://127.0.0.1:1337/auth/token").then(r => r.text());
|
||||
const response = await fetch(`http://127.0.0.1:1337/methods/${method_name}`, {
|
||||
method: 'POST',
|
||||
credentials: "include",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authentication: token
|
||||
},
|
||||
body: JSON.stringify(arg_object),
|
||||
});
|
||||
|
||||
const dta = await response.json();
|
||||
if (!dta.success) throw dta.result;
|
||||
return dta.result;
|
||||
}
|
||||
|
||||
// Source: https://stackoverflow.com/a/2117523 Thanks!
|
||||
function uuidv4() {
|
||||
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
|
||||
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
|
||||
);
|
||||
}
|
||||
|
||||
async function fetch_nocors(url, request={}) {
|
||||
let args = { method: "POST", headers: {}, body: "" };
|
||||
request = {...args, ...request};
|
||||
request.url = url;
|
||||
request.data = request.body;
|
||||
delete request.body; //maintain api-compatibility with fetch
|
||||
return await call_server_method("http_request", request);
|
||||
}
|
||||
|
||||
async function call_plugin_method(method_name, arg_object={}) {
|
||||
if (plugin_name == undefined)
|
||||
throw new Error("Plugin methods can only be called from inside plugins (duh)");
|
||||
const token = await fetch("http://127.0.0.1:1337/auth/token").then(r => r.text());
|
||||
const response = await fetch(`http://127.0.0.1:1337/plugins/${plugin_name}/methods/${method_name}`, {
|
||||
method: 'POST',
|
||||
credentials: "include",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authentication: token
|
||||
},
|
||||
body: JSON.stringify({
|
||||
args: arg_object,
|
||||
}),
|
||||
});
|
||||
|
||||
const dta = await response.json();
|
||||
if (!dta.success) throw dta.result;
|
||||
return dta.result;
|
||||
}
|
||||
|
||||
async function execute_in_tab(tab, run_async, code) {
|
||||
return await call_server_method("execute_in_tab", {
|
||||
'tab': tab,
|
||||
'run_async': run_async,
|
||||
'code': code
|
||||
});
|
||||
}
|
||||
|
||||
async function inject_css_into_tab(tab, style) {
|
||||
return await call_server_method("inject_css_into_tab", {
|
||||
'tab': tab,
|
||||
'style': style
|
||||
});
|
||||
}
|
||||
|
||||
async function remove_css_from_tab(tab, css_id) {
|
||||
return await call_server_method("remove_css_from_tab", {
|
||||
'tab': tab,
|
||||
'css_id': css_id
|
||||
});
|
||||
}
|
||||
@@ -1,215 +0,0 @@
|
||||
from asyncio import Queue, sleep
|
||||
from json.decoder import JSONDecodeError
|
||||
from logging import getLogger
|
||||
from os import listdir, path
|
||||
from pathlib import Path
|
||||
from traceback import print_exc
|
||||
|
||||
from aiohttp import web
|
||||
from genericpath import exists
|
||||
from watchdog.events import RegexMatchingEventHandler
|
||||
from watchdog.utils import UnsupportedLibc
|
||||
|
||||
try:
|
||||
from watchdog.observers.inotify import InotifyObserver as Observer
|
||||
except UnsupportedLibc:
|
||||
from watchdog.observers.fsevents import FSEventsObserver as Observer
|
||||
|
||||
from injector import get_tab, get_gamepadui_tab
|
||||
from plugin import PluginWrapper
|
||||
|
||||
|
||||
class FileChangeHandler(RegexMatchingEventHandler):
|
||||
def __init__(self, queue, plugin_path) -> None:
|
||||
super().__init__(regexes=[r'^.*?dist\/index\.js$', r'^.*?main\.py$'])
|
||||
self.logger = getLogger("file-watcher")
|
||||
self.plugin_path = plugin_path
|
||||
self.queue = queue
|
||||
self.disabled = True
|
||||
|
||||
def maybe_reload(self, src_path):
|
||||
if self.disabled:
|
||||
return
|
||||
plugin_dir = Path(path.relpath(src_path, self.plugin_path)).parts[0]
|
||||
if exists(path.join(self.plugin_path, plugin_dir, "plugin.json")):
|
||||
self.queue.put_nowait((path.join(self.plugin_path, plugin_dir, "main.py"), plugin_dir, True))
|
||||
|
||||
def on_created(self, event):
|
||||
src_path = event.src_path
|
||||
if "__pycache__" in src_path:
|
||||
return
|
||||
|
||||
# check to make sure this isn't a directory
|
||||
if path.isdir(src_path):
|
||||
return
|
||||
|
||||
# get the directory name of the plugin so that we can find its "main.py" and reload it; the
|
||||
# file that changed is not necessarily the one that needs to be reloaded
|
||||
self.logger.debug(f"file created: {src_path}")
|
||||
self.maybe_reload(src_path)
|
||||
|
||||
def on_modified(self, event):
|
||||
src_path = event.src_path
|
||||
if "__pycache__" in src_path:
|
||||
return
|
||||
|
||||
# check to make sure this isn't a directory
|
||||
if path.isdir(src_path):
|
||||
return
|
||||
|
||||
# get the directory name of the plugin so that we can find its "main.py" and reload it; the
|
||||
# file that changed is not necessarily the one that needs to be reloaded
|
||||
self.logger.debug(f"file modified: {src_path}")
|
||||
self.maybe_reload(src_path)
|
||||
|
||||
class Loader:
|
||||
def __init__(self, server_instance, plugin_path, loop, live_reload=False) -> None:
|
||||
self.loop = loop
|
||||
self.logger = getLogger("Loader")
|
||||
self.plugin_path = plugin_path
|
||||
self.logger.info(f"plugin_path: {self.plugin_path}")
|
||||
self.plugins = {}
|
||||
self.watcher = None
|
||||
self.live_reload = live_reload
|
||||
|
||||
if live_reload:
|
||||
self.reload_queue = Queue()
|
||||
self.observer = Observer()
|
||||
self.watcher = FileChangeHandler(self.reload_queue, plugin_path)
|
||||
self.observer.schedule(self.watcher, self.plugin_path, recursive=True)
|
||||
self.observer.start()
|
||||
self.loop.create_task(self.handle_reloads())
|
||||
self.loop.create_task(self.enable_reload_wait())
|
||||
|
||||
server_instance.add_routes([
|
||||
web.get("/frontend/{path:.*}", self.handle_frontend_assets),
|
||||
web.get("/plugins", self.get_plugins),
|
||||
web.get("/plugins/{plugin_name}/frontend_bundle", self.handle_frontend_bundle),
|
||||
web.post("/plugins/{plugin_name}/methods/{method_name}", self.handle_plugin_method_call),
|
||||
web.get("/plugins/{plugin_name}/assets/{path:.*}", self.handle_plugin_frontend_assets),
|
||||
|
||||
# The following is legacy plugin code.
|
||||
web.get("/plugins/load_main/{name}", self.load_plugin_main_view),
|
||||
web.get("/plugins/plugin_resource/{name}/{path:.+}", self.handle_sub_route),
|
||||
web.get("/steam_resource/{path:.+}", self.get_steam_resource)
|
||||
])
|
||||
|
||||
async def enable_reload_wait(self):
|
||||
if self.live_reload:
|
||||
await sleep(10)
|
||||
self.logger.info("Hot reload enabled")
|
||||
self.watcher.disabled = False
|
||||
|
||||
async def handle_frontend_assets(self, request):
|
||||
file = path.join(path.dirname(__file__), "static", request.match_info["path"])
|
||||
|
||||
return web.FileResponse(file, headers={"Cache-Control": "no-cache"})
|
||||
|
||||
async def get_plugins(self, request):
|
||||
plugins = list(self.plugins.values())
|
||||
return web.json_response([{"name": str(i) if not i.legacy else "$LEGACY_"+str(i), "version": i.version} for i in plugins])
|
||||
|
||||
def handle_plugin_frontend_assets(self, request):
|
||||
plugin = self.plugins[request.match_info["plugin_name"]]
|
||||
file = path.join(self.plugin_path, plugin.plugin_directory, "dist/assets", request.match_info["path"])
|
||||
|
||||
return web.FileResponse(file, headers={"Cache-Control": "no-cache"})
|
||||
|
||||
def handle_frontend_bundle(self, request):
|
||||
plugin = self.plugins[request.match_info["plugin_name"]]
|
||||
|
||||
with open(path.join(self.plugin_path, plugin.plugin_directory, "dist/index.js"), 'r') as bundle:
|
||||
return web.Response(text=bundle.read(), content_type="application/javascript")
|
||||
|
||||
def import_plugin(self, file, plugin_directory, refresh=False, batch=False):
|
||||
try:
|
||||
plugin = PluginWrapper(file, plugin_directory, self.plugin_path)
|
||||
if plugin.name in self.plugins:
|
||||
if not "debug" in plugin.flags and refresh:
|
||||
self.logger.info(f"Plugin {plugin.name} is already loaded and has requested to not be re-loaded")
|
||||
return
|
||||
else:
|
||||
self.plugins[plugin.name].stop()
|
||||
self.plugins.pop(plugin.name, None)
|
||||
if plugin.passive:
|
||||
self.logger.info(f"Plugin {plugin.name} is passive")
|
||||
self.plugins[plugin.name] = plugin.start()
|
||||
self.logger.info(f"Loaded {plugin.name}")
|
||||
if not batch:
|
||||
self.loop.create_task(self.dispatch_plugin(plugin.name if not plugin.legacy else "$LEGACY_" + plugin.name, plugin.version))
|
||||
except Exception as e:
|
||||
self.logger.error(f"Could not load {file}. {e}")
|
||||
print_exc()
|
||||
|
||||
async def dispatch_plugin(self, name, version):
|
||||
gpui_tab = await get_gamepadui_tab()
|
||||
await gpui_tab.evaluate_js(f"window.importDeckyPlugin('{name}', '{version}')")
|
||||
|
||||
def import_plugins(self):
|
||||
self.logger.info(f"import plugins from {self.plugin_path}")
|
||||
|
||||
directories = [i for i in listdir(self.plugin_path) if path.isdir(path.join(self.plugin_path, i)) and path.isfile(path.join(self.plugin_path, i, "plugin.json"))]
|
||||
for directory in directories:
|
||||
self.logger.info(f"found plugin: {directory}")
|
||||
self.import_plugin(path.join(self.plugin_path, directory, "main.py"), directory, False, True)
|
||||
|
||||
async def handle_reloads(self):
|
||||
while True:
|
||||
args = await self.reload_queue.get()
|
||||
self.import_plugin(*args)
|
||||
|
||||
async def handle_plugin_method_call(self, request):
|
||||
res = {}
|
||||
plugin = self.plugins[request.match_info["plugin_name"]]
|
||||
method_name = request.match_info["method_name"]
|
||||
try:
|
||||
method_info = await request.json()
|
||||
args = method_info["args"]
|
||||
except JSONDecodeError:
|
||||
args = {}
|
||||
try:
|
||||
if method_name.startswith("_"):
|
||||
raise RuntimeError("Tried to call private method")
|
||||
res["result"] = await plugin.execute_method(method_name, args)
|
||||
res["success"] = True
|
||||
except Exception as e:
|
||||
res["result"] = str(e)
|
||||
res["success"] = False
|
||||
return web.json_response(res)
|
||||
|
||||
"""
|
||||
The following methods are used to load legacy plugins, which are considered deprecated.
|
||||
I made the choice to re-add them so that the first iteration/version of the react loader
|
||||
can work as a drop-in replacement for the stable branch of the PluginLoader, so that we
|
||||
can introduce it more smoothly and give people the chance to sample the new features even
|
||||
without plugin support. They will be removed once legacy plugins are no longer relevant.
|
||||
"""
|
||||
async def load_plugin_main_view(self, request):
|
||||
plugin = self.plugins[request.match_info["name"]]
|
||||
with open(path.join(self.plugin_path, plugin.plugin_directory, plugin.main_view_html), 'r') as template:
|
||||
template_data = template.read()
|
||||
ret = f"""
|
||||
<script src="/legacy/library.js"></script>
|
||||
<script>window.plugin_name = '{plugin.name}' </script>
|
||||
<base href="http://127.0.0.1:1337/plugins/plugin_resource/{plugin.name}/">
|
||||
{template_data}
|
||||
"""
|
||||
return web.Response(text=ret, content_type="text/html")
|
||||
|
||||
async def handle_sub_route(self, request):
|
||||
plugin = self.plugins[request.match_info["name"]]
|
||||
route_path = request.match_info["path"]
|
||||
self.logger.info(path)
|
||||
ret = ""
|
||||
file_path = path.join(self.plugin_path, plugin.plugin_directory, route_path)
|
||||
with open(file_path, 'r') as resource_data:
|
||||
ret = resource_data.read()
|
||||
|
||||
return web.Response(text=ret)
|
||||
|
||||
async def get_steam_resource(self, request):
|
||||
tab = await get_tab("QuickAccess")
|
||||
try:
|
||||
return web.Response(text=await tab.get_steam_resource(f"https://steamloopback.host/{request.match_info['path']}"), content_type="text/html")
|
||||
except Exception as e:
|
||||
return web.Response(text=str(e), status=400)
|
||||
+3
-171
@@ -1,172 +1,4 @@
|
||||
# Change PyInstaller files permissions
|
||||
import sys
|
||||
from subprocess import call
|
||||
if hasattr(sys, '_MEIPASS'):
|
||||
call(['chmod', '-R', '755', sys._MEIPASS])
|
||||
# Full imports
|
||||
from asyncio import get_event_loop, sleep
|
||||
from json import dumps, loads
|
||||
from logging import DEBUG, INFO, basicConfig, getLogger
|
||||
from os import getenv, chmod, path
|
||||
from traceback import format_exc
|
||||
|
||||
import aiohttp_cors
|
||||
# Partial imports
|
||||
from aiohttp import ClientSession, client_exceptions
|
||||
from aiohttp.web import Application, Response, get, run_app, static
|
||||
from aiohttp_jinja2 import setup as jinja_setup
|
||||
|
||||
# local modules
|
||||
from browser import PluginBrowser
|
||||
from helpers import (REMOTE_DEBUGGER_UNIT, csrf_middleware, get_csrf_token,
|
||||
get_home_path, get_homebrew_path, get_user,
|
||||
get_user_group, set_user, set_user_group,
|
||||
stop_systemd_unit)
|
||||
from injector import get_gamepadui_tab, Tab, get_tabs
|
||||
from loader import Loader
|
||||
from settings import SettingsManager
|
||||
from updater import Updater
|
||||
from utilities import Utilities
|
||||
|
||||
# Ensure USER and GROUP vars are set first.
|
||||
# TODO: This isn't the best way to do this but supports the current
|
||||
# implementation. All the config load and environment setting eventually be
|
||||
# moved into init or a config/loader method.
|
||||
set_user()
|
||||
set_user_group()
|
||||
USER = get_user()
|
||||
GROUP = get_user_group()
|
||||
HOME_PATH = "/home/"+USER
|
||||
HOMEBREW_PATH = HOME_PATH+"/homebrew"
|
||||
CONFIG = {
|
||||
"plugin_path": getenv("PLUGIN_PATH", HOMEBREW_PATH+"/plugins"),
|
||||
"chown_plugin_path": getenv("CHOWN_PLUGIN_PATH", "1") == "1",
|
||||
"server_host": getenv("SERVER_HOST", "127.0.0.1"),
|
||||
"server_port": int(getenv("SERVER_PORT", "1337")),
|
||||
"live_reload": getenv("LIVE_RELOAD", "1") == "1",
|
||||
"log_level": {"CRITICAL": 50, "ERROR": 40, "WARNING": 30, "INFO": 20, "DEBUG": 10}[
|
||||
getenv("LOG_LEVEL", "INFO")
|
||||
],
|
||||
}
|
||||
|
||||
basicConfig(
|
||||
level=CONFIG["log_level"],
|
||||
format="[%(module)s][%(levelname)s]: %(message)s"
|
||||
)
|
||||
|
||||
logger = getLogger("Main")
|
||||
|
||||
async def chown_plugin_dir(_):
|
||||
code_chown = call(["chown", "-R", USER+":"+GROUP, CONFIG["plugin_path"]])
|
||||
code_chmod = call(["chmod", "-R", "555", CONFIG["plugin_path"]])
|
||||
if code_chown != 0 or code_chmod != 0:
|
||||
logger.error(f"chown/chmod exited with a non-zero exit code (chown: {code_chown}, chmod: {code_chmod})")
|
||||
|
||||
class PluginManager:
|
||||
def __init__(self) -> None:
|
||||
self.loop = get_event_loop()
|
||||
self.web_app = Application()
|
||||
self.web_app.middlewares.append(csrf_middleware)
|
||||
self.cors = aiohttp_cors.setup(self.web_app, defaults={
|
||||
"https://steamloopback.host": aiohttp_cors.ResourceOptions(
|
||||
expose_headers="*",
|
||||
allow_headers="*",
|
||||
allow_credentials=True
|
||||
)
|
||||
})
|
||||
self.plugin_loader = Loader(self.web_app, CONFIG["plugin_path"], self.loop, CONFIG["live_reload"])
|
||||
self.plugin_browser = PluginBrowser(CONFIG["plugin_path"], self.plugin_loader.plugins, self.plugin_loader)
|
||||
self.settings = SettingsManager("loader", path.join(HOMEBREW_PATH, "settings"))
|
||||
self.utilities = Utilities(self)
|
||||
self.updater = Updater(self)
|
||||
|
||||
jinja_setup(self.web_app)
|
||||
if CONFIG["chown_plugin_path"] == True:
|
||||
self.web_app.on_startup.append(chown_plugin_dir)
|
||||
self.loop.create_task(self.loader_reinjector())
|
||||
self.loop.create_task(self.load_plugins())
|
||||
if not self.settings.getSetting("cef_forward", False):
|
||||
self.loop.create_task(stop_systemd_unit(REMOTE_DEBUGGER_UNIT))
|
||||
self.loop.set_exception_handler(self.exception_handler)
|
||||
self.web_app.add_routes([get("/auth/token", self.get_auth_token)])
|
||||
|
||||
for route in list(self.web_app.router.routes()):
|
||||
self.cors.add(route)
|
||||
self.web_app.add_routes([static("/static", path.join(path.dirname(__file__), 'static'))])
|
||||
self.web_app.add_routes([static("/legacy", path.join(path.dirname(__file__), 'legacy'))])
|
||||
|
||||
def exception_handler(self, loop, context):
|
||||
if context["message"] == "Unclosed connection":
|
||||
return
|
||||
loop.default_exception_handler(context)
|
||||
|
||||
async def get_auth_token(self, request):
|
||||
return Response(text=get_csrf_token())
|
||||
|
||||
async def wait_for_server(self):
|
||||
async with ClientSession() as web:
|
||||
while True:
|
||||
try:
|
||||
await web.get(f"http://{CONFIG['server_host']}:{CONFIG['server_port']}")
|
||||
return
|
||||
except Exception as e:
|
||||
await sleep(0.1)
|
||||
|
||||
async def load_plugins(self):
|
||||
await self.wait_for_server()
|
||||
self.plugin_loader.import_plugins()
|
||||
# await inject_to_tab("SP", "window.syncDeckyPlugins();")
|
||||
|
||||
async def loader_reinjector(self):
|
||||
while True:
|
||||
tab = None
|
||||
while not tab:
|
||||
try:
|
||||
tab = await get_gamepadui_tab()
|
||||
except client_exceptions.ClientConnectorError or client_exceptions.ServerDisconnectedError:
|
||||
logger.debug("Couldn't connect to debugger, waiting 5 seconds.")
|
||||
pass
|
||||
except ValueError:
|
||||
logger.debug("Couldn't find GamepadUI tab, waiting 5 seconds")
|
||||
pass
|
||||
if not tab:
|
||||
await sleep(5)
|
||||
await tab.open_websocket()
|
||||
await tab.enable()
|
||||
await self.inject_javascript(tab, True)
|
||||
async for msg in tab.listen_for_message():
|
||||
logger.debug("Page event: " + str(msg.get("method", None)))
|
||||
if msg.get("method", None) == "Page.domContentEventFired":
|
||||
if not await tab.has_global_var("deckyHasLoaded", False):
|
||||
await self.inject_javascript(tab)
|
||||
if msg.get("method", None) == "Inspector.detached":
|
||||
logger.info("Steam is exiting...")
|
||||
await tab.close_websocket()
|
||||
break
|
||||
# while True:
|
||||
# await sleep(5)
|
||||
# if not await tab.has_global_var("deckyHasLoaded", False):
|
||||
# logger.info("Plugin loader isn't present in Steam anymore, reinjecting...")
|
||||
# await self.inject_javascript(tab)
|
||||
|
||||
async def inject_javascript(self, tab: Tab, first=False, request=None):
|
||||
logger.info("Loading Decky frontend!")
|
||||
try:
|
||||
if first:
|
||||
if await tab.has_global_var("deckyHasLoaded", False):
|
||||
tabs = await get_tabs()
|
||||
for t in tabs:
|
||||
if t.title != "Steam" and t.title != "SP":
|
||||
logger.debug("Closing tab: " + getattr(t, "title", "Untitled"))
|
||||
await t.close()
|
||||
await sleep(0.5)
|
||||
await tab.evaluate_js("try{if (window.deckyHasLoaded){setTimeout(() => location.reload(), 1000)}window.deckyHasLoaded = true;(async()=>{while(!window.SP_REACT){await new Promise(r => setTimeout(r, 10))};await import('http://localhost:1337/frontend/index.js')})();}catch(e){console.error(e)}", False, False, False)
|
||||
except:
|
||||
logger.info("Failed to inject JavaScript into tab\n" + format_exc())
|
||||
pass
|
||||
|
||||
def run(self):
|
||||
return run_app(self.web_app, host=CONFIG["server_host"], port=CONFIG["server_port"], loop=self.loop, access_log=None)
|
||||
|
||||
# This file is needed to make the relative imports in decky_loader/ work properly.
|
||||
if __name__ == "__main__":
|
||||
PluginManager().run()
|
||||
from decky_loader.main import main
|
||||
main()
|
||||
|
||||
@@ -1,171 +0,0 @@
|
||||
import multiprocessing
|
||||
from asyncio import (Lock, get_event_loop, new_event_loop,
|
||||
open_unix_connection, set_event_loop, sleep,
|
||||
start_unix_server, IncompleteReadError, LimitOverrunError)
|
||||
from concurrent.futures import ProcessPoolExecutor
|
||||
from importlib.util import module_from_spec, spec_from_file_location
|
||||
from json import dumps, load, loads
|
||||
from logging import getLogger
|
||||
from traceback import format_exc
|
||||
from os import path, setgid, setuid
|
||||
from signal import SIGINT, signal
|
||||
from sys import exit
|
||||
from time import time
|
||||
|
||||
multiprocessing.set_start_method("fork")
|
||||
|
||||
BUFFER_LIMIT = 2 ** 20 # 1 MiB
|
||||
|
||||
class PluginWrapper:
|
||||
def __init__(self, file, plugin_directory, plugin_path) -> None:
|
||||
self.file = file
|
||||
self.plugin_directory = plugin_directory
|
||||
self.reader = None
|
||||
self.writer = None
|
||||
self.socket_addr = f"/tmp/plugin_socket_{time()}"
|
||||
self.method_call_lock = Lock()
|
||||
|
||||
self.version = None
|
||||
|
||||
json = load(open(path.join(plugin_path, plugin_directory, "plugin.json"), "r"))
|
||||
if path.isfile(path.join(plugin_path, plugin_directory, "package.json")):
|
||||
package_json = load(open(path.join(plugin_path, plugin_directory, "package.json"), "r"))
|
||||
self.version = package_json["version"]
|
||||
|
||||
|
||||
self.legacy = False
|
||||
self.main_view_html = json["main_view_html"] if "main_view_html" in json else ""
|
||||
self.tile_view_html = json["tile_view_html"] if "tile_view_html" in json else ""
|
||||
self.legacy = self.main_view_html or self.tile_view_html
|
||||
|
||||
self.name = json["name"]
|
||||
self.author = json["author"]
|
||||
self.flags = json["flags"]
|
||||
|
||||
self.log = getLogger("plugin")
|
||||
|
||||
self.passive = not path.isfile(self.file)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
def _init(self):
|
||||
try:
|
||||
signal(SIGINT, lambda s, f: exit(0))
|
||||
|
||||
set_event_loop(new_event_loop())
|
||||
if self.passive:
|
||||
return
|
||||
setgid(0 if "root" in self.flags else 1000)
|
||||
setuid(0 if "root" in self.flags else 1000)
|
||||
spec = spec_from_file_location("_", self.file)
|
||||
module = module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
self.Plugin = module.Plugin
|
||||
|
||||
if hasattr(self.Plugin, "_main"):
|
||||
get_event_loop().create_task(self.Plugin._main(self.Plugin))
|
||||
get_event_loop().create_task(self._setup_socket())
|
||||
get_event_loop().run_forever()
|
||||
except:
|
||||
self.log.error("Failed to start " + self.name + "!\n" + format_exc())
|
||||
exit(0)
|
||||
|
||||
async def _unload(self):
|
||||
try:
|
||||
self.log.info("Attempting to unload " + self.name + "\n")
|
||||
if hasattr(self.Plugin, "_unload"):
|
||||
await self.Plugin._unload(self.Plugin)
|
||||
except:
|
||||
self.log.error("Failed to unload " + self.name + "!\n" + format_exc())
|
||||
exit(0)
|
||||
|
||||
async def _setup_socket(self):
|
||||
self.socket = await start_unix_server(self._listen_for_method_call, path=self.socket_addr, limit=BUFFER_LIMIT)
|
||||
|
||||
async def _listen_for_method_call(self, reader, writer):
|
||||
while True:
|
||||
line = bytearray()
|
||||
while True:
|
||||
try:
|
||||
line.extend(await reader.readuntil())
|
||||
except LimitOverrunError:
|
||||
line.extend(await reader.read(reader._limit))
|
||||
continue
|
||||
except IncompleteReadError as err:
|
||||
line.extend(err.partial)
|
||||
break
|
||||
else:
|
||||
break
|
||||
data = loads(line.decode("utf-8"))
|
||||
if "stop" in data:
|
||||
await self._unload()
|
||||
get_event_loop().stop()
|
||||
while get_event_loop().is_running():
|
||||
await sleep(0)
|
||||
get_event_loop().close()
|
||||
return
|
||||
d = {"res": None, "success": True}
|
||||
try:
|
||||
d["res"] = await getattr(self.Plugin, data["method"])(self.Plugin, **data["args"])
|
||||
except Exception as e:
|
||||
d["res"] = str(e)
|
||||
d["success"] = False
|
||||
finally:
|
||||
writer.write((dumps(d)+"\n").encode("utf-8"))
|
||||
await writer.drain()
|
||||
|
||||
async def _open_socket_if_not_exists(self):
|
||||
if not self.reader:
|
||||
retries = 0
|
||||
while retries < 10:
|
||||
try:
|
||||
self.reader, self.writer = await open_unix_connection(self.socket_addr, limit=BUFFER_LIMIT)
|
||||
return True
|
||||
except:
|
||||
await sleep(2)
|
||||
retries += 1
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def start(self):
|
||||
if self.passive:
|
||||
return self
|
||||
multiprocessing.Process(target=self._init).start()
|
||||
return self
|
||||
|
||||
def stop(self):
|
||||
if self.passive:
|
||||
return
|
||||
async def _(self):
|
||||
if await self._open_socket_if_not_exists():
|
||||
self.writer.write((dumps({"stop": True})+"\n").encode("utf-8"))
|
||||
await self.writer.drain()
|
||||
self.writer.close()
|
||||
get_event_loop().create_task(_(self))
|
||||
|
||||
async def execute_method(self, method_name, kwargs):
|
||||
if self.passive:
|
||||
raise RuntimeError("This plugin is passive (aka does not implement main.py)")
|
||||
async with self.method_call_lock:
|
||||
if await self._open_socket_if_not_exists():
|
||||
self.writer.write(
|
||||
(dumps({"method": method_name, "args": kwargs})+"\n").encode("utf-8"))
|
||||
await self.writer.drain()
|
||||
line = bytearray()
|
||||
while True:
|
||||
try:
|
||||
line.extend(await self.reader.readuntil())
|
||||
except LimitOverrunError:
|
||||
line.extend(await self.reader.read(self.reader._limit))
|
||||
continue
|
||||
except IncompleteReadError as err:
|
||||
line.extend(err.partial)
|
||||
break
|
||||
else:
|
||||
break
|
||||
res = loads(line.decode("utf-8"))
|
||||
if not res["success"]:
|
||||
raise Exception(res["res"])
|
||||
return res["res"]
|
||||
Generated
+1045
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,30 @@
|
||||
import os
|
||||
from PyInstaller.building.build_main import Analysis
|
||||
from PyInstaller.building.api import EXE, PYZ
|
||||
from PyInstaller.utils.hooks import copy_metadata
|
||||
|
||||
a = Analysis(
|
||||
['main.py'],
|
||||
datas=[
|
||||
('decky_loader/locales', 'decky_loader/locales'),
|
||||
('decky_loader/static', 'decky_loader/static'),
|
||||
] + copy_metadata('decky_loader'),
|
||||
hiddenimports=['logging.handlers', 'sqlite3', 'decky_plugin', 'decky'],
|
||||
)
|
||||
pyz = PYZ(a.pure, a.zipped_data)
|
||||
|
||||
noconsole = bool(os.getenv('DECKY_NOCONSOLE'))
|
||||
name = "PluginLoader"
|
||||
if noconsole:
|
||||
name += "_noconsole"
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
name=name,
|
||||
upx=True,
|
||||
console=not noconsole,
|
||||
)
|
||||
@@ -0,0 +1,47 @@
|
||||
[tool.poetry]
|
||||
name = "decky-loader"
|
||||
version = "0.0.0" # the real version will be autogenerated
|
||||
description = "A plugin loader for the Steam Deck"
|
||||
license = "GPLv2"
|
||||
authors = []
|
||||
packages = [
|
||||
{include = "decky_loader"},
|
||||
{include = "decky_loader/main.py"}
|
||||
]
|
||||
include = [
|
||||
"decky_loader/locales/*",
|
||||
"decky_loader/static/*"
|
||||
]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.10,<3.14"
|
||||
|
||||
aiohttp = "^3.10.11"
|
||||
aiohttp-jinja2 = "^1.5.1"
|
||||
aiohttp-cors = "^0.7.0"
|
||||
watchdog = "^4"
|
||||
certifi = "*"
|
||||
packaging = "^24"
|
||||
multidict = "^6.0.5"
|
||||
setproctitle = "^1.3.3"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pyinstaller = "^6.8.0"
|
||||
pyright = "^1.1.335"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
decky-loader = 'decky_loader.main:main'
|
||||
|
||||
[tool.pyright]
|
||||
strict = ["*"]
|
||||
|
||||
[tool.poetry-dynamic-versioning]
|
||||
enable = true
|
||||
|
||||
[tool.poetry-dynamic-versioning.substitution]
|
||||
# don't replace version in decky_plugin.py
|
||||
files = []
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning>=1.0.0,<2.0.0"]
|
||||
build-backend = "poetry_dynamic_versioning.backend"
|
||||
@@ -1,44 +0,0 @@
|
||||
import imp
|
||||
from json import dump, load
|
||||
from os import mkdir, path
|
||||
|
||||
from helpers import get_home_path, get_homebrew_path, get_user, set_user
|
||||
|
||||
|
||||
class SettingsManager:
|
||||
def __init__(self, name, settings_directory = None) -> None:
|
||||
set_user()
|
||||
USER = get_user()
|
||||
if settings_directory == None:
|
||||
settings_directory = get_homebrew_path(get_home_path(USER))
|
||||
self.path = path.join(settings_directory, name + ".json")
|
||||
|
||||
if not path.exists(settings_directory):
|
||||
mkdir(settings_directory)
|
||||
|
||||
self.settings = {}
|
||||
|
||||
try:
|
||||
open(self.path, "x")
|
||||
except FileExistsError as e:
|
||||
self.read()
|
||||
pass
|
||||
|
||||
def read(self):
|
||||
try:
|
||||
with open(self.path, "r") as file:
|
||||
self.settings = load(file)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
pass
|
||||
|
||||
def commit(self):
|
||||
with open(self.path, "w+") as file:
|
||||
dump(self.settings, file, indent=4)
|
||||
|
||||
def getSetting(self, key, default):
|
||||
return self.settings.get(key, default)
|
||||
|
||||
def setSetting(self, key, value):
|
||||
self.settings[key] = value
|
||||
self.commit()
|
||||
@@ -1,205 +0,0 @@
|
||||
import os
|
||||
import shutil
|
||||
import uuid
|
||||
from asyncio import sleep
|
||||
from ensurepip import version
|
||||
from json.decoder import JSONDecodeError
|
||||
from logging import getLogger
|
||||
from os import getcwd, path, remove
|
||||
from subprocess import call
|
||||
|
||||
from aiohttp import ClientSession, web
|
||||
|
||||
import helpers
|
||||
from injector import get_gamepadui_tab, inject_to_tab
|
||||
from settings import SettingsManager
|
||||
|
||||
logger = getLogger("Updater")
|
||||
|
||||
class Updater:
|
||||
def __init__(self, context) -> None:
|
||||
self.context = context
|
||||
self.settings = self.context.settings
|
||||
# Exposes updater methods to frontend
|
||||
self.updater_methods = {
|
||||
"get_branch": self._get_branch,
|
||||
"get_version": self.get_version,
|
||||
"do_update": self.do_update,
|
||||
"do_restart": self.do_restart,
|
||||
"check_for_updates": self.check_for_updates
|
||||
}
|
||||
self.remoteVer = None
|
||||
self.allRemoteVers = None
|
||||
try:
|
||||
logger.info(getcwd())
|
||||
with open(path.join(getcwd(), ".loader.version"), 'r') as version_file:
|
||||
self.localVer = version_file.readline().replace("\n", "")
|
||||
except:
|
||||
self.localVer = False
|
||||
|
||||
try:
|
||||
self.currentBranch = self.get_branch(self.context.settings)
|
||||
except:
|
||||
self.currentBranch = 0
|
||||
logger.error("Current branch could not be determined, defaulting to \"Stable\"")
|
||||
|
||||
if context:
|
||||
context.web_app.add_routes([
|
||||
web.post("/updater/{method_name}", self._handle_server_method_call)
|
||||
])
|
||||
context.loop.create_task(self.version_reloader())
|
||||
|
||||
async def _handle_server_method_call(self, request):
|
||||
method_name = request.match_info["method_name"]
|
||||
try:
|
||||
args = await request.json()
|
||||
except JSONDecodeError:
|
||||
args = {}
|
||||
res = {}
|
||||
try:
|
||||
r = await self.updater_methods[method_name](**args)
|
||||
res["result"] = r
|
||||
res["success"] = True
|
||||
except Exception as e:
|
||||
res["result"] = str(e)
|
||||
res["success"] = False
|
||||
return web.json_response(res)
|
||||
|
||||
def get_branch(self, manager: SettingsManager):
|
||||
ver = manager.getSetting("branch", -1)
|
||||
logger.debug("current branch: %i" % ver)
|
||||
if ver == -1:
|
||||
logger.info("Current branch is not set, determining branch from version...")
|
||||
if self.localVer.startswith("v") and self.localVer.find("-pre"):
|
||||
logger.info("Current version determined to be pre-release")
|
||||
return 1
|
||||
else:
|
||||
logger.info("Current version determined to be stable")
|
||||
return 0
|
||||
return ver
|
||||
|
||||
async def _get_branch(self, manager: SettingsManager):
|
||||
return self.get_branch(manager)
|
||||
|
||||
# retrieve relevant service file's url for each branch
|
||||
def get_service_url(self):
|
||||
logger.debug("Getting service URL")
|
||||
branch = self.get_branch(self.context.settings)
|
||||
match branch:
|
||||
case 0:
|
||||
url = "https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/service-updater/dist/plugin_loader-release.service"
|
||||
case 1 | 2:
|
||||
url = "https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/service-updater/dist/plugin_loader-prerelease.service"
|
||||
case _:
|
||||
logger.error("You have an invalid branch set... Defaulting to prerelease service, please send the logs to the devs!")
|
||||
url = "https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/service-updater/dist/plugin_loader-prerelease.service"
|
||||
return str(url)
|
||||
|
||||
async def get_version(self):
|
||||
if self.localVer:
|
||||
return {
|
||||
"current": self.localVer,
|
||||
"remote": self.remoteVer,
|
||||
"all": self.allRemoteVers,
|
||||
"updatable": self.localVer != None
|
||||
}
|
||||
else:
|
||||
return {"current": "unknown", "remote": self.remoteVer, "all": self.allRemoteVers, "updatable": False}
|
||||
|
||||
async def check_for_updates(self):
|
||||
logger.debug("checking for updates")
|
||||
selectedBranch = self.get_branch(self.context.settings)
|
||||
async with ClientSession() as web:
|
||||
async with web.request("GET", "https://api.github.com/repos/SteamDeckHomebrew/decky-loader/releases", ssl=helpers.get_ssl_context()) as res:
|
||||
remoteVersions = await res.json()
|
||||
self.allRemoteVers = remoteVersions
|
||||
logger.debug("determining release type to find, branch is %i" % selectedBranch)
|
||||
if selectedBranch == 0:
|
||||
logger.debug("release type: release")
|
||||
self.remoteVer = next(filter(lambda ver: ver["tag_name"].startswith("v") and not ver["prerelease"] and ver["tag_name"], remoteVersions), None)
|
||||
elif selectedBranch == 1:
|
||||
logger.debug("release type: pre-release")
|
||||
self.remoteVer = next(filter(lambda ver: ver["prerelease"] and ver["tag_name"].startswith("v") and ver["tag_name"].find("-pre"), remoteVersions), None)
|
||||
else:
|
||||
logger.error("release type: NOT FOUND")
|
||||
raise ValueError("no valid branch found")
|
||||
logger.info("Updated remote version information")
|
||||
tab = await get_gamepadui_tab()
|
||||
await tab.evaluate_js(f"window.DeckyPluginLoader.notifyUpdates()", False, True, False)
|
||||
return await self.get_version()
|
||||
|
||||
async def version_reloader(self):
|
||||
await sleep(30)
|
||||
while True:
|
||||
try:
|
||||
await self.check_for_updates()
|
||||
except:
|
||||
pass
|
||||
await sleep(60 * 60 * 6) # 6 hours
|
||||
|
||||
async def do_update(self):
|
||||
logger.debug("Starting update.")
|
||||
version = self.remoteVer["tag_name"]
|
||||
download_url = self.remoteVer["assets"][0]["browser_download_url"]
|
||||
service_url = self.get_service_url()
|
||||
logger.debug("Retrieved service URL")
|
||||
|
||||
tab = await get_gamepadui_tab()
|
||||
await tab.open_websocket()
|
||||
async with ClientSession() as web:
|
||||
logger.debug("Downloading systemd service")
|
||||
# download the relevant systemd service depending upon branch
|
||||
async with web.request("GET", service_url, ssl=helpers.get_ssl_context(), allow_redirects=True) as res:
|
||||
logger.debug("Downloading service file")
|
||||
data = await res.content.read()
|
||||
logger.debug(str(data))
|
||||
service_file_path = path.join(getcwd(), "plugin_loader.service")
|
||||
try:
|
||||
with open(path.join(getcwd(), "plugin_loader.service"), "wb") as out:
|
||||
out.write(data)
|
||||
except Exception as e:
|
||||
logger.error(f"Error at %s", exc_info=e)
|
||||
with open(path.join(getcwd(), "plugin_loader.service"), 'r') as service_file:
|
||||
service_data = service_file.read()
|
||||
service_data = service_data.replace("${HOMEBREW_FOLDER}", "/home/"+helpers.get_user()+"/homebrew")
|
||||
with open(path.join(getcwd(), "plugin_loader.service"), 'w') as service_file:
|
||||
service_file.write(service_data)
|
||||
|
||||
logger.debug("Saved service file")
|
||||
logger.debug("Copying service file over current file.")
|
||||
shutil.copy(service_file_path, "/etc/systemd/system/plugin_loader.service")
|
||||
if not os.path.exists(path.join(getcwd(), ".systemd")):
|
||||
os.mkdir(path.join(getcwd(), ".systemd"))
|
||||
shutil.move(service_file_path, path.join(getcwd(), ".systemd")+"/plugin_loader.service")
|
||||
|
||||
logger.debug("Downloading binary")
|
||||
async with web.request("GET", download_url, ssl=helpers.get_ssl_context(), allow_redirects=True) as res:
|
||||
total = int(res.headers.get('content-length', 0))
|
||||
# we need to not delete the binary until we have downloaded the new binary!
|
||||
try:
|
||||
remove(path.join(getcwd(), "PluginLoader"))
|
||||
except:
|
||||
pass
|
||||
with open(path.join(getcwd(), "PluginLoader"), "wb") as out:
|
||||
progress = 0
|
||||
raw = 0
|
||||
async for c in res.content.iter_chunked(512):
|
||||
out.write(c)
|
||||
raw += len(c)
|
||||
new_progress = round((raw / total) * 100)
|
||||
if progress != new_progress:
|
||||
self.context.loop.create_task(tab.evaluate_js(f"window.DeckyUpdater.updateProgress({new_progress})", False, False, False))
|
||||
progress = new_progress
|
||||
|
||||
with open(path.join(getcwd(), ".loader.version"), "w") as out:
|
||||
out.write(version)
|
||||
|
||||
call(['chmod', '+x', path.join(getcwd(), "PluginLoader")])
|
||||
logger.info("Updated loader installation.")
|
||||
await tab.evaluate_js("window.DeckyUpdater.finish()", False, False)
|
||||
await self.do_restart()
|
||||
await tab.client.close()
|
||||
|
||||
async def do_restart(self):
|
||||
call(["systemctl", "daemon-reload"])
|
||||
call(["systemctl", "restart", "plugin_loader"])
|
||||
@@ -1,265 +0,0 @@
|
||||
import uuid
|
||||
import os
|
||||
from json.decoder import JSONDecodeError
|
||||
from traceback import format_exc
|
||||
|
||||
from asyncio import sleep, start_server, gather, open_connection
|
||||
from aiohttp import ClientSession, web
|
||||
|
||||
from logging import getLogger
|
||||
from injector import inject_to_tab, get_gamepadui_tab
|
||||
import helpers
|
||||
import subprocess
|
||||
|
||||
|
||||
class Utilities:
|
||||
def __init__(self, context) -> None:
|
||||
self.context = context
|
||||
self.util_methods = {
|
||||
"ping": self.ping,
|
||||
"http_request": self.http_request,
|
||||
"install_plugin": self.install_plugin,
|
||||
"cancel_plugin_install": self.cancel_plugin_install,
|
||||
"confirm_plugin_install": self.confirm_plugin_install,
|
||||
"uninstall_plugin": self.uninstall_plugin,
|
||||
"execute_in_tab": self.execute_in_tab,
|
||||
"inject_css_into_tab": self.inject_css_into_tab,
|
||||
"remove_css_from_tab": self.remove_css_from_tab,
|
||||
"allow_remote_debugging": self.allow_remote_debugging,
|
||||
"disallow_remote_debugging": self.disallow_remote_debugging,
|
||||
"set_setting": self.set_setting,
|
||||
"get_setting": self.get_setting,
|
||||
"filepicker_ls": self.filepicker_ls,
|
||||
"disable_rdt": self.disable_rdt,
|
||||
"enable_rdt": self.enable_rdt
|
||||
}
|
||||
|
||||
self.logger = getLogger("Utilities")
|
||||
|
||||
self.rdt_proxy_server = None
|
||||
self.rdt_script_id = None
|
||||
self.rdt_proxy_task = None
|
||||
|
||||
if context:
|
||||
context.web_app.add_routes([
|
||||
web.post("/methods/{method_name}", self._handle_server_method_call)
|
||||
])
|
||||
|
||||
async def _handle_server_method_call(self, request):
|
||||
method_name = request.match_info["method_name"]
|
||||
try:
|
||||
args = await request.json()
|
||||
except JSONDecodeError:
|
||||
args = {}
|
||||
res = {}
|
||||
try:
|
||||
r = await self.util_methods[method_name](**args)
|
||||
res["result"] = r
|
||||
res["success"] = True
|
||||
except Exception as e:
|
||||
res["result"] = str(e)
|
||||
res["success"] = False
|
||||
return web.json_response(res)
|
||||
|
||||
async def install_plugin(self, artifact="", name="No name", version="dev", hash=False):
|
||||
return await self.context.plugin_browser.request_plugin_install(
|
||||
artifact=artifact,
|
||||
name=name,
|
||||
version=version,
|
||||
hash=hash
|
||||
)
|
||||
|
||||
async def confirm_plugin_install(self, request_id):
|
||||
return await self.context.plugin_browser.confirm_plugin_install(request_id)
|
||||
|
||||
def cancel_plugin_install(self, request_id):
|
||||
return self.context.plugin_browser.cancel_plugin_install(request_id)
|
||||
|
||||
async def uninstall_plugin(self, name):
|
||||
return await self.context.plugin_browser.uninstall_plugin(name)
|
||||
|
||||
async def http_request(self, method="", url="", **kwargs):
|
||||
async with ClientSession() as web:
|
||||
async with web.request(method, url, ssl=helpers.get_ssl_context(), **kwargs) as res:
|
||||
return {
|
||||
"status": res.status,
|
||||
"headers": dict(res.headers),
|
||||
"body": await res.text()
|
||||
}
|
||||
|
||||
async def ping(self, **kwargs):
|
||||
return "pong"
|
||||
|
||||
async def execute_in_tab(self, tab, run_async, code):
|
||||
try:
|
||||
result = await inject_to_tab(tab, code, run_async)
|
||||
if "exceptionDetails" in result["result"]:
|
||||
return {
|
||||
"success": False,
|
||||
"result": result["result"]
|
||||
}
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"result": result["result"]["result"].get("value")
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"result": e
|
||||
}
|
||||
|
||||
async def inject_css_into_tab(self, tab, style):
|
||||
try:
|
||||
css_id = str(uuid.uuid4())
|
||||
|
||||
result = await inject_to_tab(tab,
|
||||
f"""
|
||||
(function() {{
|
||||
const style = document.createElement('style');
|
||||
style.id = "{css_id}";
|
||||
document.head.append(style);
|
||||
style.textContent = `{style}`;
|
||||
}})()
|
||||
""", False)
|
||||
|
||||
if "exceptionDetails" in result["result"]:
|
||||
return {
|
||||
"success": False,
|
||||
"result": result["result"]
|
||||
}
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"result": css_id
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"result": e
|
||||
}
|
||||
|
||||
async def remove_css_from_tab(self, tab, css_id):
|
||||
try:
|
||||
result = await inject_to_tab(tab,
|
||||
f"""
|
||||
(function() {{
|
||||
let style = document.getElementById("{css_id}");
|
||||
|
||||
if (style.nodeName.toLowerCase() == 'style')
|
||||
style.parentNode.removeChild(style);
|
||||
}})()
|
||||
""", False)
|
||||
|
||||
if "exceptionDetails" in result["result"]:
|
||||
return {
|
||||
"success": False,
|
||||
"result": result
|
||||
}
|
||||
|
||||
return {
|
||||
"success": True
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"result": e
|
||||
}
|
||||
|
||||
async def get_setting(self, key, default):
|
||||
return self.context.settings.getSetting(key, default)
|
||||
|
||||
async def set_setting(self, key, value):
|
||||
return self.context.settings.setSetting(key, value)
|
||||
|
||||
async def allow_remote_debugging(self):
|
||||
await helpers.start_systemd_unit(helpers.REMOTE_DEBUGGER_UNIT)
|
||||
return True
|
||||
|
||||
async def disallow_remote_debugging(self):
|
||||
await helpers.stop_systemd_unit(helpers.REMOTE_DEBUGGER_UNIT)
|
||||
return True
|
||||
|
||||
async def filepicker_ls(self, path, include_files=True):
|
||||
# def sorter(file): # Modification time
|
||||
# if os.path.isdir(os.path.join(path, file)) or os.path.isfile(os.path.join(path, file)):
|
||||
# return os.path.getmtime(os.path.join(path, file))
|
||||
# return 0
|
||||
# file_names = sorted(os.listdir(path), key=sorter, reverse=True) # TODO provide more sort options
|
||||
file_names = sorted(os.listdir(path)) # Alphabetical
|
||||
|
||||
files = []
|
||||
|
||||
for file in file_names:
|
||||
full_path = os.path.join(path, file)
|
||||
is_dir = os.path.isdir(full_path)
|
||||
|
||||
if is_dir or include_files:
|
||||
files.append({
|
||||
"isdir": is_dir,
|
||||
"name": file,
|
||||
"realpath": os.path.realpath(full_path)
|
||||
})
|
||||
|
||||
return {
|
||||
"realpath": os.path.realpath(path),
|
||||
"files": files
|
||||
}
|
||||
|
||||
# Based on https://stackoverflow.com/a/46422554/13174603
|
||||
def start_rdt_proxy(self, ip, port):
|
||||
async def pipe(reader, writer):
|
||||
try:
|
||||
while not reader.at_eof():
|
||||
writer.write(await reader.read(2048))
|
||||
finally:
|
||||
writer.close()
|
||||
async def handle_client(local_reader, local_writer):
|
||||
try:
|
||||
remote_reader, remote_writer = await open_connection(
|
||||
ip, port)
|
||||
pipe1 = pipe(local_reader, remote_writer)
|
||||
pipe2 = pipe(remote_reader, local_writer)
|
||||
await gather(pipe1, pipe2)
|
||||
finally:
|
||||
local_writer.close()
|
||||
|
||||
self.rdt_proxy_server = start_server(handle_client, "127.0.0.1", port)
|
||||
self.rdt_proxy_task = self.context.loop.create_task(self.rdt_proxy_server)
|
||||
|
||||
def stop_rdt_proxy(self):
|
||||
if self.rdt_proxy_server:
|
||||
self.rdt_proxy_server.close()
|
||||
self.rdt_proxy_task.cancel()
|
||||
|
||||
async def enable_rdt(self):
|
||||
# TODO un-hardcode port
|
||||
try:
|
||||
self.stop_rdt_proxy()
|
||||
ip = self.context.settings.getSetting("developer.rdt.ip", None)
|
||||
|
||||
if ip != None:
|
||||
self.logger.info("Connecting to React DevTools at " + ip)
|
||||
async with ClientSession() as web:
|
||||
async with web.request("GET", "http://" + ip + ":8097", ssl=helpers.get_ssl_context()) as res:
|
||||
if res.status != 200:
|
||||
self.logger.error("Failed to connect to React DevTools at " + ip)
|
||||
return False
|
||||
self.start_rdt_proxy(ip, 8097)
|
||||
script = "if(!window.deckyHasConnectedRDT){window.deckyHasConnectedRDT=true;\n" + await res.text() + "\n}"
|
||||
self.logger.info("Connected to React DevTools, loading script")
|
||||
tab = await get_gamepadui_tab()
|
||||
# RDT needs to load before React itself to work.
|
||||
result = await tab.reload_and_evaluate(script)
|
||||
self.logger.info(result)
|
||||
|
||||
except Exception:
|
||||
self.logger.error("Failed to connect to React DevTools")
|
||||
self.logger.error(format_exc())
|
||||
|
||||
async def disable_rdt(self):
|
||||
self.logger.info("Disabling React DevTools")
|
||||
tab = await get_gamepadui_tab()
|
||||
self.rdt_script_id = None
|
||||
await tab.evaluate_js("location.reload();", False, True, False)
|
||||
self.logger.info("React DevTools disabled")
|
||||
-335
@@ -1,335 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
## Before using this script, enable sshd on the deck and setup an sshd key between the deck and your dev in sshd_config.
|
||||
## This script defaults to port 22 unless otherwise specified, and cannot run without a sudo password or LAN IP.
|
||||
## You will need to specify the path to the ssh key if using key connection exclusively.
|
||||
|
||||
## TODO: document latest changes to wiki
|
||||
|
||||
## Pre-parse arugments for ease of use
|
||||
CLONEFOLDER=${1:-""}
|
||||
INSTALLFOLDER=${2:-""}
|
||||
DECKIP=${3:-""}
|
||||
SSHPORT=${4:-""}
|
||||
PASSWORD=${5:-""}
|
||||
SSHKEYLOC=${6:-""}
|
||||
LOADERBRANCH=${7:-""}
|
||||
LIBRARYBRANCH=${8:-""}
|
||||
TEMPLATEBRANCH=${9:-""}
|
||||
LATEST=${10:-""}
|
||||
|
||||
## gather options into an array
|
||||
OPTIONSARRAY=("$CLONEFOLDER" "$INSTALLFOLDER" "$DECKIP" "$SSHPORT" "$PASSWORD" "$SSHKEYLOC" "$LOADERBRANCH" "$LIBRARYBRANCH" "$TEMPLATEBRANCH" "$LATEST")
|
||||
|
||||
## iterate through options array to check their presence
|
||||
count=0
|
||||
for OPTION in ${OPTIONSARRAY[@]}; do
|
||||
! [[ "$OPTION" == "" ]] && count=$(($count+1))
|
||||
# printf "OPTION=$OPTION\n"
|
||||
done
|
||||
|
||||
setfolder() {
|
||||
if [[ "$2" == "clone" ]]; then
|
||||
local ACTION="clone"
|
||||
local DEFAULT="git"
|
||||
elif [[ "$2" == "install" ]]; then
|
||||
local ACTION="install"
|
||||
local DEFAULT="dev"
|
||||
fi
|
||||
|
||||
if [[ "$ACTION" == "clone" ]]; then
|
||||
printf "Enter the directory in /home/user/ to ${ACTION} to.\n"
|
||||
printf "The ${ACTION} directory would be: ${HOME}/${DEFAULT}\n"
|
||||
read -p "Enter your ${ACTION} directory: " CLONEFOLDER
|
||||
if ! [[ "$CLONEFOLDER" =~ ^[[:alnum:]]+$ ]]; then
|
||||
printf "Folder name not provided. Using default, '${DEFAULT}'.\n"
|
||||
CLONEFOLDER="${DEFAULT}"
|
||||
fi
|
||||
elif [[ "$ACTION" == "install" ]]; then
|
||||
printf "Enter the directory in /home/deck/homebrew to ${ACTION} pluginloader to.\n"
|
||||
printf "The ${ACTION} directory would be: /home/deck/homebrew/${DEFAULT}/pluginloader\n"
|
||||
printf "It is highly recommended that you use the default folder path seen above, just press enter at the next prompt.\n"
|
||||
read -p "Enter your ${ACTION} directory: " INSTALLFOLDER
|
||||
if ! [[ "$INSTALLFOLDER" =~ ^[[:alnum:]]+$ ]]; then
|
||||
printf "Folder name not provided. Using default, '${DEFAULT}'.\n"
|
||||
INSTALLFOLDER="${DEFAULT}"
|
||||
fi
|
||||
else
|
||||
printf "Folder type could not be determined, exiting\n"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
checkdeckip() {
|
||||
### check that ip is provided
|
||||
if [[ "$1" == "" ]]; then
|
||||
printf "An ip address must be provided, exiting.\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
### check to make sure it's a potentially valid ipv4 address
|
||||
if ! [[ $1 =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
|
||||
printf "A valid ip address must be provided, exiting.\n"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
checksshport() {
|
||||
### check to make sure a port was specified
|
||||
if [[ "$1" == "" ]]; then
|
||||
printf "ssh port not provided. Using default, '22'.\n"
|
||||
SSHPORT="22"
|
||||
fi
|
||||
|
||||
### check for valid ssh port
|
||||
if [[ $1 -le 0 ]]; then
|
||||
printf "A valid ssh port must be provided, exiting.\n"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
checksshkey() {
|
||||
### check if ssh key is present at location provided
|
||||
if [[ "$1" == "" ]]; then
|
||||
SSHKEYLOC="$HOME/.ssh/id_rsa"
|
||||
printf "ssh key was not provided. Defaulting to $SSHKEYLOC if it exists.\n"
|
||||
fi
|
||||
|
||||
### check if sshkey is present at location
|
||||
if ! [[ -e "$1" ]]; then
|
||||
SSHKEYLOC=""
|
||||
printf "ssh key does not exist. This script will use password authentication.\n"
|
||||
fi
|
||||
}
|
||||
|
||||
checkpassword() {
|
||||
### check to make sure a password for 'deck' was specified
|
||||
if [[ "$1" == "" ]]; then
|
||||
printf "Remote deck user password was not provided, exiting.\n"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
clonefromto() {
|
||||
# printf "repo=$1\n"
|
||||
# printf "outdir=$2\n"
|
||||
# printf "branch=$3\n"
|
||||
printf "Repository: $1\n"
|
||||
git clone $1 $2 &> '/dev/null'
|
||||
CODE=$?
|
||||
# printf "CODE=${CODE}"
|
||||
if [[ $CODE -eq 128 ]]; then
|
||||
cd $2
|
||||
git fetch --all &> '/dev/null'
|
||||
fi
|
||||
if [[ -z $3 ]]; then
|
||||
printf "Enter the desired branch for repository "$1" :\n"
|
||||
local OUT="$(git branch -r | sed '/\/HEAD/d')"
|
||||
# $OUT="$($OUT > )"
|
||||
printf "$OUT\nbranch: "
|
||||
read BRANCH
|
||||
else
|
||||
printf "on branch: $3\n"
|
||||
BRANCH="$3"
|
||||
fi
|
||||
if ! [[ -z ${BRANCH} ]]; then
|
||||
git checkout $BRANCH &> '/dev/null'
|
||||
fi
|
||||
if [[ ${LATEST} == "true" ]]; then
|
||||
git pull --all
|
||||
elif [[ ${LATEST} == "true" ]]; then
|
||||
printf "Assuming user not pulling latest commits.\n"
|
||||
else
|
||||
printf "Pull latest commits? (y/N): "
|
||||
read PULL
|
||||
case ${PULL:0:1} in
|
||||
y|Y )
|
||||
printf "Pulling latest commits.\n"
|
||||
git pull --all
|
||||
;;
|
||||
* )
|
||||
printf "Not pulling latest commits.\n"
|
||||
;;
|
||||
esac
|
||||
if ! [[ "$PULL" =~ ^[[:alnum:]]+$ ]]; then
|
||||
printf "Assuming user not pulling latest commits.\n"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
pnpmtransbundle() {
|
||||
cd $1
|
||||
if [[ "$2" == "library" ]]; then
|
||||
npm install --quiet &> '/dev/null'
|
||||
npm run build --quiet &> '/dev/null'
|
||||
sudo npm link --quiet &> '/dev/null'
|
||||
elif [[ "$2" == "frontend" ]]; then
|
||||
pnpm i &> '/dev/null'
|
||||
pnpm run build &> '/dev/null'
|
||||
elif [[ "$2" == "template" ]]; then
|
||||
pnpm i &> '/dev/null'
|
||||
pnpm run build &> '/dev/null'
|
||||
fi
|
||||
}
|
||||
|
||||
if ! [[ $count -gt 9 ]] ; then
|
||||
printf "Installing Steam Deck Plugin Loader contributor/developer (for Steam Deck)...\n"
|
||||
|
||||
printf "THIS SCRIPT ASSUMES YOU ARE RUNNING IT ON A PC, NOT THE DECK!
|
||||
Not planning to contribute to or develop for PluginLoader?
|
||||
If so, you should not be using this script.\n
|
||||
If you have a release/nightly installed this script will disable it.\n"
|
||||
|
||||
printf "This script requires you to have nodejs installed. (If nodejs doesn't bundle npm on your OS/distro, then npm is required as well).\n"
|
||||
fi
|
||||
|
||||
if ! [[ $count -gt 0 ]] ; then
|
||||
read -p "Press any key to continue"
|
||||
fi
|
||||
|
||||
printf "\n"
|
||||
|
||||
## User chooses preffered clone & install directories
|
||||
|
||||
if [[ "$CLONEFOLDER" == "" ]]; then
|
||||
setfolder "$CLONEFOLDER" "clone"
|
||||
fi
|
||||
|
||||
if [[ "$INSTALLFOLDER" == "" ]]; then
|
||||
setfolder "$INSTALLFOLDER" "install"
|
||||
fi
|
||||
|
||||
CLONEDIR="$HOME/$CLONEFOLDER"
|
||||
INSTALLDIR="/home/deck/homebrew/$INSTALLFOLDER"
|
||||
|
||||
## Input ip address, port, password and sshkey
|
||||
|
||||
### DECKIP already been parsed?
|
||||
if [[ "$DECKIP" == "" ]]; then
|
||||
### get ip address of deck from user
|
||||
read -p "Enter the ip address of your Steam Deck: " DECKIP
|
||||
fi
|
||||
|
||||
### validate DECKIP
|
||||
checkdeckip "$DECKIP"
|
||||
|
||||
### SSHPORT already been parsed?
|
||||
if [[ "$SSHPORT" == "" ]]; then
|
||||
### get ssh port from user
|
||||
read -p "Enter the ssh port of your Steam Deck: " SSHPORT
|
||||
fi
|
||||
|
||||
### validate SSHPORT
|
||||
checksshport "$SSHPORT"
|
||||
|
||||
### PASSWORD already been parsed?
|
||||
if [[ "$PASSWORD" == "" ]]; then
|
||||
### prompt the user for their deck's password
|
||||
printf "Enter the password for the Steam Deck user 'deck' : "
|
||||
read -s PASSWORD
|
||||
printf "\n"
|
||||
fi
|
||||
|
||||
### validate PASSWORD
|
||||
checkpassword "$PASSWORD"
|
||||
|
||||
### SSHKEYLOC already been parsed?
|
||||
if [[ "$SSHKEYLOC" == "" ]]; then
|
||||
### prompt the user for their ssh key
|
||||
read -p "Enter the directory for your ssh key, for ease of connection : " SSHKEYLOC
|
||||
fi
|
||||
|
||||
### validate SSHKEYLOC
|
||||
checksshkey "$SSHKEYLOC"
|
||||
|
||||
if [[ "$SSHKEYLOC" == "" ]]; then
|
||||
IDENINVOC=""
|
||||
else
|
||||
IDENINVOC="-i ${SSHKEYLOC}"
|
||||
fi
|
||||
|
||||
## Create folder structure
|
||||
|
||||
printf "Cloning git repositories.\n"
|
||||
|
||||
mkdir -p ${CLONEDIR} &> '/dev/null'
|
||||
|
||||
### remove folders just in case
|
||||
# rm -r ${CLONEDIR}/pluginloader
|
||||
# rm -r ${CLONEDIR}/pluginlibrary
|
||||
# rm -r ${CLONEDIR}/plugintemplate
|
||||
|
||||
clonefromto "https://github.com/SteamDeckHomebrew/PluginLoader" ${CLONEDIR}/pluginloader "$LOADERBRANCH"
|
||||
|
||||
clonefromto "https://github.com/SteamDeckHomebrew/decky-frontend-lib" ${CLONEDIR}/pluginlibrary "$LIBRARYBRANCH"
|
||||
|
||||
clonefromto "https://github.com/SteamDeckHomebrew/decky-plugin-template" ${CLONEDIR}/plugintemplate "$TEMPLATEBRANCH"
|
||||
|
||||
## install python dependencies to deck
|
||||
|
||||
printf "\nInstalling python dependencies.\n"
|
||||
|
||||
rsync -azp --rsh="ssh -p $SSHPORT $IDENINVOC" ${CLONEDIR}/pluginloader/requirements.txt deck@${DECKIP}:${INSTALLDIR}/pluginloader/requirements.txt &> '/dev/null'
|
||||
|
||||
ssh deck@${DECKIP} -p ${SSHPORT} ${IDENINVOC} "python -m ensurepip && python -m pip install --upgrade pip && python -m pip install --upgrade setuptools && python -m pip install -r $INSTALLDIR/pluginloader/requirements.txt" &> '/dev/null'
|
||||
|
||||
## Transpile and bundle typescript
|
||||
|
||||
[ "$UID" -eq 0 ] || printf "Input password to proceed with install.\n"
|
||||
|
||||
sudo npm install -g pnpm &> '/dev/null'
|
||||
|
||||
type pnpm &> '/dev/null'
|
||||
|
||||
PNPMLIVES=$?
|
||||
|
||||
if ! [[ "$PNPMLIVES" -eq 0 ]]; then
|
||||
printf "pnpm does not appear to be installed, exiting.\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf "Transpiling and bundling typescript.\n"
|
||||
|
||||
pnpmtransbundle ${CLONEDIR}/pluginlibrary/ "library"
|
||||
|
||||
pnpmtransbundle ${CLONEDIR}/pluginloader/frontend "frontend"
|
||||
|
||||
pnpmtransbundle ${CLONEDIR}/plugintemplate "template"
|
||||
|
||||
## Transfer relevant files to deck
|
||||
|
||||
printf "Copying relevant files to install directory\n\n"
|
||||
|
||||
ssh deck@${DECKIP} -p ${SSHPORT} ${IDENINVOC} "mkdir -p $INSTALLDIR/pluginloader && mkdir -p $INSTALLDIR/plugins" &> '/dev/null'
|
||||
|
||||
### copy files for PluginLoader
|
||||
rsync -avzp --rsh="ssh -p $SSHPORT $IDENINVOC" --exclude='.git/' --exclude='.github/' --exclude='.vscode/' --exclude='frontend/' --exclude='dist/' --exclude='contrib/' --exclude='*.log' --exclude='requirements.txt' --exclude='backend/__pycache__/' --exclude='.gitignore' --delete ${CLONEDIR}/pluginloader/* deck@${DECKIP}:${INSTALLDIR}/pluginloader &> '/dev/null'
|
||||
|
||||
if ! [[ $? -eq 0 ]]; then
|
||||
printf "Error occurred when copying $CLONEDIR/pluginloader/ to $INSTALLDIR/pluginloader/\n"
|
||||
printf "Check that your Steam Deck is active, ssh is enabled and running and is accepting connections.\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
### copy files for plugin template
|
||||
rsync -avzp --rsh="ssh -p $SSHPORT $IDENINVOC" --exclude='.git/' --exclude='.github/' --exclude='.vscode/' --exclude='node_modules/' --exclude='src/' --exclude='*.log' --exclude='.gitignore' --exclude='pnpm-lock.yaml' --exclude='package.json' --exclude='rollup.config.js' --exclude='tsconfig.json' --delete ${CLONEDIR}/plugintemplate deck@${DECKIP}:${INSTALLDIR}/plugins &> '/dev/null'
|
||||
if ! [[ $? -eq 0 ]]; then
|
||||
printf "Error occurred when copying $CLONEDIR/plugintemplate to $INSTALLDIR/plugins\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
## TODO: direct contributors to wiki for this info?
|
||||
|
||||
printf "Run these commands to deploy your local changes to the deck:\n"
|
||||
printf "'rsync -avzp --mkpath --rsh=""\"ssh -p $SSHPORT $IDENINVOC\""" --exclude='.git/' --exclude='.github/' --exclude='.vscode/' --exclude='frontend/' --exclude='dist/' --exclude='contrib/' --exclude='*.log' --exclude='requirements.txt' --exclude='backend/__pycache__/' --exclude='.gitignore' --delete $CLONEDIR/pluginloader/* deck@$DECKIP:$INSTALLDIR/pluginloader/'\n"
|
||||
printf "'rsync -avzp --mkpath --rsh=""\"ssh -p $SSHPORT $IDENINVOC\""" --exclude='.git/' --exclude='.github/' --exclude='.vscode/' --exclude='node_modules/' --exclude='src/' --exclude='*.log' --exclude='.gitignore' --exclude='package-lock.json' --delete $CLONEDIR/pluginname deck@$DECKIP:$INSTALLDIR/plugins'\n\n"
|
||||
|
||||
printf "Run in console or in a script this command to run your development version:\n'ssh deck@$DECKIP -p $SSHPORT $IDENINVOC 'export PLUGIN_PATH=$INSTALLDIR/plugins; export CHOWN_PLUGIN_PATH=0; echo 'steam' | sudo -SE python3 $INSTALLDIR/pluginloader/backend/main.py'\n"
|
||||
|
||||
## Disable Releases versions if they exist
|
||||
|
||||
### ssh into deck and disable PluginLoader release/nightly service
|
||||
printf "Connecting via ssh to disable any PluginLoader release versions.\n"
|
||||
printf "Script will exit after this. All done!\n"
|
||||
|
||||
ssh deck@${DECKIP} -p ${SSHPORT} ${IDENINVOC} "printf $PASSWORD | sudo -S systemctl disable --now plugin_loader; echo $?" &> '/dev/null'
|
||||
@@ -1,168 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
## Pre-parse arugments for ease of use
|
||||
CLONEFOLDER=${1:-""}
|
||||
LOADERBRANCH=${2:-""}
|
||||
LIBRARYBRANCH=${3:-""}
|
||||
TEMPLATEBRANCH=${4:-""}
|
||||
LATEST=${5:-""}
|
||||
|
||||
## gather options into an array
|
||||
OPTIONSARRAY=("$CLONEFOLDER" "$LOADERBRANCH" "$LIBRARYBRANCH" "$TEMPLATEBRANCH" "$LATEST")
|
||||
|
||||
## iterate through options array to check their presence
|
||||
count=0
|
||||
for OPTION in ${OPTIONSARRAY[@]}; do
|
||||
! [[ "$OPTION" == "" ]] && count=$(($count+1))
|
||||
# printf "OPTION=$OPTION\n"
|
||||
done
|
||||
|
||||
clonefromto() {
|
||||
# printf "repo=$1\n"
|
||||
# printf "outdir=$2\n"
|
||||
# printf "branch=$3\n"
|
||||
printf "Repository: $1\n"
|
||||
git clone $1 $2 &> '/dev/null'
|
||||
CODE=$?
|
||||
# printf "CODE=${CODE}"
|
||||
if [[ $CODE -eq 128 ]]; then
|
||||
cd $2
|
||||
git fetch --all &> '/dev/null'
|
||||
fi
|
||||
if [[ -z $3 ]]; then
|
||||
printf "Enter the desired branch for repository "$1" :\n"
|
||||
local OUT="$(git branch -r | sed '/\/HEAD/d')"
|
||||
# $OUT="$($OUT > )"
|
||||
printf "$OUT\nbranch: "
|
||||
read BRANCH
|
||||
else
|
||||
printf "on branch: $3\n"
|
||||
BRANCH="$3"
|
||||
fi
|
||||
if ! [[ -z ${BRANCH} ]]; then
|
||||
git checkout $BRANCH &> '/dev/null'
|
||||
fi
|
||||
if [[ ${LATEST} == "true" ]]; then
|
||||
git pull --all
|
||||
elif [[ ${LATEST} == "true" ]]; then
|
||||
printf "Assuming user not pulling latest commits.\n"
|
||||
else
|
||||
printf "Pull latest commits? (y/N): "
|
||||
read PULL
|
||||
case ${PULL:0:1} in
|
||||
y|Y )
|
||||
printf "Pulling latest commits.\n"
|
||||
git pull --all
|
||||
;;
|
||||
* )
|
||||
printf "Not pulling latest commits.\n"
|
||||
;;
|
||||
esac
|
||||
if ! [[ "$PULL" =~ ^[[:alnum:]]+$ ]]; then
|
||||
printf "Assuming user not pulling latest commits.\n"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
pnpmtransbundle() {
|
||||
cd $1
|
||||
if [[ "$2" == "library" ]]; then
|
||||
npm install --quiet &> '/dev/null'
|
||||
npm run build --quiet &> '/dev/null'
|
||||
sudo npm link --quiet &> '/dev/null'
|
||||
elif [[ "$2" == "frontend" ]]; then
|
||||
pnpm i &> '/dev/null'
|
||||
pnpm run build &> '/dev/null'
|
||||
elif [[ "$2" == "template" ]]; then
|
||||
pnpm i &> '/dev/null'
|
||||
pnpm run build &> '/dev/null'
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
if ! [[ $count -gt 4 ]] ; then
|
||||
printf "Installing Steam Deck Plugin Loader contributor/developer (no Steam Deck)..."
|
||||
|
||||
printf "\nTHIS SCRIPT ASSUMES YOU ARE RUNNING IT ON A PC, NOT THE DECK!
|
||||
Not planning to contribute to or develop for PluginLoader?
|
||||
Then you should not be using this script.\n"
|
||||
|
||||
printf "\nThis script requires you to have nodejs installed. (If nodejs doesn't bundle npm on your OS/distro, then npm is required as well).\n"
|
||||
fi
|
||||
|
||||
if ! [[ $count -gt 0 ]] ; then
|
||||
read -p "Press any key to continue"
|
||||
fi
|
||||
|
||||
printf "\n"
|
||||
|
||||
if [[ "$CLONEFOLDER" == "" ]]; then
|
||||
printf "Enter the directory in /home/user/ to clone to.\n"
|
||||
printf "The clone directory would be: ${HOME}/git \n"
|
||||
read -p "Enter your clone directory: " CLONEFOLDER
|
||||
if ! [[ "$CLONEFOLDER" =~ ^[[:alnum:]]+$ ]]; then
|
||||
printf "Folder name not provided. Using default, '${DEFAULT}'.\n"
|
||||
CLONEFOLDER="${DEFAULT}"
|
||||
fi
|
||||
fi
|
||||
|
||||
CLONEDIR="$HOME/$CLONEFOLDER"
|
||||
|
||||
## Create folder structure
|
||||
|
||||
printf "Cloning git repositories.\n"
|
||||
|
||||
mkdir -p ${CLONEDIR} &> '/dev/null'
|
||||
|
||||
### remove folders just in case
|
||||
# rm -r ${CLONEDIR}/pluginloader
|
||||
# rm -r ${CLONEDIR}/pluginlibrary
|
||||
# rm -r ${CLONEDIR}/plugintemplate
|
||||
|
||||
clonefromto "https://github.com/SteamDeckHomebrew/PluginLoader" ${CLONEDIR}/pluginloader "$LOADERBRANCH"
|
||||
|
||||
clonefromto "https://github.com/SteamDeckHomebrew/decky-frontend-lib" ${CLONEDIR}/pluginlibrary "$LIBRARYBRANCH"
|
||||
|
||||
clonefromto "https://github.com/SteamDeckHomebrew/decky-plugin-template" ${CLONEDIR}/plugintemplate "$TEMPLATEBRANCH"
|
||||
|
||||
## install python dependencies (maybe use venv?)
|
||||
|
||||
python -m pip install -r ${CLONEDIR}/pluginloader/requirements.txt &> '/dev/null'
|
||||
|
||||
## Transpile and bundle typescript
|
||||
|
||||
[ "$UID" -eq 0 ] || printf "Input password to proceed with install.\n"
|
||||
|
||||
type npm &> '/dev/null'
|
||||
|
||||
NPMLIVES=$?
|
||||
|
||||
if ! [[ "$PNPMLIVES" -eq 0 ]]; then
|
||||
printf "npm does not appear to be installed, exiting.\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sudo npm install -g pnpm &> '/dev/null'
|
||||
|
||||
type pnpm &> '/dev/null'
|
||||
|
||||
PNPMLIVES=$?
|
||||
|
||||
if ! [[ "$PNPMLIVES" -eq 0 ]]; then
|
||||
printf "pnpm does not appear to be installed, exiting.\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf "Transpiling and bundling typescript.\n"
|
||||
|
||||
pnpmtransbundle ${CLONEDIR}/pluginlibrary/ "library"
|
||||
|
||||
pnpmtransbundle ${CLONEDIR}/pluginloader/frontend "frontend"
|
||||
|
||||
pnpmtransbundle ${CLONEDIR}/plugintemplate "template"
|
||||
|
||||
printf "Plugin Loader is located at '${CLONEDIR}/pluginloader/'.\n"
|
||||
|
||||
printf "Run in console or in a script these commands to run your development version:\n'export PLUGIN_PATH=${CLONEDIR}/plugins; export CHOWN_PLUGIN_PATH=0; sudo -E python3 ${CLONEDIR}/pluginloader/backend/main.py'\n"
|
||||
|
||||
printf "All done!\n"
|
||||
Vendored
+2
-65
@@ -1,66 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
[ "$UID" -eq 0 ] || exec sudo "$0" "$@"
|
||||
|
||||
echo "Installing Steam Deck Plugin Loader pre-release..."
|
||||
|
||||
USER_DIR="$(getent passwd $SUDO_USER | cut -d: -f6)"
|
||||
HOMEBREW_FOLDER="${USER_DIR}/homebrew"
|
||||
|
||||
# Create folder structure
|
||||
rm -rf "${HOMEBREW_FOLDER}/services"
|
||||
sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/services"
|
||||
sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/plugins"
|
||||
|
||||
# Download latest release and install it
|
||||
RELEASE=$(curl -s 'https://api.github.com/repos/SteamDeckHomebrew/decky-loader/releases' | jq -r "first(.[] | select(.prerelease == "true"))")
|
||||
read VERSION DOWNLOADURL < <(echo $(jq -r '.tag_name, .assets[].browser_download_url' <<< ${RELEASE}))
|
||||
|
||||
printf "Installing version %s...\n" "${VERSION}"
|
||||
curl -L $DOWNLOADURL --output ${HOMEBREW_FOLDER}/services/PluginLoader
|
||||
chmod +x ${HOMEBREW_FOLDER}/services/PluginLoader
|
||||
echo $VERSION > ${HOMEBREW_FOLDER}/services/.loader.version
|
||||
|
||||
systemctl --user stop plugin_loader 2> /dev/null
|
||||
systemctl --user disable plugin_loader 2> /dev/null
|
||||
|
||||
systemctl stop plugin_loader 2> /dev/null
|
||||
systemctl disable plugin_loader 2> /dev/null
|
||||
|
||||
curl -L https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/service-updater/dist/plugin_loader-prerelease.service --output ${HOMEBREW_FOLDER}/services/plugin_loader-prerelease.service
|
||||
|
||||
cat > "${HOMEBREW_FOLDER}/services/plugin_loader-backup.service" <<- EOM
|
||||
[Unit]
|
||||
Description=SteamDeck Plugin Loader
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
Restart=always
|
||||
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
|
||||
WorkingDirectory=${HOMEBREW_FOLDER}/services
|
||||
Environment=PLUGIN_PATH=${HOMEBREW_FOLDER}/plugins
|
||||
Environment=LOG_LEVEL=DEBUG
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOM
|
||||
|
||||
if [[ -f "${HOMEBREW_FOLDER}/services/plugin_loader-prerelease.service" ]]; then
|
||||
printf "Grabbed latest prerelease service.\n"
|
||||
sed -i -e "s|\${HOMEBREW_FOLDER}|${HOMEBREW_FOLDER}|" "${HOMEBREW_FOLDER}/services/plugin_loader-prerelease.service"
|
||||
cp -f "${HOMEBREW_FOLDER}/services/plugin_loader-prerelease.service" "/etc/systemd/system/plugin_loader.service"
|
||||
else
|
||||
printf "Could not curl latest prerelease systemd service, using built-in service as a backup!\n"
|
||||
rm -f "/etc/systemd/system/plugin_loader.service"
|
||||
cp "${HOMEBREW_FOLDER}/services/plugin_loader-backup.service" "/etc/systemd/system/plugin_loader.service"
|
||||
fi
|
||||
|
||||
mkdir -p ${HOMEBREW_FOLDER}/services/.systemd
|
||||
cp ${HOMEBREW_FOLDER}/services/plugin_loader-prerelease.service ${HOMEBREW_FOLDER}/services/.systemd/plugin_loader-prerelease.service
|
||||
cp ${HOMEBREW_FOLDER}/services/plugin_loader-backup.service ${HOMEBREW_FOLDER}/services/.systemd/plugin_loader-backup.service
|
||||
rm ${HOMEBREW_FOLDER}/services/plugin_loader-backup.service ${HOMEBREW_FOLDER}/services/plugin_loader-prerelease.service
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl start plugin_loader
|
||||
systemctl enable plugin_loader
|
||||
echo This script is deprecated! Use https://github.com/SteamDeckHomebrew/decky-installer/raw/main/cli/install_prerelease.sh instead!
|
||||
exit 1
|
||||
Vendored
+2
-65
@@ -1,66 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
[ "$UID" -eq 0 ] || exec sudo "$0" "$@"
|
||||
|
||||
echo "Installing Steam Deck Plugin Loader release..."
|
||||
|
||||
USER_DIR="$(getent passwd $SUDO_USER | cut -d: -f6)"
|
||||
HOMEBREW_FOLDER="${USER_DIR}/homebrew"
|
||||
|
||||
# Create folder structure
|
||||
rm -rf "${HOMEBREW_FOLDER}/services"
|
||||
sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/services"
|
||||
sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/plugins"
|
||||
|
||||
# Download latest release and install it
|
||||
RELEASE=$(curl -s 'https://api.github.com/repos/SteamDeckHomebrew/decky-loader/releases' | jq -r "first(.[] | select(.prerelease == "false"))")
|
||||
read VERSION DOWNLOADURL < <(echo $(jq -r '.tag_name, .assets[].browser_download_url' <<< ${RELEASE}))
|
||||
|
||||
printf "Installing version %s...\n" "${VERSION}"
|
||||
curl -L $DOWNLOADURL --output ${HOMEBREW_FOLDER}/services/PluginLoader
|
||||
chmod +x ${HOMEBREW_FOLDER}/services/PluginLoader
|
||||
echo $VERSION > ${HOMEBREW_FOLDER}/services/.loader.version
|
||||
|
||||
systemctl --user stop plugin_loader 2> /dev/null
|
||||
systemctl --user disable plugin_loader 2> /dev/null
|
||||
|
||||
systemctl stop plugin_loader 2> /dev/null
|
||||
systemctl disable plugin_loader 2> /dev/null
|
||||
|
||||
curl -L https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/service-updater/dist/plugin_loader-release.service --output ${HOMEBREW_FOLDER}/services/plugin_loader-release.service
|
||||
|
||||
cat > "${HOMEBREW_FOLDER}/services/plugin_loader-backup.service" <<- EOM
|
||||
[Unit]
|
||||
Description=SteamDeck Plugin Loader
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
Restart=always
|
||||
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
|
||||
WorkingDirectory=${HOMEBREW_FOLDER}/services
|
||||
Environment=PLUGIN_PATH=${HOMEBREW_FOLDER}/plugins
|
||||
Environment=LOG_LEVEL=INFO
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOM
|
||||
|
||||
if [[ -f "${HOMEBREW_FOLDER}/services/plugin_loader-release.service" ]]; then
|
||||
printf "Grabbed latest release service.\n"
|
||||
sed -i -e "s|\${HOMEBREW_FOLDER}|${HOMEBREW_FOLDER}|" "${HOMEBREW_FOLDER}/services/plugin_loader-release.service"
|
||||
cp -f "${HOMEBREW_FOLDER}/services/plugin_loader-release.service" "/etc/systemd/system/plugin_loader.service"
|
||||
else
|
||||
printf "Could not curl latest release systemd service, using built-in service as a backup!\n"
|
||||
rm -f "/etc/systemd/system/plugin_loader.service"
|
||||
cp "${HOMEBREW_FOLDER}/services/plugin_loader-backup.service" "/etc/systemd/system/plugin_loader.service"
|
||||
fi
|
||||
|
||||
mkdir -p ${HOMEBREW_FOLDER}/services/.systemd
|
||||
cp ${HOMEBREW_FOLDER}/services/plugin_loader-release.service ${HOMEBREW_FOLDER}/services/.systemd/plugin_loader-release.service
|
||||
cp ${HOMEBREW_FOLDER}/services/plugin_loader-backup.service ${HOMEBREW_FOLDER}/services/.systemd/plugin_loader-backup.service
|
||||
rm ${HOMEBREW_FOLDER}/services/plugin_loader-backup.service ${HOMEBREW_FOLDER}/services/plugin_loader-release.service
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl start plugin_loader
|
||||
systemctl enable plugin_loader
|
||||
echo This script is deprecated! Use https://github.com/SteamDeckHomebrew/decky-installer/raw/main/cli/install_release.sh instead!
|
||||
exit 1
|
||||
+6
-4
@@ -1,14 +1,16 @@
|
||||
[Unit]
|
||||
Description=SteamDeck Plugin Loader
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
After=network.target
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
Restart=always
|
||||
KillMode=process
|
||||
TimeoutStopSec=15
|
||||
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
|
||||
WorkingDirectory=${HOMEBREW_FOLDER}/services
|
||||
Environment=PLUGIN_PATH=${HOMEBREW_FOLDER}/plugins
|
||||
Environment=UNPRIVILEGED_PATH=${HOMEBREW_FOLDER}
|
||||
Environment=PRIVILEGED_PATH=${HOMEBREW_FOLDER}
|
||||
Environment=LOG_LEVEL=DEBUG
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
WantedBy=multi-user.target
|
||||
|
||||
Vendored
+6
-4
@@ -1,14 +1,16 @@
|
||||
[Unit]
|
||||
Description=SteamDeck Plugin Loader
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
After=network.target
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
Restart=always
|
||||
KillMode=process
|
||||
TimeoutStopSec=15
|
||||
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
|
||||
WorkingDirectory=${HOMEBREW_FOLDER}/services
|
||||
Environment=PLUGIN_PATH=${HOMEBREW_FOLDER}/plugins
|
||||
Environment=UNPRIVILEGED_PATH=${HOMEBREW_FOLDER}
|
||||
Environment=PRIVILEGED_PATH=${HOMEBREW_FOLDER}
|
||||
Environment=LOG_LEVEL=INFO
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
WantedBy=multi-user.target
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 176.36 38">
|
||||
<!-- Generator: Adobe Illustrator 29.2.1, SVG Export Plug-In . SVG Version: 2.1.0 Build 116) -->
|
||||
<defs>
|
||||
<style>
|
||||
.st0 {
|
||||
fill: #3fafa8;
|
||||
}
|
||||
|
||||
.st1 {
|
||||
fill: #fff;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<rect class="st0" x="0" y="0" width="176.36" height="38" rx="19" ry="19"/>
|
||||
<g>
|
||||
<path class="st1" d="M59.4,26.66v-15.77h4.92c2.76,0,4.85.63,6.25,1.9,1.4,1.27,2.11,3.2,2.11,5.79s-.76,4.47-2.29,5.92c-1.53,1.45-3.58,2.17-6.17,2.17h-4.82ZM62.01,13.13v11.28h2.09c1.83,0,3.25-.5,4.28-1.49,1.03-.99,1.54-2.43,1.54-4.31s-.49-3.21-1.46-4.12c-.98-.91-2.41-1.37-4.31-1.37h-2.13Z"/>
|
||||
<path class="st1" d="M80.12,26.92c-1.78,0-3.2-.52-4.25-1.57-1.05-1.05-1.57-2.46-1.57-4.23,0-1.8.56-3.24,1.67-4.34s2.54-1.64,4.31-1.64,3.17.54,4.2,1.62c1.03,1.08,1.54,2.47,1.54,4.18s-.54,3.18-1.62,4.31c-1.08,1.13-2.51,1.69-4.28,1.69ZM80.22,24.84c1.01,0,1.8-.35,2.35-1.04.56-.69.84-1.63.84-2.81s-.28-2.05-.84-2.74c-.56-.69-1.34-1.04-2.35-1.04s-1.81.36-2.41,1.07c-.6.71-.9,1.64-.9,2.78s.3,2.11.89,2.78,1.4,1.01,2.42,1.01Z"/>
|
||||
<path class="st1" d="M103.61,15.4l-3.32,11.26h-2.67l-2.02-7.33c-.05-.19-.09-.34-.11-.45-.02-.11-.05-.25-.08-.43h-.05c-.03.18-.06.32-.09.43s-.07.25-.12.41l-2.19,7.36h-2.64l-3.31-11.26h2.6l2.01,7.71c.04.13.07.27.09.41.02.14.05.31.08.49h.07c.04-.19.07-.36.1-.5.03-.14.07-.29.12-.43l2.29-7.68h2.43l2.05,7.72c.02.09.05.21.08.36.03.15.07.33.1.54h.08c.04-.21.07-.36.09-.47.02-.11.06-.25.1-.43l1.95-7.72h2.39Z"/>
|
||||
<path class="st1" d="M115.36,26.66h-2.55v-6.59c0-.93-.19-1.64-.56-2.13-.37-.49-.93-.73-1.66-.73-.8,0-1.45.29-1.95.86-.5.57-.75,1.29-.75,2.17v6.42h-2.56v-11.26h2.56v1.53h.04c.4-.57.91-1.01,1.55-1.33.63-.31,1.32-.47,2.06-.47,1.25,0,2.2.4,2.85,1.19.65.79.98,1.92.98,3.4v6.94Z"/>
|
||||
<path class="st1" d="M118.22,26.66V9.98h2.56v16.67h-2.56Z"/>
|
||||
<path class="st1" d="M128.95,26.92c-1.78,0-3.2-.52-4.25-1.57s-1.57-2.46-1.57-4.23c0-1.8.56-3.24,1.67-4.34,1.11-1.1,2.54-1.64,4.31-1.64s3.17.54,4.2,1.62c1.03,1.08,1.54,2.47,1.54,4.18s-.54,3.18-1.62,4.31c-1.08,1.13-2.51,1.69-4.28,1.69ZM129.05,24.84c1.01,0,1.8-.35,2.35-1.04.56-.69.84-1.63.84-2.81s-.28-2.05-.84-2.74c-.56-.69-1.34-1.04-2.35-1.04s-1.81.36-2.41,1.07c-.6.71-.9,1.64-.9,2.78s.3,2.11.89,2.78c.59.67,1.4,1.01,2.42,1.01Z"/>
|
||||
<path class="st1" d="M144.71,26.66h-2.48v-1.4h-.04c-.4.54-.88.96-1.45,1.24-.57.28-1.21.42-1.91.42-1.04,0-1.89-.3-2.56-.89-.66-.59-1-1.37-1-2.33,0-1.03.33-1.86,1-2.49.66-.63,1.62-1.01,2.85-1.15l3.12-.35v-.54c0-.7-.19-1.22-.58-1.57s-.9-.52-1.53-.52-1.15.14-1.57.42c-.43.28-.78.68-1.06,1.2l-1.91-.98c.38-.76.98-1.38,1.8-1.86s1.8-.73,2.93-.73c1.42,0,2.51.37,3.26,1.12.75.74,1.13,1.82,1.13,3.24v7.17ZM142.25,22.08v-.62l-2.72.3c-.62.07-1.08.24-1.36.52-.29.28-.43.65-.43,1.12s.16.86.49,1.16c.33.3.75.45,1.27.45.82,0,1.49-.28,1.99-.83s.76-1.25.76-2.09Z"/>
|
||||
<path class="st1" d="M155.4,25.1c-.41.6-.93,1.06-1.55,1.36-.62.31-1.33.46-2.12.46-1.51,0-2.7-.5-3.57-1.5-.87-1-1.3-2.38-1.3-4.13,0-1.89.49-3.39,1.46-4.5.97-1.11,2.27-1.66,3.89-1.66.7,0,1.34.14,1.91.42.57.28,1,.63,1.29,1.06h.04v-6.62h2.56v16.67h-2.56v-1.56h-.04ZM149.46,21.19c0,1.14.26,2.04.78,2.68.52.65,1.24.97,2.16.97s1.69-.32,2.24-.97c.56-.64.84-1.47.84-2.49v-1.29c0-.82-.27-1.51-.81-2.06s-1.24-.83-2.1-.83c-.96,0-1.72.34-2.28,1.03s-.84,1.67-.84,2.95Z"/>
|
||||
</g>
|
||||
<path class="st1" d="M29.96,6.28h3.98c.66,0,1.19.53,1.19,1.19v8.35h4.36c.88,0,1.33,1.07.7,1.69l-7.56,7.56c-.37.37-.98.37-1.36,0l-7.57-7.56c-.63-.63-.18-1.69.7-1.69h4.36V7.47c0-.66.53-1.19,1.19-1.19ZM44.67,24.96v5.57c0,.66-.53,1.19-1.19,1.19h-23.06c-.66,0-1.19-.53-1.19-1.19v-5.57c0-.66.53-1.19,1.19-1.19h7.29l2.44,2.44c1,1,2.61,1,3.61,0l2.44-2.44h7.29c.66,0,1.19.53,1.19,1.19ZM38.5,29.34c0-.55-.45-.99-.99-.99s-.99.45-.99.99.45.99.99.99.99-.45.99-.99ZM41.68,29.34c0-.55-.45-.99-.99-.99s-.99.45-.99.99.45.99.99.99.99-.45.99-.99Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.8 KiB |
Generated
+175
@@ -0,0 +1,175 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1710146030,
|
||||
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_2": {
|
||||
"inputs": {
|
||||
"systems": "systems_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1710146030,
|
||||
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nix-github-actions": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"poetry2nix",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1703863825,
|
||||
"narHash": "sha256-rXwqjtwiGKJheXB43ybM8NwWB8rO2dSRrEqes0S7F5Y=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nix-github-actions",
|
||||
"rev": "5163432afc817cf8bd1f031418d1869e4c9d5547",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "nix-github-actions",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1719848872,
|
||||
"narHash": "sha256-H3+EC5cYuq+gQW8y0lSrrDZfH71LB4DAf+TDFyvwCNA=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "00d80d13810dbfea8ab4ed1009b09100cca86ba8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"poetry2nix": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils_2",
|
||||
"nix-github-actions": "nix-github-actions",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"systems": "systems_3",
|
||||
"treefmt-nix": "treefmt-nix"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1719850884,
|
||||
"narHash": "sha256-UU/lVTHFx0GpEkihoLJrMuM9DcuhZmNe3db45vshSyI=",
|
||||
"owner": "nix-community",
|
||||
"repo": "poetry2nix",
|
||||
"rev": "42262f382c68afab1113ebd1911d0c93822d756e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "poetry2nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"poetry2nix": "poetry2nix"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_2": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_3": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"id": "systems",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"treefmt-nix": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"poetry2nix",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1719749022,
|
||||
"narHash": "sha256-ddPKHcqaKCIFSFc/cvxS14goUhCOAwsM1PbMr0ZtHMg=",
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"rev": "8df5ff62195d4e67e2264df0b7f5e8c9995fd0bd",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
{
|
||||
description = "Decky development environment";
|
||||
# pulls in the python deps from poetry
|
||||
|
||||
inputs = {
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
poetry2nix = {
|
||||
url = "github:nix-community/poetry2nix";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
|
||||
outputs =
|
||||
{
|
||||
self,
|
||||
nixpkgs,
|
||||
flake-utils,
|
||||
poetry2nix,
|
||||
}:
|
||||
flake-utils.lib.eachDefaultSystem (
|
||||
system:
|
||||
let
|
||||
pkgs = import nixpkgs { inherit system; };
|
||||
p2n = (poetry2nix.lib.mkPoetry2Nix { inherit pkgs; });
|
||||
in
|
||||
{
|
||||
devShells.default =
|
||||
(p2n.mkPoetryEnv {
|
||||
projectDir = self + "/backend";
|
||||
# pyinstaller fails to compile so precompiled it is
|
||||
overrides = p2n.overrides.withDefaults (
|
||||
final: prev: {
|
||||
pyinstaller = prev.pyinstaller.override { preferWheel = true; };
|
||||
pyright = null;
|
||||
}
|
||||
);
|
||||
}).env.overrideAttrs
|
||||
(oldAttrs: {
|
||||
shellHook = ''
|
||||
PYTHONPATH=`which python`
|
||||
FILE=.vscode/settings.json
|
||||
if [ -f "$FILE" ]; then
|
||||
jq --arg pythonpath "$PYTHONPATH" '.["python.defaultInterpreterPath"] = $pythonpath' $FILE > "$FILE.tmp" && mv "$FILE.tmp" "$FILE"
|
||||
else
|
||||
echo "{\"python.defaultInterpreterPath\": \"$PYTHONPATH\"}" > "$FILE"
|
||||
fi
|
||||
'';
|
||||
UV_USE_IO_URING = 0; # work around node#48444
|
||||
nativeBuildInputs = with pkgs; [
|
||||
python311Packages.setuptools
|
||||
];
|
||||
buildInputs = with pkgs; [
|
||||
nodejs_22
|
||||
nodePackages.pnpm
|
||||
poetry
|
||||
jq
|
||||
# fixes local pyright not being able to see the pythonpath properly.
|
||||
(pkgs.writeShellScriptBin "pyright" ''${pkgs.pyright}/bin/pyright --pythonpath `which python3` "$@" '')
|
||||
(pkgs.writeShellScriptBin "pyright-langserver" ''${pkgs.pyright}/bin/pyright-langserver --pythonpath `which python3` "$@" '')
|
||||
(pkgs.writeShellScriptBin "pyright-python" ''${pkgs.pyright}/bin/pyright-python --pythonpath `which python3` "$@" '')
|
||||
(pkgs.writeShellScriptBin "pyright-python-langserver" ''${pkgs.pyright}/bin/pyright-python-langserver --pythonpath `which python3` "$@" '')
|
||||
];
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -2,3 +2,5 @@ node_modules/
|
||||
|
||||
.yalc
|
||||
yalc.lock
|
||||
|
||||
stats.html
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
module.exports = {
|
||||
import importSort from 'prettier-plugin-import-sort';
|
||||
export default {
|
||||
semi: true,
|
||||
trailingComma: 'all',
|
||||
singleQuote: true,
|
||||
printWidth: 120,
|
||||
tabWidth: 2,
|
||||
endOfLine: 'auto',
|
||||
plugins: [require('prettier-plugin-import-sort')],
|
||||
plugins: [importSort],
|
||||
};
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 55 KiB |
@@ -0,0 +1,103 @@
|
||||
// import { readdir } from "fs/promises";
|
||||
|
||||
export default {
|
||||
contextSeparator: '_',
|
||||
// Key separator used in your translation keys
|
||||
|
||||
createOldCatalogs: false,
|
||||
// Save the \_old files
|
||||
|
||||
defaultNamespace: 'translation',
|
||||
// Default namespace used in your i18next config
|
||||
|
||||
defaultValue: '',
|
||||
// Default value to give to keys with no value
|
||||
// You may also specify a function accepting the locale, namespace, key, and value as arguments
|
||||
|
||||
indentation: 4,
|
||||
// Indentation of the catalog files. Weblate seems to like 4.
|
||||
|
||||
keepRemoved: true,
|
||||
// Keep keys from the catalog that are no longer in code
|
||||
|
||||
keySeparator: '.',
|
||||
// Key separator used in your translation keys
|
||||
// If you want to use plain english keys, separators such as `.` and `:` will conflict. You might want to set `keySeparator: false` and `namespaceSeparator: false`. That way, `t('Status: Loading...')` will not think that there are a namespace and three separator dots for instance.
|
||||
|
||||
// see below for more details
|
||||
lexers: {
|
||||
mjs: ['JavascriptLexer'],
|
||||
js: ['JavascriptLexer'], // if you're writing jsx inside .js files, change this to JsxLexer
|
||||
ts: ['JavascriptLexer'],
|
||||
jsx: ['JsxLexer'],
|
||||
tsx: ['JsxLexer'],
|
||||
|
||||
default: ['JavascriptLexer'],
|
||||
},
|
||||
|
||||
lineEnding: 'auto',
|
||||
// Control the line ending. See options at https://github.com/ryanve/eol
|
||||
|
||||
// locales: (await readdir('../backend/decky_loader/locales/')).map(lang => lang.replace(".json", "")),
|
||||
locales: ["en-US"],
|
||||
// An array of the locales in your applications
|
||||
|
||||
namespaceSeparator: false,
|
||||
// Namespace separator used in your translation keys
|
||||
// If you want to use plain english keys, separators such as `.` and `:` will conflict. You might want to set `keySeparator: false` and `namespaceSeparator: false`. That way, `t('Status: Loading...')` will not think that there are a namespace and three separator dots for instance.
|
||||
|
||||
output: '../backend/decky_loader/locales/$LOCALE.json',
|
||||
// Supports $LOCALE and $NAMESPACE injection
|
||||
// Supports JSON (.json) and YAML (.yml) file formats
|
||||
// Where to write the locale files relative to process.cwd()
|
||||
|
||||
pluralSeparator: '_',
|
||||
// Plural separator used in your translation keys
|
||||
// If you want to use plain english keys, separators such as `_` might conflict. You might want to set `pluralSeparator` to a different string that does not occur in your keys.
|
||||
|
||||
input: './src/**/*.{ts,tsx}',
|
||||
// An array of globs that describe where to look for source files
|
||||
// relative to the location of the configuration file
|
||||
|
||||
sort: (a, b) => a.localeCompare(b, undefined, { numeric: true }),
|
||||
// Whether or not to sort the catalog. Can also be a [compareFunction](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#parameters)
|
||||
|
||||
verbose: false,
|
||||
// Display info about the parsing including some stats
|
||||
|
||||
failOnWarnings: false,
|
||||
// Exit with an exit code of 1 on warnings
|
||||
|
||||
failOnUpdate: false,
|
||||
// Exit with an exit code of 1 when translations are updated (for CI purpose)
|
||||
|
||||
customValueTemplate: null,
|
||||
// If you wish to customize the value output the value as an object, you can set your own format.
|
||||
// ${defaultValue} is the default value you set in your translation function.
|
||||
// Any other custom property will be automatically extracted.
|
||||
//
|
||||
// Example:
|
||||
// {
|
||||
// message: "${defaultValue}",
|
||||
// description: "${maxLength}", // t('my-key', {maxLength: 150})
|
||||
// }
|
||||
|
||||
resetDefaultValueLocale: null,
|
||||
// The locale to compare with default values to determine whether a default value has been changed.
|
||||
// If this is set and a default value differs from a translation in the specified locale, all entries
|
||||
// for that key across locales are reset to the default value, and existing translations are moved to
|
||||
// the `_old` file.
|
||||
|
||||
i18nextOptions: null,
|
||||
// If you wish to customize options in internally used i18next instance, you can define an object with any
|
||||
// configuration property supported by i18next (https://www.i18next.com/overview/configuration-options).
|
||||
// { compatibilityJSON: 'v3' } can be used to generate v3 compatible plurals.
|
||||
|
||||
yamlOptions: null,
|
||||
// If you wish to customize options for yaml output, you can define an object here.
|
||||
// Configuration options are here (https://github.com/nodeca/js-yaml#dump-object---options-).
|
||||
// Example:
|
||||
// {
|
||||
// lineWidth: -1,
|
||||
// }
|
||||
}
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
declare module '*.png';
|
||||
declare module '*.jpg';
|
||||
+38
-27
@@ -1,38 +1,44 @@
|
||||
{
|
||||
"name": "decky_frontend",
|
||||
"version": "2.1.1",
|
||||
"name": "@decky/loader-frontend",
|
||||
"private": true,
|
||||
"license": "GPLV2",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"prepare": "cd .. && husky install frontend/.husky",
|
||||
"build": "rollup -c",
|
||||
"watch": "rollup -c -w",
|
||||
"lint": "prettier -c src",
|
||||
"format": "prettier -c src -w"
|
||||
"typecheck": "tsc --noEmit",
|
||||
"format": "prettier -c src -w",
|
||||
"localize": "i18next"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^21.1.0",
|
||||
"@rollup/plugin-json": "^4.1.0",
|
||||
"@rollup/plugin-node-resolve": "^13.3.0",
|
||||
"@rollup/plugin-replace": "^4.0.0",
|
||||
"@rollup/plugin-typescript": "^8.3.3",
|
||||
"@types/react": "16.14.0",
|
||||
"@types/react-file-icon": "^1.0.1",
|
||||
"@types/react-router": "5.1.18",
|
||||
"@types/webpack": "^5.28.0",
|
||||
"husky": "^8.0.1",
|
||||
"@decky/api": "^1.1.3",
|
||||
"@rollup/plugin-commonjs": "^26.0.1",
|
||||
"@rollup/plugin-image": "^3.0.3",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"@rollup/plugin-replace": "^5.0.7",
|
||||
"@rollup/plugin-typescript": "^11.1.6",
|
||||
"@types/react": "19.1.1",
|
||||
"@types/react-dom": "19.1.1",
|
||||
"@types/react-file-icon": "^1.0.4",
|
||||
"@types/react-router": "5.1.20",
|
||||
"husky": "^9.0.11",
|
||||
"i18next-parser": "^9.0.0",
|
||||
"import-sort-style-module": "^6.0.0",
|
||||
"inquirer": "^8.2.4",
|
||||
"prettier": "^2.7.1",
|
||||
"inquirer": "^9.2.23",
|
||||
"prettier": "^3.3.2",
|
||||
"prettier-plugin-import-sort": "^0.0.7",
|
||||
"react": "16.14.0",
|
||||
"react-dom": "16.14.0",
|
||||
"rollup": "^2.76.0",
|
||||
"react": "19.1.1",
|
||||
"react-dom": "19.1.1",
|
||||
"rollup": "^4.22.4",
|
||||
"rollup-plugin-delete": "^2.0.0",
|
||||
"rollup-plugin-external-globals": "^0.6.1",
|
||||
"rollup-plugin-polyfill-node": "^0.10.2",
|
||||
"tslib": "^2.4.0",
|
||||
"typescript": "^4.7.4"
|
||||
"rollup-plugin-external-globals": "^0.10.0",
|
||||
"rollup-plugin-polyfill-node": "^0.13.0",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"tslib": "^2.6.3",
|
||||
"typescript": "^5.4.5"
|
||||
},
|
||||
"importSort": {
|
||||
".js, .jsx, .ts, .tsx": {
|
||||
@@ -41,10 +47,15 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"decky-frontend-lib": "^3.7.12",
|
||||
"react-file-icon": "^1.2.0",
|
||||
"react-icons": "^4.4.0",
|
||||
"react-markdown": "^8.0.3",
|
||||
"remark-gfm": "^3.0.1"
|
||||
"@decky/ui": "^4.11.4",
|
||||
"compare-versions": "^6.1.1",
|
||||
"filesize": "^10.1.2",
|
||||
"i18next": "^25.6.0",
|
||||
"i18next-http-backend": "^2.5.2",
|
||||
"react-file-icon": "^1.6.0",
|
||||
"react-i18next": "^16.0.1",
|
||||
"react-icons": "^5.2.1",
|
||||
"react-markdown": "^9.0.1",
|
||||
"remark-gfm": "^4.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+3830
-2030
File diff suppressed because it is too large
Load Diff
+64
-30
@@ -1,39 +1,73 @@
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import image from '@rollup/plugin-image';
|
||||
import json from '@rollup/plugin-json';
|
||||
import { nodeResolve } from '@rollup/plugin-node-resolve';
|
||||
import externalGlobals from "rollup-plugin-external-globals";
|
||||
import del from 'rollup-plugin-delete'
|
||||
import replace from '@rollup/plugin-replace';
|
||||
import typescript from '@rollup/plugin-typescript';
|
||||
import { defineConfig } from 'rollup';
|
||||
import del from 'rollup-plugin-delete';
|
||||
import externalGlobals from 'rollup-plugin-external-globals';
|
||||
import { visualizer } from 'rollup-plugin-visualizer';
|
||||
|
||||
export default defineConfig({
|
||||
input: 'src/index.tsx',
|
||||
plugins: [
|
||||
del({ targets: "../backend/static/*", force: true }),
|
||||
commonjs(),
|
||||
nodeResolve(),
|
||||
externalGlobals({
|
||||
react: 'SP_REACT',
|
||||
'react-dom': 'SP_REACTDOM',
|
||||
// hack to shut up react-markdown
|
||||
'process': '{cwd: () => {}}',
|
||||
'path': '{dirname: () => {}, join: () => {}, basename: () => {}, extname: () => {}}',
|
||||
'url': '{fileURLToPath: (f) => f}'
|
||||
}),
|
||||
typescript(),
|
||||
json(),
|
||||
replace({
|
||||
preventAssignment: false,
|
||||
'process.env.NODE_ENV': JSON.stringify('production'),
|
||||
}),
|
||||
],
|
||||
preserveEntrySignatures: false,
|
||||
output: {
|
||||
dir: '../backend/static',
|
||||
format: 'esm',
|
||||
chunkFileNames: (chunkInfo) => {
|
||||
return 'chunk-[hash].js'
|
||||
const hiddenWarnings = ['THIS_IS_UNDEFINED', 'EVAL'];
|
||||
|
||||
export default defineConfig([
|
||||
// Main bundle
|
||||
{
|
||||
input: 'src/index.ts',
|
||||
plugins: [
|
||||
del({ targets: ['../backend/decky_loader/static/*', '!../backend/decky_loader/static/fallback.js'], force: true }),
|
||||
commonjs(),
|
||||
nodeResolve({
|
||||
browser: true,
|
||||
}),
|
||||
externalGlobals({
|
||||
react: 'SP_REACT',
|
||||
'react/jsx-runtime': 'SP_JSX',
|
||||
'react-dom': 'SP_REACTDOM',
|
||||
// hack to shut up react-markdown
|
||||
process: '{cwd: () => {}}',
|
||||
path: '{dirname: () => {}, join: () => {}, basename: () => {}, extname: () => {}}',
|
||||
url: '{fileURLToPath: (f) => f}',
|
||||
}),
|
||||
typescript(),
|
||||
json(),
|
||||
replace({
|
||||
preventAssignment: false,
|
||||
'process.env.NODE_ENV': JSON.stringify('production'),
|
||||
}),
|
||||
image(),
|
||||
visualizer(),
|
||||
],
|
||||
preserveEntrySignatures: false,
|
||||
treeshake: {
|
||||
// Assume all external modules have imports with side effects (the default) while allowing decky libraries to treeshake
|
||||
pureExternalImports: true,
|
||||
preset: 'smallest'
|
||||
},
|
||||
output: {
|
||||
dir: '../backend/decky_loader/static',
|
||||
format: 'esm',
|
||||
chunkFileNames: (chunkInfo) => {
|
||||
return 'chunk-[hash].js';
|
||||
},
|
||||
sourcemap: true,
|
||||
sourcemapPathTransform: (relativeSourcePath) => relativeSourcePath.replace(/^\.\.\//, `decky://decky/loader/`),
|
||||
},
|
||||
onwarn: function (message, handleWarning) {
|
||||
if (hiddenWarnings.some((warning) => message.code === warning)) return;
|
||||
handleWarning(message);
|
||||
},
|
||||
},
|
||||
// Fallback
|
||||
{
|
||||
input: 'src/fallback.ts',
|
||||
plugins: [
|
||||
typescript()
|
||||
],
|
||||
output: {
|
||||
file: '../backend/decky_loader/static/fallback.js',
|
||||
format: 'esm',
|
||||
}
|
||||
}
|
||||
});
|
||||
]);
|
||||
|
||||
@@ -0,0 +1,445 @@
|
||||
import { joinClassNames, sleep } from '@decky/ui';
|
||||
import { FunctionComponent, useEffect, useReducer, useState } from 'react';
|
||||
|
||||
import { disablePlugin, uninstallPlugin } from '../plugin';
|
||||
import { VerInfo, doRestart, doShutdown } from '../updater';
|
||||
import { ValveReactErrorInfo, getLikelyErrorSourceFromValveReactError } from '../utils/errors';
|
||||
import { useSetting } from '../utils/hooks/useSetting';
|
||||
import { UpdateBranch } from './settings/pages/general/BranchSelect';
|
||||
|
||||
interface DeckyErrorBoundaryProps {
|
||||
error: ValveReactErrorInfo;
|
||||
errorKey: string;
|
||||
identifier: string;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
SystemNetworkStore?: any;
|
||||
}
|
||||
}
|
||||
|
||||
const classes = {
|
||||
root: 'deckyErrorBoundary',
|
||||
likelyOccurred: 'likely-occured-msg',
|
||||
panel: 'panel-section',
|
||||
panelHeader: 'panel-header',
|
||||
trace: 'trace',
|
||||
rowList: 'row-list',
|
||||
rowItem: 'row-item',
|
||||
buttonDescRow: 'button-description-row',
|
||||
flexRowWGap: 'flex-row',
|
||||
marginBottom: 'margin-bottom',
|
||||
swipePrompt: 'swipe-prompt',
|
||||
};
|
||||
|
||||
const vars = {
|
||||
scrollBarwidth: '18px',
|
||||
rootMarginLeft: '15px',
|
||||
panelXPadding: '20px',
|
||||
};
|
||||
|
||||
export const startSSH = DeckyBackend.callable('utilities/start_ssh');
|
||||
export const starrCEFForwarding = DeckyBackend.callable('utilities/allow_remote_debugging');
|
||||
|
||||
function ipToString(ip: number) {
|
||||
return [(ip >>> 24) & 255, (ip >>> 16) & 255, (ip >>> 8) & 255, (ip >>> 0) & 255].join('.');
|
||||
}
|
||||
|
||||
// Intentionally not localized since we can't really trust React here
|
||||
const DeckyErrorBoundary: FunctionComponent<DeckyErrorBoundaryProps> = ({ error, identifier, reset }) => {
|
||||
const [actionLog, addLogLine] = useReducer((log: string, line: string) => (log += '\n' + line), '');
|
||||
const [actionsEnabled, setActionsEnabled] = useState<boolean>(true);
|
||||
const [debugAllowed, setDebugAllowed] = useState<boolean>(true);
|
||||
// Intentionally doesn't use DeckyState.
|
||||
const [versionInfo, setVersionInfo] = useState<VerInfo>();
|
||||
const [errorSource, wasCausedByPlugin, shouldReportToValve] = getLikelyErrorSourceFromValveReactError(error);
|
||||
useEffect(() => {
|
||||
if (!shouldReportToValve) DeckyPluginLoader.errorBoundaryHook.temporarilyDisableReporting();
|
||||
DeckyPluginLoader.updateVersion().then(setVersionInfo);
|
||||
}, []);
|
||||
|
||||
const [selectedBranch, setSelectedBranch] = useSetting<UpdateBranch>('branch', UpdateBranch.Stable);
|
||||
const [isChecking, setIsChecking] = useState<boolean>(false);
|
||||
const [updateProgress, setUpdateProgress] = useState<number>(-1);
|
||||
const [versionToUpdateTo, setSetVersionToUpdateTo] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
const a = DeckyBackend.addEventListener('updater/update_download_percentage', (percentage) => {
|
||||
setUpdateProgress(percentage);
|
||||
});
|
||||
|
||||
const b = DeckyBackend.addEventListener('updater/finish_download', () => {
|
||||
setUpdateProgress(-2);
|
||||
});
|
||||
|
||||
return () => {
|
||||
DeckyBackend.removeEventListener('updater/update_download_percentage', a);
|
||||
DeckyBackend.removeEventListener('updater/finish_download', b);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<style>
|
||||
{`
|
||||
*:has(> .${classes.root}) {
|
||||
margin-top: var(--basicui-header-height);
|
||||
overflow: scroll !important;
|
||||
background: #000;
|
||||
}
|
||||
*:has(> .${classes.root})::-webkit-scrollbar {
|
||||
display: initial !important;
|
||||
width: ${vars.scrollBarwidth};
|
||||
height: 0px;
|
||||
}
|
||||
*:has(> .${classes.root})::-webkit-scrollbar-thumb {
|
||||
background: #4349535e;
|
||||
}
|
||||
.${classes.root} {
|
||||
color: #93929e;
|
||||
font-size: 15px;
|
||||
margin: 10px 0px 40px ${vars.rootMarginLeft};
|
||||
width: calc(100vw - ${vars.scrollBarwidth} - ${vars.rootMarginLeft});
|
||||
overflow: visible;
|
||||
}
|
||||
.${classes.root} button,
|
||||
.${classes.root} select {
|
||||
border: none;
|
||||
padding: 4px 16px !important;
|
||||
background: #333;
|
||||
color: #ddd;
|
||||
font-size: 12px;
|
||||
border-radius: 3px;
|
||||
outline: none;
|
||||
height: 28px;
|
||||
}
|
||||
.${classes.panel} {
|
||||
background: #080808;
|
||||
padding: 8px ${vars.panelXPadding};
|
||||
border-radius: 3px;
|
||||
/* box-shadow: 9px 9px 20px -5px rgb(0 0 0 / 89%); */
|
||||
}
|
||||
.${classes.panelHeader} {
|
||||
font-size: 18px;
|
||||
font-weight: bolder;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.${classes.likelyOccurred} {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
color: #588fb4;
|
||||
}
|
||||
.${classes.rowItem} {
|
||||
position: relative;
|
||||
}
|
||||
.${classes.rowItem}:not(:last-child)::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -4.5px;
|
||||
left: 5px;
|
||||
right: 15px;
|
||||
height: 0.5px;
|
||||
background: #3c3c3c47;
|
||||
}
|
||||
.${classes.flexRowWGap},
|
||||
.${classes.buttonDescRow},
|
||||
.${classes.rowList},
|
||||
.${classes.panel} {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.${classes.rowList},
|
||||
.${classes.panel} {
|
||||
flex-direction: column;
|
||||
}
|
||||
.${classes.flexRowWGap},
|
||||
.${classes.rowList} {
|
||||
gap: 8px;
|
||||
}
|
||||
.${classes.marginBottom} {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.${classes.buttonDescRow} {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.${classes.swipePrompt} {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
font-style: italic;
|
||||
font-size: small;
|
||||
margin: 16px 0;
|
||||
}
|
||||
.${classes.swipePrompt} span {
|
||||
padding: 0 8px;
|
||||
background-color: #000;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.${classes.swipePrompt}::before,
|
||||
.${classes.swipePrompt}::after {
|
||||
content: "";
|
||||
flex-grow: 1;
|
||||
border-bottom: 1px solid #474752;
|
||||
top: 50%;
|
||||
}
|
||||
.${classes.swipePrompt}::before {
|
||||
right: 50%;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.${classes.swipePrompt}::after {
|
||||
left: 50%;
|
||||
margin-left: 8px;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<div className={classes.root}>
|
||||
<div className={classes.marginBottom}>An error occurred while rendering this content.</div>
|
||||
<pre className={joinClassNames(classes.marginBottom)} style={{ marginTop: '0px' }}>
|
||||
<code>
|
||||
{identifier && `Error Reference: ${identifier}`}
|
||||
{versionInfo?.current && `\nDecky Version: ${versionInfo.current}`}
|
||||
</code>
|
||||
</pre>
|
||||
<div className={joinClassNames(classes.likelyOccurred, classes.marginBottom)}>
|
||||
This error likely occurred in {errorSource}.
|
||||
</div>
|
||||
{actionLog?.length > 0 && (
|
||||
<pre>
|
||||
<code>
|
||||
Running actions...
|
||||
{actionLog}
|
||||
</code>
|
||||
</pre>
|
||||
)}
|
||||
{actionsEnabled && (
|
||||
<div className={classes.panel}>
|
||||
<div className={classes.flexRowWGap} style={{ alignItems: 'center', marginBottom: '8px' }}>
|
||||
<div className={classes.panelHeader}>Actions</div>
|
||||
<div style={{ fontSize: 'small', fontStyle: 'italic' }}>
|
||||
Use the touch screen. Solutions are listed in the recommended order. If you are still experiencing
|
||||
issues, please post in the #loader-support channel at decky.xyz/discord.
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.rowList}>
|
||||
<div className={joinClassNames(classes.rowItem, classes.buttonDescRow)}>
|
||||
Retry the action or restart
|
||||
<div className={classes.flexRowWGap}>
|
||||
<button onClick={reset}>Retry</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
addLogLine('Restarting Steam...');
|
||||
SteamClient.User.StartRestart(false);
|
||||
}}
|
||||
>
|
||||
Restart Steam
|
||||
</button>
|
||||
<button
|
||||
onClick={async () => {
|
||||
setActionsEnabled(false);
|
||||
addLogLine('Restarting Decky...');
|
||||
doRestart();
|
||||
await sleep(2000);
|
||||
addLogLine('Reloading UI...');
|
||||
}}
|
||||
>
|
||||
Restart Decky
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{wasCausedByPlugin && (
|
||||
<div className={joinClassNames(classes.rowItem, classes.buttonDescRow)}>
|
||||
Disable or uninstall the suspected plugin
|
||||
<div className={classes.flexRowWGap}>
|
||||
<button
|
||||
onClick={async () => {
|
||||
setActionsEnabled(false);
|
||||
addLogLine(`Disabling ${errorSource}...`);
|
||||
await disablePlugin(errorSource);
|
||||
await sleep(1000);
|
||||
addLogLine('Restarting Decky...');
|
||||
doRestart();
|
||||
await sleep(2000);
|
||||
addLogLine('Restarting Steam...');
|
||||
await sleep(500);
|
||||
SteamClient.User.StartRestart(false);
|
||||
}}
|
||||
>
|
||||
Disable {errorSource}
|
||||
</button>
|
||||
<button
|
||||
onClick={async () => {
|
||||
setActionsEnabled(false);
|
||||
addLogLine(`Uninstalling ${errorSource}...`);
|
||||
await uninstallPlugin(errorSource);
|
||||
await DeckyPluginLoader.frozenPluginsService.invalidate();
|
||||
await DeckyPluginLoader.hiddenPluginsService.invalidate();
|
||||
await sleep(1000);
|
||||
addLogLine('Restarting Decky...');
|
||||
doRestart();
|
||||
await sleep(2000);
|
||||
addLogLine('Restarting Steam...');
|
||||
await sleep(500);
|
||||
SteamClient.User.StartRestart(false);
|
||||
}}
|
||||
>
|
||||
Uninstall {errorSource}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={joinClassNames(classes.rowItem, classes.buttonDescRow)}>
|
||||
Disable all plugins
|
||||
<button
|
||||
onClick={async () => {
|
||||
setActionsEnabled(false);
|
||||
addLogLine(`Disabling plugins...`);
|
||||
await DeckyBackend.call('utilities/set_all_plugins_disabled');
|
||||
await sleep(1000);
|
||||
addLogLine('Restarting Decky...');
|
||||
doRestart();
|
||||
await sleep(2000);
|
||||
addLogLine('Restarting Steam...');
|
||||
await sleep(500);
|
||||
SteamClient.User.StartRestart(false);
|
||||
}}
|
||||
>
|
||||
Disable All Plugins
|
||||
</button>
|
||||
</div>
|
||||
{
|
||||
<div className={joinClassNames(classes.rowItem, classes.buttonDescRow)}>
|
||||
{updateProgress > -1
|
||||
? 'Update in progress... ' + updateProgress + '%'
|
||||
: updateProgress == -2
|
||||
? 'Update complete. Restarting...'
|
||||
: 'Check for Decky updates'}
|
||||
{
|
||||
<div className={classes.flexRowWGap}>
|
||||
{updateProgress == -1 && (
|
||||
<>
|
||||
<select
|
||||
onChange={async (e) => {
|
||||
const branch = parseInt(e.target.value);
|
||||
setSelectedBranch(branch);
|
||||
setSetVersionToUpdateTo('');
|
||||
}}
|
||||
>
|
||||
<option value="0" selected={selectedBranch == UpdateBranch.Stable}>
|
||||
Stable
|
||||
</option>
|
||||
<option value="1" selected={selectedBranch == UpdateBranch.Prerelease}>
|
||||
Pre-Release
|
||||
</option>
|
||||
<option value="2" selected={selectedBranch == UpdateBranch.Testing}>
|
||||
Testing
|
||||
</option>
|
||||
</select>
|
||||
<button
|
||||
disabled={updateProgress != -1 || isChecking}
|
||||
onClick={async () => {
|
||||
if (versionToUpdateTo == '') {
|
||||
setIsChecking(true);
|
||||
const versionInfo = (await DeckyBackend.callable(
|
||||
'updater/check_for_updates',
|
||||
)()) as unknown as VerInfo;
|
||||
setIsChecking(false);
|
||||
if (versionInfo?.remote && versionInfo?.remote?.tag_name != versionInfo?.current) {
|
||||
setSetVersionToUpdateTo(versionInfo.remote.tag_name);
|
||||
} else {
|
||||
setSetVersionToUpdateTo('');
|
||||
}
|
||||
} else {
|
||||
DeckyBackend.callable('updater/do_update')();
|
||||
setUpdateProgress(0);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{' '}
|
||||
{isChecking
|
||||
? 'Checking for updates...'
|
||||
: versionToUpdateTo != ''
|
||||
? 'Update to ' + versionToUpdateTo
|
||||
: 'Check for updates'}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<div className={joinClassNames(classes.rowItem, classes.buttonDescRow)}>
|
||||
Disable Decky until next boot
|
||||
<button
|
||||
onClick={async () => {
|
||||
setActionsEnabled(false);
|
||||
addLogLine('Stopping Decky...');
|
||||
doShutdown();
|
||||
await sleep(5000);
|
||||
addLogLine('Restarting Steam...');
|
||||
SteamClient.User.StartRestart(false);
|
||||
}}
|
||||
>
|
||||
Disable Decky
|
||||
</button>
|
||||
</div>
|
||||
{debugAllowed && (
|
||||
<div className={joinClassNames(classes.rowItem, classes.buttonDescRow)}>
|
||||
Enable remote debugging and SSH until next boot (for developers)
|
||||
<button
|
||||
onClick={async () => {
|
||||
setDebugAllowed(false);
|
||||
addLogLine('Enabling CEF debugger forwarding...');
|
||||
await starrCEFForwarding();
|
||||
addLogLine('Enabling SSH...');
|
||||
await startSSH();
|
||||
addLogLine('Ready for debugging!');
|
||||
if (window?.SystemNetworkStore?.wirelessNetworkDevice?.ip4?.addresses?.[0]?.ip) {
|
||||
const ip = ipToString(window.SystemNetworkStore.wirelessNetworkDevice.ip4.addresses[0].ip);
|
||||
addLogLine(`CEF Debugger: http://${ip}:8081`);
|
||||
addLogLine(`SSH: deck@${ip}`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Enable
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{actionsEnabled && (
|
||||
<div className={classes.swipePrompt}>
|
||||
<span>Swipe to scroll</span>
|
||||
</div>
|
||||
)}
|
||||
<div className={classes.panel}>
|
||||
<div className={classes.panelHeader}>Trace</div>
|
||||
<pre
|
||||
style={{
|
||||
margin: `8px calc(-1 * ${vars.panelXPadding})`,
|
||||
userSelect: 'auto',
|
||||
overflowX: 'scroll',
|
||||
padding: `0px ${vars.panelXPadding}`,
|
||||
maskImage: `linear-gradient(to right, transparent, black ${vars.panelXPadding}, black calc(100% - ${vars.panelXPadding}), transparent)`,
|
||||
}}
|
||||
>
|
||||
<code>
|
||||
{error.error.stack}
|
||||
{'\n\n'}
|
||||
Component Stack:
|
||||
{error.info.componentStack}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeckyErrorBoundary;
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FC, createContext, useContext, useEffect, useState } from 'react';
|
||||
import { FC, ReactNode, createContext, useContext, useEffect, useState } from 'react';
|
||||
|
||||
interface PublicDeckyGlobalComponentsState {
|
||||
components: Map<string, FC>;
|
||||
@@ -40,6 +40,7 @@ export const useDeckyGlobalComponentsState = () => useContext(DeckyGlobalCompone
|
||||
|
||||
interface Props {
|
||||
deckyGlobalComponentsState: DeckyGlobalComponentsState;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const DeckyGlobalComponentsStateContextProvider: FC<Props> = ({
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { FC, SVGAttributes } from 'react';
|
||||
|
||||
const DeckyIcon: FC<SVGAttributes<SVGElement>> = (props) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="100%" width="100%" viewBox="0 0 512 456" {...props}>
|
||||
<g>
|
||||
<path
|
||||
style={{ fill: 'none' }}
|
||||
d="M154.33,72.51v49.79c11.78-0.17,23.48,2,34.42,6.39c10.93,4.39,20.89,10.91,29.28,19.18
|
||||
c8.39,8.27,15.06,18.13,19.61,29c4.55,10.87,6.89,22.54,6.89,34.32c0,11.78-2.34,23.45-6.89,34.32
|
||||
c-4.55,10.87-11.21,20.73-19.61,29c-8.39,8.27-18.35,14.79-29.28,19.18c-10.94,4.39-22.63,6.56-34.42,6.39v49.77
|
||||
c36.78,0,72.05-14.61,98.05-40.62c26-26.01,40.61-61.28,40.61-98.05c0-36.78-14.61-72.05-40.61-98.05
|
||||
C226.38,87.12,191.11,72.51,154.33,72.51z"
|
||||
/>
|
||||
|
||||
<ellipse
|
||||
transform="matrix(0.982 -0.1891 0.1891 0.982 -37.1795 32.9988)"
|
||||
style={{ fill: 'none' }}
|
||||
cx="154.33"
|
||||
cy="211.33"
|
||||
rx="69.33"
|
||||
ry="69.33"
|
||||
/>
|
||||
<path style={{ fill: 'none' }} d="M430,97h-52v187h52c7.18,0,13-5.82,13-13V110C443,102.82,437.18,97,430,97z" />
|
||||
<path
|
||||
style={{ fill: 'currentColor' }}
|
||||
d="M432,27h-54V0H0v361c0,52.47,42.53,95,95,95h188c52.47,0,95-42.53,95-95v-7h54c44.18,0,80-35.82,80-80V107
|
||||
C512,62.82,476.18,27,432,27z M85,211.33c0-38.29,31.04-69.33,69.33-69.33c38.29,0,69.33,31.04,69.33,69.33
|
||||
c0,38.29-31.04,69.33-69.33,69.33C116.04,280.67,85,249.62,85,211.33z M252.39,309.23c-26.01,26-61.28,40.62-98.05,40.62v-49.77
|
||||
c11.78,0.17,23.48-2,34.42-6.39c10.93-4.39,20.89-10.91,29.28-19.18c8.39-8.27,15.06-18.13,19.61-29
|
||||
c4.55-10.87,6.89-22.53,6.89-34.32c0-11.78-2.34-23.45-6.89-34.32c-4.55-10.87-11.21-20.73-19.61-29
|
||||
c-8.39-8.27-18.35-14.79-29.28-19.18c-10.94-4.39-22.63-6.56-34.42-6.39V72.51c36.78,0,72.05,14.61,98.05,40.61
|
||||
c26,26.01,40.61,61.28,40.61,98.05C293,247.96,278.39,283.23,252.39,309.23z M443,271c0,7.18-5.82,13-13,13h-52V97h52
|
||||
c7.18,0,13,5.82,13,13V271z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default DeckyIcon;
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ComponentType, FC, createContext, useContext, useEffect, useState } from 'react';
|
||||
import { ComponentType, FC, ReactNode, createContext, useContext, useEffect, useState } from 'react';
|
||||
import type { RouteProps } from 'react-router';
|
||||
|
||||
export interface RouterEntry {
|
||||
@@ -71,6 +71,7 @@ export const useDeckyRouterState = () => useContext(DeckyRouterStateContext);
|
||||
|
||||
interface Props {
|
||||
deckyRouterState: DeckyRouterState;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const DeckyRouterStateContextProvider: FC<Props> = ({ children, deckyRouterState }) => {
|
||||
|
||||
@@ -1,36 +1,63 @@
|
||||
import { FC, createContext, useContext, useEffect, useState } from 'react';
|
||||
import { FC, ReactNode, createContext, useContext, useEffect, useState } from 'react';
|
||||
|
||||
import { Plugin } from '../plugin';
|
||||
import { DEFAULT_NOTIFICATION_SETTINGS, NotificationSettings } from '../notification-service';
|
||||
import { DisabledPlugin, Plugin } from '../plugin';
|
||||
import { PluginUpdateMapping } from '../store';
|
||||
import { VerInfo } from '../updater';
|
||||
|
||||
interface PublicDeckyState {
|
||||
plugins: Plugin[];
|
||||
disabledPlugins: DisabledPlugin[];
|
||||
installedPlugins: (Plugin | DisabledPlugin)[];
|
||||
pluginOrder: string[];
|
||||
frozenPlugins: string[];
|
||||
hiddenPlugins: string[];
|
||||
activePlugin: Plugin | null;
|
||||
updates: PluginUpdateMapping | null;
|
||||
hasLoaderUpdate?: boolean;
|
||||
isLoaderUpdating: boolean;
|
||||
versionInfo: VerInfo | null;
|
||||
notificationSettings: NotificationSettings;
|
||||
userInfo: UserInfo | null;
|
||||
}
|
||||
|
||||
export interface UserInfo {
|
||||
username: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export class DeckyState {
|
||||
private _plugins: Plugin[] = [];
|
||||
private _disabledPlugins: DisabledPlugin[] = [];
|
||||
private _installedPlugins: (Plugin | DisabledPlugin)[] = [];
|
||||
private _pluginOrder: string[] = [];
|
||||
private _frozenPlugins: string[] = [];
|
||||
private _hiddenPlugins: string[] = [];
|
||||
private _activePlugin: Plugin | null = null;
|
||||
private _updates: PluginUpdateMapping | null = null;
|
||||
private _hasLoaderUpdate: boolean = false;
|
||||
private _isLoaderUpdating: boolean = false;
|
||||
private _versionInfo: VerInfo | null = null;
|
||||
private _notificationSettings = DEFAULT_NOTIFICATION_SETTINGS;
|
||||
private _userInfo: UserInfo | null = null;
|
||||
|
||||
public eventBus = new EventTarget();
|
||||
|
||||
publicState(): PublicDeckyState {
|
||||
return {
|
||||
plugins: this._plugins,
|
||||
disabledPlugins: this._disabledPlugins,
|
||||
installedPlugins: this._installedPlugins,
|
||||
pluginOrder: this._pluginOrder,
|
||||
frozenPlugins: this._frozenPlugins,
|
||||
hiddenPlugins: this._hiddenPlugins,
|
||||
activePlugin: this._activePlugin,
|
||||
updates: this._updates,
|
||||
hasLoaderUpdate: this._hasLoaderUpdate,
|
||||
isLoaderUpdating: this._isLoaderUpdating,
|
||||
versionInfo: this._versionInfo,
|
||||
notificationSettings: this._notificationSettings,
|
||||
userInfo: this._userInfo,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -41,6 +68,28 @@ export class DeckyState {
|
||||
|
||||
setPlugins(plugins: Plugin[]) {
|
||||
this._plugins = plugins;
|
||||
this._installedPlugins = [...plugins, ...this._disabledPlugins];
|
||||
this.notifyUpdate();
|
||||
}
|
||||
|
||||
setDisabledPlugins(disabledPlugins: DisabledPlugin[]) {
|
||||
this._disabledPlugins = disabledPlugins;
|
||||
this._installedPlugins = [...this._plugins, ...disabledPlugins];
|
||||
this.notifyUpdate();
|
||||
}
|
||||
|
||||
setPluginOrder(pluginOrder: string[]) {
|
||||
this._pluginOrder = pluginOrder;
|
||||
this.notifyUpdate();
|
||||
}
|
||||
|
||||
setFrozenPlugins(frozenPlugins: string[]) {
|
||||
this._frozenPlugins = frozenPlugins;
|
||||
this.notifyUpdate();
|
||||
}
|
||||
|
||||
setHiddenPlugins(hiddenPlugins: string[]) {
|
||||
this._hiddenPlugins = hiddenPlugins;
|
||||
this.notifyUpdate();
|
||||
}
|
||||
|
||||
@@ -69,6 +118,16 @@ export class DeckyState {
|
||||
this.notifyUpdate();
|
||||
}
|
||||
|
||||
setNotificationSettings(notificationSettings: NotificationSettings) {
|
||||
this._notificationSettings = notificationSettings;
|
||||
this.notifyUpdate();
|
||||
}
|
||||
|
||||
setUserInfo(userInfo: UserInfo) {
|
||||
this._userInfo = userInfo;
|
||||
this.notifyUpdate();
|
||||
}
|
||||
|
||||
private notifyUpdate() {
|
||||
this.eventBus.dispatchEvent(new Event('update'));
|
||||
}
|
||||
@@ -78,15 +137,26 @@ interface DeckyStateContext extends PublicDeckyState {
|
||||
setVersionInfo(versionInfo: VerInfo): void;
|
||||
setIsLoaderUpdating(hasUpdate: boolean): void;
|
||||
setActivePlugin(name: string): void;
|
||||
setPluginOrder(pluginOrder: string[]): void;
|
||||
setDisabledPlugins(disabled: DisabledPlugin[]): void;
|
||||
closeActivePlugin(): void;
|
||||
}
|
||||
|
||||
const DeckyStateContext = createContext<DeckyStateContext>(null as any);
|
||||
const DeckyStateContext = createContext<DeckyStateContext | null>(null);
|
||||
|
||||
export const useDeckyState = () => useContext(DeckyStateContext);
|
||||
export const useDeckyState = () => {
|
||||
const deckyState = useContext(DeckyStateContext);
|
||||
|
||||
if (deckyState === null) {
|
||||
throw new Error('useDeckyState needs a parent DeckyStateContext');
|
||||
}
|
||||
|
||||
return deckyState;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
deckyState: DeckyState;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export const DeckyStateContextProvider: FC<Props> = ({ children, deckyState }) => {
|
||||
@@ -102,14 +172,24 @@ export const DeckyStateContextProvider: FC<Props> = ({ children, deckyState }) =
|
||||
return () => deckyState.eventBus.removeEventListener('update', onUpdate);
|
||||
}, []);
|
||||
|
||||
const setIsLoaderUpdating = (hasUpdate: boolean) => deckyState.setIsLoaderUpdating(hasUpdate);
|
||||
const setVersionInfo = (versionInfo: VerInfo) => deckyState.setVersionInfo(versionInfo);
|
||||
const setActivePlugin = (name: string) => deckyState.setActivePlugin(name);
|
||||
const closeActivePlugin = () => deckyState.closeActivePlugin();
|
||||
const setIsLoaderUpdating = deckyState.setIsLoaderUpdating.bind(deckyState);
|
||||
const setVersionInfo = deckyState.setVersionInfo.bind(deckyState);
|
||||
const setActivePlugin = deckyState.setActivePlugin.bind(deckyState);
|
||||
const closeActivePlugin = deckyState.closeActivePlugin.bind(deckyState);
|
||||
const setPluginOrder = deckyState.setPluginOrder.bind(deckyState);
|
||||
const setDisabledPlugins = deckyState.setDisabledPlugins.bind(deckyState);
|
||||
|
||||
return (
|
||||
<DeckyStateContext.Provider
|
||||
value={{ ...publicDeckyState, setIsLoaderUpdating, setVersionInfo, setActivePlugin, closeActivePlugin }}
|
||||
value={{
|
||||
...publicDeckyState,
|
||||
setIsLoaderUpdating,
|
||||
setVersionInfo,
|
||||
setActivePlugin,
|
||||
closeActivePlugin,
|
||||
setPluginOrder,
|
||||
setDisabledPlugins,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</DeckyStateContext.Provider>
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import { ToastData, joinClassNames } from 'decky-frontend-lib';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { ReactElement } from 'react-markdown/lib/react-markdown';
|
||||
|
||||
import { useDeckyToasterState } from './DeckyToasterState';
|
||||
import Toast, { toastClasses } from './Toast';
|
||||
|
||||
interface DeckyToasterProps {}
|
||||
|
||||
interface RenderedToast {
|
||||
component: ReactElement;
|
||||
data: ToastData;
|
||||
}
|
||||
|
||||
const DeckyToaster: FC<DeckyToasterProps> = () => {
|
||||
const { toasts, removeToast } = useDeckyToasterState();
|
||||
const [renderedToast, setRenderedToast] = useState<RenderedToast | null>(null);
|
||||
console.log(toasts);
|
||||
if (toasts.size > 0) {
|
||||
const [activeToast] = toasts;
|
||||
if (!renderedToast || activeToast != renderedToast.data) {
|
||||
// TODO play toast sound
|
||||
console.log('rendering toast', activeToast);
|
||||
setRenderedToast({ component: <Toast key={Math.random()} toast={activeToast} />, data: activeToast });
|
||||
}
|
||||
} else {
|
||||
if (renderedToast) setRenderedToast(null);
|
||||
}
|
||||
useEffect(() => {
|
||||
// not actually node but TS is shit
|
||||
let interval: NodeJS.Timer | null;
|
||||
if (renderedToast) {
|
||||
interval = setTimeout(() => {
|
||||
interval = null;
|
||||
console.log('clear toast', renderedToast.data);
|
||||
removeToast(renderedToast.data);
|
||||
}, (renderedToast.data.duration || 5e3) + 1000);
|
||||
console.log('set int', interval);
|
||||
}
|
||||
return () => {
|
||||
if (interval) {
|
||||
console.log('clearing int', interval);
|
||||
clearTimeout(interval);
|
||||
}
|
||||
};
|
||||
}, [renderedToast]);
|
||||
return (
|
||||
<div className={joinClassNames('deckyToaster', toastClasses.ToastPlaceholder)}>
|
||||
{renderedToast && renderedToast.component}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeckyToaster;
|
||||
@@ -1,69 +0,0 @@
|
||||
import { ToastData } from 'decky-frontend-lib';
|
||||
import { FC, createContext, useContext, useEffect, useState } from 'react';
|
||||
|
||||
interface PublicDeckyToasterState {
|
||||
toasts: Set<ToastData>;
|
||||
}
|
||||
|
||||
export class DeckyToasterState {
|
||||
// TODO a set would be better
|
||||
private _toasts: Set<ToastData> = new Set();
|
||||
|
||||
public eventBus = new EventTarget();
|
||||
|
||||
publicState(): PublicDeckyToasterState {
|
||||
return { toasts: this._toasts };
|
||||
}
|
||||
|
||||
addToast(toast: ToastData) {
|
||||
this._toasts.add(toast);
|
||||
this.notifyUpdate();
|
||||
}
|
||||
|
||||
removeToast(toast: ToastData) {
|
||||
this._toasts.delete(toast);
|
||||
this.notifyUpdate();
|
||||
}
|
||||
|
||||
private notifyUpdate() {
|
||||
this.eventBus.dispatchEvent(new Event('update'));
|
||||
}
|
||||
}
|
||||
|
||||
interface DeckyToasterContext extends PublicDeckyToasterState {
|
||||
addToast(toast: ToastData): void;
|
||||
removeToast(toast: ToastData): void;
|
||||
}
|
||||
|
||||
const DeckyToasterContext = createContext<DeckyToasterContext>(null as any);
|
||||
|
||||
export const useDeckyToasterState = () => useContext(DeckyToasterContext);
|
||||
|
||||
interface Props {
|
||||
deckyToasterState: DeckyToasterState;
|
||||
}
|
||||
|
||||
export const DeckyToasterStateContextProvider: FC<Props> = ({ children, deckyToasterState }) => {
|
||||
const [publicDeckyToasterState, setPublicDeckyToasterState] = useState<PublicDeckyToasterState>({
|
||||
...deckyToasterState.publicState(),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
function onUpdate() {
|
||||
setPublicDeckyToasterState({ ...deckyToasterState.publicState() });
|
||||
}
|
||||
|
||||
deckyToasterState.eventBus.addEventListener('update', onUpdate);
|
||||
|
||||
return () => deckyToasterState.eventBus.removeEventListener('update', onUpdate);
|
||||
}, []);
|
||||
|
||||
const addToast = deckyToasterState.addToast.bind(deckyToasterState);
|
||||
const removeToast = deckyToasterState.removeToast.bind(deckyToasterState);
|
||||
|
||||
return (
|
||||
<DeckyToasterContext.Provider value={{ ...publicDeckyToasterState, addToast, removeToast }}>
|
||||
{children}
|
||||
</DeckyToasterContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Navigation } from '@decky/ui';
|
||||
import { AnchorHTMLAttributes, FC } from 'react';
|
||||
|
||||
const ExternalLink: FC<AnchorHTMLAttributes<HTMLAnchorElement>> = (props) => {
|
||||
return (
|
||||
<a
|
||||
{...props}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
props.onClick ? props.onClick(e) : props.href && Navigation.NavigateToExternalWeb(props.href);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExternalLink;
|
||||
@@ -1,11 +0,0 @@
|
||||
import { VFC } from 'react';
|
||||
|
||||
interface Props {
|
||||
url: string;
|
||||
}
|
||||
|
||||
const LegacyPlugin: VFC<Props> = ({ url }) => {
|
||||
return <iframe style={{ border: 'none', width: '100%', height: '100%' }} src={url}></iframe>;
|
||||
};
|
||||
|
||||
export default LegacyPlugin;
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Focusable, Router } from 'decky-frontend-lib';
|
||||
import { Focusable, Navigation, findClass, findClassByName } from '@decky/ui';
|
||||
import { FunctionComponent, useRef } from 'react';
|
||||
import ReactMarkdown, { Options as ReactMarkdownOptions } from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
@@ -8,13 +8,16 @@ interface MarkdownProps extends ReactMarkdownOptions {
|
||||
}
|
||||
|
||||
const Markdown: FunctionComponent<MarkdownProps> = (props) => {
|
||||
const eventDetailsBodyClassName = findClassByName('EventDetailsBody') || undefined;
|
||||
const eventLinkClassName = findClass('43088', 'Link');
|
||||
|
||||
return (
|
||||
<Focusable>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
div: (nodeProps) => <Focusable {...nodeProps.node.properties}>{nodeProps.children}</Focusable>,
|
||||
a: (nodeProps) => {
|
||||
div: (nodeProps: any) => <Focusable {...nodeProps.node.properties}>{nodeProps.children}</Focusable>,
|
||||
a: (nodeProps: any) => {
|
||||
const aRef = useRef<HTMLAnchorElement>(null);
|
||||
return (
|
||||
// TODO fix focus ring
|
||||
@@ -22,11 +25,13 @@ const Markdown: FunctionComponent<MarkdownProps> = (props) => {
|
||||
onActivate={() => {}}
|
||||
onOKButton={() => {
|
||||
props.onDismiss?.();
|
||||
Router.NavigateToExternalWeb(aRef.current!.href);
|
||||
Navigation.NavigateToExternalWeb(aRef.current!.href);
|
||||
}}
|
||||
style={{ display: 'inline' }}
|
||||
focusClassName="steam-focus"
|
||||
className={eventDetailsBodyClassName}
|
||||
>
|
||||
<a ref={aRef} {...nodeProps.node.properties}>
|
||||
<a ref={aRef} {...nodeProps.node.properties} className={eventLinkClassName}>
|
||||
{nodeProps.children}
|
||||
</a>
|
||||
</Focusable>
|
||||
|
||||
@@ -1,32 +1,44 @@
|
||||
import {
|
||||
ButtonItem,
|
||||
Focusable,
|
||||
PanelSection,
|
||||
PanelSectionRow,
|
||||
joinClassNames,
|
||||
scrollClasses,
|
||||
staticClasses,
|
||||
} from 'decky-frontend-lib';
|
||||
import { VFC } from 'react';
|
||||
import { ButtonItem, ErrorBoundary, Focusable, PanelSection, PanelSectionRow } from '@decky/ui';
|
||||
import { FC, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaBan, FaEyeSlash } from 'react-icons/fa';
|
||||
|
||||
import { useDeckyState } from './DeckyState';
|
||||
import NotificationBadge from './NotificationBadge';
|
||||
import { useQuickAccessVisible } from './QuickAccessVisibleState';
|
||||
import TitleView from './TitleView';
|
||||
|
||||
const PluginView: VFC = () => {
|
||||
const { plugins, updates, activePlugin, setActivePlugin, closeActivePlugin } = useDeckyState();
|
||||
const PluginView: FC = () => {
|
||||
const {
|
||||
plugins,
|
||||
disabledPlugins,
|
||||
hiddenPlugins,
|
||||
updates,
|
||||
activePlugin,
|
||||
pluginOrder,
|
||||
setActivePlugin,
|
||||
closeActivePlugin,
|
||||
} = useDeckyState();
|
||||
const visible = useQuickAccessVisible();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const pluginList = useMemo(() => {
|
||||
console.log('updating PluginView after changes');
|
||||
|
||||
return [...plugins]
|
||||
.sort((a, b) => pluginOrder.indexOf(a.name) - pluginOrder.indexOf(b.name))
|
||||
.filter((p) => p.content)
|
||||
.filter(({ name }) => !hiddenPlugins.includes(name));
|
||||
}, [plugins, pluginOrder, hiddenPlugins]);
|
||||
|
||||
const numberOfHidden = hiddenPlugins.filter((name) => !!plugins.find((p) => p.name === name)).length;
|
||||
|
||||
if (activePlugin) {
|
||||
return (
|
||||
<Focusable onCancelButton={closeActivePlugin}>
|
||||
<TitleView />
|
||||
<div
|
||||
className={joinClassNames(staticClasses.TabGroupPanel, scrollClasses.ScrollPanel, scrollClasses.ScrollY)}
|
||||
style={{ height: '100%' }}
|
||||
>
|
||||
{(visible || activePlugin.alwaysRender) && activePlugin.content}
|
||||
<div style={{ height: '100%', paddingTop: '16px' }}>
|
||||
<ErrorBoundary>{(visible || activePlugin.alwaysRender) && activePlugin.content}</ErrorBoundary>
|
||||
</div>
|
||||
</Focusable>
|
||||
);
|
||||
@@ -34,21 +46,45 @@ const PluginView: VFC = () => {
|
||||
return (
|
||||
<>
|
||||
<TitleView />
|
||||
<div className={joinClassNames(staticClasses.TabGroupPanel, scrollClasses.ScrollPanel, scrollClasses.ScrollY)}>
|
||||
<div
|
||||
style={{
|
||||
paddingTop: '16px',
|
||||
}}
|
||||
>
|
||||
<PanelSection>
|
||||
{plugins
|
||||
.filter((p) => p.content)
|
||||
.map(({ name, icon }) => (
|
||||
<PanelSectionRow key={name}>
|
||||
<ButtonItem layout="below" onClick={() => setActivePlugin(name)}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
{icon}
|
||||
<div>{name}</div>
|
||||
<NotificationBadge show={updates?.has(name)} style={{ top: '-5px', right: '-5px' }} />
|
||||
</div>
|
||||
</ButtonItem>
|
||||
</PanelSectionRow>
|
||||
))}
|
||||
{pluginList.map(({ name, icon }) => (
|
||||
<PanelSectionRow key={name}>
|
||||
<ButtonItem layout="below" onClick={() => setActivePlugin(name)}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
{icon}
|
||||
<div>{name}</div>
|
||||
<NotificationBadge show={updates?.has(name)} style={{ top: '-5px', right: '-5px' }} />
|
||||
</div>
|
||||
</ButtonItem>
|
||||
</PanelSectionRow>
|
||||
))}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
position: 'absolute',
|
||||
justifyContent: 'center',
|
||||
padding: '5px 0px',
|
||||
}}
|
||||
>
|
||||
{numberOfHidden > 0 && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', fontSize: '0.8rem' }}>
|
||||
<FaEyeSlash />
|
||||
<div>{t('PluginView.hidden', { count: numberOfHidden })}</div>
|
||||
</div>
|
||||
)}
|
||||
{disabledPlugins.length > 0 && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', fontSize: '0.8rem' }}>
|
||||
<FaBan />
|
||||
<div>{t('PluginView.disabled', { count: disabledPlugins.length })}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PanelSection>
|
||||
</div>
|
||||
</>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user