mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-19 22:42:35 +02:00
Compare commits
913 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 226d5c1c68 | |||
| 8b36865f5e | |||
| c3be5f6dc5 | |||
| 4c0eceaa8e | |||
| 7f20250951 | |||
| 4d720b1d81 | |||
| a8921a1aba | |||
| edb9eda0a9 | |||
| 3a81676447 | |||
| 03132ff77b | |||
| 49ddecdea4 | |||
| 44ff951ec6 | |||
| 11319e0ec5 | |||
| 100e98a960 | |||
| c6100ede70 | |||
| a2986a72bd | |||
| e0e90c5f74 | |||
| 11992af81b | |||
| 15d771f7fc | |||
| 5ede474253 | |||
| 7922aa6f80 | |||
| 0c1333fa15 | |||
| 53b9ba0368 | |||
| c3a8877796 | |||
| a464ae9df5 | |||
| 0d16dd0006 | |||
| 48a96140a7 | |||
| 603ef8f295 | |||
| ab07288ba0 | |||
| c0bbe5d491 | |||
| b953ff21e7 | |||
| c14378b534 | |||
| 33d3d9a29c | |||
| 7e83793586 | |||
| 6ba9ec8bc2 | |||
| 0b02ab0e2d | |||
| ff531b5e77 | |||
| b3f9de3b83 | |||
| 86bd71b89c | |||
| 2fca7e9a01 | |||
| 2cc873ef60 | |||
| 7a66ce6bcd | |||
| 2730569b6b | |||
| ede5c4409c | |||
| 0dbe398435 | |||
| bcab3bccbc | |||
| 58c9aeb1a2 | |||
| 4702787784 | |||
| 13100dc38d | |||
| 5227041398 | |||
| 8491d4da1a | |||
| 9bea1563ca | |||
| 9e7b936663 | |||
| 19c84475db | |||
| 4164b1a3f8 | |||
| a9dc038190 | |||
| 2825db88a5 | |||
| 363099b303 | |||
| 5e25a5054f | |||
| 2bc6127f6b | |||
| 064824aedf | |||
| 52044edb2e | |||
| fb12073a82 | |||
| 9944842a2f | |||
| 99dc50894c | |||
| de39451f67 | |||
| 8f28653b28 | |||
| 6598dff6df | |||
| 389798457b | |||
| 623c47fa2e | |||
| 19861fe812 | |||
| dd1c04bea1 | |||
| e6159117f6 | |||
| 0d9e1cd3c5 | |||
| 10753eb879 | |||
| 29aec21095 | |||
| a810f82ce2 | |||
| 2c454a0ec5 | |||
| d3dca00482 | |||
| d08dffd9e2 | |||
| 5b50ac926e | |||
| 57a3be35d0 | |||
| 70f36e69e6 | |||
| 8e70f1b865 | |||
| f86fb0ee44 | |||
| fe0aac7c6e | |||
| b93447f712 | |||
| 84a5103526 | |||
| c333300906 | |||
| c94c2721d7 | |||
| 0428c1191a | |||
| 8208f92802 | |||
| 3d33c4b8e0 | |||
| d3210ec12a | |||
| c959b973fc | |||
| 40c195d4a0 | |||
| f4f1470153 | |||
| 401999b5ea | |||
| 7b53315046 | |||
| 4d170db5e0 | |||
| fa8d175101 | |||
| cbef605f22 | |||
| cf95791dcc | |||
| 919567dbdb | |||
| 8ca317a38a | |||
| ccc686ed50 | |||
| e3e7b0c345 | |||
| 5b0f359944 | |||
| 29f1bef099 | |||
| 418f34c7e8 | |||
| 21c2ab21b2 | |||
| 1ace7318f3 | |||
| 48052b88db | |||
| 715c60dc6e | |||
| 916d052688 | |||
| 993b812c3b | |||
| 43887586b5 | |||
| 03d53f21a3 | |||
| 23d7e8e5b6 | |||
| cce117c585 | |||
| 303bd1b805 | |||
| c7f4a40342 | |||
| 208c6c0776 | |||
| 7d5c8347ce | |||
| bd70131252 | |||
| 43a373eceb | |||
| 5bb3466ffe | |||
| 75e97ed008 | |||
| ee28604c11 | |||
| a7d89e1bfb | |||
| cbfd9ea559 | |||
| dae50c3bc3 | |||
| e651e59dc4 | |||
| 80d78761bf | |||
| fb85aa4f32 | |||
| 9635c95efe | |||
| 033a237488 | |||
| ec22c58822 | |||
| 274942b5ba | |||
| 94ab3da0e4 | |||
| 5d44f0f2b6 | |||
| f051e6b452 | |||
| 46a4284253 | |||
| 0a708c6892 | |||
| 0f96164dc3 | |||
| 91c4917021 | |||
| c32ebe016b | |||
| ea26eefc2d | |||
| 418f4a6075 | |||
| 0ec921709a | |||
| e0811cfd93 | |||
| f6b0778eb6 | |||
| 18aec34c0e | |||
| bd185776e7 | |||
| fca5fe38bb | |||
| 1c2c7b376d | |||
| 670df86114 | |||
| 55fb4d4562 | |||
| c703d018bd | |||
| 425a27e130 | |||
| bd1b0e875b | |||
| 1509c11f64 | |||
| 57c1097fbc | |||
| 1d1728b92b | |||
| 8202513993 | |||
| 5f9f6dbde8 | |||
| cc3639180b | |||
| 8aa4de7522 | |||
| ed1f7e7c72 | |||
| 1ecd1f5e04 | |||
| 1aa9adc899 | |||
| f8b2da93b9 | |||
| b794ff47ef | |||
| 6962a0547a | |||
| b906c1d36b | |||
| af337b1874 | |||
| 542235cca0 | |||
| f5673425b7 | |||
| 94965cf3ba | |||
| 120ded5274 | |||
| 705eb6a3fa | |||
| 1eb62b31d2 | |||
| b145187fa8 | |||
| 4da1e44fd1 | |||
| 4e70279982 | |||
| 233c8ee26e | |||
| 875adb4d79 | |||
| 456514c4d4 | |||
| dac1918b95 | |||
| 1d7429ad86 | |||
| 5d0e6615ab | |||
| dc415df8c0 | |||
| 45ce251c4c | |||
| 2bc702112f | |||
| abd73bf797 | |||
| e7e67b9572 | |||
| 1a58b693c1 | |||
| 50ecb909b4 | |||
| 5e480be8db | |||
| 48a67e51a6 | |||
| 5052bad824 | |||
| 5be92052bb | |||
| e20945692e | |||
| 191a6e2460 | |||
| c813fb4fad | |||
| bf7001b578 | |||
| 18102a2a73 | |||
| 780c1dbde1 | |||
| 879aab0d99 | |||
| 6f37bc2f5d | |||
| fc59b841d6 | |||
| c07fcdd489 | |||
| a49db10ade | |||
| 77bae98d77 | |||
| 254df7211c | |||
| f9caab48c4 | |||
| e0b5e7b808 | |||
| ac3a8da002 | |||
| 1aa45c2156 | |||
| 3cf8abd409 | |||
| db8426779c | |||
| b419e033f3 | |||
| d686fa327b | |||
| a1ce5eda43 | |||
| 1e790d1aa9 | |||
| d1d304b758 | |||
| e12b500144 | |||
| bd77651a1e | |||
| 35dc186395 | |||
| 07e78e0d12 | |||
| 5b8905c1d2 | |||
| 158a27cbae | |||
| 5769b39d78 | |||
| 5c96262c75 | |||
| 766f57dc9d | |||
| 9986078582 | |||
| e047ab5684 | |||
| a100785ad7 | |||
| 156eb4d15e | |||
| dabcfd965f | |||
| d44a71f3be | |||
| f8edd6cf3d | |||
| 2baf53c5a4 | |||
| c26e9c281f | |||
| 9f78e9b7dd | |||
| fdaf41b605 | |||
| 89526efe7a | |||
| 5e3a25c18f | |||
| cf11c4283e | |||
| 2dde04b979 | |||
| 8384f227be | |||
| 697b3bc5f5 | |||
| 9e2041521e | |||
| ee7b89ec6e | |||
| 5b143bdc76 | |||
| e3800426c9 | |||
| d9d00e452e | |||
| 14500e281c | |||
| c4623c80ff | |||
| 9e17dce9a9 | |||
| 4acc867634 | |||
| 1a061268de | |||
| daa91986ef | |||
| 63761cfc9a | |||
| 5091a5485a | |||
| d10026acd1 | |||
| f8f1cababe | |||
| ad46841397 | |||
| 20fb1e0fd0 | |||
| 9347351c37 | |||
| 0ef1f2d40f | |||
| b460f9915d | |||
| 4e195dfbc3 | |||
| 38b9fe3017 | |||
| 3c7f7bfca7 | |||
| 05230971b3 | |||
| dccdf72c73 | |||
| ca15983a72 | |||
| 4b6a2c9829 | |||
| 1755d03a6b | |||
| 869b1fc15e | |||
| ce2a2f8582 | |||
| 7b355139fb | |||
| b14518edb1 | |||
| 7d64003d1c | |||
| 0a59e04f19 | |||
| b57abb646f | |||
| dd6bde97a9 | |||
| b545545712 | |||
| c1993ffa03 | |||
| 7f7ebafa46 | |||
| b652597924 | |||
| 258fe77928 | |||
| 5a9fcd6fab | |||
| 3c05521a5b | |||
| 034b8b15ae | |||
| 7bd687331b | |||
| bdae35b1a8 | |||
| 54d58df4b6 | |||
| 9165a9f7cb | |||
| b556d1e81d | |||
| 7c25678211 | |||
| c83a9924e2 | |||
| bbeb9b83a0 | |||
| 06478f3e36 | |||
| 40f20002b2 | |||
| 442272f517 | |||
| 88dae8e9c4 | |||
| 1bbfa7d39e | |||
| edc2b3d295 | |||
| 0006da7385 | |||
| 470b7bd2e5 | |||
| 9014fb581d | |||
| b5ac8b3ec6 | |||
| 78f5169880 | |||
| 7ffa6b1bb3 | |||
| 3cd4b4503f | |||
| 3361b77aec | |||
| 8b7c9df286 | |||
| 157d5b4c36 | |||
| 44c8800bec | |||
| 2f0ba1b1f7 | |||
| 36c51f1a0c | |||
| 1dfe18aa6f | |||
| b9bbfb44c5 | |||
| 83843f192d | |||
| 8839d9f1c6 | |||
| 0630ec1d46 | |||
| 4dce8d6a80 | |||
| 3b62f999bf | |||
| d63fa521a1 | |||
| ca781dfe15 | |||
| 4bc561ceab | |||
| 3d258180bd | |||
| 65ae8610fd | |||
| c1c2000c98 | |||
| 287c2d82a1 | |||
| 5cde1650f4 | |||
| a4b90f14ab | |||
| 4826b40136 | |||
| 62618224da | |||
| 49f15e1637 | |||
| d5cab0910e | |||
| d4ccf232c1 | |||
| e36047c890 | |||
| 8f1199bd08 | |||
| d6e045ea4e | |||
| 304e48996b | |||
| f350dc83b8 | |||
| ebb7beda8c | |||
| a01f3da66e | |||
| 72f5b5fbc0 | |||
| 330aa495c8 | |||
| 0b529ae94d | |||
| 83b35183d0 | |||
| daf1d42a0f | |||
| 2cd01eb1fe | |||
| 07378f665a | |||
| bfd5f24f4c | |||
| 3d617187af | |||
| d040b93ca9 | |||
| a1d460385d | |||
| a410e2962a | |||
| f5aa8f37bb | |||
| 7e932df450 | |||
| 3d4741727e | |||
| a03b63ef74 | |||
| 15ce3e9f20 | |||
| d2ed0c65ca | |||
| da58b72f9d | |||
| 1639bd7af1 | |||
| d474121f85 | |||
| 978f76ffb6 | |||
| 084bac00f5 | |||
| 94454172dd | |||
| 891d3cf966 | |||
| 561d5ec7ab | |||
| 7ce437d50a | |||
| 4b02d4ce90 | |||
| 3107185869 | |||
| 2e3584a353 | |||
| e5b1be195c | |||
| dde30c9d76 | |||
| 3830e65de8 | |||
| c589cf167e | |||
| 2fde367c82 | |||
| 8fd188268e | |||
| b65257df42 | |||
| aaa2d7f08d | |||
| f73e25ece6 | |||
| 78d427f208 | |||
| eaeaf3538f | |||
| 85e381a85e | |||
| 1b7ee8231b | |||
| 1b8b8f5738 | |||
| 53df19b477 | |||
| ccf21b7580 | |||
| 4189d62a57 | |||
| 9a3e3af614 | |||
| f7187400dc | |||
| f55a7f0a7b | |||
| d6d35a645e | |||
| e719dcc7f5 | |||
| bc5bc5450c | |||
| f4bade0c2e | |||
| 9be59c674d | |||
| a1dec23c20 | |||
| ed926c4e37 | |||
| ab360ed6f6 | |||
| 569ba3d651 | |||
| 60fe28c2fe | |||
| 2787e29a07 | |||
| c77a4d08d6 | |||
| 9b3f90f922 | |||
| c88d457021 | |||
| b20b625820 | |||
| fd95311920 | |||
| 6da5c11731 | |||
| 4e58231308 | |||
| ef0ecf249a | |||
| 4981617f7a | |||
| 2070bc7007 | |||
| 231d2461b3 | |||
| 3b457f87c4 | |||
| de3ced4d3c | |||
| 891777e89e | |||
| 287239dd1c | |||
| 7cdded8fd7 | |||
| 8c9d045e1d | |||
| 620f5a0459 | |||
| 178d874ba0 | |||
| d44f30c8a6 | |||
| ce66937429 | |||
| 9823337375 | |||
| 11f5f0dfe1 | |||
| e1882f19e8 | |||
| 6a8b9f06c2 | |||
| 752fc8787d | |||
| 90a1cd8280 | |||
| aa570ac29d | |||
| fb7b6363f9 | |||
| 23afe7994c | |||
| 7557e6f6ba | |||
| 86b6938911 | |||
| 8f30a45fa8 | |||
| 7c9e9d5f52 | |||
| 4066ce73a8 | |||
| b5722dba1a | |||
| 81765ecafc | |||
| 84b42e9d19 | |||
| ed319a0e5f | |||
| dd55d10194 | |||
| 2084b46090 | |||
| 53443a6cf2 | |||
| 92715b5642 | |||
| 6166392515 | |||
| 49d0dead7d | |||
| 6f004830ff | |||
| e2e5e36bad | |||
| f267d264d3 | |||
| be1a77bfd7 | |||
| 41a980e826 | |||
| 09c09f3d64 | |||
| 2404399ec5 | |||
| b45d4c0557 | |||
| a41b138d3c | |||
| 1e46949dd6 | |||
| 3ed2c1ba5d | |||
| 809b99c9c9 | |||
| 4d3acdb5fb | |||
| ca9e321ef2 | |||
| da27517fcf | |||
| 192df0a3b8 | |||
| a965003a9d | |||
| 9ea26c821f | |||
| 14b699485a | |||
| 1684edc43f | |||
| 580c4418b9 | |||
| 4a65fc2358 | |||
| 71ba131fb3 | |||
| 9693b50719 | |||
| 102e2c54bb | |||
| e989590c08 | |||
| 6cee33b449 | |||
| f32498a444 | |||
| c85f71b601 | |||
| 196e55899e | |||
| ebec45076d | |||
| 561d9ae987 | |||
| 8950bd94cb | |||
| f416f197bc | |||
| 65afe5a0e6 | |||
| 4b5d347413 | |||
| 4dcc2dd0ca | |||
| 2a7a332160 | |||
| 27ee1eabda | |||
| 0034665965 | |||
| a69692be18 | |||
| dc76152166 | |||
| d7f3ae696c | |||
| 71f5449d34 | |||
| 0e64fa8d4c | |||
| 73b048d4c5 | |||
| 1c05b39861 | |||
| 7cfa6c163f | |||
| 2d4af2e867 | |||
| 1eeaffc442 | |||
| 82125b33ed | |||
| 42cbbc28fd | |||
| a7cbb0e93c | |||
| fde6148ece | |||
| df1661d75a | |||
| f938f79a35 | |||
| 333f00235b | |||
| c06475bfb3 | |||
| d1a54d0cf3 | |||
| 349437c06b | |||
| 1b03c83c84 | |||
| bb749aacf1 | |||
| 3a41b89e52 | |||
| 70cbc77381 | |||
| 3a99f5dfaa | |||
| f24435ecf4 | |||
| 4a708e316a | |||
| c2b47c998d | |||
| 534f7b3134 | |||
| d5d2692317 | |||
| dc9cc7b00f | |||
| 965e74c7e2 | |||
| 096ba54eb1 | |||
| f4e38f9e50 | |||
| c0d9409176 | |||
| 7d1f565749 | |||
| dfec4ada3b | |||
| cd695cf265 | |||
| 47ff2e0c38 | |||
| db7c09291f | |||
| 01f10c49ba | |||
| 1ff0692a72 | |||
| 116e6099d5 | |||
| 18ccaadc5b | |||
| 8f6eac7ca2 | |||
| f4610d0df5 | |||
| bf1a6b7d0a | |||
| b3fd05e62e | |||
| f7ce365618 | |||
| 77a558dbe5 | |||
| cc0c400b28 | |||
| 2bcd59cbfa | |||
| 5139acc7f1 | |||
| 1564433e02 | |||
| 1339beb7cd | |||
| cd9698ea48 | |||
| c8f8e4c5eb | |||
| 0b4ab46563 | |||
| ea1ac86134 | |||
| 790331e798 | |||
| f5d9b2ba41 | |||
| 7f26ac00b1 | |||
| fcbab10434 | |||
| c4061cc6ac | |||
| 12ac4d6b6f | |||
| 3d06e62cd4 | |||
| d7d23e1048 | |||
| 1fe9b70176 | |||
| a9cf8dd71a | |||
| 3299261db3 | |||
| e465ec8278 | |||
| d0e4a0aa1f | |||
| 74efec3235 | |||
| 13516087f2 | |||
| 0a0c16524a | |||
| 9b843a155e | |||
| cb085acbff | |||
| c3d7df166b | |||
| d312062125 | |||
| e2453192aa | |||
| 68eb0cc8f2 | |||
| cb9cecfa5d | |||
| 0f4e4a7d97 | |||
| f20a708b36 | |||
| 8c4e511883 | |||
| a4a3b8d664 | |||
| bf6530ea81 | |||
| 4a80c2aab1 | |||
| 527bbfe43f | |||
| d8e1edb60b | |||
| 245b5f74c0 | |||
| e9a1f63415 | |||
| ec370dd94b | |||
| e39d862ef3 | |||
| 7b065654aa | |||
| 918b2bbe96 | |||
| e529a3d34d | |||
| 5475778d67 | |||
| c6a3ff0a53 | |||
| cf3587f504 | |||
| d42f104884 | |||
| 6a43568369 | |||
| 85c9cd0a6e | |||
| be5920cfae | |||
| 3d25d94a77 | |||
| fe97850835 | |||
| dab9decd89 | |||
| 854651aa71 | |||
| fdd1af3287 | |||
| 0bf92b6aff | |||
| d9403bf4da | |||
| 716d8caf4d | |||
| 0f0f368a75 | |||
| ff8d7558d4 | |||
| 66f9824b68 | |||
| 44a6e5da38 | |||
| de5a4aa5f3 | |||
| e8007082a7 | |||
| 3c70c5a366 | |||
| eb6e79b055 | |||
| ea59f8dccb | |||
| aef1c584e5 | |||
| c4ce671a87 | |||
| e8a79c87ab | |||
| 249e77a5d3 | |||
| 3cf4a52a69 | |||
| eb8b02756b | |||
| 0510d34ed3 | |||
| 1c8d12e72a | |||
| 0a36a6b674 | |||
| b887c9d50f | |||
| ee4e108e4f | |||
| 5e14a0fed4 | |||
| 6045205ea9 | |||
| f2d763cdec | |||
| e5e348205a | |||
| af6d219936 | |||
| 82a07e2e09 | |||
| 12a9b99fff | |||
| 3adf761158 | |||
| 670a4c61ff | |||
| 220f50d3bb | |||
| e0bf9d2a7c | |||
| f61cf46a52 | |||
| d188128d27 | |||
| f698c4120d | |||
| 338a852d49 | |||
| a64ee2242c | |||
| e9ff5e6f0b | |||
| f3911d8b68 | |||
| 9ce0be6450 | |||
| 6ab3eff61c | |||
| 0281da1c5a | |||
| 0b4770188c | |||
| 9376bb05fa | |||
| ecca3b6793 | |||
| f41a971cd8 | |||
| 44ba66d619 | |||
| bf685a607f | |||
| 5713cf0508 | |||
| bdd50d70ca | |||
| 8188399ce6 | |||
| f72b7dbbbb | |||
| 2409afcc5c | |||
| 15c0d02c13 | |||
| a54a5081e6 | |||
| db9dfcf049 | |||
| 47f9948748 | |||
| 05e866df55 | |||
| fc431f0cb8 | |||
| 228ab359ed | |||
| 103a8587f7 | |||
| 7db0083928 | |||
| e6f6ab499a | |||
| 721b7dbba0 | |||
| a95ddab814 | |||
| 2941546ae4 | |||
| bd9b9179c1 | |||
| ce7d54c151 | |||
| 3c778c07c2 | |||
| 95207341db | |||
| 70cf24924d | |||
| a8ebba691e | |||
| ec19ea44ad | |||
| ca8dc0f0f5 | |||
| 1dc50a697c | |||
| 1167c314ee | |||
| 55781e2b34 | |||
| 7439e44e44 | |||
| cf2639df3d | |||
| 834de928c2 | |||
| 72efb21439 | |||
| aa8790ebdb | |||
| 6d491052ee | |||
| 87ff4691ce | |||
| 34d76e79ed | |||
| 31b43da96f | |||
| 0540e673e2 | |||
| 4e88a63809 | |||
| f7581f8a65 | |||
| e87a1c079c | |||
| 3f9477c246 | |||
| 05ed1e188e | |||
| f3d06e49f8 | |||
| f9a4b68967 | |||
| 3631cfe365 | |||
| da6eef905c | |||
| 8766ae176e | |||
| 36b53d490f | |||
| f9b8b812a4 | |||
| ac9eae5272 | |||
| f270cc00d8 | |||
| a5a3f970da | |||
| 987c465bf8 | |||
| cf3c766fd9 | |||
| 7efafae432 | |||
| 1b8f44dde3 | |||
| 4d93246863 | |||
| 0471886d9f | |||
| 266974b799 | |||
| c3663c67d7 | |||
| 07bb23d10b | |||
| 749fc22c6b | |||
| 9f9a4e8298 | |||
| 39e7d64d3f | |||
| 35d8610c00 | |||
| bc550ae8f5 | |||
| c76ef7f19b | |||
| b7781264d3 | |||
| 696e03941a | |||
| 4609a351dc | |||
| c275415a49 | |||
| 486ebd6bc8 | |||
| 74b9926647 | |||
| 2a6ba6d541 | |||
| 931216ab7d | |||
| 916936e179 | |||
| b535353365 | |||
| be2ae096ee | |||
| 948b85ddcb | |||
| ae904b4cd8 | |||
| aad50e7b50 | |||
| ff28a07871 | |||
| 414b6e24d2 | |||
| 9499afd815 | |||
| e7aca5cd25 | |||
| 80a6a8ac9f | |||
| c3428a695f | |||
| 1a9665b5c6 | |||
| ebb4693425 | |||
| 4f09f48ace | |||
| a0d6ff912b | |||
| a345da0feb | |||
| fc5a8d9531 | |||
| 7353edb058 | |||
| 2a7c0a5c79 | |||
| 4cf3aabe89 | |||
| ef284ba51d | |||
| 5edd389e84 | |||
| 309332ac9c | |||
| 035d19f581 | |||
| 72bb43f934 | |||
| 447ed6bf21 | |||
| db1bcfcc6b | |||
| 1ccae84933 | |||
| 152b9b23cd | |||
| a3070d8d08 | |||
| aceab7b476 | |||
| 5f1c0209a8 | |||
| 819e81b7a6 | |||
| 8193234c2f | |||
| 6263a31f41 | |||
| 481a0cda99 | |||
| b39b89e908 | |||
| ce0f98055f | |||
| 3dddf68766 | |||
| 88d687f26e | |||
| d44df42727 | |||
| 88c8dbcb7c | |||
| b4fddbe26a | |||
| ab6d7669d7 | |||
| 3f22c7f717 | |||
| f36e9588cb | |||
| 8f99f399ee | |||
| 56166a7948 | |||
| 4edd8ee1ea | |||
| a830c918ab | |||
| 53f74c4b6e | |||
| 959c192762 | |||
| 8be7b1272b | |||
| 6b57878275 | |||
| 66c7741c38 | |||
| b370af9d91 | |||
| 40b86cb5de | |||
| 84622e22aa | |||
| 092b20041e | |||
| f6cc00f471 | |||
| be2067067b | |||
| 67a7dd9698 | |||
| 6ffc067b24 | |||
| 56e6314c11 | |||
| e590bb4a19 | |||
| 35fe7f0e7a | |||
| 45d818ac81 | |||
| 7729681829 | |||
| b12d04b27d | |||
| e6608b9a5c | |||
| 2d503dfaf6 | |||
| 08934ef8de | |||
| 62d927739a | |||
| c8db8f58e8 | |||
| 0fc966a77d | |||
| 9f6c6c8cf3 | |||
| 43a6ff138c | |||
| 269a3460e7 | |||
| 18150e9e15 | |||
| 362c7f5b2c | |||
| 2adb8ad7f9 | |||
| 6b5d4e7507 | |||
| 49c82726f0 | |||
| c8ddcda384 | |||
| b75217f789 | |||
| 8ba8e535bd | |||
| e4c574db6b | |||
| fae73293d7 | |||
| 3bd0aac4f8 | |||
| 26b822e04b | |||
| 96b9b8843c | |||
| 6d9c1e17b5 | |||
| 507ad105c0 | |||
| 40a283017e | |||
| be14597670 | |||
| 837609abb9 | |||
| d64cd98b43 | |||
| 0081ff1483 | |||
| f78ca6c7ed | |||
| cfc7cbcaa4 | |||
| e533eb7778 | |||
| 7c1d0a7f88 | |||
| 01ef471708 | |||
| 2fd0a9a41d | |||
| 635749dfe4 | |||
| c4bd5626f3 | |||
| 568a0f6329 | |||
| 7ee67b5cd0 | |||
| fc94c6903c | |||
| a0af8805e7 | |||
| 9b64cde17d | |||
| f6931bcf8c | |||
| a4ff47d863 | |||
| 982d251126 | |||
| 8820a0ecc0 | |||
| b99a713ffc | |||
| dfc8c4b740 | |||
| c3df9e5259 | |||
| b9c7e0a8ca | |||
| 2c7f02a24d | |||
| 5cc8488d94 | |||
| 6f7304f59c | |||
| ea4fea4401 | |||
| 9b48664de4 | |||
| 8964dc68f0 | |||
| 4711b8055b | |||
| 84e3373fa7 | |||
| fdd7e32dd8 | |||
| e57119ebbd | |||
| ed29dd8365 | |||
| 196cacb452 | |||
| c025913fc8 | |||
| 48b2c68e72 | |||
| 689766a6ac | |||
| 9306024d17 | |||
| 195163840b | |||
| 788c54bf8f | |||
| 031aabd523 | |||
| 85db4cc4e6 | |||
| 745aad385b | |||
| ba87261f9f | |||
| 7d091382c0 | |||
| 781d0797e7 | |||
| ec12a06b88 | |||
| bf3e8867c3 | |||
| 176814a715 | |||
| 898637a616 | |||
| f1860126a7 | |||
| f8402676d7 | |||
| cf86ce1ab3 | |||
| f4cb1719e0 | |||
| 4898cb53ae | |||
| 0f60d4737e | |||
| 0dc33e1f2b | |||
| d86a997a88 | |||
| 34d4d92289 | |||
| 4cb1bf268f | |||
| 8488706ff9 | |||
| a348bb2662 | |||
| 60a17b3c67 | |||
| 386c58d4ad | |||
| 356ba01dc1 | |||
| ed2aa848da | |||
| c5dd90048f | |||
| ab04f334dc | |||
| 0d44f8a416 | |||
| d01a1545e2 | |||
| e599729ba1 | |||
| 3ac043740e | |||
| 89603d0ff3 | |||
| 05b6cd7c97 | |||
| ea5aad0631 | |||
| 96e034b9bf | |||
| 6141c36855 | |||
| 4084ab3ed0 | |||
| 34e733823a | |||
| f1d01642cd | |||
| d5551d7118 | |||
| d079a1e8e4 |
@@ -0,0 +1,2 @@
|
||||
aar/* filter=lfs diff=lfs merge=lfs -text
|
||||
app/aar/* filter=lfs diff=lfs merge=lfs -text
|
||||
@@ -0,0 +1,166 @@
|
||||
name: Bug Report
|
||||
description: Let us know about an unexpected error, a crash, or an incorrect behavior.
|
||||
labels: ["Bug", "Android"]
|
||||
title: "Bug: "
|
||||
type: bug
|
||||
projects: ["futo-org/19"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
# Thank you for taking the time to fill out this bug report.
|
||||
|
||||
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay Android Application
|
||||
|
||||
For general usage questions, please see: [The Official FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)
|
||||
|
||||
## Filing a bug report
|
||||
|
||||
To fix your issues faster, we need clear reproduction cases - ideally allowing us to make it happen locally.
|
||||
* Please include all needed context. For example, Device, OS, Application, your Grayjay Configurations and Plugin versioning info.
|
||||
* if you've found out a particular series of UI interactions can introduce buggy behavior, please label those steps 1-n with markdown
|
||||
|
||||
- type: textarea
|
||||
id: reproduction-steps
|
||||
attributes:
|
||||
label: Reproduction steps
|
||||
description: Please provide us with the steps to reproduce the issue if possible. This step makes a big difference if we are going to be able to fix it so be as precise as possible.
|
||||
placeholder: |
|
||||
0. Play a Youtube video
|
||||
1. Press on Download button
|
||||
2. Select quality 1440p
|
||||
3. Grayjay crashes when attempting to download
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: actual-result
|
||||
attributes:
|
||||
label: Actual result
|
||||
description: What happend?
|
||||
placeholder: Tell us what you saw!
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected-result
|
||||
attributes:
|
||||
label: Expected result
|
||||
description: What was suppose to happen?
|
||||
placeholder: Tell us what you expected to happen!
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: grayjay-version
|
||||
attributes:
|
||||
label: Grayjay Version
|
||||
description: In the application, select More > Settings, scroll to the bottom and locate the value next to "Version Name".
|
||||
placeholder: "311"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: plugin
|
||||
attributes:
|
||||
label: What plugins are you seeing the problem on?
|
||||
multiple: true
|
||||
options:
|
||||
- "All"
|
||||
- "Apple Podcasts"
|
||||
- "BiliBili (CN)"
|
||||
- "Bitchute"
|
||||
- "Crunchyroll"
|
||||
- "CuriosityStream"
|
||||
- "Dailymotion"
|
||||
- "Kick"
|
||||
- "Nebula"
|
||||
- "Odysee"
|
||||
- "Patreon"
|
||||
- "PeerTube"
|
||||
- "Rumble"
|
||||
- "SoundCloud"
|
||||
- "Spotify"
|
||||
- "TedTalks"
|
||||
- "Twitch"
|
||||
- "Youtube"
|
||||
- "Other"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: plugin-version
|
||||
attributes:
|
||||
label: Plugin Version
|
||||
description: In the application, select Sources > [the broken plugin], write down the value under "Version".
|
||||
placeholder: "12"
|
||||
|
||||
- type: input
|
||||
id: android-version
|
||||
attributes:
|
||||
label: Which android version are you using?
|
||||
placeholder: "Android 15"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: phone-model
|
||||
attributes:
|
||||
label: Which device are you using?
|
||||
placeholder: "Google Pixel 9"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: os-version
|
||||
attributes:
|
||||
label: Which operating system are you using?
|
||||
placeholder: "GrapheneOS/CalyxOS/Tizen/HyperOS 2/..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: login
|
||||
attributes:
|
||||
label: When do you experience the issue?
|
||||
options:
|
||||
- label: While logged in
|
||||
- label: While logged out
|
||||
- label: N/A
|
||||
|
||||
- type: dropdown
|
||||
id: vpn
|
||||
attributes:
|
||||
label: Are you using a VPN?
|
||||
multiple: false
|
||||
options:
|
||||
- "No"
|
||||
- "Yes"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: grayjay-references
|
||||
attributes:
|
||||
label: References
|
||||
description: |
|
||||
Are there any other GitHub issues, whether open or closed, that are related to the problem you've described above? If so, please create a list below that mentions each of them. For example:
|
||||
```
|
||||
- #10
|
||||
```
|
||||
placeholder:
|
||||
value:
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Relevant log output
|
||||
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
||||
render: shell
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
**Note:** If the submit button is disabled and you have filled out all required fields, please check that you did not forget a **Title** for the issue.
|
||||
@@ -0,0 +1,59 @@
|
||||
name: Feature Request
|
||||
description: Suggest a new feature or other enhancement.
|
||||
labels: ["Enhancement", "Android"]
|
||||
title: "Feature request: "
|
||||
type: feature
|
||||
projects: ["futo-org/19"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
# Thank you for opening a feature request.
|
||||
|
||||
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues and feature requests relating to the Grayjay android application
|
||||
|
||||
For discussion related to enhancements, please see: [The FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)
|
||||
|
||||
- type: textarea
|
||||
id: grayjay-use-case
|
||||
attributes:
|
||||
label: Use Cases
|
||||
description: |
|
||||
In order to properly evaluate a feature request, it is necessary to understand the use cases for it. Please describe below the _end goal_ you are trying to achieve that has led you to request this feature. Please keep this section focused on the problem and not on the suggested solution.
|
||||
placeholder:
|
||||
value:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: grayjay-proposal
|
||||
attributes:
|
||||
label: Proposal
|
||||
description: |
|
||||
If you have an idea for a way to address the problem via a change to Grayjay features, please describe it below.
|
||||
|
||||
In this section, it's helpful to include specific examples of how what you are suggesting might look in the application, this allows us to understand the full picture of what you are proposing. If you're not sure of some details, don't worry! When we evaluate the feature request we may suggest modifications as necessary to work within the design constraints of the Grayjay Core Application.
|
||||
placeholder:
|
||||
value:
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: grayjay-references
|
||||
attributes:
|
||||
label: References
|
||||
description: |
|
||||
Are there any other GitHub issues, whether open or closed, that are related to the problem you've described above or to the suggested solution? If so, please create a list below that mentions each of them. For example:
|
||||
```
|
||||
- #10
|
||||
```
|
||||
placeholder:
|
||||
value:
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
**Note:** If the submit button is disabled and you have filled out all required fields, please check that you did not forget a **Title** for the issue.
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
name: Documentation Issue
|
||||
description: Report an issue or suggest a change in the documentation.
|
||||
labels: ["Documentation"]
|
||||
title: "Documentation: "
|
||||
type: task
|
||||
projects: ["futo-org/19"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
# Thank you for opening a documentation change request.
|
||||
|
||||
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay android application. Use the `Documentation` issue type to report problems with the documentation in our code repositories, inside the application, or on [https://grayjay.app](https://grayjay.app)
|
||||
Technical writers monitor this issue type, so report Grayjay bugs or feature requests with the `Bug report` or `Feature Request` issue types instead to get engineering attention.
|
||||
|
||||
For general usage questions, please see: [The Official FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)
|
||||
|
||||
- type: textarea
|
||||
id: grayjay-affected-pages
|
||||
attributes:
|
||||
label: Affected Pages
|
||||
description: |
|
||||
Link to or describe the pages relevant to your documentation change request.
|
||||
placeholder:
|
||||
value:
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: grayjay-problem
|
||||
attributes:
|
||||
label: What is the docs issue?
|
||||
description: What problems or suggestions do you have about the documentation?
|
||||
placeholder:
|
||||
value:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: grayjay-proposal
|
||||
attributes:
|
||||
label: Proposal
|
||||
description: What documentation changes would fix this issue and where would you expect to find them? Are one or more page headings unclear? Do one or more pages need additional context, examples, or warnings? Do we need a new page or section dedicated to a specific topic? Your ideas help us understand what you and other users need from our documentation and how we can improve the content.
|
||||
placeholder:
|
||||
value:
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: grayjay-references
|
||||
attributes:
|
||||
label: References
|
||||
description: |
|
||||
Are there any other open or closed GitLab/GitHub issues related to the problem or solution you described? If so, list them below. For example:
|
||||
```
|
||||
- #6017
|
||||
```
|
||||
placeholder:
|
||||
value:
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
**Note:** If the submit button is disabled and you have filled out all required fields, please check that you did not forget a **Title** for the issue.
|
||||
@@ -0,0 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Need a Grayjay License?
|
||||
url: https://pay.futo.org/api/PaymentPortal
|
||||
about: Purchase a Grayjay license with FutoPay
|
||||
- name: Plugin Building, Usage, or other Questions
|
||||
url: https://chat.futo.org/#narrow/stream/46-Grayjay
|
||||
about: Grayjays Community Chat
|
||||
+48
-3
@@ -1,9 +1,6 @@
|
||||
[submodule "dep/polycentricandroid"]
|
||||
path = dep/polycentricandroid
|
||||
url = ../polycentricandroid.git
|
||||
[submodule "app/src/playstore/assets/sources/peertube"]
|
||||
path = app/src/playstore/assets/sources/peertube
|
||||
url = ../plugins/peertube.git
|
||||
[submodule "app/src/stable/assets/sources/kick"]
|
||||
path = app/src/stable/assets/sources/kick
|
||||
url = ../plugins/kick.git
|
||||
@@ -61,3 +58,51 @@
|
||||
[submodule "dep/futopay"]
|
||||
path = dep/futopay
|
||||
url = ../futopayclientlibraries.git
|
||||
[submodule "app/src/unstable/assets/sources/bilibili"]
|
||||
path = app/src/unstable/assets/sources/bilibili
|
||||
url = ../plugins/bilibili.git
|
||||
[submodule "app/src/stable/assets/sources/bilibili"]
|
||||
path = app/src/stable/assets/sources/bilibili
|
||||
url = ../plugins/bilibili.git
|
||||
[submodule "app/src/stable/assets/sources/spotify"]
|
||||
path = app/src/stable/assets/sources/spotify
|
||||
url = ../plugins/spotify.git
|
||||
[submodule "app/src/unstable/assets/sources/spotify"]
|
||||
path = app/src/unstable/assets/sources/spotify
|
||||
url = ../plugins/spotify.git
|
||||
[submodule "app/src/stable/assets/sources/bitchute"]
|
||||
path = app/src/stable/assets/sources/bitchute
|
||||
url = ../plugins/bitchute.git
|
||||
[submodule "app/src/unstable/assets/sources/bitchute"]
|
||||
path = app/src/unstable/assets/sources/bitchute
|
||||
url = ../plugins/bitchute.git
|
||||
[submodule "app/src/unstable/assets/sources/dailymotion"]
|
||||
path = app/src/unstable/assets/sources/dailymotion
|
||||
url = ../plugins/dailymotion.git
|
||||
[submodule "app/src/stable/assets/sources/dailymotion"]
|
||||
path = app/src/stable/assets/sources/dailymotion
|
||||
url = ../plugins/dailymotion.git
|
||||
[submodule "app/src/stable/assets/sources/apple-podcast"]
|
||||
path = app/src/stable/assets/sources/apple-podcasts
|
||||
url = ../plugins/apple-podcasts.git
|
||||
[submodule "app/src/unstable/assets/sources/apple-podcasts"]
|
||||
path = app/src/unstable/assets/sources/apple-podcasts
|
||||
url = ../plugins/apple-podcasts.git
|
||||
[submodule "app/src/stable/assets/sources/tedtalks"]
|
||||
path = app/src/stable/assets/sources/tedtalks
|
||||
url = ../plugins/tedtalks.git
|
||||
[submodule "app/src/unstable/assets/sources/tedtalks"]
|
||||
path = app/src/unstable/assets/sources/tedtalks
|
||||
url = ../plugins/tedtalks.git
|
||||
[submodule "app/src/stable/assets/sources/curiositystream"]
|
||||
path = app/src/stable/assets/sources/curiositystream
|
||||
url = ../plugins/curiositystream.git
|
||||
[submodule "app/src/unstable/assets/sources/curiositystream"]
|
||||
path = app/src/unstable/assets/sources/curiositystream
|
||||
url = ../plugins/curiositystream.git
|
||||
[submodule "app/src/unstable/assets/sources/crunchyroll"]
|
||||
path = app/src/unstable/assets/sources/crunchyroll
|
||||
url = ../plugins/crunchyroll.git
|
||||
[submodule "app/src/stable/assets/sources/crunchyroll"]
|
||||
path = app/src/stable/assets/sources/crunchyroll
|
||||
url = ../plugins/crunchyroll.git
|
||||
|
||||
+16
-2
@@ -49,9 +49,23 @@ We encourage developers to write their own plugins. Please refer to the "Getting
|
||||
|
||||
## Contributing to Core
|
||||
|
||||
**We are currently not accepting contributions to the core.**
|
||||
|
||||
The core is currently licensed under the FUTO Temporary License (FTL). The licensing and ownership of contributions to the core are complex topics that we are still working on. We'll update these guidelines when we have more clarity.
|
||||
### License
|
||||
|
||||
The core is currently licensed under the [Source First License 1.1](./LICENSE.md). All contributors have to sign FUTO Individual Contributor License Agreement before contributions can be accepted. You can read more about it at [https://cla.futo.org/](https://cla.futo.org/).
|
||||
|
||||
### How to Contribute
|
||||
|
||||
1. Fork the core repository.
|
||||
2. Clone your fork.
|
||||
3. Make your changes.
|
||||
4. Commit and push your changes.
|
||||
5. Open a pull request.
|
||||
|
||||
### Guidelines
|
||||
|
||||
- Ensure your code adheres to the existing style.
|
||||
- Include documentation and unit tests (where applicable).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
# FUTO TEMPORARY LICENSE
|
||||
This license grants you the rights, and only the rights, set out below in respect of the source code provided. If you take advantage of these rights, you accept this license. If you do not accept the license, do not access the code.
|
||||
|
||||
Words used in the Terms of Service have the same meaning in this license. Where there is any inconsistency between this license and those Terms of Service, these terms prevail.
|
||||
|
||||
## Section 1: Definitions
|
||||
- "code" means the source code made available from time, in our sole discretion, for access under this license. Reference to code in this license means the code and any part of it and any derivative of it.
|
||||
- “compilation” means to compile the code from ‘source code’ to ‘machine code’.
|
||||
- "defect" means a defect, bug, backdoor, security issue or other deficiency in the code.
|
||||
- “non-commercial distribution” means distribution of the code or any compilation of the code, or of any other application or program containing the code or any compilation of the code, where such distribution is not intended for or directed towards commercial advantage or monetary compensation.
|
||||
- "review" means to access, analyse, test and otherwise review the code as a reference, for the sole purpose of analysing it for defects.
|
||||
- "you" means the licensee of rights set out in this license.
|
||||
|
||||
## Section 2: Grant of Rights
|
||||
1. Subject to the terms of this license, we grant you a non-transferable, non-exclusive, worldwide, royalty-free license to access and use the code solely for the purposes of review, compilation and non-commercial distribution.
|
||||
2. You may provide the code to anyone else and publish excerpts of it for the purposes of review, compilation and non-commercial distribution, provided that when you do so you make any recipient of the code aware of the terms of this license, they must agree to be bound by the terms of this license and you must attribute the code to the provider.
|
||||
3. Other than in respect of those parts of the code that were developed by other parties and as specified strictly in accordance with the open source and other licenses under which those parts of the code have been made available, as set out on our website or in those items of code, you are not entitled to use or do anything with the code for any commercial or other purpose, other than review, compilation and non-commercial distribution in accordance with the terms of this license.
|
||||
4. Subject to the terms of this license, you must at all times comply with and shall be bound by our Terms of Use, Privacy and Data Policy.
|
||||
|
||||
## Section 3: Limitations
|
||||
1. This license does not grant you any rights to use the provider's name, logo, or trademarks and you must not in any way indicate you are authorised to speak on behalf of the provider.
|
||||
2. If you issue proceedings in any jurisdiction against the provider because you consider the provider has infringed copyright or any patent right in respect of the code (including any joinder or counterclaim), your license to the code is automatically terminated.
|
||||
3. THE CODE IS MADE AVAILABLE "AS-IS" AND WITHOUT ANY EXPRESS OR IMPLIED GUARANTEES AS TO FITNESS, MERCHANTABILITY, NON-INFRINGEMENT OR OTHERWISE. IT IS NOT BEING PROVIDED IN TRADE BUT ON A VOLUNTARY BASIS ON OUR PART AND IS NOT MADE AVAILABLE FOR ANY USE OUTSIDE THE TERMS OF THIS LICENSE. ANYONE ACCESSING THE CODE MUST ENSURE THEY HAVE THE REQUISITE EXPERTISE TO SECURE THEIR OWN SYSTEM AND DEVICES AND TO ACCESS AND USE THE CODE IN ACCORDANCE WITH THE TERMS OF THIS LICENSE. YOU BEAR THE RISK OF ACCESSING AND USING THE CODE. IN PARTICULAR, THE PROVIDER BEARS NO LIABILITY FOR ANY INTERFERENCE WITH OR ADVERSE EFFECT ON YOUR SYSTEM OR DEVICES AS A RESULT OF YOUR ACCESSING AND USING THE CODE IN ACCORDANCE WITH THE TERMS OF THIS LICENSE OR OTHERWISE.
|
||||
|
||||
## Section 4: Termination, suspension and variation
|
||||
1. We may suspend, terminate or vary the terms of this license and any access to the code at any time, without notice, for any reason or no reason, in respect of any licensee, group of licensees or all licensees including as may be applicable any sub-licensees.
|
||||
|
||||
## Section 5: General
|
||||
1. This license and its interpretation and operation are governed solely by the local law. You agree to submit to the exclusive jurisdiction of the local arbitral tribunals as further described in our Terms of Service and you agree not to raise any jurisdictional issue if we need to enforce an arbitral award or judgment in our jurisdiction or another country.
|
||||
2. Questions and comments regarding this license are welcomed and should be addressed at https://chat.futo.org/login/.
|
||||
|
||||
Last updated 7 June 2023.
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
# Source First License 1.1
|
||||
|
||||
## Acceptance
|
||||
By using the software, you agree to all of the terms and conditions below.
|
||||
|
||||
## Copyright License
|
||||
FUTO Holdings, Inc. (the “Licensor”) grants you a non-exclusive, royalty-free, worldwide, non-sublicensable, non-transferable license to use, copy, distribute, make available, and prepare derivative works of the software, in each case subject to the limitations below.
|
||||
|
||||
## Limitations
|
||||
You may use or modify the software only for non-commercial purposes such as personal use for research, experiment, and testing for the benefit of public knowledge, personal study, private entertainment, hobby projects, amateur pursuits, or religious observance, all without any anticipated commercial application.
|
||||
|
||||
You may distribute the software or provide it to others only if you do so free of charge for non-commercial purposes.
|
||||
|
||||
Notwithstanding the above, you may not remove or obscure any functionality in the software related to payment to the Licensor in any copy you distribute to others.
|
||||
|
||||
You may not alter, remove, or obscure any licensing, copyright, or other notices of the Licensor in the software. Any use of the Licensor’s trademarks is subject to applicable law.
|
||||
|
||||
## Patents
|
||||
If you make any written claim that the software infringes or contributes to infringement of any patent, your license for the software granted under these terms ends immediately. If your company makes such a claim, your license ends immediately for work on behalf of your company.
|
||||
|
||||
## Notices
|
||||
You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms. If you modify the software, you must include in any modified copies of the software a prominent notice stating that you have modified the software, such as but not limited to, a statement in a readme file or an in-application about section.
|
||||
|
||||
## Fair Use
|
||||
You may have "fair use" rights for the software under the law. These terms do not limit them.
|
||||
|
||||
## No Other Rights
|
||||
These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the Licensor from granting licenses to anyone else. These terms do not imply any other licenses.
|
||||
|
||||
## Termination
|
||||
If you use the software in violation of these terms, such use is not licensed, and your license will automatically terminate. If the licensor provides you with a notice of your violation, and you cease all violation of this license no later than 30 days after you receive that notice, your license will be reinstated retroactively. However, if you violate these terms after such reinstatement, any additional violation of these terms will cause your license to terminate automatically and permanently.
|
||||
|
||||
## No Liability
|
||||
As far as the law allows, the software comes as is, without any warranty or condition, and the Licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.
|
||||
|
||||
## Definitions
|
||||
- The “Licensor” is the entity offering these terms, FUTO Holdings, Inc.
|
||||
- The “software” is the software the licensor makes available under these terms, including any portion of it.
|
||||
- “You” refers to the individual or entity agreeing to these terms.
|
||||
- “Your company” is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. Control means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
|
||||
- “Your license” is the license granted to you for the software under these terms.
|
||||
- “Use” means anything you do with the software requiring your license.
|
||||
- “Trademark” means trademarks, service marks, and similar rights.
|
||||
@@ -9,8 +9,8 @@ technologies that frustrate centralization and industry consolidation.
|
||||
|
||||
<table border="0">
|
||||
<tr>
|
||||
<td><b style="font-size:30px"><img src="images/video.jpg" height="700" /></b></td>
|
||||
<td><b style="font-size:30px"><img src="images/video-details.jpg" height="700" /></b></td>
|
||||
<td><b style="font-size:30px"><img src="images/video.png" height="700" /></b></td>
|
||||
<td><b style="font-size:30px"><img src="images/video-details.png" height="700" /></b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Video</td>
|
||||
@@ -24,12 +24,10 @@ The FUTO media app is a player that exposes multiple video websites as sources i
|
||||
|
||||
<table border="0">
|
||||
<tr>
|
||||
<td><b style="font-size:30px"><img src="images/sources.jpg" height="700" /></b></td>
|
||||
<td><b style="font-size:30px"><img src="images/sources-disabled.jpg" height="700" /></b></td>
|
||||
<td><b style="font-size:30px"><img src="images/source.png" height="700" /></b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Sources (all enabled)</td>
|
||||
<td>Sources (one disabled)</td>
|
||||
<td>Sources</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -38,7 +36,7 @@ Additional sources can also be installed. These sources are JavaScript sources,
|
||||
<table border="0">
|
||||
<tr>
|
||||
<td><b style="font-size:30px"><img src="images/source-install.png" height="700" /></b></td>
|
||||
<td><b style="font-size:30px"><img src="images/source-settings.jpg" height="700" /></b></td>
|
||||
<td><b style="font-size:30px"><img src="images/source-settings.png" height="700" /></b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Install a new source</td>
|
||||
@@ -54,8 +52,8 @@ When a user enters a search term into the search bar, the query is posted to th
|
||||
|
||||
<table border="0">
|
||||
<tr>
|
||||
<td><b style="font-size:30px"><img src="images/search-list.jpg" height="700" /></b></td>
|
||||
<td><b style="font-size:30px"><img src="images/search-preview.jpg" height="700" /></b></td>
|
||||
<td><b style="font-size:30px"><img src="images/search-list.png" height="700" /></b></td>
|
||||
<td><b style="font-size:30px"><img src="images/search-preview.png" height="700" /></b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Search (list)</td>
|
||||
@@ -71,7 +69,7 @@ Creators are able to configure their profile using NeoPass.
|
||||
|
||||
<table border="0">
|
||||
<tr>
|
||||
<td><b style="font-size:30px"><img src="images/channel.jpg" height="700" /></b></td>
|
||||
<td><b style="font-size:30px"><img src="images/channel.png" height="700" /></b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Channel</td>
|
||||
@@ -112,7 +110,7 @@ The app offers a lot of settings customizing how the app looks and feels. An exa
|
||||
|
||||
<table border="0">
|
||||
<tr>
|
||||
<td><b style="font-size:30px"><img src="images/settings.jpg" height="700" /></b></td>
|
||||
<td><b style="font-size:30px"><img src="images/settings.png" height="700" /></b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Settings</td>
|
||||
@@ -125,8 +123,8 @@ Playlists allow you to make a collection of videos that you can create and custo
|
||||
|
||||
<table border="0">
|
||||
<tr>
|
||||
<td><b style="font-size:30px"><img src="images/playlists.jpg" height="700" /></b></td>
|
||||
<td><b style="font-size:30px"><img src="images/playlist.jpg" height="700" /></b></td>
|
||||
<td><b style="font-size:30px"><img src="images/playlists.png" height="700" /></b></td>
|
||||
<td><b style="font-size:30px"><img src="images/playlist.png" height="700" /></b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Playlists</td>
|
||||
@@ -142,7 +140,7 @@ Both individual videos and playlists can be downloaded for local, offline playba
|
||||
|
||||
<table border="0">
|
||||
<tr>
|
||||
<td><b style="font-size:30px"><img src="images/downloads.jpg" height="700" /></b></td>
|
||||
<td><b style="font-size:30px"><img src="images/downloads.png" height="700" /></b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Downloads</td>
|
||||
@@ -157,7 +155,7 @@ For more information about casting please click [here](./docs/casting.md).
|
||||
|
||||
<table border="0">
|
||||
<tr>
|
||||
<td><b style="font-size:30px"><img src="images/casting.jpg" height="700" /></b></td>
|
||||
<td><b style="font-size:30px"><img src="images/casting.png" height="700" /></b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Casting</td>
|
||||
@@ -182,6 +180,12 @@ In the future we hope to offer users the choice of their desired recommendation
|
||||
|
||||
1. Download a copy of the repository.
|
||||
2. Open the project in Android Studio: Once the repository is cloned, you can open it in Android Studio by selecting "Open an Existing Project" from the welcome screen and navigating to the directory where you cloned the repository.
|
||||
3. Open the terminal in Android Studio by clicking on the terminal icon on bottom left and run the following command:
|
||||
|
||||
```sh
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
3. Build the project: With the project open in Android Studio, you can build it by selecting "Build > Make Project" from the main menu. This will compile the code and generate an APK file that you can install on your device or emulator.
|
||||
4. Run the project: To run the project, select "Run > Run 'app'" from the main menu. This will launch the app on your device or emulator, allowing you to test it and make any necessary changes.
|
||||
|
||||
@@ -199,7 +203,6 @@ Create a tag on the master branch, incrementing the last version number by 1 (fo
|
||||
|
||||
Click on the CI/CD tab, you should now see the tests and build are in progress. If the build succeeds the last step will become available. The last step is a manual action which can be triggered by clicking the run button on the action. This action will deploy the build to all users using the app through auto-update.
|
||||
|
||||
|
||||
## Documentation
|
||||
|
||||
The documentation can be found [here](https://gitlab.futo.org/videostreaming/documents/-/wikis/API-Overview).
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ea10d3c5562c9f449a4e89e9c3dfcf881ed79a952f3409bc005bcc62c2cf4b81
|
||||
size 65512557
|
||||
+31
-15
@@ -2,7 +2,7 @@ plugins {
|
||||
id 'com.android.application'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.21'
|
||||
id 'org.ajoberstar.grgit' version '1.7.2'
|
||||
id 'org.ajoberstar.grgit' version '5.2.2'
|
||||
id 'com.google.protobuf'
|
||||
id 'kotlin-parcelize'
|
||||
id 'com.google.devtools.ksp'
|
||||
@@ -144,14 +144,24 @@ android {
|
||||
buildFeatures {
|
||||
buildConfig true
|
||||
}
|
||||
sourceSets {
|
||||
main {
|
||||
assets {
|
||||
srcDirs 'src/main/assets', 'src/tests/assets', 'src/test/assets'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.google.dagger:dagger:2.48'
|
||||
implementation 'androidx.test:monitor:1.7.2'
|
||||
annotationProcessor 'com.google.dagger:dagger-compiler:2.48'
|
||||
|
||||
//Core
|
||||
implementation 'androidx.core:core-ktx:1.12.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'com.google.android.material:material:1.10.0'
|
||||
implementation 'com.google.android.material:material:1.11.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
|
||||
//Images
|
||||
@@ -163,37 +173,41 @@ dependencies {
|
||||
|
||||
//HTTP
|
||||
implementation "com.squareup.okhttp3:okhttp:4.11.0"
|
||||
implementation "com.squareup.okhttp3:okhttp-urlconnection:4.11.0"
|
||||
|
||||
//JSON
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2" //Used for structured json
|
||||
implementation 'com.google.code.gson:gson:2.10.1' //Used for complex/anonymous cases like during development conversions (eg. V8RemoteObject)
|
||||
|
||||
//JS
|
||||
implementation("com.caoccao.javet:javet-android:2.2.1")
|
||||
implementation("com.caoccao.javet:javet-android:3.0.2")
|
||||
//implementation 'com.caoccao.javet:javet-v8-android:4.1.4' //Change after extensive testing the freezing edge cases are solved.
|
||||
|
||||
//Exoplayer
|
||||
implementation 'androidx.media3:media3-exoplayer:1.2.0'
|
||||
implementation 'androidx.media3:media3-exoplayer-dash:1.2.0'
|
||||
implementation 'androidx.media3:media3-ui:1.2.0'
|
||||
implementation 'androidx.media3:media3-exoplayer-hls:1.2.0'
|
||||
implementation 'androidx.media3:media3-exoplayer-rtsp:1.2.0'
|
||||
implementation 'androidx.media3:media3-exoplayer-smoothstreaming:1.2.0'
|
||||
implementation 'androidx.media3:media3-transformer:1.2.0'
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.5'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.7.5'
|
||||
implementation 'androidx.media3:media3-exoplayer:1.2.1'
|
||||
implementation 'androidx.media3:media3-exoplayer-dash:1.2.1'
|
||||
implementation 'androidx.media3:media3-ui:1.2.1'
|
||||
implementation 'androidx.media3:media3-exoplayer-hls:1.2.1'
|
||||
implementation 'androidx.media3:media3-exoplayer-rtsp:1.2.1'
|
||||
implementation 'androidx.media3:media3-exoplayer-smoothstreaming:1.2.1'
|
||||
implementation 'androidx.media3:media3-transformer:1.2.1'
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.6'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.7.6'
|
||||
implementation 'androidx.media:media:1.7.0'
|
||||
|
||||
//Other
|
||||
implementation 'org.jmdns:jmdns:3.5.1'
|
||||
implementation 'org.jsoup:jsoup:1.15.3'
|
||||
implementation 'com.google.android.flexbox:flexbox:3.0.0'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||
implementation 'com.arthenica:ffmpeg-kit-full:5.1'
|
||||
implementation fileTree(dir: 'aar', include: ['*.aar'])
|
||||
implementation 'com.arthenica:smart-exception-java:0.2.1'
|
||||
implementation 'org.jetbrains.kotlin:kotlin-reflect:1.9.0'
|
||||
implementation 'com.github.dhaval2404:imagepicker:2.1'
|
||||
implementation 'com.google.zxing:core:3.4.1'
|
||||
implementation 'com.journeyapps:zxing-android-embedded:4.3.0'
|
||||
implementation 'com.caverock:androidsvg-aar:1.4'
|
||||
implementation 'org.bouncycastle:bcprov-jdk15on:1.70'
|
||||
implementation "com.googlecode.plist:dd-plist:1.23"
|
||||
|
||||
//Protobuf
|
||||
implementation 'com.google.protobuf:protobuf-javalite:3.25.1'
|
||||
@@ -210,7 +224,9 @@ dependencies {
|
||||
implementation("androidx.room:room-ktx:2.6.1")
|
||||
|
||||
//Payment
|
||||
implementation 'com.stripe:stripe-android:20.35.1'
|
||||
implementation('com.stripe:stripe-android:20.35.1') {
|
||||
exclude group: 'org.bouncycastle', module: 'bcprov-jdk15to18'
|
||||
}
|
||||
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
package com.futo.platformplayer
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.futo.platformplayer.casting.SRPClient
|
||||
import com.futo.platformplayer.casting.TLV8Item
|
||||
import com.futo.platformplayer.casting.TLV8Tag
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.runner.RunWith
|
||||
import java.math.BigInteger
|
||||
import org.junit.Test
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@OptIn(ExperimentalStdlibApi::class, ExperimentalUnsignedTypes::class)
|
||||
class AirPlay2Test {
|
||||
@Test
|
||||
fun testSRP() {
|
||||
val N = BigInteger(1, ("FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 29024E08 8A67CC74" +
|
||||
"020BBEA6 3B139B22 514A0879 8E3404DD EF9519B3 CD3A431B 302B0A6D F25F1437" +
|
||||
"4FE1356D 6D51C245 E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED" +
|
||||
"EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D C2007CB8 A163BF05" +
|
||||
"98DA4836 1C55D39A 69163FA8 FD24CF5F 83655D23 DCA3AD96 1C62F356 208552BB" +
|
||||
"9ED52907 7096966D 670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B" +
|
||||
"E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 DE2BCBF6 95581718" +
|
||||
"3995497C EA956AE5 15D22618 98FA0510 15728E5A 8AAAC42D AD33170D 04507A33" +
|
||||
"A85521AB DF1CBA64 ECFB8504 58DBEF0A 8AEA7157 5D060C7D B3970F85 A6E1E4C7" +
|
||||
"ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226 1AD2EE6B F12FFA06 D98A0864" +
|
||||
"D8760273 3EC86A64 521F2B18 177B200C BBE11757 7A615D6C 770988C0 BAD946E2" +
|
||||
"08E24FA0 74E5AB31 43DB5BFC E0FD108E 4B82D120 A93AD2CA FFFFFFFF FFFFFFFF").replace(" ", "").hexToByteArray())
|
||||
|
||||
val g = BigInteger(1, "05".hexToByteArray())
|
||||
val I = "alice"
|
||||
val p = "password123"
|
||||
val a = BigInteger(1, "60975527 035CF2AD 1989806F 0407210B C81EDC04 E2762A56 AFD529DD DA2D4393".replace(" ", "").hexToByteArray())
|
||||
val A = BigInteger(1, ("FAB6F5D2 615D1E32 3512E799 1CC37443 F487DA60 4CA8C923 0FCB04E5 41DCE628" +
|
||||
"0B27CA46 80B0374F 179DC3BD C7553FE6 2459798C 701AD864 A91390A2 8C93B644" +
|
||||
"ADBF9C00 745B942B 79F9012A 21B9B787 82319D83 A1F83628 66FBD6F4 6BFC0DDB" +
|
||||
"2E1AB6E4 B45A9906 B82E37F0 5D6F97F6 A3EB6E18 2079759C 4F684783 7B62321A" +
|
||||
"C1B4FA68 641FCB4B B98DD697 A0C73641 385F4BAB 25B79358 4CC39FC8 D48D4BD8" +
|
||||
"67A9A3C1 0F8EA121 70268E34 FE3BBE6F F89998D6 0DA2F3E4 283CBEC1 393D52AF" +
|
||||
"724A5723 0C604E9F BCE583D7 613E6BFF D67596AD 121A8707 EEC46944 95703368" +
|
||||
"6A155F64 4D5C5863 B48F61BD BF19A53E AB6DAD0A 186B8C15 2E5F5D8C AD4B0EF8" +
|
||||
"AA4EA500 8834C3CD 342E5E0F 167AD045 92CD8BD2 79639398 EF9E114D FAAAB919" +
|
||||
"E14E8509 89224DDD 98576D79 385D2210 902E9F9B 1F2D86CF A47EE244 635465F7" +
|
||||
"1058421A 0184BE51 DD10CC9D 079E6F16 04E7AA9B 7CF7883C 7D4CE12B 06EBE160" +
|
||||
"81E23F27 A231D184 32D7D1BB 55C28AE2 1FFCF005 F57528D1 5A88881B B3BBB7FE").replace(" ", "").hexToByteArray())
|
||||
val b = BigInteger(1, "E487CB59 D31AC550 471E81F0 0F6928E0 1DDA08E9 74A004F4 9E61F5D1 05284D20".replace(" ", "").hexToByteArray())
|
||||
val B = ("40F57088 A482D4C7 733384FE 0D301FDD CA9080AD 7D4F6FDF 09A01006 C3CB6D56" +
|
||||
"2E41639A E8FA21DE 3B5DBA75 85B27558 9BDB2798 63C56280 7B2B9908 3CD1429C" +
|
||||
"DBE89E25 BFBD7E3C AD3173B2 E3C5A0B1 74DA6D53 91E6A06E 465F037A 40062548" +
|
||||
"39A56BF7 6DA84B1C 94E0AE20 8576156F E5C140A4 BA4FFC9E 38C3B07B 88845FC6" +
|
||||
"F7DDDA93 381FE0CA 6084C4CD 2D336E54 51C464CC B6EC65E7 D16E548A 273E8262" +
|
||||
"84AF2559 B6264274 215960FF F47BDD63 D3AFF064 D6137AF7 69661C9D 4FEE4738" +
|
||||
"2603C88E AA098058 1D077584 61B777E4 356DDA58 35198B51 FEEA308D 70F75450" +
|
||||
"B71675C0 8C7D8302 FD7539DD 1FF2A11C B4258AA7 0D234436 AA42B6A0 615F3F91" +
|
||||
"5D55CC3B 966B2716 B36E4D1A 06CE5E5D 2EA3BEE5 A1270E87 51DA45B6 0B997B0F" +
|
||||
"FDB0F996 2FEE4F03 BEE780BA 0A845B1D 92714217 83AE6601 A61EA2E3 42E4F2E8" +
|
||||
"BC935A40 9EAD19F2 21BD1B74 E2964DD1 9FC845F6 0EFC0933 8B60B6B2 56D8CAC8" +
|
||||
"89CCA306 CC370A0B 18C8B886 E95DA0AF 5235FEF4 393020D2 B7F30569 04759042").replace(" ", "").hexToByteArray()
|
||||
val s = "BEB25379 D1A8581E B5A72767 3A2441EE".replace(" ", "").hexToByteArray()
|
||||
val v = BigInteger(1, ("9B5E0617 01EA7AEB 39CF6E35 19655A85 3CF94C75 CAF2555E F1FAF759 BB79CB47" +
|
||||
"7014E04A 88D68FFC 05323891 D4C205B8 DE81C2F2 03D8FAD1 B24D2C10 9737F1BE" +
|
||||
"BBD71F91 2447C4A0 3C26B9FA D8EDB3E7 80778E30 2529ED1E E138CCFC 36D4BA31" +
|
||||
"3CC48B14 EA8C22A0 186B222E 655F2DF5 603FD75D F76B3B08 FF895006 9ADD03A7" +
|
||||
"54EE4AE8 8587CCE1 BFDE3679 4DBAE459 2B7B904F 442B041C B17AEBAD 1E3AEBE3" +
|
||||
"CBE99DE6 5F4BB1FA 00B0E7AF 06863DB5 3B02254E C66E781E 3B62A821 2C86BEB0" +
|
||||
"D50B5BA6 D0B478D8 C4E9BBCE C2176532 6FBD1405 8D2BBDE2 C33045F0 3873E539" +
|
||||
"48D78B79 4F0790E4 8C36AED6 E880F557 427B2FC0 6DB5E1E2 E1D7E661 AC482D18" +
|
||||
"E528D729 5EF74372 95FF1A72 D4027717 13F16876 DD050AE5 B7AD53CC B90855C9" +
|
||||
"39566483 58ADFD96 6422F524 98732D68 D1D7FBEF 10D78034 AB8DCB6F 0FCF885C" +
|
||||
"C2B2EA2C 3E6AC866 09EA058A 9DA8CC63 531DC915 414DF568 B09482DD AC1954DE" +
|
||||
"C7EB714F 6FF7D44C D5B86F6B D1158109 30637C01 D0F6013B C9740FA2 C633BA89").replace(" ", "").hexToByteArray())
|
||||
val u = BigInteger(1, ("03AE5F3C 3FA9EFF1 A50D7DBB 8D2F60A1 EA66EA71 2D50AE97 6EE34641 A1CD0E51" +
|
||||
"C4683DA3 83E8595D 6CB56A15 D5FBC754 3E07FBDD D316217E 01A391A1 8EF06DFF").replace(" ", "").hexToByteArray())
|
||||
val S = ("F1036FEC D017C823 9C0D5AF7 E0FCF0D4 08B009E3 6411618A 60B23AAB BFC38339" +
|
||||
"72682312 14BAACDC 94CA1C53 F442FB51 C1B027C3 18AE238E 16414D60 D1881B66" +
|
||||
"486ADE10 ED02BA33 D098F6CE 9BCF1BB0 C46CA2C4 7F2F174C 59A9C61E 2560899B" +
|
||||
"83EF6113 1E6FB30B 714F4E43 B735C9FE 6080477C 1B83E409 3E4D456B 9BCA492C" +
|
||||
"F9339D45 BC42E67C E6C02C24 3E49F5DA 42A869EC 855780E8 4207B8A1 EA6501C4" +
|
||||
"78AAC0DF D3D22614 F531A00D 826B7954 AE8B14A9 85A42931 5E6DD366 4CF47181" +
|
||||
"496A9432 9CDE8005 CAE63C2F 9CA4969B FE840019 24037C44 6559BDBB 9DB9D4DD" +
|
||||
"142FBCD7 5EEF2E16 2C843065 D99E8F05 762C4DB7 ABD9DB20 3D41AC85 A58C05BD" +
|
||||
"4E2DBF82 2A934523 D54E0653 D376CE8B 56DCB452 7DDDC1B9 94DC7509 463A7468" +
|
||||
"D7F02B1B EB168571 4CE1DD1E 71808A13 7F788847 B7C6B7BF A1364474 B3B7E894" +
|
||||
"78954F6A 8E68D45B 85A88E4E BFEC1336 8EC0891C 3BC86CF5 00978801 78D86135" +
|
||||
"E7287234 58538858 D715B7B2 47406222 C1019F53 603F0169 52D49710 0858824C").replace(" ", "").hexToByteArray()
|
||||
val K = ("5CBC219D B052138E E1148C71 CD449896 3D682549 CE91CA24 F098468F 06015BEB" +
|
||||
"6AF245C2 093F98C3 651BCA83 AB8CAB2B 580BBF02 184FEFDF 26142F73 DF95AC50").replace(" ", "").hexToByteArray()
|
||||
|
||||
|
||||
val srp = SRPClient(N, g, I, p)
|
||||
val A_computed = srp.srp_user_start_authentication(a)
|
||||
assert(A_computed == A) { "Mismatch in A value" }
|
||||
|
||||
val triple = srp.srp_user_process_challenge_internal(s, B)
|
||||
val u_computed = triple.first
|
||||
val v_computed = triple.second
|
||||
val M_computed = triple.third
|
||||
val S_computed = srp.getS()!!
|
||||
assert(u_computed == u) { "Mismatch in u" }
|
||||
assert(v_computed == v) { "Mismatch in v" }
|
||||
//assert(M_computed.contentEquals(M)) { "Mismatch in M" }
|
||||
assert(S_computed.contentEquals(S)) { "Mismatch in session key S" }
|
||||
|
||||
val K_computed = srp.getSessionKey()!!
|
||||
assert(K_computed.contentEquals(K)) { "Mismatch in derived key K" }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEncodeAndDecodeSimpleSmallValue() {
|
||||
val value = byteArrayOf(0x01, 0x02, 0x03, 0x04).toUByteArray()
|
||||
val item = TLV8Item(TLV8Tag.METHOD, value)
|
||||
|
||||
val encoded = TLV8Item.encode(listOf(item))
|
||||
val decoded = TLV8Item.decode(encoded.toUByteArray())
|
||||
|
||||
assertEquals(1, decoded.size)
|
||||
assertEquals(item.tag, decoded[0].tag)
|
||||
assertTrue(decoded[0].value.contentEquals(value))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEncodeAndDecodeExactly255BytesNoFragmentation() {
|
||||
val data255 = UByteArray(255) { it.toUByte() }
|
||||
val item255 = TLV8Item(TLV8Tag.IDENTIFIER, data255)
|
||||
|
||||
val encoded = TLV8Item.encode(listOf(item255))
|
||||
// Expect: 1 byte tag + 1 byte length + 255 bytes data
|
||||
assertEquals(257, encoded.size)
|
||||
assertEquals(TLV8Tag.IDENTIFIER.value.toByte(), encoded[0])
|
||||
assertEquals(0xFF, encoded[1].toInt() and 0xFF)
|
||||
|
||||
val decoded = TLV8Item.decode(encoded.toUByteArray())
|
||||
assertEquals(1, decoded.size)
|
||||
assertEquals(TLV8Tag.IDENTIFIER, decoded[0].tag)
|
||||
assertTrue(decoded[0].value.contentEquals(data255))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEncodeAndDecode256BytesWithFragmentation() {
|
||||
val data256 = UByteArray(256) { it.toUByte() }
|
||||
val item256 = TLV8Item(TLV8Tag.SALT, data256)
|
||||
|
||||
val encoded = TLV8Item.encode(listOf(item256))
|
||||
// First fragment header: SALT tag + 0xFF length
|
||||
assertEquals(TLV8Tag.SALT.value.toByte(), encoded[0])
|
||||
assertEquals(0xFF, encoded[1].toInt() and 0xFF)
|
||||
|
||||
// Locate last‐fragment header: two bytes before the final data byte
|
||||
val lastFragmentIndex = encoded.size - (1 /*remaining*/ + 2)
|
||||
assertEquals(TLV8Tag.FRAGMENT_LAST.value.toByte(), encoded[lastFragmentIndex])
|
||||
assertEquals(1.toByte(), encoded[lastFragmentIndex + 1])
|
||||
|
||||
val decoded = TLV8Item.decode(encoded.toUByteArray())
|
||||
assertEquals(1, decoded.size)
|
||||
assertTrue(decoded[0].value.contentEquals(data256))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEncodeAndDecodeMultipleItems() {
|
||||
val v1 = byteArrayOf(0x0A, 0x0B).toUByteArray()
|
||||
val v2 = byteArrayOf(0xFF.toByte(), 0xEE.toByte(), 0xDD.toByte()).toUByteArray()
|
||||
val items = listOf(
|
||||
TLV8Item(TLV8Tag.PROOF, v1),
|
||||
TLV8Item(TLV8Tag.ERROR, v2)
|
||||
)
|
||||
|
||||
val encoded = TLV8Item.encode(items)
|
||||
val decoded = TLV8Item.decode(encoded.toUByteArray())
|
||||
|
||||
assertEquals(2, decoded.size)
|
||||
assertEquals(TLV8Tag.PROOF, decoded[0].tag)
|
||||
assertTrue(decoded[0].value.contentEquals(v1))
|
||||
assertEquals(TLV8Tag.ERROR, decoded[1].tag)
|
||||
assertTrue(decoded[1].value.contentEquals(v2))
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
fun testDecodeUnknownTagThrowsIllegalArgumentException() {
|
||||
// Tag 0x10 isn’t defined in TLV8Tag
|
||||
val bogus = byteArrayOf(0x10, 0x00).toUByteArray()
|
||||
TLV8Item.decode(bogus)
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
fun testDecodeTruncatedLengthByteThrowsIllegalArgumentException() {
|
||||
// Only a tag byte, missing length byte
|
||||
val onlyTag = byteArrayOf(TLV8Tag.STATE.value.toByte()).toUByteArray()
|
||||
TLV8Item.decode(onlyTag)
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
fun testDecodeTruncatedDataThrowsIllegalArgumentException() {
|
||||
// Declared length = 2, but only 1 data byte follows
|
||||
val arr = buildList {
|
||||
add(TLV8Tag.FLAGS.value.toByte())
|
||||
add(2) // length
|
||||
add(0x5A) // only one byte of data
|
||||
}.toByteArray().toUByteArray()
|
||||
TLV8Item.decode(arr)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
package com.futo.platformplayer
|
||||
|
||||
import com.futo.platformplayer.noise.protocol.Noise
|
||||
import com.futo.platformplayer.sync.internal.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.selects.select
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Test
|
||||
import java.net.Socket
|
||||
import java.nio.ByteBuffer
|
||||
import kotlin.random.Random
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
/*
|
||||
class SyncServerTests {
|
||||
|
||||
//private val relayHost = "relay.grayjay.app"
|
||||
//private val relayKey = "xGbHRzDOvE6plRbQaFgSen82eijF+gxS0yeUaeEErkw="
|
||||
private val relayKey = "XlUaSpIlRaCg0TGzZ7JYmPupgUHDqTZXUUBco2K7ejw="
|
||||
private val relayHost = "192.168.1.138"
|
||||
private val relayPort = 9000
|
||||
|
||||
/** Creates a client connected to the live relay server. */
|
||||
private suspend fun createClient(
|
||||
onHandshakeComplete: ((SyncSocketSession) -> Unit)? = null,
|
||||
onData: ((SyncSocketSession, UByte, UByte, ByteBuffer) -> Unit)? = null,
|
||||
onNewChannel: ((SyncSocketSession, ChannelRelayed) -> Unit)? = null,
|
||||
isHandshakeAllowed: ((LinkType, SyncSocketSession, String, String?, UInt) -> Boolean)? = null,
|
||||
onException: ((Throwable) -> Unit)? = null
|
||||
): SyncSocketSession = withContext(Dispatchers.IO) {
|
||||
val p = Noise.createDH("25519")
|
||||
p.generateKeyPair()
|
||||
val socket = Socket(relayHost, relayPort)
|
||||
val inputStream = LittleEndianDataInputStream(socket.getInputStream())
|
||||
val outputStream = LittleEndianDataOutputStream(socket.getOutputStream())
|
||||
val tcs = CompletableDeferred<Boolean>()
|
||||
val socketSession = SyncSocketSession(
|
||||
relayHost,
|
||||
p,
|
||||
inputStream,
|
||||
outputStream,
|
||||
onClose = { socket.close() },
|
||||
onHandshakeComplete = { s ->
|
||||
onHandshakeComplete?.invoke(s)
|
||||
tcs.complete(true)
|
||||
},
|
||||
onData = onData ?: { _, _, _, _ -> },
|
||||
onNewChannel = onNewChannel ?: { _, _ -> },
|
||||
isHandshakeAllowed = isHandshakeAllowed ?: { _, _, _, _, _ -> true }
|
||||
)
|
||||
socketSession.authorizable = AlwaysAuthorized()
|
||||
try {
|
||||
socketSession.startAsInitiator(relayKey)
|
||||
} catch (e: Throwable) {
|
||||
onException?.invoke(e)
|
||||
}
|
||||
withTimeout(5000.milliseconds) { tcs.await() }
|
||||
return@withContext socketSession
|
||||
}
|
||||
|
||||
@Test
|
||||
fun multipleClientsHandshake_Success() = runBlocking {
|
||||
val client1 = createClient()
|
||||
val client2 = createClient()
|
||||
assertNotNull(client1.remotePublicKey, "Client 1 handshake failed")
|
||||
assertNotNull(client2.remotePublicKey, "Client 2 handshake failed")
|
||||
client1.stop()
|
||||
client2.stop()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun publishAndRequestConnectionInfo_Authorized_Success() = runBlocking {
|
||||
val clientA = createClient()
|
||||
val clientB = createClient()
|
||||
val clientC = createClient()
|
||||
clientA.publishConnectionInformation(arrayOf(clientB.localPublicKey), 12345, true, true, true, true)
|
||||
delay(100.milliseconds)
|
||||
val infoB = clientB.requestConnectionInfo(clientA.localPublicKey)
|
||||
val infoC = clientC.requestConnectionInfo(clientA.localPublicKey)
|
||||
assertNotNull("Client B should receive connection info", infoB)
|
||||
assertEquals(12345.toUShort(), infoB!!.port)
|
||||
assertNull("Client C should not receive connection info (unauthorized)", infoC)
|
||||
clientA.stop()
|
||||
clientB.stop()
|
||||
clientC.stop()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun relayedTransport_Bidirectional_Success() = runBlocking {
|
||||
val tcsA = CompletableDeferred<ChannelRelayed>()
|
||||
val tcsB = CompletableDeferred<ChannelRelayed>()
|
||||
val clientA = createClient(onNewChannel = { _, c -> tcsA.complete(c) })
|
||||
val clientB = createClient(onNewChannel = { _, c -> tcsB.complete(c) })
|
||||
val channelTask = async { clientA.startRelayedChannel(clientB.localPublicKey) }
|
||||
val channelA = withTimeout(5000.milliseconds) { tcsA.await() }
|
||||
channelA.authorizable = AlwaysAuthorized()
|
||||
val channelB = withTimeout(5000.milliseconds) { tcsB.await() }
|
||||
channelB.authorizable = AlwaysAuthorized()
|
||||
channelTask.await()
|
||||
|
||||
val tcsDataB = CompletableDeferred<ByteArray>()
|
||||
channelB.setDataHandler { _, _, o, so, d ->
|
||||
val b = ByteArray(d.remaining())
|
||||
d.get(b)
|
||||
if (o == Opcode.DATA.value && so == 0u.toUByte()) tcsDataB.complete(b)
|
||||
}
|
||||
channelA.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(byteArrayOf(1, 2, 3)))
|
||||
|
||||
val tcsDataA = CompletableDeferred<ByteArray>()
|
||||
channelA.setDataHandler { _, _, o, so, d ->
|
||||
val b = ByteArray(d.remaining())
|
||||
d.get(b)
|
||||
if (o == Opcode.DATA.value && so == 0u.toUByte()) tcsDataA.complete(b)
|
||||
}
|
||||
channelB.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(byteArrayOf(4, 5, 6)))
|
||||
|
||||
val receivedB = withTimeout(5000.milliseconds) { tcsDataB.await() }
|
||||
val receivedA = withTimeout(5000.milliseconds) { tcsDataA.await() }
|
||||
assertArrayEquals(byteArrayOf(1, 2, 3), receivedB)
|
||||
assertArrayEquals(byteArrayOf(4, 5, 6), receivedA)
|
||||
clientA.stop()
|
||||
clientB.stop()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun relayedTransport_MaximumMessageSize_Success() = runBlocking {
|
||||
val MAX_DATA_PER_PACKET = SyncSocketSession.MAXIMUM_PACKET_SIZE - SyncSocketSession.HEADER_SIZE - 8 - 16 - 16
|
||||
val maxSizeData = ByteArray(MAX_DATA_PER_PACKET).apply { Random.nextBytes(this) }
|
||||
val tcsA = CompletableDeferred<ChannelRelayed>()
|
||||
val tcsB = CompletableDeferred<ChannelRelayed>()
|
||||
val clientA = createClient(onNewChannel = { _, c -> tcsA.complete(c) })
|
||||
val clientB = createClient(onNewChannel = { _, c -> tcsB.complete(c) })
|
||||
val channelTask = async { clientA.startRelayedChannel(clientB.localPublicKey) }
|
||||
val channelA = withTimeout(5000.milliseconds) { tcsA.await() }
|
||||
channelA.authorizable = AlwaysAuthorized()
|
||||
val channelB = withTimeout(5000.milliseconds) { tcsB.await() }
|
||||
channelB.authorizable = AlwaysAuthorized()
|
||||
channelTask.await()
|
||||
|
||||
val tcsDataB = CompletableDeferred<ByteArray>()
|
||||
channelB.setDataHandler { _, _, o, so, d ->
|
||||
val b = ByteArray(d.remaining())
|
||||
d.get(b)
|
||||
if (o == Opcode.DATA.value && so == 0u.toUByte()) tcsDataB.complete(b)
|
||||
}
|
||||
channelA.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(maxSizeData))
|
||||
val receivedData = withTimeout(5000.milliseconds) { tcsDataB.await() }
|
||||
assertArrayEquals(maxSizeData, receivedData)
|
||||
clientA.stop()
|
||||
clientB.stop()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun publishAndGetRecord_Success() = runBlocking {
|
||||
val clientA = createClient()
|
||||
val clientB = createClient()
|
||||
val clientC = createClient()
|
||||
val data = byteArrayOf(1, 2, 3)
|
||||
val success = clientA.publishRecords(listOf(clientB.localPublicKey), "testKey", data)
|
||||
val recordB = clientB.getRecord(clientA.localPublicKey, "testKey")
|
||||
val recordC = clientC.getRecord(clientA.localPublicKey, "testKey")
|
||||
assertTrue(success)
|
||||
assertNotNull(recordB)
|
||||
assertArrayEquals(data, recordB!!.first)
|
||||
assertNull("Unauthorized client should not access record", recordC)
|
||||
clientA.stop()
|
||||
clientB.stop()
|
||||
clientC.stop()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getNonExistentRecord_ReturnsNull() = runBlocking {
|
||||
val clientA = createClient()
|
||||
val clientB = createClient()
|
||||
val record = clientB.getRecord(clientA.localPublicKey, "nonExistentKey")
|
||||
assertNull("Getting non-existent record should return null", record)
|
||||
clientA.stop()
|
||||
clientB.stop()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun updateRecord_TimestampUpdated() = runBlocking {
|
||||
val clientA = createClient()
|
||||
val clientB = createClient()
|
||||
val key = "updateKey"
|
||||
val data1 = byteArrayOf(1)
|
||||
val data2 = byteArrayOf(2)
|
||||
clientA.publishRecords(listOf(clientB.localPublicKey), key, data1)
|
||||
val record1 = clientB.getRecord(clientA.localPublicKey, key)
|
||||
delay(1000.milliseconds)
|
||||
clientA.publishRecords(listOf(clientB.localPublicKey), key, data2)
|
||||
val record2 = clientB.getRecord(clientA.localPublicKey, key)
|
||||
assertNotNull(record1)
|
||||
assertNotNull(record2)
|
||||
assertTrue(record2!!.second > record1!!.second)
|
||||
assertArrayEquals(data2, record2.first)
|
||||
clientA.stop()
|
||||
clientB.stop()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteRecord_Success() = runBlocking {
|
||||
val clientA = createClient()
|
||||
val clientB = createClient()
|
||||
val data = byteArrayOf(1, 2, 3)
|
||||
clientA.publishRecords(listOf(clientB.localPublicKey), "toDelete", data)
|
||||
val success = clientB.deleteRecords(clientA.localPublicKey, clientB.localPublicKey, listOf("toDelete"))
|
||||
val record = clientB.getRecord(clientA.localPublicKey, "toDelete")
|
||||
assertTrue(success)
|
||||
assertNull(record)
|
||||
clientA.stop()
|
||||
clientB.stop()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun listRecordKeys_Success() = runBlocking {
|
||||
val clientA = createClient()
|
||||
val clientB = createClient()
|
||||
val keys = arrayOf("key1", "key2", "key3")
|
||||
keys.forEach { key ->
|
||||
clientA.publishRecords(listOf(clientB.localPublicKey), key, byteArrayOf(1))
|
||||
}
|
||||
val listedKeys = clientB.listRecordKeys(clientA.localPublicKey, clientB.localPublicKey)
|
||||
assertArrayEquals(keys, listedKeys.map { it.first }.toTypedArray())
|
||||
clientA.stop()
|
||||
clientB.stop()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun singleLargeMessageViaRelayedChannel_Success() = runBlocking {
|
||||
val largeData = ByteArray(100000).apply { Random.nextBytes(this) }
|
||||
val tcsA = CompletableDeferred<ChannelRelayed>()
|
||||
val tcsB = CompletableDeferred<ChannelRelayed>()
|
||||
val clientA = createClient(onNewChannel = { _, c -> tcsA.complete(c) })
|
||||
val clientB = createClient(onNewChannel = { _, c -> tcsB.complete(c) })
|
||||
val channelTask = async { clientA.startRelayedChannel(clientB.localPublicKey) }
|
||||
val channelA = withTimeout(5000.milliseconds) { tcsA.await() }
|
||||
channelA.authorizable = AlwaysAuthorized()
|
||||
val channelB = withTimeout(5000.milliseconds) { tcsB.await() }
|
||||
channelB.authorizable = AlwaysAuthorized()
|
||||
channelTask.await()
|
||||
|
||||
val tcsDataB = CompletableDeferred<ByteArray>()
|
||||
channelB.setDataHandler { _, _, o, so, d ->
|
||||
val b = ByteArray(d.remaining())
|
||||
d.get(b)
|
||||
if (o == Opcode.DATA.value && so == 0u.toUByte()) tcsDataB.complete(b)
|
||||
}
|
||||
channelA.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(largeData))
|
||||
val receivedData = withTimeout(10000.milliseconds) { tcsDataB.await() }
|
||||
assertArrayEquals(largeData, receivedData)
|
||||
clientA.stop()
|
||||
clientB.stop()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun publishAndGetLargeRecord_Success() = runBlocking {
|
||||
val largeData = ByteArray(1000000).apply { Random.nextBytes(this) }
|
||||
val clientA = createClient()
|
||||
val clientB = createClient()
|
||||
val success = clientA.publishRecords(listOf(clientB.localPublicKey), "largeRecord", largeData)
|
||||
val record = clientB.getRecord(clientA.localPublicKey, "largeRecord")
|
||||
assertTrue(success)
|
||||
assertNotNull(record)
|
||||
assertArrayEquals(largeData, record!!.first)
|
||||
clientA.stop()
|
||||
clientB.stop()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun relayedTransport_WithValidAppId_Success() = runBlocking {
|
||||
// Arrange: Set up clients
|
||||
val allowedAppId = 1234u
|
||||
val tcsB = CompletableDeferred<ChannelRelayed>()
|
||||
|
||||
// Client B requires appId 1234
|
||||
val clientB = createClient(
|
||||
onNewChannel = { _, c -> tcsB.complete(c) },
|
||||
isHandshakeAllowed = { linkType, _, _, _, appId -> linkType == LinkType.Relayed && appId == allowedAppId }
|
||||
)
|
||||
|
||||
val clientA = createClient()
|
||||
|
||||
// Act: Start relayed channel with valid appId
|
||||
val channelTask = async { clientA.startRelayedChannel(clientB.localPublicKey, appId = allowedAppId) }
|
||||
val channelB = withTimeout(5.seconds) { tcsB.await() }
|
||||
withTimeout(5.seconds) { channelTask.await() }
|
||||
|
||||
// Assert: Channel is established
|
||||
assertNotNull("Channel should be created on target with valid appId", channelB)
|
||||
|
||||
// Clean up
|
||||
clientA.stop()
|
||||
clientB.stop()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun relayedTransport_WithInvalidAppId_Fails() = runBlocking {
|
||||
// Arrange: Set up clients
|
||||
val allowedAppId = 1234u
|
||||
val invalidAppId = 5678u
|
||||
val tcsB = CompletableDeferred<ChannelRelayed>()
|
||||
|
||||
// Client B requires appId 1234
|
||||
val clientB = createClient(
|
||||
onNewChannel = { _, c -> tcsB.complete(c) },
|
||||
isHandshakeAllowed = { linkType, _, _, _, appId -> linkType == LinkType.Relayed && appId == allowedAppId },
|
||||
onException = { }
|
||||
)
|
||||
|
||||
val clientA = createClient()
|
||||
|
||||
// Act & Assert: Attempt with invalid appId should fail
|
||||
try {
|
||||
withTimeout(5.seconds) {
|
||||
clientA.startRelayedChannel(clientB.localPublicKey, appId = invalidAppId)
|
||||
}
|
||||
fail("Starting relayed channel with invalid appId should fail")
|
||||
} catch (e: Throwable) {
|
||||
// Expected: The channel creation should time out or fail
|
||||
}
|
||||
|
||||
// Ensure no channel was created on client B
|
||||
val completedTask = select {
|
||||
tcsB.onAwait { "channel" }
|
||||
async { delay(1.seconds); "timeout" }.onAwait { "timeout" }
|
||||
}
|
||||
assertEquals("No channel should be created with invalid appId", "timeout", completedTask)
|
||||
|
||||
// Clean up
|
||||
clientA.stop()
|
||||
clientB.stop()
|
||||
}
|
||||
}
|
||||
|
||||
class AlwaysAuthorized : IAuthorizable {
|
||||
override val isAuthorized: Boolean get() = true
|
||||
}*/
|
||||
@@ -0,0 +1,512 @@
|
||||
package com.futo.platformplayer
|
||||
|
||||
import com.futo.platformplayer.noise.protocol.DHState
|
||||
import com.futo.platformplayer.noise.protocol.Noise
|
||||
import com.futo.platformplayer.sync.internal.*
|
||||
import kotlinx.coroutines.*
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Test
|
||||
import java.io.PipedInputStream
|
||||
import java.io.PipedOutputStream
|
||||
import java.nio.ByteBuffer
|
||||
import kotlin.random.Random
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
/*
|
||||
data class PipeStreams(
|
||||
val initiatorInput: LittleEndianDataInputStream,
|
||||
val initiatorOutput: LittleEndianDataOutputStream,
|
||||
val responderInput: LittleEndianDataInputStream,
|
||||
val responderOutput: LittleEndianDataOutputStream
|
||||
)
|
||||
|
||||
typealias OnHandshakeComplete = (SyncSocketSession) -> Unit
|
||||
typealias IsHandshakeAllowed = (LinkType, SyncSocketSession, String, String?, UInt) -> Boolean
|
||||
typealias OnClose = (SyncSocketSession) -> Unit
|
||||
typealias OnData = (SyncSocketSession, UByte, UByte, ByteBuffer) -> Unit
|
||||
|
||||
class SyncSocketTests {
|
||||
private fun createPipeStreams(): PipeStreams {
|
||||
val initiatorOutput = PipedOutputStream()
|
||||
val responderOutput = PipedOutputStream()
|
||||
val responderInput = PipedInputStream(initiatorOutput)
|
||||
val initiatorInput = PipedInputStream(responderOutput)
|
||||
return PipeStreams(
|
||||
LittleEndianDataInputStream(initiatorInput), LittleEndianDataOutputStream(initiatorOutput),
|
||||
LittleEndianDataInputStream(responderInput), LittleEndianDataOutputStream(responderOutput)
|
||||
)
|
||||
}
|
||||
|
||||
fun generateKeyPair(): DHState {
|
||||
val p = Noise.createDH("25519")
|
||||
p.generateKeyPair()
|
||||
return p
|
||||
}
|
||||
|
||||
private fun createSessions(
|
||||
initiatorInput: LittleEndianDataInputStream,
|
||||
initiatorOutput: LittleEndianDataOutputStream,
|
||||
responderInput: LittleEndianDataInputStream,
|
||||
responderOutput: LittleEndianDataOutputStream,
|
||||
initiatorKeyPair: DHState,
|
||||
responderKeyPair: DHState,
|
||||
onInitiatorHandshakeComplete: OnHandshakeComplete,
|
||||
onResponderHandshakeComplete: OnHandshakeComplete,
|
||||
onInitiatorClose: OnClose? = null,
|
||||
onResponderClose: OnClose? = null,
|
||||
onClose: OnClose? = null,
|
||||
isHandshakeAllowed: IsHandshakeAllowed? = null,
|
||||
onDataA: OnData? = null,
|
||||
onDataB: OnData? = null
|
||||
): Pair<SyncSocketSession, SyncSocketSession> {
|
||||
val initiatorSession = SyncSocketSession(
|
||||
"", initiatorKeyPair, initiatorInput, initiatorOutput,
|
||||
onClose = {
|
||||
onClose?.invoke(it)
|
||||
onInitiatorClose?.invoke(it)
|
||||
},
|
||||
onHandshakeComplete = onInitiatorHandshakeComplete,
|
||||
onData = onDataA,
|
||||
isHandshakeAllowed = isHandshakeAllowed
|
||||
)
|
||||
|
||||
val responderSession = SyncSocketSession(
|
||||
"", responderKeyPair, responderInput, responderOutput,
|
||||
onClose = {
|
||||
onClose?.invoke(it)
|
||||
onResponderClose?.invoke(it)
|
||||
},
|
||||
onHandshakeComplete = onResponderHandshakeComplete,
|
||||
onData = onDataB,
|
||||
isHandshakeAllowed = isHandshakeAllowed
|
||||
)
|
||||
|
||||
return Pair(initiatorSession, responderSession)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handshake_WithValidPairingCode_Succeeds(): Unit = runBlocking {
|
||||
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
|
||||
val initiatorKeyPair = generateKeyPair()
|
||||
val responderKeyPair = generateKeyPair()
|
||||
val validPairingCode = "secret"
|
||||
|
||||
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
|
||||
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
|
||||
|
||||
val (initiatorSession, responderSession) = createSessions(
|
||||
initiatorInput, initiatorOutput, responderInput, responderOutput,
|
||||
initiatorKeyPair, responderKeyPair,
|
||||
{ handshakeInitiatorCompleted.complete(true) },
|
||||
{ handshakeResponderCompleted.complete(true) },
|
||||
isHandshakeAllowed = { _, _, _, pairingCode, _ -> pairingCode == validPairingCode }
|
||||
)
|
||||
|
||||
initiatorSession.startAsInitiator(responderSession.localPublicKey, pairingCode = validPairingCode)
|
||||
responderSession.startAsResponder()
|
||||
|
||||
withTimeout(5.seconds) {
|
||||
handshakeInitiatorCompleted.await()
|
||||
handshakeResponderCompleted.await()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handshake_WithInvalidPairingCode_Fails() = runBlocking {
|
||||
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
|
||||
val initiatorKeyPair = generateKeyPair()
|
||||
val responderKeyPair = generateKeyPair()
|
||||
val validPairingCode = "secret"
|
||||
val invalidPairingCode = "wrong"
|
||||
|
||||
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
|
||||
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
|
||||
val initiatorClosed = CompletableDeferred<Boolean>()
|
||||
val responderClosed = CompletableDeferred<Boolean>()
|
||||
|
||||
val (initiatorSession, responderSession) = createSessions(
|
||||
initiatorInput, initiatorOutput, responderInput, responderOutput,
|
||||
initiatorKeyPair, responderKeyPair,
|
||||
{ handshakeInitiatorCompleted.complete(true) },
|
||||
{ handshakeResponderCompleted.complete(true) },
|
||||
onInitiatorClose = {
|
||||
initiatorClosed.complete(true)
|
||||
},
|
||||
onResponderClose = {
|
||||
responderClosed.complete(true)
|
||||
},
|
||||
isHandshakeAllowed = { _, _, _, pairingCode, _ -> pairingCode == validPairingCode }
|
||||
)
|
||||
|
||||
initiatorSession.startAsInitiator(responderSession.localPublicKey, pairingCode = invalidPairingCode)
|
||||
responderSession.startAsResponder()
|
||||
|
||||
withTimeout(100.seconds) {
|
||||
initiatorClosed.await()
|
||||
responderClosed.await()
|
||||
}
|
||||
|
||||
assertFalse(handshakeInitiatorCompleted.isCompleted)
|
||||
assertFalse(handshakeResponderCompleted.isCompleted)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handshake_WithoutPairingCodeWhenRequired_Fails() = runBlocking {
|
||||
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
|
||||
val initiatorKeyPair = generateKeyPair()
|
||||
val responderKeyPair = generateKeyPair()
|
||||
val validPairingCode = "secret"
|
||||
|
||||
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
|
||||
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
|
||||
val initiatorClosed = CompletableDeferred<Boolean>()
|
||||
val responderClosed = CompletableDeferred<Boolean>()
|
||||
|
||||
val (initiatorSession, responderSession) = createSessions(
|
||||
initiatorInput, initiatorOutput, responderInput, responderOutput,
|
||||
initiatorKeyPair, responderKeyPair,
|
||||
{ handshakeInitiatorCompleted.complete(true) },
|
||||
{ handshakeResponderCompleted.complete(true) },
|
||||
onInitiatorClose = {
|
||||
initiatorClosed.complete(true)
|
||||
},
|
||||
onResponderClose = {
|
||||
responderClosed.complete(true)
|
||||
},
|
||||
isHandshakeAllowed = { _, _, _, pairingCode, _ -> pairingCode == validPairingCode }
|
||||
)
|
||||
|
||||
initiatorSession.startAsInitiator(responderSession.localPublicKey) // No pairing code
|
||||
responderSession.startAsResponder()
|
||||
|
||||
withTimeout(5.seconds) {
|
||||
initiatorClosed.await()
|
||||
responderClosed.await()
|
||||
}
|
||||
|
||||
assertFalse(handshakeInitiatorCompleted.isCompleted)
|
||||
assertFalse(handshakeResponderCompleted.isCompleted)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handshake_WithPairingCodeWhenNotRequired_Succeeds(): Unit = runBlocking {
|
||||
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
|
||||
val initiatorKeyPair = generateKeyPair()
|
||||
val responderKeyPair = generateKeyPair()
|
||||
val pairingCode = "unnecessary"
|
||||
|
||||
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
|
||||
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
|
||||
|
||||
val (initiatorSession, responderSession) = createSessions(
|
||||
initiatorInput, initiatorOutput, responderInput, responderOutput,
|
||||
initiatorKeyPair, responderKeyPair,
|
||||
{ handshakeInitiatorCompleted.complete(true) },
|
||||
{ handshakeResponderCompleted.complete(true) },
|
||||
isHandshakeAllowed = { _, _, _, _, _ -> true } // Always allow
|
||||
)
|
||||
|
||||
initiatorSession.startAsInitiator(responderSession.localPublicKey, pairingCode = pairingCode)
|
||||
responderSession.startAsResponder()
|
||||
|
||||
withTimeout(10.seconds) {
|
||||
handshakeInitiatorCompleted.await()
|
||||
handshakeResponderCompleted.await()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sendAndReceive_SmallDataPacket_Succeeds() = runBlocking {
|
||||
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
|
||||
val initiatorKeyPair = generateKeyPair()
|
||||
val responderKeyPair = generateKeyPair()
|
||||
|
||||
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
|
||||
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
|
||||
val tcsDataReceived = CompletableDeferred<ByteArray>()
|
||||
|
||||
val (initiatorSession, responderSession) = createSessions(
|
||||
initiatorInput, initiatorOutput, responderInput, responderOutput,
|
||||
initiatorKeyPair, responderKeyPair,
|
||||
{ handshakeInitiatorCompleted.complete(true) },
|
||||
{ handshakeResponderCompleted.complete(true) },
|
||||
onDataB = { _, opcode, subOpcode, data ->
|
||||
if (opcode == Opcode.DATA.value && subOpcode == 0u.toUByte()) {
|
||||
val b = ByteArray(data.remaining())
|
||||
data.get(b)
|
||||
tcsDataReceived.complete(b)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
initiatorSession.startAsInitiator(responderSession.localPublicKey)
|
||||
responderSession.startAsResponder()
|
||||
|
||||
withTimeout(10.seconds) {
|
||||
handshakeInitiatorCompleted.await()
|
||||
handshakeResponderCompleted.await()
|
||||
}
|
||||
|
||||
// Ensure both sessions are authorized
|
||||
initiatorSession.authorizable = Authorized()
|
||||
responderSession.authorizable = Authorized()
|
||||
|
||||
val smallData = byteArrayOf(1, 2, 3)
|
||||
initiatorSession.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(smallData))
|
||||
|
||||
val receivedData = withTimeout(10.seconds) { tcsDataReceived.await() }
|
||||
assertArrayEquals(smallData, receivedData)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sendAndReceive_ExactlyMaximumPacketSize_Succeeds() = runBlocking {
|
||||
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
|
||||
val initiatorKeyPair = generateKeyPair()
|
||||
val responderKeyPair = generateKeyPair()
|
||||
|
||||
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
|
||||
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
|
||||
val tcsDataReceived = CompletableDeferred<ByteArray>()
|
||||
|
||||
val (initiatorSession, responderSession) = createSessions(
|
||||
initiatorInput, initiatorOutput, responderInput, responderOutput,
|
||||
initiatorKeyPair, responderKeyPair,
|
||||
{ handshakeInitiatorCompleted.complete(true) },
|
||||
{ handshakeResponderCompleted.complete(true) },
|
||||
onDataB = { _, opcode, subOpcode, data ->
|
||||
if (opcode == Opcode.DATA.value && subOpcode == 0u.toUByte()) {
|
||||
val b = ByteArray(data.remaining())
|
||||
data.get(b)
|
||||
tcsDataReceived.complete(b)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
initiatorSession.startAsInitiator(responderSession.localPublicKey)
|
||||
responderSession.startAsResponder()
|
||||
|
||||
withTimeout(10.seconds) {
|
||||
handshakeInitiatorCompleted.await()
|
||||
handshakeResponderCompleted.await()
|
||||
}
|
||||
|
||||
// Ensure both sessions are authorized
|
||||
initiatorSession.authorizable = Authorized()
|
||||
responderSession.authorizable = Authorized()
|
||||
|
||||
val maxData = ByteArray(SyncSocketSession.MAXIMUM_PACKET_SIZE - SyncSocketSession.HEADER_SIZE).apply { Random.nextBytes(this) }
|
||||
initiatorSession.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(maxData))
|
||||
|
||||
val receivedData = withTimeout(10.seconds) { tcsDataReceived.await() }
|
||||
assertArrayEquals(maxData, receivedData)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun stream_LargeData_Succeeds() = runBlocking {
|
||||
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
|
||||
val initiatorKeyPair = generateKeyPair()
|
||||
val responderKeyPair = generateKeyPair()
|
||||
|
||||
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
|
||||
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
|
||||
val tcsDataReceived = CompletableDeferred<ByteArray>()
|
||||
|
||||
val (initiatorSession, responderSession) = createSessions(
|
||||
initiatorInput, initiatorOutput, responderInput, responderOutput,
|
||||
initiatorKeyPair, responderKeyPair,
|
||||
{ handshakeInitiatorCompleted.complete(true) },
|
||||
{ handshakeResponderCompleted.complete(true) },
|
||||
onDataB = { _, opcode, subOpcode, data ->
|
||||
if (opcode == Opcode.DATA.value && subOpcode == 0u.toUByte()) {
|
||||
val b = ByteArray(data.remaining())
|
||||
data.get(b)
|
||||
tcsDataReceived.complete(b)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
initiatorSession.startAsInitiator(responderSession.localPublicKey)
|
||||
responderSession.startAsResponder()
|
||||
|
||||
withTimeout(10.seconds) {
|
||||
handshakeInitiatorCompleted.await()
|
||||
handshakeResponderCompleted.await()
|
||||
}
|
||||
|
||||
// Ensure both sessions are authorized
|
||||
initiatorSession.authorizable = Authorized()
|
||||
responderSession.authorizable = Authorized()
|
||||
|
||||
val largeData = ByteArray(2 * (SyncSocketSession.MAXIMUM_PACKET_SIZE - SyncSocketSession.HEADER_SIZE)).apply { Random.nextBytes(this) }
|
||||
initiatorSession.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(largeData))
|
||||
|
||||
val receivedData = withTimeout(10.seconds) { tcsDataReceived.await() }
|
||||
assertArrayEquals(largeData, receivedData)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun authorizedSession_CanSendData() = runBlocking {
|
||||
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
|
||||
val initiatorKeyPair = generateKeyPair()
|
||||
val responderKeyPair = generateKeyPair()
|
||||
|
||||
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
|
||||
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
|
||||
val tcsDataReceived = CompletableDeferred<ByteArray>()
|
||||
|
||||
val (initiatorSession, responderSession) = createSessions(
|
||||
initiatorInput, initiatorOutput, responderInput, responderOutput,
|
||||
initiatorKeyPair, responderKeyPair,
|
||||
{ handshakeInitiatorCompleted.complete(true) },
|
||||
{ handshakeResponderCompleted.complete(true) },
|
||||
onDataB = { _, opcode, subOpcode, data ->
|
||||
if (opcode == Opcode.DATA.value && subOpcode == 0u.toUByte()) {
|
||||
val b = ByteArray(data.remaining())
|
||||
data.get(b)
|
||||
tcsDataReceived.complete(b)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
initiatorSession.startAsInitiator(responderSession.localPublicKey)
|
||||
responderSession.startAsResponder()
|
||||
|
||||
withTimeout(10.seconds) {
|
||||
handshakeInitiatorCompleted.await()
|
||||
handshakeResponderCompleted.await()
|
||||
}
|
||||
|
||||
// Authorize both sessions
|
||||
initiatorSession.authorizable = Authorized()
|
||||
responderSession.authorizable = Authorized()
|
||||
|
||||
val data = byteArrayOf(1, 2, 3)
|
||||
initiatorSession.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(data))
|
||||
|
||||
val receivedData = withTimeout(10.seconds) { tcsDataReceived.await() }
|
||||
assertArrayEquals(data, receivedData)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun unauthorizedSession_CannotSendData() = runBlocking {
|
||||
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
|
||||
val initiatorKeyPair = generateKeyPair()
|
||||
val responderKeyPair = generateKeyPair()
|
||||
|
||||
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
|
||||
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
|
||||
val tcsDataReceived = CompletableDeferred<ByteArray>()
|
||||
|
||||
val (initiatorSession, responderSession) = createSessions(
|
||||
initiatorInput, initiatorOutput, responderInput, responderOutput,
|
||||
initiatorKeyPair, responderKeyPair,
|
||||
{ handshakeInitiatorCompleted.complete(true) },
|
||||
{ handshakeResponderCompleted.complete(true) },
|
||||
onDataB = { _, _, _, _ -> }
|
||||
)
|
||||
|
||||
initiatorSession.startAsInitiator(responderSession.localPublicKey)
|
||||
responderSession.startAsResponder()
|
||||
|
||||
withTimeout(10.seconds) {
|
||||
handshakeInitiatorCompleted.await()
|
||||
handshakeResponderCompleted.await()
|
||||
}
|
||||
|
||||
// Authorize initiator but not responder
|
||||
initiatorSession.authorizable = Authorized()
|
||||
responderSession.authorizable = Unauthorized()
|
||||
|
||||
val data = byteArrayOf(1, 2, 3)
|
||||
initiatorSession.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(data))
|
||||
|
||||
delay(1.seconds)
|
||||
assertFalse(tcsDataReceived.isCompleted)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun directHandshake_WithValidAppId_Succeeds() = runBlocking {
|
||||
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
|
||||
val initiatorKeyPair = generateKeyPair()
|
||||
val responderKeyPair = generateKeyPair()
|
||||
val allowedAppId = 1234u
|
||||
|
||||
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
|
||||
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
|
||||
|
||||
val responderIsHandshakeAllowed = { linkType: LinkType, _: SyncSocketSession, _: String, _: String?, appId: UInt ->
|
||||
linkType == LinkType.Direct && appId == allowedAppId
|
||||
}
|
||||
|
||||
val (initiatorSession, responderSession) = createSessions(
|
||||
initiatorInput, initiatorOutput, responderInput, responderOutput,
|
||||
initiatorKeyPair, responderKeyPair,
|
||||
{ handshakeInitiatorCompleted.complete(true) },
|
||||
{ handshakeResponderCompleted.complete(true) },
|
||||
isHandshakeAllowed = responderIsHandshakeAllowed
|
||||
)
|
||||
|
||||
initiatorSession.startAsInitiator(responderSession.localPublicKey, appId = allowedAppId)
|
||||
responderSession.startAsResponder()
|
||||
|
||||
withTimeout(5.seconds) {
|
||||
handshakeInitiatorCompleted.await()
|
||||
handshakeResponderCompleted.await()
|
||||
}
|
||||
|
||||
assertNotNull(initiatorSession.remotePublicKey)
|
||||
assertNotNull(responderSession.remotePublicKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun directHandshake_WithInvalidAppId_Fails() = runBlocking {
|
||||
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
|
||||
val initiatorKeyPair = generateKeyPair()
|
||||
val responderKeyPair = generateKeyPair()
|
||||
val allowedAppId = 1234u
|
||||
val invalidAppId = 5678u
|
||||
|
||||
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
|
||||
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
|
||||
val initiatorClosed = CompletableDeferred<Boolean>()
|
||||
val responderClosed = CompletableDeferred<Boolean>()
|
||||
|
||||
val responderIsHandshakeAllowed = { linkType: LinkType, _: SyncSocketSession, _: String, _: String?, appId: UInt ->
|
||||
linkType == LinkType.Direct && appId == allowedAppId
|
||||
}
|
||||
|
||||
val (initiatorSession, responderSession) = createSessions(
|
||||
initiatorInput, initiatorOutput, responderInput, responderOutput,
|
||||
initiatorKeyPair, responderKeyPair,
|
||||
{ handshakeInitiatorCompleted.complete(true) },
|
||||
{ handshakeResponderCompleted.complete(true) },
|
||||
onInitiatorClose = {
|
||||
initiatorClosed.complete(true)
|
||||
},
|
||||
onResponderClose = {
|
||||
responderClosed.complete(true)
|
||||
},
|
||||
isHandshakeAllowed = responderIsHandshakeAllowed
|
||||
)
|
||||
|
||||
initiatorSession.startAsInitiator(responderSession.localPublicKey, appId = invalidAppId)
|
||||
responderSession.startAsResponder()
|
||||
|
||||
withTimeout(5.seconds) {
|
||||
initiatorClosed.await()
|
||||
responderClosed.await()
|
||||
}
|
||||
|
||||
assertFalse(handshakeInitiatorCompleted.isCompleted)
|
||||
assertFalse(handshakeResponderCompleted.isCompleted)
|
||||
}
|
||||
}
|
||||
|
||||
class Authorized : IAuthorizable {
|
||||
override val isAuthorized: Boolean = true
|
||||
}
|
||||
|
||||
class Unauthorized : IAuthorizable {
|
||||
override val isAuthorized: Boolean = false
|
||||
}*/
|
||||
@@ -11,8 +11,10 @@
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
|
||||
<uses-permission android:name="com.android.alarm.permission.SET_ALARM"/>
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
|
||||
<!--<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>-->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
|
||||
<uses-permission android:name="android.permission.WRITE_SETTINGS" tools:ignore="ProtectedPermissions"/>
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
@@ -24,7 +26,8 @@
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.FutoVideo"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="31">
|
||||
tools:targetApi="31"
|
||||
android:largeHeap="true">
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="@string/authority"
|
||||
@@ -33,15 +36,18 @@
|
||||
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
|
||||
<receiver android:name=".receivers.MediaButtonReceiver" android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<service android:name=".services.MediaPlaybackService"
|
||||
android:enabled="true"
|
||||
android:foregroundServiceType="mediaPlayback" />
|
||||
<service android:name=".services.DownloadService"
|
||||
android:enabled="true"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
<service android:name=".services.ExportingService"
|
||||
android:enabled="true"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
<receiver android:name=".receivers.MediaControlReceiver" />
|
||||
<receiver android:name=".receivers.AudioNoisyReceiver" />
|
||||
@@ -49,11 +55,10 @@
|
||||
|
||||
<activity
|
||||
android:name=".activities.MainActivity"
|
||||
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout|uiMode"
|
||||
android:exported="true"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar"
|
||||
android:launchMode="singleTask"
|
||||
android:launchMode="singleInstance"
|
||||
android:resizeableActivity="true"
|
||||
android:supportsPictureInPicture="true">
|
||||
|
||||
@@ -146,34 +151,30 @@
|
||||
<data android:scheme="polycentric" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".activities.TestActivity"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
|
||||
<activity
|
||||
android:name=".activities.SettingsActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.DeveloperActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.ExceptionActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.CaptchaActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.LoginActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.AddSourceActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar">
|
||||
<intent-filter>
|
||||
@@ -187,44 +188,55 @@
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".activities.AddSourceOptionsActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.PolycentricHomeActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.PolycentricBackupActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.PolycentricCreateProfileActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.PolycentricProfileActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.PolycentricWhyActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.PolycentricImportProfileActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.ManageTabsActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
|
||||
<activity
|
||||
android:name=".activities.QRCaptureActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.FCastGuideActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.SyncHomeActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.SyncPairActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.SyncShowPairingCodeActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
</application>
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_287_2206)">
|
||||
<path d="M22.0557 38.25L43.1117 6H1L22.0557 38.25Z" fill="url(#paint0_linear_287_2206)"/>
|
||||
<path d="M6 28.2444C6.85811 27.3291 8.98625 25.2353 10.6338 24.1827C12.2814 23.13 14.257 20.1209 15.0388 18.7479C17.4224 15.2392 22.7618 7.91286 25.0501 6.67716C25.462 6.35678 26.0608 5.85718 26.3087 5.64745C27.1668 3.7405 30.0844 0.498738 34.8898 2.78706C35.3017 2.64974 36.32 2.61542 36.7777 2.61542C36.4153 2.86334 35.6564 3.58795 35.5191 4.50328C35.153 7.02039 33.7647 8.48874 33.1164 8.90825C32.6587 11.8259 32.0294 14.4002 30.6564 15.3155L31.915 17.5466C33.8029 19.5489 37.7159 23.8737 38.2649 25.1552C36.4344 24.5603 35.2521 23.992 34.8898 23.7822L38.2649 28.416C36.2818 28.2635 31.8235 26.9744 29.8556 23.0385C30.6336 25.1438 31.4001 27.7677 31.6862 28.8165C30.6183 27.9393 28.3224 25.3955 27.6816 22.2376C27.8647 25.304 27.8342 27.4816 27.7961 28.1872C27.2812 27.7105 26.0913 26.2307 25.4505 24.1255V27.6723C24.6821 26.604 23.1363 24.0104 22.9967 22.0533C23.1255 24.2716 23.047 25.3115 22.9906 25.5556L20.0731 22.8097C19.2912 23.2292 17.1898 24.1827 15.0388 24.6403C13.5743 25.876 11.797 28.969 11.0915 30.3611V28.5877L9.14643 30.5327L9.83291 28.4733L8.57433 29.5602C8.28828 29.7318 7.62468 30.0751 7.25857 30.0751C7.39585 29.7547 7.65904 29.4076 7.77345 29.2741L6.11441 29.9034C6.3051 29.3504 6.90388 28.13 7.77345 27.6723C6.58351 28.13 6.09536 28.2444 6 28.2444Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_287_2206" x1="22.0557" y1="38.25" x2="22.0557" y2="-4.75" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#01D6E6"/>
|
||||
<stop offset="1" stop-color="#0182E7"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_287_2206">
|
||||
<rect width="44" height="44" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -233,6 +233,9 @@ function pluginRemoteProp(objID, propName) {
|
||||
function pluginRemoteCall(objID, methodName, args) {
|
||||
return JSON.parse(syncPOST("/plugin/remoteCall?id=" + objID + "&method=" + methodName, {}, JSON.stringify(args)));
|
||||
}
|
||||
function pluginRemoteTest(methodName, args) {
|
||||
return JSON.parse(syncPOST("/plugin/remoteTest?method=" + methodName, {}, JSON.stringify(args)));
|
||||
}
|
||||
|
||||
function pluginIsLoggedIn(cb, err) {
|
||||
fetch("/plugin/isLoggedIn", {
|
||||
@@ -259,6 +262,17 @@ function getDevLogs(lastIndex, cb) {
|
||||
.then(x=>x.json())
|
||||
.then(y=> cb && cb(y));
|
||||
}
|
||||
function getDevHttpExchanges(cb) {
|
||||
fetch("/plugin/getDevHttpExchanges", {
|
||||
timeout: 1000
|
||||
})
|
||||
.then(x=>x.json())
|
||||
.then(y=> cb && cb(y));
|
||||
}
|
||||
function setDevHttpProxy(url, port) {
|
||||
return fetch("/dev/setDevProxy?url=" + encodeURIComponent(url) + "&port=" + port)
|
||||
.then(x=>x.json());
|
||||
}
|
||||
function sendFakeDevLog(devId, msg) {
|
||||
return syncGET("/plugin/fakeDevLog?devId=" + devId + "&msg=" + msg, {});
|
||||
}
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
<!--<link href="./dependencies/vuetify.min.css" rel="stylesheet">-->
|
||||
<link href="https://cdn.jsdelivr.net/npm/vuetify@2.7.1/dist/vuetify.min.css" rel="stylesheet">
|
||||
|
||||
<title>DevPortal</title>
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.svg">
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">
|
||||
|
||||
<style>
|
||||
@@ -150,7 +153,7 @@
|
||||
.pastPluginUrl {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
width: 500px;
|
||||
width: 700px;
|
||||
text-align: center;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
@@ -160,13 +163,122 @@
|
||||
box-shadow: 0px 1px 2px #131313;
|
||||
font-weight: lighter;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
.pastPluginUrl .deleteButton {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
height: 100%;
|
||||
width: 30px;
|
||||
top: 0px;
|
||||
padding-top: 2px;
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
font-weight: 400;
|
||||
transform: scaleX(1.5);
|
||||
}
|
||||
|
||||
[v-cloak] {
|
||||
display: none;
|
||||
}
|
||||
#cloakLoader {
|
||||
display: block;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: black;
|
||||
color: white;
|
||||
padding-top: 50px;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
.httpContainer {
|
||||
position: relative;
|
||||
}
|
||||
.httpLine {
|
||||
}
|
||||
.httpLine .request {
|
||||
height: 50px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
.httpLine .request .status {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
width: 40px;
|
||||
top: 10px;
|
||||
padding: 5px;
|
||||
background-color: #333;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
.httpLine .request .status.error {
|
||||
background-color: #880000;
|
||||
}
|
||||
.httpLine .request .status.success {
|
||||
background-color: #008800;
|
||||
}
|
||||
.httpLine .request .status.warn {
|
||||
background-color: #803500;
|
||||
}
|
||||
.httpLine .request .method {
|
||||
position: absolute;
|
||||
left: 55px;
|
||||
top: 10px;
|
||||
padding: 5px;
|
||||
background-color: #333;
|
||||
border-radius: 5px;
|
||||
width: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
.httpLine .request .url {
|
||||
position: absolute;
|
||||
left: 110px;
|
||||
top: 10px;
|
||||
padding: 5px;
|
||||
background-color: #333;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.httpLine .response {
|
||||
background-color: #111;
|
||||
margin-left: 55px;
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
}
|
||||
.httpLine .response .body{
|
||||
white-space: pre-wrap;
|
||||
font-family: monospace;
|
||||
background-color: black;
|
||||
padding: 10px;
|
||||
}
|
||||
.httpLine .response .headers {
|
||||
margin: 10px;
|
||||
}
|
||||
.httpLine .response .headers .key {
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
color: #FFF;
|
||||
}
|
||||
.httpLine .response .headers .value {
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
color: #AAA;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<v-app>
|
||||
<v-main>
|
||||
<div v-cloak id="cloakLoader" v-if="!page">
|
||||
<h2>Loading..</h2>
|
||||
First load may take longer
|
||||
</div>
|
||||
<v-main v-cloak>
|
||||
<div id="topMenu">
|
||||
<div style="height: 100%; display: inline-block; padding-left: 10px; padding-right: 20px;">
|
||||
<img src="./dependencies/FutoMainLogo.svg"
|
||||
@@ -250,10 +362,13 @@
|
||||
</div>
|
||||
|
||||
|
||||
<div v-if="pastPluginUrls" style="margin-top: 60px;">
|
||||
<div v-if="pastPluginUrls" style="margin-top: 60px; margin-left: 25px;">
|
||||
<h2 style="font-weight: lighter; text-align: center;">Past Plugins</h2>
|
||||
<div class="pastPluginUrl" v-for="pastPluginUrl in pastPluginUrls" @click="this.Plugin.newPluginUrl = pastPluginUrl; loadPlugin(pastPluginUrl)">
|
||||
{{pastPluginUrl}}
|
||||
<div class="deleteButton" @click="(ev)=>{ev.stopPropagation(); deletePastPlugin(pastPluginUrl)}">
|
||||
X
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -385,8 +500,8 @@
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<div style="width: 50%" v-if="Plugin.currentPlugin">
|
||||
<!--Get Home-->
|
||||
<v-card class="requestCard" v-for="req in Testing.requests">
|
||||
<v-text-field v-model="searchTestMethods" label="Search for source methods.." style="margin-left: 35px; margin-right: 35px;"></v-text-field>
|
||||
<v-card class="requestCard" v-for="req in Testing.requests" v-show="req.title.indexOf(searchTestMethods) >= 0">
|
||||
<v-card-text>
|
||||
<div class="title">
|
||||
<span v-if="req.isOptional">(Optional)</span>
|
||||
@@ -402,6 +517,11 @@
|
||||
<div class="code">
|
||||
{{req.code}}
|
||||
</div>
|
||||
<div class="documentation" v-if="req.docUrl" style="position: absolute; right: 15px; top: 15px;">
|
||||
<a :href="req.docUrl" target="_blank">
|
||||
Documentation
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<div class="parameter" v-for="parameter in req.parameters">
|
||||
<div class="name">
|
||||
@@ -416,6 +536,9 @@
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn @click="testSourceRemotely(req)">
|
||||
Test Android
|
||||
</v-btn>
|
||||
<v-btn @click="testSource(req)">
|
||||
Test
|
||||
</v-btn>
|
||||
@@ -497,7 +620,62 @@
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn>Clear</v-btn>
|
||||
<v-btn @click="Integration.logs = []">Clear</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
<v-card style="margin: 20px;" v-if="Plugin.currentPlugin && Integration.httpExchanges">
|
||||
<v-card-title>
|
||||
Http Logs
|
||||
</v-card-title>
|
||||
</v-card-header>
|
||||
<v-card-text>
|
||||
<div style="position: absolute; top: 0px; right: 15px;">
|
||||
<v-checkbox v-model="Integration.showHttpRequests" label="Show Http Requests"></v-checkbox>
|
||||
</div>
|
||||
<div class="httpContainer" v-if="Integration.showHttpRequests">
|
||||
<div class="httpLine" v-for="exchange of Integration.httpExchanges">
|
||||
<div class="request" @click="toggleHttpExchange(exchange)">
|
||||
<div :class="[{ success: exchange.response.status < 300, warn: exchange.response.status >= 300 && exchange.response.status < 400, error: exchange.response.status >= 400 }, 'status']">
|
||||
{{exchange.response.status}}
|
||||
</div>
|
||||
<div class="method">
|
||||
{{exchange.request.method}}
|
||||
</div>
|
||||
<div class="url">
|
||||
{{exchange.request.url}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="response" v-if="exchange.response.show">
|
||||
<h2>Request Headers</h2>
|
||||
<div class="headers">
|
||||
<div class="header" v-for="(headerValue, header) in exchange.request.headers">
|
||||
<div class="key">
|
||||
{{header}}
|
||||
</div>
|
||||
<div class="value">
|
||||
{{headerValue}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2>Response</h2>
|
||||
<div class="headers">
|
||||
<div class="header" v-for="(headerValue, header) in exchange.response.headers">
|
||||
<div class="key">
|
||||
{{header}}
|
||||
</div>
|
||||
<div class="value">
|
||||
{{headerValue}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="body">{{exchange.response.body}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn v-if="Integration.showHttpRequests" @click="Integration.httpExchanges = []">Clear</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</div>
|
||||
@@ -535,6 +713,7 @@
|
||||
<!--<script src="./dependencies/vue.js"></script>-->
|
||||
<!--<script src="./dependencies/vuetify.js"></script>-->
|
||||
<script src="./source_docs.js"></script>
|
||||
<script src="./source_doc_urls.js"></script>
|
||||
<script src="./source.js"></script>
|
||||
<script src="./dev_bridge.js"></script>
|
||||
<script>
|
||||
@@ -545,6 +724,7 @@
|
||||
new Vue({
|
||||
el: '#app',
|
||||
data: {
|
||||
searchTestMethods: "",
|
||||
page: "Plugin",
|
||||
pastPluginUrls: [],
|
||||
settings: {},
|
||||
@@ -552,7 +732,9 @@
|
||||
lastLogIndex: -1,
|
||||
lastLogDevID: "",
|
||||
logs: [],
|
||||
lastInjectTime: ""
|
||||
httpExchanges: [],
|
||||
lastInjectTime: "",
|
||||
showHttpRequests: false
|
||||
},
|
||||
Plugin: {
|
||||
loadUsingTag: false,
|
||||
@@ -570,6 +752,9 @@
|
||||
Testing: {
|
||||
requests: sourceDocs.map(x=>{
|
||||
x.parameters.forEach(y=>y.value = null);
|
||||
|
||||
if(sourceDocUrls[x.title])
|
||||
x.docUrl = sourceDocUrls[x.title];
|
||||
return x;
|
||||
}),
|
||||
lastResult: "",
|
||||
@@ -633,6 +818,16 @@
|
||||
});
|
||||
}
|
||||
});
|
||||
if(this.Integration.showHttpRequests) {
|
||||
getDevHttpExchanges((exchanges)=>{
|
||||
Vue.nextTick(()=>{
|
||||
for(i = 0; i < exchanges.length; i++) {
|
||||
exchanges[i].response.show = false;
|
||||
this.Integration.httpExchanges.unshift(exchanges[i]);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
catch(ex) {
|
||||
console.error("Failed update", ex);
|
||||
@@ -674,6 +869,12 @@
|
||||
this.reloadPlugin();
|
||||
});
|
||||
},
|
||||
deletePastPlugin(url) {
|
||||
let currentPastPlugins = this.pastPluginUrls;
|
||||
currentPastPlugins = currentPastPlugins.filter(x=>x.toLowerCase() != url.toLowerCase());
|
||||
this.pastPluginUrls = currentPastPlugins;
|
||||
localStorage.setItem("pastPlugins", JSON.stringify(currentPastPlugins));
|
||||
},
|
||||
loginTestPlugin() {
|
||||
pluginLoginTestPlugin();
|
||||
setTimeout(()=>{
|
||||
@@ -860,8 +1061,58 @@
|
||||
"Error: " + ex;
|
||||
}
|
||||
},
|
||||
testSourceRemotely(req) {
|
||||
const name = req.title;
|
||||
const parameterVals = req.parameters.map(x=>{
|
||||
if(x.value && x.value.startsWith && x.value.startsWith("json:"))
|
||||
return JSON.parse(x.value.substring(5));
|
||||
return x.value
|
||||
});
|
||||
|
||||
if(name == "enable") {
|
||||
if(parameterVals.length > 0)
|
||||
parameterVals[0] = this.Plugin.currentPlugin;
|
||||
else
|
||||
parameterVals.push(this.Plugin.currentPlugin);
|
||||
if(parameterVals.length > 1)
|
||||
parameterVals[1] = __DEV_SETTINGS;
|
||||
else
|
||||
parameterVals.push(__DEV_SETTINGS);
|
||||
}
|
||||
|
||||
const func = source[name];
|
||||
if(!func)
|
||||
alert("Test func not found");
|
||||
|
||||
try {
|
||||
const remoteResult = pluginRemoteTest(name, parameterVals);
|
||||
console.log("Result for " + req.title, remoteResult);
|
||||
this.Testing.lastResult = "//Results [" + name + "]\n" +
|
||||
JSON.stringify(remoteResult, null, 3);
|
||||
this.Testing.lastResultError = "";
|
||||
}
|
||||
catch(ex) {
|
||||
if(ex.plugin_type == "CaptchaRequiredException") {
|
||||
let shouldCaptcha = confirm("Do you want to request captcha?");
|
||||
if(shouldCaptcha) {
|
||||
pluginCaptchaTestPlugin(ex.url, ex.body);
|
||||
}
|
||||
}
|
||||
console.error("Failed to run test for " + req.title, ex);
|
||||
this.Testing.lastResult = ""
|
||||
if(ex.message)
|
||||
this.Testing.lastResultError = "//Results [" + name + "]\n\n" +
|
||||
"Error: " + ex.message + "\n\n" + ex.stack;
|
||||
else
|
||||
this.Testing.lastResultError = "//Results [" + name + "]\n\n" +
|
||||
"Error: " + ex;
|
||||
}
|
||||
},
|
||||
showTestResults(results) {
|
||||
|
||||
},
|
||||
toggleHttpExchange(exchange) {
|
||||
exchange.response.show = !exchange.response.show;
|
||||
},
|
||||
copyClipboard(cpy) {
|
||||
if(navigator.clipboard)
|
||||
|
||||
+121
-47
@@ -1,13 +1,37 @@
|
||||
|
||||
declare class ScriptException extends Error {
|
||||
//If only one parameter is provided, acts as msg
|
||||
constructor(type: string, msg: string);
|
||||
}
|
||||
declare class TimeoutException extends ScriptException {
|
||||
|
||||
declare class LoginRequiredException extends ScriptException {
|
||||
constructor(msg: string);
|
||||
}
|
||||
//Alias
|
||||
declare class ScriptLoginRequiredException extends ScriptException {
|
||||
constructor(msg: string);
|
||||
}
|
||||
|
||||
declare class CaptchaRequiredException extends ScriptException {
|
||||
constructor(url: string, body: string);
|
||||
}
|
||||
|
||||
declare class CriticalException extends ScriptException {
|
||||
constructor(msg: string);
|
||||
}
|
||||
|
||||
declare class UnavailableException extends ScriptException {
|
||||
constructor(msg: string);
|
||||
}
|
||||
|
||||
declare class AgeException extends ScriptException {
|
||||
constructor(msg: string);
|
||||
}
|
||||
|
||||
declare class TimeoutException extends ScriptException {
|
||||
constructor(msg: string);
|
||||
}
|
||||
|
||||
declare class ScriptImplementationException extends ScriptException {
|
||||
constructor(msg: string);
|
||||
}
|
||||
@@ -38,16 +62,23 @@ declare class FilterCapability {
|
||||
|
||||
|
||||
declare class PlatformAuthorLink {
|
||||
constructor(id: PlatformID, name: string, url: string, thumbnail: string, subscribers: integer?);
|
||||
constructor(id: PlatformID, name: string, url: string, thumbnail: string, subscribers: integer?, membershipUrl: string?);
|
||||
}
|
||||
|
||||
declare class PlatformAuthorMembershipLink {
|
||||
constructor(id: PlatformID, name: string, url: string, thumbnail: string, subscribers: integer?, membershipUrl: string?);
|
||||
}
|
||||
|
||||
declare interface PlatformContentDef {
|
||||
id: PlatformID,
|
||||
name: string,
|
||||
thumbnails: Thumbnails,
|
||||
author: PlatformAuthorLink,
|
||||
datetime: integer,
|
||||
url: string
|
||||
}
|
||||
declare interface PlatformContent {}
|
||||
|
||||
declare interface PlatformNestedMediaContentDef extends PlatformContentDef {
|
||||
contentUrl: string,
|
||||
contentName: string?,
|
||||
@@ -59,16 +90,26 @@ declare class PlatformNestedMediaContent {
|
||||
constructor(obj: PlatformNestedMediaContentDef);
|
||||
}
|
||||
|
||||
declare interface PlatformLockedContentDef extends PlatformContentDef {
|
||||
contentName: string?,
|
||||
contentThumbnails: Thumbnails?,
|
||||
unlockUrl: string,
|
||||
lockDescription: string?,
|
||||
}
|
||||
declare class PlatformLockedContent {
|
||||
constructor(obj: PlatformLockedContentDef);
|
||||
}
|
||||
|
||||
|
||||
declare interface PlatformVideoDef extends PlatformContentDef {
|
||||
thumbnails: Thumbnails,
|
||||
author: PlatformAuthorLink,
|
||||
|
||||
duration: int,
|
||||
viewCount: long,
|
||||
isLive: boolean
|
||||
isLive: boolean,
|
||||
shareUrl: string?
|
||||
}
|
||||
declare interface PlatformContent {}
|
||||
|
||||
declare class PlatformVideo implements PlatformContent {
|
||||
constructor(obj: PlatformVideoDef);
|
||||
}
|
||||
@@ -77,15 +118,16 @@ declare class PlatformVideo implements PlatformContent {
|
||||
declare interface PlatformVideoDetailsDef extends PlatformVideoDef {
|
||||
description: string,
|
||||
video: VideoSourceDescriptor,
|
||||
live: SubtitleSource[],
|
||||
rating: IRating
|
||||
live: IVideoSource,
|
||||
rating: IRating,
|
||||
subtitles: SubtitleSource[]
|
||||
}
|
||||
declare class PlatformVideoDetails extends PlatformVideo {
|
||||
constructor(obj: PlatformVideoDetailsDef);
|
||||
}
|
||||
|
||||
declare class PlatformPostDef extends PlatformContentDef {
|
||||
thumbnails: string[],
|
||||
declare interface PlatformPostDef extends PlatformContentDef {
|
||||
thumbnails: Thumbnails[],
|
||||
images: string[],
|
||||
description: string
|
||||
}
|
||||
@@ -93,7 +135,7 @@ declare class PlatformPost extends PlatformContent {
|
||||
constructor(obj: PlatformPostDef)
|
||||
}
|
||||
|
||||
declare class PlatformPostDetailsDef extends PlatformPostDef {
|
||||
declare interface PlatformPostDetailsDef extends PlatformPostDef {
|
||||
rating: IRating,
|
||||
textType: int,
|
||||
content: String
|
||||
@@ -110,8 +152,8 @@ declare interface MuxVideoSourceDescriptorDef {
|
||||
isUnMuxed: boolean,
|
||||
videoSources: VideoSource[]
|
||||
}
|
||||
declare class MuxVideoSourceDescriptor implements IVideoSourceDescriptor {
|
||||
constructor(obj: VideoSourceDescriptorDef);
|
||||
declare class VideoSourceDescriptor implements IVideoSourceDescriptor {
|
||||
constructor(videoSourcesOrObj: VideoSource[]);
|
||||
}
|
||||
|
||||
declare interface UnMuxVideoSourceDescriptorDef {
|
||||
@@ -129,7 +171,7 @@ declare interface IVideoSource {
|
||||
declare interface IAudioSource {
|
||||
|
||||
}
|
||||
interface VideoUrlSourceDef implements IVideoSource {
|
||||
declare interface VideoUrlSourceDef implements IVideoSource {
|
||||
width: integer,
|
||||
height: integer,
|
||||
container: string,
|
||||
@@ -139,22 +181,22 @@ interface VideoUrlSourceDef implements IVideoSource {
|
||||
duration: integer,
|
||||
url: string
|
||||
}
|
||||
class VideoUrlSource {
|
||||
declare class VideoUrlSource {
|
||||
constructor(obj: VideoUrlSourceDef);
|
||||
|
||||
getRequestModifier(): RequestModifier?;
|
||||
}
|
||||
interface VideoUrlRangeSourceDef extends VideoUrlSource {
|
||||
declare interface VideoUrlRangeSourceDef extends VideoUrlSource {
|
||||
itagId: integer,
|
||||
initStart: integer,
|
||||
initEnd: integer,
|
||||
indexStart: integer,
|
||||
indexEnd: integer,
|
||||
}
|
||||
class VideoUrlRangeSource extends VideoUrlSource {
|
||||
declare class VideoUrlRangeSource extends VideoUrlSource {
|
||||
constructor(obj: YTVideoSourceDef);
|
||||
}
|
||||
interface AudioUrlSourceDef {
|
||||
declare interface AudioUrlSourceDef {
|
||||
name: string,
|
||||
bitrate: integer,
|
||||
container: string,
|
||||
@@ -163,24 +205,12 @@ interface AudioUrlSourceDef {
|
||||
url: string,
|
||||
language: string
|
||||
}
|
||||
class AudioUrlSource implements IAudioSource {
|
||||
declare class AudioUrlSource implements IAudioSource {
|
||||
constructor(obj: AudioUrlSourceDef);
|
||||
|
||||
getRequestModifier(): RequestModifier?;
|
||||
}
|
||||
interface IRequest {
|
||||
url: string,
|
||||
headers: Map<string, string>
|
||||
}
|
||||
interface IRequestModifierDef {
|
||||
allowByteSkip: boolean
|
||||
}
|
||||
class RequestModifier {
|
||||
constructor(obj: IRequestModifierDef) { }
|
||||
|
||||
modifyRequest(url: string, headers: Map<string, string>): IRequest;
|
||||
}
|
||||
interface AudioUrlRangeSourceDef extends AudioUrlSource {
|
||||
declare interface AudioUrlRangeSourceDef extends AudioUrlSource {
|
||||
itagId: integer,
|
||||
initStart: integer,
|
||||
initEnd: integer,
|
||||
@@ -188,28 +218,44 @@ interface AudioUrlRangeSourceDef extends AudioUrlSource {
|
||||
indexEnd: integer,
|
||||
audioChannels: integer
|
||||
}
|
||||
class AudioUrlRangeSource extends AudioUrlSource {
|
||||
declare class AudioUrlRangeSource extends AudioUrlSource {
|
||||
constructor(obj: AudioUrlRangeSourceDef);
|
||||
}
|
||||
interface HLSSourceDef {
|
||||
declare interface HLSSourceDef {
|
||||
name: string,
|
||||
duration: integer,
|
||||
url: string
|
||||
url: string,
|
||||
priority: boolean?,
|
||||
language: string?
|
||||
}
|
||||
class HLSSource implements IVideoSource {
|
||||
declare class HLSSource implements IVideoSource {
|
||||
constructor(obj: HLSSourceDef);
|
||||
}
|
||||
interface DashSourceDef {
|
||||
declare interface DashSourceDef {
|
||||
name: string,
|
||||
duration: integer,
|
||||
url: string
|
||||
url: string,
|
||||
language: string?
|
||||
}
|
||||
class DashSource implements IVideoSource {
|
||||
declare class DashSource implements IVideoSource {
|
||||
constructor(obj: DashSourceDef)
|
||||
}
|
||||
|
||||
declare interface IRequest {
|
||||
url: string,
|
||||
headers: Map<string, string>
|
||||
}
|
||||
declare interface IRequestModifierDef {
|
||||
allowByteSkip: boolean
|
||||
}
|
||||
declare class RequestModifier {
|
||||
constructor(obj: IRequestModifierDef) { }
|
||||
|
||||
modifyRequest(url: string, headers: Map<string, string>): IRequest;
|
||||
}
|
||||
|
||||
//Channel
|
||||
interface PlatformChannelDef {
|
||||
declare interface PlatformChannelDef {
|
||||
id: PlatformID,
|
||||
name: string,
|
||||
thumbnail: string,
|
||||
@@ -217,12 +263,29 @@ interface PlatformChannelDef {
|
||||
subscribers: integer,
|
||||
description: string,
|
||||
url: string,
|
||||
urlAlternatives: string[],
|
||||
links: Map<string>?
|
||||
}
|
||||
class PlatformChannel {
|
||||
declare class PlatformChannel {
|
||||
constructor(obj: PlatformChannelDef);
|
||||
}
|
||||
|
||||
//Playlist
|
||||
declare interface PlatformPlaylistDef implements PlatformContent {
|
||||
videoCount: integer,
|
||||
thumbnail: string
|
||||
}
|
||||
declare class PlatformPlaylist extends PlatformContent {
|
||||
constructor(obj: PlatformPlaylistDef);
|
||||
}
|
||||
declare interface PlatformPlaylistDetailsDef implements PlatformPlaylistDef {
|
||||
contents: ContentPager
|
||||
}
|
||||
declare class PlatformPlaylistDetails extends PlatformContent {
|
||||
constructor(obj: PlatformPlaylistDetailsDef);
|
||||
}
|
||||
|
||||
|
||||
//Ratings
|
||||
interface IRating {
|
||||
type: integer
|
||||
@@ -250,7 +313,11 @@ declare class PlatformComment {
|
||||
constructor(obj: CommentDef);
|
||||
}
|
||||
|
||||
declare class PlaybackTracker {
|
||||
constructor(interval: integer);
|
||||
|
||||
setProgress(seconds: integer);
|
||||
}
|
||||
|
||||
declare class LiveEventPager {
|
||||
nextRequest = 4000;
|
||||
@@ -261,8 +328,8 @@ declare class LiveEventPager {
|
||||
nextPage(): LiveEventPager; //Could be self
|
||||
}
|
||||
|
||||
class LiveEvent {
|
||||
type: String
|
||||
declare class LiveEvent {
|
||||
constructor(type: integer);
|
||||
}
|
||||
declare class LiveEventComment extends LiveEvent {
|
||||
constructor(name: string, message: string, thumbnail: string?, colorName: string?, badges: string[]);
|
||||
@@ -287,25 +354,31 @@ declare class ContentPager {
|
||||
constructor(results: PlatformContent[], hasMore: boolean);
|
||||
|
||||
hasMorePagers(): boolean
|
||||
nextPage(): VideoPager; //Could be self
|
||||
nextPage(): ContentPager?; //Could be self
|
||||
}
|
||||
declare class VideoPager {
|
||||
constructor(results: PlatformVideo[], hasMore: boolean);
|
||||
|
||||
hasMorePagers(): boolean
|
||||
nextPage(): VideoPager; //Could be self
|
||||
nextPage(): VideoPager?; //Could be self
|
||||
}
|
||||
declare class ChannelPager {
|
||||
constructor(results: PlatformChannel[], hasMore: boolean);
|
||||
|
||||
hasMorePagers(): boolean;
|
||||
nextPage(): ChannelPager; //Could be self
|
||||
nextPage(): ChannelPager?; //Could be self
|
||||
}
|
||||
declare class PlaylistPager {
|
||||
constructor(results: PlatformPlaylist[], hasMore: boolean);
|
||||
|
||||
hasMorePagers(): boolean;
|
||||
nextPage(): PlaylistPager?;
|
||||
}
|
||||
declare class CommentPager {
|
||||
constructor(results: PlatformComment[], hasMore: boolean);
|
||||
|
||||
hasMorePagers(): boolean
|
||||
nextPage(): CommentPager; //Could be self
|
||||
nextPage(): CommentPager?; //Could be self
|
||||
}
|
||||
|
||||
interface Map<T> {
|
||||
@@ -341,8 +414,9 @@ interface Source {
|
||||
getChannelCapabilities(): ResultCapabilities;
|
||||
|
||||
isContentDetailsUrl(url: string): boolean;
|
||||
getContentDetails(url: string): PlatformVideoDetails;
|
||||
getContentDetails(url: string): PlatformContentDetails;
|
||||
|
||||
//Optional
|
||||
getLiveEvents(url: string): LiveEventPager;
|
||||
|
||||
//Optional
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -11,7 +11,8 @@ let Type = {
|
||||
Streams: "STREAMS",
|
||||
Mixed: "MIXED",
|
||||
Live: "LIVE",
|
||||
Subscriptions: "SUBSCRIPTIONS"
|
||||
Subscriptions: "SUBSCRIPTIONS",
|
||||
Shorts: "SHORTS"
|
||||
},
|
||||
Order: {
|
||||
Chronological: "CHRONOLOGICAL"
|
||||
@@ -31,7 +32,8 @@ let Type = {
|
||||
Text: {
|
||||
RAW: 0,
|
||||
HTML: 1,
|
||||
MARKUP: 2
|
||||
MARKUP: 2,
|
||||
CODE: 3
|
||||
},
|
||||
Chapter: {
|
||||
NORMAL: 0,
|
||||
@@ -78,6 +80,11 @@ class ScriptLoginRequiredException extends ScriptException {
|
||||
super("ScriptLoginRequiredException", msg);
|
||||
}
|
||||
}
|
||||
class LoginRequiredException extends ScriptException {
|
||||
constructor(msg) {
|
||||
super("ScriptLoginRequiredException", msg);
|
||||
}
|
||||
}
|
||||
class CaptchaRequiredException extends Error {
|
||||
constructor(url, body) {
|
||||
super(JSON.stringify({ 'plugin_type': 'CaptchaRequiredException', url, body }));
|
||||
@@ -96,6 +103,12 @@ class UnavailableException extends ScriptException {
|
||||
super("UnavailableException", msg);
|
||||
}
|
||||
}
|
||||
class ReloadRequiredException extends ScriptException {
|
||||
constructor(msg, reloadData) {
|
||||
super("ReloadRequiredException", msg);
|
||||
this.reloadData = reloadData;
|
||||
}
|
||||
}
|
||||
class AgeException extends ScriptException {
|
||||
constructor(msg) {
|
||||
super("AgeException", msg);
|
||||
@@ -196,7 +209,7 @@ class PlatformContent {
|
||||
obj = obj ?? {};
|
||||
this.id = obj.id ?? PlatformID(); //PlatformID
|
||||
this.name = obj.name ?? ""; //string
|
||||
this.thumbnails = obj.thumbnails; //Thumbnail[]
|
||||
this.thumbnails = obj.thumbnails ?? new Thumbnails([]); //Thumbnail[]
|
||||
this.author = obj.author; //PlatformAuthorLink
|
||||
this.datetime = obj.datetime ?? obj.uploadDate ?? 0; //OffsetDateTime (Long)
|
||||
this.url = obj.url ?? ""; //String
|
||||
@@ -239,6 +252,7 @@ class PlatformVideo extends PlatformContent {
|
||||
this.viewCount = obj.viewCount ?? -1; //Long
|
||||
|
||||
this.isLive = obj.isLive ?? false; //Boolean
|
||||
this.isShort = !!obj.isShort ?? false;
|
||||
}
|
||||
}
|
||||
class PlatformVideoDetails extends PlatformVideo {
|
||||
@@ -249,12 +263,17 @@ class PlatformVideoDetails extends PlatformVideo {
|
||||
|
||||
this.description = obj.description ?? "";//String
|
||||
this.video = obj.video ?? {}; //VideoSourceDescriptor
|
||||
this.dash = obj.dash ?? null; //DashSource
|
||||
this.hls = obj.hls ?? null; //HLSSource
|
||||
this.dash = obj.dash ?? null; //DashSource, deprecated
|
||||
this.hls = obj.hls ?? null; //HLSSource, deprecated
|
||||
this.live = obj.live ?? null; //VideoSource
|
||||
|
||||
this.rating = obj.rating ?? null; //IRating
|
||||
this.subtitles = obj.subtitles ?? [];
|
||||
this.isShort = !!obj.isShort ?? false;
|
||||
|
||||
if (obj.getContentRecommendations) {
|
||||
this.getContentRecommendations = obj.getContentRecommendations
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,12 +292,81 @@ class PlatformPostDetails extends PlatformPost {
|
||||
super(obj);
|
||||
obj = obj ?? {};
|
||||
this.plugin_type = "PlatformPostDetails";
|
||||
this.rating = obj.rating ?? RatingLikes(-1);
|
||||
this.rating = obj.rating ?? new RatingLikes(-1);
|
||||
this.textType = obj.textType ?? 0;
|
||||
this.content = obj.content ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
class PlatformWeb extends PlatformContent {
|
||||
constructor(obj) {
|
||||
super(obj, 7);
|
||||
obj = obj ?? {};
|
||||
this.plugin_type = "PlatformWeb";
|
||||
}
|
||||
}
|
||||
class PlatformWebDetails extends PlatformWeb {
|
||||
constructor(obj) {
|
||||
super(obj, 7);
|
||||
obj = obj ?? {};
|
||||
this.plugin_type = "PlatformWebDetails";
|
||||
this.html = obj.html;
|
||||
}
|
||||
}
|
||||
|
||||
class PlatformArticle extends PlatformContent {
|
||||
constructor(obj) {
|
||||
super(obj, 3);
|
||||
obj = obj ?? {};
|
||||
this.plugin_type = "PlatformArticle";
|
||||
this.rating = obj.rating ?? new RatingLikes(-1);
|
||||
this.summary = obj.summary ?? "";
|
||||
this.thumbnails = obj.thumbnails ?? new Thumbnails([]);
|
||||
}
|
||||
}
|
||||
class PlatformArticleDetails extends PlatformArticle {
|
||||
constructor(obj) {
|
||||
super(obj, 3);
|
||||
obj = obj ?? {};
|
||||
this.plugin_type = "PlatformArticleDetails";
|
||||
this.rating = obj.rating ?? new RatingLikes(-1);
|
||||
this.segments = obj.segments ?? [];
|
||||
}
|
||||
}
|
||||
class ArticleSegment {
|
||||
constructor(type) {
|
||||
this.type = type;
|
||||
}
|
||||
}
|
||||
class ArticleTextSegment extends ArticleSegment {
|
||||
constructor(content, textType) {
|
||||
super(1);
|
||||
this.textType = textType;
|
||||
this.content = content;
|
||||
}
|
||||
}
|
||||
class ArticleImagesSegment extends ArticleSegment {
|
||||
constructor(images, caption) {
|
||||
super(2);
|
||||
this.images = images;
|
||||
this.caption = caption;
|
||||
}
|
||||
}
|
||||
class ArticleHeaderSegment extends ArticleSegment {
|
||||
constructor(content, level) {
|
||||
super(3);
|
||||
this.level = level;
|
||||
this.content = content;
|
||||
}
|
||||
}
|
||||
class ArticleNestedSegment extends ArticleSegment {
|
||||
constructor(nested) {
|
||||
super(9);
|
||||
this.nested = nested;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//Sources
|
||||
class VideoSourceDescriptor {
|
||||
constructor(obj) {
|
||||
@@ -321,6 +409,18 @@ class VideoUrlSource {
|
||||
this.bitrate = obj.bitrate ?? 0;
|
||||
this.duration = obj.duration ?? 0;
|
||||
this.url = obj.url;
|
||||
if(obj.requestModifier)
|
||||
this.requestModifier = obj.requestModifier;
|
||||
}
|
||||
}
|
||||
class VideoUrlWidevineSource extends VideoUrlSource {
|
||||
constructor(obj) {
|
||||
super(obj);
|
||||
this.plugin_type = "VideoUrlWidevineSource";
|
||||
|
||||
this.licenseUri = obj.licenseUri;
|
||||
if(obj.getLicenseRequestExecutor)
|
||||
this.getLicenseRequestExecutor = obj.getLicenseRequestExecutor;
|
||||
}
|
||||
}
|
||||
class VideoUrlRangeSource extends VideoUrlSource {
|
||||
@@ -346,6 +446,35 @@ class AudioUrlSource {
|
||||
this.duration = obj.duration ?? 0;
|
||||
this.url = obj.url;
|
||||
this.language = obj.language ?? Language.UNKNOWN;
|
||||
if(obj.requestModifier)
|
||||
this.requestModifier = obj.requestModifier;
|
||||
}
|
||||
}
|
||||
class AudioUrlWidevineSource extends AudioUrlSource {
|
||||
constructor(obj) {
|
||||
super(obj);
|
||||
this.plugin_type = "AudioUrlWidevineSource";
|
||||
|
||||
this.licenseUri = obj.licenseUri;
|
||||
if(obj.getLicenseRequestExecutor)
|
||||
this.getLicenseRequestExecutor = obj.getLicenseRequestExecutor;
|
||||
|
||||
// deprecated api conversion
|
||||
if(obj.bearerToken) {
|
||||
this.getLicenseRequestExecutor = () => {
|
||||
return {
|
||||
executeRequest: (url, _headers, _method, license_request_data) => {
|
||||
return http.POST(
|
||||
url,
|
||||
license_request_data,
|
||||
{ Authorization: `Bearer ${obj.bearerToken}` },
|
||||
false,
|
||||
true
|
||||
).body
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
class AudioUrlRangeSource extends AudioUrlSource {
|
||||
@@ -371,6 +500,8 @@ class HLSSource {
|
||||
this.priority = obj.priority ?? false;
|
||||
if(obj.language)
|
||||
this.language = obj.language;
|
||||
if(obj.requestModifier)
|
||||
this.requestModifier = obj.requestModifier;
|
||||
}
|
||||
}
|
||||
class DashSource {
|
||||
@@ -382,13 +513,58 @@ class DashSource {
|
||||
this.url = obj.url;
|
||||
if(obj.language)
|
||||
this.language = obj.language;
|
||||
if(obj.requestModifier)
|
||||
this.requestModifier = obj.requestModifier;
|
||||
}
|
||||
}
|
||||
class DashWidevineSource extends DashSource {
|
||||
constructor(obj) {
|
||||
super(obj);
|
||||
this.plugin_type = "DashWidevineSource";
|
||||
|
||||
this.licenseUri = obj.licenseUri;
|
||||
if(obj.getLicenseRequestExecutor)
|
||||
this.getLicenseRequestExecutor = obj.getLicenseRequestExecutor;
|
||||
}
|
||||
}
|
||||
class DashManifestRawSource {
|
||||
constructor(obj) {
|
||||
obj = obj ?? {};
|
||||
this.plugin_type = "DashRawSource";
|
||||
this.name = obj.name ?? "";
|
||||
this.bitrate = obj.bitrate ?? 0;
|
||||
this.container = obj.container ?? "";
|
||||
this.codec = obj.codec ?? "";
|
||||
this.duration = obj.duration ?? 0;
|
||||
this.url = obj.url;
|
||||
this.language = obj.language ?? Language.UNKNOWN;
|
||||
if(obj.requestModifier)
|
||||
this.requestModifier = obj.requestModifier;
|
||||
}
|
||||
}
|
||||
|
||||
class DashManifestRawAudioSource {
|
||||
constructor(obj) {
|
||||
obj = obj ?? {};
|
||||
this.plugin_type = "DashRawAudioSource";
|
||||
this.name = obj.name ?? "";
|
||||
this.bitrate = obj.bitrate ?? 0;
|
||||
this.container = obj.container ?? "";
|
||||
this.codec = obj.codec ?? "";
|
||||
this.duration = obj.duration ?? 0;
|
||||
this.url = obj.url;
|
||||
this.language = obj.language ?? Language.UNKNOWN;
|
||||
this.manifest = obj.manifest ?? null;
|
||||
if(obj.requestModifier)
|
||||
this.requestModifier = obj.requestModifier;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class RequestModifier {
|
||||
constructor(obj) {
|
||||
obj = obj ?? {};
|
||||
this.allowByteSkip = obj.allowByteSkip;
|
||||
this.allowByteSkip = obj.allowByteSkip; //Kinda deprecated.. wip
|
||||
}
|
||||
}
|
||||
|
||||
@@ -414,7 +590,7 @@ class PlatformPlaylist extends PlatformContent {
|
||||
constructor(obj) {
|
||||
super(obj, 4);
|
||||
this.plugin_type = "PlatformPlaylist";
|
||||
this.videoCount = obj.videoCount ?? 0;
|
||||
this.videoCount = obj.videoCount ?? -1;
|
||||
this.thumbnail = obj.thumbnail;
|
||||
}
|
||||
}
|
||||
@@ -458,6 +634,8 @@ class PlatformComment {
|
||||
this.date = obj.date ?? 0;
|
||||
this.replyCount = obj.replyCount ?? 0;
|
||||
this.context = obj.context ?? {};
|
||||
if(obj.getReplies)
|
||||
this.getReplies = obj.getReplies;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -740,3 +918,99 @@ class URLSearchParams {
|
||||
return searchString;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var __REGEX_SPACE_CHARACTERS = /<%= spaceCharacters %>/g;
|
||||
var __btoa_TABLE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
||||
function btoa(input) {
|
||||
input = String(input);
|
||||
if (/[^\0-\xFF]/.test(input)) {
|
||||
// Note: no need to special-case astral symbols here, as surrogates are
|
||||
// matched, and the input is supposed to only contain ASCII anyway.
|
||||
error(
|
||||
'The string to be encoded contains characters outside of the ' +
|
||||
'Latin1 range.'
|
||||
);
|
||||
}
|
||||
var padding = input.length % 3;
|
||||
var output = '';
|
||||
var position = -1;
|
||||
var a;
|
||||
var b;
|
||||
var c;
|
||||
var buffer;
|
||||
// Make sure any padding is handled outside of the loop.
|
||||
var length = input.length - padding;
|
||||
|
||||
while (++position < length) {
|
||||
// Read three bytes, i.e. 24 bits.
|
||||
a = input.charCodeAt(position) << 16;
|
||||
b = input.charCodeAt(++position) << 8;
|
||||
c = input.charCodeAt(++position);
|
||||
buffer = a + b + c;
|
||||
// Turn the 24 bits into four chunks of 6 bits each, and append the
|
||||
// matching character for each of them to the output.
|
||||
output += (
|
||||
__btoa_TABLE.charAt(buffer >> 18 & 0x3F) +
|
||||
__btoa_TABLE.charAt(buffer >> 12 & 0x3F) +
|
||||
__btoa_TABLE.charAt(buffer >> 6 & 0x3F) +
|
||||
__btoa_TABLE.charAt(buffer & 0x3F)
|
||||
);
|
||||
}
|
||||
|
||||
if (padding == 2) {
|
||||
a = input.charCodeAt(position) << 8;
|
||||
b = input.charCodeAt(++position);
|
||||
buffer = a + b;
|
||||
output += (
|
||||
__btoa_TABLE.charAt(buffer >> 10) +
|
||||
__btoa_TABLE.charAt((buffer >> 4) & 0x3F) +
|
||||
__btoa_TABLE.charAt((buffer << 2) & 0x3F) +
|
||||
'='
|
||||
);
|
||||
} else if (padding == 1) {
|
||||
buffer = input.charCodeAt(position);
|
||||
output += (
|
||||
__btoa_TABLE.charAt(buffer >> 2) +
|
||||
__btoa_TABLE.charAt((buffer << 4) & 0x3F) +
|
||||
'=='
|
||||
);
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
function atob(input) {
|
||||
input = String(input)
|
||||
.replace(__REGEX_SPACE_CHARACTERS, '');
|
||||
var length = input.length;
|
||||
if (length % 4 == 0) {
|
||||
input = input.replace(/==?$/, '');
|
||||
length = input.length;
|
||||
}
|
||||
if (
|
||||
length % 4 == 1 ||
|
||||
// http://whatwg.org/C#alphanumeric-ascii-characters
|
||||
/[^+a-zA-Z0-9/]/.test(input)
|
||||
) {
|
||||
error(
|
||||
'Invalid character: the string to be decoded is not correctly encoded.'
|
||||
);
|
||||
}
|
||||
var bitCounter = 0;
|
||||
var bitStorage;
|
||||
var buffer;
|
||||
var output = '';
|
||||
var position = -1;
|
||||
while (++position < length) {
|
||||
buffer = __btoa_TABLE.indexOf(input.charAt(position));
|
||||
bitStorage = bitCounter % 4 ? bitStorage * 64 + buffer : buffer;
|
||||
// Unless this is the first of a group of 4 characters…
|
||||
if (bitCounter++ % 4) {
|
||||
// …convert the first 8 bits to a single ASCII character.
|
||||
output += String.fromCharCode(
|
||||
0xFF & bitStorage >> (-2 * bitCounter & 6)
|
||||
);
|
||||
}
|
||||
}
|
||||
return output;
|
||||
};
|
||||
|
||||
@@ -18,7 +18,10 @@ fun IAudioSource.isDownloadable(): Boolean = VideoHelper.isDownloadable(this);
|
||||
@UnstableApi
|
||||
fun JSSource.getHttpDataSourceFactory(): HttpDataSource.Factory {
|
||||
val requestModifier = getRequestModifier();
|
||||
return if (requestModifier != null) {
|
||||
val requestExecutor = getRequestExecutor();
|
||||
return if (requestExecutor != null) {
|
||||
JSHttpDataSource.Factory().setRequestExecutor(requestExecutor);
|
||||
} else if (requestModifier != null) {
|
||||
JSHttpDataSource.Factory().setRequestModifier(requestModifier);
|
||||
} else {
|
||||
DefaultHttpDataSource.Factory();
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,15 +1,16 @@
|
||||
package com.futo.platformplayer
|
||||
|
||||
import android.util.Log
|
||||
import com.google.common.base.CharMatcher
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.net.Inet4Address
|
||||
import java.net.Inet6Address
|
||||
import java.net.InetAddress
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Socket
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.charset.Charset
|
||||
|
||||
|
||||
private const val IPV4_PART_COUNT = 4;
|
||||
@@ -215,23 +216,36 @@ private fun ByteArray.toInetAddress(): InetAddress {
|
||||
return InetAddress.getByAddress(this);
|
||||
}
|
||||
|
||||
fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? {
|
||||
fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int): Socket? {
|
||||
ensureNotMainThread()
|
||||
|
||||
val timeout = 10000
|
||||
val addresses = if(!Settings.instance.casting.allowIpv6) attemptAddresses.filterIsInstance<Inet4Address>() else attemptAddresses;
|
||||
if(addresses.isEmpty())
|
||||
throw IllegalStateException("No valid addresses found (ipv6: ${(if(Settings.instance.casting.allowIpv6) "enabled" else "disabled")})");
|
||||
|
||||
if (addresses.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (addresses.size == 1) {
|
||||
val socket = Socket()
|
||||
|
||||
try {
|
||||
return Socket(addresses[0], port);
|
||||
return socket.apply { this.connect(InetSocketAddress(addresses[0], port), timeout) }
|
||||
} catch (e: Throwable) {
|
||||
//Ignored.
|
||||
Log.i("getConnectedSocket", "Failed to connect to: ${addresses[0]}", e)
|
||||
socket.close()
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
val sortedAddresses: List<InetAddress> = addresses
|
||||
.sortedBy { addr -> addressScore(addr) }
|
||||
|
||||
val sockets: ArrayList<Socket> = arrayListOf();
|
||||
for (i in addresses.indices) {
|
||||
for (i in sortedAddresses.indices) {
|
||||
sockets.add(Socket());
|
||||
}
|
||||
|
||||
@@ -239,7 +253,7 @@ fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? {
|
||||
var connectedSocket: Socket? = null;
|
||||
val threads: ArrayList<Thread> = arrayListOf();
|
||||
for (i in 0 until sockets.size) {
|
||||
val address = addresses[i];
|
||||
val address = sortedAddresses[i];
|
||||
val socket = sockets[i];
|
||||
val thread = Thread {
|
||||
try {
|
||||
@@ -249,7 +263,7 @@ fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? {
|
||||
}
|
||||
}
|
||||
|
||||
socket.connect(InetSocketAddress(address, port));
|
||||
socket.connect(InetSocketAddress(address, port), timeout);
|
||||
|
||||
synchronized(syncObject) {
|
||||
if (connectedSocket == null) {
|
||||
@@ -263,7 +277,7 @@ fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? {
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
//Ignore
|
||||
Log.i("getConnectedSocket", "Failed to connect to: $address", e)
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package com.futo.platformplayer
|
||||
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.states.AnnouncementType
|
||||
import com.futo.platformplayer.states.StateAnnouncement
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.polycentric.core.ProcessHandle
|
||||
import com.futo.polycentric.core.Store
|
||||
import com.futo.polycentric.core.SystemState
|
||||
import com.futo.polycentric.core.base64UrlToByteArray
|
||||
import userpackage.Protocol
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.min
|
||||
@@ -40,36 +40,25 @@ fun Protocol.ImageBundle?.selectHighestResolutionImage(): Protocol.ImageManifest
|
||||
return imageManifestsList.filter { it.byteCount < maximumFileSize }.maxByOrNull { abs(it.width * it.height) }
|
||||
}
|
||||
|
||||
fun String.getDataLinkFromUrl(): Protocol.URLInfoDataLink? {
|
||||
val urlData = if (this.startsWith("polycentric://")) {
|
||||
this.substring("polycentric://".length)
|
||||
} else this;
|
||||
|
||||
val urlBytes = urlData.base64UrlToByteArray();
|
||||
val urlInfo = Protocol.URLInfo.parseFrom(urlBytes);
|
||||
if (urlInfo.urlType != 4L) {
|
||||
return null
|
||||
}
|
||||
|
||||
val dataLink = Protocol.URLInfoDataLink.parseFrom(urlInfo.body);
|
||||
return dataLink
|
||||
}
|
||||
|
||||
fun Protocol.Claim.resolveChannelUrl(): String? {
|
||||
return StatePlatform.instance.resolveChannelUrlByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) })
|
||||
}
|
||||
|
||||
fun Protocol.Claim.resolveChannelUrls(): List<String> {
|
||||
return StatePlatform.instance.resolveChannelUrlsByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) })
|
||||
}
|
||||
|
||||
suspend fun ProcessHandle.fullyBackfillServersAnnounceExceptions() {
|
||||
val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(system))
|
||||
if (!systemState.servers.contains(PolycentricCache.STAGING_SERVER)) {
|
||||
removeServer(PolycentricCache.STAGING_SERVER)
|
||||
}
|
||||
|
||||
if (!systemState.servers.contains(PolycentricCache.SERVER)) {
|
||||
removeServer(PolycentricCache.SERVER)
|
||||
}
|
||||
|
||||
val exceptions = fullyBackfillServers()
|
||||
for (pair in exceptions) {
|
||||
val server = pair.key
|
||||
val exception = pair.value
|
||||
|
||||
StateAnnouncement.instance.registerAnnouncement(
|
||||
"backfill-failed",
|
||||
"Backfill failed",
|
||||
"Failed to backfill server $server. $exception",
|
||||
AnnouncementType.SESSION_RECURRING
|
||||
);
|
||||
|
||||
Logger.e("Backfill", "Failed to backfill server $server.", exception)
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,15 @@
|
||||
package com.futo.platformplayer
|
||||
|
||||
import android.net.Uri
|
||||
import java.net.Inet4Address
|
||||
import java.net.Inet6Address
|
||||
import java.net.InetAddress
|
||||
import java.net.URI
|
||||
import java.net.URISyntaxException
|
||||
import java.net.URLEncoder
|
||||
import java.time.Instant
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneOffset
|
||||
|
||||
//Syntax sugaring
|
||||
inline fun <reified T> Any.assume(): T?{
|
||||
@@ -25,4 +31,54 @@ fun String?.yesNoToBoolean(): Boolean {
|
||||
|
||||
fun Boolean?.toYesNo(): String {
|
||||
return if (this == true) "YES" else "NO"
|
||||
}
|
||||
|
||||
fun InetAddress?.toUrlAddress(): String {
|
||||
return when (this) {
|
||||
is Inet6Address -> {
|
||||
val hostAddr = this.hostAddress ?: throw Exception("Invalid address: hostAddress is null")
|
||||
val index = hostAddr.indexOf('%')
|
||||
if (index != -1) {
|
||||
val addrPart = hostAddr.substring(0, index)
|
||||
val scopeId = hostAddr.substring(index + 1)
|
||||
"[${addrPart}%25${scopeId}]" // %25 is URL-encoded '%'
|
||||
} else {
|
||||
"[$hostAddr]"
|
||||
}
|
||||
}
|
||||
is Inet4Address -> {
|
||||
this.hostAddress ?: throw Exception("Invalid address: hostAddress is null")
|
||||
}
|
||||
else -> {
|
||||
throw Exception("Invalid address type")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Long?.sToOffsetDateTimeUTC(): OffsetDateTime {
|
||||
if (this == null || this < 0)
|
||||
return OffsetDateTime.MIN
|
||||
if(this > 4070912400)
|
||||
return OffsetDateTime.MAX;
|
||||
return OffsetDateTime.ofInstant(Instant.ofEpochSecond(this), ZoneOffset.UTC)
|
||||
}
|
||||
|
||||
fun Long?.msToOffsetDateTimeUTC(): OffsetDateTime {
|
||||
if (this == null || this < 0)
|
||||
return OffsetDateTime.MIN
|
||||
if(this > 4070912400)
|
||||
return OffsetDateTime.MAX;
|
||||
return OffsetDateTime.ofInstant(Instant.ofEpochMilli(this), ZoneOffset.UTC)
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips a leading zero byte if BigInteger.toByteArray() included it just to indicate a positive sign.
|
||||
* Mirrors C's expectation that BN_bn2bin yields exactly the “minimal” big‐endian representation.
|
||||
*/
|
||||
fun ByteArray.stripLeadingZero(): ByteArray {
|
||||
return if (this.size > 1 && this[0] == 0.toByte()) {
|
||||
this.copyOfRange(1, this.size)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,9 @@ import com.caoccao.javet.values.primitive.*
|
||||
import com.caoccao.javet.values.reference.V8ValueArray
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
|
||||
|
||||
//V8
|
||||
@@ -24,6 +26,10 @@ fun <R> V8Value?.orDefault(default: R, handler: (V8Value)->R): R {
|
||||
return handler(this);
|
||||
}
|
||||
|
||||
inline fun V8Value.getSourcePlugin(): V8Plugin? {
|
||||
return V8Plugin.getPluginFromRuntime(this.v8Runtime);
|
||||
}
|
||||
|
||||
inline fun <reified T> V8Value.expectOrThrow(config: IV8PluginConfig, contextName: String): T {
|
||||
if(this !is T)
|
||||
throw ScriptImplementationException(config, "Expected ${contextName} to be of type ${T::class.simpleName}, but found ${this::class.simpleName}");
|
||||
@@ -89,7 +95,29 @@ inline fun <reified T> V8ValueArray.expectV8Variants(config: IV8PluginConfig, co
|
||||
.map { kv-> kv.second.orNull { it.expectV8Variant<T>(config, contextName + "[${kv.first}]", ) } as T };
|
||||
}
|
||||
|
||||
inline fun V8Plugin.ensureIsBusy() {
|
||||
this.let {
|
||||
if (!it.isThreadAlreadyBusy()) {
|
||||
//throw IllegalStateException("Tried to access V8Plugin without busy");
|
||||
val stacktrace = Thread.currentThread().stackTrace;
|
||||
Logger.w("Extensions_V8",
|
||||
"V8 USE OUTSIDE BUSY: " + stacktrace.drop(3)?.firstOrNull().toString() +
|
||||
", " + stacktrace.drop(4)?.firstOrNull().toString() +
|
||||
", " + stacktrace.drop(5)?.firstOrNull()?.toString() +
|
||||
", " + stacktrace.drop(6)?.firstOrNull()?.toString()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
inline fun V8Value.ensureIsBusy() {
|
||||
this?.getSourcePlugin()?.let {
|
||||
it.ensureIsBusy();
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified T> V8Value.expectV8Variant(config: IV8PluginConfig, contextName: String): T {
|
||||
if(false)
|
||||
ensureIsBusy();
|
||||
return when(T::class) {
|
||||
String::class -> this.expectOrThrow<V8ValueString>(config, contextName).value as T;
|
||||
Int::class -> {
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
package com.futo.platformplayer
|
||||
|
||||
import com.google.common.base.Preconditions
|
||||
import com.google.common.io.ByteStreams
|
||||
import com.google.common.primitives.Ints
|
||||
import com.google.common.primitives.Longs
|
||||
import java.io.DataInput
|
||||
import java.io.DataInputStream
|
||||
import java.io.EOFException
|
||||
import java.io.FilterInputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
|
||||
class LittleEndianDataInputStream
|
||||
/**
|
||||
* Creates a `LittleEndianDataInputStream` that wraps the given stream.
|
||||
*
|
||||
* @param in the stream to delegate to
|
||||
*/
|
||||
(`in`: InputStream?) : FilterInputStream(Preconditions.checkNotNull(`in`)), DataInput {
|
||||
/** This method will throw an [UnsupportedOperationException]. */
|
||||
override fun readLine(): String {
|
||||
throw UnsupportedOperationException("readLine is not supported")
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun readFully(b: ByteArray) {
|
||||
ByteStreams.readFully(this, b)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun readFully(b: ByteArray, off: Int, len: Int) {
|
||||
ByteStreams.readFully(this, b, off, len)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun skipBytes(n: Int): Int {
|
||||
return `in`.skip(n.toLong()).toInt()
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun readUnsignedByte(): Int {
|
||||
val b1 = `in`.read()
|
||||
if (0 > b1) {
|
||||
throw EOFException()
|
||||
}
|
||||
|
||||
return b1
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads an unsigned `short` as specified by [DataInputStream.readUnsignedShort],
|
||||
* except using little-endian byte order.
|
||||
*
|
||||
* @return the next two bytes of the input stream, interpreted as an unsigned 16-bit integer in
|
||||
* little-endian byte order
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
override fun readUnsignedShort(): Int {
|
||||
val b1 = readAndCheckByte()
|
||||
val b2 = readAndCheckByte()
|
||||
|
||||
return Ints.fromBytes(0.toByte(), 0.toByte(), b2, b1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads an integer as specified by [DataInputStream.readInt], except using little-endian
|
||||
* byte order.
|
||||
*
|
||||
* @return the next four bytes of the input stream, interpreted as an `int` in little-endian
|
||||
* byte order
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
override fun readInt(): Int {
|
||||
val b1 = readAndCheckByte()
|
||||
val b2 = readAndCheckByte()
|
||||
val b3 = readAndCheckByte()
|
||||
val b4 = readAndCheckByte()
|
||||
|
||||
return Ints.fromBytes(b4, b3, b2, b1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a `long` as specified by [DataInputStream.readLong], except using
|
||||
* little-endian byte order.
|
||||
*
|
||||
* @return the next eight bytes of the input stream, interpreted as a `long` in
|
||||
* little-endian byte order
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
override fun readLong(): Long {
|
||||
val b1 = readAndCheckByte()
|
||||
val b2 = readAndCheckByte()
|
||||
val b3 = readAndCheckByte()
|
||||
val b4 = readAndCheckByte()
|
||||
val b5 = readAndCheckByte()
|
||||
val b6 = readAndCheckByte()
|
||||
val b7 = readAndCheckByte()
|
||||
val b8 = readAndCheckByte()
|
||||
|
||||
return Longs.fromBytes(b8, b7, b6, b5, b4, b3, b2, b1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a `float` as specified by [DataInputStream.readFloat], except using
|
||||
* little-endian byte order.
|
||||
*
|
||||
* @return the next four bytes of the input stream, interpreted as a `float` in
|
||||
* little-endian byte order
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
override fun readFloat(): Float {
|
||||
return java.lang.Float.intBitsToFloat(readInt())
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a `double` as specified by [DataInputStream.readDouble], except using
|
||||
* little-endian byte order.
|
||||
*
|
||||
* @return the next eight bytes of the input stream, interpreted as a `double` in
|
||||
* little-endian byte order
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
override fun readDouble(): Double {
|
||||
return java.lang.Double.longBitsToDouble(readLong())
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun readUTF(): String {
|
||||
return DataInputStream(`in`).readUTF()
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a `short` as specified by [DataInputStream.readShort], except using
|
||||
* little-endian byte order.
|
||||
*
|
||||
* @return the next two bytes of the input stream, interpreted as a `short` in little-endian
|
||||
* byte order.
|
||||
* @throws IOException if an I/O error occurs.
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
override fun readShort(): Short {
|
||||
return readUnsignedShort().toShort()
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a char as specified by [DataInputStream.readChar], except using little-endian
|
||||
* byte order.
|
||||
*
|
||||
* @return the next two bytes of the input stream, interpreted as a `char` in little-endian
|
||||
* byte order
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
override fun readChar(): Char {
|
||||
return readUnsignedShort().toChar()
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun readByte(): Byte {
|
||||
return readUnsignedByte().toByte()
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun readBoolean(): Boolean {
|
||||
return readUnsignedByte() != 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a byte from the input stream checking that the end of file (EOF) has not been
|
||||
* encountered.
|
||||
*
|
||||
* @return byte read from input
|
||||
* @throws IOException if an error is encountered while reading
|
||||
* @throws EOFException if the end of file (EOF) is encountered.
|
||||
*/
|
||||
@Throws(IOException::class, EOFException::class)
|
||||
private fun readAndCheckByte(): Byte {
|
||||
val b1 = `in`.read()
|
||||
|
||||
if (-1 == b1) {
|
||||
throw EOFException()
|
||||
}
|
||||
|
||||
return b1.toByte()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package com.futo.platformplayer
|
||||
|
||||
import com.google.common.base.Preconditions
|
||||
import com.google.common.primitives.Longs
|
||||
import java.io.*
|
||||
|
||||
class LittleEndianDataOutputStream
|
||||
/**
|
||||
* Creates a `LittleEndianDataOutputStream` that wraps the given stream.
|
||||
*
|
||||
* @param out the stream to delegate to
|
||||
*/
|
||||
(out: OutputStream?) : FilterOutputStream(DataOutputStream(Preconditions.checkNotNull(out))),
|
||||
DataOutput {
|
||||
@Throws(IOException::class)
|
||||
override fun write(b: ByteArray, off: Int, len: Int) {
|
||||
// Override slow FilterOutputStream impl
|
||||
out.write(b, off, len)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun writeBoolean(v: Boolean) {
|
||||
(out as DataOutputStream).writeBoolean(v)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun writeByte(v: Int) {
|
||||
(out as DataOutputStream).writeByte(v)
|
||||
}
|
||||
|
||||
@Deprecated(
|
||||
"""The semantics of {@code writeBytes(String s)} are considered dangerous. Please use
|
||||
{@link #writeUTF(String s)}, {@link #writeChars(String s)} or another write method instead."""
|
||||
)
|
||||
@Throws(
|
||||
IOException::class
|
||||
)
|
||||
override fun writeBytes(s: String) {
|
||||
(out as DataOutputStream).writeBytes(s)
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a char as specified by [DataOutputStream.writeChar], except using
|
||||
* little-endian byte order.
|
||||
*
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
override fun writeChar(v: Int) {
|
||||
writeShort(v)
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a `String` as specified by [DataOutputStream.writeChars], except
|
||||
* each character is written using little-endian byte order.
|
||||
*
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
override fun writeChars(s: String) {
|
||||
for (i in 0 until s.length) {
|
||||
writeChar(s[i].code)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a `double` as specified by [DataOutputStream.writeDouble], except
|
||||
* using little-endian byte order.
|
||||
*
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
override fun writeDouble(v: Double) {
|
||||
writeLong(java.lang.Double.doubleToLongBits(v))
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a `float` as specified by [DataOutputStream.writeFloat], except using
|
||||
* little-endian byte order.
|
||||
*
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
override fun writeFloat(v: Float) {
|
||||
writeInt(java.lang.Float.floatToIntBits(v))
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes an `int` as specified by [DataOutputStream.writeInt], except using
|
||||
* little-endian byte order.
|
||||
*
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
override fun writeInt(v: Int) {
|
||||
val bytes = byteArrayOf(
|
||||
(0xFF and v).toByte(),
|
||||
(0xFF and (v shr 8)).toByte(),
|
||||
(0xFF and (v shr 16)).toByte(),
|
||||
(0xFF and (v shr 24)).toByte()
|
||||
)
|
||||
out.write(bytes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a `long` as specified by [DataOutputStream.writeLong], except using
|
||||
* little-endian byte order.
|
||||
*
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
override fun writeLong(v: Long) {
|
||||
val bytes = Longs.toByteArray(java.lang.Long.reverseBytes(v))
|
||||
write(bytes, 0, bytes.size)
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a `short` as specified by [DataOutputStream.writeShort], except using
|
||||
* little-endian byte order.
|
||||
*
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
override fun writeShort(v: Int) {
|
||||
val bytes = byteArrayOf(
|
||||
(0xFF and v).toByte(),
|
||||
(0xFF and (v shr 8)).toByte()
|
||||
)
|
||||
out.write(bytes)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun writeUTF(str: String) {
|
||||
(out as DataOutputStream).writeUTF(str)
|
||||
}
|
||||
|
||||
// Overriding close() because FilterOutputStream's close() method pre-JDK8 has bad behavior:
|
||||
// it silently ignores any exception thrown by flush(). Instead, just close the delegate stream.
|
||||
// It should flush itself if necessary.
|
||||
@Throws(IOException::class)
|
||||
override fun close() {
|
||||
out.close()
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import com.futo.platformplayer.activities.ManageTabsActivity
|
||||
import com.futo.platformplayer.activities.PolycentricHomeActivity
|
||||
import com.futo.platformplayer.activities.PolycentricProfileActivity
|
||||
import com.futo.platformplayer.activities.SettingsActivity
|
||||
import com.futo.platformplayer.activities.SyncHomeActivity
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
|
||||
@@ -28,11 +29,11 @@ import com.futo.platformplayer.states.StateUpdate
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
import com.futo.platformplayer.stores.FragmentedStorageFileJson
|
||||
import com.futo.platformplayer.views.FeedStyle
|
||||
import com.futo.platformplayer.views.fields.AdvancedField
|
||||
import com.futo.platformplayer.views.fields.DropdownFieldOptionsId
|
||||
import com.futo.platformplayer.views.fields.FieldForm
|
||||
import com.futo.platformplayer.views.fields.FormField
|
||||
import com.futo.platformplayer.views.fields.FormFieldButton
|
||||
import com.futo.platformplayer.views.fields.FormFieldWarning
|
||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -44,6 +45,7 @@ import kotlinx.serialization.json.Json
|
||||
import java.io.File
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
|
||||
@Serializable
|
||||
data class MenuBottomBarSetting(val id: Int, var enabled: Boolean);
|
||||
|
||||
@@ -57,7 +59,16 @@ class Settings : FragmentedStorageFileJson() {
|
||||
@Transient
|
||||
val onTabsChanged = Event0();
|
||||
|
||||
@FormField(R.string.manage_polycentric_identity, FieldForm.BUTTON, R.string.manage_your_polycentric_identity, -6)
|
||||
@FormField(R.string.sync_grayjay, FieldForm.BUTTON, R.string.sync_grayjay_description, -8)
|
||||
@FormFieldButton(R.drawable.ic_update)
|
||||
fun syncGrayjay() {
|
||||
SettingsActivity.getActivity()?.let {
|
||||
it.startActivity(Intent(it, SyncHomeActivity::class.java))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@FormField(R.string.manage_polycentric_identity, FieldForm.BUTTON, R.string.manage_your_polycentric_identity, -7)
|
||||
@FormFieldButton(R.drawable.ic_person)
|
||||
fun managePolycentricIdentity() {
|
||||
SettingsActivity.getActivity()?.let {
|
||||
@@ -73,7 +84,7 @@ class Settings : FragmentedStorageFileJson() {
|
||||
}
|
||||
}
|
||||
|
||||
@FormField(R.string.show_faq, FieldForm.BUTTON, R.string.get_answers_to_common_questions, -5)
|
||||
@FormField(R.string.show_faq, FieldForm.BUTTON, R.string.get_answers_to_common_questions, -6)
|
||||
@FormFieldButton(R.drawable.ic_quiz)
|
||||
fun openFAQ() {
|
||||
try {
|
||||
@@ -83,7 +94,7 @@ class Settings : FragmentedStorageFileJson() {
|
||||
//Ignored
|
||||
}
|
||||
}
|
||||
@FormField(R.string.show_issues, FieldForm.BUTTON, R.string.a_list_of_user_reported_and_self_reported_issues, -4)
|
||||
@FormField(R.string.show_issues, FieldForm.BUTTON, R.string.a_list_of_user_reported_and_self_reported_issues, -5)
|
||||
@FormFieldButton(R.drawable.ic_data_alert)
|
||||
fun openIssues() {
|
||||
try {
|
||||
@@ -115,7 +126,7 @@ class Settings : FragmentedStorageFileJson() {
|
||||
}
|
||||
}*/
|
||||
|
||||
@FormField(R.string.manage_tabs, FieldForm.BUTTON, R.string.change_tabs_visible_on_the_home_screen, -3)
|
||||
@FormField(R.string.manage_tabs, FieldForm.BUTTON, R.string.change_tabs_visible_on_the_home_screen, -4)
|
||||
@FormFieldButton(R.drawable.ic_tabs)
|
||||
fun manageTabs() {
|
||||
try {
|
||||
@@ -129,16 +140,15 @@ class Settings : FragmentedStorageFileJson() {
|
||||
|
||||
|
||||
|
||||
@FormField(R.string.import_data, FieldForm.BUTTON, R.string.import_data_description, -2)
|
||||
@FormField(R.string.import_data, FieldForm.BUTTON, R.string.import_data_description, -3)
|
||||
@FormFieldButton(R.drawable.ic_move_up)
|
||||
fun import() {
|
||||
val act = SettingsActivity.getActivity() ?: return;
|
||||
val intent = MainActivity.getImportOptionsIntent(act);
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK;
|
||||
act.startActivity(intent);
|
||||
}
|
||||
|
||||
@FormField(R.string.link_handling, FieldForm.BUTTON, R.string.allow_grayjay_to_handle_links, -1)
|
||||
@FormField(R.string.link_handling, FieldForm.BUTTON, R.string.allow_grayjay_to_handle_links, -2)
|
||||
@FormFieldButton(R.drawable.ic_link)
|
||||
fun manageLinks() {
|
||||
try {
|
||||
@@ -148,6 +158,28 @@ class Settings : FragmentedStorageFileJson() {
|
||||
}
|
||||
}
|
||||
|
||||
/*@FormField(R.string.disable_battery_optimization, FieldForm.BUTTON, R.string.click_to_go_to_battery_optimization_settings_disabling_battery_optimization_will_prevent_the_os_from_killing_media_sessions, -1)
|
||||
@FormFieldButton(R.drawable.battery_full_24px)
|
||||
fun ignoreBatteryOptimization() {
|
||||
SettingsActivity.getActivity()?.let {
|
||||
val intent = Intent()
|
||||
val packageName = it.packageName
|
||||
val pm = it.getSystemService(POWER_SERVICE) as PowerManager;
|
||||
if (!pm.isIgnoringBatteryOptimizations(packageName)) {
|
||||
intent.setAction(android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)
|
||||
intent.setData(Uri.parse("package:$packageName"))
|
||||
it.startActivity(intent)
|
||||
UIDialogs.toast(it, "Please ignore battery optimizations for Grayjay")
|
||||
} else {
|
||||
UIDialogs.toast(it, "Battery optimizations already disabled for Grayjay")
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
|
||||
@FormField(R.string.advanced_settings, FieldForm.TOGGLE, R.string.advanced_settings_description, -1, "advancedSettings")
|
||||
var advancedSettings: Boolean = false;
|
||||
|
||||
@FormField(R.string.language, "group", -1, 0)
|
||||
var language = LanguageSettings();
|
||||
@Serializable
|
||||
@@ -178,7 +210,7 @@ class Settings : FragmentedStorageFileJson() {
|
||||
var home = HomeSettings();
|
||||
@Serializable
|
||||
class HomeSettings {
|
||||
@FormField(R.string.feed_style, FieldForm.DROPDOWN, R.string.may_require_restart, 5)
|
||||
@FormField(R.string.feed_style, FieldForm.DROPDOWN, R.string.may_require_restart, 3)
|
||||
@DropdownFieldOptionsId(R.array.feed_style)
|
||||
var homeFeedStyle: Int = 1;
|
||||
|
||||
@@ -189,10 +221,16 @@ class Settings : FragmentedStorageFileJson() {
|
||||
return FeedStyle.THUMBNAIL;
|
||||
}
|
||||
|
||||
@FormField(R.string.show_home_filters, FieldForm.TOGGLE, R.string.show_home_filters_description, 4)
|
||||
var showHomeFilters: Boolean = true;
|
||||
@FormField(R.string.show_home_filters_plugin_names, FieldForm.TOGGLE, R.string.show_home_filters_plugin_names_description, 5)
|
||||
var showHomeFiltersPluginNames: Boolean = false;
|
||||
|
||||
@AdvancedField
|
||||
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
|
||||
var previewFeedItems: Boolean = true;
|
||||
|
||||
|
||||
@AdvancedField
|
||||
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
|
||||
var progressBar: Boolean = true;
|
||||
|
||||
@@ -221,12 +259,17 @@ class Settings : FragmentedStorageFileJson() {
|
||||
@DropdownFieldOptionsId(R.array.feed_style)
|
||||
var searchFeedStyle: Int = 1;
|
||||
|
||||
@AdvancedField
|
||||
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 5)
|
||||
var previewFeedItems: Boolean = true;
|
||||
|
||||
@AdvancedField
|
||||
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
|
||||
var progressBar: Boolean = true;
|
||||
|
||||
@FormField(R.string.hide_hidden_from_search, FieldForm.TOGGLE, R.string.hide_hidden_from_search_description, 7)
|
||||
var hidefromSearch: Boolean = false;
|
||||
|
||||
|
||||
fun getSearchFeedStyle(): FeedStyle {
|
||||
if(searchFeedStyle == 0)
|
||||
@@ -242,6 +285,7 @@ class Settings : FragmentedStorageFileJson() {
|
||||
@Serializable
|
||||
class ChannelSettings {
|
||||
|
||||
@AdvancedField
|
||||
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
|
||||
var progressBar: Boolean = true;
|
||||
}
|
||||
@@ -264,16 +308,23 @@ class Settings : FragmentedStorageFileJson() {
|
||||
@FormField(R.string.show_subscription_group, FieldForm.TOGGLE, R.string.show_subscription_group_description, 5)
|
||||
var showSubscriptionGroups: Boolean = true;
|
||||
|
||||
@FormField(R.string.use_subscription_exchange, FieldForm.TOGGLE, R.string.use_subscription_exchange_description, 6)
|
||||
var useSubscriptionExchange: Boolean = false;
|
||||
|
||||
@AdvancedField
|
||||
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
|
||||
var previewFeedItems: Boolean = true;
|
||||
|
||||
@AdvancedField
|
||||
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 7)
|
||||
var progressBar: Boolean = true;
|
||||
|
||||
@AdvancedField
|
||||
@FormField(R.string.fetch_on_app_boot, FieldForm.TOGGLE, R.string.shortly_after_opening_the_app_start_fetching_subscriptions, 8)
|
||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||
var fetchOnAppBoot: Boolean = true;
|
||||
|
||||
@AdvancedField
|
||||
@FormField(R.string.fetch_on_tab_opened, FieldForm.TOGGLE, R.string.fetch_on_tab_opened_description, 9)
|
||||
var fetchOnTabOpen: Boolean = true;
|
||||
|
||||
@@ -304,14 +355,20 @@ class Settings : FragmentedStorageFileJson() {
|
||||
@FormField(R.string.show_watch_metrics, FieldForm.TOGGLE, R.string.show_watch_metrics_description, 12)
|
||||
var showWatchMetrics: Boolean = false;
|
||||
|
||||
@AdvancedField
|
||||
@FormField(R.string.track_playtime_locally, FieldForm.TOGGLE, R.string.track_playtime_locally_description, 13)
|
||||
var allowPlaytimeTracking: Boolean = true;
|
||||
|
||||
|
||||
@AdvancedField
|
||||
@FormField(R.string.always_reload_from_cache, FieldForm.TOGGLE, R.string.always_reload_from_cache_description, 14)
|
||||
var alwaysReloadFromCache: Boolean = false;
|
||||
|
||||
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON, R.string.clear_channel_cache_description, 15)
|
||||
@AdvancedField
|
||||
@FormField(R.string.peek_channel_contents, FieldForm.TOGGLE, R.string.peek_channel_contents_description, 15)
|
||||
var peekChannelContents: Boolean = false;
|
||||
|
||||
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON, R.string.clear_channel_cache_description, 16)
|
||||
fun clearChannelCache() {
|
||||
UIDialogs.toast(SettingsActivity.getActivity()!!, "Started clearing..");
|
||||
StateCache.instance.clear();
|
||||
@@ -323,7 +380,7 @@ class Settings : FragmentedStorageFileJson() {
|
||||
var playback = PlaybackSettings();
|
||||
@Serializable
|
||||
class PlaybackSettings {
|
||||
@FormField(R.string.primary_language, FieldForm.DROPDOWN, -1, 0)
|
||||
@FormField(R.string.primary_language, FieldForm.DROPDOWN, -1, -2)
|
||||
@DropdownFieldOptionsId(R.array.audio_languages)
|
||||
var primaryLanguage: Int = 0;
|
||||
|
||||
@@ -347,10 +404,12 @@ class Settings : FragmentedStorageFileJson() {
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
@FormField(R.string.prefer_original_audio, FieldForm.TOGGLE, R.string.prefer_original_audio_description, -1)
|
||||
var preferOriginalAudio: Boolean = true;
|
||||
|
||||
//= context.resources.getStringArray(R.array.audio_languages)[primaryLanguage];
|
||||
|
||||
@FormField(R.string.default_playback_speed, FieldForm.DROPDOWN, -1, 1)
|
||||
@FormField(R.string.default_playback_speed, FieldForm.DROPDOWN, -1, 0)
|
||||
@DropdownFieldOptionsId(R.array.playback_speeds)
|
||||
var defaultPlaybackSpeed: Int = 3;
|
||||
fun getDefaultPlaybackSpeed(): Float = when(defaultPlaybackSpeed) {
|
||||
@@ -366,43 +425,38 @@ class Settings : FragmentedStorageFileJson() {
|
||||
else -> 1.0f;
|
||||
};
|
||||
|
||||
@FormField(R.string.preferred_quality, FieldForm.DROPDOWN, R.string.preferred_quality_description, 2)
|
||||
@FormField(R.string.preferred_quality, FieldForm.DROPDOWN, R.string.preferred_quality_description, 1)
|
||||
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
||||
var preferredQuality: Int = 0;
|
||||
|
||||
@FormField(R.string.preferred_metered_quality, FieldForm.DROPDOWN, R.string.preferred_metered_quality_description, 3)
|
||||
@FormField(R.string.preferred_metered_quality, FieldForm.DROPDOWN, R.string.preferred_metered_quality_description, 2)
|
||||
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
||||
var preferredMeteredQuality: Int = 0;
|
||||
fun getPreferredQualityPixelCount(): Int = preferedQualityToPixels(preferredQuality);
|
||||
fun getPreferredMeteredQualityPixelCount(): Int = preferedQualityToPixels(preferredMeteredQuality);
|
||||
fun getCurrentPreferredQualityPixelCount(): Int = if(!StateApp.instance.isCurrentMetered()) getPreferredQualityPixelCount() else getPreferredMeteredQualityPixelCount();
|
||||
|
||||
@FormField(R.string.preferred_preview_quality, FieldForm.DROPDOWN, R.string.preferred_preview_quality_description, 4)
|
||||
@FormField(R.string.preferred_preview_quality, FieldForm.DROPDOWN, R.string.preferred_preview_quality_description, 3)
|
||||
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
||||
var preferredPreviewQuality: Int = 5;
|
||||
fun getPreferredPreviewQualityPixelCount(): Int = preferedQualityToPixels(preferredPreviewQuality);
|
||||
|
||||
@FormField(R.string.auto_rotate, FieldForm.DROPDOWN, -1, 5)
|
||||
@DropdownFieldOptionsId(R.array.system_enabled_disabled_array)
|
||||
var autoRotate: Int = 2;
|
||||
@AdvancedField
|
||||
@FormField(R.string.simplify_sources, FieldForm.TOGGLE, R.string.simplify_sources_description, 4)
|
||||
var simplifySources: Boolean = true;
|
||||
|
||||
fun isAutoRotate() = autoRotate == 1 || (autoRotate == 2 && StateApp.instance.getCurrentSystemAutoRotate());
|
||||
@AdvancedField
|
||||
@FormField(R.string.always_allow_reverse_landscape_auto_rotate, FieldForm.TOGGLE, R.string.always_allow_reverse_landscape_auto_rotate_description, 5)
|
||||
var alwaysAllowReverseLandscapeAutoRotate: Boolean = true
|
||||
|
||||
@FormField(R.string.auto_rotate_dead_zone, FieldForm.DROPDOWN, R.string.this_prevents_the_device_from_rotating_within_the_given_amount_of_degrees, 6)
|
||||
@DropdownFieldOptionsId(R.array.auto_rotate_dead_zone)
|
||||
var autoRotateDeadZone: Int = 0;
|
||||
|
||||
fun getAutoRotateDeadZoneDegrees(): Int {
|
||||
return autoRotateDeadZone * 5;
|
||||
}
|
||||
|
||||
@FormField(R.string.background_behavior, FieldForm.DROPDOWN, -1, 7)
|
||||
@FormField(R.string.background_behavior, FieldForm.DROPDOWN, -1, 6)
|
||||
@DropdownFieldOptionsId(R.array.player_background_behavior)
|
||||
var backgroundPlay: Int = 2;
|
||||
|
||||
fun isBackgroundContinue() = backgroundPlay == 1;
|
||||
fun isBackgroundPictureInPicture() = backgroundPlay == 2;
|
||||
|
||||
@AdvancedField
|
||||
@FormField(R.string.resume_after_preview, FieldForm.DROPDOWN, R.string.when_watching_a_video_in_preview_mode_resume_at_the_position_when_opening_the_video_code, 7)
|
||||
@DropdownFieldOptionsId(R.array.resume_after_preview)
|
||||
var resumeAfterPreview: Int = 1;
|
||||
@@ -429,14 +483,10 @@ class Settings : FragmentedStorageFileJson() {
|
||||
};
|
||||
}
|
||||
|
||||
@AdvancedField
|
||||
@FormField(R.string.live_chat_webview, FieldForm.TOGGLE, R.string.use_the_live_chat_web_window_when_available_over_native_implementation, 9)
|
||||
var useLiveChatWindow: Boolean = true;
|
||||
|
||||
|
||||
|
||||
@FormField(R.string.background_switch_audio, FieldForm.TOGGLE, R.string.background_switch_audio_description, 10)
|
||||
var backgroundSwitchToAudio: Boolean = true;
|
||||
|
||||
@FormField(R.string.restart_after_audio_focus_loss, FieldForm.DROPDOWN, R.string.restart_playback_when_gaining_audio_focus_after_a_loss, 11)
|
||||
@DropdownFieldOptionsId(R.array.restart_playback_after_loss)
|
||||
var restartPlaybackAfterLoss: Int = 1;
|
||||
@@ -447,18 +497,134 @@ class Settings : FragmentedStorageFileJson() {
|
||||
|
||||
@FormField(R.string.full_screen_portrait, FieldForm.TOGGLE, R.string.allow_full_screen_portrait, 13)
|
||||
var fullscreenPortrait: Boolean = false;
|
||||
|
||||
@FormField(R.string.reverse_portrait, FieldForm.TOGGLE, R.string.reverse_portrait_description, 14)
|
||||
var reversePortrait: Boolean = false;
|
||||
|
||||
@FormField(R.string.prefer_webm, FieldForm.TOGGLE, R.string.prefer_webm_description, 18)
|
||||
var preferWebmVideo: Boolean = false;
|
||||
@FormField(R.string.prefer_webm_audio, FieldForm.TOGGLE, R.string.prefer_webm_audio_description, 19)
|
||||
var preferWebmAudio: Boolean = false;
|
||||
|
||||
@FormField(R.string.allow_under_cutout, FieldForm.TOGGLE, R.string.allow_under_cutout_description, 20)
|
||||
var allowVideoToGoUnderCutout: Boolean = true;
|
||||
|
||||
@FormField(R.string.autoplay, FieldForm.TOGGLE, R.string.autoplay, 21)
|
||||
var autoplay: Boolean = false;
|
||||
|
||||
@AdvancedField
|
||||
@FormField(R.string.delete_watchlist_on_finish, FieldForm.TOGGLE, R.string.delete_watchlist_on_finish_description, 22)
|
||||
var deleteFromWatchLaterAuto: Boolean = true;
|
||||
|
||||
@FormField(R.string.seek_offset, FieldForm.DROPDOWN, R.string.seek_offset_description, 23)
|
||||
@DropdownFieldOptionsId(R.array.seek_offset_duration)
|
||||
var seekOffset: Int = 2;
|
||||
|
||||
fun getSeekOffset(): Long {
|
||||
return when(seekOffset) {
|
||||
0 -> 3_000L;
|
||||
1 -> 5_000L;
|
||||
2 -> 10_000L;
|
||||
3 -> 20_000L;
|
||||
4 -> 30_000L;
|
||||
5 -> 60_000L;
|
||||
else -> 10_000L;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@FormField(R.string.min_playback_speed, FieldForm.DROPDOWN, R.string.min_playback_speed_description, 25)
|
||||
@DropdownFieldOptionsId(R.array.min_playback_speed)
|
||||
var minimumPlaybackSpeed: Int = 0;
|
||||
@FormField(R.string.max_playback_speed, FieldForm.DROPDOWN, R.string.max_playback_speed_description, 26)
|
||||
@DropdownFieldOptionsId(R.array.max_playback_speed)
|
||||
var maximumPlaybackSpeed: Int = 2;
|
||||
@FormField(R.string.step_playback_speed, FieldForm.DROPDOWN, R.string.step_playback_speed_description, 26)
|
||||
@DropdownFieldOptionsId(R.array.step_playback_speed)
|
||||
var stepPlaybackSpeed: Int = 1;
|
||||
|
||||
fun getPlaybackSpeedStep(): Double {
|
||||
return when(stepPlaybackSpeed) {
|
||||
0 -> 0.05
|
||||
1 -> 0.1
|
||||
2 -> 0.25
|
||||
else -> 0.1;
|
||||
}
|
||||
}
|
||||
fun getPlaybackSpeeds(): List<Double> {
|
||||
val playbackSpeeds = mutableListOf<Double>();
|
||||
playbackSpeeds.add(1.0);
|
||||
val minSpeed = when(minimumPlaybackSpeed) {
|
||||
0 -> 0.25
|
||||
1 -> 0.5
|
||||
2 -> 1.0
|
||||
else -> 0.25
|
||||
}
|
||||
val maxSpeed = when(maximumPlaybackSpeed) {
|
||||
0 -> 2.0
|
||||
1 -> 2.25
|
||||
2 -> 3.0
|
||||
3 -> 4.0
|
||||
4 -> 5.0
|
||||
else -> 2.25;
|
||||
}
|
||||
var testSpeed = 1.0;
|
||||
|
||||
while(testSpeed > minSpeed) {
|
||||
val nextSpeed = (testSpeed - 0.25) as Double;
|
||||
testSpeed = Math.max(nextSpeed, minSpeed);
|
||||
playbackSpeeds.add(testSpeed);
|
||||
}
|
||||
testSpeed = 1.0;
|
||||
while(testSpeed < maxSpeed) {
|
||||
val nextSpeed = (testSpeed + if(testSpeed < 2) 0.25 else 1.0) as Double;
|
||||
testSpeed = Math.min(nextSpeed, maxSpeed);
|
||||
playbackSpeeds.add(testSpeed);
|
||||
}
|
||||
playbackSpeeds.sort();
|
||||
return playbackSpeeds;
|
||||
}
|
||||
|
||||
@FormField(R.string.hold_playback_speed, FieldForm.DROPDOWN, R.string.hold_playback_speed_description, 27)
|
||||
@DropdownFieldOptionsId(R.array.hold_playback_speeds)
|
||||
var holdPlaybackSpeed: Int = 4;
|
||||
|
||||
fun getHoldPlaybackSpeed(): Double {
|
||||
return when(holdPlaybackSpeed) {
|
||||
0 -> 1.0
|
||||
1 -> 1.25
|
||||
2 -> 1.5
|
||||
3 -> 1.75
|
||||
4 -> 2.0
|
||||
5 -> 2.25
|
||||
6 -> 2.5
|
||||
7 -> 2.75
|
||||
8 -> 3.0
|
||||
else -> 2.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@FormField(R.string.comments, "group", R.string.comments_description, 6)
|
||||
var comments = CommentSettings();
|
||||
@Serializable
|
||||
class CommentSettings {
|
||||
var didAskPolycentricDefault: Boolean = false;
|
||||
|
||||
@FormField(R.string.default_comment_section, FieldForm.DROPDOWN, -1, 0)
|
||||
@DropdownFieldOptionsId(R.array.comment_sections)
|
||||
var defaultCommentSection: Int = 0;
|
||||
var defaultCommentSection: Int = 2;
|
||||
|
||||
@FormField(R.string.default_recommendations, FieldForm.TOGGLE, R.string.default_recommendations_description, 0)
|
||||
var recommendationsDefault: Boolean = false;
|
||||
|
||||
@AdvancedField
|
||||
@FormField(R.string.hide_recommendations, FieldForm.TOGGLE, R.string.hide_recommendations_description, 0)
|
||||
var hideRecommendations: Boolean = false;
|
||||
|
||||
@FormField(R.string.bad_reputation_comments_fading, FieldForm.TOGGLE, R.string.bad_reputation_comments_fading_description, 0)
|
||||
var badReputationCommentsFading: Boolean = true;
|
||||
|
||||
}
|
||||
|
||||
@FormField(R.string.downloads, "group", R.string.configure_downloading_of_videos, 7)
|
||||
@@ -489,10 +655,12 @@ class Settings : FragmentedStorageFileJson() {
|
||||
var preferredAudioQuality: Int = 1;
|
||||
fun isHighBitrateDefault(): Boolean = preferredAudioQuality > 0;
|
||||
|
||||
@AdvancedField
|
||||
@FormField(R.string.byte_range_download, FieldForm.TOGGLE, R.string.attempt_to_utilize_byte_ranges, 4)
|
||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||
var byteRangeDownload: Boolean = true;
|
||||
|
||||
@AdvancedField
|
||||
@FormField(R.string.byte_range_concurrency, FieldForm.DROPDOWN, R.string.number_of_concurrent_threads_to_multiply_download_speeds_from_throttled_sources, 5)
|
||||
@DropdownFieldOptionsId(R.array.thread_count)
|
||||
var byteRangeConcurrency: Int = 3;
|
||||
@@ -507,7 +675,7 @@ class Settings : FragmentedStorageFileJson() {
|
||||
class Browsing {
|
||||
@FormField(R.string.enable_video_cache, FieldForm.TOGGLE, R.string.cache_to_quickly_load_previously_fetched_videos, 0)
|
||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||
var videoCache: Boolean = true;
|
||||
var videoCache: Boolean = false; //Temporary default disabled to prevent ui freeze?
|
||||
}
|
||||
|
||||
@FormField(R.string.casting, "group", R.string.configure_casting, 9)
|
||||
@@ -522,6 +690,21 @@ class Settings : FragmentedStorageFileJson() {
|
||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||
var keepScreenOn: Boolean = true;
|
||||
|
||||
@AdvancedField
|
||||
@FormField(R.string.always_proxy_requests, FieldForm.TOGGLE, R.string.always_proxy_requests_description, 3)
|
||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||
var alwaysProxyRequests: Boolean = false;
|
||||
|
||||
@AdvancedField
|
||||
@FormField(R.string.allow_ipv6, FieldForm.TOGGLE, R.string.allow_ipv6_description, 4)
|
||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||
var allowIpv6: Boolean = true;
|
||||
|
||||
@AdvancedField
|
||||
@FormField(R.string.allow_ipv4, FieldForm.TOGGLE, R.string.allow_ipv4_description, 5)
|
||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||
var allowLinkLocalIpv4: Boolean = false;
|
||||
|
||||
/*TODO: Should we have a different casting quality?
|
||||
@FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3)
|
||||
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
||||
@@ -546,6 +729,8 @@ class Settings : FragmentedStorageFileJson() {
|
||||
@DropdownFieldOptionsId(R.array.log_levels)
|
||||
var logLevel: Int = 0;
|
||||
|
||||
fun isVerbose() = logLevel >= 4;
|
||||
|
||||
@FormField(R.string.submit_logs, FieldForm.BUTTON, R.string.submit_logs_to_help_us_narrow_down_issues, 1)
|
||||
fun submitLogs() {
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||
@@ -587,6 +772,11 @@ class Settings : FragmentedStorageFileJson() {
|
||||
@Serializable
|
||||
class Plugins {
|
||||
|
||||
@AdvancedField
|
||||
@FormField(R.string.check_disabled_plugin_updates, FieldForm.TOGGLE, R.string.check_disabled_plugin_updates_description, -1)
|
||||
var checkDisabledPluginsForUpdates: Boolean = false;
|
||||
|
||||
@AdvancedField
|
||||
@FormField(R.string.clear_cookies_on_logout, FieldForm.TOGGLE, R.string.clears_cookies_when_you_log_out, 0)
|
||||
var clearCookiesOnLogout: Boolean = true;
|
||||
|
||||
@@ -685,7 +875,9 @@ class Settings : FragmentedStorageFileJson() {
|
||||
fun manualCheck() {
|
||||
if (!BuildConfig.IS_PLAYSTORE_BUILD) {
|
||||
SettingsActivity.getActivity()?.let {
|
||||
StateUpdate.instance.checkForUpdates(it, true);
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||
StateUpdate.instance.checkForUpdates(it, true)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
SettingsActivity.getActivity()?.let {
|
||||
@@ -768,10 +960,10 @@ class Settings : FragmentedStorageFileJson() {
|
||||
fun export() {
|
||||
val activity = SettingsActivity.getActivity() ?: return;
|
||||
UISlideOverlays.showOverlay(activity.overlay, "Select export type", null, {},
|
||||
SlideUpMenuItem(activity, R.drawable.ic_share, "Share", "", null, {
|
||||
SlideUpMenuItem(activity, R.drawable.ic_share, "Share", "", tag = null, call = {
|
||||
StateBackup.shareExternalBackup();
|
||||
}),
|
||||
SlideUpMenuItem(activity, R.drawable.ic_download, "File", "", null, {
|
||||
SlideUpMenuItem(activity, R.drawable.ic_download, "File", "", tag = null, call = {
|
||||
StateBackup.saveExternalBackup(activity);
|
||||
})
|
||||
)
|
||||
@@ -785,12 +977,32 @@ class Settings : FragmentedStorageFileJson() {
|
||||
@FormField(R.string.payment_status, FieldForm.READONLYTEXT, -1, 1)
|
||||
val paymentStatus: String get() = SettingsActivity.getActivity()?.let { if (StatePayment.instance.hasPaid) it.getString(R.string.paid) else it.getString(R.string.not_paid); } ?: "Unknown";
|
||||
|
||||
@FormField(R.string.clear_payment, FieldForm.BUTTON, R.string.deletes_license_keys_from_app, 2)
|
||||
fun clearPayment() {
|
||||
StatePayment.instance.clearLicenses();
|
||||
@FormField(R.string.license_status, FieldForm.BUTTON, R.string.view_license_status, 2)
|
||||
fun viewLicenseStatus() {
|
||||
SettingsActivity.getActivity()?.let {
|
||||
UIDialogs.toast(it, it.getString(R.string.licenses_cleared_might_require_app_restart));
|
||||
it.reloadSettings();
|
||||
try {
|
||||
if (StatePayment.instance.hasPaid) {
|
||||
val paymentKey = StatePayment.instance.getPaymentKey()
|
||||
UIDialogs.showDialogOk(it, R.drawable.ic_paid, "License activated\n" + paymentKey.first)
|
||||
} else {
|
||||
UIDialogs.showDialogOk(it, R.drawable.ic_paid, "No license activated")
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to show license status dialog", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@FormField(R.string.clear_payment, FieldForm.BUTTON, R.string.deletes_license_keys_from_app, 3)
|
||||
fun clearPayment() {
|
||||
SettingsActivity.getActivity()?.let { context ->
|
||||
UIDialogs.showConfirmationDialog(context, "Are you sure you want to delete your license?", {
|
||||
StatePayment.instance.clearLicenses();
|
||||
SettingsActivity.getActivity()?.let {
|
||||
UIDialogs.toast(it, it.getString(R.string.licenses_cleared_might_require_app_restart));
|
||||
it.reloadSettings();
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -799,15 +1011,85 @@ class Settings : FragmentedStorageFileJson() {
|
||||
var other = Other();
|
||||
@Serializable
|
||||
class Other {
|
||||
@FormField(R.string.bypass_rotation_prevention, FieldForm.TOGGLE, R.string.bypass_rotation_prevention_description, 1)
|
||||
@FormFieldWarning(R.string.bypass_rotation_prevention_warning)
|
||||
var bypassRotationPrevention: Boolean = false;
|
||||
@AdvancedField
|
||||
@FormField(R.string.playlist_delete_confirmation, FieldForm.TOGGLE, R.string.playlist_delete_confirmation_description, 2)
|
||||
var playlistDeleteConfirmation: Boolean = true;
|
||||
@AdvancedField
|
||||
@FormField(R.string.playlist_allow_dups, FieldForm.TOGGLE, R.string.playlist_allow_dups_description, 3)
|
||||
var playlistAllowDups: Boolean = true;
|
||||
|
||||
@FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 1)
|
||||
@FormField(R.string.watch_later_add_start, FieldForm.TOGGLE, R.string.watch_later_add_start_description, 4)
|
||||
var watchLaterAddStart: Boolean = true;
|
||||
|
||||
@FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 5)
|
||||
var polycentricEnabled: Boolean = true;
|
||||
|
||||
@FormField(R.string.polycentric_local_cache, FieldForm.TOGGLE, R.string.polycentric_local_cache_description, 7)
|
||||
var polycentricLocalCache: Boolean = true;
|
||||
}
|
||||
|
||||
@FormField(R.string.info, FieldForm.GROUP, -1, 19)
|
||||
@FormField(R.string.gesture_controls, FieldForm.GROUP, -1, 19)
|
||||
var gestureControls = GestureControls();
|
||||
@Serializable
|
||||
class GestureControls {
|
||||
@FormField(R.string.volume_slider, FieldForm.TOGGLE, R.string.volume_slider_descr, 1)
|
||||
var volumeSlider: Boolean = true;
|
||||
|
||||
@FormField(R.string.brightness_slider, FieldForm.TOGGLE, R.string.brightness_slider_descr, 2)
|
||||
var brightnessSlider: Boolean = true;
|
||||
|
||||
@FormField(R.string.toggle_full_screen, FieldForm.TOGGLE, R.string.toggle_full_screen_descr, 3)
|
||||
var toggleFullscreen: Boolean = true;
|
||||
|
||||
@FormField(R.string.system_brightness, FieldForm.TOGGLE, R.string.system_brightness_descr, 4)
|
||||
var useSystemBrightness: Boolean = false;
|
||||
|
||||
@FormField(R.string.system_volume, FieldForm.TOGGLE, R.string.system_volume_descr, 5)
|
||||
var useSystemVolume: Boolean = true;
|
||||
|
||||
@FormField(R.string.restore_system_brightness, FieldForm.TOGGLE, R.string.restore_system_brightness_descr, 6)
|
||||
var restoreSystemBrightness: Boolean = true;
|
||||
|
||||
@FormField(R.string.zoom_option, FieldForm.TOGGLE, R.string.zoom_option_descr, 7)
|
||||
var zoom: Boolean = true;
|
||||
|
||||
@FormField(R.string.pan_option, FieldForm.TOGGLE, R.string.pan_option_descr, 8)
|
||||
var pan: Boolean = true;
|
||||
}
|
||||
|
||||
@FormField(R.string.synchronization, FieldForm.GROUP, -1, 20)
|
||||
var synchronization = Synchronization();
|
||||
@Serializable
|
||||
class Synchronization {
|
||||
@FormField(R.string.enabled, FieldForm.TOGGLE, R.string.enabled_description, 1)
|
||||
var enabled: Boolean = false;
|
||||
|
||||
@FormField(R.string.broadcast, FieldForm.TOGGLE, R.string.broadcast_description, 1)
|
||||
var broadcast: Boolean = false;
|
||||
|
||||
@FormField(R.string.connect_discovered, FieldForm.TOGGLE, R.string.connect_discovered_description, 2)
|
||||
var connectDiscovered: Boolean = true;
|
||||
|
||||
@FormField(R.string.connect_last, FieldForm.TOGGLE, R.string.connect_last_description, 3)
|
||||
var connectLast: Boolean = true;
|
||||
|
||||
@FormField(R.string.discover_through_relay, FieldForm.TOGGLE, R.string.discover_through_relay_description, 3)
|
||||
var discoverThroughRelay: Boolean = true;
|
||||
|
||||
@FormField(R.string.pair_through_relay, FieldForm.TOGGLE, R.string.pair_through_relay_description, 3)
|
||||
var pairThroughRelay: Boolean = true;
|
||||
|
||||
@FormField(R.string.connect_through_relay, FieldForm.TOGGLE, R.string.connect_through_relay_description, 3)
|
||||
var connectThroughRelay: Boolean = true;
|
||||
|
||||
@FormField(R.string.connect_local_direct_through_relay, FieldForm.TOGGLE, R.string.connect_local_direct_through_relay_description, 3)
|
||||
var connectLocalDirectThroughRelay: Boolean = true;
|
||||
|
||||
@FormField(R.string.local_connections, FieldForm.TOGGLE, R.string.local_connections_description, 3)
|
||||
var localConnections: Boolean = true;
|
||||
}
|
||||
|
||||
@FormField(R.string.info, FieldForm.GROUP, -1, 21)
|
||||
var info = Info();
|
||||
@Serializable
|
||||
class Info {
|
||||
@@ -873,4 +1155,4 @@ class Settings : FragmentedStorageFileJson() {
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.futo.platformplayer
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.webkit.CookieManager
|
||||
import androidx.work.Data
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
@@ -8,6 +9,7 @@ import androidx.work.WorkManager
|
||||
import com.caoccao.javet.values.primitive.V8ValueInteger
|
||||
import com.caoccao.javet.values.primitive.V8ValueString
|
||||
import com.futo.platformplayer.activities.DeveloperActivity
|
||||
import com.futo.platformplayer.activities.MainActivity
|
||||
import com.futo.platformplayer.activities.SettingsActivity
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
@@ -33,6 +35,7 @@ import com.futo.platformplayer.stores.FragmentedStorageFileJson
|
||||
import com.futo.platformplayer.views.fields.ButtonField
|
||||
import com.futo.platformplayer.views.fields.FieldForm
|
||||
import com.futo.platformplayer.views.fields.FormField
|
||||
import com.futo.platformplayer.views.fields.FormFieldWarning
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -233,13 +236,17 @@ class SettingsDev : FragmentedStorageFileJson() {
|
||||
R.string.test_background_worker_description, 4)
|
||||
fun triggerBackgroundUpdate() {
|
||||
val act = SettingsActivity.getActivity()!!;
|
||||
UIDialogs.toast(SettingsActivity.getActivity()!!, "Starting test background worker");
|
||||
try {
|
||||
UIDialogs.toast(SettingsActivity.getActivity()!!, "Starting test background worker");
|
||||
|
||||
val wm = WorkManager.getInstance(act);
|
||||
val req = OneTimeWorkRequestBuilder<BackgroundWorker>()
|
||||
.setInputData(Data.Builder().putBoolean("bypassMainCheck", true).build())
|
||||
.build();
|
||||
wm.enqueue(req);
|
||||
val wm = WorkManager.getInstance(act);
|
||||
val req = OneTimeWorkRequestBuilder<BackgroundWorker>()
|
||||
.setInputData(Data.Builder().putBoolean("bypassMainCheck", true).build())
|
||||
.build();
|
||||
wm.enqueue(req);
|
||||
} catch (e: Throwable) {
|
||||
UIDialogs.showGeneralErrorDialog(act, "Failed to trigger background update", e)
|
||||
}
|
||||
}
|
||||
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON,
|
||||
R.string.test_background_worker_description, 4)
|
||||
@@ -490,6 +497,24 @@ class SettingsDev : FragmentedStorageFileJson() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@FormField(R.string.test_playback, FieldForm.BUTTON,
|
||||
R.string.test_playback, 1)
|
||||
fun testPlayback(context: Context) {
|
||||
context.startActivity(MainActivity.getActionIntent(context, "TEST_PLAYBACK"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@FormField(R.string.networking, FieldForm.GROUP, -1, 18)
|
||||
var networking = Networking();
|
||||
@Serializable
|
||||
class Networking {
|
||||
@FormField(R.string.allow_all_certificates, FieldForm.TOGGLE, -1, 0)
|
||||
@FormFieldWarning(R.string.allow_all_certificates_warning)
|
||||
var allowAllCertificates: Boolean = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -503,6 +528,8 @@ class SettingsDev : FragmentedStorageFileJson() {
|
||||
var channelCacheStartupCount = StateCache.instance.channelCacheStartupCount;
|
||||
}
|
||||
|
||||
|
||||
|
||||
//region BOILERPLATE
|
||||
override fun encode(): String {
|
||||
return Json.encodeToString(this);
|
||||
|
||||
@@ -5,7 +5,10 @@ import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.net.Uri
|
||||
import android.text.Layout
|
||||
import android.text.method.ScrollingMovementMethod
|
||||
import android.util.TypedValue
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
@@ -18,6 +21,7 @@ import android.widget.Toast
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.futo.platformplayer.activities.MainActivity
|
||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.casting.StateCasting
|
||||
import com.futo.platformplayer.dialogs.AutoUpdateDialog
|
||||
import com.futo.platformplayer.dialogs.AutomaticBackupDialog
|
||||
@@ -31,12 +35,20 @@ import com.futo.platformplayer.dialogs.ConnectedCastingDialog
|
||||
import com.futo.platformplayer.dialogs.ImportDialog
|
||||
import com.futo.platformplayer.dialogs.ImportOptionsDialog
|
||||
import com.futo.platformplayer.dialogs.MigrateDialog
|
||||
import com.futo.platformplayer.dialogs.PairingCodeDialog
|
||||
import com.futo.platformplayer.dialogs.PluginUpdateDialog
|
||||
import com.futo.platformplayer.dialogs.ProgressDialog
|
||||
import com.futo.platformplayer.engine.exceptions.PluginException
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.MainFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.SourceDetailFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.VideoDetailFragment
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.ImportCache
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateBackup
|
||||
import com.futo.platformplayer.states.StatePlugins
|
||||
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||
import com.futo.platformplayer.views.ToastView
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -182,43 +194,58 @@ class UIDialogs {
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
fun showDialog(context: Context, icon: Int, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action) {
|
||||
fun showPluginUpdateDialog(context: Context, oldConfig: SourcePluginConfig, newConfig: SourcePluginConfig) {
|
||||
val dialog = PluginUpdateDialog(context, oldConfig, newConfig);
|
||||
registerDialogOpened(dialog);
|
||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
fun showDialog(context: Context, icon: Int, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action): AlertDialog {
|
||||
return showDialog(context, icon, false, text, textDetails, code, defaultCloseAction, *actions);
|
||||
}
|
||||
fun showDialog(context: Context, icon: Int, animated: Boolean, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action): AlertDialog {
|
||||
val builder = AlertDialog.Builder(context);
|
||||
val view = LayoutInflater.from(context).inflate(R.layout.dialog_multi_button, null);
|
||||
builder.setView(view);
|
||||
|
||||
builder.setCancelable(defaultCloseAction > -2);
|
||||
val dialog = builder.create();
|
||||
registerDialogOpened(dialog);
|
||||
|
||||
view.findViewById<ImageView>(R.id.dialog_icon).apply {
|
||||
this.setImageResource(icon);
|
||||
if(animated)
|
||||
this.drawable.assume<Animatable, Unit> { it.start() };
|
||||
}
|
||||
view.findViewById<TextView>(R.id.dialog_text).apply {
|
||||
this.text = text;
|
||||
};
|
||||
view.findViewById<TextView>(R.id.dialog_text_details).apply {
|
||||
if(textDetails == null)
|
||||
this.visibility = View.GONE;
|
||||
else
|
||||
this.text = textDetails;
|
||||
};
|
||||
view.findViewById<TextView>(R.id.dialog_text_code).apply {
|
||||
if(code == null)
|
||||
if (textDetails == null)
|
||||
this.visibility = View.GONE;
|
||||
else {
|
||||
this.text = textDetails;
|
||||
}
|
||||
};
|
||||
view.findViewById<TextView>(R.id.dialog_text_code).apply {
|
||||
if (code == null) this.visibility = View.GONE;
|
||||
else {
|
||||
this.text = code;
|
||||
this.movementMethod = ScrollingMovementMethod.getInstance();
|
||||
this.visibility = View.VISIBLE;
|
||||
this.textAlignment = View.TEXT_ALIGNMENT_VIEW_START
|
||||
}
|
||||
};
|
||||
view.findViewById<LinearLayout>(R.id.dialog_buttons).apply {
|
||||
val center = actions.any { it?.center == true };
|
||||
val buttons = actions.map<Action, TextView> { act ->
|
||||
val buttonView = TextView(context);
|
||||
val dp10 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10f, resources.displayMetrics).toInt();
|
||||
val dp28 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 28f, resources.displayMetrics).toInt();
|
||||
val dp14 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 14.0f, resources.displayMetrics).toInt();
|
||||
buttonView.layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
|
||||
if(actions.size > 1)
|
||||
this.marginEnd = if(actions.size > 2) dp14 else dp28;
|
||||
this.marginStart = if(actions.size >= 2) dp14 / 2 else dp28 / 2;
|
||||
this.marginEnd = if(actions.size >= 2) dp14 / 2 else dp28 / 2;
|
||||
};
|
||||
buttonView.setTextColor(Color.WHITE);
|
||||
buttonView.textSize = 14f;
|
||||
@@ -240,7 +267,7 @@ class UIDialogs {
|
||||
|
||||
return@map buttonView;
|
||||
};
|
||||
if(actions.size <= 1)
|
||||
if(actions.size <= 1 || center)
|
||||
this.gravity = Gravity.CENTER;
|
||||
else
|
||||
this.gravity = Gravity.END;
|
||||
@@ -255,6 +282,7 @@ class UIDialogs {
|
||||
registerDialogClosed(dialog);
|
||||
}
|
||||
dialog.show();
|
||||
return dialog;
|
||||
}
|
||||
|
||||
fun showGeneralErrorDialog(context: Context, msg: String, ex: Throwable? = null, button: String = "Ok", onOk: (()->Unit)? = null) {
|
||||
@@ -267,22 +295,56 @@ class UIDialogs {
|
||||
}, UIDialogs.ActionStyle.PRIMARY)
|
||||
);
|
||||
}
|
||||
fun showGeneralRetryErrorDialog(context: Context, msg: String, ex: Throwable? = null, retryAction: (() -> Unit)? = null, closeAction: (() -> Unit)? = null) {
|
||||
fun showGeneralRetryErrorDialog(context: Context, msg: String, ex: Throwable? = null, retryAction: (() -> Unit)? = null, closeAction: (() -> Unit)? = null, mainFragment: MainFragment? = null) {
|
||||
val pluginConfig = if(ex is PluginException) ex.config else null;
|
||||
val pluginInfo = if(ex is PluginException)
|
||||
"\nPlugin [${ex.config.name}]" else "";
|
||||
showDialog(context,
|
||||
R.drawable.ic_error_pred,
|
||||
"${msg}${pluginInfo}",
|
||||
(if(ex != null ) "${ex.message}" else ""),
|
||||
if(ex is PluginException) ex.code else null,
|
||||
0,
|
||||
UIDialogs.Action(context.getString(R.string.retry), {
|
||||
retryAction?.invoke();
|
||||
}, UIDialogs.ActionStyle.PRIMARY),
|
||||
UIDialogs.Action(context.getString(R.string.close), {
|
||||
closeAction?.invoke()
|
||||
}, UIDialogs.ActionStyle.NONE)
|
||||
);
|
||||
|
||||
var exMsg = if(ex != null ) "${ex.message}" else "";
|
||||
if(pluginConfig != null && pluginConfig is SourcePluginConfig && StatePlugins.instance.hasUpdateAvailable(pluginConfig))
|
||||
exMsg += "\n\nAn update is available"
|
||||
|
||||
if(mainFragment != null && pluginConfig != null && pluginConfig is SourcePluginConfig && StatePlugins.instance.hasUpdateAvailable(pluginConfig))
|
||||
showDialog(context,
|
||||
R.drawable.ic_error_pred,
|
||||
"${msg}${pluginInfo}",
|
||||
exMsg,
|
||||
if(ex is PluginException) ex.code else null,
|
||||
1,
|
||||
UIDialogs.Action(context.getString(R.string.update), {
|
||||
mainFragment.navigate<SourceDetailFragment>(SourceDetailFragment.UpdatePluginAction(pluginConfig));
|
||||
if(mainFragment is VideoDetailFragment)
|
||||
mainFragment.minimizeVideoDetail();
|
||||
}, UIDialogs.ActionStyle.ACCENT),
|
||||
UIDialogs.Action(context.getString(R.string.close), {
|
||||
closeAction?.invoke()
|
||||
}, UIDialogs.ActionStyle.NONE),
|
||||
UIDialogs.Action(context.getString(R.string.retry), {
|
||||
try {
|
||||
retryAction?.invoke();
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Unhandled exception retrying", e)
|
||||
}
|
||||
}, UIDialogs.ActionStyle.PRIMARY)
|
||||
);
|
||||
else
|
||||
showDialog(context,
|
||||
R.drawable.ic_error_pred,
|
||||
"${msg}${pluginInfo}",
|
||||
exMsg,
|
||||
if(ex is PluginException) ex.code else null,
|
||||
0,
|
||||
UIDialogs.Action(context.getString(R.string.close), {
|
||||
closeAction?.invoke()
|
||||
}, UIDialogs.ActionStyle.NONE),
|
||||
UIDialogs.Action(context.getString(R.string.retry), {
|
||||
try {
|
||||
retryAction?.invoke();
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Unhandled exception retrying", e)
|
||||
}
|
||||
}, UIDialogs.ActionStyle.PRIMARY)
|
||||
);
|
||||
}
|
||||
|
||||
fun showSingleButtonDialog(context: Context, icon: Int, text: String, buttonText: String, action: (() -> Unit)) {
|
||||
@@ -303,16 +365,27 @@ class UIDialogs {
|
||||
showDialog(context, R.drawable.ic_error, text, null, null, 0, cancelButtonAction, confirmButtonAction)
|
||||
}
|
||||
|
||||
fun showUpdateAvailableDialog(context: Context, lastVersion: Int) {
|
||||
fun showConfirmationDialog(context: Context, text: String, action: () -> Unit, cancelAction: (() -> Unit)? = null, doNotAskAgainAction: (() -> Unit)? = null) {
|
||||
val confirmButtonAction = Action(context.getString(R.string.confirm), action, ActionStyle.PRIMARY)
|
||||
val cancelButtonAction = Action(context.getString(R.string.cancel), cancelAction ?: {}, ActionStyle.ACCENT)
|
||||
val doNotAskAgain = Action(context.getString(R.string.do_not_ask_again), doNotAskAgainAction ?: {}, ActionStyle.NONE)
|
||||
showDialog(context, R.drawable.ic_error, text, null, null, 0, doNotAskAgain, cancelButtonAction, confirmButtonAction)
|
||||
}
|
||||
|
||||
fun showUpdateAvailableDialog(context: Context, lastVersion: Int, hideExceptionButtons: Boolean = false) {
|
||||
val dialog = AutoUpdateDialog(context);
|
||||
registerDialogOpened(dialog);
|
||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||
dialog.show();
|
||||
dialog.setMaxVersion(lastVersion);
|
||||
|
||||
if (hideExceptionButtons) {
|
||||
dialog.hideExceptionButtons()
|
||||
}
|
||||
}
|
||||
|
||||
fun showChangelogDialog(context: Context, lastVersion: Int) {
|
||||
val dialog = ChangelogDialog(context);
|
||||
fun showChangelogDialog(context: Context, lastVersion: Int, changelogs: Map<Int, String>? = null) {
|
||||
val dialog = ChangelogDialog(context, changelogs);
|
||||
registerDialogOpened(dialog);
|
||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||
dialog.show();
|
||||
@@ -338,8 +411,8 @@ class UIDialogs {
|
||||
}
|
||||
}
|
||||
|
||||
fun showImportDialog(context: Context, store: ManagedStore<*>, name: String, reconstructions: List<String>, onConcluded: () -> Unit) {
|
||||
val dialog = ImportDialog(context, store, name, reconstructions, onConcluded);
|
||||
fun showImportDialog(context: Context, store: ManagedStore<*>, name: String, reconstructions: List<String>, cache: ImportCache?, onConcluded: () -> Unit) {
|
||||
val dialog = ImportDialog(context, store, name, reconstructions, cache, onConcluded);
|
||||
registerDialogOpened(dialog);
|
||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||
dialog.show();
|
||||
@@ -391,6 +464,14 @@ class UIDialogs {
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
fun showPairingCodeDialog(context: Context, onSubmit: (code: String) -> Unit, onCancel: () -> Unit) {
|
||||
val dialog = PairingCodeDialog(context, onSubmit);
|
||||
registerDialogOpened(dialog);
|
||||
dialog.setOnDismissListener { registerDialogClosed(dialog) }
|
||||
dialog.setOnCancelListener { onCancel() }
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
fun toast(context : Context, text : String, long : Boolean = false) {
|
||||
Toast.makeText(context, text, if(long) Toast.LENGTH_LONG else Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
@@ -398,13 +479,28 @@ class UIDialogs {
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
||||
try {
|
||||
StateApp.withContext {
|
||||
Toast.makeText(it, text, if (long) Toast.LENGTH_LONG else Toast.LENGTH_SHORT).show();
|
||||
toast(it, text, long);
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to show toast.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
fun appToast(text: String, long: Boolean = false) {
|
||||
appToast(ToastView.Toast(text, long))
|
||||
}
|
||||
fun appToastError(text: String, long: Boolean) {
|
||||
StateApp.withContext {
|
||||
appToast(ToastView.Toast(text, long, it.getColor(R.color.pastel_red)));
|
||||
};
|
||||
}
|
||||
fun appToast(toast: ToastView.Toast) {
|
||||
StateApp.withContext {
|
||||
if(it is MainActivity) {
|
||||
it.showAppToast(toast);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun showClickableToast(context: Context, text: String, onClick: () -> Unit, isLongDuration: Boolean = false) {
|
||||
//TODO: Is not actually clickable...
|
||||
@@ -446,11 +542,13 @@ class UIDialogs {
|
||||
val text: String;
|
||||
val action: ()->Unit;
|
||||
val style: ActionStyle;
|
||||
var center: Boolean;
|
||||
|
||||
constructor(text: String, action: ()->Unit, style: ActionStyle = ActionStyle.NONE) {
|
||||
constructor(text: String, action: ()->Unit, style: ActionStyle = ActionStyle.NONE, center: Boolean = false) {
|
||||
this.text = text;
|
||||
this.action = action;
|
||||
this.style = style;
|
||||
this.center = center;
|
||||
}
|
||||
}
|
||||
enum class ActionStyle {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,7 @@ import android.os.OperationCanceledException
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.view.WindowInsetsController
|
||||
import android.view.WindowManager
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.FileProvider
|
||||
@@ -25,12 +26,24 @@ import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.PlatformVideoWithTime
|
||||
import com.futo.platformplayer.others.PlatformLinkMovementMethod
|
||||
import java.io.File
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.net.Inet4Address
|
||||
import java.net.Inet6Address
|
||||
import java.net.InetAddress
|
||||
import java.net.InterfaceAddress
|
||||
import java.net.NetworkInterface
|
||||
import java.net.SocketException
|
||||
import java.nio.ByteBuffer
|
||||
import java.security.SecureRandom
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.*
|
||||
import java.util.concurrent.ThreadLocalRandom
|
||||
import java.util.zip.GZIPInputStream
|
||||
import java.util.zip.GZIPOutputStream
|
||||
|
||||
private val _allowedCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz ";
|
||||
fun getRandomString(sizeOfRandomString: Int): String {
|
||||
@@ -62,7 +75,14 @@ fun warnIfMainThread(context: String) {
|
||||
}
|
||||
|
||||
fun ensureNotMainThread() {
|
||||
if (Looper.myLooper() == Looper.getMainLooper()) {
|
||||
val isMainLooper = try {
|
||||
Looper.myLooper() == Looper.getMainLooper()
|
||||
} catch (e: Throwable) {
|
||||
//Ignore, for unit tests where its not mocked
|
||||
false
|
||||
}
|
||||
|
||||
if (isMainLooper) {
|
||||
Logger.e("Utility", "Throwing exception because a function that should not be called on main thread, is called on main thread")
|
||||
throw IllegalStateException("Cannot run on main thread")
|
||||
}
|
||||
@@ -229,4 +249,213 @@ fun String.decodeUnicode(): String {
|
||||
i++
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> smartMerge(targetArr: List<T>, toMerge: List<T>) : List<T>{
|
||||
val missingToMerge = toMerge.filter { !targetArr.contains(it) }.toList();
|
||||
val newArrResult = targetArr.toMutableList();
|
||||
|
||||
for(missing in missingToMerge) {
|
||||
val newIndex = findNewIndex(toMerge, newArrResult, missing);
|
||||
newArrResult.add(newIndex, missing);
|
||||
}
|
||||
|
||||
return newArrResult;
|
||||
}
|
||||
fun <T> findNewIndex(originalArr: List<T>, newArr: List<T>, item: T): Int{
|
||||
var originalIndex = originalArr.indexOf(item);
|
||||
var newIndex = -1;
|
||||
|
||||
for(i in originalIndex-1 downTo 0) {
|
||||
val previousItem = originalArr[i];
|
||||
val indexInNewArr = newArr.indexOfFirst { it == previousItem };
|
||||
if(indexInNewArr >= 0) {
|
||||
newIndex = indexInNewArr + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(newIndex < 0) {
|
||||
for(i in originalIndex+1 until originalArr.size) {
|
||||
val previousItem = originalArr[i];
|
||||
val indexInNewArr = newArr.indexOfFirst { it == previousItem };
|
||||
if(indexInNewArr >= 0) {
|
||||
newIndex = indexInNewArr - 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(newIndex < 0)
|
||||
return newArr.size;
|
||||
else
|
||||
return newIndex;
|
||||
}
|
||||
|
||||
fun ByteBuffer.toUtf8String(): String {
|
||||
val remainingBytes = ByteArray(remaining())
|
||||
get(remainingBytes)
|
||||
return String(remainingBytes, Charsets.UTF_8)
|
||||
}
|
||||
|
||||
fun generateReadablePassword(length: Int): String {
|
||||
val validChars = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789"
|
||||
val secureRandom = SecureRandom()
|
||||
val randomBytes = ByteArray(length)
|
||||
secureRandom.nextBytes(randomBytes)
|
||||
val sb = StringBuilder(length)
|
||||
for (byte in randomBytes) {
|
||||
val index = (byte.toInt() and 0xFF) % validChars.length
|
||||
sb.append(validChars[index])
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
fun ByteArray.toGzip(): ByteArray {
|
||||
if (this == null || this.isEmpty()) return ByteArray(0)
|
||||
|
||||
val gzipTimeStart = OffsetDateTime.now();
|
||||
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
GZIPOutputStream(outputStream).use { gzip ->
|
||||
gzip.write(this)
|
||||
}
|
||||
val result = outputStream.toByteArray();
|
||||
Logger.i("Utility", "Gzip compression time: ${gzipTimeStart.getNowDiffMiliseconds()}ms");
|
||||
return result;
|
||||
}
|
||||
|
||||
fun ByteArray.fromGzip(): ByteArray {
|
||||
if (this == null || this.isEmpty()) return ByteArray(0)
|
||||
|
||||
val inputStream = ByteArrayInputStream(this)
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
|
||||
GZIPInputStream(inputStream).use { gzip ->
|
||||
val buffer = ByteArray(1024)
|
||||
var bytesRead: Int
|
||||
while (gzip.read(buffer).also { bytesRead = it } != -1) {
|
||||
outputStream.write(buffer, 0, bytesRead)
|
||||
}
|
||||
}
|
||||
return outputStream.toByteArray()
|
||||
}
|
||||
|
||||
fun findCandidateAddresses(): List<InetAddress> {
|
||||
val candidates = NetworkInterface.getNetworkInterfaces()
|
||||
.toList()
|
||||
.asSequence()
|
||||
.filter(::isUsableInterface)
|
||||
.flatMap { nif ->
|
||||
nif.interfaceAddresses
|
||||
.asSequence()
|
||||
.mapNotNull { ia ->
|
||||
ia.address.takeIf(::isUsableAddress)?.let { addr ->
|
||||
nif to ia
|
||||
}
|
||||
}
|
||||
}
|
||||
.toList()
|
||||
|
||||
return candidates
|
||||
.sortedWith(
|
||||
compareBy<Pair<NetworkInterface, InterfaceAddress>>(
|
||||
{ addressScore(it.second.address) },
|
||||
{ interfaceScore(it.first) },
|
||||
{ -it.second.networkPrefixLength.toInt() },
|
||||
{ -it.first.mtu }
|
||||
)
|
||||
).map { it.second.address }
|
||||
}
|
||||
|
||||
fun findPreferredAddress(): InetAddress? {
|
||||
val candidates = NetworkInterface.getNetworkInterfaces()
|
||||
.toList()
|
||||
.asSequence()
|
||||
.filter(::isUsableInterface)
|
||||
.flatMap { nif ->
|
||||
nif.interfaceAddresses
|
||||
.asSequence()
|
||||
.mapNotNull { ia ->
|
||||
ia.address.takeIf(::isUsableAddress)?.let { addr ->
|
||||
nif to ia
|
||||
}
|
||||
}
|
||||
}
|
||||
.toList()
|
||||
|
||||
return candidates
|
||||
.minWithOrNull(
|
||||
compareBy<Pair<NetworkInterface, InterfaceAddress>>(
|
||||
{ addressScore(it.second.address) },
|
||||
{ interfaceScore(it.first) },
|
||||
{ -it.second.networkPrefixLength.toInt() },
|
||||
{ -it.first.mtu }
|
||||
)
|
||||
)?.second?.address
|
||||
}
|
||||
|
||||
private fun isUsableInterface(nif: NetworkInterface): Boolean {
|
||||
val name = nif.name.lowercase()
|
||||
return try {
|
||||
// must be up, not loopback/virtual/PtP, have a MAC, not Docker/tun/etc.
|
||||
nif.isUp
|
||||
&& !nif.isLoopback
|
||||
&& !nif.isPointToPoint
|
||||
&& !nif.isVirtual
|
||||
&& !name.startsWith("docker")
|
||||
&& !name.startsWith("veth")
|
||||
&& !name.startsWith("br-")
|
||||
&& !name.startsWith("virbr")
|
||||
&& !name.startsWith("vmnet")
|
||||
&& !name.startsWith("tun")
|
||||
&& !name.startsWith("tap")
|
||||
} catch (e: SocketException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun isUsableAddress(addr: InetAddress): Boolean {
|
||||
return when {
|
||||
addr.isAnyLocalAddress -> false // 0.0.0.0 / ::
|
||||
addr.isLoopbackAddress -> false
|
||||
addr.isLinkLocalAddress -> false // 169.254.x.x or fe80::/10
|
||||
addr.isMulticastAddress -> false
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
private fun interfaceScore(nif: NetworkInterface): Int {
|
||||
val name = nif.name.lowercase()
|
||||
return when {
|
||||
name.matches(Regex("^(eth|enp|eno|ens|em)\\d+")) -> 0
|
||||
name.startsWith("eth") || name.contains("ethernet") -> 0
|
||||
name.matches(Regex("^(wlan|wlp)\\d+")) -> 1
|
||||
name.contains("wi-fi") || name.contains("wifi") -> 1
|
||||
else -> 2
|
||||
}
|
||||
}
|
||||
|
||||
fun addressScore(addr: InetAddress): Int {
|
||||
return when (addr) {
|
||||
is Inet4Address -> {
|
||||
val octets = addr.address.map { it.toInt() and 0xFF }
|
||||
when {
|
||||
octets[0] == 10 -> 0 // 10/8
|
||||
octets[0] == 192 && octets[1] == 168 -> 0 // 192.168/16
|
||||
octets[0] == 172 && octets[1] in 16..31 -> 0 // 172.16–31/12
|
||||
else -> 1 // public IPv4
|
||||
}
|
||||
}
|
||||
is Inet6Address -> {
|
||||
// ULA (fc00::/7) vs global vs others
|
||||
val b0 = addr.address[0].toInt() and 0xFF
|
||||
when {
|
||||
(b0 and 0xFE) == 0xFC -> 2 // ULA
|
||||
(b0 and 0xE0) == 0x20 -> 3 // global
|
||||
else -> 4
|
||||
}
|
||||
}
|
||||
else -> Int.MAX_VALUE
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> Enumeration<T>.toList(): List<T> = Collections.list(this)
|
||||
@@ -12,6 +12,7 @@ import android.widget.ScrollView
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
@@ -37,8 +38,10 @@ class AddSourceActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var _sourceHeader: SourceHeaderView;
|
||||
|
||||
|
||||
private lateinit var _sourcePermissions: LinearLayout;
|
||||
private lateinit var _sourceWarnings: LinearLayout;
|
||||
private lateinit var _sourceWarningsContainer: LinearLayout;
|
||||
|
||||
private lateinit var _container: ScrollView;
|
||||
private lateinit var _loader: ImageView;
|
||||
@@ -79,6 +82,7 @@ class AddSourceActivity : AppCompatActivity() {
|
||||
|
||||
_sourcePermissions = findViewById(R.id.source_permissions);
|
||||
_sourceWarnings = findViewById(R.id.source_warnings);
|
||||
_sourceWarningsContainer = findViewById(R.id.container_source_warnings);
|
||||
|
||||
_container = findViewById(R.id.configContainer);
|
||||
_loader = findViewById(R.id.loader);
|
||||
@@ -203,21 +207,30 @@ class AddSourceActivity : AppCompatActivity() {
|
||||
|
||||
val pastelRed = ContextCompat.getColor(this, R.color.pastel_red);
|
||||
|
||||
for(warning in config.getWarnings(script))
|
||||
val warnings = config.getWarnings(script);
|
||||
for(warning in warnings)
|
||||
_sourceWarnings.addView(
|
||||
SourceInfoView(this,
|
||||
R.drawable.ic_security_pred,
|
||||
warning.first,
|
||||
warning.second)
|
||||
.withDescriptionColor(pastelRed));
|
||||
_sourceWarningsContainer.isVisible = warnings.isNotEmpty();
|
||||
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
fun install(config: SourcePluginConfig, script: String) {
|
||||
val isNew = !StatePlatform.instance.getAvailableClients().any { it.id == config.id };
|
||||
StatePlugins.instance.installPlugin(this, lifecycleScope, config, script) {
|
||||
if(it)
|
||||
if(it) {
|
||||
StatePlugins.instance.clearUpdateAvailable(config)
|
||||
if(isNew)
|
||||
lifecycleScope.launch {
|
||||
StatePlatform.instance.enableClient(listOf(config.id));
|
||||
}
|
||||
backToSources();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,12 +10,15 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import com.futo.platformplayer.*
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.views.buttons.BigButton
|
||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTextInput
|
||||
import com.google.zxing.integration.android.IntentIntegrator
|
||||
|
||||
class AddSourceOptionsActivity : AppCompatActivity() {
|
||||
lateinit var _buttonBack: ImageButton;
|
||||
|
||||
lateinit var _overlayContainer: FrameLayout;
|
||||
lateinit var _buttonQR: BigButton;
|
||||
lateinit var _buttonBrowse: BigButton;
|
||||
lateinit var _buttonURL: BigButton;
|
||||
lateinit var _buttonPlugins: BigButton;
|
||||
|
||||
@@ -53,9 +56,11 @@ class AddSourceOptionsActivity : AppCompatActivity() {
|
||||
setContentView(R.layout.activity_add_source_options);
|
||||
setNavigationBarColorAndIcons();
|
||||
|
||||
_overlayContainer = findViewById(R.id.overlay_container);
|
||||
_buttonBack = findViewById(R.id.button_back);
|
||||
|
||||
_buttonQR = findViewById(R.id.option_qr);
|
||||
_buttonBrowse = findViewById(R.id.option_browse);
|
||||
_buttonURL = findViewById(R.id.option_url);
|
||||
_buttonPlugins = findViewById(R.id.option_plugins);
|
||||
|
||||
@@ -74,9 +79,30 @@ class AddSourceOptionsActivity : AppCompatActivity() {
|
||||
integrator.setCaptureActivity(QRCaptureActivity::class.java);
|
||||
_qrCodeResultLauncher.launch(integrator.createScanIntent())
|
||||
}
|
||||
_buttonBrowse.onClick.subscribe {
|
||||
startActivity(MainActivity.getTabIntent(this, "BROWSE_PLUGINS"));
|
||||
}
|
||||
|
||||
_buttonURL.onClick.subscribe {
|
||||
UIDialogs.toast(this, getString(R.string.not_implemented_yet));
|
||||
val nameInput = SlideUpMenuTextInput(this, "ex. https://yourplugin.com/config.json");
|
||||
UISlideOverlays.showOverlay(_overlayContainer, "Enter your url", "Install", {
|
||||
|
||||
val content = nameInput.text;
|
||||
|
||||
val url = if (content.startsWith("https://")) {
|
||||
content
|
||||
} else if (content.startsWith("grayjay://plugin/")) {
|
||||
content.substring("grayjay://plugin/".length)
|
||||
} else {
|
||||
UIDialogs.toast(this, getString(R.string.not_a_plugin_url))
|
||||
return@showOverlay;
|
||||
}
|
||||
|
||||
val intent = Intent(this, AddSourceActivity::class.java).apply {
|
||||
data = Uri.parse(url);
|
||||
};
|
||||
startActivity(intent);
|
||||
}, nameInput)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,15 +4,19 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.*
|
||||
import com.futo.platformplayer.BuildConfig
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.logging.LogLevel
|
||||
import com.futo.platformplayer.logging.Logging
|
||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateUpdate
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -26,6 +30,7 @@ class ExceptionActivity : AppCompatActivity() {
|
||||
private lateinit var _buttonSubmit: LinearLayout;
|
||||
private lateinit var _buttonRestart: LinearLayout;
|
||||
private lateinit var _buttonClose: LinearLayout;
|
||||
private lateinit var _buttonCheckForUpdates: LinearLayout;
|
||||
private var _file: File? = null;
|
||||
private var _submitted = false;
|
||||
|
||||
@@ -43,6 +48,7 @@ class ExceptionActivity : AppCompatActivity() {
|
||||
_buttonSubmit = findViewById(R.id.button_submit);
|
||||
_buttonRestart = findViewById(R.id.button_restart);
|
||||
_buttonClose = findViewById(R.id.button_close);
|
||||
_buttonCheckForUpdates = findViewById(R.id.button_check_for_updates);
|
||||
|
||||
val context = intent.getStringExtra(EXTRA_CONTEXT) ?: getString(R.string.unknown_context);
|
||||
val stack = intent.getStringExtra(EXTRA_STACK) ?: getString(R.string.something_went_wrong_missing_stack_trace);
|
||||
@@ -81,6 +87,17 @@ class ExceptionActivity : AppCompatActivity() {
|
||||
_buttonClose.setOnClickListener {
|
||||
finish();
|
||||
};
|
||||
|
||||
if (!BuildConfig.IS_PLAYSTORE_BUILD) {
|
||||
_buttonCheckForUpdates.visibility = View.VISIBLE
|
||||
_buttonCheckForUpdates.setOnClickListener {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
StateUpdate.instance.checkForUpdates(this@ExceptionActivity, true, true)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_buttonCheckForUpdates.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun submitFile() {
|
||||
|
||||
@@ -10,6 +10,7 @@ import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourceAuth
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginAuthConfig
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
@@ -39,6 +40,7 @@ class LoginActivity : AppCompatActivity() {
|
||||
_textUrl = findViewById(R.id.text_url);
|
||||
_buttonClose = findViewById(R.id.button_close);
|
||||
_buttonClose.setOnClickListener {
|
||||
UIDialogs.toast("Login cancelled", false);
|
||||
finish();
|
||||
}
|
||||
|
||||
@@ -111,7 +113,7 @@ class LoginActivity : AppCompatActivity() {
|
||||
|
||||
companion object {
|
||||
private val TAG = "LoginActivity";
|
||||
private val REGEX_LOGIN_BUTTON = Regex("[a-zA-Z\\-\\.#_ ]*");
|
||||
private val REGEX_LOGIN_BUTTON = Regex("[a-zA-Z\\-\\.#:_ ]*");
|
||||
|
||||
private var _callback: ((SourceAuth?) -> Unit)? = null;
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,7 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
@@ -13,16 +14,26 @@ import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateApp.Companion.withContext
|
||||
import com.futo.platformplayer.states.StatePolycentric
|
||||
import com.futo.platformplayer.views.buttons.BigButton
|
||||
import com.futo.polycentric.core.*
|
||||
import com.futo.polycentric.core.ContentType
|
||||
import com.futo.polycentric.core.SignedEvent
|
||||
import com.futo.polycentric.core.StorageTypeCRDTItem
|
||||
import com.futo.polycentric.core.StorageTypeCRDTSetItem
|
||||
import com.futo.polycentric.core.Store
|
||||
import com.futo.polycentric.core.toBase64Url
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.MultiFormatWriter
|
||||
import com.google.zxing.common.BitMatrix
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import userpackage.Protocol
|
||||
import userpackage.Protocol.ExportBundle
|
||||
import userpackage.Protocol.URLInfo
|
||||
@@ -33,6 +44,7 @@ class PolycentricBackupActivity : AppCompatActivity() {
|
||||
private lateinit var _imageQR: ImageView;
|
||||
private lateinit var _exportBundle: String;
|
||||
private lateinit var _textQR: TextView;
|
||||
private lateinit var _loader: View
|
||||
|
||||
override fun attachBaseContext(newBase: Context?) {
|
||||
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
||||
@@ -43,32 +55,52 @@ class PolycentricBackupActivity : AppCompatActivity() {
|
||||
setContentView(R.layout.activity_polycentric_backup);
|
||||
setNavigationBarColorAndIcons();
|
||||
|
||||
_buttonShare = findViewById(R.id.button_share);
|
||||
_buttonCopy = findViewById(R.id.button_copy);
|
||||
_imageQR = findViewById(R.id.image_qr);
|
||||
_textQR = findViewById(R.id.text_qr);
|
||||
_buttonShare = findViewById(R.id.button_share)
|
||||
_buttonCopy = findViewById(R.id.button_copy)
|
||||
_imageQR = findViewById(R.id.image_qr)
|
||||
_textQR = findViewById(R.id.text_qr)
|
||||
_loader = findViewById(R.id.progress_loader)
|
||||
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
|
||||
finish();
|
||||
};
|
||||
|
||||
_exportBundle = createExportBundle();
|
||||
_imageQR.visibility = View.INVISIBLE
|
||||
_textQR.visibility = View.INVISIBLE
|
||||
_loader.visibility = View.VISIBLE
|
||||
_buttonShare.visibility = View.INVISIBLE
|
||||
_buttonCopy.visibility = View.INVISIBLE
|
||||
|
||||
try {
|
||||
val dimension = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 200f, resources.displayMetrics).toInt();
|
||||
val qrCodeBitmap = generateQRCode(_exportBundle, dimension, dimension);
|
||||
_imageQR.setImageBitmap(qrCodeBitmap);
|
||||
} catch (e: Exception) {
|
||||
Logger.e(TAG, getString(R.string.failed_to_generate_qr_code), e);
|
||||
_imageQR.visibility = View.INVISIBLE;
|
||||
_textQR.visibility = View.INVISIBLE;
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val pair = withContext(Dispatchers.IO) {
|
||||
val bundle = createExportBundle()
|
||||
val dimension = TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP, 200f, resources.displayMetrics
|
||||
).toInt()
|
||||
val qr = generateQRCode(bundle, dimension, dimension)
|
||||
Pair(bundle, qr)
|
||||
}
|
||||
|
||||
_exportBundle = pair.first
|
||||
_imageQR.setImageBitmap(pair.second)
|
||||
_imageQR.visibility = View.VISIBLE
|
||||
_textQR.visibility = View.VISIBLE
|
||||
_buttonShare.visibility = View.VISIBLE
|
||||
_buttonCopy.visibility = View.VISIBLE
|
||||
} catch (e: Exception) {
|
||||
Logger.e(TAG, getString(R.string.failed_to_generate_qr_code), e)
|
||||
_imageQR.visibility = View.INVISIBLE
|
||||
_textQR.visibility = View.INVISIBLE
|
||||
_buttonShare.visibility = View.INVISIBLE
|
||||
_buttonCopy.visibility = View.INVISIBLE
|
||||
} finally {
|
||||
_loader.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
_buttonShare.onClick.subscribe {
|
||||
val shareIntent = Intent(Intent.ACTION_SEND).apply {
|
||||
type = "text/plain";
|
||||
putExtra(Intent.EXTRA_TEXT, _exportBundle);
|
||||
}
|
||||
startActivity(Intent.createChooser(shareIntent, getString(R.string.share_text)));
|
||||
val shareIntent = Intent(Intent.ACTION_VIEW, Uri.parse(_exportBundle))
|
||||
startActivity(Intent.createChooser(shareIntent, "Share ID"));
|
||||
};
|
||||
|
||||
_buttonCopy.onClick.subscribe {
|
||||
|
||||
+46
-21
@@ -3,6 +3,7 @@ package com.futo.platformplayer.activities
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageButton
|
||||
import android.widget.LinearLayout
|
||||
@@ -10,16 +11,17 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.polycentric.PolycentricStorage
|
||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StatePolycentric
|
||||
import com.futo.platformplayer.views.LoaderView
|
||||
import com.futo.polycentric.core.ApiMethods
|
||||
import com.futo.polycentric.core.ProcessHandle
|
||||
import com.futo.polycentric.core.Store
|
||||
import com.futo.polycentric.core.Synchronization
|
||||
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@@ -27,6 +29,7 @@ class PolycentricCreateProfileActivity : AppCompatActivity() {
|
||||
private lateinit var _buttonHelp: ImageButton;
|
||||
private lateinit var _profileName: EditText;
|
||||
private lateinit var _buttonCreate: LinearLayout;
|
||||
private lateinit var _loader: LoaderView;
|
||||
private val TAG = "PolycentricCreateProfileActivity";
|
||||
|
||||
private var _creating = false;
|
||||
@@ -43,6 +46,7 @@ class PolycentricCreateProfileActivity : AppCompatActivity() {
|
||||
_buttonHelp = findViewById(R.id.button_help);
|
||||
_profileName = findViewById(R.id.edit_profile_name);
|
||||
_buttonCreate = findViewById(R.id.button_create_profile);
|
||||
_loader = findViewById(R.id.loader);
|
||||
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
|
||||
finish();
|
||||
};
|
||||
@@ -65,28 +69,49 @@ class PolycentricCreateProfileActivity : AppCompatActivity() {
|
||||
return@setOnClickListener;
|
||||
}
|
||||
|
||||
_profileName.isEnabled = false;
|
||||
_buttonCreate.visibility = View.GONE;
|
||||
_loader.start();
|
||||
_loader.visibility = View.VISIBLE;
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val processHandle: ProcessHandle;
|
||||
|
||||
try {
|
||||
processHandle = ProcessHandle.create();
|
||||
Store.instance.addProcessSecret(processHandle.processSecret);
|
||||
processHandle.addServer("https://srv1-stg.polycentric.io");
|
||||
processHandle.setUsername(username);
|
||||
StatePolycentric.instance.setProcessHandle(processHandle);
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, getString(R.string.failed_to_create_profile), e);
|
||||
return@launch;
|
||||
} finally {
|
||||
_creating = false;
|
||||
try {
|
||||
processHandle = ProcessHandle.create();
|
||||
Store.instance.addProcessSecret(processHandle.processSecret);
|
||||
|
||||
try {
|
||||
PolycentricStorage.instance.addProcessSecret(processHandle.processSecret)
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
|
||||
}
|
||||
|
||||
processHandle.addServer(ApiMethods.SERVER);
|
||||
processHandle.setUsername(username);
|
||||
StatePolycentric.instance.setProcessHandle(processHandle);
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, getString(R.string.failed_to_create_profile), e);
|
||||
return@launch;
|
||||
} finally {
|
||||
_creating = false;
|
||||
}
|
||||
|
||||
try {
|
||||
Logger.i(TAG, "Started backfill");
|
||||
processHandle.fullyBackfillServersAnnounceExceptions();
|
||||
Logger.i(TAG, "Finished backfill");
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, getString(R.string.failed_to_fully_backfill_servers), e);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Logger.i(TAG, "Started backfill");
|
||||
processHandle.fullyBackfillServersAnnounceExceptions();
|
||||
Logger.i(TAG, "Finished backfill");
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, getString(R.string.failed_to_fully_backfill_servers), e);
|
||||
finally {
|
||||
withContext(Dispatchers.Main) {
|
||||
_profileName.isEnabled = true;
|
||||
_buttonCreate.visibility = View.VISIBLE;
|
||||
_loader.stop();
|
||||
_loader.visibility = View.GONE;
|
||||
}
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.os.Bundle
|
||||
import android.util.TypedValue
|
||||
import android.widget.ImageButton
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.ScrollView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.request.target.CustomTarget
|
||||
@@ -28,6 +29,7 @@ class PolycentricHomeActivity : AppCompatActivity() {
|
||||
private lateinit var _buttonNewProfile: BigButton;
|
||||
private lateinit var _buttonImportProfile: BigButton;
|
||||
private lateinit var _layoutButtons: LinearLayout;
|
||||
private lateinit var _scroll: ScrollView;
|
||||
|
||||
override fun attachBaseContext(newBase: Context?) {
|
||||
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
||||
@@ -42,6 +44,7 @@ class PolycentricHomeActivity : AppCompatActivity() {
|
||||
_buttonNewProfile = findViewById(R.id.button_new_profile);
|
||||
_buttonImportProfile = findViewById(R.id.button_import_profile);
|
||||
_layoutButtons = findViewById(R.id.layout_buttons);
|
||||
_scroll = findViewById(R.id.scroll);
|
||||
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
|
||||
finish();
|
||||
};
|
||||
@@ -78,6 +81,7 @@ class PolycentricHomeActivity : AppCompatActivity() {
|
||||
|
||||
_layoutButtons.addView(profileButton, 0);
|
||||
}
|
||||
_scroll.invalidate();
|
||||
|
||||
_buttonHelp.setOnClickListener {
|
||||
startActivity(Intent(this, PolycentricWhyActivity::class.java));
|
||||
|
||||
+9
-2
@@ -12,11 +12,12 @@ import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.polycentric.PolycentricStorage
|
||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StatePolycentric
|
||||
import com.futo.platformplayer.views.overlays.LoaderOverlay
|
||||
import com.futo.polycentric.core.ApiMethods
|
||||
import com.futo.polycentric.core.KeyPair
|
||||
import com.futo.polycentric.core.Process
|
||||
import com.futo.polycentric.core.ProcessSecret
|
||||
@@ -126,6 +127,12 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
||||
val processSecret = ProcessSecret(keyPair, Process.random());
|
||||
Store.instance.addProcessSecret(processSecret);
|
||||
|
||||
try {
|
||||
PolycentricStorage.instance.addProcessSecret(processSecret)
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
|
||||
}
|
||||
|
||||
val processHandle = processSecret.toProcessHandle();
|
||||
|
||||
for (e in exportBundle.events.eventsList) {
|
||||
@@ -138,7 +145,7 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
StatePolycentric.instance.setProcessHandle(processHandle);
|
||||
processHandle.fullyBackfillClient(PolycentricCache.SERVER);
|
||||
processHandle.fullyBackfillClient(ApiMethods.SERVER);
|
||||
withContext(Dispatchers.Main) {
|
||||
startActivity(Intent(this@PolycentricImportProfileActivity, PolycentricProfileActivity::class.java));
|
||||
finish();
|
||||
|
||||
@@ -21,18 +21,20 @@ import com.bumptech.glide.Glide
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.dp
|
||||
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.polycentric.PolycentricStorage
|
||||
import com.futo.platformplayer.selectBestImage
|
||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StatePolycentric
|
||||
import com.futo.platformplayer.views.buttons.BigButton
|
||||
import com.futo.platformplayer.views.overlays.LoaderOverlay
|
||||
import com.futo.polycentric.core.ApiMethods
|
||||
import com.futo.polycentric.core.Store
|
||||
import com.futo.polycentric.core.SystemState
|
||||
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
|
||||
import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl
|
||||
import com.futo.polycentric.core.toBase64Url
|
||||
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||
import com.github.dhaval2404.imagepicker.ImagePicker
|
||||
@@ -47,6 +49,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
||||
private lateinit var _buttonHelp: ImageButton;
|
||||
private lateinit var _editName: EditText;
|
||||
private lateinit var _buttonExport: BigButton;
|
||||
private lateinit var _buttonOpenHarborProfile: BigButton;
|
||||
private lateinit var _buttonLogout: BigButton;
|
||||
private lateinit var _buttonDelete: BigButton;
|
||||
private lateinit var _username: String;
|
||||
@@ -68,10 +71,14 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
||||
_imagePolycentric = findViewById(R.id.image_polycentric);
|
||||
_editName = findViewById(R.id.edit_profile_name);
|
||||
_buttonExport = findViewById(R.id.button_export);
|
||||
_buttonOpenHarborProfile = findViewById(R.id.button_open_harbor_profile);
|
||||
_buttonLogout = findViewById(R.id.button_logout);
|
||||
_buttonDelete = findViewById(R.id.button_delete);
|
||||
_loaderOverlay = findViewById(R.id.loader_overlay);
|
||||
_textSystem = findViewById(R.id.text_system)
|
||||
findViewById<TextView>(R.id.text_cta2).setOnClickListener {
|
||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://harbor.social")))
|
||||
}
|
||||
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
|
||||
saveIfRequired();
|
||||
finish();
|
||||
@@ -92,6 +99,16 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
||||
startActivity(Intent(this, PolycentricBackupActivity::class.java));
|
||||
};
|
||||
|
||||
_buttonOpenHarborProfile.onClick.subscribe {
|
||||
val processHandle = StatePolycentric.instance.processHandle!!;
|
||||
processHandle?.let {
|
||||
val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(it.system));
|
||||
val url = it.system.systemToURLInfoSystemLinkUrl(systemState.servers.asIterable());
|
||||
val navUrl = "https://harbor.social/" + url.substring("polycentric://".length)
|
||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(navUrl)))
|
||||
}
|
||||
}
|
||||
|
||||
_buttonLogout.onClick.subscribe {
|
||||
StatePolycentric.instance.setProcessHandle(null);
|
||||
startActivity(Intent(this, PolycentricHomeActivity::class.java));
|
||||
@@ -108,6 +125,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
||||
|
||||
StatePolycentric.instance.setProcessHandle(null);
|
||||
Store.instance.removeProcessSecret(processHandle.system);
|
||||
PolycentricStorage.instance.removeProcessSecret(processHandle.system);
|
||||
startActivity(Intent(this, PolycentricHomeActivity::class.java));
|
||||
finish();
|
||||
});
|
||||
@@ -127,7 +145,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
processHandle.fullyBackfillClient(PolycentricCache.SERVER)
|
||||
processHandle.fullyBackfillClient(ApiMethods.SERVER)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
updateUI();
|
||||
|
||||
@@ -18,6 +18,7 @@ import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.*
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.views.LoaderView
|
||||
@@ -184,12 +185,19 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
|
||||
resultLauncher.launch(intent);
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
settingsActivityClosed.emit()
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
//TODO: Temporary for solving Settings issues
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private var _lastActivity: SettingsActivity? = null;
|
||||
|
||||
val settingsActivityClosed = Event0()
|
||||
|
||||
fun getActivity(): SettingsActivity? {
|
||||
val act = _lastActivity;
|
||||
if(act != null && !act._isFinished)
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
package com.futo.platformplayer.activities
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.ImageButton
|
||||
import android.widget.LinearLayout
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateSync
|
||||
import com.futo.platformplayer.sync.internal.LinkType
|
||||
import com.futo.platformplayer.sync.internal.SyncSession
|
||||
import com.futo.platformplayer.views.sync.SyncDeviceView
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class SyncHomeActivity : AppCompatActivity() {
|
||||
private lateinit var _layoutDevices: LinearLayout
|
||||
private lateinit var _layoutEmpty: LinearLayout
|
||||
private val _viewMap: MutableMap<String, SyncDeviceView> = mutableMapOf()
|
||||
|
||||
override fun attachBaseContext(newBase: Context?) {
|
||||
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (StateApp.instance.contextOrNull == null) {
|
||||
Logger.w(TAG, "No main activity, restarting main.")
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
startActivity(intent)
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
setContentView(R.layout.activity_sync_home)
|
||||
setNavigationBarColorAndIcons()
|
||||
|
||||
_layoutDevices = findViewById(R.id.layout_devices)
|
||||
_layoutEmpty = findViewById(R.id.layout_empty)
|
||||
|
||||
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
|
||||
finish()
|
||||
}
|
||||
|
||||
findViewById<LinearLayout>(R.id.button_link_new_device).setOnClickListener {
|
||||
startActivity(Intent(this@SyncHomeActivity, SyncPairActivity::class.java))
|
||||
}
|
||||
|
||||
findViewById<LinearLayout>(R.id.button_show_pairing_code).setOnClickListener {
|
||||
startActivity(Intent(this@SyncHomeActivity, SyncShowPairingCodeActivity::class.java))
|
||||
}
|
||||
|
||||
initializeDevices()
|
||||
|
||||
StateSync.instance.deviceUpdatedOrAdded.subscribe(this) { publicKey, session ->
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
val view = _viewMap[publicKey]
|
||||
if (!session.isAuthorized) {
|
||||
if (view != null) {
|
||||
_viewMap.remove(publicKey)
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (view == null) {
|
||||
val syncDeviceView = SyncDeviceView(this@SyncHomeActivity)
|
||||
syncDeviceView.onRemove.subscribe {
|
||||
StateApp.instance.scopeOrNull?.launch {
|
||||
StateSync.instance.delete(publicKey)
|
||||
}
|
||||
}
|
||||
val v = updateDeviceView(syncDeviceView, publicKey, session)
|
||||
_layoutDevices.addView(v, 0)
|
||||
_viewMap[publicKey] = v
|
||||
} else {
|
||||
updateDeviceView(view, publicKey, session)
|
||||
}
|
||||
|
||||
updateEmptyVisibility()
|
||||
}
|
||||
}
|
||||
|
||||
StateSync.instance.deviceRemoved.subscribe(this) {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
val view = _viewMap[it]
|
||||
if (view != null) {
|
||||
_layoutDevices.removeView(view)
|
||||
_viewMap.remove(it)
|
||||
}
|
||||
|
||||
updateEmptyVisibility()
|
||||
}
|
||||
}
|
||||
|
||||
StateSync.instance.confirmStarted(this, onStarted = {
|
||||
if (StateSync.instance.syncService?.serverSocketFailedToStart == true) {
|
||||
UIDialogs.toast(this, "Server socket failed to start, is the port in use?", true)
|
||||
}
|
||||
if (StateSync.instance.syncService?.relayConnected == false) {
|
||||
UIDialogs.toast(this, "Not connected to relay, remote connections will work.", false)
|
||||
}
|
||||
if (StateSync.instance.syncService?.serverSocketStarted == false) {
|
||||
UIDialogs.toast(this, "Listener not started, local connections will not work.", false)
|
||||
}
|
||||
}, onNotStarted = {
|
||||
finish()
|
||||
})
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
|
||||
StateSync.instance.deviceUpdatedOrAdded.remove(this)
|
||||
StateSync.instance.deviceRemoved.remove(this)
|
||||
}
|
||||
|
||||
private fun updateDeviceView(syncDeviceView: SyncDeviceView, publicKey: String, session: SyncSession?): SyncDeviceView {
|
||||
val connected = session?.connected ?: false
|
||||
val authorized = session?.isAuthorized ?: false
|
||||
|
||||
syncDeviceView.setLinkType(session?.linkType ?: LinkType.None)
|
||||
.setName(session?.displayName ?: StateSync.instance.getCachedName(publicKey) ?: publicKey)
|
||||
//TODO: also display public key?
|
||||
.setStatus(if (connected && authorized) "Connected" else "Disconnected or unauthorized")
|
||||
return syncDeviceView
|
||||
}
|
||||
|
||||
private fun updateEmptyVisibility() {
|
||||
if (_viewMap.isNotEmpty()) {
|
||||
_layoutEmpty.visibility = View.GONE
|
||||
} else {
|
||||
_layoutEmpty.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
private fun initializeDevices() {
|
||||
_layoutDevices.removeAllViews()
|
||||
|
||||
for (publicKey in StateSync.instance.getAll()) {
|
||||
val syncDeviceView = SyncDeviceView(this)
|
||||
syncDeviceView.onRemove.subscribe {
|
||||
StateApp.instance.scopeOrNull?.launch {
|
||||
StateSync.instance.delete(publicKey)
|
||||
}
|
||||
}
|
||||
val view = updateDeviceView(syncDeviceView, publicKey, StateSync.instance.getSession(publicKey))
|
||||
_layoutDevices.addView(view)
|
||||
_viewMap[publicKey] = view
|
||||
}
|
||||
|
||||
updateEmptyVisibility()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "SyncHomeActivity"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package com.futo.platformplayer.activities
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.util.Base64
|
||||
import android.view.View
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageButton
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateSync
|
||||
import com.futo.platformplayer.sync.internal.SyncDeviceInfo
|
||||
import com.google.zxing.integration.android.IntentIntegrator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
class SyncPairActivity : AppCompatActivity() {
|
||||
private lateinit var _editCode: EditText
|
||||
|
||||
private lateinit var _layoutPairing: LinearLayout
|
||||
private lateinit var _textPairingStatus: TextView
|
||||
|
||||
private lateinit var _layoutPairingSuccess: LinearLayout
|
||||
|
||||
private lateinit var _layoutPairingError: LinearLayout
|
||||
private lateinit var _textError: TextView
|
||||
|
||||
private val _qrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
|
||||
scanResult?.let {
|
||||
if (it.contents != null) {
|
||||
_editCode.text.clear()
|
||||
_editCode.text.append(it.contents)
|
||||
pair(it.contents)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun attachBaseContext(newBase: Context?) {
|
||||
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_sync_pair)
|
||||
setNavigationBarColorAndIcons()
|
||||
|
||||
_editCode = findViewById(R.id.edit_code)
|
||||
_layoutPairing = findViewById(R.id.layout_pairing)
|
||||
_textPairingStatus = findViewById(R.id.text_pairing_status)
|
||||
_layoutPairingSuccess = findViewById(R.id.layout_pairing_success)
|
||||
_layoutPairingError = findViewById(R.id.layout_pairing_error)
|
||||
_textError = findViewById(R.id.text_error)
|
||||
|
||||
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
|
||||
finish()
|
||||
}
|
||||
|
||||
findViewById<LinearLayout>(R.id.button_scan_qr).setOnClickListener {
|
||||
val integrator = IntentIntegrator(this)
|
||||
integrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
|
||||
integrator.setPrompt(getString(R.string.scan_a_qr_code))
|
||||
integrator.setOrientationLocked(true);
|
||||
integrator.setCameraId(0)
|
||||
integrator.setBeepEnabled(false)
|
||||
integrator.setBarcodeImageEnabled(true)
|
||||
integrator.setCaptureActivity(QRCaptureActivity::class.java);
|
||||
_qrCodeResultLauncher.launch(integrator.createScanIntent())
|
||||
}
|
||||
|
||||
findViewById<LinearLayout>(R.id.button_link_new_device).setOnClickListener {
|
||||
pair(_editCode.text.toString())
|
||||
}
|
||||
|
||||
_layoutPairingSuccess.setOnClickListener {
|
||||
_layoutPairingSuccess.visibility = View.GONE
|
||||
finish()
|
||||
}
|
||||
_layoutPairingError.setOnClickListener {
|
||||
_layoutPairingError.visibility = View.GONE
|
||||
}
|
||||
_layoutPairingSuccess.visibility = View.GONE
|
||||
_layoutPairingError.visibility = View.GONE
|
||||
}
|
||||
|
||||
fun pair(url: String) {
|
||||
try {
|
||||
_layoutPairing.visibility = View.VISIBLE
|
||||
_textPairingStatus.text = "Parsing text..."
|
||||
|
||||
if (!url.startsWith("grayjay://sync/")) {
|
||||
throw Exception("Not a valid URL: $url")
|
||||
}
|
||||
|
||||
val deviceInfo: SyncDeviceInfo = Json.decodeFromString<SyncDeviceInfo>(Base64.decode(url.substring("grayjay://sync/".length), Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP).decodeToString())
|
||||
if (StateSync.instance.isAuthorized(deviceInfo.publicKey)) {
|
||||
throw Exception("This device is already paired")
|
||||
}
|
||||
|
||||
_textPairingStatus.text = "Connecting..."
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
StateSync.instance.syncService?.connect(deviceInfo) { complete, message ->
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
if (complete != null) {
|
||||
if (complete) {
|
||||
_layoutPairingSuccess.visibility = View.VISIBLE
|
||||
_layoutPairing.visibility = View.GONE
|
||||
} else {
|
||||
_textError.text = message
|
||||
_layoutPairingError.visibility = View.VISIBLE
|
||||
_layoutPairing.visibility = View.GONE
|
||||
}
|
||||
} else {
|
||||
_textPairingStatus.text = message
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
withContext(Dispatchers.Main) {
|
||||
_layoutPairingError.visibility = View.VISIBLE
|
||||
if(e.message == "Failed to connect") {
|
||||
_textError.text = "Failed to connect.\n\nThis may be due to not being on the same network, due to firewall, or vpn.\nSync currently operates only over local direct connections."
|
||||
}
|
||||
else
|
||||
_textError.text = e.message
|
||||
_layoutPairing.visibility = View.GONE
|
||||
Logger.e(TAG, "Failed to pair", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch(e: Throwable) {
|
||||
_layoutPairingError.visibility = View.VISIBLE
|
||||
_textError.text = e.message
|
||||
_layoutPairing.visibility = View.GONE
|
||||
Logger.e(TAG, "Failed to pair", e)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "SyncPairActivity"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package com.futo.platformplayer.activities
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.util.Base64
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateSync
|
||||
import com.futo.platformplayer.sync.internal.SyncDeviceInfo
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.MultiFormatWriter
|
||||
import com.google.zxing.common.BitMatrix
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.net.NetworkInterface
|
||||
|
||||
class SyncShowPairingCodeActivity : AppCompatActivity() {
|
||||
private lateinit var _textCode: TextView
|
||||
private lateinit var _imageQR: ImageView
|
||||
private lateinit var _textQR: TextView
|
||||
private var _code: String? = null
|
||||
|
||||
override fun attachBaseContext(newBase: Context?) {
|
||||
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
activity = null
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
activity = this
|
||||
|
||||
setContentView(R.layout.activity_sync_show_pairing_code)
|
||||
setNavigationBarColorAndIcons()
|
||||
|
||||
_textCode = findViewById(R.id.text_code)
|
||||
_imageQR = findViewById(R.id.image_qr)
|
||||
_textQR = findViewById(R.id.text_scan_qr)
|
||||
|
||||
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
|
||||
finish()
|
||||
}
|
||||
|
||||
findViewById<LinearLayout>(R.id.button_copy).setOnClickListener {
|
||||
val code = _code ?: return@setOnClickListener
|
||||
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager;
|
||||
val clip = ClipData.newPlainText(getString(R.string.copied_text), code);
|
||||
clipboard.setPrimaryClip(clip);
|
||||
UIDialogs.toast(this, "Copied to clipboard")
|
||||
}
|
||||
|
||||
val ips = getIPs()
|
||||
val publicKey = StateSync.instance.syncService?.publicKey
|
||||
val pairingCode = StateSync.instance.syncService?.pairingCode
|
||||
if (publicKey == null || pairingCode == null) {
|
||||
setCode("Public key or pairing code was not known, is sync enabled?")
|
||||
} else {
|
||||
val selfDeviceInfo = SyncDeviceInfo(publicKey, ips.toTypedArray(), StateSync.PORT, pairingCode)
|
||||
val json = Json.encodeToString(selfDeviceInfo)
|
||||
val base64 = Base64.encodeToString(json.toByteArray(), Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP)
|
||||
val url = "grayjay://sync/${base64}"
|
||||
setCode(url)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun setCode(code: String?) {
|
||||
_code = code
|
||||
|
||||
_textCode.text = code
|
||||
|
||||
if (code == null) {
|
||||
_imageQR.visibility = View.INVISIBLE
|
||||
_textQR.visibility = View.INVISIBLE
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
val dimension = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 200f, resources.displayMetrics).toInt()
|
||||
val qrCodeBitmap = generateQRCode(code, dimension, dimension)
|
||||
_imageQR.setImageBitmap(qrCodeBitmap)
|
||||
_imageQR.visibility = View.VISIBLE
|
||||
_textQR.visibility = View.VISIBLE
|
||||
} catch (e: Exception) {
|
||||
Logger.e(TAG, getString(R.string.failed_to_generate_qr_code), e)
|
||||
_imageQR.visibility = View.INVISIBLE
|
||||
_textQR.visibility = View.INVISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateQRCode(content: String, width: Int, height: Int): Bitmap {
|
||||
val bitMatrix = MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height);
|
||||
return bitMatrixToBitmap(bitMatrix);
|
||||
}
|
||||
|
||||
private fun bitMatrixToBitmap(matrix: BitMatrix): Bitmap {
|
||||
val width = matrix.width;
|
||||
val height = matrix.height;
|
||||
val bmp = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
|
||||
|
||||
for (x in 0 until width) {
|
||||
for (y in 0 until height) {
|
||||
bmp.setPixel(x, y, if (matrix[x, y]) Color.BLACK else Color.WHITE);
|
||||
}
|
||||
}
|
||||
return bmp;
|
||||
}
|
||||
|
||||
private fun getIPs(): List<String> {
|
||||
val ips = arrayListOf<String>()
|
||||
for (intf in NetworkInterface.getNetworkInterfaces()) {
|
||||
for (addr in intf.inetAddresses) {
|
||||
if (addr.isLoopbackAddress) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (addr.address.size != 4) {
|
||||
continue
|
||||
}
|
||||
|
||||
addr.hostAddress?.let { ips.add(it) }
|
||||
}
|
||||
}
|
||||
return ips
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "SyncShowPairingCodeActivity"
|
||||
var activity: SyncShowPairingCodeActivity? = null
|
||||
private set
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
package com.futo.platformplayer.api.http
|
||||
|
||||
import androidx.collection.arrayMapOf
|
||||
import com.futo.platformplayer.SettingsDev
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.ensureNotMainThread
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
import okhttp3.Call
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.OkHttpClient
|
||||
@@ -13,10 +17,16 @@ import okhttp3.Response
|
||||
import okhttp3.ResponseBody
|
||||
import okhttp3.WebSocket
|
||||
import okhttp3.WebSocketListener
|
||||
import java.security.SecureRandom
|
||||
import java.security.cert.X509Certificate
|
||||
import java.time.Duration
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.TrustManager
|
||||
import javax.net.ssl.X509TrustManager
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
open class ManagedHttpClient {
|
||||
protected val _builderTemplate: OkHttpClient.Builder;
|
||||
protected var _builderTemplate: OkHttpClient.Builder;
|
||||
|
||||
private var client: OkHttpClient;
|
||||
|
||||
@@ -25,8 +35,38 @@ open class ManagedHttpClient {
|
||||
|
||||
var user_agent = "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0"
|
||||
|
||||
fun setTimeout(timeout: Long) {
|
||||
rebuildClient {
|
||||
it.callTimeout(Duration.ofMillis(client.callTimeoutMillis.toLong()))
|
||||
.writeTimeout(Duration.ofMillis(client.writeTimeoutMillis.toLong()))
|
||||
.readTimeout(Duration.ofMillis(client.readTimeoutMillis.toLong()))
|
||||
.connectTimeout(Duration.ofMillis(timeout));
|
||||
}
|
||||
}
|
||||
|
||||
private val trustAllCerts = arrayOf<TrustManager>(
|
||||
object: X509TrustManager {
|
||||
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) { }
|
||||
override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) { }
|
||||
override fun getAcceptedIssuers(): Array<X509Certificate> {
|
||||
return arrayOf();
|
||||
}
|
||||
}
|
||||
);
|
||||
private fun trustAllCertificates(builder: OkHttpClient.Builder) {
|
||||
val sslContext = SSLContext.getInstance("SSL");
|
||||
sslContext.init(null, trustAllCerts, SecureRandom());
|
||||
builder.sslSocketFactory(sslContext.socketFactory, trustAllCerts[0] as X509TrustManager);
|
||||
builder.hostnameVerifier { a, b ->
|
||||
return@hostnameVerifier true;
|
||||
}
|
||||
Logger.w(TAG, "Creating INSECURE client (TrustAll)");
|
||||
}
|
||||
|
||||
constructor(builder: OkHttpClient.Builder = OkHttpClient.Builder()) {
|
||||
_builderTemplate = builder;
|
||||
if(FragmentedStorage.isInitialized && StateApp.instance.isMainActive && SettingsDev.instance.developerMode && SettingsDev.instance.networking.allowAllCertificates)
|
||||
trustAllCertificates(builder);
|
||||
client = builder.addNetworkInterceptor { chain ->
|
||||
val request = beforeRequest(chain.request());
|
||||
val response = afterRequest(chain.proceed(request));
|
||||
@@ -34,6 +74,15 @@ open class ManagedHttpClient {
|
||||
}.build();
|
||||
}
|
||||
|
||||
fun rebuildClient(modify: (OkHttpClient.Builder) -> OkHttpClient.Builder) {
|
||||
_builderTemplate = modify(_builderTemplate);
|
||||
client = _builderTemplate.addNetworkInterceptor { chain ->
|
||||
val request = beforeRequest(chain.request());
|
||||
val response = afterRequest(chain.proceed(request));
|
||||
return@addNetworkInterceptor response;
|
||||
}.build();
|
||||
}
|
||||
|
||||
open fun clone(): ManagedHttpClient {
|
||||
val clonedClient = ManagedHttpClient(_builderTemplate);
|
||||
clonedClient.user_agent = user_agent;
|
||||
@@ -41,6 +90,7 @@ open class ManagedHttpClient {
|
||||
}
|
||||
|
||||
fun tryHead(url: String): Map<String, String>? {
|
||||
ensureNotMainThread()
|
||||
try {
|
||||
val result = head(url);
|
||||
if(result.isOk)
|
||||
@@ -55,7 +105,7 @@ open class ManagedHttpClient {
|
||||
}
|
||||
|
||||
fun socket(url: String, headers: MutableMap<String, String> = HashMap(), listener: SocketListener): Socket {
|
||||
|
||||
ensureNotMainThread()
|
||||
val requestBuilder: okhttp3.Request.Builder = okhttp3.Request.Builder()
|
||||
.url(url);
|
||||
if(user_agent.isNotEmpty() && !headers.any { it.key.lowercase() == "user-agent" })
|
||||
@@ -251,6 +301,7 @@ open class ManagedHttpClient {
|
||||
}
|
||||
|
||||
fun send(msg: String) {
|
||||
ensureNotMainThread()
|
||||
socket.send(msg);
|
||||
}
|
||||
|
||||
|
||||
@@ -210,6 +210,20 @@ class HttpContext : AutoCloseable {
|
||||
}
|
||||
}
|
||||
}
|
||||
fun respondBytes(status: Int, headers: HttpHeaders, body: ByteArray? = null) {
|
||||
if(headers.get("content-length").isNullOrEmpty()) {
|
||||
if (body != null) {
|
||||
headers.put("content-length", body.size.toString());
|
||||
} else {
|
||||
headers.put("content-length", "0")
|
||||
}
|
||||
}
|
||||
respond(status, headers) { responseStream ->
|
||||
if(body != null) {
|
||||
responseStream.write(body);
|
||||
}
|
||||
}
|
||||
}
|
||||
fun respond(status: Int, headers: HttpHeaders, writing: (OutputStream)->Unit) {
|
||||
val responseStream = _responseStream ?: throw IllegalStateException("No response stream set");
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ package com.futo.platformplayer.api.http.server
|
||||
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.api.http.server.exceptions.EmptyRequestException
|
||||
import com.futo.platformplayer.api.http.server.handlers.HttpFuntionHandler
|
||||
import com.futo.platformplayer.api.http.server.handlers.HttpFunctionHandler
|
||||
import com.futo.platformplayer.api.http.server.handlers.HttpHandler
|
||||
import com.futo.platformplayer.api.http.server.handlers.HttpOptionsAllowHandler
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
@@ -208,20 +208,20 @@ class ManagedHttpServer(private val _requestedPort: Int = 0) {
|
||||
|
||||
for(getMethod in getMethods)
|
||||
if(getMethod.first.parameterTypes.firstOrNull() == HttpContext::class.java && getMethod.first.parameterCount == 1)
|
||||
addHandler(HttpFuntionHandler("GET", getMethod.second.path) { getMethod.first.invoke(obj, it) }).apply {
|
||||
addHandler(HttpFunctionHandler("GET", getMethod.second.path) { getMethod.first.invoke(obj, it) }).apply {
|
||||
if(!getMethod.second.contentType.isEmpty())
|
||||
this.withContentType(getMethod.second.contentType);
|
||||
}.withContentType(getMethod.second.contentType);
|
||||
for(postMethod in postMethods)
|
||||
if(postMethod.first.parameterTypes.firstOrNull() == HttpContext::class.java && postMethod.first.parameterCount == 1)
|
||||
addHandler(HttpFuntionHandler("POST", postMethod.second.path) { postMethod.first.invoke(obj, it) }).apply {
|
||||
addHandler(HttpFunctionHandler("POST", postMethod.second.path) { postMethod.first.invoke(obj, it) }).apply {
|
||||
if(!postMethod.second.contentType.isEmpty())
|
||||
this.withContentType(postMethod.second.contentType);
|
||||
}.withContentType(postMethod.second.contentType);
|
||||
|
||||
for(getField in getFields) {
|
||||
getField.first.isAccessible = true;
|
||||
addHandler(HttpFuntionHandler("GET", getField.second.path) {
|
||||
addHandler(HttpFunctionHandler("GET", getField.second.path) {
|
||||
val value = getField.first.get(obj) as String?;
|
||||
if(value != null) {
|
||||
val headers = HttpHeaders(
|
||||
|
||||
+1
-1
@@ -73,7 +73,7 @@ class HttpFileHandler(method: String, path: String, private val contentType: Str
|
||||
Logger.v(TAG, "Sent bytes $current-${current + bytesToSend}, totalBytesSent=$totalBytesSent")
|
||||
|
||||
current += bytesToSend.toLong()
|
||||
if (current >= end) {
|
||||
if (current > end) {
|
||||
Logger.i(TAG, "Expected amount of bytes sent")
|
||||
break
|
||||
}
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ package com.futo.platformplayer.api.http.server.handlers
|
||||
|
||||
import com.futo.platformplayer.api.http.server.HttpContext
|
||||
|
||||
class HttpFuntionHandler(method: String, path: String, val handler: (HttpContext)->Unit) : HttpHandler(method, path) {
|
||||
class HttpFunctionHandler(method: String, path: String, val handler: (HttpContext)->Unit) : HttpHandler(method, path) {
|
||||
override fun handle(httpContext: HttpContext) {
|
||||
httpContext.setResponseHeaders(this.headers);
|
||||
handler(httpContext);
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
package com.futo.platformplayer.api.media
|
||||
|
||||
import androidx.collection.LruCache
|
||||
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||
import com.futo.platformplayer.api.media.models.chapters.IChapter
|
||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
|
||||
import com.futo.platformplayer.api.media.models.live.ILiveChatWindowDescriptor
|
||||
import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
|
||||
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.models.ImageVariable
|
||||
|
||||
/**
|
||||
* A temporary class that caches video results
|
||||
* In future this should be part of a bigger system
|
||||
*/
|
||||
class CachedPlatformClient : IPlatformClient {
|
||||
private val _client : IPlatformClient;
|
||||
override val id: String get() = _client.id;
|
||||
override val name: String get() = _client.name;
|
||||
override val icon: ImageVariable? get() = _client.icon;
|
||||
|
||||
private val _cache: LruCache<String, IPlatformContentDetails>;
|
||||
|
||||
override val capabilities: PlatformClientCapabilities
|
||||
get() = _client.capabilities;
|
||||
|
||||
constructor(client : IPlatformClient, cacheSize : Int = 10 * 1024 * 1024) {
|
||||
this._client = client;
|
||||
this._cache = LruCache<String, IPlatformContentDetails>(cacheSize);
|
||||
}
|
||||
override fun initialize() { _client.initialize() }
|
||||
override fun disable() { _client.disable() }
|
||||
|
||||
override fun isContentDetailsUrl(url: String): Boolean = _client.isContentDetailsUrl(url);
|
||||
override fun getContentDetails(url: String): IPlatformContentDetails {
|
||||
var result = _cache.get(url);
|
||||
if(result == null) {
|
||||
result = _client.getContentDetails(url);
|
||||
_cache.put(url, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
override fun getContentChapters(url: String): List<IChapter> = _client.getContentChapters(url);
|
||||
override fun getPlaybackTracker(url: String): IPlaybackTracker? = _client.getPlaybackTracker(url);
|
||||
|
||||
override fun isChannelUrl(url: String): Boolean = _client.isChannelUrl(url);
|
||||
override fun getChannel(channelUrl: String): IPlatformChannel = _client.getChannel(channelUrl);
|
||||
|
||||
override fun getChannelCapabilities(): ResultCapabilities = _client.getChannelCapabilities();
|
||||
override fun getChannelContents(
|
||||
channelUrl: String,
|
||||
type: String?,
|
||||
order: String?,
|
||||
filters: Map<String, List<String>>?
|
||||
): IPager<IPlatformContent> = _client.getChannelContents(channelUrl);
|
||||
|
||||
override fun getChannelUrlByClaim(claimType: Int, claimValues: Map<Int, String>): String? = _client.getChannelUrlByClaim(claimType, claimValues)
|
||||
|
||||
override fun searchSuggestions(query: String): Array<String> = _client.searchSuggestions(query);
|
||||
override fun getSearchCapabilities(): ResultCapabilities = _client.getSearchCapabilities();
|
||||
override fun search(
|
||||
query: String,
|
||||
type: String?,
|
||||
order: String?,
|
||||
filters: Map<String, List<String>>?
|
||||
): IPager<IPlatformContent> = _client.search(query, type, order, filters);
|
||||
|
||||
override fun getSearchChannelContentsCapabilities(): ResultCapabilities = _client.getSearchChannelContentsCapabilities();
|
||||
override fun searchChannelContents(
|
||||
channelUrl: String,
|
||||
query: String,
|
||||
type: String?,
|
||||
order: String?,
|
||||
filters: Map<String, List<String>>?
|
||||
): IPager<IPlatformContent> = _client.searchChannelContents(channelUrl, query, type, order, filters);
|
||||
|
||||
override fun searchChannels(query: String) = _client.searchChannels(query);
|
||||
|
||||
override fun getComments(url: String): IPager<IPlatformComment> = _client.getComments(url);
|
||||
override fun getSubComments(comment: IPlatformComment): IPager<IPlatformComment> = _client.getSubComments(comment);
|
||||
|
||||
override fun getLiveChatWindow(url: String): ILiveChatWindowDescriptor? = _client.getLiveChatWindow(url);
|
||||
override fun getLiveEvents(url: String): IPager<IPlatformLiveEvent>? = _client.getLiveEvents(url);
|
||||
|
||||
override fun getHome(): IPager<IPlatformContent> = _client.getHome();
|
||||
|
||||
override fun getUserSubscriptions(): Array<String> { return arrayOf(); };
|
||||
|
||||
override fun searchPlaylists(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = _client.searchPlaylists(query, type, order, filters);
|
||||
override fun isPlaylistUrl(url: String): Boolean = _client.isPlaylistUrl(url);
|
||||
override fun getPlaylist(url: String): IPlatformPlaylistDetails = _client.getPlaylist(url);
|
||||
override fun getUserPlaylists(): Array<String> { return arrayOf(); };
|
||||
|
||||
override fun isClaimTypeSupported(claimType: Int): Boolean {
|
||||
return _client.isClaimTypeSupported(claimType);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.futo.platformplayer.api.media
|
||||
|
||||
import com.futo.platformplayer.api.media.models.IPlatformChannelContent
|
||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||
@@ -10,6 +11,7 @@ import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
|
||||
import com.futo.platformplayer.api.media.models.live.ILiveChatWindowDescriptor
|
||||
import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
|
||||
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.models.ImageVariable
|
||||
@@ -65,6 +67,11 @@ interface IPlatformClient {
|
||||
*/
|
||||
fun searchChannels(query: String): IPager<PlatformAuthorLink>;
|
||||
|
||||
/**
|
||||
* Searches for channels and returns a content pager
|
||||
*/
|
||||
fun searchChannelsAsContent(query: String): IPager<IPlatformContent>;
|
||||
|
||||
|
||||
//Video Pages
|
||||
/**
|
||||
@@ -84,6 +91,20 @@ interface IPlatformClient {
|
||||
*/
|
||||
fun getChannelContents(channelUrl: String, type: String? = null, order: String? = null, filters: Map<String, List<String>>? = null): IPager<IPlatformContent>;
|
||||
|
||||
/**
|
||||
* Describes what the plugin is capable on peek channel results
|
||||
*/
|
||||
fun getPeekChannelTypes(): List<String>;
|
||||
/**
|
||||
* Peeks contents of a channel, upload time descending
|
||||
*/
|
||||
fun peekChannelContents(channelUrl: String, type: String? = null): List<IPlatformContent>
|
||||
|
||||
/**
|
||||
* Gets all playlists of a channel
|
||||
*/
|
||||
fun getChannelPlaylists(channelUrl: String): IPager<IPlatformPlaylist>
|
||||
|
||||
/**
|
||||
* Gets the channel url associated with a claimType
|
||||
*/
|
||||
@@ -106,6 +127,11 @@ interface IPlatformClient {
|
||||
*/
|
||||
fun getPlaybackTracker(url: String): IPlaybackTracker?;
|
||||
|
||||
/**
|
||||
* Get content recommendations
|
||||
*/
|
||||
fun getContentRecommendations(url: String): IPager<IPlatformContent>?;
|
||||
|
||||
|
||||
//Comments
|
||||
/**
|
||||
|
||||
@@ -13,10 +13,14 @@ data class PlatformClientCapabilities(
|
||||
val hasGetChannelUrlByClaim: Boolean = false,
|
||||
val hasGetChannelTemplateByClaimMap: Boolean = false,
|
||||
val hasGetSearchCapabilities: Boolean = false,
|
||||
val hasGetSearchChannelContentsCapabilities: Boolean = false,
|
||||
val hasGetChannelCapabilities: Boolean = false,
|
||||
val hasGetLiveEvents: Boolean = false,
|
||||
val hasGetLiveChatWindow: Boolean = false,
|
||||
val hasGetContentChapters: Boolean = false
|
||||
val hasGetContentChapters: Boolean = false,
|
||||
val hasPeekChannelContents: Boolean = false,
|
||||
val hasGetChannelPlaylists: Boolean = false,
|
||||
val hasGetContentRecommendations: Boolean = false
|
||||
) {
|
||||
|
||||
}
|
||||
@@ -13,13 +13,17 @@ class PlatformClientPool {
|
||||
private val _pool: HashMap<JSClient, Int> = hashMapOf();
|
||||
private var _poolCounter = 0;
|
||||
private val _poolName: String?;
|
||||
private val _privatePool: Boolean;
|
||||
private val _isolatedInitialization: Boolean
|
||||
|
||||
var isDead: Boolean = false
|
||||
private set;
|
||||
val onDead = Event2<JSClient, PlatformClientPool>();
|
||||
|
||||
constructor(parentClient: IPlatformClient, name: String? = null) {
|
||||
constructor(parentClient: IPlatformClient, name: String? = null, privatePool: Boolean = false, isolatedInitialization: Boolean = false) {
|
||||
_poolName = name;
|
||||
_privatePool = privatePool;
|
||||
_isolatedInitialization = isolatedInitialization
|
||||
if(parentClient !is JSClient)
|
||||
throw IllegalArgumentException("Pooling only supported for JSClients right now");
|
||||
Logger.i(TAG, "Pool for ${parentClient.name} was started");
|
||||
@@ -51,7 +55,7 @@ class PlatformClientPool {
|
||||
reserved = _pool.keys.find { !it.isBusy };
|
||||
if(reserved == null && _pool.size < capacity) {
|
||||
Logger.i(TAG, "Started additional [${_parent.name}] client in pool [${_poolName}] (${_pool.size + 1}/${capacity})");
|
||||
reserved = _parent.getCopy();
|
||||
reserved = _parent.getCopy(_privatePool, _isolatedInitialization);
|
||||
|
||||
reserved?.onCaptchaException?.subscribe { client, ex ->
|
||||
StateApp.instance.handleCaptchaException(client, ex);
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media
|
||||
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.ensureIsBusy
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.getOrThrowNullable
|
||||
@@ -44,6 +45,7 @@ class PlatformID {
|
||||
val NONE = PlatformID("Unknown", null);
|
||||
|
||||
fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformID {
|
||||
value.ensureIsBusy();
|
||||
val contextName = "PlatformID";
|
||||
return PlatformID(
|
||||
value.getOrThrow(config, "platform", contextName),
|
||||
|
||||
@@ -6,12 +6,16 @@ class PlatformMultiClientPool {
|
||||
private val _clientPools: HashMap<IPlatformClient, PlatformClientPool> = hashMapOf();
|
||||
|
||||
private var _isFake = false;
|
||||
private var _privatePool = false;
|
||||
private val _isolatedInitialization: Boolean
|
||||
|
||||
constructor(name: String, maxCap: Int = -1) {
|
||||
constructor(name: String, maxCap: Int = -1, isPrivatePool: Boolean = false, isolatedInitialization: Boolean = false) {
|
||||
_name = name;
|
||||
_maxCap = if(maxCap > 0)
|
||||
maxCap
|
||||
else 99;
|
||||
_privatePool = isPrivatePool;
|
||||
_isolatedInitialization = isolatedInitialization
|
||||
}
|
||||
|
||||
fun getClientPooled(parentClient: IPlatformClient, capacity: Int = _maxCap): IPlatformClient {
|
||||
@@ -19,7 +23,7 @@ class PlatformMultiClientPool {
|
||||
return parentClient;
|
||||
val pool = synchronized(_clientPools) {
|
||||
if(!_clientPools.containsKey(parentClient))
|
||||
_clientPools[parentClient] = PlatformClientPool(parentClient, _name).apply {
|
||||
_clientPools[parentClient] = PlatformClientPool(parentClient, _name, _privatePool, _isolatedInitialization).apply {
|
||||
this.onDead.subscribe { _, pool ->
|
||||
synchronized(_clientPools) {
|
||||
if(_clientPools[parentClient] == pool)
|
||||
|
||||
@@ -4,6 +4,6 @@ import kotlinx.serialization.json.Json
|
||||
|
||||
class Serializer {
|
||||
companion object {
|
||||
val json = Json { ignoreUnknownKeys = true; encodeDefaults = true; };
|
||||
val json = Json { ignoreUnknownKeys = true; encodeDefaults = true; coerceInputValues = true };
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,11 @@ package com.futo.platformplayer.api.media.models
|
||||
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.api.media.PlatformID
|
||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSContent
|
||||
import com.futo.platformplayer.ensureIsBusy
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
@@ -14,7 +18,7 @@ open class PlatformAuthorLink {
|
||||
val id: PlatformID;
|
||||
val name: String;
|
||||
val url: String;
|
||||
val thumbnail: String?;
|
||||
var thumbnail: String?;
|
||||
var subscribers: Long? = null; //Optional
|
||||
|
||||
constructor(id: PlatformID, name: String, url: String, thumbnail: String? = null, subscribers: Long? = null)
|
||||
@@ -27,7 +31,10 @@ open class PlatformAuthorLink {
|
||||
}
|
||||
|
||||
companion object {
|
||||
val UNKNOWN = PlatformAuthorLink(PlatformID.NONE, "Unknown", "", null, null);
|
||||
|
||||
fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformAuthorLink {
|
||||
value.ensureIsBusy();
|
||||
if(value.has("membershipUrl"))
|
||||
return PlatformAuthorMembershipLink.fromV8(config, value);
|
||||
|
||||
@@ -40,4 +47,21 @@ open class PlatformAuthorLink {
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface IPlatformChannelContent : IPlatformContent {
|
||||
val thumbnail: String?
|
||||
val subscribers: Long?
|
||||
}
|
||||
|
||||
open class JSChannelContent : JSContent, IPlatformChannelContent {
|
||||
override val contentType: ContentType get() = ContentType.CHANNEL
|
||||
override val thumbnail: String?
|
||||
override val subscribers: Long?
|
||||
|
||||
constructor(config: SourcePluginConfig, obj: V8ValueObject) : super(config, obj) {
|
||||
val contextName = "Channel";
|
||||
thumbnail = obj.getOrDefault<String>(config, "thumbnail", contextName, null)
|
||||
subscribers = if(obj.has("subscribers")) obj.getOrThrow(config,"subscribers", contextName) else null
|
||||
}
|
||||
}
|
||||
+2
@@ -3,6 +3,7 @@ package com.futo.platformplayer.api.media.models
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.api.media.PlatformID
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.ensureIsBusy
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
@@ -20,6 +21,7 @@ class PlatformAuthorMembershipLink: PlatformAuthorLink {
|
||||
|
||||
companion object {
|
||||
fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformAuthorMembershipLink {
|
||||
value.ensureIsBusy();
|
||||
val context = "AuthorMembershipLink"
|
||||
return PlatformAuthorMembershipLink(PlatformID.fromV8(config, value.getOrThrow(config, "id", context, false)),
|
||||
value.getOrThrow(config ,"name", context),
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.caoccao.javet.values.primitive.V8ValueInteger
|
||||
import com.caoccao.javet.values.reference.V8ValueArray
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.ensureIsBusy
|
||||
import com.futo.platformplayer.expectV8Variant
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
@@ -30,6 +31,7 @@ class ResultCapabilities(
|
||||
const val TYPE_POSTS = "POSTS";
|
||||
const val TYPE_MIXED = "MIXED";
|
||||
const val TYPE_SUBSCRIPTIONS = "SUBSCRIPTIONS";
|
||||
const val TYPE_SHORTS = "SHORTS";
|
||||
|
||||
const val ORDER_CHONOLOGICAL = "CHRONOLOGICAL";
|
||||
|
||||
@@ -45,6 +47,7 @@ class ResultCapabilities(
|
||||
|
||||
fun fromV8(config: IV8PluginConfig, value: V8ValueObject): ResultCapabilities {
|
||||
val contextName = "ResultCapabilities";
|
||||
value.ensureIsBusy();
|
||||
return ResultCapabilities(
|
||||
value.getOrThrow<V8ValueArray>(config, "types", contextName).toArray().map { it.expectV8Variant(config, "Capabilities.types") },
|
||||
value.getOrThrow<V8ValueArray>(config, "sorts", contextName).toArray().map { it.expectV8Variant(config, "Capabilities.sorts"); },
|
||||
@@ -68,6 +71,7 @@ class FilterGroup(
|
||||
|
||||
companion object {
|
||||
fun fromV8(config: IV8PluginConfig, value: V8ValueObject): FilterGroup {
|
||||
value.ensureIsBusy();
|
||||
return FilterGroup(
|
||||
value.getString("name"),
|
||||
value.getOrDefault<V8ValueArray>(config, "filters", "FilterGroup", null)
|
||||
@@ -89,6 +93,7 @@ class FilterCapability(
|
||||
|
||||
companion object {
|
||||
fun fromV8(obj: V8ValueObject): FilterCapability {
|
||||
obj.ensureIsBusy();
|
||||
val value = obj.get("value") as V8Value;
|
||||
return FilterCapability(
|
||||
obj.getString("name"),
|
||||
|
||||
@@ -3,6 +3,9 @@ package com.futo.platformplayer.api.media.models
|
||||
import com.caoccao.javet.values.reference.V8ValueArray
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.engine.V8PluginConfig
|
||||
import com.futo.platformplayer.ensureIsBusy
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
@@ -29,9 +32,10 @@ class Thumbnails {
|
||||
|
||||
companion object {
|
||||
fun fromV8(config: IV8PluginConfig, value: V8ValueObject): Thumbnails {
|
||||
value.ensureIsBusy();
|
||||
return Thumbnails((value.getOrThrow<V8ValueArray>(config, "sources", "Thumbnails"))
|
||||
.toArray()
|
||||
.map { Thumbnail.fromV8(it as V8ValueObject) }
|
||||
.map { Thumbnail.fromV8(config, it as V8ValueObject) }
|
||||
.toTypedArray());
|
||||
}
|
||||
}
|
||||
@@ -40,10 +44,10 @@ class Thumbnails {
|
||||
data class Thumbnail(val url : String?, val quality : Int = 0) {
|
||||
|
||||
companion object {
|
||||
fun fromV8(value: V8ValueObject): Thumbnail {
|
||||
fun fromV8(config: IV8PluginConfig, value: V8ValueObject): Thumbnail {
|
||||
return Thumbnail(
|
||||
value.getString("url"),
|
||||
value.getInteger("quality"));
|
||||
value.getOrDefault<String>(config,"url", "Thumbnail", null),
|
||||
value.getOrDefault(config, "quality", "Thumbnail", 0) ?: 0);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.futo.platformplayer.api.media.models.article
|
||||
|
||||
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
|
||||
interface IPlatformArticle: IPlatformContent {
|
||||
val summary: String?;
|
||||
val thumbnails: Thumbnails?;
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package com.futo.platformplayer.api.media.models.article
|
||||
|
||||
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
|
||||
import com.futo.platformplayer.api.media.models.ratings.IRating
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.IJSArticleSegment
|
||||
|
||||
interface IPlatformArticleDetails: IPlatformContent, IPlatformArticle, IPlatformContentDetails {
|
||||
val segments: List<IJSArticleSegment>;
|
||||
val rating : IRating;
|
||||
}
|
||||
+4
@@ -37,6 +37,10 @@ class SerializedChannel(
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
fun isSameUrl(url: String): Boolean {
|
||||
return this.url == url || urlAlternatives.contains(url);
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromChannel(channel: IPlatformChannel): SerializedChannel {
|
||||
return SerializedChannel(
|
||||
|
||||
@@ -23,7 +23,7 @@ enum class ChapterType(val value: Int) {
|
||||
companion object {
|
||||
fun fromInt(value: Int): ChapterType
|
||||
{
|
||||
val result = ChapterType.values().firstOrNull { it.value == value };
|
||||
val result = ChapterType.entries.firstOrNull { it.value == value };
|
||||
if(result == null)
|
||||
throw UnknownPlatformException(value.toString());
|
||||
return result;
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.futo.platformplayer.api.media.models.comments
|
||||
|
||||
import com.futo.platformplayer.api.media.IPlatformClient
|
||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||
import com.futo.platformplayer.api.media.models.ratings.IRating
|
||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||
import com.futo.platformplayer.api.media.models.ratings.RatingType
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import kotlinx.coroutines.Deferred
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
class LazyComment: IPlatformComment {
|
||||
private var _commentDeferred: Deferred<IPlatformComment>;
|
||||
private var _commentLoaded: IPlatformComment? = null;
|
||||
private var _commentException: Throwable? = null;
|
||||
|
||||
override val contextUrl: String
|
||||
get() = _commentLoaded?.contextUrl ?: "";
|
||||
override val author: PlatformAuthorLink
|
||||
get() = _commentLoaded?.author ?: PlatformAuthorLink.UNKNOWN;
|
||||
override val message: String
|
||||
get() = _commentLoaded?.message ?: "";
|
||||
override val rating: IRating
|
||||
get() = _commentLoaded?.rating ?: RatingLikes(0);
|
||||
override val date: OffsetDateTime?
|
||||
get() = _commentLoaded?.date ?: OffsetDateTime.MIN;
|
||||
override val replyCount: Int?
|
||||
get() = _commentLoaded?.replyCount ?: 0;
|
||||
|
||||
val isAvailable: Boolean get() = _commentLoaded != null;
|
||||
|
||||
private var _uiHandler: ((LazyComment)->Unit)? = null;
|
||||
|
||||
constructor(commentDeferred: Deferred<IPlatformComment>) {
|
||||
_commentDeferred = commentDeferred;
|
||||
_commentDeferred.invokeOnCompletion {
|
||||
if(it == null) {
|
||||
_commentLoaded = commentDeferred.getCompleted();
|
||||
Logger.i("LazyComment", "Resolved comment");
|
||||
}
|
||||
else {
|
||||
_commentException = it;
|
||||
Logger.e("LazyComment", "Resolving comment failed: ${it.message}", it);
|
||||
}
|
||||
|
||||
_uiHandler?.invoke(this);
|
||||
}
|
||||
}
|
||||
|
||||
fun getUnderlyingComment(): IPlatformComment? {
|
||||
return _commentLoaded;
|
||||
}
|
||||
|
||||
fun setUIHandler(handler: (LazyComment)->Unit){
|
||||
_uiHandler = handler;
|
||||
}
|
||||
|
||||
override fun getReplies(client: IPlatformClient): IPager<IPlatformComment>? {
|
||||
return _commentLoaded?.getReplies(client);
|
||||
}
|
||||
|
||||
}
|
||||
+5
-2
@@ -19,8 +19,9 @@ class PolycentricPlatformComment : IPlatformComment {
|
||||
|
||||
val eventPointer: Pointer;
|
||||
val reference: Reference;
|
||||
val parentReference: Reference?;
|
||||
|
||||
constructor(contextUrl: String, author: PlatformAuthorLink, msg: String, rating: IRating, date: OffsetDateTime, eventPointer: Pointer, replyCount: Int? = null) {
|
||||
constructor(contextUrl: String, author: PlatformAuthorLink, msg: String, rating: IRating, date: OffsetDateTime, eventPointer: Pointer, parentReference: Reference?, replyCount: Int? = null) {
|
||||
this.contextUrl = contextUrl;
|
||||
this.author = author;
|
||||
this.message = msg;
|
||||
@@ -29,6 +30,7 @@ class PolycentricPlatformComment : IPlatformComment {
|
||||
this.replyCount = replyCount;
|
||||
this.eventPointer = eventPointer;
|
||||
this.reference = eventPointer.toReference();
|
||||
this.parentReference = parentReference;
|
||||
}
|
||||
|
||||
override fun getReplies(client: IPlatformClient): IPager<IPlatformComment> {
|
||||
@@ -36,10 +38,11 @@ class PolycentricPlatformComment : IPlatformComment {
|
||||
}
|
||||
|
||||
fun cloneWithUpdatedReplyCount(replyCount: Int?): PolycentricPlatformComment {
|
||||
return PolycentricPlatformComment(contextUrl, author, message, rating, date, eventPointer, replyCount);
|
||||
return PolycentricPlatformComment(contextUrl, author, message, rating, date, eventPointer, parentReference, replyCount);
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "PolycentricPlatformComment"
|
||||
val MAX_COMMENT_SIZE = 2000
|
||||
}
|
||||
}
|
||||
@@ -8,10 +8,12 @@ enum class ContentType(val value: Int) {
|
||||
POST(2),
|
||||
ARTICLE(3),
|
||||
PLAYLIST(4),
|
||||
WEB(7),
|
||||
|
||||
URL(9),
|
||||
|
||||
NESTED_VIDEO(11),
|
||||
CHANNEL(60),
|
||||
|
||||
LOCKED(70),
|
||||
|
||||
@@ -21,7 +23,7 @@ enum class ContentType(val value: Int) {
|
||||
companion object {
|
||||
fun fromInt(value: Int): ContentType
|
||||
{
|
||||
val result = ContentType.values().firstOrNull { it.value == value };
|
||||
val result = ContentType.entries.firstOrNull { it.value == value };
|
||||
if(result == null)
|
||||
throw UnknownPlatformException(value.toString());
|
||||
return result;
|
||||
|
||||
+2
@@ -2,6 +2,8 @@ package com.futo.platformplayer.api.media.models.contents
|
||||
|
||||
import com.futo.platformplayer.api.media.PlatformID
|
||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonNames
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
interface IPlatformContent {
|
||||
|
||||
+2
@@ -10,4 +10,6 @@ interface IPlatformContentDetails : IPlatformContent {
|
||||
|
||||
fun getComments(client: IPlatformClient): IPager<IPlatformComment>?;
|
||||
fun getPlaybackTracker(): IPlaybackTracker?;
|
||||
|
||||
fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>?;
|
||||
}
|
||||
+1
@@ -3,4 +3,5 @@ package com.futo.platformplayer.api.media.models.live
|
||||
interface ILiveChatWindowDescriptor {
|
||||
val url: String;
|
||||
val removeElements: List<String>;
|
||||
val removeElementsInterval: List<String>;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.live
|
||||
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.ensureIsBusy
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
interface IPlatformLiveEvent {
|
||||
@@ -10,6 +11,7 @@ interface IPlatformLiveEvent {
|
||||
|
||||
companion object {
|
||||
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject, contextName: String = "LiveEvent") : IPlatformLiveEvent {
|
||||
obj.ensureIsBusy();
|
||||
val t = LiveEventType.fromInt(obj.getOrThrow<Int>(config, "type", contextName));
|
||||
return when(t) {
|
||||
LiveEventType.COMMENT -> LiveEventComment.fromV8(config, obj);
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.caoccao.javet.values.reference.V8ValueArray
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.ensureIsBusy
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
@@ -27,6 +28,8 @@ class LiveEventComment: IPlatformLiveEvent, ILiveEventChatMessage {
|
||||
|
||||
companion object {
|
||||
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventComment {
|
||||
obj.ensureIsBusy();
|
||||
|
||||
val contextName = "LiveEventComment"
|
||||
|
||||
val colorName = obj.getOrDefault<String>(config, "colorName", contextName, null);
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.futo.platformplayer.api.media.models.live
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.ensureIsBusy
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
@@ -37,6 +38,7 @@ class LiveEventDonation: IPlatformLiveEvent, ILiveEventChatMessage {
|
||||
|
||||
companion object {
|
||||
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventDonation {
|
||||
obj.ensureIsBusy();
|
||||
val contextName = "LiveEventDonation"
|
||||
return LiveEventDonation(
|
||||
obj.getOrThrow(config, "name", contextName),
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.live
|
||||
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.ensureIsBusy
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
class LiveEventEmojis: IPlatformLiveEvent {
|
||||
@@ -15,6 +16,7 @@ class LiveEventEmojis: IPlatformLiveEvent {
|
||||
|
||||
companion object {
|
||||
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventEmojis {
|
||||
obj.ensureIsBusy();
|
||||
val contextName = "LiveEventEmojis"
|
||||
return LiveEventEmojis(
|
||||
obj.getOrThrow(config, "emojis", contextName));
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.live
|
||||
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.ensureIsBusy
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
class LiveEventRaid: IPlatformLiveEvent {
|
||||
@@ -19,6 +20,7 @@ class LiveEventRaid: IPlatformLiveEvent {
|
||||
|
||||
companion object {
|
||||
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventRaid {
|
||||
obj.ensureIsBusy();
|
||||
val contextName = "LiveEventRaid"
|
||||
return LiveEventRaid(
|
||||
obj.getOrThrow(config, "targetName", contextName),
|
||||
|
||||
@@ -10,7 +10,7 @@ enum class LiveEventType(val value : Int) {
|
||||
|
||||
companion object{
|
||||
fun fromInt(value : Int) : LiveEventType{
|
||||
return LiveEventType.values().first { it.value == value };
|
||||
return LiveEventType.entries.first { it.value == value };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.live
|
||||
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.ensureIsBusy
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
class LiveEventViewCount: IPlatformLiveEvent {
|
||||
@@ -15,6 +16,7 @@ class LiveEventViewCount: IPlatformLiveEvent {
|
||||
|
||||
companion object {
|
||||
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventViewCount {
|
||||
obj.ensureIsBusy();
|
||||
val contextName = "LiveEventViewCount"
|
||||
return LiveEventViewCount(
|
||||
obj.getOrThrow(config, "viewCount", contextName));
|
||||
|
||||
+1
@@ -3,4 +3,5 @@ package com.futo.platformplayer.api.media.models.modifier
|
||||
interface IModifierOptions {
|
||||
val applyAuthClient: String?;
|
||||
val applyCookieClient: String?;
|
||||
val applyOtherHeaders: Boolean;
|
||||
}
|
||||
+1
@@ -7,4 +7,5 @@ interface IPlaybackTracker {
|
||||
|
||||
fun onInit(seconds: Double);
|
||||
fun onProgress(seconds: Double, isPlaying: Boolean);
|
||||
fun onConcluded();
|
||||
}
|
||||
+1
-1
@@ -8,5 +8,5 @@ interface IPlatformPlaylistDetails: IPlatformPlaylist {
|
||||
//TODO: Determine if this should be IPlatformContent (probably not?)
|
||||
val contents: IPager<IPlatformVideo>;
|
||||
|
||||
fun toPlaylist(): Playlist;
|
||||
fun toPlaylist(onProgress: ((progress: Int) -> Unit)? = null): Playlist;
|
||||
}
|
||||
@@ -5,12 +5,13 @@ import com.futo.platformplayer.api.media.exceptions.UnknownPlatformException
|
||||
enum class TextType(val value: Int) {
|
||||
RAW(0),
|
||||
HTML(1),
|
||||
MARKUP(2);
|
||||
MARKUP(2),
|
||||
CODE(3);
|
||||
|
||||
companion object {
|
||||
fun fromInt(value: Int): TextType
|
||||
{
|
||||
val result = TextType.values().firstOrNull { it.value == value };
|
||||
val result = TextType.entries.firstOrNull { it.value == value };
|
||||
if(result == null)
|
||||
throw IllegalArgumentException("Unknown Texttype: $value");
|
||||
return result;
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.futo.platformplayer.api.media.models.ratings
|
||||
import com.caoccao.javet.values.V8Value
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.ensureIsBusy
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.orDefault
|
||||
import com.futo.platformplayer.serializers.IRatingSerializer
|
||||
@@ -13,8 +14,12 @@ interface IRating {
|
||||
|
||||
|
||||
companion object {
|
||||
fun fromV8OrDefault(config: IV8PluginConfig, obj: V8Value?, default: IRating) = obj.orDefault(default) { fromV8(config, it as V8ValueObject) };
|
||||
fun fromV8OrDefault(config: IV8PluginConfig, obj: V8Value?, default: IRating): IRating {
|
||||
obj?.ensureIsBusy();
|
||||
return obj.orDefault(default) { fromV8(config, it as V8ValueObject) }
|
||||
};
|
||||
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject, contextName: String = "Rating") : IRating {
|
||||
obj.ensureIsBusy();
|
||||
val t = RatingType.fromInt(obj.getOrThrow<Int>(config, "type", contextName));
|
||||
return when(t) {
|
||||
RatingType.LIKES -> RatingLikes.fromV8(config, obj);
|
||||
|
||||
+2
@@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.ratings
|
||||
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.ensureIsBusy
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
/**
|
||||
@@ -14,6 +15,7 @@ class RatingLikeDislikes(val likes: Long, val dislikes: Long) : IRating {
|
||||
|
||||
companion object {
|
||||
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : RatingLikeDislikes {
|
||||
obj.ensureIsBusy();
|
||||
return RatingLikeDislikes(obj.getOrThrow(config, "likes", "RatingLikeDislikes"), obj.getOrThrow(config, "dislikes", "RatingLikeDislikes"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.ratings
|
||||
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.ensureIsBusy
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
/**
|
||||
@@ -13,6 +14,7 @@ class RatingLikes(val likes: Long) : IRating {
|
||||
|
||||
companion object {
|
||||
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : RatingLikes {
|
||||
obj.ensureIsBusy();
|
||||
return RatingLikes(obj.getOrThrow(config, "likes", "RatingLikes"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.ratings
|
||||
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.ensureIsBusy
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
/**
|
||||
@@ -13,6 +14,7 @@ class RatingScaler(val value: Float) : IRating {
|
||||
|
||||
companion object {
|
||||
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : RatingScaler {
|
||||
obj.ensureIsBusy()
|
||||
return RatingScaler(obj.getOrThrow(config, "value", "RatingScaler"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ enum class RatingType(val value : Int) {
|
||||
|
||||
companion object{
|
||||
fun fromInt(value : Int) : RatingType{
|
||||
return RatingType.values().first { it.value == value };
|
||||
return RatingType.entries.first { it.value == value };
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -3,7 +3,7 @@ package com.futo.platformplayer.api.media.models.streams
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||
import com.futo.platformplayer.downloads.VideoLocal
|
||||
|
||||
class LocalVideoMuxedSourceDescriptor(
|
||||
class DownloadedVideoMuxedSourceDescriptor(
|
||||
private val video: VideoLocal
|
||||
) : VideoMuxedSourceDescriptor() {
|
||||
override val videoSources: Array<IVideoSource> get() = video.videoSource.toTypedArray();
|
||||
+5
-2
@@ -13,7 +13,8 @@ class AudioUrlSource(
|
||||
override val codec: String = "",
|
||||
override val language: String = Language.UNKNOWN,
|
||||
override val duration: Long? = null,
|
||||
override var priority: Boolean = false
|
||||
override var priority: Boolean = false,
|
||||
override var original: Boolean = false
|
||||
) : IAudioUrlSource, IStreamMetaDataSource{
|
||||
override var streamMetaData: StreamMetaData? = null;
|
||||
|
||||
@@ -36,7 +37,9 @@ class AudioUrlSource(
|
||||
source.container,
|
||||
source.codec,
|
||||
source.language,
|
||||
source.duration
|
||||
source.duration,
|
||||
source.priority,
|
||||
source.original
|
||||
);
|
||||
ret.streamMetaData = streamData;
|
||||
|
||||
|
||||
+1
@@ -27,6 +27,7 @@ class HLSVariantAudioUrlSource(
|
||||
override val language: String,
|
||||
override val duration: Long?,
|
||||
override val priority: Boolean,
|
||||
override val original: Boolean,
|
||||
val url: String
|
||||
) : IAudioUrlSource {
|
||||
override fun getAudioUrl(): String {
|
||||
|
||||
+1
@@ -8,4 +8,5 @@ interface IAudioSource {
|
||||
val language : String;
|
||||
val duration : Long?;
|
||||
val priority: Boolean;
|
||||
val original: Boolean;
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package com.futo.platformplayer.api.media.models.streams.sources
|
||||
|
||||
interface IAudioUrlWidevineSource : IAudioUrlSource, IWidevineSource
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package com.futo.platformplayer.api.media.models.streams.sources
|
||||
|
||||
interface IDashManifestWidevineSource : IWidevineSource {
|
||||
val url: String
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package com.futo.platformplayer.api.media.models.streams.sources
|
||||
|
||||
interface IVideoUrlWidevineSource : IVideoUrlSource, IWidevineSource
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package com.futo.platformplayer.api.media.models.streams.sources
|
||||
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
|
||||
|
||||
interface IWidevineSource {
|
||||
val licenseUri: String
|
||||
val hasLicenseRequestExecutor: Boolean
|
||||
fun getLicenseRequestExecutor(): JSRequestExecutor?
|
||||
}
|
||||
+3
-2
@@ -15,6 +15,7 @@ class LocalAudioSource : IAudioSource, IStreamMetaDataSource {
|
||||
override val duration: Long? = null;
|
||||
|
||||
override var priority: Boolean = false;
|
||||
override val original: Boolean = false;
|
||||
|
||||
val filePath : String;
|
||||
val fileSize: Long;
|
||||
@@ -33,13 +34,13 @@ class LocalAudioSource : IAudioSource, IStreamMetaDataSource {
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromSource(source: IAudioSource, path: String, fileSize: Long): LocalAudioSource {
|
||||
fun fromSource(source: IAudioSource, path: String, fileSize: Long, overrideContainer: String? = null): LocalAudioSource {
|
||||
return LocalAudioSource(
|
||||
source.name,
|
||||
path,
|
||||
fileSize,
|
||||
source.bitrate,
|
||||
source.container,
|
||||
overrideContainer ?: source.container,
|
||||
source.codec,
|
||||
source.language
|
||||
);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user