mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-16 04:52:39 +02:00
Compare commits
417 Commits
192
...
#1-rm-you.be
| Author | SHA1 | Date | |
|---|---|---|---|
| eb8d9ea9a3 | |||
| 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 | |||
| c06c00ee9b | |||
| 1d8eababc2 | |||
| 75cf1ffbdd | |||
| 5499706a9b | |||
| ba57e32920 | |||
| df96c5b51c | |||
| 75f81d20db | |||
| 3fc92e4065 | |||
| 8ffd5f411f | |||
| 918161a299 | |||
| 9f50f72eaa | |||
| 2f66f124aa | |||
| 9a11717cf4 | |||
| 0d80424799 | |||
| ed9a65b2f0 | |||
| 8a53297be2 | |||
| 20862a27c8 | |||
| 95785e6c78 | |||
| e88c649578 | |||
| 09f91e64fb | |||
| b8923e59a1 | |||
| e722c0ce9a | |||
| 56248bf4b0 | |||
| 5af4787c45 | |||
| 0990247322 | |||
| 0154525578 | |||
| 1dc6eee242 | |||
| c63a63cb33 | |||
| c1967556ac | |||
| 309a57f5a1 | |||
| ee0bc96e53 | |||
| a4422fdd56 | |||
| b7c4047f1d | |||
| 65174ffc97 | |||
| eac3e37af5 | |||
| 0d5ad90ff9 | |||
| f42b14e95a | |||
| b8acd0b5b2 | |||
| ef72561768 | |||
| d63627bd61 | |||
| 422cceb225 | |||
| 76f5962232 | |||
| 30df22d225 | |||
| cd4295be59 | |||
| 7d366110b1 | |||
| 35c5045b3f | |||
| 4930ea8183 | |||
| 02292fed04 | |||
| bf6e61ed90 | |||
| 2ac8e0e621 | |||
| 0432f06eb3 | |||
| 7bfab8409f | |||
| 52d833d726 | |||
| 14d579eb1b | |||
| d3ab8ecf3a | |||
| 627b8c2b5d | |||
| 7f1cb22c12 | |||
| 5551bd31fe | |||
| 189d855c3f | |||
| 0ab52e8f4d | |||
| 27eb5aa6e1 | |||
| 49b5b16641 | |||
| 73dd52af28 | |||
| 3b8d256bad | |||
| 5d7dc1fdcb | |||
| f31b6c50e9 | |||
| fa12f8277c | |||
| 150a7d5006 | |||
| a0a73a8e5c | |||
| 4723a0b29a | |||
| adbe0357ba | |||
| b0a35bcf3f | |||
| 0e7482321c | |||
| e50d195b85 | |||
| 33780f1046 | |||
| 8b20b4909f | |||
| 71a3828fe4 | |||
| d713f2bd55 | |||
| 069a615193 | |||
| f7d2cb4055 | |||
| f109d82537 | |||
| ab49d4749b | |||
| 507eed4f53 | |||
| 23ca4addf9 | |||
| 331ed09775 | |||
| 85303b54bc | |||
| f224cd1ca5 | |||
| d433d6e774 | |||
| 90de54ac5c | |||
| 5ff8f1ba6d | |||
| bc00b12b8c | |||
| 1c0cfa89a3 | |||
| efa1361fbe | |||
| 73918a8d76 | |||
| a3c8bbb21f | |||
| 53525cb365 | |||
| e4d39cbec4 | |||
| a15e4beafb | |||
| d47298102e | |||
| 280feea06e | |||
| f649d62e38 | |||
| 0ae05e7cd4 | |||
| b284176072 | |||
| 5fffaf2f4e | |||
| 58da91eae8 | |||
| 98d92d3fe2 | |||
| c5d35b27f0 | |||
| aee5b75c2f | |||
| fe02197bd8 | |||
| a1060a15be | |||
| dc7b2f420b | |||
| b35390a4bb | |||
| 3b253ad2b6 | |||
| 06c39ce973 | |||
| 11b8914615 | |||
| e45c8617df | |||
| 9075a2599c | |||
| dd8d50e0e2 | |||
| 55a11d82ac | |||
| 7ee4f411cb | |||
| c9d5508018 | |||
| bef8fc682c | |||
| c37d464403 | |||
| cbf2712654 | |||
| 08134b4427 | |||
| f90290c4ec | |||
| 7cde8ed538 | |||
| 585cf090d6 | |||
| 23d1085755 | |||
| fc5888d57e | |||
| c5541b1747 | |||
| 0fd8ba28bb | |||
| 6d9f4959e0 | |||
| 4be4bb631f | |||
| 948f5a2a6d | |||
| baad342aec | |||
| aeb29c54cd | |||
| a5dfa653ad | |||
| 3387c727d1 | |||
| c806ff2e33 | |||
| 1db4d427fc | |||
| 3bf73ed5e8 | |||
| db44aa2c4d | |||
| 0e6e381800 | |||
| 69e43dc533 | |||
| ee4442d553 | |||
| c49b9f7841 | |||
| 8a35cd0e82 | |||
| 0ae90ecf03 | |||
| 3d2840fe15 | |||
| b6ad3fd991 | |||
| 2ee3c30b0e | |||
| 662e94bcee | |||
| f3c9e0196e | |||
| f15eb9bf9e | |||
| 12b2552185 | |||
| d245e20b14 | |||
| e47349d010 | |||
| eb3dd854d4 | |||
| c529446219 | |||
| fa2f8c3447 | |||
| 840d1ae534 | |||
| 2530c6eb58 | |||
| 869789f0e2 | |||
| ee3761c780 | |||
| e4c89e9aa9 | |||
| 9d5888ddf7 | |||
| ecc94920d7 | |||
| 5cafbf243e | |||
| f3fa208680 | |||
| 502602e27a | |||
| 5054b093a4 | |||
| 0ffaec6bc2 | |||
| ef8ea9eecf | |||
| b09d22e479 | |||
| 01787b6229 | |||
| 4c022698d3 | |||
| bfdcab0e84 | |||
| aaea5cc963 | |||
| 23d9c33406 | |||
| fad1b216df | |||
| e221b508d3 | |||
| dfafac7d99 | |||
| 2246f8cee2 | |||
| b65fc594dc | |||
| f52b731615 | |||
| 8661ff88c0 | |||
| 99c06c516f | |||
| 0bba7fa373 | |||
| 0c1822b118 | |||
| 10e3d2122f | |||
| 6df8f84421 | |||
| 7fa80ec048 | |||
| b3f9b81984 | |||
| 1393c489c1 | |||
| 640c2cbed0 | |||
| e55509f549 | |||
| 27c7fb0c12 | |||
| 88f3815585 | |||
| 2e9405cfdb | |||
| 9c1b543ed6 | |||
| d34cb0f9c1 | |||
| 116dc90d21 | |||
| 17b9853bb6 | |||
| 8bfb8abd20 | |||
| 9ee3f1f26e | |||
| 5dcff29d8d | |||
| 6cfbd0c8bf | |||
| 01d96cce16 | |||
| 58c376f011 | |||
| 439d339330 | |||
| 44eacc2a47 | |||
| 8135d61398 | |||
| 66208f8265 | |||
| f52251e23a | |||
| dbea93efe5 | |||
| 3bf0740bd1 |
@@ -0,0 +1,78 @@
|
|||||||
|
name: Bug Report
|
||||||
|
description: Let us know about an unexpected error, a crash, or an incorrect behavior.
|
||||||
|
labels: ["bug", "new"]
|
||||||
|
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: what-happened
|
||||||
|
attributes:
|
||||||
|
label: What happened?
|
||||||
|
description: What did you expect to happen?
|
||||||
|
placeholder: Tell us what you see!
|
||||||
|
value: "A bug happened!"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
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".
|
||||||
|
render: shell
|
||||||
|
placeholder: "242"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: plugin
|
||||||
|
attributes:
|
||||||
|
label: What plugins are you seeing the problem on?
|
||||||
|
multiple: true
|
||||||
|
options:
|
||||||
|
- All
|
||||||
|
- Youtube
|
||||||
|
- BiliBili (CN)
|
||||||
|
- Twitch (Beta)
|
||||||
|
- Odysee
|
||||||
|
- Rumble
|
||||||
|
- Kick (Beta)
|
||||||
|
- PeerTube
|
||||||
|
- Patreon
|
||||||
|
- Nebula (Beta)
|
||||||
|
- SoundCloud
|
||||||
|
- Other
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: login
|
||||||
|
attributes:
|
||||||
|
label: Are you experiencing the issue when logged in?
|
||||||
|
multiple: false
|
||||||
|
options:
|
||||||
|
- "Yes"
|
||||||
|
- "No"
|
||||||
|
- N/A
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- 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
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
name: Documentation Issue
|
||||||
|
description: Report an issue or suggest a change in the documentation.
|
||||||
|
labels: ["documentation", "new"]
|
||||||
|
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. `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. 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,60 @@
|
|||||||
|
name: Feature Request
|
||||||
|
description: Suggest a new feature or other enhancement.
|
||||||
|
labels: ["enhancement", "new"]
|
||||||
|
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 relating to the Grayjay Android Application
|
||||||
|
|
||||||
|
[External Contributions are close at this time](https://github.com/tom-futo/grayjay-android/blob/master/CONTRIBUTION.md#contributing-to-core)
|
||||||
|
|
||||||
|
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,34 @@
|
|||||||
|
name: Issue labeler
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [ opened ]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
label-component:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
# required for all workflows
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Parse issue form
|
||||||
|
uses: stefanbuck/github-issue-parser@v3
|
||||||
|
id: issue-parser
|
||||||
|
with:
|
||||||
|
template-path: .github/ISSUE_TEMPLATE/bug_report.yml
|
||||||
|
|
||||||
|
- name: Set labels based on plugin field
|
||||||
|
uses: redhat-plumbers-in-action/advanced-issue-labeler@v2
|
||||||
|
with:
|
||||||
|
issue-form: ${{ steps.issue-parser.outputs.jsonString }}
|
||||||
|
section: plugin
|
||||||
|
block-list: |
|
||||||
|
None
|
||||||
|
Other
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
+12
-3
@@ -1,9 +1,6 @@
|
|||||||
[submodule "dep/polycentricandroid"]
|
[submodule "dep/polycentricandroid"]
|
||||||
path = dep/polycentricandroid
|
path = dep/polycentricandroid
|
||||||
url = ../polycentricandroid.git
|
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"]
|
[submodule "app/src/stable/assets/sources/kick"]
|
||||||
path = app/src/stable/assets/sources/kick
|
path = app/src/stable/assets/sources/kick
|
||||||
url = ../plugins/kick.git
|
url = ../plugins/kick.git
|
||||||
@@ -61,3 +58,15 @@
|
|||||||
[submodule "dep/futopay"]
|
[submodule "dep/futopay"]
|
||||||
path = dep/futopay
|
path = dep/futopay
|
||||||
url = ../futopayclientlibraries.git
|
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
|
||||||
|
|||||||
@@ -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 @@
|
|||||||
|
# Grayjay Core License 1.0
|
||||||
|
|
||||||
|
## 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 patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent 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.
|
||||||
+48
-33
@@ -1,10 +1,11 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id 'com.android.application'
|
id 'com.android.application'
|
||||||
id 'org.jetbrains.kotlin.android'
|
id 'org.jetbrains.kotlin.android'
|
||||||
id 'org.jetbrains.kotlin.plugin.serialization' version '1.6.10'
|
id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.21'
|
||||||
id 'org.ajoberstar.grgit' version '1.7.2'
|
id 'org.ajoberstar.grgit' version '1.7.2'
|
||||||
id 'com.google.protobuf'
|
id 'com.google.protobuf'
|
||||||
id 'kotlin-parcelize'
|
id 'kotlin-parcelize'
|
||||||
|
id 'com.google.devtools.ksp'
|
||||||
}
|
}
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
@@ -23,7 +24,7 @@ if (keystorePropertiesFile.exists()) {
|
|||||||
|
|
||||||
protobuf {
|
protobuf {
|
||||||
protoc {
|
protoc {
|
||||||
artifact = 'com.google.protobuf:protoc:3.22.3'
|
artifact = 'com.google.protobuf:protoc:3.25.1'
|
||||||
}
|
}
|
||||||
generateProtoTasks {
|
generateProtoTasks {
|
||||||
all().each { task ->
|
all().each { task ->
|
||||||
@@ -38,7 +39,7 @@ protobuf {
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
namespace 'com.futo.platformplayer'
|
namespace 'com.futo.platformplayer'
|
||||||
compileSdk 33
|
compileSdk 34
|
||||||
flavorDimensions "buildType"
|
flavorDimensions "buildType"
|
||||||
productFlavors {
|
productFlavors {
|
||||||
stable {
|
stable {
|
||||||
@@ -96,11 +97,15 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdk 28
|
minSdk 28
|
||||||
targetSdk 33
|
targetSdk 34
|
||||||
versionCode gitVersionCode
|
versionCode gitVersionCode
|
||||||
versionName gitVersionName
|
versionName gitVersionName
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
|
ksp {
|
||||||
|
arg("room.schemaLocation", "$projectDir/schemas")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
@@ -136,43 +141,47 @@ android {
|
|||||||
universalApk true
|
universalApk true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
buildFeatures {
|
||||||
|
buildConfig true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
//Core
|
//Core
|
||||||
implementation 'androidx.core:core-ktx:1.7.0'
|
implementation 'androidx.core:core-ktx:1.12.0'
|
||||||
implementation 'androidx.appcompat:appcompat:1.4.1'
|
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||||
implementation 'com.google.android.material:material:1.5.0'
|
implementation 'com.google.android.material:material:1.11.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||||
|
|
||||||
//Images
|
//Images
|
||||||
annotationProcessor 'com.github.bumptech.glide:compiler:4.15.1'
|
annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'
|
||||||
implementation 'com.github.bumptech.glide:glide:4.15.1'
|
implementation 'com.github.bumptech.glide:glide:4.16.0'
|
||||||
|
|
||||||
//Async
|
//Async
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.2"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
|
||||||
|
|
||||||
//HTTP
|
//HTTP
|
||||||
implementation "com.squareup.okhttp3:okhttp:4.10.0"
|
implementation "com.squareup.okhttp3:okhttp:4.11.0"
|
||||||
|
|
||||||
//JSON
|
//JSON
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1" //Used for structured 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)
|
implementation 'com.google.code.gson:gson:2.10.1' //Used for complex/anonymous cases like during development conversions (eg. V8RemoteObject)
|
||||||
|
|
||||||
//JS
|
//JS
|
||||||
implementation("com.caoccao.javet:javet-android:2.2.1")
|
implementation("com.caoccao.javet:javet-android:3.0.2")
|
||||||
|
|
||||||
//Exoplayer
|
//Exoplayer
|
||||||
implementation 'com.google.android.exoplayer:exoplayer-core:2.18.7'
|
implementation 'androidx.media3:media3-exoplayer:1.2.1'
|
||||||
implementation 'com.google.android.exoplayer:exoplayer-dash:2.18.7'
|
implementation 'androidx.media3:media3-exoplayer-dash:1.2.1'
|
||||||
implementation 'com.google.android.exoplayer:exoplayer-ui:2.18.7'
|
implementation 'androidx.media3:media3-ui:1.2.1'
|
||||||
implementation 'com.google.android.exoplayer:exoplayer-hls:2.18.7'
|
implementation 'androidx.media3:media3-exoplayer-hls:1.2.1'
|
||||||
implementation 'com.google.android.exoplayer:exoplayer-rtsp:2.18.7'
|
implementation 'androidx.media3:media3-exoplayer-rtsp:1.2.1'
|
||||||
implementation 'com.google.android.exoplayer:exoplayer-smoothstreaming:2.18.7'
|
implementation 'androidx.media3:media3-exoplayer-smoothstreaming:1.2.1'
|
||||||
implementation 'com.google.android.exoplayer:exoplayer-transformer:2.18.7'
|
implementation 'androidx.media3:media3-transformer:1.2.1'
|
||||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.3'
|
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.6'
|
||||||
implementation 'androidx.navigation:navigation-ui-ktx:2.5.3'
|
implementation 'androidx.navigation:navigation-ui-ktx:2.7.6'
|
||||||
|
implementation 'androidx.media:media:1.7.0'
|
||||||
|
|
||||||
//Other
|
//Other
|
||||||
implementation 'org.jmdns:jmdns:3.5.1'
|
implementation 'org.jmdns:jmdns:3.5.1'
|
||||||
@@ -180,28 +189,34 @@ dependencies {
|
|||||||
implementation 'com.google.android.flexbox:flexbox:3.0.0'
|
implementation 'com.google.android.flexbox:flexbox:3.0.0'
|
||||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||||
implementation 'com.arthenica:ffmpeg-kit-full:5.1'
|
implementation 'com.arthenica:ffmpeg-kit-full:5.1'
|
||||||
implementation 'org.jetbrains.kotlin:kotlin-reflect:1.7.20'
|
implementation 'org.jetbrains.kotlin:kotlin-reflect:1.9.0'
|
||||||
implementation 'com.github.dhaval2404:imagepicker:2.1'
|
implementation 'com.github.dhaval2404:imagepicker:2.1'
|
||||||
implementation 'com.google.zxing:core:3.4.1'
|
implementation 'com.google.zxing:core:3.4.1'
|
||||||
implementation 'com.journeyapps:zxing-android-embedded:4.2.0'
|
implementation 'com.journeyapps:zxing-android-embedded:4.3.0'
|
||||||
implementation 'com.caverock:androidsvg-aar:1.4'
|
implementation 'com.caverock:androidsvg-aar:1.4'
|
||||||
|
|
||||||
//Protobuf
|
//Protobuf
|
||||||
implementation 'com.google.protobuf:protobuf-javalite:3.22.3'
|
implementation 'com.google.protobuf:protobuf-javalite:3.25.1'
|
||||||
|
|
||||||
implementation 'com.polycentric.core:app:1.0'
|
implementation 'com.polycentric.core:app:1.0'
|
||||||
implementation 'com.futo.futopay:app:1.0'
|
implementation 'com.futo.futopay:app:1.0'
|
||||||
implementation 'androidx.work:work-runtime-ktx:2.8.1'
|
implementation 'androidx.work:work-runtime-ktx:2.9.0'
|
||||||
implementation 'androidx.concurrent:concurrent-futures-ktx:1.1.0'
|
implementation 'androidx.concurrent:concurrent-futures-ktx:1.1.0'
|
||||||
|
|
||||||
|
//Database
|
||||||
|
implementation("androidx.room:room-runtime:2.6.1")
|
||||||
|
annotationProcessor("androidx.room:room-compiler:2.6.1")
|
||||||
|
ksp("androidx.room:room-compiler:2.6.1")
|
||||||
|
implementation("androidx.room:room-ktx:2.6.1")
|
||||||
|
|
||||||
//Payment
|
//Payment
|
||||||
implementation 'com.stripe:stripe-android:20.28.3'
|
implementation 'com.stripe:stripe-android:20.35.1'
|
||||||
|
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.2'
|
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
|
||||||
testImplementation "org.jetbrains.kotlin:kotlin-test:1.8.20"
|
testImplementation "org.jetbrains.kotlin:kotlin-test:1.8.22"
|
||||||
testImplementation "org.xmlunit:xmlunit-core:2.9.1"
|
testImplementation "org.xmlunit:xmlunit-core:2.9.1"
|
||||||
testImplementation "org.mockito:mockito-core:5.4.0"
|
testImplementation "org.mockito:mockito-core:5.4.0"
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 3,
|
||||||
|
"identityHash": "ffba56c2f572c25080ce8596e8bb8945",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `url` TEXT NOT NULL, `position` INTEGER NOT NULL, `datetime` INTEGER NOT NULL, `name` TEXT NOT NULL, `serialized` BLOB)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "position",
|
||||||
|
"columnName": "position",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "datetime",
|
||||||
|
"columnName": "datetime",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serialized",
|
||||||
|
"columnName": "serialized",
|
||||||
|
"affinity": "BLOB",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_history_url",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_history_url` ON `${TABLE_NAME}` (`url`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_history_name",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_history_name` ON `${TABLE_NAME}` (`name`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_history_datetime",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"datetime"
|
||||||
|
],
|
||||||
|
"orders": [
|
||||||
|
"DESC"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_history_datetime` ON `${TABLE_NAME}` (`datetime` DESC)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ffba56c2f572c25080ce8596e8bb8945')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 5,
|
||||||
|
"identityHash": "eb813d54b9c44d29f1d7bb198a16d4d1",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "subscription_cache",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `url` TEXT, `channelUrl` TEXT, `datetime` INTEGER, `serialized` BLOB)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "channelUrl",
|
||||||
|
"columnName": "channelUrl",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "datetime",
|
||||||
|
"columnName": "datetime",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serialized",
|
||||||
|
"columnName": "serialized",
|
||||||
|
"affinity": "BLOB",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_subscription_cache_url",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_subscription_cache_url` ON `${TABLE_NAME}` (`url`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_subscription_cache_channelUrl",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"channelUrl"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_subscription_cache_channelUrl` ON `${TABLE_NAME}` (`channelUrl`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_subscription_cache_datetime",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"datetime"
|
||||||
|
],
|
||||||
|
"orders": [
|
||||||
|
"DESC"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_subscription_cache_datetime` ON `${TABLE_NAME}` (`datetime` DESC)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'eb813d54b9c44d29f1d7bb198a16d4d1')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 3,
|
||||||
|
"identityHash": "6e3b2d286325c4ea8a7a4c94c290daec",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "testing",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`someString` TEXT NOT NULL, `someNum` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT, `serialized` BLOB)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "someString",
|
||||||
|
"columnName": "someString",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "someNum",
|
||||||
|
"columnName": "someNum",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serialized",
|
||||||
|
"columnName": "serialized",
|
||||||
|
"affinity": "BLOB",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6e3b2d286325c4ea8a7a4c94c290daec')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package com.futo.platformplayer
|
||||||
|
|
||||||
|
import android.util.Base64
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import com.futo.platformplayer.casting.FCastCastingDevice
|
||||||
|
import com.futo.platformplayer.casting.Opcode
|
||||||
|
import com.futo.platformplayer.casting.models.FCastDecryptedMessage
|
||||||
|
import com.futo.platformplayer.casting.models.FCastEncryptedMessage
|
||||||
|
import com.futo.platformplayer.casting.models.FCastKeyExchangeMessage
|
||||||
|
import com.futo.platformplayer.casting.models.FCastPlayMessage
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import org.junit.Assert.*
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import java.security.KeyFactory
|
||||||
|
import java.security.spec.PKCS8EncodedKeySpec
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class FCastEncryptionTests {
|
||||||
|
@Test
|
||||||
|
fun testDHEncryptionSelf() {
|
||||||
|
val keyPair1 = FCastCastingDevice.generateKeyPair()
|
||||||
|
val keyPair2 = FCastCastingDevice.generateKeyPair()
|
||||||
|
Log.i("testDHEncryptionSelf", "privates (1: ${Base64.encodeToString(keyPair1.private.encoded, Base64.NO_WRAP)}, 2: ${Base64.encodeToString(keyPair2.private.encoded, Base64.NO_WRAP)})")
|
||||||
|
|
||||||
|
val keyExchangeMessage1 = FCastCastingDevice.getKeyExchangeMessage(keyPair1)
|
||||||
|
val keyExchangeMessage2 = FCastCastingDevice.getKeyExchangeMessage(keyPair2)
|
||||||
|
Log.i("testDHEncryptionSelf", "publics (1: ${keyExchangeMessage1.publicKey}, 2: ${keyExchangeMessage2.publicKey})")
|
||||||
|
|
||||||
|
val aesKey1 = FCastCastingDevice.computeSharedSecret(keyPair1.private, keyExchangeMessage2)
|
||||||
|
val aesKey2 = FCastCastingDevice.computeSharedSecret(keyPair2.private, keyExchangeMessage1)
|
||||||
|
|
||||||
|
assertEquals(Base64.encodeToString(aesKey1.encoded, Base64.NO_WRAP), Base64.encodeToString(aesKey2.encoded, Base64.NO_WRAP))
|
||||||
|
Log.i("testDHEncryptionSelf", "aesKey ${Base64.encodeToString(aesKey1.encoded, Base64.NO_WRAP)}")
|
||||||
|
|
||||||
|
val message = FCastPlayMessage("text/html")
|
||||||
|
val serializedBody = Json.encodeToString(message)
|
||||||
|
val encryptedMessage = FCastCastingDevice.encryptMessage(aesKey1, FCastDecryptedMessage(Opcode.Play.value.toLong(), serializedBody))
|
||||||
|
Log.i("testDHEncryptionSelf", Json.encodeToString(encryptedMessage))
|
||||||
|
|
||||||
|
val decryptedMessage = FCastCastingDevice.decryptMessage(aesKey1, encryptedMessage)
|
||||||
|
|
||||||
|
assertEquals(Opcode.Play.value.toLong(), decryptedMessage.opcode)
|
||||||
|
assertEquals(serializedBody, decryptedMessage.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testAESKeyGeneration() {
|
||||||
|
val cases = listOf(
|
||||||
|
listOf(
|
||||||
|
//Public other
|
||||||
|
"MIIBHzCBlQYJKoZIhvcNAQMBMIGHAoGBAP//////////yQ/aoiFowjTExmKLgNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7OZTgf//////////AgECA4GEAAKBgEnOS0oHteVA+3kND3u4yXe7GGRohy1LkR9Q5tL4c4ylC5n4iSwWSoIhcSIvUMWth6KAhPhu05sMcPY74rFMSS2AGTNCdT/5KilediipuUMdFVvjGqfNMNH1edzW5mquIw3iXKdfQmfY/qxLTI2wccyDj4hHFhLCZL3Y+shsm3KF",
|
||||||
|
//Private self
|
||||||
|
"MIIBIQIBADCBlQYJKoZIhvcNAQMBMIGHAoGBAP//////////yQ/aoiFowjTExmKLgNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7OZTgf//////////AgECBIGDAoGAeo/ceIeH8Jt1ZRNKX5aTHkMi23GCV1LtcS2O6Tktn9k8DCv7gIoekysQUhMyWtR+MsZlq2mXjr1JFpAyxl89rqoEPU6QDsGe9q8R4O8eBZ2u+48mkUkGSh7xPGRQUBvmhH2yk4hIEA8aK4BcYi1OTsCZtmk7pQq+uaFkKovD/8M=",
|
||||||
|
//AES
|
||||||
|
"7dpl1/6KQTTooOrFf2VlUOSqgrFHi6IYxapX0IxFfwk="
|
||||||
|
),
|
||||||
|
listOf(
|
||||||
|
//Public other
|
||||||
|
"MIIBHzCBlQYJKoZIhvcNAQMBMIGHAoGBAP//////////yQ/aoiFowjTExmKLgNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7OZTgf//////////AgECA4GEAAKBgGvIlCP/S+xpAuNEHSn4cEDOL1esUf+uMuY2Kp5J10a7HGbwzNd+7eYsgEc4+adddgB7hJgTvjsGg7lXUhHQ7WbfbCGgt7dbkx8qkic6Rgq4f5eRYd1Cgidw4MhZt7mEIOKrHweqnV6B9rypbXjbqauc6nGgtwx+Gvl6iLpVATRK",
|
||||||
|
//Private self
|
||||||
|
"MIIBIQIBADCBlQYJKoZIhvcNAQMBMIGHAoGBAP//////////yQ/aoiFowjTExmKLgNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7OZTgf//////////AgECBIGDAoGAMXmiIgWyutbaO+f4UiMAb09iVVSCI6Lb6xzNyD2MpUZyk4/JOT04Daj4JeCKFkF1Fq79yKhrnFlXCrF4WFX00xUOXb8BpUUUH35XG5ApvolQQLL6N0om8/MYP4FK/3PUxuZAJz45TUsI/v3u6UqJelVTNL83ltcFbZDIfEVftRA=",
|
||||||
|
//AES
|
||||||
|
"a2tUSxnXifKohfNocAQHkAlPffDv6ReihJ7OojBGt0Q="
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
for (case in cases) {
|
||||||
|
val decodedPrivateKey1 = Base64.decode(case[1], Base64.NO_WRAP)
|
||||||
|
val keyExchangeMessage2 = FCastKeyExchangeMessage(1, case[0])
|
||||||
|
|
||||||
|
val keyFactory = KeyFactory.getInstance("DH")
|
||||||
|
val privateKeySpec = PKCS8EncodedKeySpec(decodedPrivateKey1)
|
||||||
|
val privateKey = keyFactory.generatePrivate(privateKeySpec)
|
||||||
|
val aesKey1 = FCastCastingDevice.computeSharedSecret(privateKey, keyExchangeMessage2)
|
||||||
|
assertEquals(case[2], Base64.encodeToString(aesKey1.encoded, Base64.NO_WRAP))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDHEncryptionKnown() {
|
||||||
|
val decodedPrivateKey1 = Base64.decode("MIIDJwIBADCCAhgGCSqGSIb3DQEDATCCAgkCggEBAJVHXPXZPllsP80dkCrdAvQn9fPHIQMTu0X7TVuy5f4cvWeM1LvdhMmDa+HzHAd3clrrbC/Di4X0gHb6drzYFGzImm+y9wbdcZiYwgg9yNiW+EBi4snJTRN7BUqNgJatuNUZUjmO7KhSoK8S34Pkdapl1OwMOKlWDVZhGG/5i5/J62Du6LAwN2sja8c746zb10/WHB0kdfowd7jwgEZ4gf9+HKVv7gZteVBq3lHtu1RDpWOSfbxLpSAIZ0YXXIiFkl68ZMYUeQZ3NJaZDLcU7GZzBOJh+u4zs8vfAI4MP6kGUNl9OQnJJ1v0rIb/yz0D5t/IraWTQkLdbTvMoqQGywsCggEAQt67naWz2IzJVuCHh+w/Ogm7pfSLiJp0qvUxdKoPvn48W4/NelO+9WOw6YVgMolgqVF/QBTTMl/Hlivx4Ek3DXbRMUp2E355Lz8NuFnQleSluTICTweezy7wnHl0UrB3DhNQeC7Vfd95SXnc7yPLlvGDBhllxOvJPJxxxWuSWVWnX5TMzxRJrEPVhtC+7kMlGwsihzSdaN4NFEQD8T6AL0FG2ILgV68ZtvYnXGZ2yPoOPKJxOjJX/Rsn0GOfaV40fY0c+ayBmibKmwTLDrm3sDWYjRW7rGUhKlUjnPx+WPrjjXJQq5mR/7yXE0Al/ozgTEOZrZZWm+kaVG9JeGk8egSCAQQCggEAECNvEczf0y6IoX/IwhrPeWZ5IxrHcpwjcdVAuyZQLLlOq0iqnYMFcSD8QjMF8NKObfZZCDQUJlzGzRsG0oXsWiWtmoRvUZ9tQK0j28hDylpbyP00Bt9NlMgeHXkAy54P7Z2v/BPCd3o23kzjgXzYaSRuCFY7zQo1g1IQG8mfjYjdE4jjRVdVrlh8FS8x4OLPeglc+cp2/kuyxaVEfXAG84z/M8019mRSfdczi4z1iidPX6HgDEEWsN42Ud60mNKy5jsQpQYkRdOLmxR3+iQEtGFjdzbVhVCUr7S5EORU9B1MOl5gyPJpjfU3baOqrg6WXVyTvMDaA05YEnAHQNOOfA==", Base64.NO_WRAP)
|
||||||
|
val keyExchangeMessage2 = FCastKeyExchangeMessage(1, "MIIDJTCCAhgGCSqGSIb3DQEDATCCAgkCggEBAJVHXPXZPllsP80dkCrdAvQn9fPHIQMTu0X7TVuy5f4cvWeM1LvdhMmDa+HzHAd3clrrbC/Di4X0gHb6drzYFGzImm+y9wbdcZiYwgg9yNiW+EBi4snJTRN7BUqNgJatuNUZUjmO7KhSoK8S34Pkdapl1OwMOKlWDVZhGG/5i5/J62Du6LAwN2sja8c746zb10/WHB0kdfowd7jwgEZ4gf9+HKVv7gZteVBq3lHtu1RDpWOSfbxLpSAIZ0YXXIiFkl68ZMYUeQZ3NJaZDLcU7GZzBOJh+u4zs8vfAI4MP6kGUNl9OQnJJ1v0rIb/yz0D5t/IraWTQkLdbTvMoqQGywsCggEAQt67naWz2IzJVuCHh+w/Ogm7pfSLiJp0qvUxdKoPvn48W4/NelO+9WOw6YVgMolgqVF/QBTTMl/Hlivx4Ek3DXbRMUp2E355Lz8NuFnQleSluTICTweezy7wnHl0UrB3DhNQeC7Vfd95SXnc7yPLlvGDBhllxOvJPJxxxWuSWVWnX5TMzxRJrEPVhtC+7kMlGwsihzSdaN4NFEQD8T6AL0FG2ILgV68ZtvYnXGZ2yPoOPKJxOjJX/Rsn0GOfaV40fY0c+ayBmibKmwTLDrm3sDWYjRW7rGUhKlUjnPx+WPrjjXJQq5mR/7yXE0Al/ozgTEOZrZZWm+kaVG9JeGk8egOCAQUAAoIBAGlL9EYsrFz3I83NdlwhM241M+M7PA9P5WXgtdvS+pcalIaqN2IYdfzzCUfye7lchVkT9A2Y9eWQYX0OUhmjf8PPKkRkATLXrqO5HTsxV96aYNxMjz5ipQ6CaErTQaPLr3OPoauIMPVVI9zM+WT0KOGp49YMyx+B5rafT066vOVbF/0z1crq0ZXxyYBUv135rwFkIHxBMj5bhRLXKsZ2G5aLAZg0DsVam104mgN/v75f7Spg/n5hO7qxbNgbvSrvQ7Ag/rMk5T3sk7KoM23Qsjl08IZKs2jjx21MiOtyLqGuCW6GOTNK4yEEDF5gA0K13eXGwL5lPS0ilRw+Lrw7cJU=")
|
||||||
|
|
||||||
|
val keyFactory = KeyFactory.getInstance("DH")
|
||||||
|
val privateKeySpec = PKCS8EncodedKeySpec(decodedPrivateKey1)
|
||||||
|
val privateKey = keyFactory.generatePrivate(privateKeySpec)
|
||||||
|
val aesKey1 = FCastCastingDevice.computeSharedSecret(privateKey, keyExchangeMessage2)
|
||||||
|
assertEquals("vI5LGE625zGEG350ggkyBsIAXm2y4sNohiPcED1oAEE=", Base64.encodeToString(aesKey1.encoded, Base64.NO_WRAP))
|
||||||
|
|
||||||
|
val message = FCastPlayMessage("text/html")
|
||||||
|
val serializedBody = Json.encodeToString(message)
|
||||||
|
val encryptedMessage = FCastCastingDevice.encryptMessage(aesKey1, FCastDecryptedMessage(Opcode.Play.value.toLong(), serializedBody))
|
||||||
|
val decryptedMessage = FCastCastingDevice.decryptMessage(aesKey1, encryptedMessage)
|
||||||
|
|
||||||
|
assertEquals(Opcode.Play.value.toLong(), decryptedMessage.opcode)
|
||||||
|
assertEquals(serializedBody, decryptedMessage.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDecryptMessageKnown() {
|
||||||
|
val encryptedMessage = Json.decodeFromString<FCastEncryptedMessage>("{\"version\":1,\"iv\":\"C4H70VC5FWrNtkty9/cLIA==\",\"blob\":\"K6/N7JMyi1PFwKhU0mFj7ZJmd/tPp3NCOMldmQUtDaQ7hSmPoIMI5QNMOj+NFEiP4qTgtYp5QmBPoQum6O88pA==\"}")
|
||||||
|
val aesKey = SecretKeySpec(Base64.decode("+hr9Jg8yre7S9WGUohv2AUSzHNQN514JPh6MoFAcFNU=", Base64.NO_WRAP), "AES")
|
||||||
|
val decryptedMessage = FCastCastingDevice.decryptMessage(aesKey, encryptedMessage)
|
||||||
|
assertEquals(Opcode.Play.value.toLong(), decryptedMessage.opcode)
|
||||||
|
assertEquals("{\"container\":\"text/html\"}", decryptedMessage.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,368 @@
|
|||||||
|
package com.futo.platformplayer
|
||||||
|
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
|
import com.futo.platformplayer.stores.db.ManagedDBDescriptor
|
||||||
|
import com.futo.platformplayer.stores.db.ManagedDBStore
|
||||||
|
import com.futo.platformplayer.testing.DBTOs
|
||||||
|
import org.junit.Assert
|
||||||
|
import org.junit.Test
|
||||||
|
import java.util.UUID
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import java.util.concurrent.ConcurrentMap
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
class ManagedDBStoreTests {
|
||||||
|
val context = InstrumentationRegistry.getInstrumentation().targetContext;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun startup() {
|
||||||
|
val store = ManagedDBStore.create("test", Descriptor())
|
||||||
|
.load(context, true);
|
||||||
|
store.deleteAll();
|
||||||
|
|
||||||
|
store.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun insert() {
|
||||||
|
val store = ManagedDBStore.create("test", Descriptor())
|
||||||
|
.load(context, true);
|
||||||
|
store.deleteAll();
|
||||||
|
|
||||||
|
val testObj = DBTOs.TestObject();
|
||||||
|
createAndAssert(store, testObj);
|
||||||
|
|
||||||
|
store.shutdown();
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
fun update() {
|
||||||
|
val store = ManagedDBStore.create("test", Descriptor())
|
||||||
|
.load(context, true);
|
||||||
|
store.deleteAll();
|
||||||
|
|
||||||
|
val testObj = DBTOs.TestObject();
|
||||||
|
val obj = createAndAssert(store, testObj);
|
||||||
|
|
||||||
|
testObj.someStr = "Testing";
|
||||||
|
store.update(obj.id!!, testObj);
|
||||||
|
val obj2 = store.get(obj.id!!);
|
||||||
|
assertIndexEquals(obj2, testObj);
|
||||||
|
|
||||||
|
store.shutdown();
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
fun delete() {
|
||||||
|
val store = ManagedDBStore.create("test", Descriptor())
|
||||||
|
.load(context, true);
|
||||||
|
store.deleteAll();
|
||||||
|
|
||||||
|
val testObj = DBTOs.TestObject();
|
||||||
|
val obj = createAndAssert(store, testObj);
|
||||||
|
store.delete(obj.id!!);
|
||||||
|
|
||||||
|
Assert.assertEquals(store.count(), 0);
|
||||||
|
Assert.assertNull(store.getOrNull(obj.id!!));
|
||||||
|
|
||||||
|
store.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun withIndex() {
|
||||||
|
val index = ConcurrentHashMap<Any, DBTOs.TestIndex>();
|
||||||
|
val store = ManagedDBStore.create("test", Descriptor())
|
||||||
|
.withIndex({it.someString}, index, true)
|
||||||
|
.load(context, true);
|
||||||
|
store.deleteAll();
|
||||||
|
|
||||||
|
val testObj1 = DBTOs.TestObject();
|
||||||
|
val testObj2 = DBTOs.TestObject();
|
||||||
|
val testObj3 = DBTOs.TestObject();
|
||||||
|
val obj1 = createAndAssert(store, testObj1);
|
||||||
|
val obj2 = createAndAssert(store, testObj2);
|
||||||
|
val obj3 = createAndAssert(store, testObj3);
|
||||||
|
Assert.assertEquals(store.count(), 3);
|
||||||
|
|
||||||
|
Assert.assertTrue(index.containsKey(testObj1.someStr));
|
||||||
|
Assert.assertTrue(index.containsKey(testObj2.someStr));
|
||||||
|
Assert.assertTrue(index.containsKey(testObj3.someStr));
|
||||||
|
Assert.assertEquals(index.size, 3);
|
||||||
|
|
||||||
|
val oldStr = testObj1.someStr;
|
||||||
|
testObj1.someStr = UUID.randomUUID().toString();
|
||||||
|
store.update(obj1.id!!, testObj1);
|
||||||
|
|
||||||
|
Assert.assertEquals(index.size, 3);
|
||||||
|
Assert.assertFalse(index.containsKey(oldStr));
|
||||||
|
Assert.assertTrue(index.containsKey(testObj1.someStr));
|
||||||
|
Assert.assertTrue(index.containsKey(testObj2.someStr));
|
||||||
|
Assert.assertTrue(index.containsKey(testObj3.someStr));
|
||||||
|
|
||||||
|
store.delete(obj2.id!!);
|
||||||
|
Assert.assertEquals(index.size, 2);
|
||||||
|
|
||||||
|
Assert.assertFalse(index.containsKey(oldStr));
|
||||||
|
Assert.assertTrue(index.containsKey(testObj1.someStr));
|
||||||
|
Assert.assertFalse(index.containsKey(testObj2.someStr));
|
||||||
|
Assert.assertTrue(index.containsKey(testObj3.someStr));
|
||||||
|
store.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun withUnique() {
|
||||||
|
val index = ConcurrentHashMap<Any, DBTOs.TestIndex>();
|
||||||
|
val store = ManagedDBStore.create("test", Descriptor())
|
||||||
|
.withIndex({it.someString}, index, false, true)
|
||||||
|
.load(context, true);
|
||||||
|
store.deleteAll();
|
||||||
|
|
||||||
|
val testObj1 = DBTOs.TestObject();
|
||||||
|
val testObj2 = DBTOs.TestObject();
|
||||||
|
val testObj3 = DBTOs.TestObject();
|
||||||
|
val obj1 = createAndAssert(store, testObj1);
|
||||||
|
val obj2 = createAndAssert(store, testObj2);
|
||||||
|
|
||||||
|
testObj3.someStr = testObj2.someStr;
|
||||||
|
Assert.assertEquals(store.insert(testObj3), obj2.id!!);
|
||||||
|
Assert.assertEquals(store.count(), 2);
|
||||||
|
|
||||||
|
store.shutdown();
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
fun getPage() {
|
||||||
|
val store = ManagedDBStore.create("test", Descriptor())
|
||||||
|
.load(context, true);
|
||||||
|
store.deleteAll();
|
||||||
|
|
||||||
|
val testObjs = createSequence(store, 25);
|
||||||
|
|
||||||
|
val page1 = store.getPage(0, 10);
|
||||||
|
val page2 = store.getPage(1, 10);
|
||||||
|
val page3 = store.getPage(2, 10);
|
||||||
|
Assert.assertEquals(10, page1.size);
|
||||||
|
Assert.assertEquals(10, page2.size);
|
||||||
|
Assert.assertEquals(5, page3.size);
|
||||||
|
|
||||||
|
store.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun query() {
|
||||||
|
val store = ManagedDBStore.create("test", Descriptor())
|
||||||
|
.load(context, true);
|
||||||
|
store.deleteAll();
|
||||||
|
|
||||||
|
val testStr = UUID.randomUUID().toString();
|
||||||
|
|
||||||
|
val testObj1 = DBTOs.TestObject();
|
||||||
|
val testObj2 = DBTOs.TestObject();
|
||||||
|
val testObj3 = DBTOs.TestObject();
|
||||||
|
val testObj4 = DBTOs.TestObject();
|
||||||
|
testObj3.someStr = testStr;
|
||||||
|
testObj4.someStr = testStr;
|
||||||
|
val obj1 = createAndAssert(store, testObj1);
|
||||||
|
val obj2 = createAndAssert(store, testObj2);
|
||||||
|
val obj3 = createAndAssert(store, testObj3);
|
||||||
|
val obj4 = createAndAssert(store, testObj4);
|
||||||
|
|
||||||
|
val results = store.query(DBTOs.TestIndex::someString, testStr);
|
||||||
|
|
||||||
|
Assert.assertEquals(2, results.size);
|
||||||
|
for(result in results) {
|
||||||
|
if(result.someNum == obj3.someNum)
|
||||||
|
assertIndexEquals(obj3, result);
|
||||||
|
else
|
||||||
|
assertIndexEquals(obj4, result);
|
||||||
|
}
|
||||||
|
store.shutdown();
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
fun queryPage() {
|
||||||
|
val index = ConcurrentHashMap<Any, DBTOs.TestIndex>();
|
||||||
|
val store = ManagedDBStore.create("test", Descriptor())
|
||||||
|
.withIndex({ it.someNum }, index)
|
||||||
|
.load(context, true);
|
||||||
|
store.deleteAll();
|
||||||
|
|
||||||
|
val testStr = UUID.randomUUID().toString();
|
||||||
|
|
||||||
|
val testResults = createSequence(store, 40, { i, testObject ->
|
||||||
|
if(i % 2 == 0)
|
||||||
|
testObject.someStr = testStr;
|
||||||
|
});
|
||||||
|
val page1 = store.queryPage(DBTOs.TestIndex::someString, testStr, 0,10);
|
||||||
|
val page2 = store.queryPage(DBTOs.TestIndex::someString, testStr, 1,10);
|
||||||
|
val page3 = store.queryPage(DBTOs.TestIndex::someString, testStr, 2,10);
|
||||||
|
|
||||||
|
Assert.assertEquals(10, page1.size);
|
||||||
|
Assert.assertEquals(10, page2.size);
|
||||||
|
Assert.assertEquals(0, page3.size);
|
||||||
|
|
||||||
|
|
||||||
|
store.shutdown();
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
fun queryPager() {
|
||||||
|
val testStr = UUID.randomUUID().toString();
|
||||||
|
testQuery(100, { i, testObject ->
|
||||||
|
if(i % 2 == 0)
|
||||||
|
testObject.someStr = testStr;
|
||||||
|
}) {
|
||||||
|
val pager = it.queryPager(DBTOs.TestIndex::someString, testStr, 10);
|
||||||
|
|
||||||
|
val items = pager.getResults().toMutableList();
|
||||||
|
while(pager.hasMorePages()) {
|
||||||
|
pager.nextPage();
|
||||||
|
items.addAll(pager.getResults());
|
||||||
|
}
|
||||||
|
Assert.assertEquals(50, items.size);
|
||||||
|
for(i in 0 until 50) {
|
||||||
|
val k = i * 2;
|
||||||
|
Assert.assertEquals(k, items[i].someNum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun queryLike() {
|
||||||
|
val testStr = UUID.randomUUID().toString();
|
||||||
|
val testStrLike = testStr.substring(0, 8) + "Testing" + testStr.substring(8, testStr.length);
|
||||||
|
testQuery(100, { i, testObject ->
|
||||||
|
if(i % 2 == 0)
|
||||||
|
testObject.someStr = testStrLike;
|
||||||
|
}) {
|
||||||
|
val results = it.queryLike(DBTOs.TestIndex::someString, "%Testing%");
|
||||||
|
|
||||||
|
Assert.assertEquals(50, results.size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
fun queryLikePager() {
|
||||||
|
val testStr = UUID.randomUUID().toString();
|
||||||
|
val testStrLike = testStr.substring(0, 8) + "Testing" + testStr.substring(8, testStr.length);
|
||||||
|
testQuery(100, { i, testObject ->
|
||||||
|
if(i % 2 == 0)
|
||||||
|
testObject.someStr = testStrLike;
|
||||||
|
|
||||||
|
}) {
|
||||||
|
val pager = it.queryLikePager(DBTOs.TestIndex::someString, "%Testing%", 10);
|
||||||
|
val items = pager.getResults().toMutableList();
|
||||||
|
while(pager.hasMorePages()) {
|
||||||
|
pager.nextPage();
|
||||||
|
items.addAll(pager.getResults());
|
||||||
|
}
|
||||||
|
Assert.assertEquals(50, items.size);
|
||||||
|
for(i in 0 until 50) {
|
||||||
|
val k = i * 2;
|
||||||
|
Assert.assertEquals(k, items[i].someNum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun queryGreater() {
|
||||||
|
testQuery(100, { i, testObject ->
|
||||||
|
testObject.someNum = i;
|
||||||
|
}) {
|
||||||
|
val results = it.queryGreater(DBTOs.TestIndex::someNum, 51);
|
||||||
|
Assert.assertEquals(48, results.size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
fun querySmaller() {
|
||||||
|
testQuery(100, { i, testObject ->
|
||||||
|
testObject.someNum = i;
|
||||||
|
}) {
|
||||||
|
val results = it.querySmaller(DBTOs.TestIndex::someNum, 30);
|
||||||
|
Assert.assertEquals(30, results.size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
fun queryBetween() {
|
||||||
|
testQuery(100, { i, testObject ->
|
||||||
|
testObject.someNum = i;
|
||||||
|
}) {
|
||||||
|
val results = it.queryBetween(DBTOs.TestIndex::someNum, 30, 65);
|
||||||
|
Assert.assertEquals(34, results.size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
fun queryIn() {
|
||||||
|
val ids = mutableListOf<String>()
|
||||||
|
testQuery(1100, { i, testObject ->
|
||||||
|
testObject.someNum = i;
|
||||||
|
ids.add(testObject.someStr);
|
||||||
|
}) {
|
||||||
|
val pager = it.queryInPager(DBTOs.TestIndex::someString, ids.take(1000), 65);
|
||||||
|
val list = mutableListOf<Any>();
|
||||||
|
list.addAll(pager.getResults());
|
||||||
|
while(pager.hasMorePages())
|
||||||
|
{
|
||||||
|
pager.nextPage();
|
||||||
|
list.addAll(pager.getResults());
|
||||||
|
}
|
||||||
|
Assert.assertEquals(1000, list.size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun testQuery(items: Int, modifier: (Int, DBTOs.TestObject)->Unit, testing: (ManagedDBStore<DBTOs.TestIndex, DBTOs.TestObject, DBTOs.DB, DBTOs.DBDAO>)->Unit) {
|
||||||
|
val store = ManagedDBStore.create("test", Descriptor())
|
||||||
|
.load(context, true);
|
||||||
|
store.deleteAll();
|
||||||
|
createSequence(store, items, modifier);
|
||||||
|
try {
|
||||||
|
testing(store);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
store.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun createSequence(store: ManagedDBStore<DBTOs.TestIndex, DBTOs.TestObject, DBTOs.DB, DBTOs.DBDAO>, count: Int, modifier: ((Int, DBTOs.TestObject)->Unit)? = null): List<DBTOs.TestIndex> {
|
||||||
|
val list = mutableListOf<DBTOs.TestIndex>();
|
||||||
|
for(i in 0 until count) {
|
||||||
|
val obj = DBTOs.TestObject();
|
||||||
|
obj.someNum = i;
|
||||||
|
modifier?.invoke(i, obj);
|
||||||
|
list.add(createAndAssert(store, obj));
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createAndAssert(store: ManagedDBStore<DBTOs.TestIndex, DBTOs.TestObject, DBTOs.DB, DBTOs.DBDAO>, obj: DBTOs.TestObject): DBTOs.TestIndex {
|
||||||
|
val id = store.insert(obj);
|
||||||
|
Assert.assertTrue(id > 0);
|
||||||
|
|
||||||
|
val dbObj = store.get(id);
|
||||||
|
assertIndexEquals(dbObj, obj);
|
||||||
|
return dbObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assertObjectEquals(obj1: DBTOs.TestObject, obj2: DBTOs.TestObject) {
|
||||||
|
Assert.assertEquals(obj1.someStr, obj2.someStr);
|
||||||
|
Assert.assertEquals(obj1.someNum, obj2.someNum);
|
||||||
|
}
|
||||||
|
private fun assertIndexEquals(obj1: DBTOs.TestIndex, obj2: DBTOs.TestObject) {
|
||||||
|
Assert.assertEquals(obj1.someString, obj2.someStr);
|
||||||
|
Assert.assertEquals(obj1.someNum, obj2.someNum);
|
||||||
|
assertObjectEquals(obj1.obj, obj2);
|
||||||
|
}
|
||||||
|
private fun assertIndexEquals(obj1: DBTOs.TestIndex, obj2: DBTOs.TestIndex) {
|
||||||
|
Assert.assertEquals(obj1.someString, obj2.someString);
|
||||||
|
Assert.assertEquals(obj1.someNum, obj2.someNum);
|
||||||
|
assertIndexEquals(obj1, obj2.obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Descriptor: ManagedDBDescriptor<DBTOs.TestObject, DBTOs.TestIndex, DBTOs.DB, DBTOs.DBDAO>() {
|
||||||
|
override val table_name: String = "testing";
|
||||||
|
override fun indexClass(): KClass<DBTOs.TestIndex> = DBTOs.TestIndex::class;
|
||||||
|
override fun dbClass(): KClass<DBTOs.DB> = DBTOs.DB::class;
|
||||||
|
override fun create(obj: DBTOs.TestObject): DBTOs.TestIndex = DBTOs.TestIndex(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,8 +7,13 @@
|
|||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" tools:ignore="ScopedStorage" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<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.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
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
@@ -20,7 +25,8 @@
|
|||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.FutoVideo"
|
android:theme="@style/Theme.FutoVideo"
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
tools:targetApi="31">
|
tools:targetApi="31"
|
||||||
|
android:largeHeap="true">
|
||||||
<provider
|
<provider
|
||||||
android:name="androidx.core.content.FileProvider"
|
android:name="androidx.core.content.FileProvider"
|
||||||
android:authorities="@string/authority"
|
android:authorities="@string/authority"
|
||||||
@@ -33,12 +39,12 @@
|
|||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:foregroundServiceType="mediaPlayback" />
|
android:foregroundServiceType="mediaPlayback" />
|
||||||
<service android:name=".services.DownloadService"
|
<service android:name=".services.DownloadService"
|
||||||
android:enabled="true" />
|
android:enabled="true"
|
||||||
<service android:name=".services.ExportingService"
|
android:foregroundServiceType="dataSync" />
|
||||||
android:enabled="true" />
|
|
||||||
|
|
||||||
<receiver android:name=".receivers.MediaControlReceiver" />
|
<receiver android:name=".receivers.MediaControlReceiver" />
|
||||||
<receiver android:name=".receivers.AudioNoisyReceiver" />
|
<receiver android:name=".receivers.AudioNoisyReceiver" />
|
||||||
|
<receiver android:name=".receivers.PlannedNotificationReceiver" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.MainActivity"
|
android:name=".activities.MainActivity"
|
||||||
@@ -58,6 +64,14 @@
|
|||||||
|
|
||||||
<data android:scheme="grayjay" />
|
<data android:scheme="grayjay" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="fcast" />
|
||||||
|
</intent-filter>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
@@ -207,5 +221,9 @@
|
|||||||
android:name=".activities.QRCaptureActivity"
|
android:name=".activities.QRCaptureActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
|
<activity
|
||||||
|
android:name=".activities.FCastGuideActivity"
|
||||||
|
android:screenOrientation="portrait"
|
||||||
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
</application>
|
</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) {
|
function pluginRemoteCall(objID, methodName, args) {
|
||||||
return JSON.parse(syncPOST("/plugin/remoteCall?id=" + objID + "&method=" + methodName, {}, JSON.stringify(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) {
|
function pluginIsLoggedIn(cb, err) {
|
||||||
fetch("/plugin/isLoggedIn", {
|
fetch("/plugin/isLoggedIn", {
|
||||||
@@ -259,6 +262,17 @@ function getDevLogs(lastIndex, cb) {
|
|||||||
.then(x=>x.json())
|
.then(x=>x.json())
|
||||||
.then(y=> cb && cb(y));
|
.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) {
|
function sendFakeDevLog(devId, msg) {
|
||||||
return syncGET("/plugin/fakeDevLog?devId=" + devId + "&msg=" + msg, {});
|
return syncGET("/plugin/fakeDevLog?devId=" + devId + "&msg=" + msg, {});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,9 @@
|
|||||||
<!--<link href="./dependencies/vuetify.min.css" rel="stylesheet">-->
|
<!--<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">
|
<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">
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -150,7 +153,7 @@
|
|||||||
.pastPluginUrl {
|
.pastPluginUrl {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
width: 500px;
|
width: 700px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
@@ -160,13 +163,122 @@
|
|||||||
box-shadow: 0px 1px 2px #131313;
|
box-shadow: 0px 1px 2px #131313;
|
||||||
font-weight: lighter;
|
font-weight: lighter;
|
||||||
cursor: pointer;
|
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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<v-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 id="topMenu">
|
||||||
<div style="height: 100%; display: inline-block; padding-left: 10px; padding-right: 20px;">
|
<div style="height: 100%; display: inline-block; padding-left: 10px; padding-right: 20px;">
|
||||||
<img src="./dependencies/FutoMainLogo.svg"
|
<img src="./dependencies/FutoMainLogo.svg"
|
||||||
@@ -250,10 +362,13 @@
|
|||||||
</div>
|
</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>
|
<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)">
|
<div class="pastPluginUrl" v-for="pastPluginUrl in pastPluginUrls" @click="this.Plugin.newPluginUrl = pastPluginUrl; loadPlugin(pastPluginUrl)">
|
||||||
{{pastPluginUrl}}
|
{{pastPluginUrl}}
|
||||||
|
<div class="deleteButton" @click="(ev)=>{ev.stopPropagation(); deletePastPlugin(pastPluginUrl)}">
|
||||||
|
X
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -385,8 +500,8 @@
|
|||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
<div style="width: 50%" v-if="Plugin.currentPlugin">
|
<div style="width: 50%" v-if="Plugin.currentPlugin">
|
||||||
<!--Get Home-->
|
<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-card class="requestCard" v-for="req in Testing.requests" v-show="req.title.indexOf(searchTestMethods) >= 0">
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<span v-if="req.isOptional">(Optional)</span>
|
<span v-if="req.isOptional">(Optional)</span>
|
||||||
@@ -402,6 +517,11 @@
|
|||||||
<div class="code">
|
<div class="code">
|
||||||
{{req.code}}
|
{{req.code}}
|
||||||
</div>
|
</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>
|
||||||
<div class="parameter" v-for="parameter in req.parameters">
|
<div class="parameter" v-for="parameter in req.parameters">
|
||||||
<div class="name">
|
<div class="name">
|
||||||
@@ -416,6 +536,9 @@
|
|||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-card-actions>
|
<v-card-actions>
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn @click="testSourceRemotely(req)">
|
||||||
|
Test Android
|
||||||
|
</v-btn>
|
||||||
<v-btn @click="testSource(req)">
|
<v-btn @click="testSource(req)">
|
||||||
Test
|
Test
|
||||||
</v-btn>
|
</v-btn>
|
||||||
@@ -497,7 +620,62 @@
|
|||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-card-actions>
|
<v-card-actions>
|
||||||
<v-spacer></v-spacer>
|
<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-actions>
|
||||||
</v-card>
|
</v-card>
|
||||||
</div>
|
</div>
|
||||||
@@ -535,14 +713,18 @@
|
|||||||
<!--<script src="./dependencies/vue.js"></script>-->
|
<!--<script src="./dependencies/vue.js"></script>-->
|
||||||
<!--<script src="./dependencies/vuetify.js"></script>-->
|
<!--<script src="./dependencies/vuetify.js"></script>-->
|
||||||
<script src="./source_docs.js"></script>
|
<script src="./source_docs.js"></script>
|
||||||
|
<script src="./source_doc_urls.js"></script>
|
||||||
<script src="./source.js"></script>
|
<script src="./source.js"></script>
|
||||||
<script src="./dev_bridge.js"></script>
|
<script src="./dev_bridge.js"></script>
|
||||||
<script>
|
<script>
|
||||||
IS_TESTING = true;
|
IS_TESTING = true;
|
||||||
let lastScriptTag = null;
|
let lastScriptTag = null;
|
||||||
|
let shouldDevLog = true;
|
||||||
|
let shouldLoginCheck = true;
|
||||||
new Vue({
|
new Vue({
|
||||||
el: '#app',
|
el: '#app',
|
||||||
data: {
|
data: {
|
||||||
|
searchTestMethods: "",
|
||||||
page: "Plugin",
|
page: "Plugin",
|
||||||
pastPluginUrls: [],
|
pastPluginUrls: [],
|
||||||
settings: {},
|
settings: {},
|
||||||
@@ -550,7 +732,9 @@
|
|||||||
lastLogIndex: -1,
|
lastLogIndex: -1,
|
||||||
lastLogDevID: "",
|
lastLogDevID: "",
|
||||||
logs: [],
|
logs: [],
|
||||||
lastInjectTime: ""
|
httpExchanges: [],
|
||||||
|
lastInjectTime: "",
|
||||||
|
showHttpRequests: false
|
||||||
},
|
},
|
||||||
Plugin: {
|
Plugin: {
|
||||||
loadUsingTag: false,
|
loadUsingTag: false,
|
||||||
@@ -568,6 +752,9 @@
|
|||||||
Testing: {
|
Testing: {
|
||||||
requests: sourceDocs.map(x=>{
|
requests: sourceDocs.map(x=>{
|
||||||
x.parameters.forEach(y=>y.value = null);
|
x.parameters.forEach(y=>y.value = null);
|
||||||
|
|
||||||
|
if(sourceDocUrls[x.title])
|
||||||
|
x.docUrl = sourceDocUrls[x.title];
|
||||||
return x;
|
return x;
|
||||||
}),
|
}),
|
||||||
lastResult: "",
|
lastResult: "",
|
||||||
@@ -603,7 +790,7 @@
|
|||||||
};
|
};
|
||||||
setInterval(()=>{
|
setInterval(()=>{
|
||||||
try{
|
try{
|
||||||
if(!this.Plugin.currentPlugin)
|
if(!this.Plugin.currentPlugin || !shouldDevLog)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
getDevLogs(this.Integration.lastLogIndex, (newLogs)=> {
|
getDevLogs(this.Integration.lastLogIndex, (newLogs)=> {
|
||||||
@@ -631,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) {
|
catch(ex) {
|
||||||
console.error("Failed update", ex);
|
console.error("Failed update", ex);
|
||||||
@@ -638,7 +835,8 @@
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
setInterval(()=>{
|
setInterval(()=>{
|
||||||
try{
|
try{
|
||||||
this.isTestLoggedIn();
|
if(shouldLoginCheck)
|
||||||
|
this.isTestLoggedIn();
|
||||||
}catch(ex){}
|
}catch(ex){}
|
||||||
}, 2500);
|
}, 2500);
|
||||||
},
|
},
|
||||||
@@ -671,6 +869,12 @@
|
|||||||
this.reloadPlugin();
|
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() {
|
loginTestPlugin() {
|
||||||
pluginLoginTestPlugin();
|
pluginLoginTestPlugin();
|
||||||
setTimeout(()=>{
|
setTimeout(()=>{
|
||||||
@@ -857,8 +1061,58 @@
|
|||||||
"Error: " + ex;
|
"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) {
|
showTestResults(results) {
|
||||||
|
|
||||||
|
},
|
||||||
|
toggleHttpExchange(exchange) {
|
||||||
|
exchange.response.show = !exchange.response.show;
|
||||||
},
|
},
|
||||||
copyClipboard(cpy) {
|
copyClipboard(cpy) {
|
||||||
if(navigator.clipboard)
|
if(navigator.clipboard)
|
||||||
|
|||||||
+121
-47
@@ -1,13 +1,37 @@
|
|||||||
|
|
||||||
declare class ScriptException extends Error {
|
declare class ScriptException extends Error {
|
||||||
|
//If only one parameter is provided, acts as msg
|
||||||
constructor(type: string, msg: string);
|
constructor(type: string, msg: string);
|
||||||
}
|
}
|
||||||
declare class TimeoutException extends ScriptException {
|
|
||||||
|
declare class LoginRequiredException extends ScriptException {
|
||||||
constructor(msg: string);
|
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 {
|
declare class UnavailableException extends ScriptException {
|
||||||
constructor(msg: string);
|
constructor(msg: string);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare class AgeException extends ScriptException {
|
||||||
|
constructor(msg: string);
|
||||||
|
}
|
||||||
|
|
||||||
|
declare class TimeoutException extends ScriptException {
|
||||||
|
constructor(msg: string);
|
||||||
|
}
|
||||||
|
|
||||||
declare class ScriptImplementationException extends ScriptException {
|
declare class ScriptImplementationException extends ScriptException {
|
||||||
constructor(msg: string);
|
constructor(msg: string);
|
||||||
}
|
}
|
||||||
@@ -38,16 +62,23 @@ declare class FilterCapability {
|
|||||||
|
|
||||||
|
|
||||||
declare class PlatformAuthorLink {
|
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 {
|
declare interface PlatformContentDef {
|
||||||
id: PlatformID,
|
id: PlatformID,
|
||||||
name: string,
|
name: string,
|
||||||
|
thumbnails: Thumbnails,
|
||||||
author: PlatformAuthorLink,
|
author: PlatformAuthorLink,
|
||||||
datetime: integer,
|
datetime: integer,
|
||||||
url: string
|
url: string
|
||||||
}
|
}
|
||||||
|
declare interface PlatformContent {}
|
||||||
|
|
||||||
declare interface PlatformNestedMediaContentDef extends PlatformContentDef {
|
declare interface PlatformNestedMediaContentDef extends PlatformContentDef {
|
||||||
contentUrl: string,
|
contentUrl: string,
|
||||||
contentName: string?,
|
contentName: string?,
|
||||||
@@ -59,16 +90,26 @@ declare class PlatformNestedMediaContent {
|
|||||||
constructor(obj: PlatformNestedMediaContentDef);
|
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 {
|
declare interface PlatformVideoDef extends PlatformContentDef {
|
||||||
thumbnails: Thumbnails,
|
thumbnails: Thumbnails,
|
||||||
author: PlatformAuthorLink,
|
author: PlatformAuthorLink,
|
||||||
|
|
||||||
duration: int,
|
duration: int,
|
||||||
viewCount: long,
|
viewCount: long,
|
||||||
isLive: boolean
|
isLive: boolean,
|
||||||
|
shareUrl: string?
|
||||||
}
|
}
|
||||||
declare interface PlatformContent {}
|
|
||||||
|
|
||||||
declare class PlatformVideo implements PlatformContent {
|
declare class PlatformVideo implements PlatformContent {
|
||||||
constructor(obj: PlatformVideoDef);
|
constructor(obj: PlatformVideoDef);
|
||||||
}
|
}
|
||||||
@@ -77,15 +118,16 @@ declare class PlatformVideo implements PlatformContent {
|
|||||||
declare interface PlatformVideoDetailsDef extends PlatformVideoDef {
|
declare interface PlatformVideoDetailsDef extends PlatformVideoDef {
|
||||||
description: string,
|
description: string,
|
||||||
video: VideoSourceDescriptor,
|
video: VideoSourceDescriptor,
|
||||||
live: SubtitleSource[],
|
live: IVideoSource,
|
||||||
rating: IRating
|
rating: IRating,
|
||||||
|
subtitles: SubtitleSource[]
|
||||||
}
|
}
|
||||||
declare class PlatformVideoDetails extends PlatformVideo {
|
declare class PlatformVideoDetails extends PlatformVideo {
|
||||||
constructor(obj: PlatformVideoDetailsDef);
|
constructor(obj: PlatformVideoDetailsDef);
|
||||||
}
|
}
|
||||||
|
|
||||||
declare class PlatformPostDef extends PlatformContentDef {
|
declare interface PlatformPostDef extends PlatformContentDef {
|
||||||
thumbnails: string[],
|
thumbnails: Thumbnails[],
|
||||||
images: string[],
|
images: string[],
|
||||||
description: string
|
description: string
|
||||||
}
|
}
|
||||||
@@ -93,7 +135,7 @@ declare class PlatformPost extends PlatformContent {
|
|||||||
constructor(obj: PlatformPostDef)
|
constructor(obj: PlatformPostDef)
|
||||||
}
|
}
|
||||||
|
|
||||||
declare class PlatformPostDetailsDef extends PlatformPostDef {
|
declare interface PlatformPostDetailsDef extends PlatformPostDef {
|
||||||
rating: IRating,
|
rating: IRating,
|
||||||
textType: int,
|
textType: int,
|
||||||
content: String
|
content: String
|
||||||
@@ -110,8 +152,8 @@ declare interface MuxVideoSourceDescriptorDef {
|
|||||||
isUnMuxed: boolean,
|
isUnMuxed: boolean,
|
||||||
videoSources: VideoSource[]
|
videoSources: VideoSource[]
|
||||||
}
|
}
|
||||||
declare class MuxVideoSourceDescriptor implements IVideoSourceDescriptor {
|
declare class VideoSourceDescriptor implements IVideoSourceDescriptor {
|
||||||
constructor(obj: VideoSourceDescriptorDef);
|
constructor(videoSourcesOrObj: VideoSource[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
declare interface UnMuxVideoSourceDescriptorDef {
|
declare interface UnMuxVideoSourceDescriptorDef {
|
||||||
@@ -129,7 +171,7 @@ declare interface IVideoSource {
|
|||||||
declare interface IAudioSource {
|
declare interface IAudioSource {
|
||||||
|
|
||||||
}
|
}
|
||||||
interface VideoUrlSourceDef implements IVideoSource {
|
declare interface VideoUrlSourceDef implements IVideoSource {
|
||||||
width: integer,
|
width: integer,
|
||||||
height: integer,
|
height: integer,
|
||||||
container: string,
|
container: string,
|
||||||
@@ -139,22 +181,22 @@ interface VideoUrlSourceDef implements IVideoSource {
|
|||||||
duration: integer,
|
duration: integer,
|
||||||
url: string
|
url: string
|
||||||
}
|
}
|
||||||
class VideoUrlSource {
|
declare class VideoUrlSource {
|
||||||
constructor(obj: VideoUrlSourceDef);
|
constructor(obj: VideoUrlSourceDef);
|
||||||
|
|
||||||
getRequestModifier(): RequestModifier?;
|
getRequestModifier(): RequestModifier?;
|
||||||
}
|
}
|
||||||
interface VideoUrlRangeSourceDef extends VideoUrlSource {
|
declare interface VideoUrlRangeSourceDef extends VideoUrlSource {
|
||||||
itagId: integer,
|
itagId: integer,
|
||||||
initStart: integer,
|
initStart: integer,
|
||||||
initEnd: integer,
|
initEnd: integer,
|
||||||
indexStart: integer,
|
indexStart: integer,
|
||||||
indexEnd: integer,
|
indexEnd: integer,
|
||||||
}
|
}
|
||||||
class VideoUrlRangeSource extends VideoUrlSource {
|
declare class VideoUrlRangeSource extends VideoUrlSource {
|
||||||
constructor(obj: YTVideoSourceDef);
|
constructor(obj: YTVideoSourceDef);
|
||||||
}
|
}
|
||||||
interface AudioUrlSourceDef {
|
declare interface AudioUrlSourceDef {
|
||||||
name: string,
|
name: string,
|
||||||
bitrate: integer,
|
bitrate: integer,
|
||||||
container: string,
|
container: string,
|
||||||
@@ -163,24 +205,12 @@ interface AudioUrlSourceDef {
|
|||||||
url: string,
|
url: string,
|
||||||
language: string
|
language: string
|
||||||
}
|
}
|
||||||
class AudioUrlSource implements IAudioSource {
|
declare class AudioUrlSource implements IAudioSource {
|
||||||
constructor(obj: AudioUrlSourceDef);
|
constructor(obj: AudioUrlSourceDef);
|
||||||
|
|
||||||
getRequestModifier(): RequestModifier?;
|
getRequestModifier(): RequestModifier?;
|
||||||
}
|
}
|
||||||
interface IRequest {
|
declare interface AudioUrlRangeSourceDef extends AudioUrlSource {
|
||||||
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 {
|
|
||||||
itagId: integer,
|
itagId: integer,
|
||||||
initStart: integer,
|
initStart: integer,
|
||||||
initEnd: integer,
|
initEnd: integer,
|
||||||
@@ -188,28 +218,44 @@ interface AudioUrlRangeSourceDef extends AudioUrlSource {
|
|||||||
indexEnd: integer,
|
indexEnd: integer,
|
||||||
audioChannels: integer
|
audioChannels: integer
|
||||||
}
|
}
|
||||||
class AudioUrlRangeSource extends AudioUrlSource {
|
declare class AudioUrlRangeSource extends AudioUrlSource {
|
||||||
constructor(obj: AudioUrlRangeSourceDef);
|
constructor(obj: AudioUrlRangeSourceDef);
|
||||||
}
|
}
|
||||||
interface HLSSourceDef {
|
declare interface HLSSourceDef {
|
||||||
name: string,
|
name: string,
|
||||||
duration: integer,
|
duration: integer,
|
||||||
url: string
|
url: string,
|
||||||
|
priority: boolean?,
|
||||||
|
language: string?
|
||||||
}
|
}
|
||||||
class HLSSource implements IVideoSource {
|
declare class HLSSource implements IVideoSource {
|
||||||
constructor(obj: HLSSourceDef);
|
constructor(obj: HLSSourceDef);
|
||||||
}
|
}
|
||||||
interface DashSourceDef {
|
declare interface DashSourceDef {
|
||||||
name: string,
|
name: string,
|
||||||
duration: integer,
|
duration: integer,
|
||||||
url: string
|
url: string,
|
||||||
|
language: string?
|
||||||
}
|
}
|
||||||
class DashSource implements IVideoSource {
|
declare class DashSource implements IVideoSource {
|
||||||
constructor(obj: DashSourceDef)
|
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
|
//Channel
|
||||||
interface PlatformChannelDef {
|
declare interface PlatformChannelDef {
|
||||||
id: PlatformID,
|
id: PlatformID,
|
||||||
name: string,
|
name: string,
|
||||||
thumbnail: string,
|
thumbnail: string,
|
||||||
@@ -217,12 +263,29 @@ interface PlatformChannelDef {
|
|||||||
subscribers: integer,
|
subscribers: integer,
|
||||||
description: string,
|
description: string,
|
||||||
url: string,
|
url: string,
|
||||||
|
urlAlternatives: string[],
|
||||||
links: Map<string>?
|
links: Map<string>?
|
||||||
}
|
}
|
||||||
class PlatformChannel {
|
declare class PlatformChannel {
|
||||||
constructor(obj: PlatformChannelDef);
|
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
|
//Ratings
|
||||||
interface IRating {
|
interface IRating {
|
||||||
type: integer
|
type: integer
|
||||||
@@ -250,7 +313,11 @@ declare class PlatformComment {
|
|||||||
constructor(obj: CommentDef);
|
constructor(obj: CommentDef);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare class PlaybackTracker {
|
||||||
|
constructor(interval: integer);
|
||||||
|
|
||||||
|
setProgress(seconds: integer);
|
||||||
|
}
|
||||||
|
|
||||||
declare class LiveEventPager {
|
declare class LiveEventPager {
|
||||||
nextRequest = 4000;
|
nextRequest = 4000;
|
||||||
@@ -261,8 +328,8 @@ declare class LiveEventPager {
|
|||||||
nextPage(): LiveEventPager; //Could be self
|
nextPage(): LiveEventPager; //Could be self
|
||||||
}
|
}
|
||||||
|
|
||||||
class LiveEvent {
|
declare class LiveEvent {
|
||||||
type: String
|
constructor(type: integer);
|
||||||
}
|
}
|
||||||
declare class LiveEventComment extends LiveEvent {
|
declare class LiveEventComment extends LiveEvent {
|
||||||
constructor(name: string, message: string, thumbnail: string?, colorName: string?, badges: string[]);
|
constructor(name: string, message: string, thumbnail: string?, colorName: string?, badges: string[]);
|
||||||
@@ -287,25 +354,31 @@ declare class ContentPager {
|
|||||||
constructor(results: PlatformContent[], hasMore: boolean);
|
constructor(results: PlatformContent[], hasMore: boolean);
|
||||||
|
|
||||||
hasMorePagers(): boolean
|
hasMorePagers(): boolean
|
||||||
nextPage(): VideoPager; //Could be self
|
nextPage(): ContentPager?; //Could be self
|
||||||
}
|
}
|
||||||
declare class VideoPager {
|
declare class VideoPager {
|
||||||
constructor(results: PlatformVideo[], hasMore: boolean);
|
constructor(results: PlatformVideo[], hasMore: boolean);
|
||||||
|
|
||||||
hasMorePagers(): boolean
|
hasMorePagers(): boolean
|
||||||
nextPage(): VideoPager; //Could be self
|
nextPage(): VideoPager?; //Could be self
|
||||||
}
|
}
|
||||||
declare class ChannelPager {
|
declare class ChannelPager {
|
||||||
constructor(results: PlatformChannel[], hasMore: boolean);
|
constructor(results: PlatformChannel[], hasMore: boolean);
|
||||||
|
|
||||||
hasMorePagers(): 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 {
|
declare class CommentPager {
|
||||||
constructor(results: PlatformComment[], hasMore: boolean);
|
constructor(results: PlatformComment[], hasMore: boolean);
|
||||||
|
|
||||||
hasMorePagers(): boolean
|
hasMorePagers(): boolean
|
||||||
nextPage(): CommentPager; //Could be self
|
nextPage(): CommentPager?; //Could be self
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Map<T> {
|
interface Map<T> {
|
||||||
@@ -341,8 +414,9 @@ interface Source {
|
|||||||
getChannelCapabilities(): ResultCapabilities;
|
getChannelCapabilities(): ResultCapabilities;
|
||||||
|
|
||||||
isContentDetailsUrl(url: string): boolean;
|
isContentDetailsUrl(url: string): boolean;
|
||||||
getContentDetails(url: string): PlatformVideoDetails;
|
getContentDetails(url: string): PlatformContentDetails;
|
||||||
|
|
||||||
|
//Optional
|
||||||
getLiveEvents(url: string): LiveEventPager;
|
getLiveEvents(url: string): LiveEventPager;
|
||||||
|
|
||||||
//Optional
|
//Optional
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ let Type = {
|
|||||||
Videos: "VIDEOS",
|
Videos: "VIDEOS",
|
||||||
Streams: "STREAMS",
|
Streams: "STREAMS",
|
||||||
Mixed: "MIXED",
|
Mixed: "MIXED",
|
||||||
Live: "LIVE"
|
Live: "LIVE",
|
||||||
|
Subscriptions: "SUBSCRIPTIONS"
|
||||||
},
|
},
|
||||||
Order: {
|
Order: {
|
||||||
Chronological: "CHRONOLOGICAL"
|
Chronological: "CHRONOLOGICAL"
|
||||||
@@ -36,24 +37,26 @@ let Type = {
|
|||||||
NORMAL: 0,
|
NORMAL: 0,
|
||||||
|
|
||||||
SKIPPABLE: 5,
|
SKIPPABLE: 5,
|
||||||
SKIP: 6
|
SKIP: 6,
|
||||||
|
SKIPONCE: 7
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let Language = {
|
let Language = {
|
||||||
UNKNOWN: "Unknown",
|
UNKNOWN: "Unknown",
|
||||||
ARABIC: "Arabic",
|
ARABIC: "ar",
|
||||||
SPANISH: "Spanish",
|
SPANISH: "es",
|
||||||
FRENCH: "French",
|
FRENCH: "fr",
|
||||||
HINDI: "Hindi",
|
HINDI: "hi",
|
||||||
INDONESIAN: "Indonesian",
|
INDONESIAN: "id",
|
||||||
KOREAN: "Korean",
|
KOREAN: "ko",
|
||||||
PORTBRAZIL: "Portuguese Brazilian",
|
PORTUGUESE: "pt",
|
||||||
RUSSIAN: "Russian",
|
PORTBRAZIL: "pt",
|
||||||
THAI: "Thai",
|
RUSSIAN: "ru",
|
||||||
TURKISH: "Turkish",
|
THAI: "th",
|
||||||
VIETNAMESE: "Vietnamese",
|
TURKISH: "tr",
|
||||||
ENGLISH: "English"
|
VIETNAMESE: "vi",
|
||||||
|
ENGLISH: "en"
|
||||||
}
|
}
|
||||||
|
|
||||||
class ScriptException extends Error {
|
class ScriptException extends Error {
|
||||||
@@ -70,6 +73,16 @@ class ScriptException extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
class ScriptLoginRequiredException extends ScriptException {
|
||||||
|
constructor(msg) {
|
||||||
|
super("ScriptLoginRequiredException", msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class LoginRequiredException extends ScriptException {
|
||||||
|
constructor(msg) {
|
||||||
|
super("ScriptLoginRequiredException", msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
class CaptchaRequiredException extends Error {
|
class CaptchaRequiredException extends Error {
|
||||||
constructor(url, body) {
|
constructor(url, body) {
|
||||||
super(JSON.stringify({ 'plugin_type': 'CaptchaRequiredException', url, body }));
|
super(JSON.stringify({ 'plugin_type': 'CaptchaRequiredException', url, body }));
|
||||||
@@ -210,6 +223,16 @@ class PlatformNestedMediaContent extends PlatformContent {
|
|||||||
this.contentThumbnails = obj.contentThumbnails ?? new Thumbnails();
|
this.contentThumbnails = obj.contentThumbnails ?? new Thumbnails();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
class PlatformLockedContent extends PlatformContent {
|
||||||
|
constructor(obj) {
|
||||||
|
super(obj, 70);
|
||||||
|
obj = obj ?? {};
|
||||||
|
this.contentName = obj.contentName;
|
||||||
|
this.contentThumbnails = obj.contentThumbnails ?? new Thumbnails();
|
||||||
|
this.unlockUrl = obj.unlockUrl ?? "";
|
||||||
|
this.lockDescription = obj.lockDescription;
|
||||||
|
}
|
||||||
|
}
|
||||||
class PlatformVideo extends PlatformContent {
|
class PlatformVideo extends PlatformContent {
|
||||||
constructor(obj) {
|
constructor(obj) {
|
||||||
super(obj, 1);
|
super(obj, 1);
|
||||||
@@ -231,8 +254,8 @@ class PlatformVideoDetails extends PlatformVideo {
|
|||||||
|
|
||||||
this.description = obj.description ?? "";//String
|
this.description = obj.description ?? "";//String
|
||||||
this.video = obj.video ?? {}; //VideoSourceDescriptor
|
this.video = obj.video ?? {}; //VideoSourceDescriptor
|
||||||
this.dash = obj.dash ?? null; //DashSource
|
this.dash = obj.dash ?? null; //DashSource, deprecated
|
||||||
this.hls = obj.hls ?? null; //HLSSource
|
this.hls = obj.hls ?? null; //HLSSource, deprecated
|
||||||
this.live = obj.live ?? null; //VideoSource
|
this.live = obj.live ?? null; //VideoSource
|
||||||
|
|
||||||
this.rating = obj.rating ?? null; //IRating
|
this.rating = obj.rating ?? null; //IRating
|
||||||
@@ -303,6 +326,8 @@ class VideoUrlSource {
|
|||||||
this.bitrate = obj.bitrate ?? 0;
|
this.bitrate = obj.bitrate ?? 0;
|
||||||
this.duration = obj.duration ?? 0;
|
this.duration = obj.duration ?? 0;
|
||||||
this.url = obj.url;
|
this.url = obj.url;
|
||||||
|
if(obj.requestModifier)
|
||||||
|
this.requestModifier = obj.requestModifier;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class VideoUrlRangeSource extends VideoUrlSource {
|
class VideoUrlRangeSource extends VideoUrlSource {
|
||||||
@@ -328,6 +353,17 @@ class AudioUrlSource {
|
|||||||
this.duration = obj.duration ?? 0;
|
this.duration = obj.duration ?? 0;
|
||||||
this.url = obj.url;
|
this.url = obj.url;
|
||||||
this.language = obj.language ?? Language.UNKNOWN;
|
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.bearerToken = obj.bearerToken;
|
||||||
|
this.licenseUri = obj.licenseUri;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class AudioUrlRangeSource extends AudioUrlSource {
|
class AudioUrlRangeSource extends AudioUrlSource {
|
||||||
@@ -353,6 +389,8 @@ class HLSSource {
|
|||||||
this.priority = obj.priority ?? false;
|
this.priority = obj.priority ?? false;
|
||||||
if(obj.language)
|
if(obj.language)
|
||||||
this.language = obj.language;
|
this.language = obj.language;
|
||||||
|
if(obj.requestModifier)
|
||||||
|
this.requestModifier = obj.requestModifier;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class DashSource {
|
class DashSource {
|
||||||
@@ -364,13 +402,15 @@ class DashSource {
|
|||||||
this.url = obj.url;
|
this.url = obj.url;
|
||||||
if(obj.language)
|
if(obj.language)
|
||||||
this.language = obj.language;
|
this.language = obj.language;
|
||||||
|
if(obj.requestModifier)
|
||||||
|
this.requestModifier = obj.requestModifier;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class RequestModifier {
|
class RequestModifier {
|
||||||
constructor(obj) {
|
constructor(obj) {
|
||||||
obj = obj ?? {};
|
obj = obj ?? {};
|
||||||
this.allowByteSkip = obj.allowByteSkip;
|
this.allowByteSkip = obj.allowByteSkip; //Kinda deprecated.. wip
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -396,7 +436,7 @@ class PlatformPlaylist extends PlatformContent {
|
|||||||
constructor(obj) {
|
constructor(obj) {
|
||||||
super(obj, 4);
|
super(obj, 4);
|
||||||
this.plugin_type = "PlatformPlaylist";
|
this.plugin_type = "PlatformPlaylist";
|
||||||
this.videoCount = obj.videoCount ?? 0;
|
this.videoCount = obj.videoCount ?? -1;
|
||||||
this.thumbnail = obj.thumbnail;
|
this.thumbnail = obj.thumbnail;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,28 @@
|
|||||||
package com.futo.platformplayer
|
import androidx.annotation.OptIn
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
import androidx.media3.datasource.DefaultHttpDataSource
|
||||||
|
import androidx.media3.datasource.HttpDataSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
|
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
|
||||||
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
|
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
|
||||||
import com.futo.platformplayer.helpers.VideoHelper
|
import com.futo.platformplayer.helpers.VideoHelper
|
||||||
|
import com.futo.platformplayer.views.video.datasources.JSHttpDataSource
|
||||||
|
|
||||||
fun IPlatformVideoDetails.isDownloadable(): Boolean = VideoHelper.isDownloadable(this);
|
fun IPlatformVideoDetails.isDownloadable(): Boolean = VideoHelper.isDownloadable(this);
|
||||||
fun IVideoSource.isDownloadable(): Boolean = VideoHelper.isDownloadable(this);
|
fun IVideoSource.isDownloadable(): Boolean = VideoHelper.isDownloadable(this);
|
||||||
fun IAudioSource.isDownloadable(): Boolean = VideoHelper.isDownloadable(this);
|
fun IAudioSource.isDownloadable(): Boolean = VideoHelper.isDownloadable(this);
|
||||||
|
|
||||||
|
@UnstableApi
|
||||||
|
fun JSSource.getHttpDataSourceFactory(): HttpDataSource.Factory {
|
||||||
|
val requestModifier = getRequestModifier();
|
||||||
|
return if (requestModifier != null) {
|
||||||
|
JSHttpDataSource.Factory().setRequestModifier(requestModifier);
|
||||||
|
} else {
|
||||||
|
DefaultHttpDataSource.Factory();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun IVideoSourceDescriptor.hasAnySource(): Boolean = this.videoSources.any() || (this is VideoUnMuxedSourceDescriptor && this.audioSources.any());
|
fun IVideoSourceDescriptor.hasAnySource(): Boolean = this.videoSources.any() || (this is VideoUnMuxedSourceDescriptor && this.audioSources.any());
|
||||||
@@ -13,7 +13,8 @@ import java.text.DecimalFormat
|
|||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.time.temporal.ChronoUnit
|
import java.time.temporal.ChronoUnit
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.time.toDuration
|
import kotlin.math.roundToInt
|
||||||
|
import kotlin.math.roundToLong
|
||||||
|
|
||||||
|
|
||||||
//Long
|
//Long
|
||||||
@@ -120,7 +121,8 @@ fun OffsetDateTime.getNowDiffMonths(): Long {
|
|||||||
return ChronoUnit.MONTHS.between(this, OffsetDateTime.now());
|
return ChronoUnit.MONTHS.between(this, OffsetDateTime.now());
|
||||||
}
|
}
|
||||||
fun OffsetDateTime.getNowDiffYears(): Long {
|
fun OffsetDateTime.getNowDiffYears(): Long {
|
||||||
return ChronoUnit.YEARS.between(this, OffsetDateTime.now());
|
val diff = ChronoUnit.MONTHS.between(this, OffsetDateTime.now()) / 12.0;
|
||||||
|
return diff.roundToLong();
|
||||||
}
|
}
|
||||||
|
|
||||||
fun OffsetDateTime.getDiffDays(otherDate: OffsetDateTime): Long {
|
fun OffsetDateTime.getDiffDays(otherDate: OffsetDateTime): Long {
|
||||||
@@ -151,6 +153,7 @@ fun OffsetDateTime.toHumanNowDiffString(abs: Boolean = false) : String {
|
|||||||
if(value >= secondsInYear) {
|
if(value >= secondsInYear) {
|
||||||
value = getNowDiffYears();
|
value = getNowDiffYears();
|
||||||
if(abs) value = abs(value);
|
if(abs) value = abs(value);
|
||||||
|
value = Math.max(1, value);
|
||||||
unit = "year";
|
unit = "year";
|
||||||
}
|
}
|
||||||
else if(value >= secondsInMonth) {
|
else if(value >= secondsInMonth) {
|
||||||
@@ -228,6 +231,18 @@ fun String.fixHtmlWhitespace(): Spanned {
|
|||||||
return Html.fromHtml(replace("\n", "<br />"), HtmlCompat.FROM_HTML_MODE_LEGACY);
|
return Html.fromHtml(replace("\n", "<br />"), HtmlCompat.FROM_HTML_MODE_LEGACY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Long.formatDuration(): String {
|
||||||
|
val hours = this / 3600000
|
||||||
|
val minutes = (this % 3600000) / 60000
|
||||||
|
val seconds = (this % 60000) / 1000
|
||||||
|
|
||||||
|
return if (hours > 0) {
|
||||||
|
String.format("%02d:%02d:%02d", hours, minutes, seconds)
|
||||||
|
} else {
|
||||||
|
String.format("%02d:%02d", minutes, seconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun String.fixHtmlLinks(): Spanned {
|
fun String.fixHtmlLinks(): Spanned {
|
||||||
//TODO: Properly fix whitespace handling.
|
//TODO: Properly fix whitespace handling.
|
||||||
val doc = Jsoup.parse(replace("\n", "<br />"));
|
val doc = Jsoup.parse(replace("\n", "<br />"));
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
package com.futo.platformplayer
|
package com.futo.platformplayer
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import com.google.common.base.CharMatcher
|
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.Inet4Address
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import java.net.InetSocketAddress
|
import java.net.InetSocketAddress
|
||||||
@@ -165,7 +169,7 @@ private fun parseHextet(ipString: String, start: Int, end: Int): Short {
|
|||||||
var hextet = 0
|
var hextet = 0
|
||||||
for (i in start until end) {
|
for (i in start until end) {
|
||||||
hextet = hextet shl 4
|
hextet = hextet shl 4
|
||||||
hextet = hextet or ipString[i].digitToIntOrNull(16)!! ?: -1
|
hextet = hextet or ipString[i].digitToIntOrNull(16)!!
|
||||||
}
|
}
|
||||||
return hextet.toShort()
|
return hextet.toShort()
|
||||||
}
|
}
|
||||||
@@ -212,15 +216,20 @@ private fun ByteArray.toInetAddress(): InetAddress {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? {
|
fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? {
|
||||||
|
val timeout = 2000
|
||||||
|
|
||||||
if (addresses.isEmpty()) {
|
if (addresses.isEmpty()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (addresses.size == 1) {
|
if (addresses.size == 1) {
|
||||||
|
val socket = Socket()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return Socket(addresses[0], port);
|
return socket.apply { this.connect(InetSocketAddress(addresses[0], port), timeout) }
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
//Ignored.
|
Log.i("getConnectedSocket", "Failed to connect to: ${addresses[0]}", e)
|
||||||
|
socket.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@@ -245,7 +254,7 @@ fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.connect(InetSocketAddress(address, port));
|
socket.connect(InetSocketAddress(address, port), timeout);
|
||||||
|
|
||||||
synchronized(syncObject) {
|
synchronized(syncObject) {
|
||||||
if (connectedSocket == null) {
|
if (connectedSocket == null) {
|
||||||
@@ -259,7 +268,7 @@ fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
//Ignore
|
Log.i("getConnectedSocket", "Failed to connect to: $address", e)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -273,3 +282,46 @@ fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? {
|
|||||||
|
|
||||||
return connectedSocket;
|
return connectedSocket;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun InputStream.readHttpHeaderBytes() : ByteArray {
|
||||||
|
val headerBytes = ByteArrayOutputStream()
|
||||||
|
var crlfCount = 0
|
||||||
|
|
||||||
|
while (crlfCount < 4) {
|
||||||
|
val b = read()
|
||||||
|
if (b == -1) {
|
||||||
|
throw IOException("Unexpected end of stream while reading headers")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (b == 0x0D || b == 0x0A) { // CR or LF
|
||||||
|
crlfCount++
|
||||||
|
} else {
|
||||||
|
crlfCount = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
headerBytes.write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
return headerBytes.toByteArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun InputStream.readLine() : String? {
|
||||||
|
val line = ByteArrayOutputStream()
|
||||||
|
var crlfCount = 0
|
||||||
|
|
||||||
|
while (crlfCount < 2) {
|
||||||
|
val b = read()
|
||||||
|
if (b == -1) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (b == 0x0D || b == 0x0A) { // CR or LF
|
||||||
|
crlfCount++
|
||||||
|
} else {
|
||||||
|
crlfCount = 0
|
||||||
|
line.write(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(line.toByteArray(), Charsets.UTF_8)
|
||||||
|
}
|
||||||
@@ -1,6 +1,13 @@
|
|||||||
package com.futo.platformplayer
|
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.platformplayer.states.StatePlatform
|
||||||
|
import com.futo.polycentric.core.ProcessHandle
|
||||||
|
import com.futo.polycentric.core.Store
|
||||||
|
import com.futo.polycentric.core.SystemState
|
||||||
import userpackage.Protocol
|
import userpackage.Protocol
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
@@ -39,4 +46,27 @@ fun Protocol.Claim.resolveChannelUrl(): String? {
|
|||||||
|
|
||||||
fun Protocol.Claim.resolveChannelUrls(): List<String> {
|
fun Protocol.Claim.resolveChannelUrls(): List<String> {
|
||||||
return StatePlatform.instance.resolveChannelUrlsByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) })
|
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.SERVER)) {
|
||||||
|
Logger.w("Backfill", "Polycentric prod server not added, adding it.")
|
||||||
|
addServer(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,5 +1,10 @@
|
|||||||
package com.futo.platformplayer
|
package com.futo.platformplayer
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import java.net.URI
|
||||||
|
import java.net.URISyntaxException
|
||||||
|
import java.net.URLEncoder
|
||||||
|
|
||||||
//Syntax sugaring
|
//Syntax sugaring
|
||||||
inline fun <reified T> Any.assume(): T?{
|
inline fun <reified T> Any.assume(): T?{
|
||||||
if(this is T)
|
if(this is T)
|
||||||
@@ -12,4 +17,12 @@ inline fun <reified T, R> Any.assume(cb: (T) -> R): R? {
|
|||||||
if(result != null)
|
if(result != null)
|
||||||
return cb(result);
|
return cb(result);
|
||||||
return null;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fun String?.yesNoToBoolean(): Boolean {
|
||||||
|
return this?.uppercase() == "YES"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Boolean?.toYesNo(): String {
|
||||||
|
return if (this == true) "YES" else "NO"
|
||||||
}
|
}
|
||||||
@@ -27,7 +27,7 @@ fun <R> V8Value?.orDefault(default: R, handler: (V8Value)->R): R {
|
|||||||
inline fun <reified T> V8Value.expectOrThrow(config: IV8PluginConfig, contextName: String): T {
|
inline fun <reified T> V8Value.expectOrThrow(config: IV8PluginConfig, contextName: String): T {
|
||||||
if(this !is T)
|
if(this !is T)
|
||||||
throw ScriptImplementationException(config, "Expected ${contextName} to be of type ${T::class.simpleName}, but found ${this::class.simpleName}");
|
throw ScriptImplementationException(config, "Expected ${contextName} to be of type ${T::class.simpleName}, but found ${this::class.simpleName}");
|
||||||
return this as T;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
//Singles
|
//Singles
|
||||||
@@ -109,11 +109,29 @@ inline fun <reified T> V8Value.expectV8Variant(config: IV8PluginConfig, contextN
|
|||||||
else
|
else
|
||||||
return this.expectOrThrow<V8ValueLong>(config, contextName).value.toLong() as T
|
return this.expectOrThrow<V8ValueLong>(config, contextName).value.toLong() as T
|
||||||
};
|
};
|
||||||
|
Float::class -> {
|
||||||
|
if(this is V8ValueDouble)
|
||||||
|
return this.value.toFloat() as T;
|
||||||
|
else if(this is V8ValueInteger)
|
||||||
|
return this.value.toFloat() as T;
|
||||||
|
else if(this is V8ValueLong)
|
||||||
|
return this.value.toFloat() as T;
|
||||||
|
else
|
||||||
|
return this.expectOrThrow<V8ValueDouble>(config, contextName).value.toDouble() as T
|
||||||
|
};
|
||||||
|
Double::class -> {
|
||||||
|
if(this is V8ValueDouble)
|
||||||
|
return this.value.toDouble() as T;
|
||||||
|
else if(this is V8ValueInteger)
|
||||||
|
return this.value.toDouble() as T;
|
||||||
|
else if(this is V8ValueLong)
|
||||||
|
return this.value.toDouble() as T;
|
||||||
|
else
|
||||||
|
return this.expectOrThrow<V8ValueDouble>(config, contextName).value.toDouble() as T
|
||||||
|
};
|
||||||
V8ValueObject::class -> this.expectOrThrow<V8ValueObject>(config, contextName) as T
|
V8ValueObject::class -> this.expectOrThrow<V8ValueObject>(config, contextName) as T
|
||||||
V8ValueArray::class -> this.expectOrThrow<V8ValueArray>(config, contextName) as T;
|
V8ValueArray::class -> this.expectOrThrow<V8ValueArray>(config, contextName) as T;
|
||||||
Boolean::class -> this.expectOrThrow<V8ValueBoolean>(config, contextName).value as T;
|
Boolean::class -> this.expectOrThrow<V8ValueBoolean>(config, contextName).value as T;
|
||||||
Float::class -> this.expectOrThrow<V8ValueDouble>(config, contextName).value.toFloat() as T;
|
|
||||||
Double::class -> this.expectOrThrow<V8ValueDouble>(config, contextName).value as T;
|
|
||||||
HashMap::class -> this.expectOrThrow<V8ValueObject>(config, contextName).let { V8ObjectToHashMap(it) } as T;
|
HashMap::class -> this.expectOrThrow<V8ValueObject>(config, contextName).let { V8ObjectToHashMap(it) } as T;
|
||||||
Map::class -> this.expectOrThrow<V8ValueObject>(config, contextName).let { V8ObjectToHashMap(it) } as T;
|
Map::class -> this.expectOrThrow<V8ValueObject>(config, contextName).let { V8ObjectToHashMap(it) } as T;
|
||||||
List::class -> this.expectOrThrow<V8ValueArray>(config, contextName).let { V8ArrayToStringList(it) } as T;
|
List::class -> this.expectOrThrow<V8ValueArray>(config, contextName).let { V8ArrayToStringList(it) } as T;
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.futo.platformplayer
|
||||||
|
|
||||||
|
class PresetImages {
|
||||||
|
companion object {
|
||||||
|
val images = mapOf<String, Int>(
|
||||||
|
Pair("xp_book", R.drawable.xp_book),
|
||||||
|
Pair("xp_forest", R.drawable.xp_forest),
|
||||||
|
Pair("xp_code", R.drawable.xp_code),
|
||||||
|
Pair("xp_controller", R.drawable.xp_controller),
|
||||||
|
Pair("xp_laptop", R.drawable.xp_laptop)
|
||||||
|
);
|
||||||
|
|
||||||
|
fun getPresetResIdByName(name: String): Int {
|
||||||
|
return images[name] ?: -1;
|
||||||
|
}
|
||||||
|
fun getPresetNameByResId(id: Int): String? {
|
||||||
|
return images.entries.find { it.value == id }?.key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,29 +6,43 @@ import android.content.Intent
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.webkit.CookieManager
|
import android.webkit.CookieManager
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.activities.*
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
|
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.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.constructs.Event0
|
import com.futo.platformplayer.constructs.Event0
|
||||||
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.serializers.FlexibleBooleanSerializer
|
import com.futo.platformplayer.serializers.FlexibleBooleanSerializer
|
||||||
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
|
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
|
||||||
import com.futo.platformplayer.states.*
|
import com.futo.platformplayer.states.StateAnnouncement
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.futo.platformplayer.states.StateBackup
|
||||||
|
import com.futo.platformplayer.states.StateCache
|
||||||
|
import com.futo.platformplayer.states.StateMeta
|
||||||
|
import com.futo.platformplayer.states.StatePayment
|
||||||
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
|
import com.futo.platformplayer.states.StateUpdate
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
import com.futo.platformplayer.stores.FragmentedStorageFileJson
|
import com.futo.platformplayer.stores.FragmentedStorageFileJson
|
||||||
import com.futo.platformplayer.views.FeedStyle
|
import com.futo.platformplayer.views.FeedStyle
|
||||||
import com.futo.platformplayer.views.fields.DropdownFieldOptionsId
|
import com.futo.platformplayer.views.fields.DropdownFieldOptionsId
|
||||||
import com.futo.platformplayer.views.fields.FormField
|
|
||||||
import com.futo.platformplayer.views.fields.FieldForm
|
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.FormFieldButton
|
||||||
|
import com.futo.platformplayer.views.fields.FormFieldWarning
|
||||||
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.*
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.*
|
import kotlinx.serialization.Transient
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class MenuBottomBarSetting(val id: Int, var enabled: Boolean);
|
data class MenuBottomBarSetting(val id: Int, var enabled: Boolean);
|
||||||
@@ -43,25 +57,23 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@Transient
|
@Transient
|
||||||
val onTabsChanged = Event0();
|
val onTabsChanged = Event0();
|
||||||
|
|
||||||
@FormField(
|
@FormField(R.string.manage_polycentric_identity, FieldForm.BUTTON, R.string.manage_your_polycentric_identity, -6)
|
||||||
R.string.manage_polycentric_identity, FieldForm.BUTTON,
|
|
||||||
R.string.manage_your_polycentric_identity, -5
|
|
||||||
)
|
|
||||||
@FormFieldButton(R.drawable.ic_person)
|
@FormFieldButton(R.drawable.ic_person)
|
||||||
fun managePolycentricIdentity() {
|
fun managePolycentricIdentity() {
|
||||||
SettingsActivity.getActivity()?.let {
|
SettingsActivity.getActivity()?.let {
|
||||||
if (StatePolycentric.instance.processHandle != null) {
|
if (StatePolycentric.instance.enabled) {
|
||||||
it.startActivity(Intent(it, PolycentricProfileActivity::class.java));
|
if (StatePolycentric.instance.processHandle != null) {
|
||||||
|
it.startActivity(Intent(it, PolycentricProfileActivity::class.java));
|
||||||
|
} else {
|
||||||
|
it.startActivity(Intent(it, PolycentricHomeActivity::class.java));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
it.startActivity(Intent(it, PolycentricHomeActivity::class.java));
|
UIDialogs.toast(it, "Polycentric is disabled")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(
|
@FormField(R.string.show_faq, FieldForm.BUTTON, R.string.get_answers_to_common_questions, -5)
|
||||||
R.string.show_faq, FieldForm.BUTTON,
|
|
||||||
R.string.get_answers_to_common_questions, -4
|
|
||||||
)
|
|
||||||
@FormFieldButton(R.drawable.ic_quiz)
|
@FormFieldButton(R.drawable.ic_quiz)
|
||||||
fun openFAQ() {
|
fun openFAQ() {
|
||||||
try {
|
try {
|
||||||
@@ -71,10 +83,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
//Ignored
|
//Ignored
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@FormField(
|
@FormField(R.string.show_issues, FieldForm.BUTTON, R.string.a_list_of_user_reported_and_self_reported_issues, -4)
|
||||||
R.string.show_issues, FieldForm.BUTTON,
|
|
||||||
R.string.a_list_of_user_reported_and_self_reported_issues, -3
|
|
||||||
)
|
|
||||||
@FormFieldButton(R.drawable.ic_data_alert)
|
@FormFieldButton(R.drawable.ic_data_alert)
|
||||||
fun openIssues() {
|
fun openIssues() {
|
||||||
try {
|
try {
|
||||||
@@ -106,10 +115,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
}
|
}
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
@FormField(
|
@FormField(R.string.manage_tabs, FieldForm.BUTTON, R.string.change_tabs_visible_on_the_home_screen, -3)
|
||||||
R.string.manage_tabs, FieldForm.BUTTON,
|
|
||||||
R.string.change_tabs_visible_on_the_home_screen, -2
|
|
||||||
)
|
|
||||||
@FormFieldButton(R.drawable.ic_tabs)
|
@FormFieldButton(R.drawable.ic_tabs)
|
||||||
fun manageTabs() {
|
fun manageTabs() {
|
||||||
try {
|
try {
|
||||||
@@ -123,7 +129,26 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
@FormField(R.string.home, "group", R.string.configure_how_your_home_tab_works_and_feels, 0)
|
@FormField(R.string.import_data, FieldForm.BUTTON, R.string.import_data_description, -2)
|
||||||
|
@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)
|
||||||
|
@FormFieldButton(R.drawable.ic_link)
|
||||||
|
fun manageLinks() {
|
||||||
|
try {
|
||||||
|
SettingsActivity.getActivity()?.let { UIDialogs.showUrlHandlingPrompt(it) }
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to show url handling prompt", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@FormField(R.string.language, "group", -1, 0)
|
||||||
var language = LanguageSettings();
|
var language = LanguageSettings();
|
||||||
@Serializable
|
@Serializable
|
||||||
class LanguageSettings {
|
class LanguageSettings {
|
||||||
@@ -166,6 +191,21 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
|
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
|
||||||
var previewFeedItems: Boolean = true;
|
var previewFeedItems: Boolean = true;
|
||||||
|
|
||||||
|
|
||||||
|
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
|
||||||
|
var progressBar: Boolean = true;
|
||||||
|
|
||||||
|
|
||||||
|
@FormField(R.string.clear_hidden, FieldForm.BUTTON, R.string.clear_hidden_description, 8)
|
||||||
|
@FormFieldButton(R.drawable.ic_visibility_off)
|
||||||
|
fun clearHidden() {
|
||||||
|
StateMeta.instance.removeAllHiddenCreators();
|
||||||
|
StateMeta.instance.removeAllHiddenVideos();
|
||||||
|
SettingsActivity.getActivity()?.let {
|
||||||
|
UIDialogs.toast(it, "Creators and videos should show up again");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.search, "group", -1, 2)
|
@FormField(R.string.search, "group", -1, 2)
|
||||||
@@ -184,6 +224,8 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 5)
|
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 5)
|
||||||
var previewFeedItems: Boolean = true;
|
var previewFeedItems: Boolean = true;
|
||||||
|
|
||||||
|
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
|
||||||
|
var progressBar: Boolean = true;
|
||||||
|
|
||||||
|
|
||||||
fun getSearchFeedStyle(): FeedStyle {
|
fun getSearchFeedStyle(): FeedStyle {
|
||||||
@@ -194,7 +236,17 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.subscriptions, "group", R.string.configure_how_your_subscriptions_works_and_feels, 3)
|
|
||||||
|
@FormField(R.string.channel, "group", -1, 3)
|
||||||
|
var channel = ChannelSettings();
|
||||||
|
@Serializable
|
||||||
|
class ChannelSettings {
|
||||||
|
|
||||||
|
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
|
||||||
|
var progressBar: Boolean = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@FormField(R.string.subscriptions, "group", R.string.configure_how_your_subscriptions_works_and_feels, 4)
|
||||||
var subscriptions = SubscriptionsSettings();
|
var subscriptions = SubscriptionsSettings();
|
||||||
@Serializable
|
@Serializable
|
||||||
class SubscriptionsSettings {
|
class SubscriptionsSettings {
|
||||||
@@ -209,17 +261,23 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
return FeedStyle.THUMBNAIL;
|
return FeedStyle.THUMBNAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 5)
|
@FormField(R.string.show_subscription_group, FieldForm.TOGGLE, R.string.show_subscription_group_description, 5)
|
||||||
|
var showSubscriptionGroups: Boolean = true;
|
||||||
|
|
||||||
|
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
|
||||||
var previewFeedItems: Boolean = true;
|
var previewFeedItems: Boolean = true;
|
||||||
|
|
||||||
@FormField(R.string.fetch_on_app_boot, FieldForm.TOGGLE, R.string.shortly_after_opening_the_app_start_fetching_subscriptions, 6)
|
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 7)
|
||||||
|
var progressBar: Boolean = true;
|
||||||
|
|
||||||
|
@FormField(R.string.fetch_on_app_boot, FieldForm.TOGGLE, R.string.shortly_after_opening_the_app_start_fetching_subscriptions, 8)
|
||||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||||
var fetchOnAppBoot: Boolean = true;
|
var fetchOnAppBoot: Boolean = true;
|
||||||
|
|
||||||
@FormField(R.string.fetch_on_tab_opened, FieldForm.TOGGLE, R.string.fetch_on_tab_opened_description, 6)
|
@FormField(R.string.fetch_on_tab_opened, FieldForm.TOGGLE, R.string.fetch_on_tab_opened_description, 9)
|
||||||
var fetchOnTabOpen: Boolean = true;
|
var fetchOnTabOpen: Boolean = true;
|
||||||
|
|
||||||
@FormField(R.string.background_update, FieldForm.DROPDOWN, R.string.experimental_background_update_for_subscriptions_cache, 7)
|
@FormField(R.string.background_update, FieldForm.DROPDOWN, R.string.experimental_background_update_for_subscriptions_cache, 10, "background_update")
|
||||||
@DropdownFieldOptionsId(R.array.background_interval)
|
@DropdownFieldOptionsId(R.array.background_interval)
|
||||||
var subscriptionsBackgroundUpdateInterval: Int = 0;
|
var subscriptionsBackgroundUpdateInterval: Int = 0;
|
||||||
|
|
||||||
@@ -235,7 +293,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@FormField(R.string.subscription_concurrency, FieldForm.DROPDOWN, R.string.specify_how_many_threads_are_used_to_fetch_channels, 8)
|
@FormField(R.string.subscription_concurrency, FieldForm.DROPDOWN, R.string.specify_how_many_threads_are_used_to_fetch_channels, 11)
|
||||||
@DropdownFieldOptionsId(R.array.thread_count)
|
@DropdownFieldOptionsId(R.array.thread_count)
|
||||||
var subscriptionConcurrency: Int = 3;
|
var subscriptionConcurrency: Int = 3;
|
||||||
|
|
||||||
@@ -243,18 +301,28 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
return threadIndexToCount(subscriptionConcurrency);
|
return threadIndexToCount(subscriptionConcurrency);
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.show_watch_metrics, FieldForm.TOGGLE, R.string.show_watch_metrics_description, 9)
|
@FormField(R.string.show_watch_metrics, FieldForm.TOGGLE, R.string.show_watch_metrics_description, 12)
|
||||||
var showWatchMetrics: Boolean = false;
|
var showWatchMetrics: Boolean = false;
|
||||||
|
|
||||||
@FormField(R.string.track_playtime_locally, FieldForm.TOGGLE, R.string.track_playtime_locally_description, 10)
|
@FormField(R.string.track_playtime_locally, FieldForm.TOGGLE, R.string.track_playtime_locally_description, 13)
|
||||||
var allowPlaytimeTracking: Boolean = true;
|
var allowPlaytimeTracking: Boolean = true;
|
||||||
|
|
||||||
|
|
||||||
@FormField(R.string.always_reload_from_cache, FieldForm.TOGGLE, R.string.always_reload_from_cache_description, 11)
|
@FormField(R.string.always_reload_from_cache, FieldForm.TOGGLE, R.string.always_reload_from_cache_description, 14)
|
||||||
var alwaysReloadFromCache: Boolean = false;
|
var alwaysReloadFromCache: Boolean = false;
|
||||||
|
|
||||||
|
@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();
|
||||||
|
UIDialogs.toast(SettingsActivity.getActivity()!!, "Finished clearing");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.player, "group", R.string.change_behavior_of_the_player, 4)
|
@FormField(R.string.player, "group", R.string.change_behavior_of_the_player, 5)
|
||||||
var playback = PlaybackSettings();
|
var playback = PlaybackSettings();
|
||||||
@Serializable
|
@Serializable
|
||||||
class PlaybackSettings {
|
class PlaybackSettings {
|
||||||
@@ -262,7 +330,28 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@DropdownFieldOptionsId(R.array.audio_languages)
|
@DropdownFieldOptionsId(R.array.audio_languages)
|
||||||
var primaryLanguage: Int = 0;
|
var primaryLanguage: Int = 0;
|
||||||
|
|
||||||
fun getPrimaryLanguage(context: Context) = context.resources.getStringArray(R.array.audio_languages)[primaryLanguage];
|
fun getPrimaryLanguage(context: Context): String? {
|
||||||
|
return when(primaryLanguage) {
|
||||||
|
0 -> "en";
|
||||||
|
1 -> "es";
|
||||||
|
2 -> "de";
|
||||||
|
3 -> "fr";
|
||||||
|
4 -> "ja";
|
||||||
|
5 -> "ko";
|
||||||
|
6 -> "th";
|
||||||
|
7 -> "vi";
|
||||||
|
8 -> "id";
|
||||||
|
9 -> "hi";
|
||||||
|
10 -> "ar";
|
||||||
|
11 -> "tu";
|
||||||
|
12 -> "ru";
|
||||||
|
13 -> "pt";
|
||||||
|
14 -> "zh";
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//= 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, 1)
|
||||||
@DropdownFieldOptionsId(R.array.playback_speeds)
|
@DropdownFieldOptionsId(R.array.playback_speeds)
|
||||||
@@ -280,29 +369,29 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
else -> 1.0f;
|
else -> 1.0f;
|
||||||
};
|
};
|
||||||
|
|
||||||
@FormField(R.string.preferred_quality, FieldForm.DROPDOWN, -1, 2)
|
@FormField(R.string.preferred_quality, FieldForm.DROPDOWN, R.string.preferred_quality_description, 2)
|
||||||
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
||||||
var preferredQuality: Int = 0;
|
var preferredQuality: Int = 0;
|
||||||
|
|
||||||
@FormField(R.string.preferred_metered_quality, FieldForm.DROPDOWN, -1, 2)
|
@FormField(R.string.preferred_metered_quality, FieldForm.DROPDOWN, R.string.preferred_metered_quality_description, 3)
|
||||||
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
||||||
var preferredMeteredQuality: Int = 0;
|
var preferredMeteredQuality: Int = 0;
|
||||||
fun getPreferredQualityPixelCount(): Int = preferedQualityToPixels(preferredQuality);
|
fun getPreferredQualityPixelCount(): Int = preferedQualityToPixels(preferredQuality);
|
||||||
fun getPreferredMeteredQualityPixelCount(): Int = preferedQualityToPixels(preferredMeteredQuality);
|
fun getPreferredMeteredQualityPixelCount(): Int = preferedQualityToPixels(preferredMeteredQuality);
|
||||||
fun getCurrentPreferredQualityPixelCount(): Int = if(!StateApp.instance.isCurrentMetered()) getPreferredQualityPixelCount() else getPreferredMeteredQualityPixelCount();
|
fun getCurrentPreferredQualityPixelCount(): Int = if(!StateApp.instance.isCurrentMetered()) getPreferredQualityPixelCount() else getPreferredMeteredQualityPixelCount();
|
||||||
|
|
||||||
@FormField(R.string.preferred_preview_quality, FieldForm.DROPDOWN, -1, 3)
|
@FormField(R.string.preferred_preview_quality, FieldForm.DROPDOWN, R.string.preferred_preview_quality_description, 4)
|
||||||
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
||||||
var preferredPreviewQuality: Int = 5;
|
var preferredPreviewQuality: Int = 5;
|
||||||
fun getPreferredPreviewQualityPixelCount(): Int = preferedQualityToPixels(preferredPreviewQuality);
|
fun getPreferredPreviewQualityPixelCount(): Int = preferedQualityToPixels(preferredPreviewQuality);
|
||||||
|
|
||||||
@FormField(R.string.auto_rotate, FieldForm.DROPDOWN, -1, 4)
|
@FormField(R.string.auto_rotate, FieldForm.DROPDOWN, -1, 5)
|
||||||
@DropdownFieldOptionsId(R.array.system_enabled_disabled_array)
|
@DropdownFieldOptionsId(R.array.system_enabled_disabled_array)
|
||||||
var autoRotate: Int = 2;
|
var autoRotate: Int = 2;
|
||||||
|
|
||||||
fun isAutoRotate() = autoRotate == 1 || (autoRotate == 2 && StateApp.instance.getCurrentSystemAutoRotate());
|
fun isAutoRotate() = autoRotate == 1 || (autoRotate == 2 && StateApp.instance.getCurrentSystemAutoRotate());
|
||||||
|
|
||||||
@FormField(R.string.auto_rotate_dead_zone, FieldForm.DROPDOWN, R.string.this_prevents_the_device_from_rotating_within_the_given_amount_of_degrees, 5)
|
@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)
|
@DropdownFieldOptionsId(R.array.auto_rotate_dead_zone)
|
||||||
var autoRotateDeadZone: Int = 0;
|
var autoRotateDeadZone: Int = 0;
|
||||||
|
|
||||||
@@ -310,7 +399,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
return autoRotateDeadZone * 5;
|
return autoRotateDeadZone * 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.background_behavior, FieldForm.DROPDOWN, -1, 6)
|
@FormField(R.string.background_behavior, FieldForm.DROPDOWN, -1, 7)
|
||||||
@DropdownFieldOptionsId(R.array.player_background_behavior)
|
@DropdownFieldOptionsId(R.array.player_background_behavior)
|
||||||
var backgroundPlay: Int = 2;
|
var backgroundPlay: Int = 2;
|
||||||
|
|
||||||
@@ -329,16 +418,53 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.live_chat_webview, FieldForm.TOGGLE, R.string.use_the_live_chat_web_window_when_available_over_native_implementation, 8)
|
@FormField(R.string.chapter_update_fps_title, FieldForm.DROPDOWN, R.string.chapter_update_fps_description, 8)
|
||||||
|
@DropdownFieldOptionsId(R.array.chapter_fps)
|
||||||
|
var chapterUpdateFPS: Int = 0;
|
||||||
|
|
||||||
|
fun getChapterUpdateFrames(): Int {
|
||||||
|
return when(chapterUpdateFPS) {
|
||||||
|
0 -> 24
|
||||||
|
1 -> 30
|
||||||
|
2 -> 60
|
||||||
|
3 -> 120
|
||||||
|
else -> 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@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;
|
var useLiveChatWindow: Boolean = true;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@FormField(R.string.background_switch_audio, FieldForm.TOGGLE, R.string.background_switch_audio_description, 8)
|
@FormField(R.string.background_switch_audio, FieldForm.TOGGLE, R.string.background_switch_audio_description, 10)
|
||||||
var backgroundSwitchToAudio: Boolean = true;
|
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;
|
||||||
|
|
||||||
|
@FormField(R.string.restart_after_connectivity_loss, FieldForm.DROPDOWN, R.string.restart_playback_when_gaining_connectivity_after_a_loss, 12)
|
||||||
|
@DropdownFieldOptionsId(R.array.restart_playback_after_loss)
|
||||||
|
var restartPlaybackAfterConnectivityLoss: Int = 1;
|
||||||
|
|
||||||
|
@FormField(R.string.full_screen_portrait, FieldForm.TOGGLE, R.string.allow_full_screen_portrait, 13)
|
||||||
|
var fullscreenPortrait: Boolean = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.downloads, "group", R.string.configure_downloading_of_videos, 5)
|
@FormField(R.string.comments, "group", R.string.comments_description, 6)
|
||||||
|
var comments = CommentSettings();
|
||||||
|
@Serializable
|
||||||
|
class CommentSettings {
|
||||||
|
@FormField(R.string.default_comment_section, FieldForm.DROPDOWN, -1, 0)
|
||||||
|
@DropdownFieldOptionsId(R.array.comment_sections)
|
||||||
|
var defaultCommentSection: Int = 0;
|
||||||
|
|
||||||
|
@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)
|
||||||
var downloads = Downloads();
|
var downloads = Downloads();
|
||||||
@Serializable
|
@Serializable
|
||||||
class Downloads {
|
class Downloads {
|
||||||
@@ -378,7 +504,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.browsing, "group", R.string.configure_browsing_behavior, 6)
|
@FormField(R.string.browsing, "group", R.string.configure_browsing_behavior, 8)
|
||||||
var browsing = Browsing();
|
var browsing = Browsing();
|
||||||
@Serializable
|
@Serializable
|
||||||
class Browsing {
|
class Browsing {
|
||||||
@@ -387,7 +513,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
var videoCache: Boolean = true;
|
var videoCache: Boolean = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.casting, "group", R.string.configure_casting, 7)
|
@FormField(R.string.casting, "group", R.string.configure_casting, 9)
|
||||||
var casting = Casting();
|
var casting = Casting();
|
||||||
@Serializable
|
@Serializable
|
||||||
class Casting {
|
class Casting {
|
||||||
@@ -395,6 +521,9 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||||
var enabled: Boolean = true;
|
var enabled: Boolean = true;
|
||||||
|
|
||||||
|
@FormField(R.string.keep_screen_on, FieldForm.TOGGLE, R.string.keep_screen_on_while_casting, 1)
|
||||||
|
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||||
|
var keepScreenOn: Boolean = true;
|
||||||
|
|
||||||
/*TODO: Should we have a different casting quality?
|
/*TODO: Should we have a different casting quality?
|
||||||
@FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3)
|
@FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3)
|
||||||
@@ -412,8 +541,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
}*/
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@FormField(R.string.logging, FieldForm.GROUP, -1, 10)
|
||||||
@FormField(R.string.logging, FieldForm.GROUP, -1, 8)
|
|
||||||
var logging = Logging();
|
var logging = Logging();
|
||||||
@Serializable
|
@Serializable
|
||||||
class Logging {
|
class Logging {
|
||||||
@@ -421,10 +549,9 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@DropdownFieldOptionsId(R.array.log_levels)
|
@DropdownFieldOptionsId(R.array.log_levels)
|
||||||
var logLevel: Int = 0;
|
var logLevel: Int = 0;
|
||||||
|
|
||||||
@FormField(
|
fun isVerbose() = logLevel >= 4;
|
||||||
R.string.submit_logs, FieldForm.BUTTON,
|
|
||||||
R.string.submit_logs_to_help_us_narrow_down_issues, 1
|
@FormField(R.string.submit_logs, FieldForm.BUTTON, R.string.submit_logs_to_help_us_narrow_down_issues, 1)
|
||||||
)
|
|
||||||
fun submitLogs() {
|
fun submitLogs() {
|
||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
@@ -440,23 +567,26 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@FormField(R.string.announcement, FieldForm.GROUP, -1, 11)
|
||||||
|
|
||||||
@FormField(R.string.announcement, FieldForm.GROUP, -1, 10)
|
|
||||||
var announcementSettings = AnnouncementSettings();
|
var announcementSettings = AnnouncementSettings();
|
||||||
@Serializable
|
@Serializable
|
||||||
class AnnouncementSettings {
|
class AnnouncementSettings {
|
||||||
@FormField(
|
@FormField(R.string.reset_announcements, FieldForm.BUTTON, R.string.reset_hidden_announcements, 1)
|
||||||
R.string.reset_announcements, FieldForm.BUTTON,
|
|
||||||
R.string.reset_hidden_announcements, 1
|
|
||||||
)
|
|
||||||
fun resetAnnouncements() {
|
fun resetAnnouncements() {
|
||||||
StateAnnouncement.instance.resetAnnouncements();
|
StateAnnouncement.instance.resetAnnouncements();
|
||||||
SettingsActivity.getActivity()?.let { UIDialogs.toast(it, it.getString(R.string.announcements_reset)); };
|
SettingsActivity.getActivity()?.let { UIDialogs.toast(it, it.getString(R.string.announcements_reset)); };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.plugins, FieldForm.GROUP, -1, 11)
|
@FormField(R.string.notifications, FieldForm.GROUP, -1, 12)
|
||||||
|
var notifications = NotificationSettings();
|
||||||
|
@Serializable
|
||||||
|
class NotificationSettings {
|
||||||
|
@FormField(R.string.planned_content_notifications, FieldForm.TOGGLE, R.string.planned_content_notifications_description, 1)
|
||||||
|
var plannedContentNotification: Boolean = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@FormField(R.string.plugins, FieldForm.GROUP, -1, 13)
|
||||||
@Transient
|
@Transient
|
||||||
var plugins = Plugins();
|
var plugins = Plugins();
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -465,18 +595,12 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@FormField(R.string.clear_cookies_on_logout, FieldForm.TOGGLE, R.string.clears_cookies_when_you_log_out, 0)
|
@FormField(R.string.clear_cookies_on_logout, FieldForm.TOGGLE, R.string.clears_cookies_when_you_log_out, 0)
|
||||||
var clearCookiesOnLogout: Boolean = true;
|
var clearCookiesOnLogout: Boolean = true;
|
||||||
|
|
||||||
@FormField(
|
@FormField(R.string.clear_cookies, FieldForm.BUTTON, R.string.clears_in_app_browser_cookies, 1)
|
||||||
R.string.clear_cookies, FieldForm.BUTTON,
|
|
||||||
R.string.clears_in_app_browser_cookies, 1
|
|
||||||
)
|
|
||||||
fun clearCookies() {
|
fun clearCookies() {
|
||||||
val cookieManager: CookieManager = CookieManager.getInstance();
|
val cookieManager: CookieManager = CookieManager.getInstance();
|
||||||
cookieManager.removeAllCookies(null);
|
cookieManager.removeAllCookies(null);
|
||||||
}
|
}
|
||||||
@FormField(
|
/*@FormField(R.string.reinstall_embedded_plugins, FieldForm.BUTTON, R.string.also_removes_any_data_related_plugin_like_login_or_settings, 1)
|
||||||
R.string.reinstall_embedded_plugins, FieldForm.BUTTON,
|
|
||||||
R.string.also_removes_any_data_related_plugin_like_login_or_settings, 1
|
|
||||||
)
|
|
||||||
fun reinstallEmbedded() {
|
fun reinstallEmbedded() {
|
||||||
StateApp.instance.scopeOrNull!!.launch(Dispatchers.IO) {
|
StateApp.instance.scopeOrNull!!.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
@@ -495,11 +619,11 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@FormField(R.string.external_storage, FieldForm.GROUP, -1, 12)
|
@FormField(R.string.external_storage, FieldForm.GROUP, -1, 14)
|
||||||
var storage = Storage();
|
var storage = Storage();
|
||||||
@Serializable
|
@Serializable
|
||||||
class Storage {
|
class Storage {
|
||||||
@@ -523,10 +647,17 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
StateApp.instance.changeExternalDownloadDirectory(it);
|
StateApp.instance.changeExternalDownloadDirectory(it);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@FormField(R.string.clear_external_downloads_directory, FieldForm.BUTTON, R.string.clear_the_external_storage_for_download_files, 5)
|
||||||
|
fun clearStorageDownload() {
|
||||||
|
Settings.instance.storage.storage_download = null;
|
||||||
|
Settings.instance.save();
|
||||||
|
SettingsActivity.getActivity()?.let { UIDialogs.toast(it, "Cleared download storage directory") };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@FormField(R.string.auto_update, "group", R.string.configure_the_auto_updater, 12)
|
@FormField(R.string.auto_update, "group", R.string.configure_the_auto_updater, 15)
|
||||||
var autoUpdate = AutoUpdate();
|
var autoUpdate = AutoUpdate();
|
||||||
@Serializable
|
@Serializable
|
||||||
class AutoUpdate {
|
class AutoUpdate {
|
||||||
@@ -555,14 +686,13 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
return check == 0 && !BuildConfig.IS_PLAYSTORE_BUILD;
|
return check == 0 && !BuildConfig.IS_PLAYSTORE_BUILD;
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(
|
@FormField(R.string.manual_check, FieldForm.BUTTON, R.string.manually_check_for_updates, 3)
|
||||||
R.string.manual_check, FieldForm.BUTTON,
|
|
||||||
R.string.manually_check_for_updates, 3
|
|
||||||
)
|
|
||||||
fun manualCheck() {
|
fun manualCheck() {
|
||||||
if (!BuildConfig.IS_PLAYSTORE_BUILD) {
|
if (!BuildConfig.IS_PLAYSTORE_BUILD) {
|
||||||
SettingsActivity.getActivity()?.let {
|
SettingsActivity.getActivity()?.let {
|
||||||
StateUpdate.instance.checkForUpdates(it, true);
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
|
StateUpdate.instance.checkForUpdates(it, true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
SettingsActivity.getActivity()?.let {
|
SettingsActivity.getActivity()?.let {
|
||||||
@@ -575,10 +705,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(
|
@FormField(R.string.view_changelog, FieldForm.BUTTON, R.string.review_the_current_and_past_changelogs, 4)
|
||||||
R.string.view_changelog, FieldForm.BUTTON,
|
|
||||||
R.string.review_the_current_and_past_changelogs, 4
|
|
||||||
)
|
|
||||||
fun viewChangelog() {
|
fun viewChangelog() {
|
||||||
SettingsActivity.getActivity()?.let {
|
SettingsActivity.getActivity()?.let {
|
||||||
UIDialogs.toast(it.getString(R.string.retrieving_changelog));
|
UIDialogs.toast(it.getString(R.string.retrieving_changelog));
|
||||||
@@ -598,10 +725,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(
|
@FormField(R.string.remove_cached_version, FieldForm.BUTTON, R.string.remove_the_last_downloaded_version, 5)
|
||||||
R.string.remove_cached_version, FieldForm.BUTTON,
|
|
||||||
R.string.remove_the_last_downloaded_version, 5
|
|
||||||
)
|
|
||||||
fun removeCachedVersion() {
|
fun removeCachedVersion() {
|
||||||
StateApp.withContext {
|
StateApp.withContext {
|
||||||
val outputDirectory = File(it.filesDir, "autoupdate");
|
val outputDirectory = File(it.filesDir, "autoupdate");
|
||||||
@@ -617,7 +741,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.backup, FieldForm.GROUP, -1, 13)
|
@FormField(R.string.backup, FieldForm.GROUP, -1, 16)
|
||||||
var backup = Backup();
|
var backup = Backup();
|
||||||
@Serializable
|
@Serializable
|
||||||
class Backup {
|
class Backup {
|
||||||
@@ -649,28 +773,19 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
@FormField(R.string.export_data, FieldForm.BUTTON, R.string.creates_a_zip_file_with_your_data_which_can_be_imported_by_opening_it_with_grayjay, 3)
|
@FormField(R.string.export_data, FieldForm.BUTTON, R.string.creates_a_zip_file_with_your_data_which_can_be_imported_by_opening_it_with_grayjay, 3)
|
||||||
fun export() {
|
fun export() {
|
||||||
StateBackup.startExternalBackup();
|
val activity = SettingsActivity.getActivity() ?: return;
|
||||||
|
UISlideOverlays.showOverlay(activity.overlay, "Select export type", null, {},
|
||||||
|
SlideUpMenuItem(activity, R.drawable.ic_share, "Share", "", null, {
|
||||||
|
StateBackup.shareExternalBackup();
|
||||||
|
}),
|
||||||
|
SlideUpMenuItem(activity, R.drawable.ic_download, "File", "", null, {
|
||||||
|
StateBackup.saveExternalBackup(activity);
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
@FormField(R.string.import_data, FieldForm.BUTTON, R.string.import_data_description, 4)
|
|
||||||
fun import() {
|
|
||||||
val act = SettingsActivity.getActivity() ?: return;
|
|
||||||
StateApp.instance.requestFileReadAccess(act, null) {
|
|
||||||
if(it != null && it.exists()) {
|
|
||||||
val name = it.name;
|
|
||||||
val contents = it.readBytes(act);
|
|
||||||
if(contents != null) {
|
|
||||||
if(name != null && name.endsWith(".zip", true))
|
|
||||||
StateBackup.importZipBytes(act, act.lifecycleScope, contents);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.payment, FieldForm.GROUP, -1, 14)
|
@FormField(R.string.payment, FieldForm.GROUP, -1, 17)
|
||||||
var payment = Payment();
|
var payment = Payment();
|
||||||
@Serializable
|
@Serializable
|
||||||
class Payment {
|
class Payment {
|
||||||
@@ -687,7 +802,48 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.info, FieldForm.GROUP, -1, 15)
|
@FormField(R.string.other, FieldForm.GROUP, -1, 18)
|
||||||
|
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;
|
||||||
|
|
||||||
|
@FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 1)
|
||||||
|
var polycentricEnabled: Boolean = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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.info, FieldForm.GROUP, -1, 20)
|
||||||
var info = Info();
|
var info = Info();
|
||||||
@Serializable
|
@Serializable
|
||||||
class Info {
|
class Info {
|
||||||
|
|||||||
@@ -2,19 +2,16 @@ package com.futo.platformplayer
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.webkit.CookieManager
|
import android.webkit.CookieManager
|
||||||
import androidx.work.Constraints
|
|
||||||
import androidx.work.Data
|
import androidx.work.Data
|
||||||
import androidx.work.ExistingPeriodicWorkPolicy
|
|
||||||
import androidx.work.NetworkType
|
|
||||||
import androidx.work.OneTimeWorkRequestBuilder
|
import androidx.work.OneTimeWorkRequestBuilder
|
||||||
import androidx.work.PeriodicWorkRequest
|
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import androidx.work.WorkerParameters
|
|
||||||
import com.caoccao.javet.values.primitive.V8ValueInteger
|
import com.caoccao.javet.values.primitive.V8ValueInteger
|
||||||
import com.caoccao.javet.values.primitive.V8ValueString
|
import com.caoccao.javet.values.primitive.V8ValueString
|
||||||
|
import com.futo.platformplayer.activities.DeveloperActivity
|
||||||
import com.futo.platformplayer.activities.SettingsActivity
|
import com.futo.platformplayer.activities.SettingsActivity
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginDescriptor
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginDescriptor
|
||||||
@@ -25,21 +22,28 @@ import com.futo.platformplayer.logging.Logger
|
|||||||
import com.futo.platformplayer.serializers.FlexibleBooleanSerializer
|
import com.futo.platformplayer.serializers.FlexibleBooleanSerializer
|
||||||
import com.futo.platformplayer.states.StateAnnouncement
|
import com.futo.platformplayer.states.StateAnnouncement
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.futo.platformplayer.states.StateCache
|
||||||
import com.futo.platformplayer.states.StateDeveloper
|
import com.futo.platformplayer.states.StateDeveloper
|
||||||
import com.futo.platformplayer.states.StateDownloads
|
import com.futo.platformplayer.states.StateDownloads
|
||||||
|
import com.futo.platformplayer.states.StateHistory
|
||||||
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.platformplayer.states.StateSubscriptions
|
import com.futo.platformplayer.states.StateSubscriptions
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
import com.futo.platformplayer.stores.FragmentedStorageFileJson
|
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.FieldForm
|
||||||
import com.futo.platformplayer.views.fields.FormField
|
import com.futo.platformplayer.views.fields.FormField
|
||||||
|
import com.futo.platformplayer.views.fields.FormFieldWarning
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.*
|
import kotlinx.serialization.Contextual
|
||||||
import kotlinx.serialization.json.*
|
import kotlinx.serialization.Serializable
|
||||||
import java.util.UUID
|
import kotlinx.serialization.Transient
|
||||||
import java.util.concurrent.TimeUnit
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import java.time.OffsetDateTime
|
||||||
import java.util.stream.IntStream.range
|
import java.util.stream.IntStream.range
|
||||||
import kotlin.system.measureTimeMillis
|
import kotlin.system.measureTimeMillis
|
||||||
|
|
||||||
@@ -81,26 +85,153 @@ class SettingsDev : FragmentedStorageFileJson() {
|
|||||||
var backgroundSubscriptionFetching: Boolean = false;
|
var backgroundSubscriptionFetching: Boolean = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@FormField(R.string.cache, FieldForm.GROUP, -1, 3)
|
||||||
|
val cache: Cache = Cache();
|
||||||
|
@Serializable
|
||||||
|
class Cache {
|
||||||
|
|
||||||
|
@FormField(R.string.subscriptions_cache_5000, FieldForm.BUTTON, -1, 1, "subscription_cache_button")
|
||||||
|
fun subscriptionsCache5000() {
|
||||||
|
Logger.i("SettingsDev", "Started caching 5000 sub items");
|
||||||
|
UIDialogs.toast(
|
||||||
|
SettingsActivity.getActivity()!!,
|
||||||
|
"Started caching 5000 sub items"
|
||||||
|
);
|
||||||
|
val button = DeveloperActivity.getActivity()?.getField("subscription_cache_button");
|
||||||
|
if(button is ButtonField)
|
||||||
|
button.setButtonEnabled(false);
|
||||||
|
StateApp.instance.scope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val subsCache =
|
||||||
|
StateSubscriptions.instance.getSubscriptionsFeedWithExceptions(cacheScope = this).first;
|
||||||
|
|
||||||
|
var total = 0;
|
||||||
|
var page = 0;
|
||||||
|
var lastToast = System.currentTimeMillis();
|
||||||
|
while(subsCache.hasMorePages() && total < 5000) {
|
||||||
|
subsCache.nextPage();
|
||||||
|
total += subsCache.getResults().size;
|
||||||
|
page++;
|
||||||
|
|
||||||
|
if(page % 10 == 0)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
val diff = System.currentTimeMillis() - lastToast;
|
||||||
|
lastToast = System.currentTimeMillis();
|
||||||
|
UIDialogs.toast(
|
||||||
|
SettingsActivity.getActivity()!!,
|
||||||
|
"Page: ${page}, Total: ${total}, Speed: ${diff}ms"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Thread.sleep(250);
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
UIDialogs.toast(
|
||||||
|
SettingsActivity.getActivity()!!,
|
||||||
|
"FINISHED Page: ${page}, Total: ${total}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e("SettingsDev", ex.message, ex);
|
||||||
|
Logger.i("SettingsDev", "Failed: ${ex.message}");
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
if(button is ButtonField)
|
||||||
|
button.setButtonEnabled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@FormField(R.string.history_cache_100, FieldForm.BUTTON, -1, 1, "history_cache_button")
|
||||||
|
fun historyCache100() {
|
||||||
|
Logger.i("SettingsDev", "Started caching 100 history items (from home)");
|
||||||
|
UIDialogs.toast(
|
||||||
|
SettingsActivity.getActivity()!!,
|
||||||
|
"Started caching 100 history items (from home)"
|
||||||
|
);
|
||||||
|
val button = DeveloperActivity.getActivity()?.getField("history_cache_button");
|
||||||
|
if(button is ButtonField)
|
||||||
|
button.setButtonEnabled(false);
|
||||||
|
StateApp.instance.scope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val subsCache = StatePlatform.instance.getHome();
|
||||||
|
|
||||||
|
var num = 0;
|
||||||
|
for(item in subsCache.getResults().filterIsInstance<IPlatformVideo>()) {
|
||||||
|
StateHistory.instance.getHistoryByVideo(item, true, OffsetDateTime.now().minusHours(num.toLong() * 4))
|
||||||
|
num++;
|
||||||
|
}
|
||||||
|
|
||||||
|
var total = 0;
|
||||||
|
var page = 0;
|
||||||
|
var lastToast = System.currentTimeMillis();
|
||||||
|
while(subsCache.hasMorePages() && total < 5000) {
|
||||||
|
subsCache.nextPage();
|
||||||
|
total += subsCache.getResults().size;
|
||||||
|
page++;
|
||||||
|
|
||||||
|
for(item in subsCache.getResults().filterIsInstance<IPlatformVideo>()) {
|
||||||
|
StateHistory.instance.getHistoryByVideo(item, true, OffsetDateTime.now().minusHours(num.toLong() * 4))
|
||||||
|
num++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(page % 4 == 0)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
val diff = System.currentTimeMillis() - lastToast;
|
||||||
|
lastToast = System.currentTimeMillis();
|
||||||
|
UIDialogs.toast(
|
||||||
|
SettingsActivity.getActivity()!!,
|
||||||
|
"Page: ${page}, Total: ${total}, Speed: ${diff}ms"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Thread.sleep(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
UIDialogs.toast(
|
||||||
|
SettingsActivity.getActivity()!!,
|
||||||
|
"FINISHED Page: ${page}, Total: ${total}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e("SettingsDev", ex.message, ex);
|
||||||
|
Logger.i("SettingsDev", "Failed: ${ex.message}");
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
if(button is ButtonField)
|
||||||
|
button.setButtonEnabled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@FormField(R.string.crash_me, FieldForm.BUTTON,
|
@FormField(R.string.crash_me, FieldForm.BUTTON,
|
||||||
R.string.crashes_the_application_on_purpose, 2)
|
R.string.crashes_the_application_on_purpose, 3)
|
||||||
fun crashMe() {
|
fun crashMe() {
|
||||||
throw java.lang.IllegalStateException("This is an uncaught exception triggered on purpose!");
|
throw java.lang.IllegalStateException("This is an uncaught exception triggered on purpose!");
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.delete_announcements, FieldForm.BUTTON,
|
@FormField(R.string.delete_announcements, FieldForm.BUTTON,
|
||||||
R.string.delete_all_announcements, 2)
|
R.string.delete_all_announcements, 3)
|
||||||
fun deleteAnnouncements() {
|
fun deleteAnnouncements() {
|
||||||
StateAnnouncement.instance.deleteAllAnnouncements();
|
StateAnnouncement.instance.deleteAllAnnouncements();
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.clear_cookies, FieldForm.BUTTON,
|
@FormField(R.string.clear_cookies, FieldForm.BUTTON,
|
||||||
R.string.clear_all_cookies_from_the_cookieManager, 2)
|
R.string.clear_all_cookies_from_the_cookieManager, 3)
|
||||||
fun clearCookies() {
|
fun clearCookies() {
|
||||||
val cookieManager: CookieManager = CookieManager.getInstance()
|
val cookieManager: CookieManager = CookieManager.getInstance()
|
||||||
cookieManager.removeAllCookies(null);
|
cookieManager.removeAllCookies(null);
|
||||||
}
|
}
|
||||||
@FormField(R.string.test_background_worker, FieldForm.BUTTON,
|
@FormField(R.string.test_background_worker, FieldForm.BUTTON,
|
||||||
R.string.test_background_worker_description, 3)
|
R.string.test_background_worker_description, 4)
|
||||||
fun triggerBackgroundUpdate() {
|
fun triggerBackgroundUpdate() {
|
||||||
val act = SettingsActivity.getActivity()!!;
|
val act = SettingsActivity.getActivity()!!;
|
||||||
UIDialogs.toast(SettingsActivity.getActivity()!!, "Starting test background worker");
|
UIDialogs.toast(SettingsActivity.getActivity()!!, "Starting test background worker");
|
||||||
@@ -111,6 +242,14 @@ class SettingsDev : FragmentedStorageFileJson() {
|
|||||||
.build();
|
.build();
|
||||||
wm.enqueue(req);
|
wm.enqueue(req);
|
||||||
}
|
}
|
||||||
|
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON,
|
||||||
|
R.string.test_background_worker_description, 4)
|
||||||
|
fun clearChannelContentCache() {
|
||||||
|
UIDialogs.toast(SettingsActivity.getActivity()!!, "Clearing cache");
|
||||||
|
StateCache.instance.clearToday();
|
||||||
|
UIDialogs.toast(SettingsActivity.getActivity()!!, "Cleared");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Contextual
|
@Contextual
|
||||||
@Transient
|
@Transient
|
||||||
@@ -231,9 +370,9 @@ class SettingsDev : FragmentedStorageFileJson() {
|
|||||||
@FormField(R.string.getHome, FieldForm.BUTTON, R.string.attempts_to_fetch_2_pages_from_getHome, 2)
|
@FormField(R.string.getHome, FieldForm.BUTTON, R.string.attempts_to_fetch_2_pages_from_getHome, 2)
|
||||||
fun testV8Home() {
|
fun testV8Home() {
|
||||||
runTestPlugin(_currentPlugin) {
|
runTestPlugin(_currentPlugin) {
|
||||||
var home: IPager<IPlatformContent>? = null;
|
var home: IPager<IPlatformContent>?;
|
||||||
var resultPage1: String = "";
|
val resultPage1: String;
|
||||||
var resultPage2: String = "";
|
val resultPage2: String;
|
||||||
val page1Time = measureTimeMillis {
|
val page1Time = measureTimeMillis {
|
||||||
home = it.getHome();
|
home = it.getHome();
|
||||||
val results = home!!.getResults();
|
val results = home!!.getResults();
|
||||||
@@ -354,6 +493,30 @@ class SettingsDev : FragmentedStorageFileJson() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Contextual
|
||||||
|
@Transient
|
||||||
|
@FormField(R.string.info, FieldForm.GROUP, -1, 19)
|
||||||
|
var info = Info();
|
||||||
|
@Serializable
|
||||||
|
class Info {
|
||||||
|
@FormField(R.string.dev_info_channel_cache_size, FieldForm.READONLYTEXT, -1, 1, "channelCacheSize")
|
||||||
|
var channelCacheStartupCount = StateCache.instance.channelCacheStartupCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//region BOILERPLATE
|
//region BOILERPLATE
|
||||||
override fun encode(): String {
|
override fun encode(): String {
|
||||||
return Json.encodeToString(this);
|
return Json.encodeToString(this);
|
||||||
|
|||||||
@@ -1,24 +1,50 @@
|
|||||||
package com.futo.platformplayer
|
package com.futo.platformplayer
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
|
import android.net.Uri
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
import android.widget.*
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.core.content.ContextCompat
|
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.models.comments.IPlatformComment
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.casting.StateCasting
|
import com.futo.platformplayer.casting.StateCasting
|
||||||
import com.futo.platformplayer.dialogs.*
|
import com.futo.platformplayer.dialogs.AutoUpdateDialog
|
||||||
|
import com.futo.platformplayer.dialogs.AutomaticBackupDialog
|
||||||
|
import com.futo.platformplayer.dialogs.AutomaticRestoreDialog
|
||||||
|
import com.futo.platformplayer.dialogs.CastingAddDialog
|
||||||
|
import com.futo.platformplayer.dialogs.CastingHelpDialog
|
||||||
|
import com.futo.platformplayer.dialogs.ChangelogDialog
|
||||||
|
import com.futo.platformplayer.dialogs.CommentDialog
|
||||||
|
import com.futo.platformplayer.dialogs.ConnectCastingDialog
|
||||||
|
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.PluginUpdateDialog
|
||||||
|
import com.futo.platformplayer.dialogs.ProgressDialog
|
||||||
import com.futo.platformplayer.engine.exceptions.PluginException
|
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.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateAnnouncement
|
import com.futo.platformplayer.models.ImportCache
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StateBackup
|
import com.futo.platformplayer.states.StateBackup
|
||||||
|
import com.futo.platformplayer.states.StatePlugins
|
||||||
import com.futo.platformplayer.stores.v2.ManagedStore
|
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||||
|
import com.futo.platformplayer.views.ToastView
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -91,6 +117,50 @@ class UIDialogs {
|
|||||||
}.toTypedArray());
|
}.toTypedArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun showUrlHandlingPrompt(context: Context, onYes: (() -> Unit)? = null) {
|
||||||
|
val builder = AlertDialog.Builder(context)
|
||||||
|
val view = LayoutInflater.from(context).inflate(R.layout.dialog_url_handling, null)
|
||||||
|
builder.setView(view)
|
||||||
|
|
||||||
|
val dialog = builder.create()
|
||||||
|
registerDialogOpened(dialog)
|
||||||
|
|
||||||
|
view.findViewById<TextView>(R.id.button_no).apply {
|
||||||
|
this.setOnClickListener {
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
view.findViewById<LinearLayout>(R.id.button_yes).apply {
|
||||||
|
this.setOnClickListener {
|
||||||
|
if (BuildConfig.IS_PLAYSTORE_BUILD) {
|
||||||
|
dialog.dismiss()
|
||||||
|
showDialogOk(context, R.drawable.ic_error_pred, context.getString(R.string.play_store_version_does_not_support_default_url_handling)) {
|
||||||
|
onYes?.invoke()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
val intent =
|
||||||
|
Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||||
|
val uri = Uri.fromParts("package", context.packageName, null)
|
||||||
|
intent.data = uri
|
||||||
|
context.startActivity(intent)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
toast(context, context.getString(R.string.failed_to_show_settings))
|
||||||
|
}
|
||||||
|
|
||||||
|
onYes?.invoke()
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.setOnDismissListener {
|
||||||
|
registerDialogClosed(dialog)
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.show()
|
||||||
|
}
|
||||||
|
|
||||||
fun showAutomaticBackupDialog(context: Context, skipRestoreCheck: Boolean = false, onClosed: (()->Unit)? = null) {
|
fun showAutomaticBackupDialog(context: Context, skipRestoreCheck: Boolean = false, onClosed: (()->Unit)? = null) {
|
||||||
val dialogAction: ()->Unit = {
|
val dialogAction: ()->Unit = {
|
||||||
@@ -107,7 +177,8 @@ class UIDialogs {
|
|||||||
}, UIDialogs.ActionStyle.DANGEROUS),
|
}, UIDialogs.ActionStyle.DANGEROUS),
|
||||||
UIDialogs.Action(context.getString(R.string.restore), {
|
UIDialogs.Action(context.getString(R.string.restore), {
|
||||||
UIDialogs.showAutomaticRestoreDialog(context, StateApp.instance.scope);
|
UIDialogs.showAutomaticRestoreDialog(context, StateApp.instance.scope);
|
||||||
}, UIDialogs.ActionStyle.PRIMARY));
|
}, UIDialogs.ActionStyle.PRIMARY)
|
||||||
|
);
|
||||||
else {
|
else {
|
||||||
dialogAction();
|
dialogAction();
|
||||||
}
|
}
|
||||||
@@ -119,6 +190,14 @@ class UIDialogs {
|
|||||||
dialog.show();
|
dialog.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
fun showDialog(context: Context, icon: Int, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action) {
|
||||||
val builder = AlertDialog.Builder(context);
|
val builder = AlertDialog.Builder(context);
|
||||||
val view = LayoutInflater.from(context).inflate(R.layout.dialog_multi_button, null);
|
val view = LayoutInflater.from(context).inflate(R.layout.dialog_multi_button, null);
|
||||||
@@ -142,8 +221,10 @@ class UIDialogs {
|
|||||||
view.findViewById<TextView>(R.id.dialog_text_code).apply {
|
view.findViewById<TextView>(R.id.dialog_text_code).apply {
|
||||||
if(code == null)
|
if(code == null)
|
||||||
this.visibility = View.GONE;
|
this.visibility = View.GONE;
|
||||||
else
|
else {
|
||||||
this.text = code;
|
this.text = code;
|
||||||
|
this.visibility = View.VISIBLE;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
view.findViewById<LinearLayout>(R.id.dialog_buttons).apply {
|
view.findViewById<LinearLayout>(R.id.dialog_buttons).apply {
|
||||||
val buttons = actions.map<Action, TextView> { act ->
|
val buttons = actions.map<Action, TextView> { act ->
|
||||||
@@ -202,22 +283,48 @@ class UIDialogs {
|
|||||||
}, UIDialogs.ActionStyle.PRIMARY)
|
}, 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)
|
val pluginInfo = if(ex is PluginException)
|
||||||
"\nPlugin [${ex.config.name}]" else "";
|
"\nPlugin [${ex.config.name}]" else "";
|
||||||
showDialog(context,
|
|
||||||
R.drawable.ic_error_pred,
|
var exMsg = if(ex != null ) "${ex.message}" else "";
|
||||||
"${msg}${pluginInfo}",
|
if(pluginConfig != null && pluginConfig is SourcePluginConfig && StatePlugins.instance.hasUpdateAvailable(pluginConfig))
|
||||||
(if(ex != null ) "${ex.message}" else ""),
|
exMsg += "\n\nAn update is available"
|
||||||
if(ex is PluginException) ex.code else null,
|
|
||||||
0,
|
if(mainFragment != null && pluginConfig != null && pluginConfig is SourcePluginConfig && StatePlugins.instance.hasUpdateAvailable(pluginConfig))
|
||||||
UIDialogs.Action(context.getString(R.string.retry), {
|
showDialog(context,
|
||||||
retryAction?.invoke();
|
R.drawable.ic_error_pred,
|
||||||
}, UIDialogs.ActionStyle.PRIMARY),
|
"${msg}${pluginInfo}",
|
||||||
UIDialogs.Action(context.getString(R.string.close), {
|
exMsg,
|
||||||
closeAction?.invoke()
|
if(ex is PluginException) ex.code else null,
|
||||||
}, UIDialogs.ActionStyle.NONE)
|
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), {
|
||||||
|
retryAction?.invoke();
|
||||||
|
}, 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), {
|
||||||
|
retryAction?.invoke();
|
||||||
|
}, UIDialogs.ActionStyle.PRIMARY)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showSingleButtonDialog(context: Context, icon: Int, text: String, buttonText: String, action: (() -> Unit)) {
|
fun showSingleButtonDialog(context: Context, icon: Int, text: String, buttonText: String, action: (() -> Unit)) {
|
||||||
@@ -238,12 +345,16 @@ class UIDialogs {
|
|||||||
showDialog(context, R.drawable.ic_error, text, null, null, 0, cancelButtonAction, confirmButtonAction)
|
showDialog(context, R.drawable.ic_error, text, null, null, 0, cancelButtonAction, confirmButtonAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showUpdateAvailableDialog(context: Context, lastVersion: Int) {
|
fun showUpdateAvailableDialog(context: Context, lastVersion: Int, hideExceptionButtons: Boolean = false) {
|
||||||
val dialog = AutoUpdateDialog(context);
|
val dialog = AutoUpdateDialog(context);
|
||||||
registerDialogOpened(dialog);
|
registerDialogOpened(dialog);
|
||||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||||
dialog.show();
|
dialog.show();
|
||||||
dialog.setMaxVersion(lastVersion);
|
dialog.setMaxVersion(lastVersion);
|
||||||
|
|
||||||
|
if (hideExceptionButtons) {
|
||||||
|
dialog.hideExceptionButtons()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showChangelogDialog(context: Context, lastVersion: Int) {
|
fun showChangelogDialog(context: Context, lastVersion: Int) {
|
||||||
@@ -273,8 +384,14 @@ class UIDialogs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showImportDialog(context: Context, store: ManagedStore<*>, name: String, reconstructions: List<String>, onConcluded: () -> Unit) {
|
fun showImportDialog(context: Context, store: ManagedStore<*>, name: String, reconstructions: List<String>, cache: ImportCache?, onConcluded: () -> Unit) {
|
||||||
val dialog = ImportDialog(context, store, name, reconstructions, onConcluded);
|
val dialog = ImportDialog(context, store, name, reconstructions, cache, onConcluded);
|
||||||
|
registerDialogOpened(dialog);
|
||||||
|
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||||
|
dialog.show();
|
||||||
|
}
|
||||||
|
fun showImportOptionsDialog(context: MainActivity) {
|
||||||
|
val dialog = ImportOptionsDialog(context);
|
||||||
registerDialogOpened(dialog);
|
registerDialogOpened(dialog);
|
||||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||||
dialog.show();
|
dialog.show();
|
||||||
@@ -285,17 +402,34 @@ class UIDialogs {
|
|||||||
val d = StateCasting.instance.activeDevice;
|
val d = StateCasting.instance.activeDevice;
|
||||||
if (d != null) {
|
if (d != null) {
|
||||||
val dialog = ConnectedCastingDialog(context);
|
val dialog = ConnectedCastingDialog(context);
|
||||||
|
if (context is Activity) {
|
||||||
|
dialog.setOwnerActivity(context)
|
||||||
|
}
|
||||||
registerDialogOpened(dialog);
|
registerDialogOpened(dialog);
|
||||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||||
dialog.show();
|
dialog.show();
|
||||||
} else {
|
} else {
|
||||||
val dialog = ConnectCastingDialog(context);
|
val dialog = ConnectCastingDialog(context);
|
||||||
|
if (context is Activity) {
|
||||||
|
dialog.setOwnerActivity(context)
|
||||||
|
}
|
||||||
registerDialogOpened(dialog);
|
registerDialogOpened(dialog);
|
||||||
|
val c = context
|
||||||
|
if (c is Activity) {
|
||||||
|
dialog.setOwnerActivity(c);
|
||||||
|
}
|
||||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||||
dialog.show();
|
dialog.show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun showCastingTutorialDialog(context: Context) {
|
||||||
|
val dialog = CastingHelpDialog(context);
|
||||||
|
registerDialogOpened(dialog);
|
||||||
|
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||||
|
dialog.show();
|
||||||
|
}
|
||||||
|
|
||||||
fun showCastingAddDialog(context: Context) {
|
fun showCastingAddDialog(context: Context) {
|
||||||
val dialog = CastingAddDialog(context);
|
val dialog = CastingAddDialog(context);
|
||||||
registerDialogOpened(dialog);
|
registerDialogOpened(dialog);
|
||||||
@@ -310,13 +444,28 @@ class UIDialogs {
|
|||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
||||||
try {
|
try {
|
||||||
StateApp.withContext {
|
StateApp.withContext {
|
||||||
Toast.makeText(it, text, if (long) Toast.LENGTH_LONG else Toast.LENGTH_SHORT).show();
|
toast(it, text, long);
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to show toast.", e);
|
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) {
|
fun showClickableToast(context: Context, text: String, onClick: () -> Unit, isLongDuration: Boolean = false) {
|
||||||
//TODO: Is not actually clickable...
|
//TODO: Is not actually clickable...
|
||||||
|
|||||||
@@ -1,46 +1,69 @@
|
|||||||
package com.futo.platformplayer
|
package com.futo.platformplayer
|
||||||
|
|
||||||
|
import android.app.NotificationManager
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
import android.graphics.Color
|
import android.content.Context
|
||||||
import android.util.TypedValue
|
import android.content.Intent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.LinearLayout
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import android.widget.TextView
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
|
import com.futo.platformplayer.activities.SettingsActivity
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
|
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
||||||
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
|
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantAudioUrlSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantVideoUrlSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudioSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.SubtitleRawSource
|
|
||||||
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||||
import com.futo.platformplayer.downloads.VideoLocal
|
import com.futo.platformplayer.downloads.VideoLocal
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionGroupFragment
|
||||||
import com.futo.platformplayer.helpers.VideoHelper
|
import com.futo.platformplayer.helpers.VideoHelper
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.models.ImageVariable
|
||||||
import com.futo.platformplayer.models.Playlist
|
import com.futo.platformplayer.models.Playlist
|
||||||
import com.futo.platformplayer.models.Subscription
|
import com.futo.platformplayer.models.Subscription
|
||||||
import com.futo.platformplayer.states.*
|
import com.futo.platformplayer.models.SubscriptionGroup
|
||||||
import com.futo.platformplayer.views.Loader
|
import com.futo.platformplayer.parsers.HLS
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.futo.platformplayer.states.StateDownloads
|
||||||
|
import com.futo.platformplayer.states.StateMeta
|
||||||
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
|
import com.futo.platformplayer.states.StatePlayer
|
||||||
|
import com.futo.platformplayer.states.StatePlaylists
|
||||||
|
import com.futo.platformplayer.states.StateSubscriptionGroups
|
||||||
|
import com.futo.platformplayer.states.StateSubscriptions
|
||||||
|
import com.futo.platformplayer.views.AnyAdapterView
|
||||||
|
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
|
||||||
|
import com.futo.platformplayer.views.LoaderView
|
||||||
|
import com.futo.platformplayer.views.adapters.viewholders.SubscriptionGroupBarViewHolder
|
||||||
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuFilters
|
||||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuGroup
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuGroup
|
||||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
|
||||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
||||||
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuRecycler
|
||||||
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTextInput
|
||||||
import com.futo.platformplayer.views.pills.RoundButton
|
import com.futo.platformplayer.views.pills.RoundButton
|
||||||
import com.futo.platformplayer.views.pills.RoundButtonGroup
|
import com.futo.platformplayer.views.pills.RoundButtonGroup
|
||||||
import com.futo.platformplayer.views.overlays.slideup.*
|
|
||||||
import com.futo.platformplayer.views.video.FutoVideoPlayerBase
|
import com.futo.platformplayer.views.video.FutoVideoPlayerBase
|
||||||
|
import isDownloadable
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.lang.IllegalStateException
|
|
||||||
|
|
||||||
class UISlideOverlays {
|
class UISlideOverlays {
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "UISlideOverlays";
|
private const val TAG = "UISlideOverlays";
|
||||||
|
|
||||||
fun showOverlay(container: ViewGroup, title: String, okButton: String?, onOk: ()->Unit, vararg views: View) {
|
fun showOverlay(container: ViewGroup, title: String, okButton: String?, onOk: ()->Unit, vararg views: View): SlideUpMenuOverlay {
|
||||||
var menu = SlideUpMenuOverlay(container.context, container, title, okButton, true, *views);
|
var menu = SlideUpMenuOverlay(container.context, container, title, okButton, true, *views);
|
||||||
|
|
||||||
menu.onOK.subscribe {
|
menu.onOK.subscribe {
|
||||||
@@ -48,11 +71,11 @@ class UISlideOverlays {
|
|||||||
onOk.invoke();
|
onOk.invoke();
|
||||||
};
|
};
|
||||||
menu.show();
|
menu.show();
|
||||||
|
return menu;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup) {
|
fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup): SlideUpMenuOverlay {
|
||||||
val items = arrayListOf<View>();
|
val items = arrayListOf<View>();
|
||||||
var menu: SlideUpMenuOverlay? = null;
|
|
||||||
|
|
||||||
val originalNotif = subscription.doNotifications;
|
val originalNotif = subscription.doNotifications;
|
||||||
val originalLive = subscription.doFetchLive;
|
val originalLive = subscription.doFetchLive;
|
||||||
@@ -60,54 +83,232 @@ class UISlideOverlays {
|
|||||||
val originalVideo = subscription.doFetchVideos;
|
val originalVideo = subscription.doFetchVideos;
|
||||||
val originalPosts = subscription.doFetchPosts;
|
val originalPosts = subscription.doFetchPosts;
|
||||||
|
|
||||||
items.addAll(listOf(
|
val menu = SlideUpMenuOverlay(container.context, container, "Subscription Settings", null, true, listOf());
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_notifications, "Notifications", "", "notifications", {
|
|
||||||
subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications;
|
|
||||||
}, false),
|
|
||||||
SlideUpMenuGroup(container.context, "Fetch Settings",
|
|
||||||
"Depending on the platform you might not need to enable a type for it to be available.",
|
|
||||||
-1, listOf()),
|
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_live_tv, "Livestreams", "Check for livestreams", "fetchLive", {
|
|
||||||
subscription.doFetchLive = menu?.selectOption(null, "fetchLive", true, true) ?: subscription.doFetchLive;
|
|
||||||
}, false),
|
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_play, "Streams", "Check for finished streams", "fetchStreams", {
|
|
||||||
subscription.doFetchStreams = menu?.selectOption(null, "fetchStreams", true, true) ?: subscription.doFetchLive;
|
|
||||||
}, false),
|
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_play, "Videos", "Check for videos", "fetchVideos", {
|
|
||||||
subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchLive;
|
|
||||||
}, false),
|
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_chat, "Posts", "Check for posts", "fetchPosts", {
|
|
||||||
subscription.doFetchPosts = menu?.selectOption(null, "fetchPosts", true, true) ?: subscription.doFetchLive;
|
|
||||||
}, false)));
|
|
||||||
|
|
||||||
menu = SlideUpMenuOverlay(container.context, container, "Subscription Settings", null, true, items);
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO){
|
||||||
|
val plugin = StatePlatform.instance.getChannelClient(subscription.channel.url);
|
||||||
|
val capabilities = plugin.getChannelCapabilities();
|
||||||
|
|
||||||
if(subscription.doNotifications)
|
withContext(Dispatchers.Main) {
|
||||||
menu.selectOption(null, "notifications", true, true);
|
items.addAll(listOf(
|
||||||
if(subscription.doFetchLive)
|
SlideUpMenuItem(container.context, R.drawable.ic_notifications, "Notifications", "", "notifications", {
|
||||||
menu.selectOption(null, "fetchLive", true, true);
|
subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications;
|
||||||
if(subscription.doFetchStreams)
|
}, false),
|
||||||
menu.selectOption(null, "fetchStreams", true, true);
|
if(StateSubscriptionGroups.instance.getSubscriptionGroups().isNotEmpty())
|
||||||
if(subscription.doFetchVideos)
|
SlideUpMenuGroup(container.context, "Subscription Groups",
|
||||||
menu.selectOption(null, "fetchVideos", true, true);
|
"You can select which groups this subscription is part of.",
|
||||||
if(subscription.doFetchPosts)
|
-1, listOf()) else null,
|
||||||
menu.selectOption(null, "fetchPosts", true, true);
|
if(StateSubscriptionGroups.instance.getSubscriptionGroups().isNotEmpty())
|
||||||
|
SlideUpMenuRecycler(container.context, "as") {
|
||||||
|
val groups = ArrayList<SubscriptionGroup>(StateSubscriptionGroups.instance.getSubscriptionGroups()
|
||||||
|
.map { SubscriptionGroup.Selectable(it, it.urls.contains(subscription.channel.url)) }
|
||||||
|
.sortedBy { !it.selected });
|
||||||
|
var adapter: AnyAdapterView<SubscriptionGroup, SubscriptionGroupBarViewHolder>? = null;
|
||||||
|
adapter = it.asAny(groups, RecyclerView.HORIZONTAL) {
|
||||||
|
it.onClick.subscribe {
|
||||||
|
if(it is SubscriptionGroup.Selectable) {
|
||||||
|
val actualGroup = StateSubscriptionGroups.instance.getSubscriptionGroup(it.id)
|
||||||
|
?: return@subscribe;
|
||||||
|
groups.clear();
|
||||||
|
if(it.selected)
|
||||||
|
actualGroup.urls.remove(subscription.channel.url);
|
||||||
|
else
|
||||||
|
actualGroup.urls.add(subscription.channel.url);
|
||||||
|
|
||||||
menu.onOK.subscribe {
|
StateSubscriptionGroups.instance.updateSubscriptionGroup(actualGroup);
|
||||||
subscription.save();
|
groups.addAll(StateSubscriptionGroups.instance.getSubscriptionGroups()
|
||||||
menu.hide(true);
|
.map { SubscriptionGroup.Selectable(it, it.urls.contains(subscription.channel.url)) }
|
||||||
};
|
.sortedBy { !it.selected });
|
||||||
menu.onCancel.subscribe {
|
adapter?.notifyContentChanged();
|
||||||
subscription.doNotifications = originalNotif;
|
}
|
||||||
subscription.doFetchLive = originalLive;
|
}
|
||||||
subscription.doFetchStreams = originalStream;
|
};
|
||||||
subscription.doFetchVideos = originalVideo;
|
return@SlideUpMenuRecycler adapter;
|
||||||
subscription.doFetchPosts = originalPosts;
|
} else null,
|
||||||
};
|
SlideUpMenuGroup(container.context, "Fetch Settings",
|
||||||
|
"Depending on the platform you might not need to enable a type for it to be available.",
|
||||||
|
-1, listOf()),
|
||||||
|
if(capabilities.hasType(ResultCapabilities.TYPE_LIVE)) SlideUpMenuItem(container.context, R.drawable.ic_live_tv, "Livestreams", "Check for livestreams", "fetchLive", {
|
||||||
|
subscription.doFetchLive = menu?.selectOption(null, "fetchLive", true, true) ?: subscription.doFetchLive;
|
||||||
|
}, false) else null,
|
||||||
|
if(capabilities.hasType(ResultCapabilities.TYPE_STREAMS)) SlideUpMenuItem(container.context, R.drawable.ic_play, "Streams", "Check for streams", "fetchStreams", {
|
||||||
|
subscription.doFetchStreams = menu?.selectOption(null, "fetchStreams", true, true) ?: subscription.doFetchStreams;
|
||||||
|
}, false) else null,
|
||||||
|
if(capabilities.hasType(ResultCapabilities.TYPE_VIDEOS))
|
||||||
|
SlideUpMenuItem(container.context, R.drawable.ic_play, "Videos", "Check for videos", "fetchVideos", {
|
||||||
|
subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos;
|
||||||
|
}, false) else if(capabilities.hasType(ResultCapabilities.TYPE_MIXED) || capabilities.types.isEmpty())
|
||||||
|
SlideUpMenuItem(container.context, R.drawable.ic_play, "Content", "Check for content", "fetchVideos", {
|
||||||
|
subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos;
|
||||||
|
}, false) else null,
|
||||||
|
if(capabilities.hasType(ResultCapabilities.TYPE_POSTS)) SlideUpMenuItem(container.context, R.drawable.ic_chat, "Posts", "Check for posts", "fetchPosts", {
|
||||||
|
subscription.doFetchPosts = menu?.selectOption(null, "fetchPosts", true, true) ?: subscription.doFetchPosts;
|
||||||
|
}, false) else null/*,,
|
||||||
|
|
||||||
menu.setOk("Save");
|
SlideUpMenuGroup(container.context, "Actions",
|
||||||
|
"Various things you can do with this subscription",
|
||||||
|
-1, listOf())
|
||||||
|
SlideUpMenuItem(container.context, R.drawable.ic_list, "Add to Group", "", "btnAddToGroup", {
|
||||||
|
showCreateSubscriptionGroup(container, subscription.channel);
|
||||||
|
}, false)*/
|
||||||
|
).filterNotNull());
|
||||||
|
|
||||||
|
menu.setItems(items);
|
||||||
|
|
||||||
|
if(subscription.doNotifications)
|
||||||
|
menu.selectOption(null, "notifications", true, true);
|
||||||
|
if(subscription.doFetchLive)
|
||||||
|
menu.selectOption(null, "fetchLive", true, true);
|
||||||
|
if(subscription.doFetchStreams)
|
||||||
|
menu.selectOption(null, "fetchStreams", true, true);
|
||||||
|
if(subscription.doFetchVideos)
|
||||||
|
menu.selectOption(null, "fetchVideos", true, true);
|
||||||
|
if(subscription.doFetchPosts)
|
||||||
|
menu.selectOption(null, "fetchPosts", true, true);
|
||||||
|
|
||||||
|
menu.onOK.subscribe {
|
||||||
|
subscription.save();
|
||||||
|
menu.hide(true);
|
||||||
|
|
||||||
|
if(subscription.doNotifications && !originalNotif) {
|
||||||
|
val mainContext = StateApp.instance.contextOrNull;
|
||||||
|
if(Settings.instance.subscriptions.subscriptionsBackgroundUpdateInterval == 0) {
|
||||||
|
UIDialogs.toast(container.context, "Enable 'Background Update' in settings for notifications to work");
|
||||||
|
|
||||||
|
if(mainContext is MainActivity) {
|
||||||
|
UIDialogs.showDialog(mainContext, R.drawable.ic_settings, "Background Updating Required",
|
||||||
|
"You need to set a Background Updating interval for notifications", null, 0,
|
||||||
|
UIDialogs.Action("Cancel", {}),
|
||||||
|
UIDialogs.Action("Configure", {
|
||||||
|
val intent = Intent(mainContext, SettingsActivity::class.java);
|
||||||
|
intent.putExtra("query", mainContext.getString(R.string.background_update));
|
||||||
|
mainContext.startActivity(intent);
|
||||||
|
}, UIDialogs.ActionStyle.PRIMARY));
|
||||||
|
}
|
||||||
|
return@subscribe;
|
||||||
|
}
|
||||||
|
else if(!(mainContext?.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).areNotificationsEnabled()) {
|
||||||
|
UIDialogs.toast(container.context, "Android notifications are disabled");
|
||||||
|
if(mainContext is MainActivity) {
|
||||||
|
mainContext.requestNotificationPermissions("Notifications are required for subscription updating and notifications to work");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
menu.onCancel.subscribe {
|
||||||
|
subscription.doNotifications = originalNotif;
|
||||||
|
subscription.doFetchLive = originalLive;
|
||||||
|
subscription.doFetchStreams = originalStream;
|
||||||
|
subscription.doFetchVideos = originalVideo;
|
||||||
|
subscription.doFetchPosts = originalPosts;
|
||||||
|
};
|
||||||
|
|
||||||
|
menu.setOk("Save");
|
||||||
|
|
||||||
|
menu.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return menu;
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showAddToGroupOverlay(channel: IPlatformVideo, container: ViewGroup) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showHlsPicker(video: IPlatformVideoDetails, source: Any, sourceUrl: String, container: ViewGroup): SlideUpMenuOverlay {
|
||||||
|
val items = arrayListOf<View>(LoaderView(container.context))
|
||||||
|
val slideUpMenuOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.download_video), null, true, items)
|
||||||
|
|
||||||
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
|
val masterPlaylistResponse = ManagedHttpClient().get(sourceUrl)
|
||||||
|
check(masterPlaylistResponse.isOk) { "Failed to get master playlist: ${masterPlaylistResponse.code}" }
|
||||||
|
|
||||||
|
val masterPlaylistContent = masterPlaylistResponse.body?.string()
|
||||||
|
?: throw Exception("Master playlist content is empty")
|
||||||
|
|
||||||
|
val videoButtons = arrayListOf<SlideUpMenuItem>()
|
||||||
|
val audioButtons = arrayListOf<SlideUpMenuItem>()
|
||||||
|
//TODO: Implement subtitles
|
||||||
|
//val subtitleButtons = arrayListOf<SlideUpMenuItem>()
|
||||||
|
|
||||||
|
var selectedVideoVariant: HLSVariantVideoUrlSource? = null
|
||||||
|
var selectedAudioVariant: HLSVariantAudioUrlSource? = null
|
||||||
|
//TODO: Implement subtitles
|
||||||
|
//var selectedSubtitleVariant: HLSVariantSubtitleUrlSource? = null
|
||||||
|
|
||||||
|
val masterPlaylist: HLS.MasterPlaylist
|
||||||
|
try {
|
||||||
|
masterPlaylist = HLS.parseMasterPlaylist(masterPlaylistContent, sourceUrl)
|
||||||
|
|
||||||
|
masterPlaylist.getAudioSources().forEach { it ->
|
||||||
|
audioButtons.add(SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, listOf(it.language, it.codec).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "), it, {
|
||||||
|
selectedAudioVariant = it
|
||||||
|
slideUpMenuOverlay.selectOption(audioButtons, it)
|
||||||
|
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
||||||
|
}, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
/*masterPlaylist.getSubtitleSources().forEach { it ->
|
||||||
|
subtitleButtons.add(SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, listOf(it.format).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "), it, {
|
||||||
|
selectedSubtitleVariant = it
|
||||||
|
slideUpMenuOverlay.selectOption(subtitleButtons, it)
|
||||||
|
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
||||||
|
}, false))
|
||||||
|
}*/
|
||||||
|
|
||||||
|
masterPlaylist.getVideoSources().forEach {
|
||||||
|
videoButtons.add(SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "${it.width}x${it.height}", it, {
|
||||||
|
selectedVideoVariant = it
|
||||||
|
slideUpMenuOverlay.selectOption(videoButtons, it)
|
||||||
|
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
||||||
|
}, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
val newItems = arrayListOf<View>()
|
||||||
|
if (videoButtons.isNotEmpty()) {
|
||||||
|
newItems.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.video), videoButtons, videoButtons))
|
||||||
|
}
|
||||||
|
if (audioButtons.isNotEmpty()) {
|
||||||
|
newItems.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.audio), audioButtons, audioButtons))
|
||||||
|
}
|
||||||
|
//TODO: Implement subtitles
|
||||||
|
/*if (subtitleButtons.isNotEmpty()) {
|
||||||
|
newItems.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.subtitles), subtitleButtons, subtitleButtons))
|
||||||
|
}*/
|
||||||
|
|
||||||
|
slideUpMenuOverlay.onOK.subscribe {
|
||||||
|
//TODO: Fix SubtitleRawSource issue
|
||||||
|
StateDownloads.instance.download(video, selectedVideoVariant, selectedAudioVariant, null);
|
||||||
|
slideUpMenuOverlay.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
slideUpMenuOverlay.setItems(newItems)
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
if (masterPlaylistContent.lines().any { it.startsWith("#EXTINF:") }) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
if (source is IHLSManifestSource) {
|
||||||
|
StateDownloads.instance.download(video, HLSVariantVideoUrlSource("variant", 0, 0, "application/vnd.apple.mpegurl", "", null, 0, false, sourceUrl), null, null)
|
||||||
|
UIDialogs.toast(container.context, "Variant video HLS playlist download started")
|
||||||
|
slideUpMenuOverlay.hide()
|
||||||
|
} else if (source is IHLSManifestAudioSource) {
|
||||||
|
StateDownloads.instance.download(video, null, HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, sourceUrl), null)
|
||||||
|
UIDialogs.toast(container.context, "Variant audio HLS playlist download started")
|
||||||
|
slideUpMenuOverlay.hide()
|
||||||
|
} else {
|
||||||
|
throw NotImplementedError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return slideUpMenuOverlay.apply { show() }
|
||||||
|
|
||||||
menu.show();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showDownloadVideoOverlay(video: IPlatformVideoDetails, container: ViewGroup, contentResolver: ContentResolver? = null): SlideUpMenuOverlay? {
|
fun showDownloadVideoOverlay(video: IPlatformVideoDetails, container: ViewGroup, contentResolver: ContentResolver? = null): SlideUpMenuOverlay? {
|
||||||
@@ -128,7 +329,7 @@ class UISlideOverlays {
|
|||||||
val audioSources = if(descriptor is VideoUnMuxedSourceDescriptor) descriptor.audioSources else null;
|
val audioSources = if(descriptor is VideoUnMuxedSourceDescriptor) descriptor.audioSources else null;
|
||||||
val subtitleSources = video.subtitles;
|
val subtitleSources = video.subtitles;
|
||||||
|
|
||||||
if(videoSources.size == 0 && (audioSources?.size ?: 0) == 0) {
|
if(videoSources.isEmpty() && (audioSources?.size ?: 0) == 0) {
|
||||||
UIDialogs.toast(container.context.getString(R.string.no_downloads_available), false);
|
UIDialogs.toast(container.context.getString(R.string.no_downloads_available), false);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -149,49 +350,72 @@ class UISlideOverlays {
|
|||||||
videoSources
|
videoSources
|
||||||
.filter { it.isDownloadable() }
|
.filter { it.isDownloadable() }
|
||||||
.map {
|
.map {
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "${it.width}x${it.height}", it, {
|
when (it) {
|
||||||
selectedVideo = it as IVideoUrlSource;
|
is IVideoUrlSource -> {
|
||||||
menu?.selectOption(videoSources, it);
|
SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "${it.width}x${it.height}", it, {
|
||||||
if(selectedAudio != null || !requiresAudio)
|
selectedVideo = it
|
||||||
menu?.setOk(container.context.getString(R.string.download));
|
menu?.selectOption(videoSources, it);
|
||||||
}, false)
|
if(selectedAudio != null || !requiresAudio)
|
||||||
|
menu?.setOk(container.context.getString(R.string.download));
|
||||||
|
}, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
is IHLSManifestSource -> {
|
||||||
|
SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "HLS", it, {
|
||||||
|
showHlsPicker(video, it, it.url, container)
|
||||||
|
}, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
throw Exception("Unhandled source type")
|
||||||
|
}
|
||||||
|
}
|
||||||
}).flatten().toList()
|
}).flatten().toList()
|
||||||
));
|
));
|
||||||
|
|
||||||
if(Settings.instance.downloads.getDefaultVideoQualityPixels() > 0 && videoSources.size > 0)
|
if(Settings.instance.downloads.getDefaultVideoQualityPixels() > 0 && videoSources.isNotEmpty()) {
|
||||||
selectedVideo = VideoHelper.selectBestVideoSource(videoSources.filter { it.isDownloadable() }.asIterable(),
|
//TODO: Add HLS support here
|
||||||
|
selectedVideo = VideoHelper.selectBestVideoSource(
|
||||||
|
videoSources.filter { it is IVideoUrlSource && it.isDownloadable() }.asIterable(),
|
||||||
Settings.instance.downloads.getDefaultVideoQualityPixels(),
|
Settings.instance.downloads.getDefaultVideoQualityPixels(),
|
||||||
FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS) as IVideoUrlSource;
|
FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS
|
||||||
|
) as IVideoUrlSource?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (audioSources != null) {
|
||||||
audioSources?.let { audioSources ->
|
|
||||||
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.audio), audioSources, audioSources
|
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.audio), audioSources, audioSources
|
||||||
.filter { VideoHelper.isDownloadable(it) }
|
.filter { VideoHelper.isDownloadable(it) }
|
||||||
.map {
|
.map {
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, "${it.bitrate}", it, {
|
when (it) {
|
||||||
selectedAudio = it as IAudioUrlSource;
|
is IAudioUrlSource -> {
|
||||||
menu?.selectOption(audioSources, it);
|
SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, "${it.bitrate}", it, {
|
||||||
menu?.setOk(container.context.getString(R.string.download));
|
selectedAudio = it
|
||||||
}, false);
|
menu?.selectOption(audioSources, it);
|
||||||
|
menu?.setOk(container.context.getString(R.string.download));
|
||||||
|
}, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
is IHLSManifestAudioSource -> {
|
||||||
|
SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "HLS Audio", it, {
|
||||||
|
showHlsPicker(video, it, it.url, container)
|
||||||
|
}, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
throw Exception("Unhandled source type")
|
||||||
|
}
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
val asources = audioSources;
|
|
||||||
val preferredAudioSource = VideoHelper.selectBestAudioSource(asources.asIterable(),
|
|
||||||
FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS,
|
|
||||||
Settings.instance.playback.getPrimaryLanguage(container.context),
|
|
||||||
if(Settings.instance.downloads.isHighBitrateDefault()) 99999999 else 1);
|
|
||||||
menu?.selectOption(asources, preferredAudioSource);
|
|
||||||
|
|
||||||
|
//TODO: Add HLS support here
|
||||||
selectedAudio = VideoHelper.selectBestAudioSource(audioSources.filter { it.isDownloadable() }.asIterable(),
|
selectedAudio = VideoHelper.selectBestAudioSource(audioSources.filter { it is IAudioUrlSource && it.isDownloadable() }.asIterable(),
|
||||||
FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS,
|
FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS,
|
||||||
Settings.instance.playback.getPrimaryLanguage(container.context),
|
Settings.instance.playback.getPrimaryLanguage(container.context),
|
||||||
if(Settings.instance.downloads.isHighBitrateDefault()) 9999999 else 1) as IAudioUrlSource?;
|
if(Settings.instance.downloads.isHighBitrateDefault()) 9999999 else 1) as IAudioUrlSource?;
|
||||||
}
|
}
|
||||||
|
|
||||||
//ContentResolver is required for subtitles..
|
if(contentResolver != null && subtitleSources.isNotEmpty()) {
|
||||||
if(contentResolver != null) {
|
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.subtitles), subtitleSources, subtitleSources.map {
|
||||||
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.subtitles), subtitleSources, subtitleSources
|
|
||||||
.map {
|
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_edit, it.name, "", it, {
|
SlideUpMenuItem(container.context, R.drawable.ic_edit, it.name, "", it, {
|
||||||
if (selectedSubtitle == it) {
|
if (selectedSubtitle == it) {
|
||||||
selectedSubtitle = null;
|
selectedSubtitle = null;
|
||||||
@@ -201,7 +425,8 @@ class UISlideOverlays {
|
|||||||
menu?.selectOption(subtitleSources, it);
|
menu?.selectOption(subtitleSources, it);
|
||||||
}
|
}
|
||||||
}, false);
|
}, false);
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
menu = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.download_video), null, true, items);
|
menu = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.download_video), null, true, items);
|
||||||
@@ -285,10 +510,15 @@ class UISlideOverlays {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun showDownloadPlaylistOverlay(playlist: Playlist, container: ViewGroup) {
|
fun showDownloadPlaylistOverlay(playlist: Playlist, container: ViewGroup) {
|
||||||
showUnknownVideoDownload(container.context.getString(R.string.video), container) { px, bitrate ->
|
showUnknownVideoDownload(container.context.getString(R.string.playlist), container) { px, bitrate ->
|
||||||
StateDownloads.instance.download(playlist, px, bitrate);
|
StateDownloads.instance.download(playlist, px, bitrate);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
fun showDownloadWatchlaterOverlay(container: ViewGroup) {
|
||||||
|
showUnknownVideoDownload(container.context.getString(R.string.watch_later), container, { px, bitrate ->
|
||||||
|
StateDownloads.instance.downloadWatchLater(px, bitrate);
|
||||||
|
})
|
||||||
|
}
|
||||||
private fun showUnknownVideoDownload(toDownload: String, container: ViewGroup, cb: (Long?, Long?)->Unit) {
|
private fun showUnknownVideoDownload(toDownload: String, container: ViewGroup, cb: (Long?, Long?)->Unit) {
|
||||||
val items = arrayListOf<View>();
|
val items = arrayListOf<View>();
|
||||||
var menu: SlideUpMenuOverlay? = null;
|
var menu: SlideUpMenuOverlay? = null;
|
||||||
@@ -361,7 +591,7 @@ class UISlideOverlays {
|
|||||||
val dp70 = 70.dp(container.context.resources);
|
val dp70 = 70.dp(container.context.resources);
|
||||||
val dp15 = 15.dp(container.context.resources);
|
val dp15 = 15.dp(container.context.resources);
|
||||||
val overlay = SlideUpMenuOverlay(container.context, container, text, null, true, listOf(
|
val overlay = SlideUpMenuOverlay(container.context, container, text, null, true, listOf(
|
||||||
Loader(container.context, true, dp70).apply {
|
LoaderView(container.context, true, dp70).apply {
|
||||||
this.setPadding(0, dp15, 0, dp15);
|
this.setPadding(0, dp15, 0, dp15);
|
||||||
}
|
}
|
||||||
), true);
|
), true);
|
||||||
@@ -369,6 +599,75 @@ class UISlideOverlays {
|
|||||||
return overlay;
|
return overlay;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun showCreateSubscriptionGroup(container: ViewGroup, initialChannel: IPlatformChannel? = null, onCreate: ((String) -> Unit)? = null): SlideUpMenuOverlay {
|
||||||
|
val nameInput = SlideUpMenuTextInput(container.context, container.context.getString(R.string.name));
|
||||||
|
val addSubGroupOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.create_new_subgroup), container.context.getString(R.string.ok), false, nameInput);
|
||||||
|
|
||||||
|
addSubGroupOverlay.onOK.subscribe {
|
||||||
|
val text = nameInput.text;
|
||||||
|
if (text.isBlank()) {
|
||||||
|
return@subscribe;
|
||||||
|
}
|
||||||
|
|
||||||
|
addSubGroupOverlay.hide();
|
||||||
|
nameInput.deactivate();
|
||||||
|
nameInput.clear();
|
||||||
|
if(onCreate == null)
|
||||||
|
{
|
||||||
|
//TODO: Do this better, temp
|
||||||
|
StateApp.instance.contextOrNull?.let {
|
||||||
|
if(it is MainActivity) {
|
||||||
|
val subGroup = SubscriptionGroup(text);
|
||||||
|
if(initialChannel != null) {
|
||||||
|
subGroup.urls.add(initialChannel.url);
|
||||||
|
if(initialChannel.thumbnail != null)
|
||||||
|
subGroup.image = ImageVariable(initialChannel.thumbnail);
|
||||||
|
}
|
||||||
|
it.navigate(it.getFragment<SubscriptionGroupFragment>(), subGroup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
onCreate(text)
|
||||||
|
};
|
||||||
|
|
||||||
|
addSubGroupOverlay.onCancel.subscribe {
|
||||||
|
nameInput.deactivate();
|
||||||
|
nameInput.clear();
|
||||||
|
};
|
||||||
|
|
||||||
|
addSubGroupOverlay.show();
|
||||||
|
nameInput.activate();
|
||||||
|
|
||||||
|
return addSubGroupOverlay
|
||||||
|
}
|
||||||
|
fun showCreatePlaylistOverlay(container: ViewGroup, onCreate: (String) -> Unit): SlideUpMenuOverlay {
|
||||||
|
val nameInput = SlideUpMenuTextInput(container.context, container.context.getString(R.string.name));
|
||||||
|
val addPlaylistOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.create_new_playlist), container.context.getString(R.string.ok), false, nameInput);
|
||||||
|
|
||||||
|
addPlaylistOverlay.onOK.subscribe {
|
||||||
|
val text = nameInput.text;
|
||||||
|
if (text.isBlank()) {
|
||||||
|
return@subscribe;
|
||||||
|
}
|
||||||
|
|
||||||
|
addPlaylistOverlay.hide();
|
||||||
|
nameInput.deactivate();
|
||||||
|
nameInput.clear();
|
||||||
|
onCreate(text)
|
||||||
|
};
|
||||||
|
|
||||||
|
addPlaylistOverlay.onCancel.subscribe {
|
||||||
|
nameInput.deactivate();
|
||||||
|
nameInput.clear();
|
||||||
|
};
|
||||||
|
|
||||||
|
addPlaylistOverlay.show();
|
||||||
|
nameInput.activate();
|
||||||
|
|
||||||
|
return addPlaylistOverlay
|
||||||
|
}
|
||||||
|
|
||||||
fun showVideoOptionsOverlay(video: IPlatformVideo, container: ViewGroup, vararg actions: SlideUpMenuItem): SlideUpMenuOverlay {
|
fun showVideoOptionsOverlay(video: IPlatformVideo, container: ViewGroup, vararg actions: SlideUpMenuItem): SlideUpMenuOverlay {
|
||||||
val items = arrayListOf<View>();
|
val items = arrayListOf<View>();
|
||||||
val lastUpdated = StatePlaylists.instance.getLastUpdatedPlaylist();
|
val lastUpdated = StatePlaylists.instance.getLastUpdatedPlaylist();
|
||||||
@@ -389,8 +688,21 @@ class UISlideOverlays {
|
|||||||
val watchLater = StatePlaylists.instance.getWatchLater();
|
val watchLater = StatePlaylists.instance.getWatchLater();
|
||||||
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.actions), "actions",
|
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.actions), "actions",
|
||||||
(listOf(
|
(listOf(
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_download, container.context.getString(R.string.download), container.context.getString(R.string.download_the_video), container.context.getString(R.string.download),
|
SlideUpMenuItem(container.context, R.drawable.ic_download, container.context.getString(R.string.download), container.context.getString(R.string.download_the_video), "download", {
|
||||||
{ showDownloadVideoOverlay(video, container, true); }, false))
|
showDownloadVideoOverlay(video, container, true);
|
||||||
|
}, false),
|
||||||
|
SlideUpMenuItem(container.context, R.drawable.ic_share, container.context.getString(R.string.share), "Share the video", "share", {
|
||||||
|
val url = if(video.shareUrl.isNotEmpty()) video.shareUrl else video.url;
|
||||||
|
container.context.startActivity(Intent.createChooser(Intent().apply {
|
||||||
|
action = Intent.ACTION_SEND;
|
||||||
|
putExtra(Intent.EXTRA_TEXT, url);
|
||||||
|
type = "text/plain";
|
||||||
|
}, null));
|
||||||
|
}, false),
|
||||||
|
SlideUpMenuItem(container.context, R.drawable.ic_visibility_off, container.context.getString(R.string.hide_creator_from_home), "", "hide_creator", {
|
||||||
|
StateMeta.instance.addHiddenCreator(video.author.url);
|
||||||
|
UIDialogs.toast(container.context, "[${video.author.name}] hidden, you may need to reload home");
|
||||||
|
}))
|
||||||
+ actions)
|
+ actions)
|
||||||
));
|
));
|
||||||
items.add(
|
items.add(
|
||||||
@@ -402,6 +714,13 @@ class UISlideOverlays {
|
|||||||
));
|
));
|
||||||
|
|
||||||
val playlistItems = arrayListOf<SlideUpMenuItem>();
|
val playlistItems = arrayListOf<SlideUpMenuItem>();
|
||||||
|
playlistItems.add(SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, container.context.getString(R.string.new_playlist), container.context.getString(R.string.add_to_new_playlist), "add_to_new_playlist", {
|
||||||
|
showCreatePlaylistOverlay(container) {
|
||||||
|
val playlist = Playlist(it, arrayListOf(SerializedPlatformVideo.fromVideo(video)));
|
||||||
|
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
|
||||||
|
};
|
||||||
|
}, false))
|
||||||
|
|
||||||
for (playlist in allPlaylists) {
|
for (playlist in allPlaylists) {
|
||||||
playlistItems.add(SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, "${container.context.getString(R.string.add_to)} " + playlist.name + "", "${playlist.videos.size} " + container.context.getString(R.string.videos), "",
|
playlistItems.add(SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, "${container.context.getString(R.string.add_to)} " + playlist.name + "", "${playlist.videos.size} " + container.context.getString(R.string.videos), "",
|
||||||
{
|
{
|
||||||
@@ -417,7 +736,7 @@ class UISlideOverlays {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun showAddToOverlay(video: IPlatformVideo, container: ViewGroup): SlideUpMenuOverlay {
|
fun showAddToOverlay(video: IPlatformVideo, container: ViewGroup, slideUpMenuOverlayUpdated: (SlideUpMenuOverlay) -> Unit): SlideUpMenuOverlay {
|
||||||
|
|
||||||
val items = arrayListOf<View>();
|
val items = arrayListOf<View>();
|
||||||
|
|
||||||
@@ -448,6 +767,13 @@ class UISlideOverlays {
|
|||||||
);
|
);
|
||||||
|
|
||||||
val playlistItems = arrayListOf<SlideUpMenuItem>();
|
val playlistItems = arrayListOf<SlideUpMenuItem>();
|
||||||
|
playlistItems.add(SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, container.context.getString(R.string.new_playlist), container.context.getString(R.string.add_to_new_playlist), "add_to_new_playlist", {
|
||||||
|
slideUpMenuOverlayUpdated(showCreatePlaylistOverlay(container) {
|
||||||
|
val playlist = Playlist(it, arrayListOf(SerializedPlatformVideo.fromVideo(video)));
|
||||||
|
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
|
||||||
|
});
|
||||||
|
}, false))
|
||||||
|
|
||||||
for (playlist in allPlaylists) {
|
for (playlist in allPlaylists) {
|
||||||
playlistItems.add(SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, playlist.name, "${playlist.videos.size} " + container.context.getString(R.string.videos), "",
|
playlistItems.add(SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, playlist.name, "${playlist.videos.size} " + container.context.getString(R.string.videos), "",
|
||||||
{
|
{
|
||||||
@@ -462,21 +788,22 @@ class UISlideOverlays {
|
|||||||
return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.add_to), null, true, items).apply { show() };
|
return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.add_to), null, true, items).apply { show() };
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showFiltersOverlay(lifecycleScope: CoroutineScope, container: ViewGroup, enabledClientsIds: List<String>, filterValues: HashMap<String, List<String>>): SlideUpMenuFilters {
|
fun showFiltersOverlay(lifecycleScope: CoroutineScope, container: ViewGroup, enabledClientsIds: List<String>, filterValues: HashMap<String, List<String>>, isChannelSearch: Boolean = false): SlideUpMenuFilters {
|
||||||
val overlay = SlideUpMenuFilters(lifecycleScope, container, enabledClientsIds, filterValues);
|
val overlay = SlideUpMenuFilters(lifecycleScope, container, enabledClientsIds, filterValues, isChannelSearch);
|
||||||
overlay.show();
|
overlay.show();
|
||||||
return overlay;
|
return overlay;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun showMoreButtonOverlay(container: ViewGroup, buttonGroup: RoundButtonGroup, ignoreTags: List<Any> = listOf(), onPinnedbuttons: ((List<RoundButton>)->Unit)? = null): SlideUpMenuOverlay {
|
fun showMoreButtonOverlay(container: ViewGroup, buttonGroup: RoundButtonGroup, ignoreTags: List<Any> = listOf(), invokeParents: Boolean = true, onPinnedbuttons: ((List<RoundButton>)->Unit)? = null): SlideUpMenuOverlay {
|
||||||
val visible = buttonGroup.getVisibleButtons().filter { !ignoreTags.contains(it.tagRef) };
|
val visible = buttonGroup.getVisibleButtons().filter { !ignoreTags.contains(it.tagRef) };
|
||||||
val hidden = buttonGroup.getInvisibleButtons().filter { !ignoreTags.contains(it.tagRef) };
|
val hidden = buttonGroup.getInvisibleButtons().filter { !ignoreTags.contains(it.tagRef) };
|
||||||
|
|
||||||
val views = arrayOf(hidden
|
val views = arrayOf(
|
||||||
.map { btn -> SlideUpMenuItem(container.context, btn.iconResource, btn.text.text.toString(), "", "", {
|
hidden
|
||||||
btn.handler?.invoke(btn);
|
.map { btn -> SlideUpMenuItem(container.context, btn.iconResource, btn.text.text.toString(), "", "", {
|
||||||
}, true) as View }.toTypedArray() ?: arrayOf(),
|
btn.handler?.invoke(btn);
|
||||||
|
}, invokeParents) as View }.toTypedArray(),
|
||||||
arrayOf(SlideUpMenuItem(container.context, R.drawable.ic_pin, container.context.getString(R.string.change_pins), container.context.getString(R.string.decide_which_buttons_should_be_pinned), "", {
|
arrayOf(SlideUpMenuItem(container.context, R.drawable.ic_pin, container.context.getString(R.string.change_pins), container.context.getString(R.string.decide_which_buttons_should_be_pinned), "", {
|
||||||
showOrderOverlay(container, container.context.getString(R.string.select_your_pins_in_order), (visible + hidden).map { Pair(it.text.text.toString(), it.tagRef!!) }) {
|
showOrderOverlay(container, container.context.getString(R.string.select_your_pins_in_order), (visible + hidden).map { Pair(it.text.text.toString(), it.tagRef!!) }) {
|
||||||
val selected = it
|
val selected = it
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ fun InputStream.copyToOutputStream(inputStreamLength: Long, outputStream: Output
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
fun Activity.setNavigationBarColorAndIcons() {
|
fun Activity.setNavigationBarColorAndIcons() {
|
||||||
window.navigationBarColor = ContextCompat.getColor(this, android.R.color.black);
|
window.navigationBarColor = ContextCompat.getColor(this, android.R.color.black);
|
||||||
|
|
||||||
@@ -164,9 +165,7 @@ fun Int.sp(resources: Resources): Int {
|
|||||||
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, this.toFloat(), resources.displayMetrics).toInt()
|
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, this.toFloat(), resources.displayMetrics).toInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun File.share(context: Context) {
|
fun DocumentFile.share(context: Context) {
|
||||||
val uri = FileProvider.getUriForFile(context, context.resources.getString(R.string.authority), this);
|
|
||||||
|
|
||||||
val shareIntent = Intent();
|
val shareIntent = Intent();
|
||||||
shareIntent.action = Intent.ACTION_SEND;
|
shareIntent.action = Intent.ACTION_SEND;
|
||||||
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||||
|
|||||||
@@ -1,16 +1,25 @@
|
|||||||
package com.futo.platformplayer.activities
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.drawable.Animatable
|
import android.graphics.drawable.Animatable
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.*
|
import android.widget.ImageButton
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.ScrollView
|
||||||
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.platformplayer.states.StatePlugins
|
import com.futo.platformplayer.states.StatePlugins
|
||||||
@@ -29,8 +38,10 @@ class AddSourceActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private lateinit var _sourceHeader: SourceHeaderView;
|
private lateinit var _sourceHeader: SourceHeaderView;
|
||||||
|
|
||||||
|
|
||||||
private lateinit var _sourcePermissions: LinearLayout;
|
private lateinit var _sourcePermissions: LinearLayout;
|
||||||
private lateinit var _sourceWarnings: LinearLayout;
|
private lateinit var _sourceWarnings: LinearLayout;
|
||||||
|
private lateinit var _sourceWarningsContainer: LinearLayout;
|
||||||
|
|
||||||
private lateinit var _container: ScrollView;
|
private lateinit var _container: ScrollView;
|
||||||
private lateinit var _loader: ImageView;
|
private lateinit var _loader: ImageView;
|
||||||
@@ -45,6 +56,10 @@ class AddSourceActivity : AppCompatActivity() {
|
|||||||
private var _config: SourcePluginConfig? = null;
|
private var _config: SourcePluginConfig? = null;
|
||||||
private var _script: String? = null;
|
private var _script: String? = null;
|
||||||
|
|
||||||
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
|
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
@@ -67,6 +82,7 @@ class AddSourceActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
_sourcePermissions = findViewById(R.id.source_permissions);
|
_sourcePermissions = findViewById(R.id.source_permissions);
|
||||||
_sourceWarnings = findViewById(R.id.source_warnings);
|
_sourceWarnings = findViewById(R.id.source_warnings);
|
||||||
|
_sourceWarningsContainer = findViewById(R.id.container_source_warnings);
|
||||||
|
|
||||||
_container = findViewById(R.id.configContainer);
|
_container = findViewById(R.id.configContainer);
|
||||||
_loader = findViewById(R.id.loader);
|
_loader = findViewById(R.id.loader);
|
||||||
@@ -189,23 +205,32 @@ class AddSourceActivity : AppCompatActivity() {
|
|||||||
config.allowUrls, true)
|
config.allowUrls, true)
|
||||||
)
|
)
|
||||||
|
|
||||||
val pastelRed = resources.getColor(R.color.pastel_red);
|
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(
|
_sourceWarnings.addView(
|
||||||
SourceInfoView(this,
|
SourceInfoView(this,
|
||||||
R.drawable.ic_security_pred,
|
R.drawable.ic_security_pred,
|
||||||
warning.first,
|
warning.first,
|
||||||
warning.second)
|
warning.second)
|
||||||
.withDescriptionColor(pastelRed));
|
.withDescriptionColor(pastelRed));
|
||||||
|
_sourceWarningsContainer.isVisible = warnings.isNotEmpty();
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
fun install(config: SourcePluginConfig, script: String) {
|
fun install(config: SourcePluginConfig, script: String) {
|
||||||
|
val isNew = !StatePlatform.instance.getAvailableClients().any { it.id == config.id };
|
||||||
StatePlugins.instance.installPlugin(this, lifecycleScope, config, script) {
|
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();
|
backToSources();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.futo.platformplayer.activities
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@@ -7,14 +8,15 @@ import android.widget.*
|
|||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.views.buttons.BigButton
|
import com.futo.platformplayer.views.buttons.BigButton
|
||||||
import com.google.zxing.integration.android.IntentIntegrator
|
import com.google.zxing.integration.android.IntentIntegrator
|
||||||
import com.journeyapps.barcodescanner.CaptureActivity
|
|
||||||
|
|
||||||
class AddSourceOptionsActivity : AppCompatActivity() {
|
class AddSourceOptionsActivity : AppCompatActivity() {
|
||||||
lateinit var _buttonBack: ImageButton;
|
lateinit var _buttonBack: ImageButton;
|
||||||
|
|
||||||
lateinit var _buttonQR: BigButton;
|
lateinit var _buttonQR: BigButton;
|
||||||
|
lateinit var _buttonBrowse: BigButton;
|
||||||
lateinit var _buttonURL: BigButton;
|
lateinit var _buttonURL: BigButton;
|
||||||
lateinit var _buttonPlugins: BigButton;
|
lateinit var _buttonPlugins: BigButton;
|
||||||
|
|
||||||
@@ -43,6 +45,10 @@ class AddSourceOptionsActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
|
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.activity_add_source_options);
|
setContentView(R.layout.activity_add_source_options);
|
||||||
@@ -51,6 +57,7 @@ class AddSourceOptionsActivity : AppCompatActivity() {
|
|||||||
_buttonBack = findViewById(R.id.button_back);
|
_buttonBack = findViewById(R.id.button_back);
|
||||||
|
|
||||||
_buttonQR = findViewById(R.id.option_qr);
|
_buttonQR = findViewById(R.id.option_qr);
|
||||||
|
_buttonBrowse = findViewById(R.id.option_browse);
|
||||||
_buttonURL = findViewById(R.id.option_url);
|
_buttonURL = findViewById(R.id.option_url);
|
||||||
_buttonPlugins = findViewById(R.id.option_plugins);
|
_buttonPlugins = findViewById(R.id.option_plugins);
|
||||||
|
|
||||||
@@ -69,6 +76,9 @@ class AddSourceOptionsActivity : AppCompatActivity() {
|
|||||||
integrator.setCaptureActivity(QRCaptureActivity::class.java);
|
integrator.setCaptureActivity(QRCaptureActivity::class.java);
|
||||||
_qrCodeResultLauncher.launch(integrator.createScanIntent())
|
_qrCodeResultLauncher.launch(integrator.createScanIntent())
|
||||||
}
|
}
|
||||||
|
_buttonBrowse.onClick.subscribe {
|
||||||
|
startActivity(MainActivity.getTabIntent(this, "BROWSE_PLUGINS"));
|
||||||
|
}
|
||||||
|
|
||||||
_buttonURL.onClick.subscribe {
|
_buttonURL.onClick.subscribe {
|
||||||
UIDialogs.toast(this, getString(R.string.not_implemented_yet));
|
UIDialogs.toast(this, getString(R.string.not_implemented_yet));
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import com.futo.platformplayer.engine.IV8PluginConfig
|
|||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.others.CaptchaWebViewClient
|
import com.futo.platformplayer.others.CaptchaWebViewClient
|
||||||
import com.futo.platformplayer.others.LoginWebViewClient
|
import com.futo.platformplayer.others.LoginWebViewClient
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
@@ -31,6 +32,10 @@ class CaptchaActivity : AppCompatActivity() {
|
|||||||
private lateinit var _webView: WebView;
|
private lateinit var _webView: WebView;
|
||||||
private lateinit var _buttonClose: Button;
|
private lateinit var _buttonClose: Button;
|
||||||
|
|
||||||
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
|
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.activity_captcha);
|
setContentView(R.layout.activity_captcha);
|
||||||
|
|||||||
@@ -1,17 +1,24 @@
|
|||||||
package com.futo.platformplayer.activities
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
import com.futo.platformplayer.views.fields.FieldForm
|
import com.futo.platformplayer.views.fields.FieldForm
|
||||||
|
import com.futo.platformplayer.views.fields.IField
|
||||||
|
|
||||||
class DeveloperActivity : AppCompatActivity() {
|
class DeveloperActivity : AppCompatActivity() {
|
||||||
private lateinit var _form: FieldForm;
|
private lateinit var _form: FieldForm;
|
||||||
private lateinit var _buttonBack: ImageButton;
|
private lateinit var _buttonBack: ImageButton;
|
||||||
|
|
||||||
|
fun getField(id: String): IField? {
|
||||||
|
return _form.findField(id);
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
DeveloperActivity._lastActivity = this;
|
||||||
setContentView(R.layout.activity_dev);
|
setContentView(R.layout.activity_dev);
|
||||||
setNavigationBarColorAndIcons();
|
setNavigationBarColorAndIcons();
|
||||||
|
|
||||||
@@ -19,7 +26,7 @@ class DeveloperActivity : AppCompatActivity() {
|
|||||||
_form = findViewById(R.id.settings_form);
|
_form = findViewById(R.id.settings_form);
|
||||||
|
|
||||||
_form.fromObject(SettingsDev.instance);
|
_form.fromObject(SettingsDev.instance);
|
||||||
_form.onChanged.subscribe { field, value ->
|
_form.onChanged.subscribe { _, _ ->
|
||||||
_form.setObjectValues();
|
_form.setObjectValues();
|
||||||
SettingsDev.instance.save();
|
SettingsDev.instance.save();
|
||||||
};
|
};
|
||||||
@@ -33,4 +40,19 @@ class DeveloperActivity : AppCompatActivity() {
|
|||||||
super.finish()
|
super.finish()
|
||||||
overridePendingTransition(R.anim.slide_lighten, R.anim.slide_out_up)
|
overridePendingTransition(R.anim.slide_lighten, R.anim.slide_out_up)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
//TODO: Temporary for solving Settings issues
|
||||||
|
@SuppressLint("StaticFieldLeak")
|
||||||
|
private var _lastActivity: DeveloperActivity? = null;
|
||||||
|
|
||||||
|
fun getActivity(): DeveloperActivity? {
|
||||||
|
val act = _lastActivity;
|
||||||
|
if(act != null)
|
||||||
|
return act;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,22 @@
|
|||||||
package com.futo.platformplayer.activities
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
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.LogLevel
|
||||||
import com.futo.platformplayer.logging.Logging
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -24,9 +30,14 @@ class ExceptionActivity : AppCompatActivity() {
|
|||||||
private lateinit var _buttonSubmit: LinearLayout;
|
private lateinit var _buttonSubmit: LinearLayout;
|
||||||
private lateinit var _buttonRestart: LinearLayout;
|
private lateinit var _buttonRestart: LinearLayout;
|
||||||
private lateinit var _buttonClose: LinearLayout;
|
private lateinit var _buttonClose: LinearLayout;
|
||||||
|
private lateinit var _buttonCheckForUpdates: LinearLayout;
|
||||||
private var _file: File? = null;
|
private var _file: File? = null;
|
||||||
private var _submitted = false;
|
private var _submitted = false;
|
||||||
|
|
||||||
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
|
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.activity_exception);
|
setContentView(R.layout.activity_exception);
|
||||||
@@ -37,6 +48,7 @@ class ExceptionActivity : AppCompatActivity() {
|
|||||||
_buttonSubmit = findViewById(R.id.button_submit);
|
_buttonSubmit = findViewById(R.id.button_submit);
|
||||||
_buttonRestart = findViewById(R.id.button_restart);
|
_buttonRestart = findViewById(R.id.button_restart);
|
||||||
_buttonClose = findViewById(R.id.button_close);
|
_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 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);
|
val stack = intent.getStringExtra(EXTRA_STACK) ?: getString(R.string.something_went_wrong_missing_stack_trace);
|
||||||
@@ -75,6 +87,17 @@ class ExceptionActivity : AppCompatActivity() {
|
|||||||
_buttonClose.setOnClickListener {
|
_buttonClose.setOnClickListener {
|
||||||
finish();
|
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() {
|
private fun submitFile() {
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.Html
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
|
import com.futo.platformplayer.dialogs.CastingHelpDialog
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.futo.platformplayer.views.buttons.BigButton
|
||||||
|
|
||||||
|
class FCastGuideActivity : AppCompatActivity() {
|
||||||
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
|
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_fcast_guide);
|
||||||
|
setNavigationBarColorAndIcons();
|
||||||
|
|
||||||
|
findViewById<TextView>(R.id.text_explanation).apply {
|
||||||
|
val guideText = """
|
||||||
|
<h3>1. Install FCast Receiver:</h3>
|
||||||
|
<p>- Open Play Store, FireStore, or FCast website on your TV/desktop.<br>
|
||||||
|
- Search for "FCast Receiver", install and open it.</p>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<h3>2. Prepare the Grayjay App:</h3>
|
||||||
|
<p>- Ensure it's connected to the same network as the FCast Receiver.</p>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<h3>3. Initiate Casting from Grayjay:</h3>
|
||||||
|
<p>- Click the cast button in Grayjay.</p>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<h3>4. Connect to FCast Receiver:</h3>
|
||||||
|
<p>- Wait for your device to show in the list or add it manually with its IP address.</p>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<h3>5. Confirm Connection:</h3>
|
||||||
|
<p>- Click "OK" to confirm your device selection.</p>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<h3>6. Start Casting:</h3>
|
||||||
|
<p>- Press "start" next to the device you've added.</p>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<h3>7. Play Your Video:</h3>
|
||||||
|
<p>- Start any video in Grayjay to cast.</p>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<h3>Finding Your IP Address:</h3>
|
||||||
|
<p><b>On FCast Receiver (Android):</b> Displayed on the main screen.<br>
|
||||||
|
<b>On Windows:</b> Use 'ipconfig' in Command Prompt.<br>
|
||||||
|
<b>On Linux:</b> Use 'hostname -I' or 'ip addr' in Terminal.<br>
|
||||||
|
<b>On MacOS:</b> System Preferences > Network.</p>
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
text = Html.fromHtml(guideText, Html.FROM_HTML_MODE_COMPACT)
|
||||||
|
}
|
||||||
|
|
||||||
|
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
|
||||||
|
UIDialogs.showCastingTutorialDialog(this)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
findViewById<BigButton>(R.id.button_close).onClick.subscribe {
|
||||||
|
UIDialogs.showCastingTutorialDialog(this)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
findViewById<BigButton>(R.id.button_website).onClick.subscribe {
|
||||||
|
try {
|
||||||
|
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://fcast.org/"))
|
||||||
|
startActivity(browserIntent);
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.i(TAG, "Failed to open browser.", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
findViewById<BigButton>(R.id.button_technical).onClick.subscribe {
|
||||||
|
try {
|
||||||
|
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://gitlab.com/futo-org/fcast/-/wikis/Protocol-version-1"))
|
||||||
|
startActivity(browserIntent);
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.i(TAG, "Failed to open browser.", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBackPressed() {
|
||||||
|
UIDialogs.showCastingTutorialDialog(this)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "FCastGuideActivity";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@ package com.futo.platformplayer.activities
|
|||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import androidx.activity.result.ActivityResult
|
import androidx.activity.result.ActivityResult
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
|
||||||
|
|
||||||
interface IWithResultLauncher {
|
interface IWithResultLauncher {
|
||||||
fun launchForResult(intent: Intent, code: Int, handler: (ActivityResult)->Unit);
|
fun launchForResult(intent: Intent, code: Int, handler: (ActivityResult)->Unit);
|
||||||
|
|||||||
@@ -3,23 +3,23 @@ package com.futo.platformplayer.activities
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.webkit.ConsoleMessage
|
|
||||||
import android.webkit.CookieManager
|
import android.webkit.CookieManager
|
||||||
import android.webkit.WebChromeClient
|
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.*
|
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.SourceAuth
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginAuthConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginAuthConfig
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.others.LoginWebViewClient
|
import com.futo.platformplayer.others.LoginWebViewClient
|
||||||
|
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
@@ -28,6 +28,9 @@ class LoginActivity : AppCompatActivity() {
|
|||||||
private lateinit var _textUrl: TextView;
|
private lateinit var _textUrl: TextView;
|
||||||
private lateinit var _buttonClose: ImageButton;
|
private lateinit var _buttonClose: ImageButton;
|
||||||
|
|
||||||
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
|
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
@@ -37,6 +40,7 @@ class LoginActivity : AppCompatActivity() {
|
|||||||
_textUrl = findViewById(R.id.text_url);
|
_textUrl = findViewById(R.id.text_url);
|
||||||
_buttonClose = findViewById(R.id.button_close);
|
_buttonClose = findViewById(R.id.button_close);
|
||||||
_buttonClose.setOnClickListener {
|
_buttonClose.setOnClickListener {
|
||||||
|
UIDialogs.toast("Login cancelled", false);
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +102,7 @@ class LoginActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
override fun finish() {
|
override fun finish() {
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
_webView?.loadUrl("about:blank");
|
_webView.loadUrl("about:blank");
|
||||||
}
|
}
|
||||||
_callback?.let {
|
_callback?.let {
|
||||||
_callback = null;
|
_callback = null;
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
package com.futo.platformplayer.activities
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
import android.content.pm.ActivityInfo
|
import android.content.pm.ActivityInfo
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.preference.PreferenceManager
|
import android.util.Log
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
@@ -16,19 +18,20 @@ import androidx.activity.result.ActivityResultLauncher
|
|||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.constraintlayout.motion.widget.MotionLayout
|
import androidx.constraintlayout.motion.widget.MotionLayout
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.core.view.WindowInsetsControllerCompat
|
import androidx.core.view.WindowInsetsControllerCompat
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentContainerView
|
import androidx.fragment.app.FragmentContainerView
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
import com.futo.platformplayer.api.media.PlatformID
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
|
|
||||||
import com.futo.platformplayer.casting.StateCasting
|
import com.futo.platformplayer.casting.StateCasting
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.constructs.Event3
|
|
||||||
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.*
|
import com.futo.platformplayer.fragment.mainactivity.main.*
|
||||||
import com.futo.platformplayer.fragment.mainactivity.topbar.AddTopBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.topbar.AddTopBarFragment
|
||||||
@@ -38,20 +41,24 @@ import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFrag
|
|||||||
import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
|
||||||
import com.futo.platformplayer.listeners.OrientationManager
|
import com.futo.platformplayer.listeners.OrientationManager
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.models.ImportCache
|
||||||
import com.futo.platformplayer.models.UrlVideoWithTime
|
import com.futo.platformplayer.models.UrlVideoWithTime
|
||||||
import com.futo.platformplayer.states.*
|
import com.futo.platformplayer.states.*
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
import com.futo.platformplayer.stores.SubscriptionStorage
|
import com.futo.platformplayer.stores.SubscriptionStorage
|
||||||
import com.futo.platformplayer.stores.v2.ManagedStore
|
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||||
|
import com.futo.platformplayer.views.ToastView
|
||||||
|
import com.futo.polycentric.core.ApiMethods
|
||||||
import com.google.gson.JsonParser
|
import com.google.gson.JsonParser
|
||||||
|
import com.google.zxing.integration.android.IntentIntegrator
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.PrintWriter
|
import java.io.PrintWriter
|
||||||
import java.io.StringWriter
|
import java.io.StringWriter
|
||||||
import java.lang.reflect.InvocationTargetException
|
import java.lang.reflect.InvocationTargetException
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import java.util.concurrent.ConcurrentLinkedQueue
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity, IWithResultLauncher {
|
class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||||
|
|
||||||
@@ -63,6 +70,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
lateinit var rootView : MotionLayout;
|
lateinit var rootView : MotionLayout;
|
||||||
|
|
||||||
private lateinit var _overlayContainer: FrameLayout;
|
private lateinit var _overlayContainer: FrameLayout;
|
||||||
|
private lateinit var _toastView: ToastView;
|
||||||
|
|
||||||
//Segment Containers
|
//Segment Containers
|
||||||
private lateinit var _fragContainerTopBar: FragmentContainerView;
|
private lateinit var _fragContainerTopBar: FragmentContainerView;
|
||||||
@@ -89,11 +97,14 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
lateinit var _fragMainPlaylistSearchResults: PlaylistSearchResultsFragment;
|
lateinit var _fragMainPlaylistSearchResults: PlaylistSearchResultsFragment;
|
||||||
lateinit var _fragMainSuggestions: SuggestionsFragment;
|
lateinit var _fragMainSuggestions: SuggestionsFragment;
|
||||||
lateinit var _fragMainSubscriptions: CreatorsFragment;
|
lateinit var _fragMainSubscriptions: CreatorsFragment;
|
||||||
|
lateinit var _fragMainComments: CommentsFragment;
|
||||||
lateinit var _fragMainSubscriptionsFeed: SubscriptionsFeedFragment;
|
lateinit var _fragMainSubscriptionsFeed: SubscriptionsFeedFragment;
|
||||||
lateinit var _fragMainChannel: ChannelFragment;
|
lateinit var _fragMainChannel: ChannelFragment;
|
||||||
lateinit var _fragMainSources: SourcesFragment;
|
lateinit var _fragMainSources: SourcesFragment;
|
||||||
|
lateinit var _fragMainTutorial: TutorialFragment;
|
||||||
lateinit var _fragMainPlaylists: PlaylistsFragment;
|
lateinit var _fragMainPlaylists: PlaylistsFragment;
|
||||||
lateinit var _fragMainPlaylist: PlaylistFragment;
|
lateinit var _fragMainPlaylist: PlaylistFragment;
|
||||||
|
lateinit var _fragMainRemotePlaylist: RemotePlaylistFragment;
|
||||||
lateinit var _fragWatchlist: WatchLaterFragment;
|
lateinit var _fragWatchlist: WatchLaterFragment;
|
||||||
lateinit var _fragHistory: HistoryFragment;
|
lateinit var _fragHistory: HistoryFragment;
|
||||||
lateinit var _fragSourceDetail: SourceDetailFragment;
|
lateinit var _fragSourceDetail: SourceDetailFragment;
|
||||||
@@ -101,6 +112,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
lateinit var _fragImportSubscriptions: ImportSubscriptionsFragment;
|
lateinit var _fragImportSubscriptions: ImportSubscriptionsFragment;
|
||||||
lateinit var _fragImportPlaylists: ImportPlaylistsFragment;
|
lateinit var _fragImportPlaylists: ImportPlaylistsFragment;
|
||||||
lateinit var _fragBuy: BuyFragment;
|
lateinit var _fragBuy: BuyFragment;
|
||||||
|
lateinit var _fragSubGroup: SubscriptionGroupFragment;
|
||||||
|
lateinit var _fragSubGroupList: SubscriptionGroupListFragment;
|
||||||
|
|
||||||
lateinit var _fragBrowser: BrowserFragment;
|
lateinit var _fragBrowser: BrowserFragment;
|
||||||
|
|
||||||
@@ -122,7 +135,29 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
private var _isVisible = true;
|
private var _isVisible = true;
|
||||||
private var _wasStopped = false;
|
private var _wasStopped = false;
|
||||||
|
|
||||||
|
private val _urlQrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
|
val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
|
||||||
|
scanResult?.let {
|
||||||
|
val content = it.contents
|
||||||
|
if (content == null) {
|
||||||
|
UIDialogs.toast(this, getString(R.string.failed_to_scan_qr_code))
|
||||||
|
return@let
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
runBlocking {
|
||||||
|
handleUrlAll(content)
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.i(TAG, "Failed to handle URL.", e)
|
||||||
|
UIDialogs.toast(this, "Failed to handle URL: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
constructor() : super() {
|
constructor() : super() {
|
||||||
|
ApiMethods.UserAgent = "Grayjay Android (${BuildConfig.VERSION_CODE})";
|
||||||
|
|
||||||
Thread.setDefaultUncaughtExceptionHandler { _, throwable ->
|
Thread.setDefaultUncaughtExceptionHandler { _, throwable ->
|
||||||
val writer = StringWriter();
|
val writer = StringWriter();
|
||||||
|
|
||||||
@@ -161,6 +196,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
Logger.i(TAG, "MainActivity Starting");
|
||||||
StateApp.instance.setGlobalContext(this, lifecycleScope);
|
StateApp.instance.setGlobalContext(this, lifecycleScope);
|
||||||
StateApp.instance.mainAppStarting(this);
|
StateApp.instance.mainAppStarting(this);
|
||||||
|
|
||||||
@@ -183,7 +219,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
_fragContainerVideoDetail = findViewById(R.id.fragment_overlay);
|
_fragContainerVideoDetail = findViewById(R.id.fragment_overlay);
|
||||||
_fragContainerOverlay = findViewById(R.id.fragment_overlay_container);
|
_fragContainerOverlay = findViewById(R.id.fragment_overlay_container);
|
||||||
_overlayContainer = findViewById(R.id.overlay_container);
|
_overlayContainer = findViewById(R.id.overlay_container);
|
||||||
//_overlayContainer.visibility = View.GONE;
|
_toastView = findViewById(R.id.toast_view);
|
||||||
|
|
||||||
//Initialize fragments
|
//Initialize fragments
|
||||||
|
|
||||||
@@ -199,16 +235,19 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
|
|
||||||
//Main
|
//Main
|
||||||
_fragMainHome = HomeFragment.newInstance();
|
_fragMainHome = HomeFragment.newInstance();
|
||||||
|
_fragMainTutorial = TutorialFragment.newInstance()
|
||||||
_fragMainSuggestions = SuggestionsFragment.newInstance();
|
_fragMainSuggestions = SuggestionsFragment.newInstance();
|
||||||
_fragMainVideoSearchResults = ContentSearchResultsFragment.newInstance();
|
_fragMainVideoSearchResults = ContentSearchResultsFragment.newInstance();
|
||||||
_fragMainCreatorSearchResults = CreatorSearchResultsFragment.newInstance();
|
_fragMainCreatorSearchResults = CreatorSearchResultsFragment.newInstance();
|
||||||
_fragMainPlaylistSearchResults = PlaylistSearchResultsFragment.newInstance();
|
_fragMainPlaylistSearchResults = PlaylistSearchResultsFragment.newInstance();
|
||||||
_fragMainSubscriptions = CreatorsFragment.newInstance();
|
_fragMainSubscriptions = CreatorsFragment.newInstance();
|
||||||
|
_fragMainComments = CommentsFragment.newInstance();
|
||||||
_fragMainChannel = ChannelFragment.newInstance();
|
_fragMainChannel = ChannelFragment.newInstance();
|
||||||
_fragMainSubscriptionsFeed = SubscriptionsFeedFragment.newInstance();
|
_fragMainSubscriptionsFeed = SubscriptionsFeedFragment.newInstance();
|
||||||
_fragMainSources = SourcesFragment.newInstance();
|
_fragMainSources = SourcesFragment.newInstance();
|
||||||
_fragMainPlaylists = PlaylistsFragment.newInstance();
|
_fragMainPlaylists = PlaylistsFragment.newInstance();
|
||||||
_fragMainPlaylist = PlaylistFragment.newInstance();
|
_fragMainPlaylist = PlaylistFragment.newInstance();
|
||||||
|
_fragMainRemotePlaylist = RemotePlaylistFragment.newInstance();
|
||||||
_fragPostDetail = PostDetailFragment.newInstance();
|
_fragPostDetail = PostDetailFragment.newInstance();
|
||||||
_fragWatchlist = WatchLaterFragment.newInstance();
|
_fragWatchlist = WatchLaterFragment.newInstance();
|
||||||
_fragHistory = HistoryFragment.newInstance();
|
_fragHistory = HistoryFragment.newInstance();
|
||||||
@@ -217,6 +256,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
_fragImportSubscriptions = ImportSubscriptionsFragment.newInstance();
|
_fragImportSubscriptions = ImportSubscriptionsFragment.newInstance();
|
||||||
_fragImportPlaylists = ImportPlaylistsFragment.newInstance();
|
_fragImportPlaylists = ImportPlaylistsFragment.newInstance();
|
||||||
_fragBuy = BuyFragment.newInstance();
|
_fragBuy = BuyFragment.newInstance();
|
||||||
|
_fragSubGroup = SubscriptionGroupFragment.newInstance();
|
||||||
|
_fragSubGroupList = SubscriptionGroupListFragment.newInstance();
|
||||||
|
|
||||||
_fragBrowser = BrowserFragment.newInstance();
|
_fragBrowser = BrowserFragment.newInstance();
|
||||||
|
|
||||||
@@ -281,15 +322,18 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
//Set top bars
|
//Set top bars
|
||||||
_fragMainHome.topBar = _fragTopBarGeneral;
|
_fragMainHome.topBar = _fragTopBarGeneral;
|
||||||
_fragMainSubscriptions.topBar = _fragTopBarGeneral;
|
_fragMainSubscriptions.topBar = _fragTopBarGeneral;
|
||||||
|
_fragMainComments.topBar = _fragTopBarGeneral;
|
||||||
_fragMainSuggestions.topBar = _fragTopBarSearch;
|
_fragMainSuggestions.topBar = _fragTopBarSearch;
|
||||||
_fragMainVideoSearchResults.topBar = _fragTopBarSearch;
|
_fragMainVideoSearchResults.topBar = _fragTopBarSearch;
|
||||||
_fragMainCreatorSearchResults.topBar = _fragTopBarSearch;
|
_fragMainCreatorSearchResults.topBar = _fragTopBarSearch;
|
||||||
_fragMainPlaylistSearchResults.topBar = _fragTopBarSearch;
|
_fragMainPlaylistSearchResults.topBar = _fragTopBarSearch;
|
||||||
_fragMainChannel.topBar = _fragTopBarNavigation;
|
_fragMainChannel.topBar = _fragTopBarNavigation;
|
||||||
|
_fragMainTutorial.topBar = _fragTopBarNavigation;
|
||||||
_fragMainSubscriptionsFeed.topBar = _fragTopBarGeneral;
|
_fragMainSubscriptionsFeed.topBar = _fragTopBarGeneral;
|
||||||
_fragMainSources.topBar = _fragTopBarAdd;
|
_fragMainSources.topBar = _fragTopBarAdd;
|
||||||
_fragMainPlaylists.topBar = _fragTopBarGeneral;
|
_fragMainPlaylists.topBar = _fragTopBarGeneral;
|
||||||
_fragMainPlaylist.topBar = _fragTopBarNavigation;
|
_fragMainPlaylist.topBar = _fragTopBarNavigation;
|
||||||
|
_fragMainRemotePlaylist.topBar = _fragTopBarNavigation;
|
||||||
_fragPostDetail.topBar = _fragTopBarNavigation;
|
_fragPostDetail.topBar = _fragTopBarNavigation;
|
||||||
_fragWatchlist.topBar = _fragTopBarNavigation;
|
_fragWatchlist.topBar = _fragTopBarNavigation;
|
||||||
_fragHistory.topBar = _fragTopBarNavigation;
|
_fragHistory.topBar = _fragTopBarNavigation;
|
||||||
@@ -297,9 +341,10 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
_fragDownloads.topBar = _fragTopBarGeneral;
|
_fragDownloads.topBar = _fragTopBarGeneral;
|
||||||
_fragImportSubscriptions.topBar = _fragTopBarImport;
|
_fragImportSubscriptions.topBar = _fragTopBarImport;
|
||||||
_fragImportPlaylists.topBar = _fragTopBarImport;
|
_fragImportPlaylists.topBar = _fragTopBarImport;
|
||||||
|
_fragSubGroupList.topBar = _fragTopBarAdd;
|
||||||
|
|
||||||
_fragBrowser.topBar = _fragTopBarNavigation;
|
_fragBrowser.topBar = _fragTopBarNavigation;
|
||||||
|
|
||||||
fragCurrent = _fragMainHome;
|
fragCurrent = _fragMainHome;
|
||||||
|
|
||||||
val defaultTab = Settings.instance.tabs.mapNotNull {
|
val defaultTab = Settings.instance.tabs.mapNotNull {
|
||||||
@@ -327,6 +372,15 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
fragCurrent.onOrientationChanged(it);
|
fragCurrent.onOrientationChanged(it);
|
||||||
if(_fragVideoDetail.state == VideoDetailFragment.State.MAXIMIZED)
|
if(_fragVideoDetail.state == VideoDetailFragment.State.MAXIMIZED)
|
||||||
_fragVideoDetail.onOrientationChanged(it);
|
_fragVideoDetail.onOrientationChanged(it);
|
||||||
|
else if(Settings.instance.other.bypassRotationPrevention)
|
||||||
|
{
|
||||||
|
requestedOrientation = when(orientation) {
|
||||||
|
OrientationManager.Orientation.PORTRAIT -> ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||||
|
OrientationManager.Orientation.LANDSCAPE -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||||
|
OrientationManager.Orientation.REVERSED_PORTRAIT -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT
|
||||||
|
OrientationManager.Orientation.REVERSED_LANDSCAPE -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
_orientationManager.enable();
|
_orientationManager.enable();
|
||||||
|
|
||||||
@@ -372,6 +426,16 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
StateApp.instance.mainAppStartedWithExternalFiles(this);
|
StateApp.instance.mainAppStartedWithExternalFiles(this);
|
||||||
|
|
||||||
//startActivity(Intent(this, TestActivity::class.java));
|
//startActivity(Intent(this, TestActivity::class.java));
|
||||||
|
|
||||||
|
val sharedPreferences = getSharedPreferences("GrayjayFirstBoot", Context.MODE_PRIVATE)
|
||||||
|
val isFirstBoot = sharedPreferences.getBoolean("IsFirstBoot", true)
|
||||||
|
if (isFirstBoot) {
|
||||||
|
UIDialogs.showConfirmationDialog(this, getString(R.string.do_you_want_to_see_the_tutorials_you_can_find_them_at_any_time_through_the_more_button), {
|
||||||
|
navigate(_fragMainTutorial)
|
||||||
|
})
|
||||||
|
|
||||||
|
sharedPreferences.edit().putBoolean("IsFirstBoot", false).apply()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -396,6 +460,23 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
UIDialogs.toast(this, "No external file permissions\nExporting and auto backups will not work");
|
UIDialogs.toast(this, "No external file permissions\nExporting and auto backups will not work");
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
|
fun showUrlQrCodeScanner() {
|
||||||
|
try {
|
||||||
|
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.captureActivity = QRCaptureActivity::class.java
|
||||||
|
_urlQrCodeResultLauncher.launch(integrator.createScanIntent())
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.i(TAG, "Failed to handle show QR scanner.", e)
|
||||||
|
UIDialogs.toast(this, "Failed to show QR scanner: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
Logger.v(TAG, "onResume")
|
Logger.v(TAG, "onResume")
|
||||||
@@ -411,21 +492,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_isVisible = true;
|
_isVisible = true;
|
||||||
val videoToOpen = StateSaved.instance.videoToOpen;
|
|
||||||
|
|
||||||
if (_wasStopped) {
|
|
||||||
_wasStopped = false;
|
|
||||||
|
|
||||||
if (videoToOpen != null && _fragVideoDetail.state == VideoDetailFragment.State.CLOSED) {
|
|
||||||
Logger.i(TAG, "onResume videoToOpen=$videoToOpen");
|
|
||||||
if (StatePlatform.instance.hasEnabledVideoClient(videoToOpen.url)) {
|
|
||||||
navigate(_fragVideoDetail, UrlVideoWithTime(videoToOpen.url, videoToOpen.timeSeconds, false));
|
|
||||||
_fragVideoDetail.maximizeVideoDetail(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
StateSaved.instance.setVideoToOpenNonBlocking(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
@@ -469,6 +535,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
val url = intent.getStringExtra("VIDEO");
|
val url = intent.getStringExtra("VIDEO");
|
||||||
navigate(_fragVideoDetail, url);
|
navigate(_fragVideoDetail, url);
|
||||||
}
|
}
|
||||||
|
"IMPORT_OPTIONS" -> {
|
||||||
|
UIDialogs.showImportOptionsDialog(this);
|
||||||
|
}
|
||||||
"TAB" -> {
|
"TAB" -> {
|
||||||
when(intent.getStringExtra("TAB")){
|
when(intent.getStringExtra("TAB")){
|
||||||
"Sources" -> {
|
"Sources" -> {
|
||||||
@@ -477,81 +546,27 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
navigate(_fragMainSources);
|
navigate(_fragMainSources);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
"BROWSE_PLUGINS" -> {
|
||||||
|
navigate(_fragBrowser, BrowserFragment.NavigateOptions("https://plugins.grayjay.app/phone.html", mapOf(
|
||||||
|
Pair("grayjay") { req ->
|
||||||
|
StateApp.instance.contextOrNull?.let {
|
||||||
|
if(it is MainActivity) {
|
||||||
|
runBlocking {
|
||||||
|
it.handleUrlAll(req.url.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (targetData != null) {
|
if (targetData != null) {
|
||||||
when(intent.scheme) {
|
runBlocking {
|
||||||
"grayjay" -> {
|
handleUrlAll(targetData)
|
||||||
if(targetData.startsWith("grayjay://license/")) {
|
|
||||||
if(StatePayment.instance.setPaymentLicenseUrl(targetData))
|
|
||||||
{
|
|
||||||
UIDialogs.showDialogOk(this, R.drawable.ic_check, getString(R.string.your_license_key_has_been_set_an_app_restart_might_be_required));
|
|
||||||
|
|
||||||
if(fragCurrent is BuyFragment)
|
|
||||||
closeSegment(fragCurrent);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
UIDialogs.toast(getString(R.string.invalid_license_format));
|
|
||||||
|
|
||||||
}
|
|
||||||
else if(targetData.startsWith("grayjay://plugin/")) {
|
|
||||||
val intent = Intent(this, AddSourceActivity::class.java).apply {
|
|
||||||
data = Uri.parse(targetData.substring("grayjay://plugin/".length));
|
|
||||||
};
|
|
||||||
startActivity(intent);
|
|
||||||
}
|
|
||||||
else if(targetData.startsWith("grayjay://video/")) {
|
|
||||||
val videoUrl = targetData.substring("grayjay://video/".length);
|
|
||||||
navigate(_fragVideoDetail, videoUrl);
|
|
||||||
}
|
|
||||||
else if(targetData.startsWith("grayjay://channel/")) {
|
|
||||||
val channelUrl = targetData.substring("grayjay://channel/".length);
|
|
||||||
navigate(_fragMainChannel, channelUrl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"content" -> {
|
|
||||||
if(!handleContent(targetData, intent.type)) {
|
|
||||||
UIDialogs.showSingleButtonDialog(
|
|
||||||
this,
|
|
||||||
R.drawable.ic_play,
|
|
||||||
getString(R.string.unknown_content_format) + " [${targetData}]",
|
|
||||||
"Ok",
|
|
||||||
{ });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"file" -> {
|
|
||||||
if(!handleFile(targetData)) {
|
|
||||||
UIDialogs.showSingleButtonDialog(
|
|
||||||
this,
|
|
||||||
R.drawable.ic_play,
|
|
||||||
getString(R.string.unknown_file_format) + " [${targetData}]",
|
|
||||||
"Ok",
|
|
||||||
{ });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"polycentric" -> {
|
|
||||||
if(!handlePolycentric(targetData)) {
|
|
||||||
UIDialogs.showSingleButtonDialog(
|
|
||||||
this,
|
|
||||||
R.drawable.ic_play,
|
|
||||||
getString(R.string.unknown_polycentric_format) + " [${targetData}]",
|
|
||||||
"Ok",
|
|
||||||
{ });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
if (!handleUrl(targetData)) {
|
|
||||||
UIDialogs.showSingleButtonDialog(
|
|
||||||
this,
|
|
||||||
R.drawable.ic_play,
|
|
||||||
getString(R.string.unknown_url_format) + " [${targetData}]",
|
|
||||||
"Ok",
|
|
||||||
{ });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -560,23 +575,122 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleUrl(url: String): Boolean {
|
suspend fun handleUrlAll(url: String) {
|
||||||
|
val uri = Uri.parse(url)
|
||||||
|
when (uri.scheme) {
|
||||||
|
"grayjay" -> {
|
||||||
|
if(url.startsWith("grayjay://license/")) {
|
||||||
|
if(StatePayment.instance.setPaymentLicenseUrl(url))
|
||||||
|
{
|
||||||
|
UIDialogs.showDialogOk(this, R.drawable.ic_check, getString(R.string.your_license_key_has_been_set_an_app_restart_might_be_required));
|
||||||
|
|
||||||
|
if(fragCurrent is BuyFragment)
|
||||||
|
closeSegment(fragCurrent);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
UIDialogs.toast(getString(R.string.invalid_license_format));
|
||||||
|
|
||||||
|
}
|
||||||
|
else if(url.startsWith("grayjay://plugin/")) {
|
||||||
|
val intent = Intent(this, AddSourceActivity::class.java).apply {
|
||||||
|
data = Uri.parse(url.substring("grayjay://plugin/".length));
|
||||||
|
};
|
||||||
|
startActivity(intent);
|
||||||
|
}
|
||||||
|
else if(url.startsWith("grayjay://video/")) {
|
||||||
|
val videoUrl = url.substring("grayjay://video/".length);
|
||||||
|
navigate(_fragVideoDetail, videoUrl);
|
||||||
|
}
|
||||||
|
else if(url.startsWith("grayjay://channel/")) {
|
||||||
|
val channelUrl = url.substring("grayjay://channel/".length);
|
||||||
|
navigate(_fragMainChannel, channelUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"content" -> {
|
||||||
|
if(!handleContent(url, intent.type)) {
|
||||||
|
UIDialogs.showSingleButtonDialog(
|
||||||
|
this,
|
||||||
|
R.drawable.ic_play,
|
||||||
|
getString(R.string.unknown_content_format) + " [${url}]\n[${intent.type}]",
|
||||||
|
"Ok",
|
||||||
|
{ });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"file" -> {
|
||||||
|
if(!handleFile(url)) {
|
||||||
|
UIDialogs.showSingleButtonDialog(
|
||||||
|
this,
|
||||||
|
R.drawable.ic_play,
|
||||||
|
getString(R.string.unknown_file_format) + " [${url}]",
|
||||||
|
"Ok",
|
||||||
|
{ });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"polycentric" -> {
|
||||||
|
if(!handlePolycentric(url)) {
|
||||||
|
UIDialogs.showSingleButtonDialog(
|
||||||
|
this,
|
||||||
|
R.drawable.ic_play,
|
||||||
|
getString(R.string.unknown_polycentric_format) + " [${url}]",
|
||||||
|
"Ok",
|
||||||
|
{ });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"fcast" -> {
|
||||||
|
if(!handleFCast(url)) {
|
||||||
|
UIDialogs.showSingleButtonDialog(
|
||||||
|
this,
|
||||||
|
R.drawable.ic_cast,
|
||||||
|
"Unknown FCast format [${url}]",
|
||||||
|
"Ok",
|
||||||
|
{ });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
if (!handleUrl(url)) {
|
||||||
|
UIDialogs.showSingleButtonDialog(
|
||||||
|
this,
|
||||||
|
R.drawable.ic_play,
|
||||||
|
getString(R.string.unknown_url_format) + " [${url}]",
|
||||||
|
"Ok",
|
||||||
|
{ });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun handleUrl(url: String): Boolean {
|
||||||
Logger.i(TAG, "handleUrl(url=$url)")
|
Logger.i(TAG, "handleUrl(url=$url)")
|
||||||
|
|
||||||
if (StatePlatform.instance.hasEnabledVideoClient(url)) {
|
return withContext(Dispatchers.IO) {
|
||||||
navigate(_fragVideoDetail, url);
|
Logger.i(TAG, "handleUrl(url=$url) on IO");
|
||||||
_fragVideoDetail.maximizeVideoDetail(true);
|
if (StatePlatform.instance.hasEnabledVideoClient(url)) {
|
||||||
return true;
|
Logger.i(TAG, "handleUrl(url=$url) found video client");
|
||||||
} else if(StatePlatform.instance.hasEnabledChannelClient(url)) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
navigate(_fragMainChannel, url);
|
navigate(_fragVideoDetail, url);
|
||||||
|
|
||||||
lifecycleScope.launch {
|
_fragVideoDetail.maximizeVideoDetail(true);
|
||||||
delay(100);
|
}
|
||||||
_fragVideoDetail.minimizeVideoDetail();
|
return@withContext true;
|
||||||
};
|
} else if (StatePlatform.instance.hasEnabledChannelClient(url)) {
|
||||||
return true;
|
Logger.i(TAG, "handleUrl(url=$url) found channel client");
|
||||||
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
navigate(_fragMainChannel, url);
|
||||||
|
delay(100);
|
||||||
|
_fragVideoDetail.minimizeVideoDetail();
|
||||||
|
};
|
||||||
|
return@withContext true;
|
||||||
|
} else if (StatePlatform.instance.hasEnabledPlaylistClient(url)) {
|
||||||
|
Logger.i(TAG, "handleUrl(url=$url) found playlist client");
|
||||||
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
navigate(_fragMainPlaylist, url);
|
||||||
|
delay(100);
|
||||||
|
_fragVideoDetail.minimizeVideoDetail();
|
||||||
|
};
|
||||||
|
return@withContext true;
|
||||||
|
}
|
||||||
|
return@withContext false;
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
fun handleContent(file: String, mime: String? = null): Boolean {
|
fun handleContent(file: String, mime: String? = null): Boolean {
|
||||||
Logger.i(TAG, "handleContent(url=$file)");
|
Logger.i(TAG, "handleContent(url=$file)");
|
||||||
@@ -585,12 +699,24 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
if(file.lowercase().endsWith(".json") || mime == "application/json") {
|
if(file.lowercase().endsWith(".json") || mime == "application/json") {
|
||||||
var recon = String(data);
|
var recon = String(data);
|
||||||
if(!recon.trim().startsWith("["))
|
if(!recon.trim().startsWith("["))
|
||||||
return handleUnknownJson(file, recon);
|
return handleUnknownJson(recon);
|
||||||
|
|
||||||
|
var reconLines = Json.decodeFromString<List<String>>(recon);
|
||||||
|
val cacheStr = reconLines.find { it.startsWith("__CACHE:") }?.substring("__CACHE:".length);
|
||||||
|
reconLines = reconLines.filter { !it.startsWith("__CACHE:") }; //TODO: constant prefix
|
||||||
|
var cache: ImportCache? = null;
|
||||||
|
try {
|
||||||
|
if(cacheStr != null)
|
||||||
|
cache = Json.decodeFromString(cacheStr);
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to deserialize cache");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
val reconLines = Json.decodeFromString<List<String>>(recon);
|
|
||||||
recon = reconLines.joinToString("\n");
|
recon = reconLines.joinToString("\n");
|
||||||
Logger.i(TAG, "Opened shared playlist reconstruction\n${recon}");
|
Logger.i(TAG, "Opened shared playlist reconstruction\n${recon}");
|
||||||
handleReconstruction(recon);
|
handleReconstruction(recon, cache);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else if(file.lowercase().endsWith(".zip") || mime == "application/zip") {
|
else if(file.lowercase().endsWith(".zip") || mime == "application/zip") {
|
||||||
@@ -605,12 +731,25 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
fun handleFile(file: String): Boolean {
|
fun handleFile(file: String): Boolean {
|
||||||
Logger.i(TAG, "handleFile(url=$file)");
|
Logger.i(TAG, "handleFile(url=$file)");
|
||||||
if(file.lowercase().endsWith(".json")) {
|
if(file.lowercase().endsWith(".json")) {
|
||||||
val recon = String(readSharedFile(file));
|
var recon = String(readSharedFile(file));
|
||||||
if(!recon.startsWith("["))
|
if(!recon.startsWith("["))
|
||||||
return handleUnknownJson(file, recon);
|
return handleUnknownJson(recon);
|
||||||
|
|
||||||
|
var reconLines = Json.decodeFromString<List<String>>(recon);
|
||||||
|
val cacheStr = reconLines.find { it.startsWith("__CACHE:") }?.substring("__CACHE:".length);
|
||||||
|
reconLines = reconLines.filter { !it.startsWith("__CACHE:") }; //TODO: constant prefix
|
||||||
|
var cache: ImportCache? = null;
|
||||||
|
try {
|
||||||
|
if(cacheStr != null)
|
||||||
|
cache = Json.decodeFromString(cacheStr);
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to deserialize cache");
|
||||||
|
}
|
||||||
|
recon = reconLines.joinToString("\n");
|
||||||
|
|
||||||
Logger.i(TAG, "Opened shared playlist reconstruction\n${recon}");
|
Logger.i(TAG, "Opened shared playlist reconstruction\n${recon}");
|
||||||
handleReconstruction(recon);
|
handleReconstruction(recon, cache);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else if(file.lowercase().endsWith(".zip")) {
|
else if(file.lowercase().endsWith(".zip")) {
|
||||||
@@ -622,7 +761,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
fun handleReconstruction(recon: String) {
|
fun handleReconstruction(recon: String, cache: ImportCache? = null) {
|
||||||
val type = ManagedStore.getReconstructionIdentifier(recon);
|
val type = ManagedStore.getReconstructionIdentifier(recon);
|
||||||
val store: ManagedStore<*> = when(type) {
|
val store: ManagedStore<*> = when(type) {
|
||||||
"Playlist" -> StatePlaylists.instance.playlistStore
|
"Playlist" -> StatePlaylists.instance.playlistStore
|
||||||
@@ -639,7 +778,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
|
|
||||||
|
|
||||||
if(!type.isNullOrEmpty()) {
|
if(!type.isNullOrEmpty()) {
|
||||||
UIDialogs.showImportDialog(this, store, name, listOf(recon)) {
|
UIDialogs.showImportDialog(this, store, name, listOf(recon), cache) {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -659,7 +798,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
fun handleUnknownJson(name: String?, json: String): Boolean {
|
fun handleUnknownJson(json: String): Boolean {
|
||||||
|
|
||||||
val context = this;
|
val context = this;
|
||||||
|
|
||||||
@@ -669,18 +808,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
if (!newPipeSubsParsed.has("subscriptions") || !newPipeSubsParsed["subscriptions"].isJsonArray)
|
if (!newPipeSubsParsed.has("subscriptions") || !newPipeSubsParsed["subscriptions"].isJsonArray)
|
||||||
return false;//throw IllegalArgumentException("Invalid NewPipe json structure found");
|
return false;//throw IllegalArgumentException("Invalid NewPipe json structure found");
|
||||||
|
|
||||||
val jsonSubs = newPipeSubsParsed["subscriptions"]
|
StateBackup.importNewPipeSubs(this, newPipeSubsParsed);
|
||||||
val jsonSubsArray = jsonSubs.asJsonArray;
|
|
||||||
val jsonSubsArrayItt = jsonSubsArray.iterator();
|
|
||||||
val subs = mutableListOf<String>()
|
|
||||||
while(jsonSubsArrayItt.hasNext()) {
|
|
||||||
val jsonSubObj = jsonSubsArrayItt.next().asJsonObject;
|
|
||||||
|
|
||||||
if(jsonSubObj.has("url"))
|
|
||||||
subs.add(jsonSubObj["url"].asString);
|
|
||||||
}
|
|
||||||
|
|
||||||
navigate(_fragImportSubscriptions, subs);
|
|
||||||
}
|
}
|
||||||
catch(ex: Exception) {
|
catch(ex: Exception) {
|
||||||
Logger.e(TAG, ex.message, ex);
|
Logger.e(TAG, ex.message, ex);
|
||||||
@@ -706,6 +834,20 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
startActivity(Intent(this, PolycentricImportProfileActivity::class.java).apply { putExtra("url", url) })
|
startActivity(Intent(this, PolycentricImportProfileActivity::class.java).apply { putExtra("url", url) })
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun handleFCast(url: String): Boolean {
|
||||||
|
Logger.i(TAG, "handleFCast");
|
||||||
|
|
||||||
|
try {
|
||||||
|
StateCasting.instance.handleUrl(this, url)
|
||||||
|
return true;
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.e(TAG, "Failed to parse FCast URL '${url}'.", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
private fun readSharedContent(contentPath: String): ByteArray {
|
private fun readSharedContent(contentPath: String): ByteArray {
|
||||||
return contentResolver.openInputStream(Uri.parse(contentPath))?.use {
|
return contentResolver.openInputStream(Uri.parse(contentPath))?.use {
|
||||||
return it.readBytes();
|
return it.readBytes();
|
||||||
@@ -726,11 +868,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
if(_fragBotBarMenu.onBackPressed())
|
if(_fragBotBarMenu.onBackPressed())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if(_fragVideoDetail.state == VideoDetailFragment.State.MAXIMIZED &&
|
if(_fragVideoDetail.state == VideoDetailFragment.State.MAXIMIZED && _fragVideoDetail.onBackPressed())
|
||||||
_fragVideoDetail.onBackPressed())
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
||||||
if(!fragCurrent.onBackPressed())
|
if(!fragCurrent.onBackPressed())
|
||||||
closeSegment();
|
closeSegment();
|
||||||
}
|
}
|
||||||
@@ -765,7 +905,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
|
|
||||||
val isStop: Boolean = lifecycle.currentState == Lifecycle.State.CREATED;
|
val isStop: Boolean = lifecycle.currentState == Lifecycle.State.CREATED;
|
||||||
Logger.v(TAG, "onPictureInPictureModeChanged isInPictureInPictureMode=$isInPictureInPictureMode isStop=$isStop")
|
Logger.v(TAG, "onPictureInPictureModeChanged isInPictureInPictureMode=$isInPictureInPictureMode isStop=$isStop")
|
||||||
_fragVideoDetail?.onPictureInPictureModeChanged(isInPictureInPictureMode, isStop, newConfig);
|
_fragVideoDetail.onPictureInPictureModeChanged(isInPictureInPictureMode, isStop, newConfig);
|
||||||
Logger.v(TAG, "onPictureInPictureModeChanged Ready");
|
Logger.v(TAG, "onPictureInPictureModeChanged Ready");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -776,7 +916,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
_orientationManager.disable();
|
_orientationManager.disable();
|
||||||
|
|
||||||
StateApp.instance.mainAppDestroyed(this);
|
StateApp.instance.mainAppDestroyed(this);
|
||||||
StateSaved.instance.setVideoToOpenBlocking(null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun <reified T> isFragmentActive(): Boolean {
|
inline fun <reified T> isFragmentActive(): Boolean {
|
||||||
@@ -787,6 +926,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
* Navigate takes a MainFragment, and makes them the current main visible view
|
* Navigate takes a MainFragment, and makes them the current main visible view
|
||||||
* A parameter can be provided which becomes available in the onShow of said fragment
|
* A parameter can be provided which becomes available in the onShow of said fragment
|
||||||
*/
|
*/
|
||||||
|
@SuppressLint("CommitTransaction")
|
||||||
fun navigate(segment: MainFragment, parameter: Any? = null, withHistory: Boolean = true, isBack: Boolean = false) {
|
fun navigate(segment: MainFragment, parameter: Any? = null, withHistory: Boolean = true, isBack: Boolean = false) {
|
||||||
Logger.i(TAG, "Navigate to $segment (parameter=$parameter, withHistory=$withHistory, isBack=$isBack)")
|
Logger.i(TAG, "Navigate to $segment (parameter=$parameter, withHistory=$withHistory, isBack=$isBack)")
|
||||||
|
|
||||||
@@ -822,7 +962,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
|
|
||||||
transaction = transaction.replace(R.id.fragment_main, segment);
|
transaction = transaction.replace(R.id.fragment_main, segment);
|
||||||
|
|
||||||
val extraBottomDP = if(_fragVideoDetail.state == VideoDetailFragment.State.MINIMIZED) HEIGHT_VIDEO_MINIMIZED_DP else 0f
|
|
||||||
if (segment.hasBottomBar) {
|
if (segment.hasBottomBar) {
|
||||||
if (!fragCurrent.hasBottomBar)
|
if (!fragCurrent.hasBottomBar)
|
||||||
transaction = transaction.show(_fragBotBarMenu);
|
transaction = transaction.show(_fragBotBarMenu);
|
||||||
@@ -832,13 +971,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
transaction = transaction.hide(_fragBotBarMenu);
|
transaction = transaction.hide(_fragBotBarMenu);
|
||||||
}
|
}
|
||||||
transaction.commitNow();
|
transaction.commitNow();
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
//Special cases
|
|
||||||
if(segment is VideoDetailFragment) {
|
|
||||||
_fragContainerVideoDetail.visibility = View.VISIBLE;
|
|
||||||
_fragVideoDetail.maximizeVideoDetail();
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!segment.hasBottomBar) {
|
if(!segment.hasBottomBar) {
|
||||||
supportFragmentManager.beginTransaction()
|
supportFragmentManager.beginTransaction()
|
||||||
@@ -875,15 +1008,20 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
|
|
||||||
if((fragment?.isOverlay ?: false) && fragBeforeOverlay != null) {
|
if((fragment?.isOverlay ?: false) && fragBeforeOverlay != null) {
|
||||||
navigate(fragBeforeOverlay!!, null, false, true);
|
navigate(fragBeforeOverlay!!, null, false, true);
|
||||||
|
} else {
|
||||||
}
|
|
||||||
else {
|
|
||||||
val last = _queue.lastOrNull();
|
val last = _queue.lastOrNull();
|
||||||
if (last != null) {
|
if (last != null) {
|
||||||
_queue.remove(last);
|
_queue.remove(last);
|
||||||
navigate(last.first, last.second, false, true);
|
navigate(last.first, last.second, false, true);
|
||||||
} else
|
} else {
|
||||||
finish();
|
if (_fragVideoDetail.state == VideoDetailFragment.State.CLOSED) {
|
||||||
|
finish();
|
||||||
|
} else {
|
||||||
|
UIDialogs.showConfirmationDialog(this, "There is a video playing, are you sure you want to exit the app?", {
|
||||||
|
finish();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -893,6 +1031,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
inline fun <reified T : Fragment> getFragment() : T {
|
inline fun <reified T : Fragment> getFragment() : T {
|
||||||
return when(T::class) {
|
return when(T::class) {
|
||||||
HomeFragment::class -> _fragMainHome as T;
|
HomeFragment::class -> _fragMainHome as T;
|
||||||
|
TutorialFragment::class -> _fragMainTutorial as T;
|
||||||
ContentSearchResultsFragment::class -> _fragMainVideoSearchResults as T;
|
ContentSearchResultsFragment::class -> _fragMainVideoSearchResults as T;
|
||||||
CreatorSearchResultsFragment::class -> _fragMainCreatorSearchResults as T;
|
CreatorSearchResultsFragment::class -> _fragMainCreatorSearchResults as T;
|
||||||
SuggestionsFragment::class -> _fragMainSuggestions as T;
|
SuggestionsFragment::class -> _fragMainSuggestions as T;
|
||||||
@@ -901,12 +1040,14 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
GeneralTopBarFragment::class -> _fragTopBarGeneral as T;
|
GeneralTopBarFragment::class -> _fragTopBarGeneral as T;
|
||||||
SearchTopBarFragment::class -> _fragTopBarSearch as T;
|
SearchTopBarFragment::class -> _fragTopBarSearch as T;
|
||||||
CreatorsFragment::class -> _fragMainSubscriptions as T;
|
CreatorsFragment::class -> _fragMainSubscriptions as T;
|
||||||
|
CommentsFragment::class -> _fragMainComments as T;
|
||||||
SubscriptionsFeedFragment::class -> _fragMainSubscriptionsFeed as T;
|
SubscriptionsFeedFragment::class -> _fragMainSubscriptionsFeed as T;
|
||||||
PlaylistSearchResultsFragment::class -> _fragMainPlaylistSearchResults as T;
|
PlaylistSearchResultsFragment::class -> _fragMainPlaylistSearchResults as T;
|
||||||
ChannelFragment::class -> _fragMainChannel as T;
|
ChannelFragment::class -> _fragMainChannel as T;
|
||||||
SourcesFragment::class -> _fragMainSources as T;
|
SourcesFragment::class -> _fragMainSources as T;
|
||||||
PlaylistsFragment::class -> _fragMainPlaylists as T;
|
PlaylistsFragment::class -> _fragMainPlaylists as T;
|
||||||
PlaylistFragment::class -> _fragMainPlaylist as T;
|
PlaylistFragment::class -> _fragMainPlaylist as T;
|
||||||
|
RemotePlaylistFragment::class -> _fragMainRemotePlaylist as T;
|
||||||
PostDetailFragment::class -> _fragPostDetail as T;
|
PostDetailFragment::class -> _fragPostDetail as T;
|
||||||
WatchLaterFragment::class -> _fragWatchlist as T;
|
WatchLaterFragment::class -> _fragWatchlist as T;
|
||||||
HistoryFragment::class -> _fragHistory as T;
|
HistoryFragment::class -> _fragHistory as T;
|
||||||
@@ -916,6 +1057,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
ImportPlaylistsFragment::class -> _fragImportPlaylists as T;
|
ImportPlaylistsFragment::class -> _fragImportPlaylists as T;
|
||||||
BrowserFragment::class -> _fragBrowser as T;
|
BrowserFragment::class -> _fragBrowser as T;
|
||||||
BuyFragment::class -> _fragBuy as T;
|
BuyFragment::class -> _fragBuy as T;
|
||||||
|
SubscriptionGroupFragment::class -> _fragSubGroup as T;
|
||||||
|
SubscriptionGroupListFragment::class -> _fragSubGroupList as T;
|
||||||
else -> throw IllegalArgumentException("Fragment type ${T::class.java.name} is not available in MainActivity");
|
else -> throw IllegalArgumentException("Fragment type ${T::class.java.name} is not available in MainActivity");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -935,6 +1078,70 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
val notifPermission = "android.permission.POST_NOTIFICATIONS";
|
||||||
|
val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
|
||||||
|
if (isGranted)
|
||||||
|
UIDialogs.toast(this, "Notification permission granted");
|
||||||
|
else
|
||||||
|
UIDialogs.toast(this, "Notification permission denied");
|
||||||
|
}
|
||||||
|
fun requestNotificationPermissions(reason: String) {
|
||||||
|
when {
|
||||||
|
ContextCompat.checkSelfPermission(this, notifPermission) == PackageManager.PERMISSION_GRANTED -> {
|
||||||
|
|
||||||
|
}
|
||||||
|
ActivityCompat.shouldShowRequestPermissionRationale(this, notifPermission) -> {
|
||||||
|
UIDialogs.showDialog(this, R.drawable.ic_notifications, "Notifications Required",
|
||||||
|
reason, null, 0,
|
||||||
|
UIDialogs.Action("Cancel", {}),
|
||||||
|
UIDialogs.Action("Enable", {
|
||||||
|
requestPermissionLauncher.launch(notifPermission);
|
||||||
|
}, UIDialogs.ActionStyle.PRIMARY));
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
requestPermissionLauncher.launch(notifPermission);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val _toastQueue = ConcurrentLinkedQueue<ToastView.Toast>();
|
||||||
|
private var _toastJob: Job? = null;
|
||||||
|
fun showAppToast(toast: ToastView.Toast) {
|
||||||
|
synchronized(_toastQueue) {
|
||||||
|
_toastQueue.add(toast);
|
||||||
|
if(_toastJob?.isActive != true)
|
||||||
|
_toastJob = lifecycleScope.launch(Dispatchers.Default) {
|
||||||
|
launchAppToastJob();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private suspend fun launchAppToastJob() {
|
||||||
|
Logger.i(TAG, "Starting appToast loop");
|
||||||
|
while(!_toastQueue.isEmpty()) {
|
||||||
|
val toast = _toastQueue.poll() ?: continue;
|
||||||
|
Logger.i(TAG, "Showing next toast (${toast.msg})");
|
||||||
|
|
||||||
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
if (!_toastView.isVisible) {
|
||||||
|
Logger.i(TAG, "First showing toast");
|
||||||
|
_toastView.setToast(toast);
|
||||||
|
_toastView.show(true);
|
||||||
|
} else {
|
||||||
|
_toastView.setToastAnimated(toast);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(toast.long)
|
||||||
|
delay(5000);
|
||||||
|
else
|
||||||
|
delay(3000);
|
||||||
|
}
|
||||||
|
Logger.i(TAG, "Ending appToast loop");
|
||||||
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
_toastView.hide(true) {
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//TODO: Only calls last handler due to missing request codes on ActivityResultLaunchers.
|
//TODO: Only calls last handler due to missing request codes on ActivityResultLaunchers.
|
||||||
private var resultLauncherMap = mutableMapOf<Int, (ActivityResult)->Unit>();
|
private var resultLauncherMap = mutableMapOf<Int, (ActivityResult)->Unit>();
|
||||||
@@ -973,5 +1180,12 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||||
return sourcesIntent;
|
return sourcesIntent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getImportOptionsIntent(context: Context): Intent {
|
||||||
|
val sourcesIntent = Intent(context, MainActivity::class.java);
|
||||||
|
sourcesIntent.action = "IMPORT_OPTIONS";
|
||||||
|
sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||||
|
return sourcesIntent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.futo.platformplayer.activities
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
@@ -10,6 +11,7 @@ import com.futo.platformplayer.R
|
|||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
|
||||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.views.AnyAdapterView
|
import com.futo.platformplayer.views.AnyAdapterView
|
||||||
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
|
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
|
||||||
import com.futo.platformplayer.views.adapters.ItemMoveCallback
|
import com.futo.platformplayer.views.adapters.ItemMoveCallback
|
||||||
@@ -23,6 +25,10 @@ class ManageTabsActivity : AppCompatActivity() {
|
|||||||
private lateinit var _recyclerTabs: RecyclerView;
|
private lateinit var _recyclerTabs: RecyclerView;
|
||||||
private lateinit var _touchHelper: ItemTouchHelper;
|
private lateinit var _touchHelper: ItemTouchHelper;
|
||||||
|
|
||||||
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
|
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.activity_manage_tabs);
|
setContentView(R.layout.activity_manage_tabs);
|
||||||
@@ -49,10 +55,10 @@ class ManageTabsActivity : AppCompatActivity() {
|
|||||||
Settings.instance.save()
|
Settings.instance.save()
|
||||||
}
|
}
|
||||||
|
|
||||||
val items = Settings.instance.tabs.mapNotNull {
|
val items = ArrayList(Settings.instance.tabs.mapNotNull {
|
||||||
val buttonDefinition = MenuBottomBarFragment.buttonDefinitions.find { d -> it.id == d.id } ?: return@mapNotNull null
|
val buttonDefinition = MenuBottomBarFragment.buttonDefinitions.find { d -> it.id == d.id } ?: return@mapNotNull null
|
||||||
TabViewHolderData(buttonDefinition, it.enabled)
|
TabViewHolderData(buttonDefinition, it.enabled)
|
||||||
};
|
});
|
||||||
|
|
||||||
_listTabs = _recyclerTabs.asAny(items) {
|
_listTabs = _recyclerTabs.asAny(items) {
|
||||||
it.onDragDrop.subscribe { vh ->
|
it.onDragDrop.subscribe { vh ->
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@@ -16,9 +17,15 @@ import androidx.appcompat.app.AppCompatActivity
|
|||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
import com.futo.platformplayer.views.buttons.BigButton
|
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.BarcodeFormat
|
||||||
import com.google.zxing.MultiFormatWriter
|
import com.google.zxing.MultiFormatWriter
|
||||||
import com.google.zxing.common.BitMatrix
|
import com.google.zxing.common.BitMatrix
|
||||||
@@ -33,6 +40,10 @@ class PolycentricBackupActivity : AppCompatActivity() {
|
|||||||
private lateinit var _exportBundle: String;
|
private lateinit var _exportBundle: String;
|
||||||
private lateinit var _textQR: TextView;
|
private lateinit var _textQR: TextView;
|
||||||
|
|
||||||
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
|
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.activity_polycentric_backup);
|
setContentView(R.layout.activity_polycentric_backup);
|
||||||
@@ -59,11 +70,8 @@ class PolycentricBackupActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_buttonShare.onClick.subscribe {
|
_buttonShare.onClick.subscribe {
|
||||||
val shareIntent = Intent(Intent.ACTION_SEND).apply {
|
val shareIntent = Intent(Intent.ACTION_VIEW, Uri.parse(_exportBundle))
|
||||||
type = "text/plain";
|
startActivity(Intent.createChooser(shareIntent, "Share ID"));
|
||||||
putExtra(Intent.EXTRA_TEXT, _exportBundle);
|
|
||||||
}
|
|
||||||
startActivity(Intent.createChooser(shareIntent, getString(R.string.share_text)));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
_buttonCopy.onClick.subscribe {
|
_buttonCopy.onClick.subscribe {
|
||||||
|
|||||||
+18
-4
@@ -1,5 +1,6 @@
|
|||||||
package com.futo.platformplayer.activities
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
@@ -9,14 +10,16 @@ import androidx.appcompat.app.AppCompatActivity
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
|
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
||||||
import com.futo.platformplayer.logging.Logger
|
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.setNavigationBarColorAndIcons
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
import com.futo.polycentric.core.ProcessHandle
|
import com.futo.polycentric.core.ProcessHandle
|
||||||
import com.futo.polycentric.core.Store
|
import com.futo.polycentric.core.Store
|
||||||
import com.futo.polycentric.core.Synchronization
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
@@ -28,6 +31,10 @@ class PolycentricCreateProfileActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private var _creating = false;
|
private var _creating = false;
|
||||||
|
|
||||||
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
|
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.activity_polycentric_create_profile);
|
setContentView(R.layout.activity_polycentric_create_profile);
|
||||||
@@ -64,7 +71,14 @@ class PolycentricCreateProfileActivity : AppCompatActivity() {
|
|||||||
try {
|
try {
|
||||||
processHandle = ProcessHandle.create();
|
processHandle = ProcessHandle.create();
|
||||||
Store.instance.addProcessSecret(processHandle.processSecret);
|
Store.instance.addProcessSecret(processHandle.processSecret);
|
||||||
processHandle.addServer("https://srv1-stg.polycentric.io");
|
|
||||||
|
try {
|
||||||
|
PolycentricStorage.instance.addProcessSecret(processHandle.processSecret)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
processHandle.addServer(PolycentricCache.SERVER);
|
||||||
processHandle.setUsername(username);
|
processHandle.setUsername(username);
|
||||||
StatePolycentric.instance.setProcessHandle(processHandle);
|
StatePolycentric.instance.setProcessHandle(processHandle);
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
@@ -76,7 +90,7 @@ class PolycentricCreateProfileActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
Logger.i(TAG, "Started backfill");
|
Logger.i(TAG, "Started backfill");
|
||||||
processHandle.fullyBackfillServers();
|
processHandle.fullyBackfillServersAnnounceExceptions();
|
||||||
Logger.i(TAG, "Finished backfill");
|
Logger.i(TAG, "Finished backfill");
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, getString(R.string.failed_to_fully_backfill_servers), e);
|
Logger.e(TAG, getString(R.string.failed_to_fully_backfill_servers), e);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.futo.platformplayer.activities
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
@@ -15,6 +16,7 @@ import com.futo.platformplayer.R
|
|||||||
import com.futo.platformplayer.dp
|
import com.futo.platformplayer.dp
|
||||||
import com.futo.platformplayer.selectBestImage
|
import com.futo.platformplayer.selectBestImage
|
||||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
import com.futo.platformplayer.views.buttons.BigButton
|
import com.futo.platformplayer.views.buttons.BigButton
|
||||||
import com.futo.polycentric.core.Store
|
import com.futo.polycentric.core.Store
|
||||||
@@ -27,6 +29,10 @@ class PolycentricHomeActivity : AppCompatActivity() {
|
|||||||
private lateinit var _buttonImportProfile: BigButton;
|
private lateinit var _buttonImportProfile: BigButton;
|
||||||
private lateinit var _layoutButtons: LinearLayout;
|
private lateinit var _layoutButtons: LinearLayout;
|
||||||
|
|
||||||
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
|
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.activity_polycentric_home);
|
setContentView(R.layout.activity_polycentric_home);
|
||||||
|
|||||||
+65
-30
@@ -1,5 +1,6 @@
|
|||||||
package com.futo.platformplayer.activities
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
@@ -11,13 +12,20 @@ import androidx.lifecycle.lifecycleScope
|
|||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.logging.Logger
|
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.setNavigationBarColorAndIcons
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
import com.futo.polycentric.core.*
|
import com.futo.platformplayer.views.overlays.LoaderOverlay
|
||||||
|
import com.futo.polycentric.core.KeyPair
|
||||||
|
import com.futo.polycentric.core.Process
|
||||||
|
import com.futo.polycentric.core.ProcessSecret
|
||||||
|
import com.futo.polycentric.core.SignedEvent
|
||||||
|
import com.futo.polycentric.core.Store
|
||||||
|
import com.futo.polycentric.core.base64UrlToByteArray
|
||||||
import com.google.zxing.integration.android.IntentIntegrator
|
import com.google.zxing.integration.android.IntentIntegrator
|
||||||
import com.journeyapps.barcodescanner.CaptureActivity
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import userpackage.Protocol
|
import userpackage.Protocol
|
||||||
@@ -28,6 +36,7 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
|||||||
private lateinit var _buttonScanProfile: LinearLayout;
|
private lateinit var _buttonScanProfile: LinearLayout;
|
||||||
private lateinit var _buttonImportProfile: LinearLayout;
|
private lateinit var _buttonImportProfile: LinearLayout;
|
||||||
private lateinit var _editProfile: EditText;
|
private lateinit var _editProfile: EditText;
|
||||||
|
private lateinit var _loaderOverlay: LoaderOverlay;
|
||||||
|
|
||||||
private val _qrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
private val _qrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
|
val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
|
||||||
@@ -39,6 +48,10 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
|
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.activity_polycentric_import_profile);
|
setContentView(R.layout.activity_polycentric_import_profile);
|
||||||
@@ -47,6 +60,7 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
|||||||
_buttonHelp = findViewById(R.id.button_help);
|
_buttonHelp = findViewById(R.id.button_help);
|
||||||
_buttonScanProfile = findViewById(R.id.button_scan_profile);
|
_buttonScanProfile = findViewById(R.id.button_scan_profile);
|
||||||
_buttonImportProfile = findViewById(R.id.button_import_profile);
|
_buttonImportProfile = findViewById(R.id.button_import_profile);
|
||||||
|
_loaderOverlay = findViewById(R.id.loader_overlay);
|
||||||
_editProfile = findViewById(R.id.edit_profile);
|
_editProfile = findViewById(R.id.edit_profile);
|
||||||
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
|
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
|
||||||
finish();
|
finish();
|
||||||
@@ -89,42 +103,63 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
_loaderOverlay.show()
|
||||||
val data = url.substring("polycentric://".length).base64UrlToByteArray();
|
|
||||||
val urlInfo = Protocol.URLInfo.parseFrom(data);
|
|
||||||
if (urlInfo.urlType != 3L) {
|
|
||||||
throw Exception("Expected urlInfo struct of type ExportBundle")
|
|
||||||
}
|
|
||||||
|
|
||||||
val exportBundle = ExportBundle.parseFrom(urlInfo.body);
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val keyPair = KeyPair.fromProto(exportBundle.keyPair);
|
try {
|
||||||
|
val data = url.substring("polycentric://".length).base64UrlToByteArray();
|
||||||
|
val urlInfo = Protocol.URLInfo.parseFrom(data);
|
||||||
|
if (urlInfo.urlType != 3L) {
|
||||||
|
throw Exception("Expected urlInfo struct of type ExportBundle")
|
||||||
|
}
|
||||||
|
|
||||||
val existingProcessSecret = Store.instance.getProcessSecret(keyPair.publicKey);
|
val exportBundle = ExportBundle.parseFrom(urlInfo.body);
|
||||||
if (existingProcessSecret != null) {
|
val keyPair = KeyPair.fromProto(exportBundle.keyPair);
|
||||||
UIDialogs.toast(this, getString(R.string.this_profile_is_already_imported));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
val processSecret = ProcessSecret(keyPair, Process.random());
|
val existingProcessSecret = Store.instance.getProcessSecret(keyPair.publicKey);
|
||||||
Store.instance.addProcessSecret(processSecret);
|
if (existingProcessSecret != null) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
UIDialogs.toast(this@PolycentricImportProfileActivity, getString(R.string.this_profile_is_already_imported));
|
||||||
|
}
|
||||||
|
return@launch;
|
||||||
|
}
|
||||||
|
|
||||||
val processHandle = processSecret.toProcessHandle();
|
val processSecret = ProcessSecret(keyPair, Process.random());
|
||||||
|
Store.instance.addProcessSecret(processSecret);
|
||||||
|
|
||||||
for (e in exportBundle.events.eventsList) {
|
|
||||||
try {
|
try {
|
||||||
val se = SignedEvent.fromProto(e);
|
PolycentricStorage.instance.addProcessSecret(processSecret)
|
||||||
Store.instance.putSignedEvent(se);
|
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.w(TAG, "Ignored invalid event", e);
|
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
val processHandle = processSecret.toProcessHandle();
|
||||||
|
|
||||||
|
for (e in exportBundle.events.eventsList) {
|
||||||
|
try {
|
||||||
|
val se = SignedEvent.fromProto(e);
|
||||||
|
Store.instance.putSignedEvent(se);
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.w(TAG, "Ignored invalid event", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StatePolycentric.instance.setProcessHandle(processHandle);
|
||||||
|
processHandle.fullyBackfillClient(PolycentricCache.SERVER);
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
startActivity(Intent(this@PolycentricImportProfileActivity, PolycentricProfileActivity::class.java));
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.w(TAG, "Failed to import profile", e);
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
UIDialogs.toast(this@PolycentricImportProfileActivity, getString(R.string.failed_to_import_profile) + " '${e.message}'");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
_loaderOverlay.hide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StatePolycentric.instance.setProcessHandle(processHandle);
|
|
||||||
startActivity(Intent(this@PolycentricImportProfileActivity, PolycentricProfileActivity::class.java));
|
|
||||||
finish();
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.w(TAG, "Failed to import profile", e);
|
|
||||||
UIDialogs.toast(this, getString(R.string.failed_to_import_profile) + " '${e.message}'");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+56
-24
@@ -1,7 +1,10 @@
|
|||||||
package com.futo.platformplayer.activities
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
@@ -11,24 +14,26 @@ import android.webkit.MimeTypeMap
|
|||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.dialogs.CommentDialog
|
|
||||||
import com.futo.platformplayer.dp
|
import com.futo.platformplayer.dp
|
||||||
|
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
||||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||||
import com.futo.platformplayer.selectBestImage
|
import com.futo.platformplayer.selectBestImage
|
||||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
import com.futo.platformplayer.views.buttons.BigButton
|
import com.futo.platformplayer.views.buttons.BigButton
|
||||||
|
import com.futo.platformplayer.views.overlays.LoaderOverlay
|
||||||
import com.futo.polycentric.core.Store
|
import com.futo.polycentric.core.Store
|
||||||
import com.futo.polycentric.core.Synchronization
|
|
||||||
import com.futo.polycentric.core.SystemState
|
import com.futo.polycentric.core.SystemState
|
||||||
import com.futo.polycentric.core.toURLInfoDataLink
|
import com.futo.polycentric.core.toBase64Url
|
||||||
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||||
import com.github.dhaval2404.imagepicker.ImagePicker
|
import com.github.dhaval2404.imagepicker.ImagePicker
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -46,8 +51,14 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
|||||||
private lateinit var _buttonDelete: BigButton;
|
private lateinit var _buttonDelete: BigButton;
|
||||||
private lateinit var _username: String;
|
private lateinit var _username: String;
|
||||||
private lateinit var _imagePolycentric: ImageView;
|
private lateinit var _imagePolycentric: ImageView;
|
||||||
|
private lateinit var _loaderOverlay: LoaderOverlay;
|
||||||
|
private lateinit var _textSystem: TextView;
|
||||||
private var _avatarUri: Uri? = null;
|
private var _avatarUri: Uri? = null;
|
||||||
|
|
||||||
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
|
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.activity_polycentric_profile);
|
setContentView(R.layout.activity_polycentric_profile);
|
||||||
@@ -59,28 +70,13 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
|||||||
_buttonExport = findViewById(R.id.button_export);
|
_buttonExport = findViewById(R.id.button_export);
|
||||||
_buttonLogout = findViewById(R.id.button_logout);
|
_buttonLogout = findViewById(R.id.button_logout);
|
||||||
_buttonDelete = findViewById(R.id.button_delete);
|
_buttonDelete = findViewById(R.id.button_delete);
|
||||||
|
_loaderOverlay = findViewById(R.id.loader_overlay);
|
||||||
|
_textSystem = findViewById(R.id.text_system)
|
||||||
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
|
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
|
||||||
saveIfRequired();
|
saveIfRequired();
|
||||||
finish();
|
finish();
|
||||||
};
|
};
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
val processHandle = StatePolycentric.instance.processHandle!!;
|
|
||||||
Synchronization.fullyBackFillClient(processHandle, processHandle.system, "https://srv1-stg.polycentric.io");
|
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
updateUI();
|
|
||||||
}
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.failed_to_backfill_client));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateUI();
|
|
||||||
|
|
||||||
_imagePolycentric.setOnClickListener {
|
_imagePolycentric.setOnClickListener {
|
||||||
ImagePicker.with(this)
|
ImagePicker.with(this)
|
||||||
.cropSquare()
|
.cropSquare()
|
||||||
@@ -116,6 +112,37 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
|||||||
finish();
|
finish();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_textSystem.setOnLongClickListener {
|
||||||
|
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
|
val clip: ClipData = ClipData.newPlainText("system", _textSystem.text)
|
||||||
|
clipboard.setPrimaryClip(clip)
|
||||||
|
return@setOnLongClickListener true
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUI()
|
||||||
|
|
||||||
|
StatePolycentric.instance.processHandle?.let { processHandle ->
|
||||||
|
_loaderOverlay.show()
|
||||||
|
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
processHandle.fullyBackfillClient(PolycentricCache.SERVER)
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
updateUI();
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.failed_to_backfill_client));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
_loaderOverlay.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveIfRequired() {
|
private fun saveIfRequired() {
|
||||||
@@ -124,13 +151,17 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
|||||||
var hasChanges = false;
|
var hasChanges = false;
|
||||||
val username = _editName.text.toString();
|
val username = _editName.text.toString();
|
||||||
if (username.length < 3) {
|
if (username.length < 3) {
|
||||||
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.name_must_be_at_least_3_characters_long));
|
withContext(Dispatchers.Main) {
|
||||||
|
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.name_must_be_at_least_3_characters_long));
|
||||||
|
}
|
||||||
return@launch;
|
return@launch;
|
||||||
}
|
}
|
||||||
|
|
||||||
val processHandle = StatePolycentric.instance.processHandle;
|
val processHandle = StatePolycentric.instance.processHandle;
|
||||||
if (processHandle == null) {
|
if (processHandle == null) {
|
||||||
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.process_handle_unset));
|
withContext(Dispatchers.Main) {
|
||||||
|
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.process_handle_unset));
|
||||||
|
}
|
||||||
return@launch;
|
return@launch;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,7 +220,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
|||||||
if (hasChanges) {
|
if (hasChanges) {
|
||||||
try {
|
try {
|
||||||
Logger.i(TAG, "Started backfill");
|
Logger.i(TAG, "Started backfill");
|
||||||
processHandle.fullyBackfillServers();
|
processHandle.fullyBackfillServersAnnounceExceptions();
|
||||||
Logger.i(TAG, "Finished backfill");
|
Logger.i(TAG, "Finished backfill");
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.changes_have_been_saved));
|
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.changes_have_been_saved));
|
||||||
@@ -215,6 +246,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
|||||||
private fun updateUI() {
|
private fun updateUI() {
|
||||||
val processHandle = StatePolycentric.instance.processHandle!!;
|
val processHandle = StatePolycentric.instance.processHandle!!;
|
||||||
val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(processHandle.system))
|
val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(processHandle.system))
|
||||||
|
_textSystem.text = processHandle.system.key.toBase64Url()
|
||||||
_username = systemState.username;
|
_username = systemState.username;
|
||||||
_editName.text.clear();
|
_editName.text.clear();
|
||||||
_editName.text.append(_username);
|
_editName.text.append(_username);
|
||||||
@@ -244,7 +276,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun getMimeType(contentResolver: ContentResolver, uri: Uri): String? {
|
private fun getMimeType(contentResolver: ContentResolver, uri: Uri): String? {
|
||||||
var mimeType: String? = null;
|
var mimeType: String?;
|
||||||
|
|
||||||
// Try to get MIME type from the content URI
|
// Try to get MIME type from the content URI
|
||||||
mimeType = contentResolver.getType(uri);
|
mimeType = contentResolver.getType(uri);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.futo.platformplayer.activities
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@@ -7,12 +8,17 @@ import android.widget.ImageButton
|
|||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.views.buttons.BigButton
|
import com.futo.platformplayer.views.buttons.BigButton
|
||||||
|
|
||||||
class PolycentricWhyActivity : AppCompatActivity() {
|
class PolycentricWhyActivity : AppCompatActivity() {
|
||||||
private lateinit var _buttonVideo: BigButton;
|
private lateinit var _buttonVideo: BigButton;
|
||||||
private lateinit var _buttonTechnical: BigButton;
|
private lateinit var _buttonTechnical: BigButton;
|
||||||
|
|
||||||
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
|
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.activity_polycentric_why);
|
setContentView(R.layout.activity_polycentric_why);
|
||||||
|
|||||||
@@ -1,21 +1,26 @@
|
|||||||
package com.futo.platformplayer.activities
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.NotificationManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.widget.FrameLayout
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import androidx.activity.result.ActivityResult
|
import androidx.activity.result.ActivityResult
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.views.Loader
|
import com.futo.platformplayer.views.LoaderView
|
||||||
import com.futo.platformplayer.views.fields.FieldForm
|
import com.futo.platformplayer.views.fields.FieldForm
|
||||||
import com.futo.platformplayer.views.fields.ReadOnlyTextField
|
import com.futo.platformplayer.views.fields.ReadOnlyTextField
|
||||||
import com.google.android.material.button.MaterialButton
|
import com.google.android.material.button.MaterialButton
|
||||||
@@ -23,13 +28,23 @@ import com.google.android.material.button.MaterialButton
|
|||||||
class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
|
class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
|
||||||
private lateinit var _form: FieldForm;
|
private lateinit var _form: FieldForm;
|
||||||
private lateinit var _buttonBack: ImageButton;
|
private lateinit var _buttonBack: ImageButton;
|
||||||
private lateinit var _loader: Loader;
|
private lateinit var _loaderView: LoaderView;
|
||||||
|
|
||||||
private lateinit var _devSets: LinearLayout;
|
private lateinit var _devSets: LinearLayout;
|
||||||
private lateinit var _buttonDev: MaterialButton;
|
private lateinit var _buttonDev: MaterialButton;
|
||||||
|
|
||||||
private var _isFinished = false;
|
private var _isFinished = false;
|
||||||
|
|
||||||
|
lateinit var overlay: FrameLayout;
|
||||||
|
|
||||||
|
val notifPermission = "android.permission.POST_NOTIFICATIONS";
|
||||||
|
val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
|
||||||
|
if (isGranted)
|
||||||
|
UIDialogs.toast(this, "Notification permission granted");
|
||||||
|
else
|
||||||
|
UIDialogs.toast(this, "Notification permission denied");
|
||||||
|
}
|
||||||
|
|
||||||
override fun attachBaseContext(newBase: Context?) {
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
Logger.i("SettingsActivity", "SettingsActivity.attachBaseContext")
|
Logger.i("SettingsActivity", "SettingsActivity.attachBaseContext")
|
||||||
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
||||||
@@ -43,9 +58,10 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
|
|||||||
_buttonBack = findViewById(R.id.button_back);
|
_buttonBack = findViewById(R.id.button_back);
|
||||||
_buttonDev = findViewById(R.id.button_dev);
|
_buttonDev = findViewById(R.id.button_dev);
|
||||||
_devSets = findViewById(R.id.dev_settings);
|
_devSets = findViewById(R.id.dev_settings);
|
||||||
_loader = findViewById(R.id.loader);
|
_loaderView = findViewById(R.id.loader);
|
||||||
|
overlay = findViewById(R.id.overlay_container);
|
||||||
|
|
||||||
_form.onChanged.subscribe { field, value ->
|
_form.onChanged.subscribe { field, _ ->
|
||||||
Logger.i("SettingsActivity", "Setting [${field.field?.name}] changed, saving");
|
Logger.i("SettingsActivity", "Setting [${field.field?.name}] changed, saving");
|
||||||
_form.setObjectValues();
|
_form.setObjectValues();
|
||||||
Settings.instance.save();
|
Settings.instance.save();
|
||||||
@@ -54,6 +70,33 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
|
|||||||
Logger.i("SettingsActivity", "App language change detected, propogating to shared preferences");
|
Logger.i("SettingsActivity", "App language change detected, propogating to shared preferences");
|
||||||
StateApp.instance.setLocaleSetting(this, Settings.instance.language.getAppLanguageLocaleString());
|
StateApp.instance.setLocaleSetting(this, Settings.instance.language.getAppLanguageLocaleString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(field.descriptor?.id == "background_update") {
|
||||||
|
Logger.i("SettingsActivity", "Detected change in background work ${field.value}");
|
||||||
|
if(Settings.instance.subscriptions.subscriptionsBackgroundUpdateInterval > 0) {
|
||||||
|
val notifManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
|
||||||
|
if(!notifManager.areNotificationsEnabled()) {
|
||||||
|
UIDialogs.toast(this, "Notifications aren't enabled");
|
||||||
|
|
||||||
|
when {
|
||||||
|
ContextCompat.checkSelfPermission(this, notifPermission) == PackageManager.PERMISSION_GRANTED -> {
|
||||||
|
|
||||||
|
}
|
||||||
|
ActivityCompat.shouldShowRequestPermissionRationale(this, notifPermission) -> {
|
||||||
|
UIDialogs.showDialog(this, R.drawable.ic_notifications, "Notifications Required",
|
||||||
|
"Notifications need to be enabled for background updating to function", null, 0,
|
||||||
|
UIDialogs.Action("Cancel", {}),
|
||||||
|
UIDialogs.Action("Enable", {
|
||||||
|
requestPermissionLauncher.launch(notifPermission);
|
||||||
|
}, UIDialogs.ActionStyle.PRIMARY));
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
requestPermissionLauncher.launch(notifPermission);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
_buttonBack.setOnClickListener {
|
_buttonBack.setOnClickListener {
|
||||||
finish();
|
finish();
|
||||||
@@ -68,10 +111,15 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
|
|||||||
reloadSettings();
|
reloadSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isFirstLoad = true;
|
||||||
fun reloadSettings() {
|
fun reloadSettings() {
|
||||||
_loader.start();
|
val firstLoad = isFirstLoad;
|
||||||
|
isFirstLoad = false;
|
||||||
|
_form.setSearchVisible(false);
|
||||||
|
_loaderView.start();
|
||||||
_form.fromObject(lifecycleScope, Settings.instance) {
|
_form.fromObject(lifecycleScope, Settings.instance) {
|
||||||
_loader.stop();
|
_loaderView.stop();
|
||||||
|
_form.setSearchVisible(true);
|
||||||
|
|
||||||
var devCounter = 0;
|
var devCounter = 0;
|
||||||
_form.findField("code")?.assume<ReadOnlyTextField>()?.setOnClickListener {
|
_form.findField("code")?.assume<ReadOnlyTextField>()?.setOnClickListener {
|
||||||
@@ -84,6 +132,13 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
|
|||||||
UIDialogs.toast(this, getString(R.string.you_are_now_in_developer_mode));
|
UIDialogs.toast(this, getString(R.string.you_are_now_in_developer_mode));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if(firstLoad) {
|
||||||
|
val query = intent.getStringExtra("query");
|
||||||
|
if(!query.isNullOrEmpty()) {
|
||||||
|
_form.setSearchQuery(query);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,6 +184,7 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
|
|||||||
resultLauncher.launch(intent);
|
resultLauncher.launch(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
//TODO: Temporary for solving Settings issues
|
//TODO: Temporary for solving Settings issues
|
||||||
@SuppressLint("StaticFieldLeak")
|
@SuppressLint("StaticFieldLeak")
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.futo.platformplayer.api.http
|
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.constructs.Event1
|
||||||
import com.futo.platformplayer.ensureNotMainThread
|
import com.futo.platformplayer.ensureNotMainThread
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
@@ -13,8 +15,11 @@ import okhttp3.Response
|
|||||||
import okhttp3.ResponseBody
|
import okhttp3.ResponseBody
|
||||||
import okhttp3.WebSocket
|
import okhttp3.WebSocket
|
||||||
import okhttp3.WebSocketListener
|
import okhttp3.WebSocketListener
|
||||||
import java.util.Dictionary
|
import java.security.SecureRandom
|
||||||
import java.util.concurrent.TimeUnit
|
import java.security.cert.X509Certificate
|
||||||
|
import javax.net.ssl.SSLContext
|
||||||
|
import javax.net.ssl.TrustManager
|
||||||
|
import javax.net.ssl.X509TrustManager
|
||||||
import kotlin.system.measureTimeMillis
|
import kotlin.system.measureTimeMillis
|
||||||
|
|
||||||
open class ManagedHttpClient {
|
open class ManagedHttpClient {
|
||||||
@@ -27,8 +32,29 @@ open class ManagedHttpClient {
|
|||||||
|
|
||||||
var user_agent = "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0"
|
var user_agent = "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0"
|
||||||
|
|
||||||
|
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()) {
|
constructor(builder: OkHttpClient.Builder = OkHttpClient.Builder()) {
|
||||||
_builderTemplate = builder;
|
_builderTemplate = builder;
|
||||||
|
if(SettingsDev.instance.developerMode && SettingsDev.instance.networking.allowAllCertificates)
|
||||||
|
trustAllCertificates(builder);
|
||||||
client = builder.addNetworkInterceptor { chain ->
|
client = builder.addNetworkInterceptor { chain ->
|
||||||
val request = beforeRequest(chain.request());
|
val request = beforeRequest(chain.request());
|
||||||
val response = afterRequest(chain.proceed(request));
|
val response = afterRequest(chain.proceed(request));
|
||||||
@@ -60,7 +86,7 @@ open class ManagedHttpClient {
|
|||||||
|
|
||||||
val requestBuilder: okhttp3.Request.Builder = okhttp3.Request.Builder()
|
val requestBuilder: okhttp3.Request.Builder = okhttp3.Request.Builder()
|
||||||
.url(url);
|
.url(url);
|
||||||
if(user_agent != null && !user_agent.isEmpty() && !headers.any { it.key.lowercase() == "user-agent" })
|
if(user_agent.isNotEmpty() && !headers.any { it.key.lowercase() == "user-agent" })
|
||||||
requestBuilder.addHeader("User-Agent", user_agent)
|
requestBuilder.addHeader("User-Agent", user_agent)
|
||||||
|
|
||||||
for (pair in headers.entries)
|
for (pair in headers.entries)
|
||||||
@@ -137,7 +163,7 @@ open class ManagedHttpClient {
|
|||||||
val requestBuilder: okhttp3.Request.Builder = okhttp3.Request.Builder()
|
val requestBuilder: okhttp3.Request.Builder = okhttp3.Request.Builder()
|
||||||
.method(request.method, requestBody)
|
.method(request.method, requestBody)
|
||||||
.url(request.url);
|
.url(request.url);
|
||||||
if(user_agent != null && !user_agent.isEmpty() && !request.headers.any { it.key.lowercase() == "user-agent" })
|
if(user_agent.isNotEmpty() && !request.headers.any { it.key.lowercase() == "user-agent" })
|
||||||
requestBuilder.addHeader("User-Agent", user_agent)
|
requestBuilder.addHeader("User-Agent", user_agent)
|
||||||
|
|
||||||
for (pair in request.headers.entries)
|
for (pair in request.headers.entries)
|
||||||
@@ -148,7 +174,7 @@ open class ManagedHttpClient {
|
|||||||
|
|
||||||
val time = measureTimeMillis {
|
val time = measureTimeMillis {
|
||||||
val call = client.newCall(requestBuilder.build());
|
val call = client.newCall(requestBuilder.build());
|
||||||
request.onCallCreated?.emit(call);
|
request.onCallCreated.emit(call);
|
||||||
response = call.execute()
|
response = call.execute()
|
||||||
resp = Response(
|
resp = Response(
|
||||||
response.code,
|
response.code,
|
||||||
|
|||||||
@@ -8,16 +8,20 @@ import kotlinx.serialization.decodeFromString
|
|||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.io.StringWriter
|
import java.io.StringWriter
|
||||||
import java.net.SocketTimeoutException
|
import java.net.SocketTimeoutException
|
||||||
|
|
||||||
class HttpContext : AutoCloseable {
|
class HttpContext : AutoCloseable {
|
||||||
private val _stream: BufferedReader;
|
private val _inputStream: InputStream;
|
||||||
private var _responseStream: OutputStream? = null;
|
private var _responseStream: OutputStream? = null;
|
||||||
|
|
||||||
var id: String? = null;
|
var id: String? = null;
|
||||||
|
|
||||||
var head: String = "";
|
var head: String = "";
|
||||||
var headers: HttpHeaders = HttpHeaders();
|
var headers: HttpHeaders = HttpHeaders();
|
||||||
|
|
||||||
@@ -39,76 +43,130 @@ class HttpContext : AutoCloseable {
|
|||||||
private val _responseHeaders: HttpHeaders = HttpHeaders();
|
private val _responseHeaders: HttpHeaders = HttpHeaders();
|
||||||
|
|
||||||
|
|
||||||
constructor(stream: BufferedReader, responseStream: OutputStream? = null, requestId: String? = null, timeout: Int? = null) {
|
constructor(inputStream: InputStream, responseStream: OutputStream? = null, requestId: String? = null, timeout: Int? = null) {
|
||||||
_stream = stream;
|
_inputStream = inputStream;
|
||||||
_responseStream = responseStream;
|
_responseStream = responseStream;
|
||||||
this.id = requestId;
|
this.id = requestId;
|
||||||
|
|
||||||
try {
|
val headerBytes = readHeaderBytes()
|
||||||
head = stream.readLine() ?: throw EmptyRequestException("No head found");
|
ByteArrayInputStream(headerBytes).use {
|
||||||
}
|
val reader = it.bufferedReader(Charsets.UTF_8)
|
||||||
catch(ex: SocketTimeoutException) {
|
try {
|
||||||
if((timeout ?: 0) > 0)
|
head = reader.readLine() ?: throw EmptyRequestException("No head found");
|
||||||
throw KeepAliveTimeoutException("Keep-Alive timedout", ex);
|
}
|
||||||
throw ex;
|
catch(ex: SocketTimeoutException) {
|
||||||
}
|
if((timeout ?: 0) > 0)
|
||||||
|
throw KeepAliveTimeoutException("Keep-Alive timedout", ex);
|
||||||
val methodEndIndex = head.indexOf(' ');
|
throw ex;
|
||||||
val urlEndIndex = head.indexOf(' ', methodEndIndex + 1);
|
|
||||||
if (methodEndIndex == -1 || urlEndIndex == -1) {
|
|
||||||
Logger.w(TAG, "Skipped request, wrong format.");
|
|
||||||
throw IllegalStateException("Invalid request");
|
|
||||||
}
|
|
||||||
|
|
||||||
method = head.substring(0, methodEndIndex);
|
|
||||||
path = head.substring(methodEndIndex + 1, urlEndIndex);
|
|
||||||
|
|
||||||
if (path.contains("?")) {
|
|
||||||
val queryPartIndex = path.indexOf("?");
|
|
||||||
val queryParts = path.substring(queryPartIndex + 1).split("&");
|
|
||||||
path = path.substring(0, queryPartIndex);
|
|
||||||
|
|
||||||
for(queryPart in queryParts) {
|
|
||||||
val eqIndex = queryPart.indexOf("=");
|
|
||||||
if(eqIndex > 0)
|
|
||||||
query.put(queryPart.substring(0, eqIndex), queryPart.substring(eqIndex + 1));
|
|
||||||
else
|
|
||||||
query.put(queryPart, "");
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
while (true) {
|
val methodEndIndex = head.indexOf(' ');
|
||||||
val line = stream.readLine();
|
val urlEndIndex = head.indexOf(' ', methodEndIndex + 1);
|
||||||
val headerEndIndex = line.indexOf(":");
|
if (methodEndIndex == -1 || urlEndIndex == -1) {
|
||||||
if (headerEndIndex == -1)
|
Logger.w(TAG, "Skipped request, wrong format.");
|
||||||
break;
|
throw IllegalStateException("Invalid request");
|
||||||
|
}
|
||||||
|
|
||||||
val headerKey = line.substring(0, headerEndIndex).lowercase()
|
method = head.substring(0, methodEndIndex);
|
||||||
val headerValue = line.substring(headerEndIndex + 1).trim();
|
path = head.substring(methodEndIndex + 1, urlEndIndex);
|
||||||
headers[headerKey] = headerValue;
|
|
||||||
|
|
||||||
when(headerKey) {
|
if (path.contains("?")) {
|
||||||
"content-length" -> contentLength = headerValue.toLong();
|
val queryPartIndex = path.indexOf("?");
|
||||||
"content-type" -> contentType = headerValue;
|
val queryParts = path.substring(queryPartIndex + 1).split("&");
|
||||||
"connection" -> keepAlive = headerValue.lowercase() == "keep-alive";
|
path = path.substring(0, queryPartIndex);
|
||||||
"keep-alive" -> {
|
|
||||||
val keepAliveParams = headerValue.split(",");
|
for(queryPart in queryParts) {
|
||||||
for(keepAliveParam in keepAliveParams) {
|
val eqIndex = queryPart.indexOf("=");
|
||||||
val eqIndex = keepAliveParam.indexOf("=");
|
if(eqIndex > 0)
|
||||||
if(eqIndex > 0){
|
query.put(queryPart.substring(0, eqIndex), queryPart.substring(eqIndex + 1));
|
||||||
when(keepAliveParam.substring(0, eqIndex)) {
|
else
|
||||||
"timeout" -> keepAliveTimeout = keepAliveParam.substring(eqIndex+1).toInt();
|
query.put(queryPart, "");
|
||||||
"max" -> keepAliveTimeout = keepAliveParam.substring(eqIndex+1).toInt();
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
val line = reader.readLine();
|
||||||
|
val headerEndIndex = line.indexOf(":");
|
||||||
|
if (headerEndIndex == -1)
|
||||||
|
break;
|
||||||
|
|
||||||
|
val headerKey = line.substring(0, headerEndIndex).lowercase()
|
||||||
|
val headerValue = line.substring(headerEndIndex + 1).trim();
|
||||||
|
headers[headerKey] = headerValue;
|
||||||
|
|
||||||
|
when(headerKey) {
|
||||||
|
"content-length" -> contentLength = headerValue.toLong();
|
||||||
|
"content-type" -> contentType = headerValue;
|
||||||
|
"connection" -> keepAlive = headerValue.lowercase() == "keep-alive";
|
||||||
|
"keep-alive" -> {
|
||||||
|
val keepAliveParams = headerValue.split(",");
|
||||||
|
for(keepAliveParam in keepAliveParams) {
|
||||||
|
val eqIndex = keepAliveParam.indexOf("=");
|
||||||
|
if(eqIndex > 0){
|
||||||
|
when(keepAliveParam.substring(0, eqIndex)) {
|
||||||
|
"timeout" -> keepAliveTimeout = keepAliveParam.substring(eqIndex+1).toInt();
|
||||||
|
"max" -> keepAliveTimeout = keepAliveParam.substring(eqIndex+1).toInt();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if(line.isNullOrEmpty())
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
if(line.isNullOrEmpty())
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun readHeaderBytes(): ByteArray {
|
||||||
|
val headerBytes = ByteArrayOutputStream()
|
||||||
|
var crlfCount = 0
|
||||||
|
|
||||||
|
while (crlfCount < 4) {
|
||||||
|
val b = _inputStream.read()
|
||||||
|
if (b == -1) {
|
||||||
|
throw IOException("Unexpected end of stream while reading headers")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (b == 0x0D || b == 0x0A) { // CR or LF
|
||||||
|
crlfCount++
|
||||||
|
} else {
|
||||||
|
crlfCount = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
headerBytes.write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
return headerBytes.toByteArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readContentBytes(buffer: ByteArray, length: Int): Int {
|
||||||
|
val remainingBytes = (contentLength - _totalRead).coerceAtMost(length.toLong()).toInt()
|
||||||
|
val read = _inputStream.read(buffer, 0, remainingBytes);
|
||||||
|
if (read > 0) {
|
||||||
|
_totalRead += read
|
||||||
|
}
|
||||||
|
|
||||||
|
return read;
|
||||||
|
}
|
||||||
|
fun readContentString(): String {
|
||||||
|
val byteArrayOutputStream = ByteArrayOutputStream()
|
||||||
|
val buffer = ByteArray(4096)
|
||||||
|
var read: Int
|
||||||
|
while (true) {
|
||||||
|
read = readContentBytes(buffer, buffer.size)
|
||||||
|
if (read <= 0) break
|
||||||
|
byteArrayOutputStream.write(buffer, 0, read)
|
||||||
|
}
|
||||||
|
return byteArrayOutputStream.toString(Charsets.UTF_8.name())
|
||||||
|
}
|
||||||
|
inline fun <reified T> readContentJson() : T {
|
||||||
|
return Serializer.json.decodeFromString(readContentString());
|
||||||
|
}
|
||||||
|
fun skipBody() {
|
||||||
|
if (contentLength > 0)
|
||||||
|
_inputStream.skip(contentLength - _totalRead)
|
||||||
|
}
|
||||||
|
|
||||||
fun getHttpHeaderString(): String {
|
fun getHttpHeaderString(): String {
|
||||||
val writer = StringWriter();
|
val writer = StringWriter();
|
||||||
writer.write(head + "\r\n");
|
writer.write(head + "\r\n");
|
||||||
@@ -139,8 +197,13 @@ class HttpContext : AutoCloseable {
|
|||||||
}
|
}
|
||||||
fun respondCode(status: Int, headers: HttpHeaders, body: String? = null) {
|
fun respondCode(status: Int, headers: HttpHeaders, body: String? = null) {
|
||||||
val bytes = body?.toByteArray(Charsets.UTF_8);
|
val bytes = body?.toByteArray(Charsets.UTF_8);
|
||||||
if(body != null && headers.get("content-length").isNullOrEmpty())
|
if(headers.get("content-length").isNullOrEmpty()) {
|
||||||
headers.put("content-length", bytes!!.size.toString());
|
if (body != null) {
|
||||||
|
headers.put("content-length", bytes!!.size.toString());
|
||||||
|
} else {
|
||||||
|
headers.put("content-length", "0")
|
||||||
|
}
|
||||||
|
}
|
||||||
respond(status, headers) { responseStream ->
|
respond(status, headers) { responseStream ->
|
||||||
if(body != null) {
|
if(body != null) {
|
||||||
responseStream.write(bytes!!);
|
responseStream.write(bytes!!);
|
||||||
@@ -161,8 +224,7 @@ class HttpContext : AutoCloseable {
|
|||||||
headersToRespond.put("keep-alive", "timeout=5, max=1000");
|
headersToRespond.put("keep-alive", "timeout=5, max=1000");
|
||||||
}
|
}
|
||||||
|
|
||||||
val responseHeader = HttpResponse(status, headers);
|
val responseHeader = HttpResponse(status, headersToRespond);
|
||||||
|
|
||||||
responseStream.write(responseHeader.getHttpHeaderBytes());
|
responseStream.write(responseHeader.getHttpHeaderBytes());
|
||||||
|
|
||||||
if(method != "HEAD") {
|
if(method != "HEAD") {
|
||||||
@@ -172,38 +234,9 @@ class HttpContext : AutoCloseable {
|
|||||||
statusCode = status;
|
statusCode = status;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun readContentBytes(buffer: CharArray, length: Int) : Int {
|
|
||||||
val reading = Math.min(length, (contentLength - _totalRead).toInt());
|
|
||||||
val read = _stream.read(buffer, 0, reading);
|
|
||||||
_totalRead += read;
|
|
||||||
|
|
||||||
//TODO: Fix this properly
|
|
||||||
if(contentLength - _totalRead < 400 && read < length) {
|
|
||||||
_totalRead = contentLength;
|
|
||||||
}
|
|
||||||
return read;
|
|
||||||
}
|
|
||||||
fun readContentString() : String{
|
|
||||||
val writer = StringWriter();
|
|
||||||
var read = 0;
|
|
||||||
val buffer = CharArray(4096);
|
|
||||||
do {
|
|
||||||
read = readContentBytes(buffer, buffer.size);
|
|
||||||
writer.write(buffer, 0, read);
|
|
||||||
} while(read > 0);
|
|
||||||
return writer.toString();
|
|
||||||
}
|
|
||||||
inline fun <reified T> readContentJson() : T {
|
|
||||||
return Serializer.json.decodeFromString(readContentString());
|
|
||||||
}
|
|
||||||
fun skipBody() {
|
|
||||||
if(contentLength > 0)
|
|
||||||
_stream.skip(contentLength - _totalRead);
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun close() {
|
override fun close() {
|
||||||
if(!keepAlive) {
|
if(!keepAlive) {
|
||||||
_stream?.close();
|
_inputStream.close();
|
||||||
_responseStream?.close();
|
_responseStream?.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
package com.futo.platformplayer.api.http.server
|
package com.futo.platformplayer.api.http.server
|
||||||
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.api.http.server.exceptions.EmptyRequestException
|
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.HttpFuntionHandler
|
||||||
import com.futo.platformplayer.api.http.server.handlers.HttpHandler
|
import com.futo.platformplayer.api.http.server.handlers.HttpHandler
|
||||||
import java.io.BufferedReader
|
import com.futo.platformplayer.api.http.server.handlers.HttpOptionsAllowHandler
|
||||||
import java.io.InputStreamReader
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import java.io.BufferedInputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.lang.reflect.Field
|
import java.lang.reflect.Field
|
||||||
import java.lang.reflect.Method
|
import java.lang.reflect.Method
|
||||||
@@ -14,7 +14,7 @@ import java.net.InetAddress
|
|||||||
import java.net.NetworkInterface
|
import java.net.NetworkInterface
|
||||||
import java.net.ServerSocket
|
import java.net.ServerSocket
|
||||||
import java.net.Socket
|
import java.net.Socket
|
||||||
import java.util.*
|
import java.util.UUID
|
||||||
import java.util.concurrent.ExecutorService
|
import java.util.concurrent.ExecutorService
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
import java.util.stream.IntStream.range
|
import java.util.stream.IntStream.range
|
||||||
@@ -29,7 +29,8 @@ class ManagedHttpServer(private val _requestedPort: Int = 0) {
|
|||||||
var port = 0
|
var port = 0
|
||||||
private set;
|
private set;
|
||||||
|
|
||||||
private val _handlers = mutableListOf<HttpHandler>();
|
private val _handlers = hashMapOf<String, HashMap<String, HttpHandler>>()
|
||||||
|
private val _headHandlers = hashMapOf<String, HttpHandler>()
|
||||||
private var _workerPool: ExecutorService? = null;
|
private var _workerPool: ExecutorService? = null;
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
@@ -76,12 +77,12 @@ class ManagedHttpServer(private val _requestedPort: Int = 0) {
|
|||||||
|
|
||||||
private fun handleClientRequest(socket: Socket) {
|
private fun handleClientRequest(socket: Socket) {
|
||||||
_workerPool?.submit {
|
_workerPool?.submit {
|
||||||
val requestReader = BufferedReader(InputStreamReader(socket.getInputStream()))
|
val requestStream = BufferedInputStream(socket.getInputStream());
|
||||||
val responseStream = socket.getOutputStream();
|
val responseStream = socket.getOutputStream();
|
||||||
|
|
||||||
val requestId = UUID.randomUUID().toString().substring(0, 5);
|
val requestId = UUID.randomUUID().toString().substring(0, 5);
|
||||||
try {
|
try {
|
||||||
keepAliveLoop(requestReader, responseStream, requestId) { req ->
|
keepAliveLoop(requestStream, responseStream, requestId) { req ->
|
||||||
req.use { httpContext ->
|
req.use { httpContext ->
|
||||||
if(!httpContext.path.startsWith("/plugin/"))
|
if(!httpContext.path.startsWith("/plugin/"))
|
||||||
Logger.i(TAG, "[${req.id}] ${httpContext.method}: ${httpContext.path}")
|
Logger.i(TAG, "[${req.id}] ${httpContext.method}: ${httpContext.path}")
|
||||||
@@ -107,7 +108,7 @@ class ManagedHttpServer(private val _requestedPort: Int = 0) {
|
|||||||
Logger.e(TAG, "Failed to handle client request.", e);
|
Logger.e(TAG, "Failed to handle client request.", e);
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
requestReader.close();
|
requestStream.close();
|
||||||
responseStream.close();
|
responseStream.close();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -115,36 +116,82 @@ class ManagedHttpServer(private val _requestedPort: Int = 0) {
|
|||||||
|
|
||||||
fun getHandler(method: String, path: String) : HttpHandler? {
|
fun getHandler(method: String, path: String) : HttpHandler? {
|
||||||
synchronized(_handlers) {
|
synchronized(_handlers) {
|
||||||
//TODO: Support regex paths?
|
if (method == "HEAD") {
|
||||||
if(method == "HEAD")
|
return _headHandlers[path]
|
||||||
return _handlers.firstOrNull { it.path == path && (it.allowHEAD || it.method == "HEAD") }
|
}
|
||||||
return _handlers.firstOrNull { it.method == method && it.path == path };
|
|
||||||
|
val handlerMap = _handlers[method] ?: return null
|
||||||
|
return handlerMap[path]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun addHandler(handler: HttpHandler, withHEAD: Boolean = false) : HttpHandler {
|
fun addHandler(handler: HttpHandler, withHEAD: Boolean = false) : HttpHandler {
|
||||||
synchronized(_handlers) {
|
synchronized(_handlers) {
|
||||||
_handlers.add(handler);
|
|
||||||
handler.allowHEAD = withHEAD;
|
handler.allowHEAD = withHEAD;
|
||||||
|
|
||||||
|
var handlerMap: HashMap<String, HttpHandler>? = _handlers[handler.method];
|
||||||
|
if (handlerMap == null) {
|
||||||
|
handlerMap = hashMapOf()
|
||||||
|
_handlers[handler.method] = handlerMap
|
||||||
|
}
|
||||||
|
|
||||||
|
handlerMap[handler.path] = handler;
|
||||||
|
if (handler.allowHEAD || handler.method == "HEAD") {
|
||||||
|
_headHandlers[handler.path] = handler
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return handler;
|
return handler;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun addHandlerWithAllowAllOptions(handler: HttpHandler, withHEAD: Boolean = false) : HttpHandler {
|
||||||
|
val allowedMethods = arrayListOf(handler.method, "OPTIONS")
|
||||||
|
if (withHEAD) {
|
||||||
|
allowedMethods.add("HEAD")
|
||||||
|
}
|
||||||
|
|
||||||
|
val tag = handler.tag
|
||||||
|
if (tag != null) {
|
||||||
|
addHandler(HttpOptionsAllowHandler(handler.path, allowedMethods).withTag(tag))
|
||||||
|
} else {
|
||||||
|
addHandler(HttpOptionsAllowHandler(handler.path, allowedMethods))
|
||||||
|
}
|
||||||
|
|
||||||
|
return addHandler(handler, withHEAD)
|
||||||
|
}
|
||||||
|
|
||||||
fun removeHandler(method: String, path: String) {
|
fun removeHandler(method: String, path: String) {
|
||||||
synchronized(_handlers) {
|
synchronized(_handlers) {
|
||||||
val handler = getHandler(method, path);
|
val handlerMap = _handlers[method] ?: return
|
||||||
if(handler != null)
|
val handler = handlerMap.remove(path) ?: return
|
||||||
_handlers.remove(handler);
|
if (method == "HEAD" || handler.allowHEAD) {
|
||||||
|
_headHandlers.remove(path)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun removeAllHandlers(tag: String? = null) {
|
fun removeAllHandlers(tag: String? = null) {
|
||||||
synchronized(_handlers) {
|
synchronized(_handlers) {
|
||||||
if(tag == null)
|
if(tag == null)
|
||||||
_handlers.clear();
|
_handlers.clear();
|
||||||
else
|
else {
|
||||||
_handlers.removeIf { it.tag == tag };
|
for (pair in _handlers) {
|
||||||
|
val toRemove = ArrayList<String>()
|
||||||
|
for (innerPair in pair.value) {
|
||||||
|
if (innerPair.value.tag == tag) {
|
||||||
|
toRemove.add(innerPair.key)
|
||||||
|
|
||||||
|
if (pair.key == "HEAD" || innerPair.value.allowHEAD) {
|
||||||
|
_headHandlers.remove(innerPair.key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (x in toRemove)
|
||||||
|
pair.value.remove(x)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun addBridgeHandlers(obj: Any, tag: String? = null) {
|
fun addBridgeHandlers(obj: Any, tag: String? = null) {
|
||||||
val tagToUse = tag ?: obj.javaClass.name;
|
//val tagToUse = tag ?: obj.javaClass.name;
|
||||||
val getMethods = obj::class.java.declaredMethods
|
val getMethods = obj::class.java.declaredMethods
|
||||||
.filter { it.getAnnotation(HttpGET::class.java) != null }
|
.filter { it.getAnnotation(HttpGET::class.java) != null }
|
||||||
.map { Pair<Method, HttpGET>(it, it.getAnnotation(HttpGET::class.java)!!) }
|
.map { Pair<Method, HttpGET>(it, it.getAnnotation(HttpGET::class.java)!!) }
|
||||||
@@ -164,13 +211,13 @@ class ManagedHttpServer(private val _requestedPort: Int = 0) {
|
|||||||
addHandler(HttpFuntionHandler("GET", getMethod.second.path) { getMethod.first.invoke(obj, it) }).apply {
|
addHandler(HttpFuntionHandler("GET", getMethod.second.path) { getMethod.first.invoke(obj, it) }).apply {
|
||||||
if(!getMethod.second.contentType.isEmpty())
|
if(!getMethod.second.contentType.isEmpty())
|
||||||
this.withContentType(getMethod.second.contentType);
|
this.withContentType(getMethod.second.contentType);
|
||||||
}.withContentType(getMethod.second.contentType ?: "");
|
}.withContentType(getMethod.second.contentType);
|
||||||
for(postMethod in postMethods)
|
for(postMethod in postMethods)
|
||||||
if(postMethod.first.parameterTypes.firstOrNull() == HttpContext::class.java && postMethod.first.parameterCount == 1)
|
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(HttpFuntionHandler("POST", postMethod.second.path) { postMethod.first.invoke(obj, it) }).apply {
|
||||||
if(!postMethod.second.contentType.isEmpty())
|
if(!postMethod.second.contentType.isEmpty())
|
||||||
this.withContentType(postMethod.second.contentType);
|
this.withContentType(postMethod.second.contentType);
|
||||||
}.withContentType(postMethod.second.contentType ?: "");
|
}.withContentType(postMethod.second.contentType);
|
||||||
|
|
||||||
for(getField in getFields) {
|
for(getField in getFields) {
|
||||||
getField.first.isAccessible = true;
|
getField.first.isAccessible = true;
|
||||||
@@ -184,13 +231,13 @@ class ManagedHttpServer(private val _requestedPort: Int = 0) {
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
it.respondCode(204);
|
it.respondCode(204);
|
||||||
}).withContentType(getField.second.contentType ?: "");
|
}).withContentType(getField.second.contentType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun keepAliveLoop(requestReader: BufferedReader, responseStream: OutputStream, requestId: String, handler: (HttpContext)->Unit) {
|
private fun keepAliveLoop(requestReader: BufferedInputStream, responseStream: OutputStream, requestId: String, handler: (HttpContext)->Unit) {
|
||||||
val stopCount = _stopCount;
|
val stopCount = _stopCount;
|
||||||
var keepAlive = false;
|
var keepAlive: Boolean;
|
||||||
var requestsMax = 0;
|
var requestsMax = 0;
|
||||||
var requestsTotal = 0;
|
var requestsTotal = 0;
|
||||||
do {
|
do {
|
||||||
@@ -240,11 +287,13 @@ class ManagedHttpServer(private val _requestedPort: Int = 0) {
|
|||||||
for (intf in NetworkInterface.getNetworkInterfaces()) {
|
for (intf in NetworkInterface.getNetworkInterfaces()) {
|
||||||
for (addr in intf.inetAddresses) {
|
for (addr in intf.inetAddresses) {
|
||||||
if (!addr.isLoopbackAddress) {
|
if (!addr.isLoopbackAddress) {
|
||||||
val ipString: String = addr.hostAddress;
|
val ipString: String = addr.hostAddress ?: continue
|
||||||
val isIPv4 = ipString.indexOf(':') < 0;
|
val isIPv4 = ipString.indexOf(':') < 0
|
||||||
if (!isIPv4)
|
if (!isIPv4) {
|
||||||
continue;
|
continue
|
||||||
addresses.add(addr);
|
}
|
||||||
|
|
||||||
|
addresses.add(addr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-3
@@ -1,6 +1,3 @@
|
|||||||
package com.futo.platformplayer.api.http.server.exceptions
|
package com.futo.platformplayer.api.http.server.exceptions
|
||||||
|
|
||||||
import java.net.SocketTimeoutException
|
|
||||||
import java.util.concurrent.TimeoutException
|
|
||||||
|
|
||||||
class EmptyRequestException(msg: String) : Exception(msg) {}
|
class EmptyRequestException(msg: String) : Exception(msg) {}
|
||||||
-1
@@ -7,7 +7,6 @@ class HttpConstantHandler(method: String, path: String, val content: String, val
|
|||||||
val headers = this.headers.clone();
|
val headers = this.headers.clone();
|
||||||
if(contentType != null)
|
if(contentType != null)
|
||||||
headers["Content-Type"] = contentType;
|
headers["Content-Type"] = contentType;
|
||||||
headers["Content-Length"] = content.length.toString();
|
|
||||||
|
|
||||||
httpContext.respondCode(200, headers, content);
|
httpContext.respondCode(200, headers, content);
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-22
@@ -1,14 +1,16 @@
|
|||||||
package com.futo.platformplayer.api.http.server.handlers
|
package com.futo.platformplayer.api.http.server.handlers
|
||||||
|
|
||||||
import com.futo.platformplayer.api.http.server.HttpContext
|
import com.futo.platformplayer.api.http.server.HttpContext
|
||||||
|
import com.futo.platformplayer.api.http.server.HttpHeaders
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.zip.GZIPOutputStream
|
import java.util.zip.GZIPOutputStream
|
||||||
|
|
||||||
class HttpFileHandler(method: String, path: String, private val contentType: String, private val filePath: String, private val closeAfterRequest: Boolean = false): HttpHandler(method, path) {
|
class HttpFileHandler(method: String, path: String, private val contentType: String, private val filePath: String): HttpHandler(method, path) {
|
||||||
override fun handle(httpContext: HttpContext) {
|
override fun handle(httpContext: HttpContext) {
|
||||||
val requestHeaders = httpContext.headers;
|
val requestHeaders = httpContext.headers;
|
||||||
val responseHeaders = this.headers.clone();
|
val responseHeaders = this.headers.clone();
|
||||||
@@ -30,19 +32,13 @@ class HttpFileHandler(method: String, path: String, private val contentType: Str
|
|||||||
|
|
||||||
responseHeaders["Content-Disposition"] = "attachment; filename=\"${file.name.replace("\"", "\\\"")}\""
|
responseHeaders["Content-Disposition"] = "attachment; filename=\"${file.name.replace("\"", "\\\"")}\""
|
||||||
|
|
||||||
val acceptEncoding = requestHeaders["Accept-Encoding"]
|
|
||||||
val shouldGzip = acceptEncoding != null && acceptEncoding.split(',').any { it.trim().equals("gzip", ignoreCase = true) || it == "*" }
|
|
||||||
if (shouldGzip) {
|
|
||||||
responseHeaders["Content-Encoding"] = "gzip"
|
|
||||||
}
|
|
||||||
|
|
||||||
val range = requestHeaders["Range"]
|
val range = requestHeaders["Range"]
|
||||||
var start: Long
|
val start: Long
|
||||||
val end: Long
|
val end: Long
|
||||||
if (range != null && range.startsWith("bytes=")) {
|
if (range != null && range.startsWith("bytes=")) {
|
||||||
val parts = range.substring(6).split("-")
|
val parts = range.substring(6).split("-")
|
||||||
start = parts[0].toLong()
|
start = parts[0].toLong()
|
||||||
end = parts.getOrNull(1)?.toLong() ?: (file.length() - 1)
|
end = parts.getOrNull(1)?.toLongOrNull() ?: (file.length() - 1)
|
||||||
responseHeaders["Content-Range"] = "bytes $start-$end/${file.length()}"
|
responseHeaders["Content-Range"] = "bytes $start-$end/${file.length()}"
|
||||||
} else {
|
} else {
|
||||||
start = 0
|
start = 0
|
||||||
@@ -51,18 +47,19 @@ class HttpFileHandler(method: String, path: String, private val contentType: Str
|
|||||||
|
|
||||||
var totalBytesSent = 0
|
var totalBytesSent = 0
|
||||||
val contentLength = end - start + 1
|
val contentLength = end - start + 1
|
||||||
Logger.i(TAG, "Sending $contentLength bytes (start: $start, end: $end, shouldGzip: $shouldGzip)")
|
|
||||||
responseHeaders["Content-Length"] = contentLength.toString()
|
responseHeaders["Content-Length"] = contentLength.toString()
|
||||||
|
Logger.i(TAG, "Sending $contentLength bytes (start: $start, end: $end)")
|
||||||
|
|
||||||
file.inputStream().use { inputStream ->
|
file.inputStream().use { inputStream ->
|
||||||
httpContext.respond(if (range == null) 200 else 206, responseHeaders) { responseStream ->
|
httpContext.respond(if (range != null) 206 else 200, responseHeaders) { responseStream ->
|
||||||
try {
|
try {
|
||||||
val buffer = ByteArray(8192)
|
val buffer = ByteArray(8192)
|
||||||
inputStream.skip(start)
|
inputStream.skip(start)
|
||||||
|
var current = start
|
||||||
|
|
||||||
val outputStream = if (shouldGzip) GZIPOutputStream(responseStream) else responseStream
|
val outputStream = responseStream
|
||||||
while (true) {
|
while (true) {
|
||||||
val expectedBytesRead = (end - start + 1).coerceAtMost(buffer.size.toLong());
|
val expectedBytesRead = (end - current + 1).coerceAtMost(buffer.size.toLong());
|
||||||
val bytesRead = inputStream.read(buffer);
|
val bytesRead = inputStream.read(buffer);
|
||||||
if (bytesRead < 0) {
|
if (bytesRead < 0) {
|
||||||
Logger.i(TAG, "End of file reached")
|
Logger.i(TAG, "End of file reached")
|
||||||
@@ -73,27 +70,21 @@ class HttpFileHandler(method: String, path: String, private val contentType: Str
|
|||||||
outputStream.write(buffer, 0, bytesToSend)
|
outputStream.write(buffer, 0, bytesToSend)
|
||||||
|
|
||||||
totalBytesSent += bytesToSend
|
totalBytesSent += bytesToSend
|
||||||
Logger.v(TAG, "Sent bytes $start-${start + bytesToSend}, totalBytesSent=$totalBytesSent")
|
Logger.v(TAG, "Sent bytes $current-${current + bytesToSend}, totalBytesSent=$totalBytesSent")
|
||||||
|
|
||||||
start += bytesToSend.toLong()
|
current += bytesToSend.toLong()
|
||||||
if (start >= end) {
|
if (current >= end) {
|
||||||
Logger.i(TAG, "Expected amount of bytes sent")
|
Logger.i(TAG, "Expected amount of bytes sent")
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.i(TAG, "Finished sending file (segment)")
|
Logger.i(TAG, "Finished sending file (segment)")
|
||||||
|
|
||||||
if (shouldGzip) (outputStream as GZIPOutputStream).finish()
|
|
||||||
outputStream.flush()
|
outputStream.flush()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
httpContext.respondCode(500, headers)
|
httpContext.respondCode(500, headers)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (closeAfterRequest) {
|
|
||||||
httpContext.keepAlive = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ abstract class HttpHandler(val method: String, val path: String) {
|
|||||||
headers.put(key, value);
|
headers.put(key, value);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun withContentType(contentType: String) = withHeader("Content-Type", contentType);
|
fun withContentType(contentType: String) = withHeader("Content-Type", contentType);
|
||||||
|
|
||||||
fun withTag(tag: String) : HttpHandler {
|
fun withTag(tag: String) : HttpHandler {
|
||||||
|
|||||||
+9
-10
@@ -2,19 +2,18 @@ package com.futo.platformplayer.api.http.server.handlers
|
|||||||
|
|
||||||
import com.futo.platformplayer.api.http.server.HttpContext
|
import com.futo.platformplayer.api.http.server.HttpContext
|
||||||
|
|
||||||
class HttpOptionsAllowHandler(path: String) : HttpHandler("OPTIONS", path) {
|
class HttpOptionsAllowHandler(path: String, val allowedMethods: List<String> = listOf()) : HttpHandler("OPTIONS", path) {
|
||||||
override fun handle(httpContext: HttpContext) {
|
override fun handle(httpContext: HttpContext) {
|
||||||
//Just allow whatever is requested
|
val newHeaders = headers.clone()
|
||||||
|
newHeaders.put("Access-Control-Allow-Origin", "*")
|
||||||
|
|
||||||
val requestedOrigin = httpContext.headers.getOrDefault("Access-Control-Request-Origin", "");
|
if (allowedMethods.isNotEmpty()) {
|
||||||
val requestedMethods = httpContext.headers.getOrDefault("Access-Control-Request-Method", "");
|
newHeaders.put("Access-Control-Allow-Methods", allowedMethods.map { it.uppercase() }.joinToString(", "))
|
||||||
val requestedHeaders = httpContext.headers.getOrDefault("Access-Control-Request-Headers", "");
|
} else {
|
||||||
|
newHeaders.put("Access-Control-Allow-Methods", "*")
|
||||||
val newHeaders = headers.clone();
|
}
|
||||||
newHeaders.put("Allow", requestedMethods);
|
|
||||||
newHeaders.put("Access-Control-Allow-Methods", requestedMethods);
|
|
||||||
newHeaders.put("Access-Control-Allow-Headers", "*");
|
|
||||||
|
|
||||||
|
newHeaders.put("Access-Control-Allow-Headers", "*")
|
||||||
httpContext.respondCode(200, newHeaders);
|
httpContext.respondCode(200, newHeaders);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+160
-6
@@ -1,11 +1,20 @@
|
|||||||
package com.futo.platformplayer.api.http.server.handlers
|
package com.futo.platformplayer.api.http.server.handlers
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
import com.futo.platformplayer.api.http.server.HttpContext
|
import com.futo.platformplayer.api.http.server.HttpContext
|
||||||
import com.futo.platformplayer.api.http.server.HttpHeaders
|
import com.futo.platformplayer.api.http.server.HttpHeaders
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.parsers.HttpResponseParser
|
||||||
|
import com.futo.platformplayer.readLine
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.lang.Exception
|
||||||
|
import java.net.Socket
|
||||||
|
import javax.net.ssl.SSLSocketFactory
|
||||||
|
|
||||||
class HttpProxyHandler(method: String, path: String, val targetUrl: String): HttpHandler(method, path) {
|
class HttpProxyHandler(method: String, path: String, val targetUrl: String, private val useTcp: Boolean = false): HttpHandler(method, path) {
|
||||||
var content: String? = null;
|
var content: String? = null;
|
||||||
var contentType: String? = null;
|
var contentType: String? = null;
|
||||||
|
|
||||||
@@ -17,10 +26,17 @@ class HttpProxyHandler(method: String, path: String, val targetUrl: String): Htt
|
|||||||
private var _injectHost = false;
|
private var _injectHost = false;
|
||||||
private var _injectReferer = false;
|
private var _injectReferer = false;
|
||||||
|
|
||||||
|
|
||||||
private val _client = ManagedHttpClient();
|
private val _client = ManagedHttpClient();
|
||||||
|
|
||||||
override fun handle(context: HttpContext) {
|
override fun handle(context: HttpContext) {
|
||||||
|
if (useTcp) {
|
||||||
|
handleWithTcp(context)
|
||||||
|
} else {
|
||||||
|
handleWithOkHttp(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleWithOkHttp(context: HttpContext) {
|
||||||
val proxyHeaders = HashMap<String, String>();
|
val proxyHeaders = HashMap<String, String>();
|
||||||
for (header in context.headers.filter { !_ignoreRequestHeaders.contains(it.key.lowercase()) })
|
for (header in context.headers.filter { !_ignoreRequestHeaders.contains(it.key.lowercase()) })
|
||||||
proxyHeaders[header.key] = header.value;
|
proxyHeaders[header.key] = header.value;
|
||||||
@@ -34,8 +50,8 @@ class HttpProxyHandler(method: String, path: String, val targetUrl: String): Htt
|
|||||||
proxyHeaders.put("Referer", targetUrl);
|
proxyHeaders.put("Referer", targetUrl);
|
||||||
|
|
||||||
val useMethod = if (method == "inherit") context.method else method;
|
val useMethod = if (method == "inherit") context.method else method;
|
||||||
//Logger.i(TAG, "Proxied Request ${useMethod}: ${targetUrl}");
|
Logger.i(TAG, "handleWithOkHttp Proxied Request ${useMethod}: ${targetUrl}");
|
||||||
//Logger.i(TAG, "Headers:" + proxyHeaders.map { "${it.key}: ${it.value}" }.joinToString("\n"));
|
Logger.i(TAG, "handleWithOkHttp Headers:" + proxyHeaders.map { "${it.key}: ${it.value}" }.joinToString("\n"));
|
||||||
|
|
||||||
val resp = when (useMethod) {
|
val resp = when (useMethod) {
|
||||||
"GET" -> _client.get(targetUrl, proxyHeaders);
|
"GET" -> _client.get(targetUrl, proxyHeaders);
|
||||||
@@ -44,8 +60,8 @@ class HttpProxyHandler(method: String, path: String, val targetUrl: String): Htt
|
|||||||
else -> _client.requestMethod(useMethod, targetUrl, proxyHeaders);
|
else -> _client.requestMethod(useMethod, targetUrl, proxyHeaders);
|
||||||
};
|
};
|
||||||
|
|
||||||
//Logger.i(TAG, "Proxied Response [${resp.code}]");
|
Logger.i(TAG, "Proxied Response [${resp.code}]");
|
||||||
val headersFiltered = HttpHeaders(resp.getHeadersFlat().filter { !_ignoreRequestHeaders.contains(it.key.lowercase()) });
|
val headersFiltered = HttpHeaders(resp.getHeadersFlat().filter { !_ignoreResponseHeaders.contains(it.key.lowercase()) });
|
||||||
for(newHeader in headers)
|
for(newHeader in headers)
|
||||||
headersFiltered.put(newHeader.key, newHeader.value);
|
headersFiltered.put(newHeader.key, newHeader.value);
|
||||||
|
|
||||||
@@ -65,6 +81,140 @@ class HttpProxyHandler(method: String, path: String, val targetUrl: String): Htt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleWithTcp(context: HttpContext) {
|
||||||
|
if (content != null)
|
||||||
|
throw NotImplementedError("Content body is not supported")
|
||||||
|
|
||||||
|
val proxyHeaders = HashMap<String, String>();
|
||||||
|
for (header in context.headers.filter { !_ignoreRequestHeaders.contains(it.key.lowercase()) })
|
||||||
|
proxyHeaders[header.key] = header.value;
|
||||||
|
for (injectHeader in _injectRequestHeader)
|
||||||
|
proxyHeaders[injectHeader.first] = injectHeader.second;
|
||||||
|
|
||||||
|
val parsed = Uri.parse(targetUrl);
|
||||||
|
if(_injectHost)
|
||||||
|
proxyHeaders.put("Host", parsed.host!!);
|
||||||
|
if(_injectReferer)
|
||||||
|
proxyHeaders.put("Referer", targetUrl);
|
||||||
|
|
||||||
|
val useMethod = if (method == "inherit") context.method else method;
|
||||||
|
Logger.i(TAG, "handleWithTcp Proxied Request ${useMethod}: ${parsed}");
|
||||||
|
Logger.i(TAG, "handleWithTcp Headers:" + proxyHeaders.map { "${it.key}: ${it.value}" }.joinToString("\n"));
|
||||||
|
|
||||||
|
makeTcpRequest(proxyHeaders, useMethod, parsed, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun makeTcpRequest(proxyHeaders: HashMap<String, String>, useMethod: String, parsed: Uri, context: HttpContext) {
|
||||||
|
val requestBuilder = StringBuilder()
|
||||||
|
requestBuilder.append("$useMethod $parsed HTTP/1.1\r\n")
|
||||||
|
proxyHeaders.forEach { (key, value) -> requestBuilder.append("$key: $value\r\n") }
|
||||||
|
requestBuilder.append("\r\n")
|
||||||
|
|
||||||
|
val port = if (parsed.port == -1) {
|
||||||
|
when (parsed.scheme) {
|
||||||
|
"https" -> 443
|
||||||
|
"http" -> 80
|
||||||
|
else -> throw Exception("Unhandled scheme")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
parsed.port
|
||||||
|
}
|
||||||
|
|
||||||
|
val socket = if (parsed.scheme == "https") {
|
||||||
|
val sslSocketFactory = SSLSocketFactory.getDefault() as SSLSocketFactory
|
||||||
|
sslSocketFactory.createSocket(parsed.host, port)
|
||||||
|
} else {
|
||||||
|
Socket(parsed.host, port)
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.use { s ->
|
||||||
|
s.getOutputStream().write(requestBuilder.toString().encodeToByteArray())
|
||||||
|
|
||||||
|
val inputStream = s.getInputStream()
|
||||||
|
val resp = HttpResponseParser(inputStream)
|
||||||
|
if (resp.statusCode == 302) {
|
||||||
|
val location = resp.location!!
|
||||||
|
Logger.i(TAG, "handleWithTcp Proxied ${resp.statusCode} following redirect to $location");
|
||||||
|
makeTcpRequest(proxyHeaders, useMethod, Uri.parse(location)!!, context)
|
||||||
|
} else {
|
||||||
|
val isChunked = resp.transferEncoding.equals("chunked", ignoreCase = true)
|
||||||
|
val contentLength = resp.contentLength.toInt()
|
||||||
|
|
||||||
|
val headersFiltered = HttpHeaders(resp.headers.filter { !_ignoreResponseHeaders.contains(it.key.lowercase()) });
|
||||||
|
for (newHeader in headers)
|
||||||
|
headersFiltered.put(newHeader.key, newHeader.value);
|
||||||
|
|
||||||
|
context.respond(resp.statusCode, headersFiltered) { responseStream ->
|
||||||
|
if (isChunked) {
|
||||||
|
Logger.i(TAG, "handleWithTcp handleChunkedTransfer");
|
||||||
|
handleChunkedTransfer(inputStream, responseStream)
|
||||||
|
} else if (contentLength > 0) {
|
||||||
|
Logger.i(TAG, "handleWithTcp transferFixedLengthContent $contentLength");
|
||||||
|
transferFixedLengthContent(inputStream, responseStream, contentLength)
|
||||||
|
} else if (contentLength == -1) {
|
||||||
|
Logger.i(TAG, "handleWithTcp transferUntilEndOfStream");
|
||||||
|
transferUntilEndOfStream(inputStream, responseStream)
|
||||||
|
} else {
|
||||||
|
Logger.i(TAG, "handleWithTcp no content");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleChunkedTransfer(inputStream: InputStream, responseStream: OutputStream) {
|
||||||
|
var line: String?
|
||||||
|
val buffer = ByteArray(8192)
|
||||||
|
|
||||||
|
while (inputStream.readLine().also { line = it } != null) {
|
||||||
|
val size = line!!.trim().toInt(16)
|
||||||
|
|
||||||
|
responseStream.write(line!!.encodeToByteArray())
|
||||||
|
responseStream.write("\r\n".encodeToByteArray())
|
||||||
|
|
||||||
|
if (size == 0) {
|
||||||
|
inputStream.skip(2)
|
||||||
|
responseStream.write("\r\n".encodeToByteArray())
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalRead = 0
|
||||||
|
while (totalRead < size) {
|
||||||
|
val read = inputStream.read(buffer, 0, minOf(buffer.size, size - totalRead))
|
||||||
|
if (read == -1) break
|
||||||
|
responseStream.write(buffer, 0, read)
|
||||||
|
totalRead += read
|
||||||
|
}
|
||||||
|
|
||||||
|
inputStream.skip(2)
|
||||||
|
responseStream.write("\r\n".encodeToByteArray())
|
||||||
|
responseStream.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun transferFixedLengthContent(inputStream: InputStream, responseStream: OutputStream, contentLength: Int) {
|
||||||
|
val buffer = ByteArray(8192)
|
||||||
|
var totalRead = 0
|
||||||
|
while (totalRead < contentLength) {
|
||||||
|
val read = inputStream.read(buffer, 0, minOf(buffer.size, contentLength - totalRead))
|
||||||
|
if (read == -1) break
|
||||||
|
responseStream.write(buffer, 0, read)
|
||||||
|
totalRead += read
|
||||||
|
}
|
||||||
|
|
||||||
|
responseStream.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun transferUntilEndOfStream(inputStream: InputStream, responseStream: OutputStream) {
|
||||||
|
val buffer = ByteArray(8192)
|
||||||
|
var read: Int
|
||||||
|
while (inputStream.read(buffer).also { read = it } >= 0) {
|
||||||
|
responseStream.write(buffer, 0, read)
|
||||||
|
}
|
||||||
|
|
||||||
|
responseStream.flush()
|
||||||
|
}
|
||||||
|
|
||||||
fun withContent(body: String) : HttpProxyHandler {
|
fun withContent(body: String) : HttpProxyHandler {
|
||||||
this.content = body;
|
this.content = body;
|
||||||
return this;
|
return this;
|
||||||
@@ -92,4 +242,8 @@ class HttpProxyHandler(method: String, path: String, val targetUrl: String): Htt
|
|||||||
_ignoreRequestHeaders.add("referer");
|
_ignoreRequestHeaders.add("referer");
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "HttpProxyHandler"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,107 +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.IPlatformPlaylist
|
|
||||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
|
||||||
import com.futo.platformplayer.models.ImageVariable
|
|
||||||
import com.futo.platformplayer.models.Playlist
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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);
|
|
||||||
if (result != null)
|
|
||||||
_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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -14,7 +14,6 @@ import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
|||||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.models.ImageVariable
|
import com.futo.platformplayer.models.ImageVariable
|
||||||
import com.futo.platformplayer.models.Playlist
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A client for a specific platform
|
* A client for a specific platform
|
||||||
@@ -86,6 +85,20 @@ interface IPlatformClient {
|
|||||||
*/
|
*/
|
||||||
fun getChannelContents(channelUrl: String, type: String? = null, order: String? = null, filters: Map<String, List<String>>? = null): IPager<IPlatformContent>;
|
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
|
* Gets the channel url associated with a claimType
|
||||||
*/
|
*/
|
||||||
@@ -108,6 +121,11 @@ interface IPlatformClient {
|
|||||||
*/
|
*/
|
||||||
fun getPlaybackTracker(url: String): IPlaybackTracker?;
|
fun getPlaybackTracker(url: String): IPlaybackTracker?;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get content recommendations
|
||||||
|
*/
|
||||||
|
fun getContentRecommendations(url: String): IPager<IPlatformContent>?;
|
||||||
|
|
||||||
|
|
||||||
//Comments
|
//Comments
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import com.caverock.androidsvg.SVG
|
|||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
|
import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
|
||||||
import com.futo.platformplayer.api.media.models.live.LiveEventComment
|
import com.futo.platformplayer.api.media.models.live.LiveEventComment
|
||||||
import com.futo.platformplayer.api.media.models.live.LiveEventDonation
|
|
||||||
import com.futo.platformplayer.api.media.models.live.LiveEventEmojis
|
import com.futo.platformplayer.api.media.models.live.LiveEventEmojis
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSLiveEventPager
|
import com.futo.platformplayer.api.media.platforms.js.models.JSLiveEventPager
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
@@ -195,7 +194,7 @@ class LiveChatManager {
|
|||||||
|
|
||||||
fun getEmojiDrawable(emoji: String, cb: (drawable: Drawable?)->Unit) {
|
fun getEmojiDrawable(emoji: String, cb: (drawable: Drawable?)->Unit) {
|
||||||
var drawable: Drawable? = null;
|
var drawable: Drawable? = null;
|
||||||
var url: String? = null;
|
var url: String?;
|
||||||
synchronized(_cache_lock) {
|
synchronized(_cache_lock) {
|
||||||
url = _cache_urls[emoji];
|
url = _cache_urls[emoji];
|
||||||
if(url != null)
|
if(url != null)
|
||||||
|
|||||||
@@ -13,10 +13,14 @@ data class PlatformClientCapabilities(
|
|||||||
val hasGetChannelUrlByClaim: Boolean = false,
|
val hasGetChannelUrlByClaim: Boolean = false,
|
||||||
val hasGetChannelTemplateByClaimMap: Boolean = false,
|
val hasGetChannelTemplateByClaimMap: Boolean = false,
|
||||||
val hasGetSearchCapabilities: Boolean = false,
|
val hasGetSearchCapabilities: Boolean = false,
|
||||||
|
val hasGetSearchChannelContentsCapabilities: Boolean = false,
|
||||||
val hasGetChannelCapabilities: Boolean = false,
|
val hasGetChannelCapabilities: Boolean = false,
|
||||||
val hasGetLiveEvents: Boolean = false,
|
val hasGetLiveEvents: Boolean = false,
|
||||||
val hasGetLiveChatWindow: 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
|
||||||
) {
|
) {
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -20,7 +20,7 @@ class PlatformMultiClientPool {
|
|||||||
val pool = synchronized(_clientPools) {
|
val pool = synchronized(_clientPools) {
|
||||||
if(!_clientPools.containsKey(parentClient))
|
if(!_clientPools.containsKey(parentClient))
|
||||||
_clientPools[parentClient] = PlatformClientPool(parentClient, _name).apply {
|
_clientPools[parentClient] = PlatformClientPool(parentClient, _name).apply {
|
||||||
this.onDead.subscribe { client, pool ->
|
this.onDead.subscribe { _, pool ->
|
||||||
synchronized(_clientPools) {
|
synchronized(_clientPools) {
|
||||||
if(_clientPools[parentClient] == pool)
|
if(_clientPools[parentClient] == pool)
|
||||||
_clientPools.remove(parentClient);
|
_clientPools.remove(parentClient);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ open class PlatformAuthorLink {
|
|||||||
val id: PlatformID;
|
val id: PlatformID;
|
||||||
val name: String;
|
val name: String;
|
||||||
val url: String;
|
val url: String;
|
||||||
val thumbnail: String?;
|
var thumbnail: String?;
|
||||||
var subscribers: Long? = null; //Optional
|
var subscribers: Long? = null; //Optional
|
||||||
|
|
||||||
constructor(id: PlatformID, name: String, url: String, thumbnail: String? = null, subscribers: Long? = null)
|
constructor(id: PlatformID, name: String, url: String, thumbnail: String? = null, subscribers: Long? = null)
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ class ResultCapabilities(
|
|||||||
const val TYPE_LIVE = "LIVE";
|
const val TYPE_LIVE = "LIVE";
|
||||||
const val TYPE_POSTS = "POSTS";
|
const val TYPE_POSTS = "POSTS";
|
||||||
const val TYPE_MIXED = "MIXED";
|
const val TYPE_MIXED = "MIXED";
|
||||||
|
const val TYPE_SUBSCRIPTIONS = "SUBSCRIPTIONS";
|
||||||
|
|
||||||
const val ORDER_CHONOLOGICAL = "CHRONOLOGICAL";
|
const val ORDER_CHONOLOGICAL = "CHRONOLOGICAL";
|
||||||
|
|
||||||
@@ -63,7 +64,6 @@ class FilterGroup(
|
|||||||
val isMultiSelect: Boolean,
|
val isMultiSelect: Boolean,
|
||||||
val id: String? = null
|
val id: String? = null
|
||||||
) {
|
) {
|
||||||
@kotlinx.serialization.Transient
|
|
||||||
val idOrName: String get() = id ?: name;
|
val idOrName: String get() = id ?: name;
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package com.futo.platformplayer.api.media.models
|
|||||||
import com.caoccao.javet.values.reference.V8ValueArray
|
import com.caoccao.javet.values.reference.V8ValueArray
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
|
import com.futo.platformplayer.engine.V8PluginConfig
|
||||||
|
import com.futo.platformplayer.getOrDefault
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
|
||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
@@ -31,7 +33,7 @@ class Thumbnails {
|
|||||||
fun fromV8(config: IV8PluginConfig, value: V8ValueObject): Thumbnails {
|
fun fromV8(config: IV8PluginConfig, value: V8ValueObject): Thumbnails {
|
||||||
return Thumbnails((value.getOrThrow<V8ValueArray>(config, "sources", "Thumbnails"))
|
return Thumbnails((value.getOrThrow<V8ValueArray>(config, "sources", "Thumbnails"))
|
||||||
.toArray()
|
.toArray()
|
||||||
.map { Thumbnail.fromV8(it as V8ValueObject) }
|
.map { Thumbnail.fromV8(config, it as V8ValueObject) }
|
||||||
.toTypedArray());
|
.toTypedArray());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -40,10 +42,10 @@ class Thumbnails {
|
|||||||
data class Thumbnail(val url : String?, val quality : Int = 0) {
|
data class Thumbnail(val url : String?, val quality : Int = 0) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromV8(value: V8ValueObject): Thumbnail {
|
fun fromV8(config: IV8PluginConfig, value: V8ValueObject): Thumbnail {
|
||||||
return Thumbnail(
|
return Thumbnail(
|
||||||
value.getString("url"),
|
value.getOrDefault<String>(config,"url", "Thumbnail", null),
|
||||||
value.getInteger("quality"));
|
value.getOrDefault(config, "quality", "Thumbnail", 0) ?: 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
+4
@@ -37,6 +37,10 @@ class SerializedChannel(
|
|||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isSameUrl(url: String): Boolean {
|
||||||
|
return this.url == url || urlAlternatives.contains(url);
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromChannel(channel: IPlatformChannel): SerializedChannel {
|
fun fromChannel(channel: IPlatformChannel): SerializedChannel {
|
||||||
return SerializedChannel(
|
return SerializedChannel(
|
||||||
|
|||||||
@@ -6,15 +6,16 @@ import com.futo.platformplayer.api.media.models.contents.ContentType
|
|||||||
interface IChapter {
|
interface IChapter {
|
||||||
val name: String;
|
val name: String;
|
||||||
val type: ChapterType;
|
val type: ChapterType;
|
||||||
val timeStart: Int;
|
val timeStart: Double;
|
||||||
val timeEnd: Int;
|
val timeEnd: Double;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class ChapterType(val value: Int) {
|
enum class ChapterType(val value: Int) {
|
||||||
NORMAL(0),
|
NORMAL(0),
|
||||||
|
|
||||||
SKIPPABLE(5),
|
SKIPPABLE(5),
|
||||||
SKIP(6);
|
SKIP(6),
|
||||||
|
SKIPONCE(7);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+8
-6
@@ -4,10 +4,7 @@ import com.futo.platformplayer.api.media.IPlatformClient
|
|||||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
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.IRating
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
|
||||||
import com.futo.platformplayer.states.StatePolycentric
|
|
||||||
import com.futo.polycentric.core.Pointer
|
import com.futo.polycentric.core.Pointer
|
||||||
import com.futo.polycentric.core.SignedEvent
|
|
||||||
import userpackage.Protocol.Reference
|
import userpackage.Protocol.Reference
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
@@ -20,16 +17,20 @@ class PolycentricPlatformComment : IPlatformComment {
|
|||||||
|
|
||||||
override val replyCount: Int?;
|
override val replyCount: Int?;
|
||||||
|
|
||||||
|
val eventPointer: Pointer;
|
||||||
val reference: Reference;
|
val reference: Reference;
|
||||||
|
val parentReference: Reference?;
|
||||||
|
|
||||||
constructor(contextUrl: String, author: PlatformAuthorLink, msg: String, rating: IRating, date: OffsetDateTime, reference: Reference, 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.contextUrl = contextUrl;
|
||||||
this.author = author;
|
this.author = author;
|
||||||
this.message = msg;
|
this.message = msg;
|
||||||
this.rating = rating;
|
this.rating = rating;
|
||||||
this.date = date;
|
this.date = date;
|
||||||
this.replyCount = replyCount;
|
this.replyCount = replyCount;
|
||||||
this.reference = reference;
|
this.eventPointer = eventPointer;
|
||||||
|
this.reference = eventPointer.toReference();
|
||||||
|
this.parentReference = parentReference;
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getReplies(client: IPlatformClient): IPager<IPlatformComment> {
|
override fun getReplies(client: IPlatformClient): IPager<IPlatformComment> {
|
||||||
@@ -37,10 +38,11 @@ class PolycentricPlatformComment : IPlatformComment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun cloneWithUpdatedReplyCount(replyCount: Int?): PolycentricPlatformComment {
|
fun cloneWithUpdatedReplyCount(replyCount: Int?): PolycentricPlatformComment {
|
||||||
return PolycentricPlatformComment(contextUrl, author, message, rating, date, reference, replyCount);
|
return PolycentricPlatformComment(contextUrl, author, message, rating, date, eventPointer, parentReference, replyCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private const val TAG = "PolycentricPlatformComment"
|
||||||
val MAX_COMMENT_SIZE = 2000
|
val MAX_COMMENT_SIZE = 2000
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13,6 +13,7 @@ enum class ContentType(val value: Int) {
|
|||||||
|
|
||||||
NESTED_VIDEO(11),
|
NESTED_VIDEO(11),
|
||||||
|
|
||||||
|
LOCKED(70),
|
||||||
|
|
||||||
PLACEHOLDER(90),
|
PLACEHOLDER(90),
|
||||||
DEFERRED(91);
|
DEFERRED(91);
|
||||||
|
|||||||
+2
@@ -10,4 +10,6 @@ interface IPlatformContentDetails : IPlatformContent {
|
|||||||
|
|
||||||
fun getComments(client: IPlatformClient): IPager<IPlatformComment>?;
|
fun getComments(client: IPlatformClient): IPager<IPlatformComment>?;
|
||||||
fun getPlaybackTracker(): IPlaybackTracker?;
|
fun getPlaybackTracker(): IPlaybackTracker?;
|
||||||
|
|
||||||
|
fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>?;
|
||||||
}
|
}
|
||||||
+1
@@ -3,4 +3,5 @@ package com.futo.platformplayer.api.media.models.live
|
|||||||
interface ILiveChatWindowDescriptor {
|
interface ILiveChatWindowDescriptor {
|
||||||
val url: String;
|
val url: String;
|
||||||
val removeElements: List<String>;
|
val removeElements: List<String>;
|
||||||
|
val removeElementsInterval: List<String>;
|
||||||
}
|
}
|
||||||
+4
-12
@@ -1,31 +1,23 @@
|
|||||||
package com.futo.platformplayer.api.media.models.live
|
package com.futo.platformplayer.api.media.models.live
|
||||||
|
|
||||||
import com.caoccao.javet.values.V8Value
|
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
import com.futo.platformplayer.api.media.models.ratings.IRating
|
|
||||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes
|
|
||||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
|
||||||
import com.futo.platformplayer.api.media.models.ratings.RatingScaler
|
|
||||||
import com.futo.platformplayer.api.media.models.ratings.RatingType
|
|
||||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
import com.futo.platformplayer.orDefault
|
|
||||||
|
|
||||||
interface IPlatformLiveEvent {
|
interface IPlatformLiveEvent {
|
||||||
val type : LiveEventType;
|
val type : LiveEventType;
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject, contextName: String = "Unknown") : IPlatformLiveEvent {
|
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject, contextName: String = "LiveEvent") : IPlatformLiveEvent {
|
||||||
val contextName = "LiveEvent";
|
val t = LiveEventType.fromInt(obj.getOrThrow<Int>(config, "type", contextName));
|
||||||
val type = LiveEventType.fromInt(obj.getOrThrow<Int>(config, "type", contextName));
|
return when(t) {
|
||||||
return when(type) {
|
|
||||||
LiveEventType.COMMENT -> LiveEventComment.fromV8(config, obj);
|
LiveEventType.COMMENT -> LiveEventComment.fromV8(config, obj);
|
||||||
LiveEventType.EMOJIS -> LiveEventEmojis.fromV8(config, obj);
|
LiveEventType.EMOJIS -> LiveEventEmojis.fromV8(config, obj);
|
||||||
LiveEventType.DONATION -> LiveEventDonation.fromV8(config, obj);
|
LiveEventType.DONATION -> LiveEventDonation.fromV8(config, obj);
|
||||||
LiveEventType.VIEWCOUNT -> LiveEventViewCount.fromV8(config, obj);
|
LiveEventType.VIEWCOUNT -> LiveEventViewCount.fromV8(config, obj);
|
||||||
LiveEventType.RAID -> LiveEventRaid.fromV8(config, obj);
|
LiveEventType.RAID -> LiveEventRaid.fromV8(config, obj);
|
||||||
else -> throw NotImplementedError("Unknown type ${type}");
|
else -> throw NotImplementedError("Unknown type $t");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
package com.futo.platformplayer.api.media.models.locked
|
||||||
|
|
||||||
|
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
|
|
||||||
|
interface IPlatformLockedContent: IPlatformContent {
|
||||||
|
val lockContentType: ContentType;
|
||||||
|
val lockDescription: String?;
|
||||||
|
val unlockUrl: String?;
|
||||||
|
val contentName: String?;
|
||||||
|
val contentThumbnails: Thumbnails;
|
||||||
|
}
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
package com.futo.platformplayer.api.media.models.modifier
|
||||||
|
|
||||||
|
class AdhocRequestModifier: IRequestModifier {
|
||||||
|
val _handler: (String, Map<String,String>)->IRequest;
|
||||||
|
override var allowByteSkip: Boolean = false;
|
||||||
|
|
||||||
|
constructor(modifyReq: (String, Map<String,String>)->IRequest) {
|
||||||
|
_handler = modifyReq;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun modifyRequest(url: String, headers: Map<String, String>): IRequest {
|
||||||
|
return _handler(url, headers);
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
package com.futo.platformplayer.api.media.models.modifier
|
||||||
|
|
||||||
|
interface IModifierOptions {
|
||||||
|
val applyAuthClient: String?;
|
||||||
|
val applyCookieClient: String?;
|
||||||
|
val applyOtherHeaders: Boolean;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.futo.platformplayer.api.media.models.modifier
|
||||||
|
|
||||||
|
interface IRequest {
|
||||||
|
val url: String?;
|
||||||
|
val headers: Map<String, String>;
|
||||||
|
}
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
package com.futo.platformplayer.api.media.models.modifier
|
||||||
|
|
||||||
|
|
||||||
|
interface IRequestModifier {
|
||||||
|
var allowByteSkip: Boolean;
|
||||||
|
fun modifyRequest(url: String, headers: Map<String, String>): IRequest
|
||||||
|
}
|
||||||
+1
@@ -7,4 +7,5 @@ interface IPlaybackTracker {
|
|||||||
|
|
||||||
fun onInit(seconds: Double);
|
fun onInit(seconds: Double);
|
||||||
fun onProgress(seconds: Double, isPlaying: Boolean);
|
fun onProgress(seconds: Double, isPlaying: Boolean);
|
||||||
|
fun onConcluded();
|
||||||
}
|
}
|
||||||
-3
@@ -1,9 +1,6 @@
|
|||||||
package com.futo.platformplayer.api.media.models.playlists
|
package com.futo.platformplayer.api.media.models.playlists
|
||||||
|
|
||||||
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.IPlatformContent
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
|
||||||
|
|
||||||
interface IPlatformPlaylist : IPlatformContent {
|
interface IPlatformPlaylist : IPlatformContent {
|
||||||
val thumbnail: String?;
|
val thumbnail: String?;
|
||||||
|
|||||||
+1
-1
@@ -8,5 +8,5 @@ interface IPlatformPlaylistDetails: IPlatformPlaylist {
|
|||||||
//TODO: Determine if this should be IPlatformContent (probably not?)
|
//TODO: Determine if this should be IPlatformContent (probably not?)
|
||||||
val contents: IPager<IPlatformVideo>;
|
val contents: IPager<IPlatformVideo>;
|
||||||
|
|
||||||
fun toPlaylist(): Playlist;
|
fun toPlaylist(onProgress: ((progress: Int) -> Unit)? = null): Playlist;
|
||||||
}
|
}
|
||||||
-4
@@ -2,10 +2,6 @@ package com.futo.platformplayer.api.media.models.post
|
|||||||
|
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
|
||||||
import com.futo.platformplayer.api.media.models.ratings.IRating
|
import com.futo.platformplayer.api.media.models.ratings.IRating
|
||||||
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
|
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.*
|
|
||||||
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A detailed video model with data including video/audio sources
|
* A detailed video model with data including video/audio sources
|
||||||
|
|||||||
@@ -14,14 +14,13 @@ interface IRating {
|
|||||||
|
|
||||||
companion object {
|
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) = obj.orDefault(default) { fromV8(config, it as V8ValueObject) };
|
||||||
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject, contextName: String = "Unknown") : IRating {
|
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject, contextName: String = "Rating") : IRating {
|
||||||
val contextName = "Rating";
|
val t = RatingType.fromInt(obj.getOrThrow<Int>(config, "type", contextName));
|
||||||
val type = RatingType.fromInt(obj.getOrThrow<Int>(config, "type", contextName));
|
return when(t) {
|
||||||
return when(type) {
|
|
||||||
RatingType.LIKES -> RatingLikes.fromV8(config, obj);
|
RatingType.LIKES -> RatingLikes.fromV8(config, obj);
|
||||||
RatingType.LIKEDISLIKES -> RatingLikeDislikes.fromV8(config, obj);
|
RatingType.LIKEDISLIKES -> RatingLikeDislikes.fromV8(config, obj);
|
||||||
RatingType.SCALE -> RatingScaler.fromV8(config, obj);
|
RatingType.SCALE -> RatingScaler.fromV8(config, obj);
|
||||||
else -> throw NotImplementedError("Unknown type ${type}");
|
else -> throw NotImplementedError("Unknown type $t");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
package com.futo.platformplayer.api.media.models.streams.sources
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
||||||
|
|
||||||
|
class HLSVariantVideoUrlSource(
|
||||||
|
override val name: String,
|
||||||
|
override val width: Int,
|
||||||
|
override val height: Int,
|
||||||
|
override val container: String,
|
||||||
|
override val codec: String,
|
||||||
|
override val bitrate: Int?,
|
||||||
|
override val duration: Long,
|
||||||
|
override val priority: Boolean,
|
||||||
|
val url: String
|
||||||
|
) : IVideoUrlSource {
|
||||||
|
override fun getVideoUrl(): String {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HLSVariantAudioUrlSource(
|
||||||
|
override val name: String,
|
||||||
|
override val bitrate: Int,
|
||||||
|
override val container: String,
|
||||||
|
override val codec: String,
|
||||||
|
override val language: String,
|
||||||
|
override val duration: Long?,
|
||||||
|
override val priority: Boolean,
|
||||||
|
val url: String
|
||||||
|
) : IAudioUrlSource {
|
||||||
|
override fun getAudioUrl(): String {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HLSVariantSubtitleUrlSource(
|
||||||
|
override val name: String,
|
||||||
|
override val url: String,
|
||||||
|
override val format: String,
|
||||||
|
) : ISubtitleSource {
|
||||||
|
override val hasFetch: Boolean = false
|
||||||
|
|
||||||
|
override fun getSubtitles(): String? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getSubtitlesURI(): Uri? {
|
||||||
|
return Uri.parse(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
package com.futo.platformplayer.api.media.models.streams.sources
|
||||||
|
|
||||||
|
interface IAudioUrlWidevineSource : IAudioUrlSource {
|
||||||
|
val bearerToken: String
|
||||||
|
val licenseUri: String
|
||||||
|
}
|
||||||
-2
@@ -1,8 +1,6 @@
|
|||||||
package com.futo.platformplayer.api.media.models.subtitles
|
package com.futo.platformplayer.api.media.models.subtitles
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Deferred
|
|
||||||
|
|
||||||
interface ISubtitleSource {
|
interface ISubtitleSource {
|
||||||
val name: String;
|
val name: String;
|
||||||
|
|||||||
+3
-4
@@ -1,13 +1,12 @@
|
|||||||
package com.futo.platformplayer.api.media.models.video
|
package com.futo.platformplayer.api.media.models.video
|
||||||
|
|
||||||
import com.futo.platformplayer.api.media.IPlatformClient
|
|
||||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
|
||||||
import com.futo.platformplayer.api.media.models.ratings.IRating
|
import com.futo.platformplayer.api.media.models.ratings.IRating
|
||||||
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
|
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.*
|
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||||
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A detailed video model with data including video/audio sources
|
* A detailed video model with data including video/audio sources
|
||||||
|
|||||||
+6
@@ -2,12 +2,17 @@ package com.futo.platformplayer.api.media.models.video
|
|||||||
|
|
||||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
|
import com.futo.platformplayer.api.media.models.locked.IPlatformLockedContent
|
||||||
import com.futo.platformplayer.api.media.models.nested.IPlatformNestedContent
|
import com.futo.platformplayer.api.media.models.nested.IPlatformNestedContent
|
||||||
import com.futo.platformplayer.api.media.models.post.IPlatformPost
|
import com.futo.platformplayer.api.media.models.post.IPlatformPost
|
||||||
import com.futo.platformplayer.serializers.PlatformContentSerializer
|
import com.futo.platformplayer.serializers.PlatformContentSerializer
|
||||||
|
import kotlinx.serialization.EncodeDefault
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
|
||||||
@kotlinx.serialization.Serializable(with = PlatformContentSerializer::class)
|
@kotlinx.serialization.Serializable(with = PlatformContentSerializer::class)
|
||||||
interface SerializedPlatformContent: IPlatformContent {
|
interface SerializedPlatformContent: IPlatformContent {
|
||||||
|
override val contentType: ContentType;
|
||||||
|
|
||||||
fun toJson() : String;
|
fun toJson() : String;
|
||||||
fun fromJson(str : String) : SerializedPlatformContent;
|
fun fromJson(str : String) : SerializedPlatformContent;
|
||||||
fun fromJsonArray(str : String) : Array<SerializedPlatformContent>;
|
fun fromJsonArray(str : String) : Array<SerializedPlatformContent>;
|
||||||
@@ -18,6 +23,7 @@ interface SerializedPlatformContent: IPlatformContent {
|
|||||||
ContentType.MEDIA -> SerializedPlatformVideo.fromVideo(content as IPlatformVideo);
|
ContentType.MEDIA -> SerializedPlatformVideo.fromVideo(content as IPlatformVideo);
|
||||||
ContentType.NESTED_VIDEO -> SerializedPlatformNestedContent.fromNested(content as IPlatformNestedContent);
|
ContentType.NESTED_VIDEO -> SerializedPlatformNestedContent.fromNested(content as IPlatformNestedContent);
|
||||||
ContentType.POST -> SerializedPlatformPost.fromPost(content as IPlatformPost);
|
ContentType.POST -> SerializedPlatformPost.fromPost(content as IPlatformPost);
|
||||||
|
ContentType.LOCKED -> SerializedPlatformLockedContent.fromLocked(content as IPlatformLockedContent);
|
||||||
else -> throw NotImplementedError("Content type ${content.contentType} not implemented");
|
else -> throw NotImplementedError("Content type ${content.contentType} not implemented");
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+62
@@ -0,0 +1,62 @@
|
|||||||
|
package com.futo.platformplayer.api.media.models.video
|
||||||
|
|
||||||
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
|
import com.futo.platformplayer.api.media.Serializer
|
||||||
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
|
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||||
|
import com.futo.platformplayer.api.media.models.locked.IPlatformLockedContent
|
||||||
|
import com.futo.platformplayer.api.media.models.nested.IPlatformNestedContent
|
||||||
|
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||||
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
|
import com.futo.polycentric.core.combineHashCodes
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
|
@kotlinx.serialization.Serializable
|
||||||
|
open class SerializedPlatformLockedContent(
|
||||||
|
override val id: PlatformID,
|
||||||
|
override val name: String,
|
||||||
|
override val author: PlatformAuthorLink,
|
||||||
|
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
||||||
|
override val datetime: OffsetDateTime?,
|
||||||
|
override val url: String,
|
||||||
|
override val shareUrl: String,
|
||||||
|
override val lockContentType: ContentType,
|
||||||
|
override val contentName: String?,
|
||||||
|
override val lockDescription: String? = null,
|
||||||
|
override val unlockUrl: String? = null,
|
||||||
|
override val contentThumbnails: Thumbnails
|
||||||
|
) : IPlatformLockedContent, SerializedPlatformContent {
|
||||||
|
override val contentType: ContentType = ContentType.LOCKED;
|
||||||
|
|
||||||
|
override fun toJson() : String {
|
||||||
|
return Json.encodeToString(this);
|
||||||
|
}
|
||||||
|
override fun fromJson(str : String) : SerializedPlatformLockedContent {
|
||||||
|
return Serializer.json.decodeFromString<SerializedPlatformLockedContent>(str);
|
||||||
|
}
|
||||||
|
override fun fromJsonArray(str : String) : Array<SerializedPlatformContent> {
|
||||||
|
return Serializer.json.decodeFromString<Array<SerializedPlatformContent>>(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromLocked(content: IPlatformLockedContent) : SerializedPlatformLockedContent {
|
||||||
|
return SerializedPlatformLockedContent(
|
||||||
|
content.id,
|
||||||
|
content.name,
|
||||||
|
content.author,
|
||||||
|
content.datetime,
|
||||||
|
content.url,
|
||||||
|
content.shareUrl,
|
||||||
|
content.lockContentType,
|
||||||
|
content.contentName,
|
||||||
|
content.lockDescription,
|
||||||
|
content.unlockUrl,
|
||||||
|
content.contentThumbnails
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -30,7 +30,7 @@ open class SerializedPlatformNestedContent(
|
|||||||
override val contentProvider: String?,
|
override val contentProvider: String?,
|
||||||
override val contentThumbnails: Thumbnails
|
override val contentThumbnails: Thumbnails
|
||||||
) : IPlatformNestedContent, SerializedPlatformContent {
|
) : IPlatformNestedContent, SerializedPlatformContent {
|
||||||
final override val contentType: ContentType get() = ContentType.NESTED_VIDEO;
|
final override val contentType: ContentType = ContentType.NESTED_VIDEO;
|
||||||
|
|
||||||
override val contentPlugin: String? = StatePlatform.instance.getContentClientOrNull(contentUrl)?.id;
|
override val contentPlugin: String? = StatePlatform.instance.getContentClientOrNull(contentUrl)?.id;
|
||||||
override val contentSupported: Boolean get() = contentPlugin != null;
|
override val contentSupported: Boolean get() = contentPlugin != null;
|
||||||
|
|||||||
+2
-1
@@ -8,6 +8,7 @@ import com.futo.platformplayer.api.media.models.contents.ContentType
|
|||||||
import com.futo.platformplayer.api.media.models.post.IPlatformPost
|
import com.futo.platformplayer.api.media.models.post.IPlatformPost
|
||||||
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||||
import com.futo.polycentric.core.combineHashCodes
|
import com.futo.polycentric.core.combineHashCodes
|
||||||
|
import kotlinx.serialization.EncodeDefault
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
@@ -26,7 +27,7 @@ open class SerializedPlatformPost(
|
|||||||
override val thumbnails: List<Thumbnails?>,
|
override val thumbnails: List<Thumbnails?>,
|
||||||
override val images: List<String>
|
override val images: List<String>
|
||||||
) : IPlatformPost, SerializedPlatformContent {
|
) : IPlatformPost, SerializedPlatformContent {
|
||||||
final override val contentType: ContentType get() = ContentType.POST;
|
override val contentType: ContentType = ContentType.POST;
|
||||||
|
|
||||||
override fun toJson() : String {
|
override fun toJson() : String {
|
||||||
return Json.encodeToString(this);
|
return Json.encodeToString(this);
|
||||||
|
|||||||
+1
-1
@@ -26,7 +26,7 @@ open class SerializedPlatformVideo(
|
|||||||
override val duration: Long,
|
override val duration: Long,
|
||||||
override val viewCount: Long,
|
override val viewCount: Long,
|
||||||
) : IPlatformVideo, SerializedPlatformContent {
|
) : IPlatformVideo, SerializedPlatformContent {
|
||||||
final override val contentType: ContentType get() = ContentType.MEDIA;
|
override val contentType: ContentType = ContentType.MEDIA;
|
||||||
|
|
||||||
override val isLive: Boolean = false;
|
override val isLive: Boolean = false;
|
||||||
|
|
||||||
|
|||||||
+2
@@ -7,6 +7,7 @@ import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
|||||||
import com.futo.platformplayer.api.media.models.Thumbnails
|
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
||||||
import com.futo.platformplayer.api.media.models.ratings.IRating
|
import com.futo.platformplayer.api.media.models.ratings.IRating
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.*
|
import com.futo.platformplayer.api.media.models.streams.sources.*
|
||||||
@@ -56,6 +57,7 @@ open class SerializedPlatformVideoDetails(
|
|||||||
|
|
||||||
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? = null;
|
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? = null;
|
||||||
override fun getPlaybackTracker(): IPlaybackTracker? = null;
|
override fun getPlaybackTracker(): IPlaybackTracker? = null;
|
||||||
|
override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? = null;
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromVideo(video : IPlatformVideoDetails, subtitleSources: List<SubtitleRawSource>) : SerializedPlatformVideoDetails {
|
fun fromVideo(video : IPlatformVideoDetails, subtitleSources: List<SubtitleRawSource>) : SerializedPlatformVideoDetails {
|
||||||
|
|||||||
-1
@@ -8,6 +8,5 @@ import com.futo.platformplayer.api.media.models.streams.sources.VideoUrlSource
|
|||||||
class SerializedVideoMuxedSourceDescriptor(
|
class SerializedVideoMuxedSourceDescriptor(
|
||||||
val _videoSources: Array<VideoUrlSource>
|
val _videoSources: Array<VideoUrlSource>
|
||||||
): VideoMuxedSourceDescriptor(), ISerializedVideoSourceDescriptor {
|
): VideoMuxedSourceDescriptor(), ISerializedVideoSourceDescriptor {
|
||||||
@kotlinx.serialization.Transient
|
|
||||||
override val videoSources: Array<IVideoSource> get() = _videoSources.map { it }.toTypedArray();
|
override val videoSources: Array<IVideoSource> get() = _videoSources.map { it }.toTypedArray();
|
||||||
};
|
};
|
||||||
+4
-3
@@ -1,15 +1,16 @@
|
|||||||
package com.futo.platformplayer.api.media.models.video
|
package com.futo.platformplayer.api.media.models.video
|
||||||
|
|
||||||
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
|
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.*
|
import com.futo.platformplayer.api.media.models.streams.sources.AudioUrlSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.VideoUrlSource
|
||||||
|
|
||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
class SerializedVideoNonMuxedSourceDescriptor(
|
class SerializedVideoNonMuxedSourceDescriptor(
|
||||||
val _videoSources: Array<VideoUrlSource>,
|
val _videoSources: Array<VideoUrlSource>,
|
||||||
val _audioSources: Array<AudioUrlSource>
|
val _audioSources: Array<AudioUrlSource>
|
||||||
): VideoUnMuxedSourceDescriptor(), ISerializedVideoSourceDescriptor {
|
): VideoUnMuxedSourceDescriptor(), ISerializedVideoSourceDescriptor {
|
||||||
@kotlinx.serialization.Transient
|
|
||||||
override val videoSources: Array<IVideoSource> get() = _videoSources.map { it }.toTypedArray();
|
override val videoSources: Array<IVideoSource> get() = _videoSources.map { it }.toTypedArray();
|
||||||
@kotlinx.serialization.Transient
|
|
||||||
override val audioSources: Array<IAudioSource> get() = _audioSources.map { it }.toTypedArray();
|
override val audioSources: Array<IAudioSource> get() = _audioSources.map { it }.toTypedArray();
|
||||||
};
|
};
|
||||||
@@ -1,14 +1,16 @@
|
|||||||
package com.futo.platformplayer.api.media.platforms.js
|
package com.futo.platformplayer.api.media.platforms.js
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.futo.platformplayer.states.StateDeveloper
|
|
||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
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.IPlatformContent
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import java.util.*
|
import com.futo.platformplayer.states.StateDeveloper
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
class DevJSClient : JSClient {
|
class DevJSClient : JSClient {
|
||||||
override val id: String
|
override val id: String
|
||||||
@@ -20,14 +22,14 @@ class DevJSClient : JSClient {
|
|||||||
|
|
||||||
val devID: String;
|
val devID: String;
|
||||||
|
|
||||||
constructor(context: Context, config: SourcePluginConfig, script: String, auth: SourceAuth? = null, captcha: SourceCaptchaData? = null, devID: String? = null): super(context, SourcePluginDescriptor(config, auth?.toEncrypted(), captcha?.toEncrypted(), listOf("DEV")), null, script) {
|
constructor(context: Context, config: SourcePluginConfig, script: String, auth: SourceAuth? = null, captcha: SourceCaptchaData? = null, devID: String? = null, settings: HashMap<String, String?>? = null): super(context, SourcePluginDescriptor(config, auth?.toEncrypted(), captcha?.toEncrypted(), listOf("DEV"), settings), null, script) {
|
||||||
_devScript = script;
|
_devScript = script;
|
||||||
_auth = auth;
|
_auth = auth;
|
||||||
_captcha = captcha;
|
_captcha = captcha;
|
||||||
this.devID = devID ?: UUID.randomUUID().toString().substring(0, 5);
|
this.devID = devID ?: UUID.randomUUID().toString().substring(0, 5);
|
||||||
|
|
||||||
onCaptchaException.subscribe { client, captcha ->
|
onCaptchaException.subscribe { client, c ->
|
||||||
StateApp.instance.handleCaptchaException(client, captcha);
|
StateApp.instance.handleCaptchaException(client, c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//TODO: Misisng auth/captcha pass on purpose?
|
//TODO: Misisng auth/captcha pass on purpose?
|
||||||
@@ -37,8 +39,8 @@ class DevJSClient : JSClient {
|
|||||||
_captcha = captcha;
|
_captcha = captcha;
|
||||||
this.devID = devID ?: UUID.randomUUID().toString().substring(0, 5);
|
this.devID = devID ?: UUID.randomUUID().toString().substring(0, 5);
|
||||||
|
|
||||||
onCaptchaException.subscribe { client, captcha ->
|
onCaptchaException.subscribe { client, c ->
|
||||||
StateApp.instance.handleCaptchaException(client, captcha);
|
StateApp.instance.handleCaptchaException(client, c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,7 +51,7 @@ class DevJSClient : JSClient {
|
|||||||
_auth = auth;
|
_auth = auth;
|
||||||
}
|
}
|
||||||
fun recreate(context: Context): DevJSClient {
|
fun recreate(context: Context): DevJSClient {
|
||||||
return DevJSClient(context, config, _devScript, _auth, _captcha, devID);
|
return DevJSClient(context, config, _devScript, _auth, _captcha, devID, descriptor.settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getCopy(): JSClient {
|
override fun getCopy(): JSClient {
|
||||||
@@ -115,7 +117,7 @@ class DevJSClient : JSClient {
|
|||||||
|
|
||||||
//Video
|
//Video
|
||||||
override fun isContentDetailsUrl(url: String): Boolean {
|
override fun isContentDetailsUrl(url: String): Boolean {
|
||||||
return StateDeveloper.instance.handleDevCall(devID, "isVideoDetailsUrl"){
|
return StateDeveloper.instance.handleDevCall(devID, "isVideoDetailsUrl(${Json.encodeToString(url)})"){
|
||||||
super.isContentDetailsUrl(url);
|
super.isContentDetailsUrl(url);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,10 @@ import android.content.Context
|
|||||||
import com.caoccao.javet.values.V8Value
|
import com.caoccao.javet.values.V8Value
|
||||||
import com.caoccao.javet.values.primitive.V8ValueBoolean
|
import com.caoccao.javet.values.primitive.V8ValueBoolean
|
||||||
import com.caoccao.javet.values.primitive.V8ValueInteger
|
import com.caoccao.javet.values.primitive.V8ValueInteger
|
||||||
import com.caoccao.javet.values.primitive.V8ValueNull
|
|
||||||
import com.caoccao.javet.values.primitive.V8ValueString
|
import com.caoccao.javet.values.primitive.V8ValueString
|
||||||
import com.caoccao.javet.values.reference.V8ValueArray
|
import com.caoccao.javet.values.reference.V8ValueArray
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.states.StatePlugins
|
|
||||||
import com.futo.platformplayer.api.media.IPlatformClient
|
import com.futo.platformplayer.api.media.IPlatformClient
|
||||||
import com.futo.platformplayer.api.media.PlatformClientCapabilities
|
import com.futo.platformplayer.api.media.PlatformClientCapabilities
|
||||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
@@ -22,43 +20,66 @@ 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.ILiveChatWindowDescriptor
|
||||||
import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
|
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.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.models.playlists.IPlatformPlaylistDetails
|
||||||
import com.futo.platformplayer.api.media.platforms.js.internal.*
|
import com.futo.platformplayer.api.media.platforms.js.internal.JSCallDocs
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.*
|
import com.futo.platformplayer.api.media.platforms.js.internal.JSDocs
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.internal.JSDocsParameter
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.internal.JSOptional
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.internal.JSParameterDocs
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.IJSContent
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.IJSContentDetails
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.JSChannel
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.JSChannelPager
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.JSChapter
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.JSComment
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.JSCommentPager
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.JSContentPager
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.JSLiveChatWindowDescriptor
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.JSLiveEventPager
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.JSPlaybackTracker
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.JSPlaylistDetails
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.JSPlaylistPager
|
||||||
import com.futo.platformplayer.api.media.structures.EmptyPager
|
import com.futo.platformplayer.api.media.structures.EmptyPager
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.constructs.Event2
|
import com.futo.platformplayer.constructs.Event2
|
||||||
import com.futo.platformplayer.engine.V8Plugin
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
import com.futo.platformplayer.engine.exceptions.PluginEngineException
|
import com.futo.platformplayer.engine.exceptions.PluginEngineException
|
||||||
import com.futo.platformplayer.engine.exceptions.PluginEngineStoppedException
|
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||||
|
import com.futo.platformplayer.engine.exceptions.ScriptException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptValidationException
|
import com.futo.platformplayer.engine.exceptions.ScriptValidationException
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.ImageVariable
|
import com.futo.platformplayer.models.ImageVariable
|
||||||
import com.futo.platformplayer.states.AnnouncementType
|
import com.futo.platformplayer.states.AnnouncementType
|
||||||
import com.futo.platformplayer.states.StateAnnouncement
|
import com.futo.platformplayer.states.StateAnnouncement
|
||||||
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
|
import com.futo.platformplayer.states.StatePlugins
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
import kotlin.Exception
|
||||||
import kotlin.reflect.full.findAnnotations
|
import kotlin.reflect.full.findAnnotations
|
||||||
import kotlin.reflect.jvm.kotlinFunction
|
import kotlin.reflect.jvm.kotlinFunction
|
||||||
|
import kotlin.streams.asSequence
|
||||||
|
|
||||||
open class JSClient : IPlatformClient {
|
open class JSClient : IPlatformClient {
|
||||||
val config: SourcePluginConfig;
|
val config: SourcePluginConfig;
|
||||||
protected val _context: Context;
|
protected val _context: Context;
|
||||||
private val _plugin: V8Plugin;
|
private val _plugin: V8Plugin;
|
||||||
private val plugin: V8Plugin get() = _plugin ?: throw IllegalStateException("Client not enabled");
|
private val plugin: V8Plugin get() = _plugin
|
||||||
|
|
||||||
var descriptor: SourcePluginDescriptor
|
var descriptor: SourcePluginDescriptor
|
||||||
private set;
|
private set;
|
||||||
|
|
||||||
private val _client: JSHttpClient;
|
private val _httpClient: JSHttpClient;
|
||||||
private val _clientAuth: JSHttpClient?;
|
private val _httpClientAuth: JSHttpClient?;
|
||||||
private var _searchCapabilities: ResultCapabilities? = null;
|
private var _searchCapabilities: ResultCapabilities? = null;
|
||||||
private var _searchChannelContentsCapabilities: ResultCapabilities? = null;
|
private var _searchChannelContentsCapabilities: ResultCapabilities? = null;
|
||||||
private var _channelCapabilities: ResultCapabilities? = null;
|
private var _channelCapabilities: ResultCapabilities? = null;
|
||||||
|
private var _peekChannelTypes: List<String>? = null;
|
||||||
|
|
||||||
protected val _script: String;
|
protected val _script: String;
|
||||||
|
|
||||||
@@ -77,7 +98,11 @@ open class JSClient : IPlatformClient {
|
|||||||
|
|
||||||
private val _busyLock = Object();
|
private val _busyLock = Object();
|
||||||
private var _busyCounter = 0;
|
private var _busyCounter = 0;
|
||||||
|
private var _busyAction = "";
|
||||||
val isBusy: Boolean get() = _busyCounter > 0;
|
val isBusy: Boolean get() = _busyCounter > 0;
|
||||||
|
val isBusyAction: String get() {
|
||||||
|
return _busyAction;
|
||||||
|
}
|
||||||
|
|
||||||
val settings: HashMap<String, String?> get() = descriptor.settings;
|
val settings: HashMap<String, String?> get() = descriptor.settings;
|
||||||
|
|
||||||
@@ -118,9 +143,9 @@ open class JSClient : IPlatformClient {
|
|||||||
_captcha = descriptor.getCaptchaData();
|
_captcha = descriptor.getCaptchaData();
|
||||||
flags = descriptor.flags.toTypedArray();
|
flags = descriptor.flags.toTypedArray();
|
||||||
|
|
||||||
_client = JSHttpClient(this, null, _captcha);
|
_httpClient = JSHttpClient(this, null, _captcha);
|
||||||
_clientAuth = JSHttpClient(this, _auth, _captcha);
|
_httpClientAuth = JSHttpClient(this, _auth, _captcha);
|
||||||
_plugin = V8Plugin(context, descriptor.config, null, _client, _clientAuth);
|
_plugin = V8Plugin(context, descriptor.config, null, _httpClient, _httpClientAuth);
|
||||||
_plugin.withDependency(context, "scripts/polyfil.js");
|
_plugin.withDependency(context, "scripts/polyfil.js");
|
||||||
_plugin.withDependency(context, "scripts/source.js");
|
_plugin.withDependency(context, "scripts/source.js");
|
||||||
|
|
||||||
@@ -136,6 +161,8 @@ open class JSClient : IPlatformClient {
|
|||||||
if(it is ScriptCaptchaRequiredException)
|
if(it is ScriptCaptchaRequiredException)
|
||||||
onCaptchaException.emit(this, it);
|
onCaptchaException.emit(this, it);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_plugin.changeAllowDevSubmit(descriptor.appSettings.allowDeveloperSubmit);
|
||||||
}
|
}
|
||||||
constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String?, script: String) {
|
constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String?, script: String) {
|
||||||
this._context = context;
|
this._context = context;
|
||||||
@@ -147,9 +174,9 @@ open class JSClient : IPlatformClient {
|
|||||||
_captcha = descriptor.getCaptchaData();
|
_captcha = descriptor.getCaptchaData();
|
||||||
flags = descriptor.flags.toTypedArray();
|
flags = descriptor.flags.toTypedArray();
|
||||||
|
|
||||||
_client = JSHttpClient(this, null, _captcha);
|
_httpClient = JSHttpClient(this, null, _captcha);
|
||||||
_clientAuth = JSHttpClient(this, _auth, _captcha);
|
_httpClientAuth = JSHttpClient(this, _auth, _captcha);
|
||||||
_plugin = V8Plugin(context, descriptor.config, script, _client, _clientAuth);
|
_plugin = V8Plugin(context, descriptor.config, script, _httpClient, _httpClientAuth);
|
||||||
_plugin.withDependency(context, "scripts/polyfil.js");
|
_plugin.withDependency(context, "scripts/polyfil.js");
|
||||||
_plugin.withDependency(context, "scripts/source.js");
|
_plugin.withDependency(context, "scripts/source.js");
|
||||||
_plugin.withScript(script);
|
_plugin.withScript(script);
|
||||||
@@ -159,6 +186,8 @@ open class JSClient : IPlatformClient {
|
|||||||
if(it is ScriptCaptchaRequiredException)
|
if(it is ScriptCaptchaRequiredException)
|
||||||
onCaptchaException.emit(this, it);
|
onCaptchaException.emit(this, it);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_plugin.changeAllowDevSubmit(descriptor.appSettings.allowDeveloperSubmit);
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun getCopy(): JSClient {
|
open fun getCopy(): JSClient {
|
||||||
@@ -168,6 +197,13 @@ open class JSClient : IPlatformClient {
|
|||||||
fun getUnderlyingPlugin(): V8Plugin {
|
fun getUnderlyingPlugin(): V8Plugin {
|
||||||
return _plugin;
|
return _plugin;
|
||||||
}
|
}
|
||||||
|
fun getHttpClientById(id: String): JSHttpClient? {
|
||||||
|
if(_httpClient.clientId == id)
|
||||||
|
return _httpClient;
|
||||||
|
if(_httpClientAuth?.clientId == id)
|
||||||
|
return _httpClientAuth;
|
||||||
|
return plugin.httpClientOthers[id];
|
||||||
|
}
|
||||||
|
|
||||||
override fun initialize() {
|
override fun initialize() {
|
||||||
Logger.i(TAG, "Plugin [${config.name}] initializing");
|
Logger.i(TAG, "Plugin [${config.name}] initializing");
|
||||||
@@ -193,9 +229,12 @@ open class JSClient : IPlatformClient {
|
|||||||
hasGetChannelTemplateByClaimMap = plugin.executeBoolean("!!source.getChannelTemplateByClaimMap") ?: false,
|
hasGetChannelTemplateByClaimMap = plugin.executeBoolean("!!source.getChannelTemplateByClaimMap") ?: false,
|
||||||
hasGetSearchCapabilities = plugin.executeBoolean("!!source.getSearchCapabilities") ?: false,
|
hasGetSearchCapabilities = plugin.executeBoolean("!!source.getSearchCapabilities") ?: false,
|
||||||
hasGetChannelCapabilities = plugin.executeBoolean("!!source.getChannelCapabilities") ?: false,
|
hasGetChannelCapabilities = plugin.executeBoolean("!!source.getChannelCapabilities") ?: false,
|
||||||
|
hasGetSearchChannelContentsCapabilities = plugin.executeBoolean("!!source.getSearchChannelContentsCapabilities") ?: false,
|
||||||
hasGetLiveEvents = plugin.executeBoolean("!!source.getLiveEvents") ?: false,
|
hasGetLiveEvents = plugin.executeBoolean("!!source.getLiveEvents") ?: false,
|
||||||
hasGetLiveChatWindow = plugin.executeBoolean("!!source.getLiveChatWindow") ?: false,
|
hasGetLiveChatWindow = plugin.executeBoolean("!!source.getLiveChatWindow") ?: false,
|
||||||
hasGetContentChapters = plugin.executeBoolean("!!source.getContentChapters") ?: false,
|
hasGetContentChapters = plugin.executeBoolean("!!source.getContentChapters") ?: false,
|
||||||
|
hasPeekChannelContents = plugin.executeBoolean("!!source.peekChannelContents") ?: false,
|
||||||
|
hasGetChannelPlaylists = plugin.executeBoolean("!!source.getChannelPlaylists") ?: false
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -239,15 +278,15 @@ open class JSClient : IPlatformClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@JSDocs(2, "source.getHome()", "Gets the HomeFeed of the platform")
|
@JSDocs(2, "source.getHome()", "Gets the HomeFeed of the platform")
|
||||||
override fun getHome(): IPager<IPlatformContent> = isBusyWith {
|
override fun getHome(): IPager<IPlatformContent> = isBusyWith("getHome") {
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
return@isBusyWith JSContentPager(config, plugin,
|
return@isBusyWith JSContentPager(config, this,
|
||||||
plugin.executeTyped("source.getHome()"));
|
plugin.executeTyped("source.getHome()"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JSDocs(3, "source.searchSuggestions(query)", "Gets search suggestions for a given query")
|
@JSDocs(3, "source.searchSuggestions(query)", "Gets search suggestions for a given query")
|
||||||
@JSDocsParameter("query", "Query to complete suggestions for")
|
@JSDocsParameter("query", "Query to complete suggestions for")
|
||||||
override fun searchSuggestions(query: String): Array<String> = isBusyWith {
|
override fun searchSuggestions(query: String): Array<String> = isBusyWith("searchSuggestions") {
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
return@isBusyWith plugin.executeTyped<V8ValueArray>("source.searchSuggestions(${Json.encodeToString(query)})")
|
return@isBusyWith plugin.executeTyped<V8ValueArray>("source.searchSuggestions(${Json.encodeToString(query)})")
|
||||||
.toArray()
|
.toArray()
|
||||||
@@ -277,14 +316,17 @@ open class JSClient : IPlatformClient {
|
|||||||
@JSDocsParameter("order", "(optional) Order in which contents should be returned")
|
@JSDocsParameter("order", "(optional) Order in which contents should be returned")
|
||||||
@JSDocsParameter("filters", "(optional) Filters to apply on contents")
|
@JSDocsParameter("filters", "(optional) Filters to apply on contents")
|
||||||
@JSDocsParameter("channelId", "(optional) Channel id to search in")
|
@JSDocsParameter("channelId", "(optional) Channel id to search in")
|
||||||
override fun search(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith {
|
override fun search(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith("search") {
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
return@isBusyWith JSContentPager(config, plugin,
|
return@isBusyWith JSContentPager(config, this,
|
||||||
plugin.executeTyped("source.search(${Json.encodeToString(query)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})"));
|
plugin.executeTyped("source.search(${Json.encodeToString(query)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JSDocs(4, "source.getSearchChannelContentsCapabilities()", "Gets capabilities this plugin has for search videos")
|
@JSDocs(4, "source.getSearchChannelContentsCapabilities()", "Gets capabilities this plugin has for search videos")
|
||||||
override fun getSearchChannelContentsCapabilities(): ResultCapabilities {
|
override fun getSearchChannelContentsCapabilities(): ResultCapabilities {
|
||||||
|
if(!capabilities.hasGetSearchChannelContentsCapabilities)
|
||||||
|
return ResultCapabilities(listOf(ResultCapabilities.TYPE_MIXED));
|
||||||
|
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
if (_searchChannelContentsCapabilities != null)
|
if (_searchChannelContentsCapabilities != null)
|
||||||
return _searchChannelContentsCapabilities!!;
|
return _searchChannelContentsCapabilities!!;
|
||||||
@@ -298,21 +340,21 @@ open class JSClient : IPlatformClient {
|
|||||||
@JSDocsParameter("type", "(optional) Type of contents to get from search ")
|
@JSDocsParameter("type", "(optional) Type of contents to get from search ")
|
||||||
@JSDocsParameter("order", "(optional) Order in which contents should be returned")
|
@JSDocsParameter("order", "(optional) Order in which contents should be returned")
|
||||||
@JSDocsParameter("filters", "(optional) Filters to apply on contents")
|
@JSDocsParameter("filters", "(optional) Filters to apply on contents")
|
||||||
override fun searchChannelContents(channelUrl: String, query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith {
|
override fun searchChannelContents(channelUrl: String, query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith("searchChannelContents") {
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
if(!capabilities.hasSearchChannelContents)
|
if(!capabilities.hasSearchChannelContents)
|
||||||
throw IllegalStateException("This plugin does not support channel search");
|
throw IllegalStateException("This plugin does not support channel search");
|
||||||
|
|
||||||
return@isBusyWith JSContentPager(config, plugin,
|
return@isBusyWith JSContentPager(config, this,
|
||||||
plugin.executeTyped("source.searchChannelContents(${Json.encodeToString(channelUrl)}, ${Json.encodeToString(query)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})"));
|
plugin.executeTyped("source.searchChannelContents(${Json.encodeToString(channelUrl)}, ${Json.encodeToString(query)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JSOptional
|
@JSOptional
|
||||||
@JSDocs(5, "source.searchChannels(query)", "Searches for channels on the platform")
|
@JSDocs(5, "source.searchChannels(query)", "Searches for channels on the platform")
|
||||||
@JSDocsParameter("query", "Query that channels should match")
|
@JSDocsParameter("query", "Query that channels should match")
|
||||||
override fun searchChannels(query: String): IPager<PlatformAuthorLink> = isBusyWith {
|
override fun searchChannels(query: String): IPager<PlatformAuthorLink> = isBusyWith("searchChannels") {
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
return@isBusyWith JSChannelPager(config, plugin,
|
return@isBusyWith JSChannelPager(config, this,
|
||||||
plugin.executeTyped("source.searchChannels(${Json.encodeToString(query)})"));
|
plugin.executeTyped("source.searchChannels(${Json.encodeToString(query)})"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,7 +372,7 @@ open class JSClient : IPlatformClient {
|
|||||||
}
|
}
|
||||||
@JSDocs(7, "source.getChannel(channelUrl)", "Gets a channel by its url")
|
@JSDocs(7, "source.getChannel(channelUrl)", "Gets a channel by its url")
|
||||||
@JSDocsParameter("channelUrl", "A channel url (this platform)")
|
@JSDocsParameter("channelUrl", "A channel url (this platform)")
|
||||||
override fun getChannel(channelUrl: String): IPlatformChannel = isBusyWith {
|
override fun getChannel(channelUrl: String): IPlatformChannel = isBusyWith("getChannel") {
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
return@isBusyWith JSChannel(config,
|
return@isBusyWith JSChannel(config,
|
||||||
plugin.executeTyped("source.getChannel(${Json.encodeToString(channelUrl)})"));
|
plugin.executeTyped("source.getChannel(${Json.encodeToString(channelUrl)})"));
|
||||||
@@ -357,12 +399,57 @@ open class JSClient : IPlatformClient {
|
|||||||
@JSDocsParameter("type", "(optional) Type of contents to get from channel")
|
@JSDocsParameter("type", "(optional) Type of contents to get from channel")
|
||||||
@JSDocsParameter("order", "(optional) Order in which contents should be returned")
|
@JSDocsParameter("order", "(optional) Order in which contents should be returned")
|
||||||
@JSDocsParameter("filters", "(optional) Filters to apply on contents")
|
@JSDocsParameter("filters", "(optional) Filters to apply on contents")
|
||||||
override fun getChannelContents(channelUrl: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith {
|
override fun getChannelContents(channelUrl: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith("getChannelContents") {
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
return@isBusyWith JSContentPager(config, plugin,
|
return@isBusyWith JSContentPager(config, this,
|
||||||
plugin.executeTyped("source.getChannelContents(${Json.encodeToString(channelUrl)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})"));
|
plugin.executeTyped("source.getChannelContents(${Json.encodeToString(channelUrl)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JSDocs(10, "source.getChannelPlaylists(url)", "Gets playlists of a channel")
|
||||||
|
@JSDocsParameter("channelUrl", "A channel url (this platform)")
|
||||||
|
override fun getChannelPlaylists(channelUrl: String): IPager<IPlatformPlaylist> = isBusyWith("getChannelPlaylists") {
|
||||||
|
ensureEnabled();
|
||||||
|
if(!capabilities.hasGetChannelPlaylists)
|
||||||
|
return@isBusyWith EmptyPager();
|
||||||
|
return@isBusyWith JSPlaylistPager(config, this,
|
||||||
|
plugin.executeTyped("source.getChannelPlaylists(${Json.encodeToString(channelUrl)})"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JSDocs(10, "source.getPeekChannelTypes()", "Gets types this plugin has for peek channel contents")
|
||||||
|
override fun getPeekChannelTypes(): List<String> {
|
||||||
|
if(!capabilities.hasPeekChannelContents)
|
||||||
|
return listOf();
|
||||||
|
try {
|
||||||
|
if (_peekChannelTypes != null) {
|
||||||
|
return _peekChannelTypes!!;
|
||||||
|
}
|
||||||
|
val arr: V8ValueArray = plugin.executeTyped("source.getPeekChannelTypes()");
|
||||||
|
|
||||||
|
_peekChannelTypes = arr.keys.mapNotNull {
|
||||||
|
val str = arr.get<V8ValueString>(it);
|
||||||
|
return@mapNotNull str.value;
|
||||||
|
};
|
||||||
|
return _peekChannelTypes ?: listOf();
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
announcePluginUnhandledException("getPeekChannelTypes", ex);
|
||||||
|
return listOf();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JSDocs(10, "source.peekChannelContents(url, type)", "Peek contents of a channel (reverse chronological order)")
|
||||||
|
@JSDocsParameter("channelUrl", "A channel url (this platform)")
|
||||||
|
@JSDocsParameter("type", "(optional) Type of contents to get from channel")
|
||||||
|
override fun peekChannelContents(channelUrl: String, type: String?): List<IPlatformContent> = isBusyWith("peekChannelContents") {
|
||||||
|
ensureEnabled();
|
||||||
|
|
||||||
|
val items: V8ValueArray = plugin.executeTyped("source.peekChannelContents(${Json.encodeToString(channelUrl)}, ${Json.encodeToString(type)})");
|
||||||
|
return@isBusyWith items.keys.mapNotNull {
|
||||||
|
val obj = items.get<V8ValueObject>(it);
|
||||||
|
return@mapNotNull IJSContent.fromV8(this, obj);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@JSOptional
|
@JSOptional
|
||||||
@JSDocs(11, "source.getChannelUrlByClaim(claimType, claimValues)", "Gets the channel url that should be used to fetch a given polycentric claim")
|
@JSDocs(11, "source.getChannelUrlByClaim(claimType, claimValues)", "Gets the channel url that should be used to fetch a given polycentric claim")
|
||||||
@JSDocsParameter("claimType", "Polycentric claimtype id")
|
@JSDocsParameter("claimType", "Polycentric claimtype id")
|
||||||
@@ -423,16 +510,16 @@ open class JSClient : IPlatformClient {
|
|||||||
}
|
}
|
||||||
@JSDocs(14, "source.getContentDetails(url)", "Gets content details by its url")
|
@JSDocs(14, "source.getContentDetails(url)", "Gets content details by its url")
|
||||||
@JSDocsParameter("url", "A content url (this platform)")
|
@JSDocsParameter("url", "A content url (this platform)")
|
||||||
override fun getContentDetails(url: String): IPlatformContentDetails = isBusyWith {
|
override fun getContentDetails(url: String): IPlatformContentDetails = isBusyWith("getContentDetails") {
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
return@isBusyWith IJSContentDetails.fromV8(config,
|
return@isBusyWith IJSContentDetails.fromV8(this,
|
||||||
plugin.executeTyped("source.getContentDetails(${Json.encodeToString(url)})"));
|
plugin.executeTyped("source.getContentDetails(${Json.encodeToString(url)})"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JSOptional //getContentChapters = function(url, initialData)
|
@JSOptional //getContentChapters = function(url, initialData)
|
||||||
@JSDocs(15, "source.getContentChapters(url)", "Gets chapters for content details")
|
@JSDocs(15, "source.getContentChapters(url)", "Gets chapters for content details")
|
||||||
@JSDocsParameter("url", "A content url (this platform)")
|
@JSDocsParameter("url", "A content url (this platform)")
|
||||||
override fun getContentChapters(url: String): List<IChapter> = isBusyWith {
|
override fun getContentChapters(url: String): List<IChapter> = isBusyWith("getContentChapters") {
|
||||||
if(!capabilities.hasGetContentChapters)
|
if(!capabilities.hasGetContentChapters)
|
||||||
return@isBusyWith listOf();
|
return@isBusyWith listOf();
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
@@ -443,7 +530,7 @@ open class JSClient : IPlatformClient {
|
|||||||
@JSOptional
|
@JSOptional
|
||||||
@JSDocs(15, "source.getPlaybackTracker(url)", "Gets a playback tracker for given content url")
|
@JSDocs(15, "source.getPlaybackTracker(url)", "Gets a playback tracker for given content url")
|
||||||
@JSDocsParameter("url", "A content url (this platform)")
|
@JSDocsParameter("url", "A content url (this platform)")
|
||||||
override fun getPlaybackTracker(url: String): IPlaybackTracker? = isBusyWith {
|
override fun getPlaybackTracker(url: String): IPlaybackTracker? = isBusyWith("getPlaybackTracker") {
|
||||||
if(!capabilities.hasGetPlaybackTracker)
|
if(!capabilities.hasGetPlaybackTracker)
|
||||||
return@isBusyWith null;
|
return@isBusyWith null;
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
@@ -457,67 +544,88 @@ open class JSClient : IPlatformClient {
|
|||||||
|
|
||||||
@JSDocs(16, "source.getComments(url)", "Gets comments for a content by its url")
|
@JSDocs(16, "source.getComments(url)", "Gets comments for a content by its url")
|
||||||
@JSDocsParameter("url", "A content url (this platform)")
|
@JSDocsParameter("url", "A content url (this platform)")
|
||||||
override fun getComments(url: String): IPager<IPlatformComment> = isBusyWith {
|
override fun getComments(url: String): IPager<IPlatformComment> = isBusyWith("getComments") {
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
val pager = plugin.executeTyped<V8Value>("source.getComments(${Json.encodeToString(url)})");
|
val pager = plugin.executeTyped<V8Value>("source.getComments(${Json.encodeToString(url)})");
|
||||||
if (pager !is V8ValueObject) { //TODO: Maybe solve this better
|
if (pager !is V8ValueObject) { //TODO: Maybe solve this better
|
||||||
return@isBusyWith EmptyPager<IPlatformComment>();
|
return@isBusyWith EmptyPager<IPlatformComment>();
|
||||||
}
|
}
|
||||||
return@isBusyWith JSCommentPager(config, plugin, pager);
|
return@isBusyWith JSCommentPager(config, this, pager);
|
||||||
}
|
}
|
||||||
@JSDocs(17, "source.getSubComments(comment)", "Gets replies for a given comment")
|
@JSDocs(17, "source.getSubComments(comment)", "Gets replies for a given comment")
|
||||||
@JSDocsParameter("comment", "Comment object that was returned by getComments")
|
@JSDocsParameter("comment", "Comment object that was returned by getComments")
|
||||||
override fun getSubComments(comment: IPlatformComment): IPager<IPlatformComment> {
|
override fun getSubComments(comment: IPlatformComment): IPager<IPlatformComment> {
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
return comment.getReplies(this) ?: JSCommentPager(config, plugin,
|
return comment.getReplies(this) ?: JSCommentPager(config, this,
|
||||||
plugin.executeTyped("source.getSubComments(${Json.encodeToString(comment as JSComment)})"));
|
plugin.executeTyped("source.getSubComments(${Json.encodeToString(comment as JSComment)})"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JSDocs(16, "source.getLiveChatWindow(url)", "Gets live events for a livestream")
|
@JSDocs(18, "source.getLiveChatWindow(url)", "Gets live events for a livestream")
|
||||||
@JSDocsParameter("url", "Url of live stream")
|
@JSDocsParameter("url", "Url of live stream")
|
||||||
override fun getLiveChatWindow(url: String): ILiveChatWindowDescriptor? = isBusyWith {
|
override fun getLiveChatWindow(url: String): ILiveChatWindowDescriptor? = isBusyWith("getLiveChatWindow") {
|
||||||
if(!capabilities.hasGetLiveChatWindow)
|
if(!capabilities.hasGetLiveChatWindow)
|
||||||
return@isBusyWith null;
|
return@isBusyWith null;
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
return@isBusyWith JSLiveChatWindowDescriptor(config,
|
return@isBusyWith JSLiveChatWindowDescriptor(config,
|
||||||
plugin.executeTyped("source.getLiveChatWindow(${Json.encodeToString(url)})"));
|
plugin.executeTyped("source.getLiveChatWindow(${Json.encodeToString(url)})"));
|
||||||
}
|
}
|
||||||
@JSDocs(16, "source.getLiveEvents(url)", "Gets live events for a livestream")
|
@JSDocs(19, "source.getLiveEvents(url)", "Gets live events for a livestream")
|
||||||
@JSDocsParameter("url", "Url of live stream")
|
@JSDocsParameter("url", "Url of live stream")
|
||||||
override fun getLiveEvents(url: String): IPager<IPlatformLiveEvent>? = isBusyWith {
|
override fun getLiveEvents(url: String): IPager<IPlatformLiveEvent>? = isBusyWith("getLiveEvents") {
|
||||||
if(!capabilities.hasGetLiveEvents)
|
if(!capabilities.hasGetLiveEvents)
|
||||||
return@isBusyWith null;
|
return@isBusyWith null;
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
return@isBusyWith JSLiveEventPager(config, plugin,
|
return@isBusyWith JSLiveEventPager(config, this,
|
||||||
plugin.executeTyped("source.getLiveEvents(${Json.encodeToString(url)})"));
|
plugin.executeTyped("source.getLiveEvents(${Json.encodeToString(url)})"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@JSDocs(19, "source.getContentRecommendations(url)", "Gets recommendations of a content page")
|
||||||
|
@JSDocsParameter("url", "Url of content")
|
||||||
|
override fun getContentRecommendations(url: String): IPager<IPlatformContent>? = isBusyWith("getContentRecommendations") {
|
||||||
|
if(!capabilities.hasGetContentRecommendations)
|
||||||
|
return@isBusyWith null;
|
||||||
|
ensureEnabled();
|
||||||
|
return@isBusyWith JSContentPager(config, this,
|
||||||
|
plugin.executeTyped("source.getContentRecommendations(${Json.encodeToString(url)})"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@JSDocs(19, "source.searchPlaylists(query)", "Searches for playlists on the platform")
|
@JSDocs(19, "source.searchPlaylists(query)", "Searches for playlists on the platform")
|
||||||
@JSDocsParameter("query", "Query that search results should match")
|
@JSDocsParameter("query", "Query that search results should match")
|
||||||
@JSDocsParameter("type", "(optional) Type of contents to get from search ")
|
@JSDocsParameter("type", "(optional) Type of contents to get from search ")
|
||||||
@JSDocsParameter("order", "(optional) Order in which contents should be returned")
|
@JSDocsParameter("order", "(optional) Order in which contents should be returned")
|
||||||
@JSDocsParameter("filters", "(optional) Filters to apply on contents")
|
@JSDocsParameter("filters", "(optional) Filters to apply on contents")
|
||||||
@JSDocsParameter("channelId", "(optional) Channel id to search in")
|
@JSDocsParameter("channelId", "(optional) Channel id to search in")
|
||||||
override fun searchPlaylists(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith {
|
override fun searchPlaylists(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith("searchPlaylists") {
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
if(!capabilities.hasSearchPlaylists)
|
if(!capabilities.hasSearchPlaylists)
|
||||||
throw IllegalStateException("This plugin does not support playlist search");
|
throw IllegalStateException("This plugin does not support playlist search");
|
||||||
return@isBusyWith JSContentPager(config, plugin, plugin.executeTyped("source.searchPlaylists(${Json.encodeToString(query)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})"));
|
return@isBusyWith JSContentPager(config, this, plugin.executeTyped("source.searchPlaylists(${Json.encodeToString(query)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})"));
|
||||||
}
|
}
|
||||||
@JSOptional
|
@JSOptional
|
||||||
@JSDocs(20, "source.isPlaylistUrl(url)", "Validates if a playlist url is for this platform")
|
@JSDocs(20, "source.isPlaylistUrl(url)", "Validates if a playlist url is for this platform")
|
||||||
@JSDocsParameter("url", "Url of playlist")
|
@JSDocsParameter("url", "Url of playlist")
|
||||||
override fun isPlaylistUrl(url: String): Boolean {
|
override fun isPlaylistUrl(url: String): Boolean {
|
||||||
ensureEnabled();
|
|
||||||
if (!capabilities.hasGetPlaylist)
|
if (!capabilities.hasGetPlaylist)
|
||||||
return false;
|
return false;
|
||||||
return plugin.executeBoolean("source.isPlaylistUrl(${Json.encodeToString(url)})") ?: false;
|
|
||||||
|
try {
|
||||||
|
return plugin.executeTyped<V8ValueBoolean>("source.isPlaylistUrl(${Json.encodeToString(url)})")
|
||||||
|
.value;
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
announcePluginUnhandledException("isPlaylistUrl", ex);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@JSOptional
|
@JSOptional
|
||||||
@JSDocs(21, "source.getPlaylist(url)", "Gets the playlist of the current user")
|
@JSDocs(21, "source.getPlaylist(url)", "Gets the playlist of the current user")
|
||||||
@JSDocsParameter("url", "Url of playlist")
|
@JSDocsParameter("url", "Url of playlist")
|
||||||
override fun getPlaylist(url: String): IPlatformPlaylistDetails = isBusyWith {
|
override fun getPlaylist(url: String): IPlatformPlaylistDetails = isBusyWith("getPlaylist") {
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
return@isBusyWith JSPlaylistDetails(plugin, plugin.config as SourcePluginConfig, plugin.executeTyped("source.getPlaylist(${Json.encodeToString(url)})"));
|
return@isBusyWith JSPlaylistDetails(this, plugin.config as SourcePluginConfig, plugin.executeTyped("source.getPlaylist(${Json.encodeToString(url)})"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JSOptional
|
@JSOptional
|
||||||
@@ -612,19 +720,24 @@ open class JSClient : IPlatformClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun <T> isBusyWith(handle: ()->T): T {
|
private fun <T> isBusyWith(actionName: String, handle: ()->T): T {
|
||||||
try {
|
try {
|
||||||
synchronized(_busyLock) {
|
synchronized(_busyLock) {
|
||||||
_busyCounter++;
|
_busyCounter++;
|
||||||
}
|
}
|
||||||
|
_busyAction = actionName;
|
||||||
return handle();
|
return handle();
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
|
_busyAction = "";
|
||||||
synchronized(_busyLock) {
|
synchronized(_busyLock) {
|
||||||
_busyCounter--;
|
_busyCounter--;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
private fun <T> isBusyWith(handle: ()->T): T {
|
||||||
|
return isBusyWith("Unknown", handle);
|
||||||
|
}
|
||||||
|
|
||||||
private fun announcePluginUnhandledException(method: String, ex: Throwable) {
|
private fun announcePluginUnhandledException(method: String, ex: Throwable) {
|
||||||
if(ex is PluginEngineException)
|
if(ex is PluginEngineException)
|
||||||
@@ -641,10 +754,43 @@ open class JSClient : IPlatformClient {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val TAG = "JSClient";
|
val TAG = "JSClient";
|
||||||
|
private val _lock = Object();
|
||||||
|
private var _docs: Map<String, String>? = null;
|
||||||
|
|
||||||
|
fun getMethodDocs(names: List<String>): Map<String, String>? {
|
||||||
|
synchronized(_lock) {
|
||||||
|
if(_docs == null) {
|
||||||
|
val client = ManagedHttpClient();
|
||||||
|
val docs = names
|
||||||
|
.map { stringWithoutBrackets(it) }
|
||||||
|
.distinct()
|
||||||
|
.parallelStream()
|
||||||
|
.map {
|
||||||
|
val url = "https://github.com/futo-org/grayjay-android/blob/master/docs/source/${it}.md";
|
||||||
|
val resp = client.head(url);
|
||||||
|
if(resp.isOk)
|
||||||
|
return@map Pair(it, url);
|
||||||
|
else
|
||||||
|
return@map null;
|
||||||
|
}.asSequence()
|
||||||
|
.filterNotNull()
|
||||||
|
.toMap();
|
||||||
|
_docs = docs;
|
||||||
|
}
|
||||||
|
return _docs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun getMethodDocUrls(): Map<String, String>? {
|
||||||
|
if(_docs != null)
|
||||||
|
return _docs;
|
||||||
|
val methods = JSClient::class.java.declaredMethods.filter { it.getAnnotation(JSDocs::class.java) != null }
|
||||||
|
return getMethodDocs(methods.map { it.name });
|
||||||
|
}
|
||||||
|
|
||||||
fun getJSDocs(): List<JSCallDocs> {
|
fun getJSDocs(): List<JSCallDocs> {
|
||||||
val docs = mutableListOf<JSCallDocs>();
|
val docs = mutableListOf<JSCallDocs>();
|
||||||
val methods = JSClient::class.java.declaredMethods.filter { it.getAnnotation(JSDocs::class.java) != null }
|
val methods = JSClient::class.java.declaredMethods.filter { it.getAnnotation(JSDocs::class.java) != null }
|
||||||
|
|
||||||
for(method in methods.sortedBy { it.getAnnotation(JSDocs::class.java)?.order }) {
|
for(method in methods.sortedBy { it.getAnnotation(JSDocs::class.java)?.order }) {
|
||||||
val doc = method.getAnnotation(JSDocs::class.java);
|
val doc = method.getAnnotation(JSDocs::class.java);
|
||||||
val parameters = method.kotlinFunction!!.findAnnotations<JSDocsParameter>();
|
val parameters = method.kotlinFunction!!.findAnnotations<JSDocsParameter>();
|
||||||
@@ -657,5 +803,12 @@ open class JSClient : IPlatformClient {
|
|||||||
}
|
}
|
||||||
return docs;
|
return docs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun stringWithoutBrackets(name: String): String {
|
||||||
|
val index = name.indexOf('(');
|
||||||
|
if(index >= 0)
|
||||||
|
return name.substring(0, index);
|
||||||
|
return name;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+1
@@ -11,4 +11,5 @@ class SourcePluginAuthConfig(
|
|||||||
val userAgent: String? = null,
|
val userAgent: String? = null,
|
||||||
val loginButton: String? = null,
|
val loginButton: String? = null,
|
||||||
val domainHeadersToFind: Map<String, List<String>>? = null,
|
val domainHeadersToFind: Map<String, List<String>>? = null,
|
||||||
|
val loginWarning: String? = null
|
||||||
) { }
|
) { }
|
||||||
+47
-4
@@ -5,9 +5,8 @@ import com.futo.platformplayer.SignatureProvider
|
|||||||
import com.futo.platformplayer.api.media.Serializer
|
import com.futo.platformplayer.api.media.Serializer
|
||||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
import com.futo.platformplayer.states.StatePlugins
|
import com.futo.platformplayer.states.StatePlugins
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.util.*
|
import java.util.UUID
|
||||||
|
|
||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
class SourcePluginConfig(
|
class SourcePluginConfig(
|
||||||
@@ -46,7 +45,9 @@ class SourcePluginConfig(
|
|||||||
var enableInSearch: Boolean = true,
|
var enableInSearch: Boolean = true,
|
||||||
var enableInHome: Boolean = true,
|
var enableInHome: Boolean = true,
|
||||||
var supportedClaimTypes: List<Int> = listOf(),
|
var supportedClaimTypes: List<Int> = listOf(),
|
||||||
var primaryClaimFieldType: Int? = null
|
var primaryClaimFieldType: Int? = null,
|
||||||
|
var developerSubmitUrl: String? = null,
|
||||||
|
var allowAllHttpHeaderAccess: Boolean = false,
|
||||||
) : IV8PluginConfig {
|
) : IV8PluginConfig {
|
||||||
|
|
||||||
val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl);
|
val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl);
|
||||||
@@ -80,6 +81,44 @@ class SourcePluginConfig(
|
|||||||
return _allowUrlsLowerVal!!;
|
return _allowUrlsLowerVal!!;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fun isLowRiskUpdate(oldScript: String, newConfig: SourcePluginConfig, newScript: String): Boolean{
|
||||||
|
//New allow header access
|
||||||
|
if(!allowAllHttpHeaderAccess && newConfig.allowAllHttpHeaderAccess)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
//All urls should already be allowed
|
||||||
|
for(url in newConfig.allowUrls) {
|
||||||
|
if(!allowUrls.contains(url))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
//All packages should already be allowed
|
||||||
|
for(pack in newConfig.packages) {
|
||||||
|
if(!packages.contains(pack))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
//Developer Submit Url should be same or empty
|
||||||
|
if(!newConfig.developerSubmitUrl.isNullOrEmpty() && developerSubmitUrl != newConfig.developerSubmitUrl)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
//Should have a public key
|
||||||
|
if(scriptPublicKey.isNullOrEmpty() || scriptSignature.isNullOrEmpty())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
//Should be same public key
|
||||||
|
if(scriptPublicKey != newConfig.scriptPublicKey)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
//Old signature should be valid
|
||||||
|
if(!validate(oldScript))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
//New signature should be valid
|
||||||
|
if(!newConfig.validate(newScript))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
fun getWarnings(scriptToCheck: String? = null) : List<Pair<String,String>> {
|
fun getWarnings(scriptToCheck: String? = null) : List<Pair<String,String>> {
|
||||||
val list = mutableListOf<Pair<String,String>>();
|
val list = mutableListOf<Pair<String,String>>();
|
||||||
|
|
||||||
@@ -108,6 +147,11 @@ class SourcePluginConfig(
|
|||||||
list.add(Pair(
|
list.add(Pair(
|
||||||
"Unrestricted Web Access",
|
"Unrestricted Web Access",
|
||||||
"This plugin requires access to all URLs, this may include malicious URLs."));
|
"This plugin requires access to all URLs, this may include malicious URLs."));
|
||||||
|
if(allowAllHttpHeaderAccess)
|
||||||
|
list.add(Pair(
|
||||||
|
"Unrestricted Http Header access",
|
||||||
|
"Allows this plugin to access all headers (including cookies and authorization headers) for unauthenticated requests."
|
||||||
|
))
|
||||||
|
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
@@ -149,7 +193,6 @@ class SourcePluginConfig(
|
|||||||
val warningDialog: String? = null,
|
val warningDialog: String? = null,
|
||||||
val options: List<String>? = null
|
val options: List<String>? = null
|
||||||
) {
|
) {
|
||||||
@kotlinx.serialization.Transient
|
|
||||||
val variableOrName: String get() = variable ?: name;
|
val variableOrName: String get() = variable ?: name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+40
-7
@@ -2,10 +2,13 @@ package com.futo.platformplayer.api.media.platforms.js
|
|||||||
|
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.constructs.Event0
|
import com.futo.platformplayer.constructs.Event0
|
||||||
import com.futo.platformplayer.serializers.FlexibleBooleanSerializer
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.states.AnnouncementType
|
||||||
|
import com.futo.platformplayer.states.StateAnnouncement
|
||||||
import com.futo.platformplayer.views.fields.DropdownFieldOptions
|
import com.futo.platformplayer.views.fields.DropdownFieldOptions
|
||||||
import com.futo.platformplayer.views.fields.FieldForm
|
import com.futo.platformplayer.views.fields.FieldForm
|
||||||
import com.futo.platformplayer.views.fields.FormField
|
import com.futo.platformplayer.views.fields.FormField
|
||||||
|
import com.futo.platformplayer.views.fields.FormFieldWarning
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -27,17 +30,19 @@ class SourcePluginDescriptor {
|
|||||||
@kotlinx.serialization.Transient
|
@kotlinx.serialization.Transient
|
||||||
val onCaptchaChanged = Event0();
|
val onCaptchaChanged = Event0();
|
||||||
|
|
||||||
constructor(config :SourcePluginConfig, authEncrypted: String? = null, captchaEncrypted: String? = null) {
|
constructor(config :SourcePluginConfig, authEncrypted: String? = null, captchaEncrypted: String? = null, settings: HashMap<String, String?>? = null) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.authEncrypted = authEncrypted;
|
this.authEncrypted = authEncrypted;
|
||||||
this.captchaEncrypted = captchaEncrypted;
|
this.captchaEncrypted = captchaEncrypted;
|
||||||
this.flags = listOf();
|
this.flags = listOf();
|
||||||
|
this.settings = settings ?: hashMapOf();
|
||||||
}
|
}
|
||||||
constructor(config :SourcePluginConfig, authEncrypted: String? = null, captchaEncrypted: String? = null, flags: List<String>) {
|
constructor(config :SourcePluginConfig, authEncrypted: String? = null, captchaEncrypted: String? = null, flags: List<String>, settings: HashMap<String, String?>? = null) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.authEncrypted = authEncrypted;
|
this.authEncrypted = authEncrypted;
|
||||||
this.captchaEncrypted = captchaEncrypted;
|
this.captchaEncrypted = captchaEncrypted;
|
||||||
this.flags = flags;
|
this.flags = flags;
|
||||||
|
this.settings = settings ?: hashMapOf();
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSettingsWithDefaults(): HashMap<String, String?> {
|
fun getSettingsWithDefaults(): HashMap<String, String?> {
|
||||||
@@ -54,7 +59,16 @@ class SourcePluginDescriptor {
|
|||||||
onCaptchaChanged.emit();
|
onCaptchaChanged.emit();
|
||||||
}
|
}
|
||||||
fun getCaptchaData(): SourceCaptchaData? {
|
fun getCaptchaData(): SourceCaptchaData? {
|
||||||
return SourceCaptchaData.fromEncrypted(captchaEncrypted);
|
try {
|
||||||
|
return SourceCaptchaData.fromEncrypted(captchaEncrypted);
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e("SourcePluginDescriptor", "Captcha decode failed, disabling auth.", ex);
|
||||||
|
StateAnnouncement.instance.registerAnnouncement("CAP_BROKEN_" + config.id,
|
||||||
|
"Captcha corrupted for plugin [${config.name}]",
|
||||||
|
"Something went wrong in the stored captcha, you'll have to login again", AnnouncementType.SESSION);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateAuth(str: SourceAuth?) {
|
fun updateAuth(str: SourceAuth?) {
|
||||||
@@ -62,12 +76,26 @@ class SourcePluginDescriptor {
|
|||||||
onAuthChanged.emit();
|
onAuthChanged.emit();
|
||||||
}
|
}
|
||||||
fun getAuth(): SourceAuth? {
|
fun getAuth(): SourceAuth? {
|
||||||
return SourceAuth.fromEncrypted(authEncrypted);
|
try {
|
||||||
|
return SourceAuth.fromEncrypted(authEncrypted);
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e("SourcePluginDescriptor", "Authentication decode failed, disabling auth.", ex);
|
||||||
|
StateAnnouncement.instance.registerAnnouncement("AUTH_BROKEN_" + config.id,
|
||||||
|
"Authentication corrupted for plugin [${config.name}]",
|
||||||
|
"Something went wrong in the stored authentication, you'll have to login again", AnnouncementType.SESSION);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class AppPluginSettings {
|
class AppPluginSettings {
|
||||||
|
|
||||||
|
@FormField(R.string.check_for_updates_setting, FieldForm.TOGGLE, R.string.check_for_updates_setting_description, -1)
|
||||||
|
var checkForUpdates: Boolean = true;
|
||||||
|
@FormField(R.string.automatic_update_setting, FieldForm.TOGGLE, R.string.automatic_update_setting_description, 0)
|
||||||
|
var automaticUpdate: Boolean = false;
|
||||||
|
|
||||||
@FormField(R.string.visibility, "group", R.string.enable_where_this_plugins_content_are_visible, 2)
|
@FormField(R.string.visibility, "group", R.string.enable_where_this_plugins_content_are_visible, 2)
|
||||||
var tabEnabled = TabEnabled();
|
var tabEnabled = TabEnabled();
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -105,11 +133,16 @@ class SourcePluginDescriptor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@FormField(R.string.allow_developer_submit, FieldForm.TOGGLE, R.string.allow_developer_submit_description, 1, "devSubmit")
|
||||||
|
var allowDeveloperSubmit: Boolean = false;
|
||||||
|
|
||||||
|
|
||||||
fun loadDefaults(config: SourcePluginConfig) {
|
fun loadDefaults(config: SourcePluginConfig) {
|
||||||
if(tabEnabled.enableHome == null)
|
if(tabEnabled.enableHome == null)
|
||||||
tabEnabled.enableHome = config.enableInHome ?: true;
|
tabEnabled.enableHome = config.enableInHome
|
||||||
if(tabEnabled.enableSearch == null)
|
if(tabEnabled.enableSearch == null)
|
||||||
tabEnabled.enableSearch = config.enableInSearch ?: true;
|
tabEnabled.enableSearch = config.enableInSearch
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user