mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-29 19:13:01 +02:00
Compare commits
274 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fd95311920 | |||
| 6da5c11731 | |||
| 4e58231308 | |||
| ef0ecf249a | |||
| 4981617f7a | |||
| 2070bc7007 | |||
| 231d2461b3 | |||
| 3b457f87c4 | |||
| de3ced4d3c | |||
| 891777e89e | |||
| 287239dd1c | |||
| 7cdded8fd7 | |||
| 8c9d045e1d | |||
| 620f5a0459 | |||
| 178d874ba0 | |||
| d44f30c8a6 | |||
| ce66937429 | |||
| 9823337375 | |||
| 11f5f0dfe1 | |||
| e1882f19e8 | |||
| 6a8b9f06c2 | |||
| 752fc8787d | |||
| 90a1cd8280 | |||
| aa570ac29d | |||
| fb7b6363f9 | |||
| 23afe7994c | |||
| 7557e6f6ba | |||
| 86b6938911 | |||
| 8f30a45fa8 | |||
| 7c9e9d5f52 | |||
| 4066ce73a8 | |||
| b5722dba1a | |||
| 81765ecafc | |||
| 84b42e9d19 | |||
| ed319a0e5f | |||
| dd55d10194 | |||
| 2084b46090 | |||
| 53443a6cf2 | |||
| 92715b5642 | |||
| 6166392515 | |||
| 49d0dead7d | |||
| 6f004830ff | |||
| e2e5e36bad | |||
| f267d264d3 | |||
| be1a77bfd7 | |||
| 41a980e826 | |||
| 09c09f3d64 | |||
| 2404399ec5 | |||
| b45d4c0557 | |||
| a41b138d3c | |||
| 1e46949dd6 | |||
| 3ed2c1ba5d | |||
| 809b99c9c9 | |||
| 4d3acdb5fb | |||
| ca9e321ef2 | |||
| da27517fcf | |||
| 192df0a3b8 | |||
| a965003a9d | |||
| 9ea26c821f | |||
| 14b699485a | |||
| 1684edc43f | |||
| 580c4418b9 | |||
| 4a65fc2358 | |||
| 71ba131fb3 | |||
| 9693b50719 | |||
| 102e2c54bb | |||
| e989590c08 | |||
| 6cee33b449 | |||
| f32498a444 | |||
| c85f71b601 | |||
| 196e55899e | |||
| ebec45076d | |||
| 561d9ae987 | |||
| 8950bd94cb | |||
| f416f197bc | |||
| 65afe5a0e6 | |||
| 4b5d347413 | |||
| 4dcc2dd0ca | |||
| 2a7a332160 | |||
| 27ee1eabda | |||
| 0034665965 | |||
| a69692be18 | |||
| dc76152166 | |||
| d7f3ae696c | |||
| 71f5449d34 | |||
| 0e64fa8d4c | |||
| 73b048d4c5 | |||
| 1c05b39861 | |||
| 7cfa6c163f | |||
| 2d4af2e867 | |||
| 1eeaffc442 | |||
| 82125b33ed | |||
| 42cbbc28fd | |||
| a7cbb0e93c | |||
| fde6148ece | |||
| df1661d75a | |||
| f938f79a35 | |||
| 333f00235b | |||
| c06475bfb3 | |||
| d1a54d0cf3 | |||
| 349437c06b | |||
| 1b03c83c84 | |||
| bb749aacf1 | |||
| 3a41b89e52 | |||
| 70cbc77381 | |||
| 3a99f5dfaa | |||
| f24435ecf4 | |||
| 4a708e316a | |||
| c2b47c998d | |||
| 534f7b3134 | |||
| d5d2692317 | |||
| dc9cc7b00f | |||
| 965e74c7e2 | |||
| 096ba54eb1 | |||
| f4e38f9e50 | |||
| c0d9409176 | |||
| 7d1f565749 | |||
| dfec4ada3b | |||
| cd695cf265 | |||
| 47ff2e0c38 | |||
| db7c09291f | |||
| 01f10c49ba | |||
| 1ff0692a72 | |||
| 116e6099d5 | |||
| 18ccaadc5b | |||
| 8f6eac7ca2 | |||
| f4610d0df5 | |||
| bf1a6b7d0a | |||
| b3fd05e62e | |||
| f7ce365618 | |||
| 77a558dbe5 | |||
| cc0c400b28 | |||
| 2bcd59cbfa | |||
| 5139acc7f1 | |||
| 1564433e02 | |||
| 1339beb7cd | |||
| cd9698ea48 | |||
| c8f8e4c5eb | |||
| 0b4ab46563 | |||
| ea1ac86134 | |||
| 790331e798 | |||
| f5d9b2ba41 | |||
| 7f26ac00b1 | |||
| fcbab10434 | |||
| c4061cc6ac | |||
| 12ac4d6b6f | |||
| 3d06e62cd4 | |||
| d7d23e1048 | |||
| 1fe9b70176 | |||
| a9cf8dd71a | |||
| 3299261db3 | |||
| e465ec8278 | |||
| d0e4a0aa1f | |||
| 74efec3235 | |||
| 13516087f2 | |||
| 0a0c16524a | |||
| 9b843a155e | |||
| cb085acbff | |||
| c3d7df166b | |||
| d312062125 | |||
| e2453192aa | |||
| 68eb0cc8f2 | |||
| cb9cecfa5d | |||
| 0f4e4a7d97 | |||
| f20a708b36 | |||
| 8c4e511883 | |||
| a4a3b8d664 | |||
| bf6530ea81 | |||
| 4a80c2aab1 | |||
| 527bbfe43f | |||
| d8e1edb60b | |||
| 245b5f74c0 | |||
| e9a1f63415 | |||
| ec370dd94b | |||
| e39d862ef3 | |||
| 7b065654aa | |||
| 918b2bbe96 | |||
| e529a3d34d | |||
| 5475778d67 | |||
| c6a3ff0a53 | |||
| cf3587f504 | |||
| d42f104884 | |||
| 6a43568369 | |||
| 85c9cd0a6e | |||
| be5920cfae | |||
| 3d25d94a77 | |||
| fe97850835 | |||
| dab9decd89 | |||
| 854651aa71 | |||
| fdd1af3287 | |||
| 0bf92b6aff | |||
| d9403bf4da | |||
| 716d8caf4d | |||
| 0f0f368a75 | |||
| ff8d7558d4 | |||
| 66f9824b68 | |||
| 44a6e5da38 | |||
| de5a4aa5f3 | |||
| e8007082a7 | |||
| 3c70c5a366 | |||
| eb6e79b055 | |||
| ea59f8dccb | |||
| aef1c584e5 | |||
| c4ce671a87 | |||
| e8a79c87ab | |||
| 249e77a5d3 | |||
| 3cf4a52a69 | |||
| eb8b02756b | |||
| 0510d34ed3 | |||
| 1c8d12e72a | |||
| 0a36a6b674 | |||
| b887c9d50f | |||
| ee4e108e4f | |||
| 5e14a0fed4 | |||
| 6045205ea9 | |||
| f2d763cdec | |||
| e5e348205a | |||
| af6d219936 | |||
| 82a07e2e09 | |||
| 12a9b99fff | |||
| 3adf761158 | |||
| 670a4c61ff | |||
| 220f50d3bb | |||
| e0bf9d2a7c | |||
| f61cf46a52 | |||
| d188128d27 | |||
| f698c4120d | |||
| 338a852d49 | |||
| a64ee2242c | |||
| e9ff5e6f0b | |||
| f3911d8b68 | |||
| 9ce0be6450 | |||
| 6ab3eff61c | |||
| 0281da1c5a | |||
| 0b4770188c | |||
| 9376bb05fa | |||
| ecca3b6793 | |||
| f41a971cd8 | |||
| 44ba66d619 | |||
| bf685a607f | |||
| 5713cf0508 | |||
| bdd50d70ca | |||
| 8188399ce6 | |||
| f72b7dbbbb | |||
| 2409afcc5c | |||
| 15c0d02c13 | |||
| a54a5081e6 | |||
| db9dfcf049 | |||
| 47f9948748 | |||
| 05e866df55 | |||
| fc431f0cb8 | |||
| 228ab359ed | |||
| 103a8587f7 | |||
| 7db0083928 | |||
| e6f6ab499a | |||
| 721b7dbba0 | |||
| a95ddab814 | |||
| 2941546ae4 | |||
| bd9b9179c1 | |||
| ce7d54c151 | |||
| 3c778c07c2 | |||
| 95207341db | |||
| 70cf24924d | |||
| a8ebba691e | |||
| ec19ea44ad | |||
| ca8dc0f0f5 | |||
| 1dc50a697c | |||
| 1167c314ee | |||
| 55781e2b34 | |||
| 7439e44e44 | |||
| cf2639df3d | |||
| 834de928c2 | |||
| 72efb21439 | |||
| da6eef905c |
+12
@@ -70,3 +70,15 @@
|
|||||||
[submodule "app/src/unstable/assets/sources/spotify"]
|
[submodule "app/src/unstable/assets/sources/spotify"]
|
||||||
path = app/src/unstable/assets/sources/spotify
|
path = app/src/unstable/assets/sources/spotify
|
||||||
url = ../plugins/spotify.git
|
url = ../plugins/spotify.git
|
||||||
|
[submodule "app/src/stable/assets/sources/bitchute"]
|
||||||
|
path = app/src/stable/assets/sources/bitchute
|
||||||
|
url = ../plugins/bitchute.git
|
||||||
|
[submodule "app/src/unstable/assets/sources/bitchute"]
|
||||||
|
path = app/src/unstable/assets/sources/bitchute
|
||||||
|
url = ../plugins/bitchute.git
|
||||||
|
[submodule "app/src/unstable/assets/sources/dailymotion"]
|
||||||
|
path = app/src/unstable/assets/sources/dailymotion
|
||||||
|
url = ../plugins/dailymotion.git
|
||||||
|
[submodule "app/src/stable/assets/sources/dailymotion"]
|
||||||
|
path = app/src/stable/assets/sources/dailymotion
|
||||||
|
url = ../plugins/dailymotion.git
|
||||||
|
|||||||
+16
-2
@@ -49,9 +49,23 @@ We encourage developers to write their own plugins. Please refer to the "Getting
|
|||||||
|
|
||||||
## Contributing to Core
|
## Contributing to Core
|
||||||
|
|
||||||
**We are currently not accepting contributions to the core.**
|
|
||||||
|
|
||||||
The core is currently licensed under the FUTO Temporary License (FTL). The licensing and ownership of contributions to the core are complex topics that we are still working on. We'll update these guidelines when we have more clarity.
|
### License
|
||||||
|
|
||||||
|
The core is currently licensed under the [Source First License 1.1](./LICENSE.md). All contributors have to sign FUTO Individual Contributor License Agreement before contributions can be accepted. You can read more about it at [https://cla.futo.org/](https://cla.futo.org/).
|
||||||
|
|
||||||
|
### How to Contribute
|
||||||
|
|
||||||
|
1. Fork the core repository.
|
||||||
|
2. Clone your fork.
|
||||||
|
3. Make your changes.
|
||||||
|
4. Commit and push your changes.
|
||||||
|
5. Open a pull request.
|
||||||
|
|
||||||
|
### Guidelines
|
||||||
|
|
||||||
|
- Ensure your code adheres to the existing style.
|
||||||
|
- Include documentation and unit tests (where applicable).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
# Grayjay Core License 1.0
|
# Source First License 1.1
|
||||||
|
|
||||||
## Acceptance
|
## Acceptance
|
||||||
By using the software, you agree to all of the terms and conditions below.
|
By using the software, you agree to all of the terms and conditions below.
|
||||||
@@ -16,7 +16,7 @@ Notwithstanding the above, you may not remove or obscure any functionality in th
|
|||||||
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.
|
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
|
## 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.
|
If you make any written claim that the software infringes or contributes to infringement of any patent, your license for the software granted under these terms ends immediately. If your company makes such a claim, your license ends immediately for work on behalf of your company.
|
||||||
|
|
||||||
## Notices
|
## 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.
|
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.
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ technologies that frustrate centralization and industry consolidation.
|
|||||||
|
|
||||||
<table border="0">
|
<table border="0">
|
||||||
<tr>
|
<tr>
|
||||||
<td><b style="font-size:30px"><img src="images/video.jpg" height="700" /></b></td>
|
<td><b style="font-size:30px"><img src="images/video.png" height="700" /></b></td>
|
||||||
<td><b style="font-size:30px"><img src="images/video-details.jpg" height="700" /></b></td>
|
<td><b style="font-size:30px"><img src="images/video-details.png" height="700" /></b></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Video</td>
|
<td>Video</td>
|
||||||
@@ -24,12 +24,10 @@ The FUTO media app is a player that exposes multiple video websites as sources i
|
|||||||
|
|
||||||
<table border="0">
|
<table border="0">
|
||||||
<tr>
|
<tr>
|
||||||
<td><b style="font-size:30px"><img src="images/sources.jpg" height="700" /></b></td>
|
<td><b style="font-size:30px"><img src="images/source.png" height="700" /></b></td>
|
||||||
<td><b style="font-size:30px"><img src="images/sources-disabled.jpg" height="700" /></b></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Sources (all enabled)</td>
|
<td>Sources</td>
|
||||||
<td>Sources (one disabled)</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
@@ -38,7 +36,7 @@ Additional sources can also be installed. These sources are JavaScript sources,
|
|||||||
<table border="0">
|
<table border="0">
|
||||||
<tr>
|
<tr>
|
||||||
<td><b style="font-size:30px"><img src="images/source-install.png" height="700" /></b></td>
|
<td><b style="font-size:30px"><img src="images/source-install.png" height="700" /></b></td>
|
||||||
<td><b style="font-size:30px"><img src="images/source-settings.jpg" height="700" /></b></td>
|
<td><b style="font-size:30px"><img src="images/source-settings.png" height="700" /></b></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Install a new source</td>
|
<td>Install a new source</td>
|
||||||
@@ -54,8 +52,8 @@ When a user enters a search term into the search bar, the query is posted to th
|
|||||||
|
|
||||||
<table border="0">
|
<table border="0">
|
||||||
<tr>
|
<tr>
|
||||||
<td><b style="font-size:30px"><img src="images/search-list.jpg" height="700" /></b></td>
|
<td><b style="font-size:30px"><img src="images/search-list.png" height="700" /></b></td>
|
||||||
<td><b style="font-size:30px"><img src="images/search-preview.jpg" height="700" /></b></td>
|
<td><b style="font-size:30px"><img src="images/search-preview.png" height="700" /></b></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Search (list)</td>
|
<td>Search (list)</td>
|
||||||
@@ -71,7 +69,7 @@ Creators are able to configure their profile using NeoPass.
|
|||||||
|
|
||||||
<table border="0">
|
<table border="0">
|
||||||
<tr>
|
<tr>
|
||||||
<td><b style="font-size:30px"><img src="images/channel.jpg" height="700" /></b></td>
|
<td><b style="font-size:30px"><img src="images/channel.png" height="700" /></b></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Channel</td>
|
<td>Channel</td>
|
||||||
@@ -112,7 +110,7 @@ The app offers a lot of settings customizing how the app looks and feels. An exa
|
|||||||
|
|
||||||
<table border="0">
|
<table border="0">
|
||||||
<tr>
|
<tr>
|
||||||
<td><b style="font-size:30px"><img src="images/settings.jpg" height="700" /></b></td>
|
<td><b style="font-size:30px"><img src="images/settings.png" height="700" /></b></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Settings</td>
|
<td>Settings</td>
|
||||||
@@ -125,8 +123,8 @@ Playlists allow you to make a collection of videos that you can create and custo
|
|||||||
|
|
||||||
<table border="0">
|
<table border="0">
|
||||||
<tr>
|
<tr>
|
||||||
<td><b style="font-size:30px"><img src="images/playlists.jpg" height="700" /></b></td>
|
<td><b style="font-size:30px"><img src="images/playlists.png" height="700" /></b></td>
|
||||||
<td><b style="font-size:30px"><img src="images/playlist.jpg" height="700" /></b></td>
|
<td><b style="font-size:30px"><img src="images/playlist.png" height="700" /></b></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Playlists</td>
|
<td>Playlists</td>
|
||||||
@@ -142,7 +140,7 @@ Both individual videos and playlists can be downloaded for local, offline playba
|
|||||||
|
|
||||||
<table border="0">
|
<table border="0">
|
||||||
<tr>
|
<tr>
|
||||||
<td><b style="font-size:30px"><img src="images/downloads.jpg" height="700" /></b></td>
|
<td><b style="font-size:30px"><img src="images/downloads.png" height="700" /></b></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Downloads</td>
|
<td>Downloads</td>
|
||||||
@@ -157,7 +155,7 @@ For more information about casting please click [here](./docs/casting.md).
|
|||||||
|
|
||||||
<table border="0">
|
<table border="0">
|
||||||
<tr>
|
<tr>
|
||||||
<td><b style="font-size:30px"><img src="images/casting.jpg" height="700" /></b></td>
|
<td><b style="font-size:30px"><img src="images/casting.png" height="700" /></b></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Casting</td>
|
<td>Casting</td>
|
||||||
@@ -182,6 +180,12 @@ In the future we hope to offer users the choice of their desired recommendation
|
|||||||
|
|
||||||
1. Download a copy of the repository.
|
1. Download a copy of the repository.
|
||||||
2. Open the project in Android Studio: Once the repository is cloned, you can open it in Android Studio by selecting "Open an Existing Project" from the welcome screen and navigating to the directory where you cloned the repository.
|
2. Open the project in Android Studio: Once the repository is cloned, you can open it in Android Studio by selecting "Open an Existing Project" from the welcome screen and navigating to the directory where you cloned the repository.
|
||||||
|
3. Open the terminal in Android Studio by clicking on the terminal icon on bottom left and run the following command:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git submodule update --init --recursive
|
||||||
|
```
|
||||||
|
|
||||||
3. Build the project: With the project open in Android Studio, you can build it by selecting "Build > Make Project" from the main menu. This will compile the code and generate an APK file that you can install on your device or emulator.
|
3. Build the project: With the project open in Android Studio, you can build it by selecting "Build > Make Project" from the main menu. This will compile the code and generate an APK file that you can install on your device or emulator.
|
||||||
4. Run the project: To run the project, select "Run > Run 'app'" from the main menu. This will launch the app on your device or emulator, allowing you to test it and make any necessary changes.
|
4. Run the project: To run the project, select "Run > Run 'app'" from the main menu. This will launch the app on your device or emulator, allowing you to test it and make any necessary changes.
|
||||||
|
|
||||||
@@ -199,7 +203,6 @@ Create a tag on the master branch, incrementing the last version number by 1 (fo
|
|||||||
|
|
||||||
Click on the CI/CD tab, you should now see the tests and build are in progress. If the build succeeds the last step will become available. The last step is a manual action which can be triggered by clicking the run button on the action. This action will deploy the build to all users using the app through auto-update.
|
Click on the CI/CD tab, you should now see the tests and build are in progress. If the build succeeds the last step will become available. The last step is a manual action which can be triggered by clicking the run button on the action. This action will deploy the build to all users using the app through auto-update.
|
||||||
|
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
The documentation can be found [here](https://gitlab.futo.org/videostreaming/documents/-/wikis/API-Overview).
|
The documentation can be found [here](https://gitlab.futo.org/videostreaming/documents/-/wikis/API-Overview).
|
||||||
|
|||||||
+11
-2
@@ -2,7 +2,7 @@ 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.9.21'
|
id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.21'
|
||||||
id 'org.ajoberstar.grgit' version '1.7.2'
|
id 'org.ajoberstar.grgit' version '5.2.2'
|
||||||
id 'com.google.protobuf'
|
id 'com.google.protobuf'
|
||||||
id 'kotlin-parcelize'
|
id 'kotlin-parcelize'
|
||||||
id 'com.google.devtools.ksp'
|
id 'com.google.devtools.ksp'
|
||||||
@@ -144,9 +144,19 @@ android {
|
|||||||
buildFeatures {
|
buildFeatures {
|
||||||
buildConfig true
|
buildConfig true
|
||||||
}
|
}
|
||||||
|
sourceSets {
|
||||||
|
main {
|
||||||
|
assets {
|
||||||
|
srcDirs 'src/main/assets', 'src/tests/assets', 'src/test/assets'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation 'com.google.dagger:dagger:2.48'
|
||||||
|
implementation 'androidx.test:monitor:1.7.2'
|
||||||
|
annotationProcessor 'com.google.dagger:dagger-compiler:2.48'
|
||||||
|
|
||||||
//Core
|
//Core
|
||||||
implementation 'androidx.core:core-ktx:1.12.0'
|
implementation 'androidx.core:core-ktx:1.12.0'
|
||||||
@@ -184,7 +194,6 @@ dependencies {
|
|||||||
implementation 'androidx.media:media:1.7.0'
|
implementation 'androidx.media:media:1.7.0'
|
||||||
|
|
||||||
//Other
|
//Other
|
||||||
implementation 'org.jmdns:jmdns:3.5.1'
|
|
||||||
implementation 'org.jsoup:jsoup:1.15.3'
|
implementation 'org.jsoup:jsoup:1.15.3'
|
||||||
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'
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
|
<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="com.android.alarm.permission.SET_ALARM"/>
|
||||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
|
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
|
||||||
|
<!--<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>-->
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
|
||||||
<uses-permission android:name="android.permission.WRITE_SETTINGS" tools:ignore="ProtectedPermissions"/>
|
<uses-permission android:name="android.permission.WRITE_SETTINGS" tools:ignore="ProtectedPermissions"/>
|
||||||
@@ -35,6 +36,12 @@
|
|||||||
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" />
|
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" />
|
||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
|
<receiver android:name=".receivers.MediaButtonReceiver" android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
<service android:name=".services.MediaPlaybackService"
|
<service android:name=".services.MediaPlaybackService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:foregroundServiceType="mediaPlayback" />
|
android:foregroundServiceType="mediaPlayback" />
|
||||||
@@ -50,9 +57,8 @@
|
|||||||
android:name=".activities.MainActivity"
|
android:name=".activities.MainActivity"
|
||||||
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
|
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:screenOrientation="portrait"
|
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar"
|
android:theme="@style/Theme.FutoVideo.NoActionBar"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleInstance"
|
||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
android:supportsPictureInPicture="true">
|
android:supportsPictureInPicture="true">
|
||||||
|
|
||||||
@@ -145,34 +151,31 @@
|
|||||||
<data android:scheme="polycentric" />
|
<data android:scheme="polycentric" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.TestActivity"
|
android:name=".activities.TestActivity"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.SettingsActivity"
|
android:name=".activities.SettingsActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.DeveloperActivity"
|
android:name=".activities.DeveloperActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.ExceptionActivity"
|
android:name=".activities.ExceptionActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.CaptchaActivity"
|
android:name=".activities.CaptchaActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.LoginActivity"
|
android:name=".activities.LoginActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.AddSourceActivity"
|
android:name=".activities.AddSourceActivity"
|
||||||
android:screenOrientation="portrait"
|
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar">
|
android:theme="@style/Theme.FutoVideo.NoActionBar">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
@@ -186,44 +189,55 @@
|
|||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.AddSourceOptionsActivity"
|
android:name=".activities.AddSourceOptionsActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.PolycentricHomeActivity"
|
android:name=".activities.PolycentricHomeActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.PolycentricBackupActivity"
|
android:name=".activities.PolycentricBackupActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.PolycentricCreateProfileActivity"
|
android:name=".activities.PolycentricCreateProfileActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.PolycentricProfileActivity"
|
android:name=".activities.PolycentricProfileActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.PolycentricWhyActivity"
|
android:name=".activities.PolycentricWhyActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.PolycentricImportProfileActivity"
|
android:name=".activities.PolycentricImportProfileActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.ManageTabsActivity"
|
android:name=".activities.ManageTabsActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.QRCaptureActivity"
|
android:name=".activities.QRCaptureActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.FCastGuideActivity"
|
android:name=".activities.FCastGuideActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
|
<activity
|
||||||
|
android:name=".activities.SyncHomeActivity"
|
||||||
|
android:screenOrientation="sensorPortrait"
|
||||||
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
|
<activity
|
||||||
|
android:name=".activities.SyncPairActivity"
|
||||||
|
android:screenOrientation="sensorPortrait"
|
||||||
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
|
<activity
|
||||||
|
android:name=".activities.SyncShowPairingCodeActivity"
|
||||||
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -201,7 +201,7 @@ class PlatformContent {
|
|||||||
obj = obj ?? {};
|
obj = obj ?? {};
|
||||||
this.id = obj.id ?? PlatformID(); //PlatformID
|
this.id = obj.id ?? PlatformID(); //PlatformID
|
||||||
this.name = obj.name ?? ""; //string
|
this.name = obj.name ?? ""; //string
|
||||||
this.thumbnails = obj.thumbnails; //Thumbnail[]
|
this.thumbnails = obj.thumbnails ?? new Thumbnails([]); //Thumbnail[]
|
||||||
this.author = obj.author; //PlatformAuthorLink
|
this.author = obj.author; //PlatformAuthorLink
|
||||||
this.datetime = obj.datetime ?? obj.uploadDate ?? 0; //OffsetDateTime (Long)
|
this.datetime = obj.datetime ?? obj.uploadDate ?? 0; //OffsetDateTime (Long)
|
||||||
this.url = obj.url ?? ""; //String
|
this.url = obj.url ?? ""; //String
|
||||||
@@ -278,12 +278,49 @@ class PlatformPostDetails extends PlatformPost {
|
|||||||
super(obj);
|
super(obj);
|
||||||
obj = obj ?? {};
|
obj = obj ?? {};
|
||||||
this.plugin_type = "PlatformPostDetails";
|
this.plugin_type = "PlatformPostDetails";
|
||||||
this.rating = obj.rating ?? RatingLikes(-1);
|
this.rating = obj.rating ?? new RatingLikes(-1);
|
||||||
this.textType = obj.textType ?? 0;
|
this.textType = obj.textType ?? 0;
|
||||||
this.content = obj.content ?? "";
|
this.content = obj.content ?? "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class PlatformArticleDetails extends PlatformContent {
|
||||||
|
constructor(obj) {
|
||||||
|
super(obj, 3);
|
||||||
|
obj = obj ?? {};
|
||||||
|
this.plugin_type = "PlatformArticleDetails";
|
||||||
|
this.rating = obj.rating ?? new RatingLikes(-1);
|
||||||
|
this.summary = obj.summary ?? "";
|
||||||
|
this.segments = obj.segments ?? [];
|
||||||
|
this.thumbnails = obj.thumbnails ?? new Thumbnails([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class ArticleSegment {
|
||||||
|
constructor(type) {
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class ArticleTextSegment extends ArticleSegment {
|
||||||
|
constructor(content, textType) {
|
||||||
|
super(1);
|
||||||
|
this.textType = textType;
|
||||||
|
this.content = content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class ArticleImagesSegment extends ArticleSegment {
|
||||||
|
constructor(images) {
|
||||||
|
super(2);
|
||||||
|
this.images = images;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class ArticleNestedSegment extends ArticleSegment {
|
||||||
|
constructor(nested) {
|
||||||
|
super(9);
|
||||||
|
this.nested = nested;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//Sources
|
//Sources
|
||||||
class VideoSourceDescriptor {
|
class VideoSourceDescriptor {
|
||||||
constructor(obj) {
|
constructor(obj) {
|
||||||
@@ -330,6 +367,16 @@ class VideoUrlSource {
|
|||||||
this.requestModifier = obj.requestModifier;
|
this.requestModifier = obj.requestModifier;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
class VideoUrlWidevineSource extends VideoUrlSource {
|
||||||
|
constructor(obj) {
|
||||||
|
super(obj);
|
||||||
|
this.plugin_type = "VideoUrlWidevineSource";
|
||||||
|
|
||||||
|
this.licenseUri = obj.licenseUri;
|
||||||
|
if(obj.getLicenseRequestExecutor)
|
||||||
|
this.getLicenseRequestExecutor = obj.getLicenseRequestExecutor;
|
||||||
|
}
|
||||||
|
}
|
||||||
class VideoUrlRangeSource extends VideoUrlSource {
|
class VideoUrlRangeSource extends VideoUrlSource {
|
||||||
constructor(obj) {
|
constructor(obj) {
|
||||||
super(obj);
|
super(obj);
|
||||||
@@ -362,8 +409,26 @@ class AudioUrlWidevineSource extends AudioUrlSource {
|
|||||||
super(obj);
|
super(obj);
|
||||||
this.plugin_type = "AudioUrlWidevineSource";
|
this.plugin_type = "AudioUrlWidevineSource";
|
||||||
|
|
||||||
this.bearerToken = obj.bearerToken;
|
|
||||||
this.licenseUri = obj.licenseUri;
|
this.licenseUri = obj.licenseUri;
|
||||||
|
if(obj.getLicenseRequestExecutor)
|
||||||
|
this.getLicenseRequestExecutor = obj.getLicenseRequestExecutor;
|
||||||
|
|
||||||
|
// deprecated api conversion
|
||||||
|
if(obj.bearerToken) {
|
||||||
|
this.getLicenseRequestExecutor = () => {
|
||||||
|
return {
|
||||||
|
executeRequest: (url, _headers, _method, license_request_data) => {
|
||||||
|
return http.POST(
|
||||||
|
url,
|
||||||
|
license_request_data,
|
||||||
|
{ Authorization: `Bearer ${obj.bearerToken}` },
|
||||||
|
false,
|
||||||
|
true
|
||||||
|
).body
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class AudioUrlRangeSource extends AudioUrlSource {
|
class AudioUrlRangeSource extends AudioUrlSource {
|
||||||
@@ -406,6 +471,49 @@ class DashSource {
|
|||||||
this.requestModifier = obj.requestModifier;
|
this.requestModifier = obj.requestModifier;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
class DashWidevineSource extends DashSource {
|
||||||
|
constructor(obj) {
|
||||||
|
super(obj);
|
||||||
|
this.plugin_type = "DashWidevineSource";
|
||||||
|
|
||||||
|
this.licenseUri = obj.licenseUri;
|
||||||
|
if(obj.getLicenseRequestExecutor)
|
||||||
|
this.getLicenseRequestExecutor = obj.getLicenseRequestExecutor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class DashManifestRawSource {
|
||||||
|
constructor(obj) {
|
||||||
|
obj = obj ?? {};
|
||||||
|
this.plugin_type = "DashRawSource";
|
||||||
|
this.name = obj.name ?? "";
|
||||||
|
this.bitrate = obj.bitrate ?? 0;
|
||||||
|
this.container = obj.container ?? "";
|
||||||
|
this.codec = obj.codec ?? "";
|
||||||
|
this.duration = obj.duration ?? 0;
|
||||||
|
this.url = obj.url;
|
||||||
|
this.language = obj.language ?? Language.UNKNOWN;
|
||||||
|
if(obj.requestModifier)
|
||||||
|
this.requestModifier = obj.requestModifier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DashManifestRawAudioSource {
|
||||||
|
constructor(obj) {
|
||||||
|
obj = obj ?? {};
|
||||||
|
this.plugin_type = "DashRawAudioSource";
|
||||||
|
this.name = obj.name ?? "";
|
||||||
|
this.bitrate = obj.bitrate ?? 0;
|
||||||
|
this.container = obj.container ?? "";
|
||||||
|
this.codec = obj.codec ?? "";
|
||||||
|
this.duration = obj.duration ?? 0;
|
||||||
|
this.url = obj.url;
|
||||||
|
this.language = obj.language ?? Language.UNKNOWN;
|
||||||
|
this.manifest = obj.manifest ?? null;
|
||||||
|
if(obj.requestModifier)
|
||||||
|
this.requestModifier = obj.requestModifier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class RequestModifier {
|
class RequestModifier {
|
||||||
constructor(obj) {
|
constructor(obj) {
|
||||||
@@ -762,3 +870,99 @@ class URLSearchParams {
|
|||||||
return searchString;
|
return searchString;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var __REGEX_SPACE_CHARACTERS = /<%= spaceCharacters %>/g;
|
||||||
|
var __btoa_TABLE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
||||||
|
function btoa(input) {
|
||||||
|
input = String(input);
|
||||||
|
if (/[^\0-\xFF]/.test(input)) {
|
||||||
|
// Note: no need to special-case astral symbols here, as surrogates are
|
||||||
|
// matched, and the input is supposed to only contain ASCII anyway.
|
||||||
|
error(
|
||||||
|
'The string to be encoded contains characters outside of the ' +
|
||||||
|
'Latin1 range.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
var padding = input.length % 3;
|
||||||
|
var output = '';
|
||||||
|
var position = -1;
|
||||||
|
var a;
|
||||||
|
var b;
|
||||||
|
var c;
|
||||||
|
var buffer;
|
||||||
|
// Make sure any padding is handled outside of the loop.
|
||||||
|
var length = input.length - padding;
|
||||||
|
|
||||||
|
while (++position < length) {
|
||||||
|
// Read three bytes, i.e. 24 bits.
|
||||||
|
a = input.charCodeAt(position) << 16;
|
||||||
|
b = input.charCodeAt(++position) << 8;
|
||||||
|
c = input.charCodeAt(++position);
|
||||||
|
buffer = a + b + c;
|
||||||
|
// Turn the 24 bits into four chunks of 6 bits each, and append the
|
||||||
|
// matching character for each of them to the output.
|
||||||
|
output += (
|
||||||
|
__btoa_TABLE.charAt(buffer >> 18 & 0x3F) +
|
||||||
|
__btoa_TABLE.charAt(buffer >> 12 & 0x3F) +
|
||||||
|
__btoa_TABLE.charAt(buffer >> 6 & 0x3F) +
|
||||||
|
__btoa_TABLE.charAt(buffer & 0x3F)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (padding == 2) {
|
||||||
|
a = input.charCodeAt(position) << 8;
|
||||||
|
b = input.charCodeAt(++position);
|
||||||
|
buffer = a + b;
|
||||||
|
output += (
|
||||||
|
__btoa_TABLE.charAt(buffer >> 10) +
|
||||||
|
__btoa_TABLE.charAt((buffer >> 4) & 0x3F) +
|
||||||
|
__btoa_TABLE.charAt((buffer << 2) & 0x3F) +
|
||||||
|
'='
|
||||||
|
);
|
||||||
|
} else if (padding == 1) {
|
||||||
|
buffer = input.charCodeAt(position);
|
||||||
|
output += (
|
||||||
|
__btoa_TABLE.charAt(buffer >> 2) +
|
||||||
|
__btoa_TABLE.charAt((buffer << 4) & 0x3F) +
|
||||||
|
'=='
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
};
|
||||||
|
function atob(input) {
|
||||||
|
input = String(input)
|
||||||
|
.replace(__REGEX_SPACE_CHARACTERS, '');
|
||||||
|
var length = input.length;
|
||||||
|
if (length % 4 == 0) {
|
||||||
|
input = input.replace(/==?$/, '');
|
||||||
|
length = input.length;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
length % 4 == 1 ||
|
||||||
|
// http://whatwg.org/C#alphanumeric-ascii-characters
|
||||||
|
/[^+a-zA-Z0-9/]/.test(input)
|
||||||
|
) {
|
||||||
|
error(
|
||||||
|
'Invalid character: the string to be decoded is not correctly encoded.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
var bitCounter = 0;
|
||||||
|
var bitStorage;
|
||||||
|
var buffer;
|
||||||
|
var output = '';
|
||||||
|
var position = -1;
|
||||||
|
while (++position < length) {
|
||||||
|
buffer = __btoa_TABLE.indexOf(input.charAt(position));
|
||||||
|
bitStorage = bitCounter % 4 ? bitStorage * 64 + buffer : buffer;
|
||||||
|
// Unless this is the first of a group of 4 characters…
|
||||||
|
if (bitCounter++ % 4) {
|
||||||
|
// …convert the first 8 bits to a single ASCII character.
|
||||||
|
output += String.fromCharCode(
|
||||||
|
0xFF & bitStorage >> (-2 * bitCounter & 6)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
};
|
||||||
|
|||||||
@@ -18,7 +18,10 @@ fun IAudioSource.isDownloadable(): Boolean = VideoHelper.isDownloadable(this);
|
|||||||
@UnstableApi
|
@UnstableApi
|
||||||
fun JSSource.getHttpDataSourceFactory(): HttpDataSource.Factory {
|
fun JSSource.getHttpDataSourceFactory(): HttpDataSource.Factory {
|
||||||
val requestModifier = getRequestModifier();
|
val requestModifier = getRequestModifier();
|
||||||
return if (requestModifier != null) {
|
val requestExecutor = getRequestExecutor();
|
||||||
|
return if (requestExecutor != null) {
|
||||||
|
JSHttpDataSource.Factory().setRequestExecutor(requestExecutor);
|
||||||
|
} else if (requestModifier != null) {
|
||||||
JSHttpDataSource.Factory().setRequestModifier(requestModifier);
|
JSHttpDataSource.Factory().setRequestModifier(requestModifier);
|
||||||
} else {
|
} else {
|
||||||
DefaultHttpDataSource.Factory();
|
DefaultHttpDataSource.Factory();
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,6 +1,9 @@
|
|||||||
package com.futo.platformplayer
|
package com.futo.platformplayer
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import java.net.Inet4Address
|
||||||
|
import java.net.Inet6Address
|
||||||
|
import java.net.InetAddress
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.net.URISyntaxException
|
import java.net.URISyntaxException
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
@@ -25,4 +28,18 @@ fun String?.yesNoToBoolean(): Boolean {
|
|||||||
|
|
||||||
fun Boolean?.toYesNo(): String {
|
fun Boolean?.toYesNo(): String {
|
||||||
return if (this == true) "YES" else "NO"
|
return if (this == true) "YES" else "NO"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun InetAddress?.toUrlAddress(): String {
|
||||||
|
return when (this) {
|
||||||
|
is Inet6Address -> {
|
||||||
|
"[${toString()}]"
|
||||||
|
}
|
||||||
|
is Inet4Address -> {
|
||||||
|
toString()
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
throw Exception("Invalid address type")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
package com.futo.platformplayer
|
||||||
|
|
||||||
|
import com.google.common.base.Preconditions
|
||||||
|
import com.google.common.io.ByteStreams
|
||||||
|
import com.google.common.primitives.Ints
|
||||||
|
import com.google.common.primitives.Longs
|
||||||
|
import java.io.DataInput
|
||||||
|
import java.io.DataInputStream
|
||||||
|
import java.io.EOFException
|
||||||
|
import java.io.FilterInputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
class LittleEndianDataInputStream
|
||||||
|
/**
|
||||||
|
* Creates a `LittleEndianDataInputStream` that wraps the given stream.
|
||||||
|
*
|
||||||
|
* @param in the stream to delegate to
|
||||||
|
*/
|
||||||
|
(`in`: InputStream?) : FilterInputStream(Preconditions.checkNotNull(`in`)), DataInput {
|
||||||
|
/** This method will throw an [UnsupportedOperationException]. */
|
||||||
|
override fun readLine(): String {
|
||||||
|
throw UnsupportedOperationException("readLine is not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun readFully(b: ByteArray) {
|
||||||
|
ByteStreams.readFully(this, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun readFully(b: ByteArray, off: Int, len: Int) {
|
||||||
|
ByteStreams.readFully(this, b, off, len)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun skipBytes(n: Int): Int {
|
||||||
|
return `in`.skip(n.toLong()).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun readUnsignedByte(): Int {
|
||||||
|
val b1 = `in`.read()
|
||||||
|
if (0 > b1) {
|
||||||
|
throw EOFException()
|
||||||
|
}
|
||||||
|
|
||||||
|
return b1
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads an unsigned `short` as specified by [DataInputStream.readUnsignedShort],
|
||||||
|
* except using little-endian byte order.
|
||||||
|
*
|
||||||
|
* @return the next two bytes of the input stream, interpreted as an unsigned 16-bit integer in
|
||||||
|
* little-endian byte order
|
||||||
|
* @throws IOException if an I/O error occurs
|
||||||
|
*/
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun readUnsignedShort(): Int {
|
||||||
|
val b1 = readAndCheckByte()
|
||||||
|
val b2 = readAndCheckByte()
|
||||||
|
|
||||||
|
return Ints.fromBytes(0.toByte(), 0.toByte(), b2, b1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads an integer as specified by [DataInputStream.readInt], except using little-endian
|
||||||
|
* byte order.
|
||||||
|
*
|
||||||
|
* @return the next four bytes of the input stream, interpreted as an `int` in little-endian
|
||||||
|
* byte order
|
||||||
|
* @throws IOException if an I/O error occurs
|
||||||
|
*/
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun readInt(): Int {
|
||||||
|
val b1 = readAndCheckByte()
|
||||||
|
val b2 = readAndCheckByte()
|
||||||
|
val b3 = readAndCheckByte()
|
||||||
|
val b4 = readAndCheckByte()
|
||||||
|
|
||||||
|
return Ints.fromBytes(b4, b3, b2, b1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a `long` as specified by [DataInputStream.readLong], except using
|
||||||
|
* little-endian byte order.
|
||||||
|
*
|
||||||
|
* @return the next eight bytes of the input stream, interpreted as a `long` in
|
||||||
|
* little-endian byte order
|
||||||
|
* @throws IOException if an I/O error occurs
|
||||||
|
*/
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun readLong(): Long {
|
||||||
|
val b1 = readAndCheckByte()
|
||||||
|
val b2 = readAndCheckByte()
|
||||||
|
val b3 = readAndCheckByte()
|
||||||
|
val b4 = readAndCheckByte()
|
||||||
|
val b5 = readAndCheckByte()
|
||||||
|
val b6 = readAndCheckByte()
|
||||||
|
val b7 = readAndCheckByte()
|
||||||
|
val b8 = readAndCheckByte()
|
||||||
|
|
||||||
|
return Longs.fromBytes(b8, b7, b6, b5, b4, b3, b2, b1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a `float` as specified by [DataInputStream.readFloat], except using
|
||||||
|
* little-endian byte order.
|
||||||
|
*
|
||||||
|
* @return the next four bytes of the input stream, interpreted as a `float` in
|
||||||
|
* little-endian byte order
|
||||||
|
* @throws IOException if an I/O error occurs
|
||||||
|
*/
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun readFloat(): Float {
|
||||||
|
return java.lang.Float.intBitsToFloat(readInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a `double` as specified by [DataInputStream.readDouble], except using
|
||||||
|
* little-endian byte order.
|
||||||
|
*
|
||||||
|
* @return the next eight bytes of the input stream, interpreted as a `double` in
|
||||||
|
* little-endian byte order
|
||||||
|
* @throws IOException if an I/O error occurs
|
||||||
|
*/
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun readDouble(): Double {
|
||||||
|
return java.lang.Double.longBitsToDouble(readLong())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun readUTF(): String {
|
||||||
|
return DataInputStream(`in`).readUTF()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a `short` as specified by [DataInputStream.readShort], except using
|
||||||
|
* little-endian byte order.
|
||||||
|
*
|
||||||
|
* @return the next two bytes of the input stream, interpreted as a `short` in little-endian
|
||||||
|
* byte order.
|
||||||
|
* @throws IOException if an I/O error occurs.
|
||||||
|
*/
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun readShort(): Short {
|
||||||
|
return readUnsignedShort().toShort()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a char as specified by [DataInputStream.readChar], except using little-endian
|
||||||
|
* byte order.
|
||||||
|
*
|
||||||
|
* @return the next two bytes of the input stream, interpreted as a `char` in little-endian
|
||||||
|
* byte order
|
||||||
|
* @throws IOException if an I/O error occurs
|
||||||
|
*/
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun readChar(): Char {
|
||||||
|
return readUnsignedShort().toChar()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun readByte(): Byte {
|
||||||
|
return readUnsignedByte().toByte()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun readBoolean(): Boolean {
|
||||||
|
return readUnsignedByte() != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a byte from the input stream checking that the end of file (EOF) has not been
|
||||||
|
* encountered.
|
||||||
|
*
|
||||||
|
* @return byte read from input
|
||||||
|
* @throws IOException if an error is encountered while reading
|
||||||
|
* @throws EOFException if the end of file (EOF) is encountered.
|
||||||
|
*/
|
||||||
|
@Throws(IOException::class, EOFException::class)
|
||||||
|
private fun readAndCheckByte(): Byte {
|
||||||
|
val b1 = `in`.read()
|
||||||
|
|
||||||
|
if (-1 == b1) {
|
||||||
|
throw EOFException()
|
||||||
|
}
|
||||||
|
|
||||||
|
return b1.toByte()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
package com.futo.platformplayer
|
||||||
|
|
||||||
|
import com.google.common.base.Preconditions
|
||||||
|
import com.google.common.primitives.Longs
|
||||||
|
import java.io.*
|
||||||
|
|
||||||
|
class LittleEndianDataOutputStream
|
||||||
|
/**
|
||||||
|
* Creates a `LittleEndianDataOutputStream` that wraps the given stream.
|
||||||
|
*
|
||||||
|
* @param out the stream to delegate to
|
||||||
|
*/
|
||||||
|
(out: OutputStream?) : FilterOutputStream(DataOutputStream(Preconditions.checkNotNull(out))),
|
||||||
|
DataOutput {
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun write(b: ByteArray, off: Int, len: Int) {
|
||||||
|
// Override slow FilterOutputStream impl
|
||||||
|
out.write(b, off, len)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun writeBoolean(v: Boolean) {
|
||||||
|
(out as DataOutputStream).writeBoolean(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun writeByte(v: Int) {
|
||||||
|
(out as DataOutputStream).writeByte(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated(
|
||||||
|
"""The semantics of {@code writeBytes(String s)} are considered dangerous. Please use
|
||||||
|
{@link #writeUTF(String s)}, {@link #writeChars(String s)} or another write method instead."""
|
||||||
|
)
|
||||||
|
@Throws(
|
||||||
|
IOException::class
|
||||||
|
)
|
||||||
|
override fun writeBytes(s: String) {
|
||||||
|
(out as DataOutputStream).writeBytes(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a char as specified by [DataOutputStream.writeChar], except using
|
||||||
|
* little-endian byte order.
|
||||||
|
*
|
||||||
|
* @throws IOException if an I/O error occurs
|
||||||
|
*/
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun writeChar(v: Int) {
|
||||||
|
writeShort(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a `String` as specified by [DataOutputStream.writeChars], except
|
||||||
|
* each character is written using little-endian byte order.
|
||||||
|
*
|
||||||
|
* @throws IOException if an I/O error occurs
|
||||||
|
*/
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun writeChars(s: String) {
|
||||||
|
for (i in 0 until s.length) {
|
||||||
|
writeChar(s[i].code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a `double` as specified by [DataOutputStream.writeDouble], except
|
||||||
|
* using little-endian byte order.
|
||||||
|
*
|
||||||
|
* @throws IOException if an I/O error occurs
|
||||||
|
*/
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun writeDouble(v: Double) {
|
||||||
|
writeLong(java.lang.Double.doubleToLongBits(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a `float` as specified by [DataOutputStream.writeFloat], except using
|
||||||
|
* little-endian byte order.
|
||||||
|
*
|
||||||
|
* @throws IOException if an I/O error occurs
|
||||||
|
*/
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun writeFloat(v: Float) {
|
||||||
|
writeInt(java.lang.Float.floatToIntBits(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes an `int` as specified by [DataOutputStream.writeInt], except using
|
||||||
|
* little-endian byte order.
|
||||||
|
*
|
||||||
|
* @throws IOException if an I/O error occurs
|
||||||
|
*/
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun writeInt(v: Int) {
|
||||||
|
val bytes = byteArrayOf(
|
||||||
|
(0xFF and v).toByte(),
|
||||||
|
(0xFF and (v shr 8)).toByte(),
|
||||||
|
(0xFF and (v shr 16)).toByte(),
|
||||||
|
(0xFF and (v shr 24)).toByte()
|
||||||
|
)
|
||||||
|
out.write(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a `long` as specified by [DataOutputStream.writeLong], except using
|
||||||
|
* little-endian byte order.
|
||||||
|
*
|
||||||
|
* @throws IOException if an I/O error occurs
|
||||||
|
*/
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun writeLong(v: Long) {
|
||||||
|
val bytes = Longs.toByteArray(java.lang.Long.reverseBytes(v))
|
||||||
|
write(bytes, 0, bytes.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a `short` as specified by [DataOutputStream.writeShort], except using
|
||||||
|
* little-endian byte order.
|
||||||
|
*
|
||||||
|
* @throws IOException if an I/O error occurs
|
||||||
|
*/
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun writeShort(v: Int) {
|
||||||
|
val bytes = byteArrayOf(
|
||||||
|
(0xFF and v).toByte(),
|
||||||
|
(0xFF and (v shr 8)).toByte()
|
||||||
|
)
|
||||||
|
out.write(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun writeUTF(str: String) {
|
||||||
|
(out as DataOutputStream).writeUTF(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overriding close() because FilterOutputStream's close() method pre-JDK8 has bad behavior:
|
||||||
|
// it silently ignores any exception thrown by flush(). Instead, just close the delegate stream.
|
||||||
|
// It should flush itself if necessary.
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun close() {
|
||||||
|
out.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import com.futo.platformplayer.activities.ManageTabsActivity
|
|||||||
import com.futo.platformplayer.activities.PolycentricHomeActivity
|
import com.futo.platformplayer.activities.PolycentricHomeActivity
|
||||||
import com.futo.platformplayer.activities.PolycentricProfileActivity
|
import com.futo.platformplayer.activities.PolycentricProfileActivity
|
||||||
import com.futo.platformplayer.activities.SettingsActivity
|
import com.futo.platformplayer.activities.SettingsActivity
|
||||||
|
import com.futo.platformplayer.activities.SyncHomeActivity
|
||||||
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
|
||||||
@@ -32,7 +33,6 @@ import com.futo.platformplayer.views.fields.DropdownFieldOptionsId
|
|||||||
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.FormFieldButton
|
import com.futo.platformplayer.views.fields.FormFieldButton
|
||||||
import com.futo.platformplayer.views.fields.FormFieldWarning
|
|
||||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -44,6 +44,7 @@ import kotlinx.serialization.json.Json
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class MenuBottomBarSetting(val id: Int, var enabled: Boolean);
|
data class MenuBottomBarSetting(val id: Int, var enabled: Boolean);
|
||||||
|
|
||||||
@@ -57,7 +58,16 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@Transient
|
@Transient
|
||||||
val onTabsChanged = Event0();
|
val onTabsChanged = Event0();
|
||||||
|
|
||||||
@FormField(R.string.manage_polycentric_identity, FieldForm.BUTTON, R.string.manage_your_polycentric_identity, -6)
|
@FormField(R.string.sync_grayjay, FieldForm.BUTTON, R.string.sync_grayjay_description, -8)
|
||||||
|
@FormFieldButton(R.drawable.ic_update)
|
||||||
|
fun syncGrayjay() {
|
||||||
|
SettingsActivity.getActivity()?.let {
|
||||||
|
it.startActivity(Intent(it, SyncHomeActivity::class.java))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@FormField(R.string.manage_polycentric_identity, FieldForm.BUTTON, R.string.manage_your_polycentric_identity, -7)
|
||||||
@FormFieldButton(R.drawable.ic_person)
|
@FormFieldButton(R.drawable.ic_person)
|
||||||
fun managePolycentricIdentity() {
|
fun managePolycentricIdentity() {
|
||||||
SettingsActivity.getActivity()?.let {
|
SettingsActivity.getActivity()?.let {
|
||||||
@@ -73,7 +83,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.show_faq, FieldForm.BUTTON, R.string.get_answers_to_common_questions, -5)
|
@FormField(R.string.show_faq, FieldForm.BUTTON, R.string.get_answers_to_common_questions, -6)
|
||||||
@FormFieldButton(R.drawable.ic_quiz)
|
@FormFieldButton(R.drawable.ic_quiz)
|
||||||
fun openFAQ() {
|
fun openFAQ() {
|
||||||
try {
|
try {
|
||||||
@@ -83,7 +93,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
//Ignored
|
//Ignored
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@FormField(R.string.show_issues, FieldForm.BUTTON, R.string.a_list_of_user_reported_and_self_reported_issues, -4)
|
@FormField(R.string.show_issues, FieldForm.BUTTON, R.string.a_list_of_user_reported_and_self_reported_issues, -5)
|
||||||
@FormFieldButton(R.drawable.ic_data_alert)
|
@FormFieldButton(R.drawable.ic_data_alert)
|
||||||
fun openIssues() {
|
fun openIssues() {
|
||||||
try {
|
try {
|
||||||
@@ -115,7 +125,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
}
|
}
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
@FormField(R.string.manage_tabs, FieldForm.BUTTON, R.string.change_tabs_visible_on_the_home_screen, -3)
|
@FormField(R.string.manage_tabs, FieldForm.BUTTON, R.string.change_tabs_visible_on_the_home_screen, -4)
|
||||||
@FormFieldButton(R.drawable.ic_tabs)
|
@FormFieldButton(R.drawable.ic_tabs)
|
||||||
fun manageTabs() {
|
fun manageTabs() {
|
||||||
try {
|
try {
|
||||||
@@ -129,16 +139,15 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
@FormField(R.string.import_data, FieldForm.BUTTON, R.string.import_data_description, -2)
|
@FormField(R.string.import_data, FieldForm.BUTTON, R.string.import_data_description, -3)
|
||||||
@FormFieldButton(R.drawable.ic_move_up)
|
@FormFieldButton(R.drawable.ic_move_up)
|
||||||
fun import() {
|
fun import() {
|
||||||
val act = SettingsActivity.getActivity() ?: return;
|
val act = SettingsActivity.getActivity() ?: return;
|
||||||
val intent = MainActivity.getImportOptionsIntent(act);
|
val intent = MainActivity.getImportOptionsIntent(act);
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK;
|
|
||||||
act.startActivity(intent);
|
act.startActivity(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.link_handling, FieldForm.BUTTON, R.string.allow_grayjay_to_handle_links, -1)
|
@FormField(R.string.link_handling, FieldForm.BUTTON, R.string.allow_grayjay_to_handle_links, -2)
|
||||||
@FormFieldButton(R.drawable.ic_link)
|
@FormFieldButton(R.drawable.ic_link)
|
||||||
fun manageLinks() {
|
fun manageLinks() {
|
||||||
try {
|
try {
|
||||||
@@ -148,6 +157,24 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*@FormField(R.string.disable_battery_optimization, FieldForm.BUTTON, R.string.click_to_go_to_battery_optimization_settings_disabling_battery_optimization_will_prevent_the_os_from_killing_media_sessions, -1)
|
||||||
|
@FormFieldButton(R.drawable.battery_full_24px)
|
||||||
|
fun ignoreBatteryOptimization() {
|
||||||
|
SettingsActivity.getActivity()?.let {
|
||||||
|
val intent = Intent()
|
||||||
|
val packageName = it.packageName
|
||||||
|
val pm = it.getSystemService(POWER_SERVICE) as PowerManager;
|
||||||
|
if (!pm.isIgnoringBatteryOptimizations(packageName)) {
|
||||||
|
intent.setAction(android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)
|
||||||
|
intent.setData(Uri.parse("package:$packageName"))
|
||||||
|
it.startActivity(intent)
|
||||||
|
UIDialogs.toast(it, "Please ignore battery optimizations for Grayjay")
|
||||||
|
} else {
|
||||||
|
UIDialogs.toast(it, "Battery optimizations already disabled for Grayjay")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
|
||||||
@FormField(R.string.language, "group", -1, 0)
|
@FormField(R.string.language, "group", -1, 0)
|
||||||
var language = LanguageSettings();
|
var language = LanguageSettings();
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -326,7 +353,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
var playback = PlaybackSettings();
|
var playback = PlaybackSettings();
|
||||||
@Serializable
|
@Serializable
|
||||||
class PlaybackSettings {
|
class PlaybackSettings {
|
||||||
@FormField(R.string.primary_language, FieldForm.DROPDOWN, -1, 0)
|
@FormField(R.string.primary_language, FieldForm.DROPDOWN, -1, -1)
|
||||||
@DropdownFieldOptionsId(R.array.audio_languages)
|
@DropdownFieldOptionsId(R.array.audio_languages)
|
||||||
var primaryLanguage: Int = 0;
|
var primaryLanguage: Int = 0;
|
||||||
|
|
||||||
@@ -353,7 +380,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
//= context.resources.getStringArray(R.array.audio_languages)[primaryLanguage];
|
//= context.resources.getStringArray(R.array.audio_languages)[primaryLanguage];
|
||||||
|
|
||||||
@FormField(R.string.default_playback_speed, FieldForm.DROPDOWN, -1, 1)
|
@FormField(R.string.default_playback_speed, FieldForm.DROPDOWN, -1, 0)
|
||||||
@DropdownFieldOptionsId(R.array.playback_speeds)
|
@DropdownFieldOptionsId(R.array.playback_speeds)
|
||||||
var defaultPlaybackSpeed: Int = 3;
|
var defaultPlaybackSpeed: Int = 3;
|
||||||
fun getDefaultPlaybackSpeed(): Float = when(defaultPlaybackSpeed) {
|
fun getDefaultPlaybackSpeed(): Float = when(defaultPlaybackSpeed) {
|
||||||
@@ -369,37 +396,29 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
else -> 1.0f;
|
else -> 1.0f;
|
||||||
};
|
};
|
||||||
|
|
||||||
@FormField(R.string.preferred_quality, FieldForm.DROPDOWN, R.string.preferred_quality_description, 2)
|
@FormField(R.string.preferred_quality, FieldForm.DROPDOWN, R.string.preferred_quality_description, 1)
|
||||||
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
||||||
var preferredQuality: Int = 0;
|
var preferredQuality: Int = 0;
|
||||||
|
|
||||||
@FormField(R.string.preferred_metered_quality, FieldForm.DROPDOWN, R.string.preferred_metered_quality_description, 3)
|
@FormField(R.string.preferred_metered_quality, FieldForm.DROPDOWN, R.string.preferred_metered_quality_description, 2)
|
||||||
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
@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, R.string.preferred_preview_quality_description, 4)
|
@FormField(R.string.preferred_preview_quality, FieldForm.DROPDOWN, R.string.preferred_preview_quality_description, 3)
|
||||||
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
@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, 5)
|
@FormField(R.string.simplify_sources, FieldForm.TOGGLE, R.string.simplify_sources_description, 4)
|
||||||
@DropdownFieldOptionsId(R.array.system_enabled_disabled_array)
|
var simplifySources: Boolean = true;
|
||||||
var autoRotate: Int = 2;
|
|
||||||
|
|
||||||
fun isAutoRotate() = autoRotate == 1 || (autoRotate == 2 && StateApp.instance.getCurrentSystemAutoRotate());
|
@FormField(R.string.force_allow_full_screen_rotation, FieldForm.TOGGLE, R.string.force_allow_full_screen_rotation_description, 5)
|
||||||
|
var forceAllowFullScreenRotation: Boolean = false
|
||||||
|
|
||||||
@FormField(R.string.auto_rotate_dead_zone, FieldForm.DROPDOWN, R.string.this_prevents_the_device_from_rotating_within_the_given_amount_of_degrees, 6)
|
@FormField(R.string.background_behavior, FieldForm.DROPDOWN, -1, 6)
|
||||||
@DropdownFieldOptionsId(R.array.auto_rotate_dead_zone)
|
|
||||||
var autoRotateDeadZone: Int = 0;
|
|
||||||
|
|
||||||
fun getAutoRotateDeadZoneDegrees(): Int {
|
|
||||||
return autoRotateDeadZone * 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
@FormField(R.string.background_behavior, FieldForm.DROPDOWN, -1, 7)
|
|
||||||
@DropdownFieldOptionsId(R.array.player_background_behavior)
|
@DropdownFieldOptionsId(R.array.player_background_behavior)
|
||||||
var backgroundPlay: Int = 2;
|
var backgroundPlay: Int = 2;
|
||||||
|
|
||||||
@@ -450,18 +469,44 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
@FormField(R.string.full_screen_portrait, FieldForm.TOGGLE, R.string.allow_full_screen_portrait, 13)
|
@FormField(R.string.full_screen_portrait, FieldForm.TOGGLE, R.string.allow_full_screen_portrait, 13)
|
||||||
var fullscreenPortrait: Boolean = false;
|
var fullscreenPortrait: Boolean = false;
|
||||||
|
|
||||||
|
@FormField(R.string.reverse_portrait, FieldForm.TOGGLE, R.string.reverse_portrait_description, 14)
|
||||||
|
var reversePortrait: Boolean = false;
|
||||||
|
|
||||||
|
@FormField(R.string.prefer_webm, FieldForm.TOGGLE, R.string.prefer_webm_description, 18)
|
||||||
|
var preferWebmVideo: Boolean = false;
|
||||||
|
@FormField(R.string.prefer_webm_audio, FieldForm.TOGGLE, R.string.prefer_webm_audio_description, 19)
|
||||||
|
var preferWebmAudio: Boolean = false;
|
||||||
|
|
||||||
|
@FormField(R.string.allow_under_cutout, FieldForm.TOGGLE, R.string.allow_under_cutout_description, 20)
|
||||||
|
var allowVideoToGoUnderCutout: Boolean = true;
|
||||||
|
|
||||||
|
@FormField(R.string.autoplay, FieldForm.TOGGLE, R.string.autoplay, 21)
|
||||||
|
var autoplay: Boolean = false;
|
||||||
|
|
||||||
|
@FormField(R.string.delete_watchlist_on_finish, FieldForm.TOGGLE, R.string.delete_watchlist_on_finish_description, 22)
|
||||||
|
var deleteFromWatchLaterAuto: Boolean = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.comments, "group", R.string.comments_description, 6)
|
@FormField(R.string.comments, "group", R.string.comments_description, 6)
|
||||||
var comments = CommentSettings();
|
var comments = CommentSettings();
|
||||||
@Serializable
|
@Serializable
|
||||||
class CommentSettings {
|
class CommentSettings {
|
||||||
|
var didAskPolycentricDefault: Boolean = false;
|
||||||
|
|
||||||
@FormField(R.string.default_comment_section, FieldForm.DROPDOWN, -1, 0)
|
@FormField(R.string.default_comment_section, FieldForm.DROPDOWN, -1, 0)
|
||||||
@DropdownFieldOptionsId(R.array.comment_sections)
|
@DropdownFieldOptionsId(R.array.comment_sections)
|
||||||
var defaultCommentSection: Int = 0;
|
var defaultCommentSection: Int = 2;
|
||||||
|
|
||||||
|
@FormField(R.string.default_recommendations, FieldForm.TOGGLE, R.string.default_recommendations_description, 0)
|
||||||
|
var recommendationsDefault: Boolean = false;
|
||||||
|
|
||||||
|
@FormField(R.string.hide_recommendations, FieldForm.TOGGLE, R.string.hide_recommendations_description, 0)
|
||||||
|
var hideRecommendations: Boolean = false;
|
||||||
|
|
||||||
@FormField(R.string.bad_reputation_comments_fading, FieldForm.TOGGLE, R.string.bad_reputation_comments_fading_description, 0)
|
@FormField(R.string.bad_reputation_comments_fading, FieldForm.TOGGLE, R.string.bad_reputation_comments_fading_description, 0)
|
||||||
var badReputationCommentsFading: Boolean = true;
|
var badReputationCommentsFading: Boolean = true;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.downloads, "group", R.string.configure_downloading_of_videos, 7)
|
@FormField(R.string.downloads, "group", R.string.configure_downloading_of_videos, 7)
|
||||||
@@ -510,7 +555,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
class Browsing {
|
class Browsing {
|
||||||
@FormField(R.string.enable_video_cache, FieldForm.TOGGLE, R.string.cache_to_quickly_load_previously_fetched_videos, 0)
|
@FormField(R.string.enable_video_cache, FieldForm.TOGGLE, R.string.cache_to_quickly_load_previously_fetched_videos, 0)
|
||||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||||
var videoCache: Boolean = true;
|
var videoCache: Boolean = false; //Temporary default disabled to prevent ui freeze?
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.casting, "group", R.string.configure_casting, 9)
|
@FormField(R.string.casting, "group", R.string.configure_casting, 9)
|
||||||
@@ -779,10 +824,10 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
fun export() {
|
fun export() {
|
||||||
val activity = SettingsActivity.getActivity() ?: return;
|
val activity = SettingsActivity.getActivity() ?: return;
|
||||||
UISlideOverlays.showOverlay(activity.overlay, "Select export type", null, {},
|
UISlideOverlays.showOverlay(activity.overlay, "Select export type", null, {},
|
||||||
SlideUpMenuItem(activity, R.drawable.ic_share, "Share", "", null, {
|
SlideUpMenuItem(activity, R.drawable.ic_share, "Share", "", tag = null, call = {
|
||||||
StateBackup.shareExternalBackup();
|
StateBackup.shareExternalBackup();
|
||||||
}),
|
}),
|
||||||
SlideUpMenuItem(activity, R.drawable.ic_download, "File", "", null, {
|
SlideUpMenuItem(activity, R.drawable.ic_download, "File", "", tag = null, call = {
|
||||||
StateBackup.saveExternalBackup(activity);
|
StateBackup.saveExternalBackup(activity);
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@@ -798,10 +843,14 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
@FormField(R.string.clear_payment, FieldForm.BUTTON, R.string.deletes_license_keys_from_app, 2)
|
@FormField(R.string.clear_payment, FieldForm.BUTTON, R.string.deletes_license_keys_from_app, 2)
|
||||||
fun clearPayment() {
|
fun clearPayment() {
|
||||||
StatePayment.instance.clearLicenses();
|
SettingsActivity.getActivity()?.let { context ->
|
||||||
SettingsActivity.getActivity()?.let {
|
UIDialogs.showConfirmationDialog(context, "Are you sure you want to delete your license?", {
|
||||||
UIDialogs.toast(it, it.getString(R.string.licenses_cleared_might_require_app_restart));
|
StatePayment.instance.clearLicenses();
|
||||||
it.reloadSettings();
|
SettingsActivity.getActivity()?.let {
|
||||||
|
UIDialogs.toast(it, it.getString(R.string.licenses_cleared_might_require_app_restart));
|
||||||
|
it.reloadSettings();
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -810,12 +859,14 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
var other = Other();
|
var other = Other();
|
||||||
@Serializable
|
@Serializable
|
||||||
class Other {
|
class Other {
|
||||||
@FormField(R.string.bypass_rotation_prevention, FieldForm.TOGGLE, R.string.bypass_rotation_prevention_description, 1)
|
@FormField(R.string.playlist_delete_confirmation, FieldForm.TOGGLE, R.string.playlist_delete_confirmation_description, 2)
|
||||||
@FormFieldWarning(R.string.bypass_rotation_prevention_warning)
|
var playlistDeleteConfirmation: Boolean = true;
|
||||||
var bypassRotationPrevention: Boolean = false;
|
|
||||||
|
|
||||||
@FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 1)
|
@FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 3)
|
||||||
var polycentricEnabled: Boolean = true;
|
var polycentricEnabled: Boolean = true;
|
||||||
|
|
||||||
|
@FormField(R.string.polycentric_local_cache, FieldForm.TOGGLE, R.string.polycentric_local_cache_description, 4)
|
||||||
|
var polycentricLocalCache: Boolean = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.gesture_controls, FieldForm.GROUP, -1, 19)
|
@FormField(R.string.gesture_controls, FieldForm.GROUP, -1, 19)
|
||||||
@@ -847,7 +898,24 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
var pan: Boolean = true;
|
var pan: Boolean = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.info, FieldForm.GROUP, -1, 20)
|
@FormField(R.string.synchronization, FieldForm.GROUP, -1, 20)
|
||||||
|
var synchronization = Synchronization();
|
||||||
|
@Serializable
|
||||||
|
class Synchronization {
|
||||||
|
@FormField(R.string.enabled, FieldForm.TOGGLE, R.string.enabled_description, 1)
|
||||||
|
var enabled: Boolean = true;
|
||||||
|
|
||||||
|
@FormField(R.string.broadcast, FieldForm.TOGGLE, R.string.broadcast_description, 1)
|
||||||
|
var broadcast: Boolean = false;
|
||||||
|
|
||||||
|
@FormField(R.string.connect_discovered, FieldForm.TOGGLE, R.string.connect_discovered_description, 2)
|
||||||
|
var connectDiscovered: Boolean = true;
|
||||||
|
|
||||||
|
@FormField(R.string.connect_last, FieldForm.TOGGLE, R.string.connect_last_description, 3)
|
||||||
|
var connectLast: Boolean = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@FormField(R.string.info, FieldForm.GROUP, -1, 21)
|
||||||
var info = Info();
|
var info = Info();
|
||||||
@Serializable
|
@Serializable
|
||||||
class Info {
|
class Info {
|
||||||
|
|||||||
@@ -235,13 +235,17 @@ class SettingsDev : FragmentedStorageFileJson() {
|
|||||||
R.string.test_background_worker_description, 4)
|
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");
|
try {
|
||||||
|
UIDialogs.toast(SettingsActivity.getActivity()!!, "Starting test background worker");
|
||||||
|
|
||||||
val wm = WorkManager.getInstance(act);
|
val wm = WorkManager.getInstance(act);
|
||||||
val req = OneTimeWorkRequestBuilder<BackgroundWorker>()
|
val req = OneTimeWorkRequestBuilder<BackgroundWorker>()
|
||||||
.setInputData(Data.Builder().putBoolean("bypassMainCheck", true).build())
|
.setInputData(Data.Builder().putBoolean("bypassMainCheck", true).build())
|
||||||
.build();
|
.build();
|
||||||
wm.enqueue(req);
|
wm.enqueue(req);
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
UIDialogs.showGeneralErrorDialog(act, "Failed to trigger background update", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON,
|
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON,
|
||||||
R.string.test_background_worker_description, 4)
|
R.string.test_background_worker_description, 4)
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.text.Layout
|
||||||
|
import android.text.method.ScrollingMovementMethod
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
@@ -197,7 +199,6 @@ class UIDialogs {
|
|||||||
dialog.show();
|
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);
|
||||||
@@ -213,28 +214,31 @@ class UIDialogs {
|
|||||||
this.text = text;
|
this.text = text;
|
||||||
};
|
};
|
||||||
view.findViewById<TextView>(R.id.dialog_text_details).apply {
|
view.findViewById<TextView>(R.id.dialog_text_details).apply {
|
||||||
if(textDetails == null)
|
if (textDetails == null)
|
||||||
this.visibility = View.GONE;
|
|
||||||
else
|
|
||||||
this.text = textDetails;
|
|
||||||
};
|
|
||||||
view.findViewById<TextView>(R.id.dialog_text_code).apply {
|
|
||||||
if(code == null)
|
|
||||||
this.visibility = View.GONE;
|
this.visibility = View.GONE;
|
||||||
|
else {
|
||||||
|
this.text = textDetails;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
view.findViewById<TextView>(R.id.dialog_text_code).apply {
|
||||||
|
if (code == null) this.visibility = View.GONE;
|
||||||
else {
|
else {
|
||||||
this.text = code;
|
this.text = code;
|
||||||
|
this.movementMethod = ScrollingMovementMethod.getInstance();
|
||||||
this.visibility = View.VISIBLE;
|
this.visibility = View.VISIBLE;
|
||||||
|
this.textAlignment = View.TEXT_ALIGNMENT_VIEW_START
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
view.findViewById<LinearLayout>(R.id.dialog_buttons).apply {
|
view.findViewById<LinearLayout>(R.id.dialog_buttons).apply {
|
||||||
|
val center = actions.any { it?.center == true };
|
||||||
val buttons = actions.map<Action, TextView> { act ->
|
val buttons = actions.map<Action, TextView> { act ->
|
||||||
val buttonView = TextView(context);
|
val buttonView = TextView(context);
|
||||||
val dp10 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10f, resources.displayMetrics).toInt();
|
val dp10 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10f, resources.displayMetrics).toInt();
|
||||||
val dp28 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 28f, resources.displayMetrics).toInt();
|
val dp28 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 28f, resources.displayMetrics).toInt();
|
||||||
val dp14 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 14.0f, resources.displayMetrics).toInt();
|
val dp14 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 14.0f, resources.displayMetrics).toInt();
|
||||||
buttonView.layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
|
buttonView.layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
|
||||||
if(actions.size > 1)
|
this.marginStart = if(actions.size >= 2) dp14 / 2 else dp28 / 2;
|
||||||
this.marginEnd = if(actions.size > 2) dp14 else dp28;
|
this.marginEnd = if(actions.size >= 2) dp14 / 2 else dp28 / 2;
|
||||||
};
|
};
|
||||||
buttonView.setTextColor(Color.WHITE);
|
buttonView.setTextColor(Color.WHITE);
|
||||||
buttonView.textSize = 14f;
|
buttonView.textSize = 14f;
|
||||||
@@ -256,7 +260,7 @@ class UIDialogs {
|
|||||||
|
|
||||||
return@map buttonView;
|
return@map buttonView;
|
||||||
};
|
};
|
||||||
if(actions.size <= 1)
|
if(actions.size <= 1 || center)
|
||||||
this.gravity = Gravity.CENTER;
|
this.gravity = Gravity.CENTER;
|
||||||
else
|
else
|
||||||
this.gravity = Gravity.END;
|
this.gravity = Gravity.END;
|
||||||
@@ -345,6 +349,13 @@ 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 showConfirmationDialog(context: Context, text: String, action: () -> Unit, cancelAction: (() -> Unit)? = null, doNotAskAgainAction: (() -> Unit)? = null) {
|
||||||
|
val confirmButtonAction = Action(context.getString(R.string.confirm), action, ActionStyle.PRIMARY)
|
||||||
|
val cancelButtonAction = Action(context.getString(R.string.cancel), cancelAction ?: {}, ActionStyle.ACCENT)
|
||||||
|
val doNotAskAgain = Action(context.getString(R.string.do_not_ask_again), doNotAskAgainAction ?: {}, ActionStyle.NONE)
|
||||||
|
showDialog(context, R.drawable.ic_error, text, null, null, 0, doNotAskAgain, cancelButtonAction, confirmButtonAction)
|
||||||
|
}
|
||||||
|
|
||||||
fun showUpdateAvailableDialog(context: Context, lastVersion: Int, hideExceptionButtons: Boolean = false) {
|
fun showUpdateAvailableDialog(context: Context, lastVersion: Int, hideExceptionButtons: Boolean = false) {
|
||||||
val dialog = AutoUpdateDialog(context);
|
val dialog = AutoUpdateDialog(context);
|
||||||
registerDialogOpened(dialog);
|
registerDialogOpened(dialog);
|
||||||
@@ -507,11 +518,13 @@ class UIDialogs {
|
|||||||
val text: String;
|
val text: String;
|
||||||
val action: ()->Unit;
|
val action: ()->Unit;
|
||||||
val style: ActionStyle;
|
val style: ActionStyle;
|
||||||
|
var center: Boolean;
|
||||||
|
|
||||||
constructor(text: String, action: ()->Unit, style: ActionStyle = ActionStyle.NONE) {
|
constructor(text: String, action: ()->Unit, style: ActionStyle = ActionStyle.NONE, center: Boolean = false) {
|
||||||
this.text = text;
|
this.text = text;
|
||||||
this.action = action;
|
this.action = action;
|
||||||
this.style = style;
|
this.style = style;
|
||||||
|
this.center = center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
enum class ActionStyle {
|
enum class ActionStyle {
|
||||||
|
|||||||
@@ -15,14 +15,19 @@ 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.HLSVariantAudioUrlSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantVideoUrlSource
|
import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantVideoUrlSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||||
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.IHLSManifestAudioSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
|
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.streams.sources.IVideoUrlSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
||||||
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.api.media.platforms.js.JSClient
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawAudioSource
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawSource
|
||||||
import com.futo.platformplayer.downloads.VideoLocal
|
import com.futo.platformplayer.downloads.VideoLocal
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionGroupFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionGroupFragment
|
||||||
import com.futo.platformplayer.helpers.VideoHelper
|
import com.futo.platformplayer.helpers.VideoHelper
|
||||||
@@ -34,12 +39,12 @@ import com.futo.platformplayer.models.SubscriptionGroup
|
|||||||
import com.futo.platformplayer.parsers.HLS
|
import com.futo.platformplayer.parsers.HLS
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StateDownloads
|
import com.futo.platformplayer.states.StateDownloads
|
||||||
|
import com.futo.platformplayer.states.StateHistory
|
||||||
import com.futo.platformplayer.states.StateMeta
|
import com.futo.platformplayer.states.StateMeta
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.platformplayer.states.StatePlayer
|
import com.futo.platformplayer.states.StatePlayer
|
||||||
import com.futo.platformplayer.states.StatePlaylists
|
import com.futo.platformplayer.states.StatePlaylists
|
||||||
import com.futo.platformplayer.states.StateSubscriptionGroups
|
import com.futo.platformplayer.states.StateSubscriptionGroups
|
||||||
import com.futo.platformplayer.states.StateSubscriptions
|
|
||||||
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.LoaderView
|
import com.futo.platformplayer.views.LoaderView
|
||||||
@@ -91,9 +96,17 @@ class UISlideOverlays {
|
|||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
items.addAll(listOf(
|
items.addAll(listOf(
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_notifications, "Notifications", "", "notifications", {
|
SlideUpMenuItem(
|
||||||
subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications;
|
container.context,
|
||||||
}, false),
|
R.drawable.ic_notifications,
|
||||||
|
"Notifications",
|
||||||
|
"",
|
||||||
|
tag = "notifications",
|
||||||
|
call = {
|
||||||
|
subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications;
|
||||||
|
},
|
||||||
|
invokeParent = false
|
||||||
|
),
|
||||||
if(StateSubscriptionGroups.instance.getSubscriptionGroups().isNotEmpty())
|
if(StateSubscriptionGroups.instance.getSubscriptionGroups().isNotEmpty())
|
||||||
SlideUpMenuGroup(container.context, "Subscription Groups",
|
SlideUpMenuGroup(container.context, "Subscription Groups",
|
||||||
"You can select which groups this subscription is part of.",
|
"You can select which groups this subscription is part of.",
|
||||||
@@ -128,22 +141,62 @@ class UISlideOverlays {
|
|||||||
SlideUpMenuGroup(container.context, "Fetch Settings",
|
SlideUpMenuGroup(container.context, "Fetch Settings",
|
||||||
"Depending on the platform you might not need to enable a type for it to be available.",
|
"Depending on the platform you might not need to enable a type for it to be available.",
|
||||||
-1, listOf()),
|
-1, listOf()),
|
||||||
if(capabilities.hasType(ResultCapabilities.TYPE_LIVE)) SlideUpMenuItem(container.context, R.drawable.ic_live_tv, "Livestreams", "Check for livestreams", "fetchLive", {
|
if(capabilities.hasType(ResultCapabilities.TYPE_LIVE)) SlideUpMenuItem(
|
||||||
subscription.doFetchLive = menu?.selectOption(null, "fetchLive", true, true) ?: subscription.doFetchLive;
|
container.context,
|
||||||
}, false) else null,
|
R.drawable.ic_live_tv,
|
||||||
if(capabilities.hasType(ResultCapabilities.TYPE_STREAMS)) SlideUpMenuItem(container.context, R.drawable.ic_play, "Streams", "Check for streams", "fetchStreams", {
|
"Livestreams",
|
||||||
subscription.doFetchStreams = menu?.selectOption(null, "fetchStreams", true, true) ?: subscription.doFetchStreams;
|
"Check for livestreams",
|
||||||
}, false) else null,
|
tag = "fetchLive",
|
||||||
|
call = {
|
||||||
|
subscription.doFetchLive = menu?.selectOption(null, "fetchLive", true, true) ?: subscription.doFetchLive;
|
||||||
|
},
|
||||||
|
invokeParent = false
|
||||||
|
) else null,
|
||||||
|
if(capabilities.hasType(ResultCapabilities.TYPE_STREAMS)) SlideUpMenuItem(
|
||||||
|
container.context,
|
||||||
|
R.drawable.ic_play,
|
||||||
|
"Streams",
|
||||||
|
"Check for streams",
|
||||||
|
tag = "fetchStreams",
|
||||||
|
call = {
|
||||||
|
subscription.doFetchStreams = menu?.selectOption(null, "fetchStreams", true, true) ?: subscription.doFetchStreams;
|
||||||
|
},
|
||||||
|
invokeParent = false
|
||||||
|
) else null,
|
||||||
if(capabilities.hasType(ResultCapabilities.TYPE_VIDEOS))
|
if(capabilities.hasType(ResultCapabilities.TYPE_VIDEOS))
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_play, "Videos", "Check for videos", "fetchVideos", {
|
SlideUpMenuItem(
|
||||||
subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos;
|
container.context,
|
||||||
}, false) else if(capabilities.hasType(ResultCapabilities.TYPE_MIXED) || capabilities.types.isEmpty())
|
R.drawable.ic_play,
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_play, "Content", "Check for content", "fetchVideos", {
|
"Videos",
|
||||||
subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos;
|
"Check for videos",
|
||||||
}, false) else null,
|
tag = "fetchVideos",
|
||||||
if(capabilities.hasType(ResultCapabilities.TYPE_POSTS)) SlideUpMenuItem(container.context, R.drawable.ic_chat, "Posts", "Check for posts", "fetchPosts", {
|
call = {
|
||||||
subscription.doFetchPosts = menu?.selectOption(null, "fetchPosts", true, true) ?: subscription.doFetchPosts;
|
subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos;
|
||||||
}, false) else null/*,,
|
},
|
||||||
|
invokeParent = false
|
||||||
|
) else if(capabilities.hasType(ResultCapabilities.TYPE_MIXED) || capabilities.types.isEmpty())
|
||||||
|
SlideUpMenuItem(
|
||||||
|
container.context,
|
||||||
|
R.drawable.ic_play,
|
||||||
|
"Content",
|
||||||
|
"Check for content",
|
||||||
|
tag = "fetchVideos",
|
||||||
|
call = {
|
||||||
|
subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos;
|
||||||
|
},
|
||||||
|
invokeParent = false
|
||||||
|
) else null,
|
||||||
|
if(capabilities.hasType(ResultCapabilities.TYPE_POSTS)) SlideUpMenuItem(
|
||||||
|
container.context,
|
||||||
|
R.drawable.ic_chat,
|
||||||
|
"Posts",
|
||||||
|
"Check for posts",
|
||||||
|
tag = "fetchPosts",
|
||||||
|
call = {
|
||||||
|
subscription.doFetchPosts = menu?.selectOption(null, "fetchPosts", true, true) ?: subscription.doFetchPosts;
|
||||||
|
},
|
||||||
|
invokeParent = false
|
||||||
|
) else null/*,,
|
||||||
|
|
||||||
SlideUpMenuGroup(container.context, "Actions",
|
SlideUpMenuGroup(container.context, "Actions",
|
||||||
"Various things you can do with this subscription",
|
"Various things you can do with this subscription",
|
||||||
@@ -242,11 +295,23 @@ class UISlideOverlays {
|
|||||||
masterPlaylist = HLS.parseMasterPlaylist(masterPlaylistContent, sourceUrl)
|
masterPlaylist = HLS.parseMasterPlaylist(masterPlaylistContent, sourceUrl)
|
||||||
|
|
||||||
masterPlaylist.getAudioSources().forEach { it ->
|
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
|
val estSize = VideoHelper.estimateSourceSize(it);
|
||||||
slideUpMenuOverlay.selectOption(audioButtons, it)
|
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
||||||
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
audioButtons.add(SlideUpMenuItem(
|
||||||
}, false))
|
container.context,
|
||||||
|
R.drawable.ic_music,
|
||||||
|
it.name,
|
||||||
|
listOf(it.language, it.codec).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "),
|
||||||
|
(prefix + it.codec).trim(),
|
||||||
|
tag = it,
|
||||||
|
call = {
|
||||||
|
selectedAudioVariant = it
|
||||||
|
slideUpMenuOverlay.selectOption(audioButtons, it)
|
||||||
|
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
||||||
|
},
|
||||||
|
invokeParent = false
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/*masterPlaylist.getSubtitleSources().forEach { it ->
|
/*masterPlaylist.getSubtitleSources().forEach { it ->
|
||||||
@@ -258,11 +323,22 @@ class UISlideOverlays {
|
|||||||
}*/
|
}*/
|
||||||
|
|
||||||
masterPlaylist.getVideoSources().forEach {
|
masterPlaylist.getVideoSources().forEach {
|
||||||
videoButtons.add(SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "${it.width}x${it.height}", it, {
|
val estSize = VideoHelper.estimateSourceSize(it);
|
||||||
selectedVideoVariant = it
|
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
||||||
slideUpMenuOverlay.selectOption(videoButtons, it)
|
videoButtons.add(SlideUpMenuItem(
|
||||||
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
container.context,
|
||||||
}, false))
|
R.drawable.ic_movie,
|
||||||
|
it.name,
|
||||||
|
"${it.width}x${it.height}",
|
||||||
|
(prefix + it.codec).trim(),
|
||||||
|
tag = it,
|
||||||
|
call = {
|
||||||
|
selectedVideoVariant = it
|
||||||
|
slideUpMenuOverlay.selectOption(videoButtons, it)
|
||||||
|
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
||||||
|
},
|
||||||
|
invokeParent = false
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
val newItems = arrayListOf<View>()
|
val newItems = arrayListOf<View>()
|
||||||
@@ -321,8 +397,8 @@ class UISlideOverlays {
|
|||||||
|
|
||||||
|
|
||||||
val requiresAudio = descriptor is VideoUnMuxedSourceDescriptor;
|
val requiresAudio = descriptor is VideoUnMuxedSourceDescriptor;
|
||||||
var selectedVideo: IVideoUrlSource? = null;
|
var selectedVideo: IVideoSource? = null;
|
||||||
var selectedAudio: IAudioUrlSource? = null;
|
var selectedAudio: IAudioSource? = null;
|
||||||
var selectedSubtitle: ISubtitleSource? = null;
|
var selectedSubtitle: ISubtitleSource? = null;
|
||||||
|
|
||||||
val videoSources = descriptor.videoSources;
|
val videoSources = descriptor.videoSources;
|
||||||
@@ -341,45 +417,93 @@ class UISlideOverlays {
|
|||||||
}
|
}
|
||||||
|
|
||||||
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.video), videoSources,
|
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.video), videoSources,
|
||||||
listOf(listOf(SlideUpMenuItem(container.context, R.drawable.ic_movie, container.context.getString(R.string.none), container.context.getString(R.string.audio_only), "none", {
|
listOf(listOf(SlideUpMenuItem(
|
||||||
selectedVideo = null;
|
container.context,
|
||||||
menu?.selectOption(videoSources, "none");
|
R.drawable.ic_movie,
|
||||||
if(selectedAudio != null || !requiresAudio)
|
container.context.getString(R.string.none),
|
||||||
menu?.setOk(container.context.getString(R.string.download));
|
container.context.getString(R.string.audio_only),
|
||||||
}, false)) +
|
tag = "none",
|
||||||
|
call = {
|
||||||
|
selectedVideo = null;
|
||||||
|
menu?.selectOption(videoSources, "none");
|
||||||
|
if(selectedAudio != null || !requiresAudio)
|
||||||
|
menu?.setOk(container.context.getString(R.string.download));
|
||||||
|
},
|
||||||
|
invokeParent = false
|
||||||
|
)) +
|
||||||
videoSources
|
videoSources
|
||||||
.filter { it.isDownloadable() }
|
.filter { it.isDownloadable() }
|
||||||
.map {
|
.map {
|
||||||
when (it) {
|
when (it) {
|
||||||
is IVideoUrlSource -> {
|
is IVideoUrlSource -> {
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "${it.width}x${it.height}", it, {
|
val estSize = VideoHelper.estimateSourceSize(it);
|
||||||
selectedVideo = it
|
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
||||||
menu?.selectOption(videoSources, it);
|
SlideUpMenuItem(
|
||||||
if(selectedAudio != null || !requiresAudio)
|
container.context,
|
||||||
menu?.setOk(container.context.getString(R.string.download));
|
R.drawable.ic_movie,
|
||||||
}, false)
|
it.name,
|
||||||
|
"${it.width}x${it.height}",
|
||||||
|
(prefix + it.codec).trim(),
|
||||||
|
tag = it,
|
||||||
|
call = {
|
||||||
|
selectedVideo = it
|
||||||
|
menu?.selectOption(videoSources, it);
|
||||||
|
if(selectedAudio != null || !requiresAudio)
|
||||||
|
menu?.setOk(container.context.getString(R.string.download));
|
||||||
|
},
|
||||||
|
invokeParent = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is JSDashManifestRawSource -> {
|
||||||
|
val estSize = VideoHelper.estimateSourceSize(it);
|
||||||
|
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
||||||
|
SlideUpMenuItem(
|
||||||
|
container.context,
|
||||||
|
R.drawable.ic_movie,
|
||||||
|
it.name,
|
||||||
|
"${it.width}x${it.height}",
|
||||||
|
(prefix + it.codec).trim(),
|
||||||
|
tag = it,
|
||||||
|
call = {
|
||||||
|
selectedVideo = it
|
||||||
|
menu?.selectOption(videoSources, it);
|
||||||
|
if(selectedAudio != null || !requiresAudio)
|
||||||
|
menu?.setOk(container.context.getString(R.string.download));
|
||||||
|
},
|
||||||
|
invokeParent = false
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is IHLSManifestSource -> {
|
is IHLSManifestSource -> {
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "HLS", it, {
|
SlideUpMenuItem(
|
||||||
showHlsPicker(video, it, it.url, container)
|
container.context,
|
||||||
}, false)
|
R.drawable.ic_movie,
|
||||||
|
it.name,
|
||||||
|
"HLS",
|
||||||
|
tag = it,
|
||||||
|
call = {
|
||||||
|
showHlsPicker(video, it, it.url, container)
|
||||||
|
},
|
||||||
|
invokeParent = false
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
throw Exception("Unhandled source type")
|
Logger.w(TAG, "Unhandled source type for UISlideOverlay download items");
|
||||||
|
null;//throw Exception("Unhandled source type")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}).flatten().toList()
|
}.filterNotNull()).flatten().toList()
|
||||||
));
|
));
|
||||||
|
|
||||||
if(Settings.instance.downloads.getDefaultVideoQualityPixels() > 0 && videoSources.isNotEmpty()) {
|
if(Settings.instance.downloads.getDefaultVideoQualityPixels() > 0 && videoSources.isNotEmpty()) {
|
||||||
//TODO: Add HLS support here
|
//TODO: Add HLS support here
|
||||||
selectedVideo = VideoHelper.selectBestVideoSource(
|
selectedVideo = VideoHelper.selectBestVideoSource(
|
||||||
videoSources.filter { it is IVideoUrlSource && it.isDownloadable() }.asIterable(),
|
videoSources.filter { it is IVideoSource && it.isDownloadable() }.asIterable(),
|
||||||
Settings.instance.downloads.getDefaultVideoQualityPixels(),
|
Settings.instance.downloads.getDefaultVideoQualityPixels(),
|
||||||
FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS
|
FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS
|
||||||
) as IVideoUrlSource?;
|
) as IVideoSource?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (audioSources != null) {
|
if (audioSources != null) {
|
||||||
@@ -388,43 +512,90 @@ class UISlideOverlays {
|
|||||||
.map {
|
.map {
|
||||||
when (it) {
|
when (it) {
|
||||||
is IAudioUrlSource -> {
|
is IAudioUrlSource -> {
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, "${it.bitrate}", it, {
|
val estSize = VideoHelper.estimateSourceSize(it);
|
||||||
selectedAudio = it
|
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
||||||
menu?.selectOption(audioSources, it);
|
SlideUpMenuItem(
|
||||||
menu?.setOk(container.context.getString(R.string.download));
|
container.context,
|
||||||
}, false);
|
R.drawable.ic_music,
|
||||||
|
it.name,
|
||||||
|
"${it.bitrate}",
|
||||||
|
(prefix + it.codec).trim(),
|
||||||
|
tag = it,
|
||||||
|
call = {
|
||||||
|
selectedAudio = it
|
||||||
|
menu?.selectOption(audioSources, it);
|
||||||
|
menu?.setOk(container.context.getString(R.string.download));
|
||||||
|
},
|
||||||
|
invokeParent = false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
is JSDashManifestRawAudioSource -> {
|
||||||
|
val estSize = VideoHelper.estimateSourceSize(it);
|
||||||
|
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
||||||
|
SlideUpMenuItem(
|
||||||
|
container.context,
|
||||||
|
R.drawable.ic_music,
|
||||||
|
it.name,
|
||||||
|
"${it.bitrate}",
|
||||||
|
(prefix + it.codec).trim(),
|
||||||
|
tag = it,
|
||||||
|
call = {
|
||||||
|
selectedAudio = it
|
||||||
|
menu?.selectOption(audioSources, it);
|
||||||
|
menu?.setOk(container.context.getString(R.string.download));
|
||||||
|
},
|
||||||
|
invokeParent = false
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
is IHLSManifestAudioSource -> {
|
is IHLSManifestAudioSource -> {
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "HLS Audio", it, {
|
SlideUpMenuItem(
|
||||||
showHlsPicker(video, it, it.url, container)
|
container.context,
|
||||||
}, false)
|
R.drawable.ic_movie,
|
||||||
|
it.name,
|
||||||
|
"HLS Audio",
|
||||||
|
tag = it,
|
||||||
|
call = {
|
||||||
|
showHlsPicker(video, it, it.url, container)
|
||||||
|
},
|
||||||
|
invokeParent = false
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
throw Exception("Unhandled source type")
|
Logger.w(TAG, "Unhandled source type for UISlideOverlay download items");
|
||||||
|
null;//throw Exception("Unhandled source type")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}));
|
}.filterNotNull()));
|
||||||
|
|
||||||
//TODO: Add HLS support here
|
//TODO: Add HLS support here
|
||||||
selectedAudio = VideoHelper.selectBestAudioSource(audioSources.filter { it is IAudioUrlSource && it.isDownloadable() }.asIterable(),
|
selectedAudio = VideoHelper.selectBestAudioSource(audioSources.filter { it is IAudioSource && 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 IAudioSource?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(contentResolver != null && subtitleSources.isNotEmpty()) {
|
if(contentResolver != null && subtitleSources.isNotEmpty()) {
|
||||||
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(
|
||||||
if (selectedSubtitle == it) {
|
container.context,
|
||||||
selectedSubtitle = null;
|
R.drawable.ic_edit,
|
||||||
menu?.selectOption(subtitleSources, null);
|
it.name,
|
||||||
} else {
|
"",
|
||||||
selectedSubtitle = it;
|
tag = it,
|
||||||
menu?.selectOption(subtitleSources, it);
|
call = {
|
||||||
}
|
if (selectedSubtitle == it) {
|
||||||
}, false);
|
selectedSubtitle = null;
|
||||||
|
menu?.selectOption(subtitleSources, null);
|
||||||
|
} else {
|
||||||
|
selectedSubtitle = it;
|
||||||
|
menu?.selectOption(subtitleSources, it);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
invokeParent = false
|
||||||
|
);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -442,6 +613,18 @@ class UISlideOverlays {
|
|||||||
}
|
}
|
||||||
|
|
||||||
menu.onOK.subscribe {
|
menu.onOK.subscribe {
|
||||||
|
val sv = selectedVideo
|
||||||
|
if (sv is IHLSManifestSource) {
|
||||||
|
showHlsPicker(video, sv, sv.url, container)
|
||||||
|
return@subscribe
|
||||||
|
}
|
||||||
|
|
||||||
|
val sa = selectedAudio
|
||||||
|
if (sa is IHLSManifestAudioSource) {
|
||||||
|
showHlsPicker(video, sa, sa.url, container)
|
||||||
|
return@subscribe
|
||||||
|
}
|
||||||
|
|
||||||
menu.hide();
|
menu.hide();
|
||||||
val subtitleToDownload = selectedSubtitle;
|
val subtitleToDownload = selectedSubtitle;
|
||||||
if(selectedAudio != null || !requiresAudio) {
|
if(selectedAudio != null || !requiresAudio) {
|
||||||
@@ -498,8 +681,9 @@ class UISlideOverlays {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch(ex: Throwable) {
|
catch(ex: Throwable) {
|
||||||
|
Logger.e(TAG, "Fetching details for download failed due to: " + ex.message, ex);
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
UIDialogs.toast(container.context.getString(R.string.failed_to_fetch_details_for_download));
|
UIDialogs.toast(container.context.getString(R.string.failed_to_fetch_details_for_download) + "\n" + ex.message);
|
||||||
handleUnknownDownload();
|
handleUnknownDownload();
|
||||||
loader.hide(true);
|
loader.hide(true);
|
||||||
}
|
}
|
||||||
@@ -536,23 +720,47 @@ class UISlideOverlays {
|
|||||||
);
|
);
|
||||||
|
|
||||||
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.target_resolution), "Video", resolutions.map {
|
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.target_resolution), "Video", resolutions.map {
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_movie, it.first, it.second, it.third, {
|
SlideUpMenuItem(
|
||||||
targetPxSize = it.third;
|
container.context,
|
||||||
menu?.selectOption("Video", it.third);
|
R.drawable.ic_movie,
|
||||||
}, false)
|
it.first,
|
||||||
|
it.second,
|
||||||
|
tag = it.third,
|
||||||
|
call = {
|
||||||
|
targetPxSize = it.third;
|
||||||
|
menu?.selectOption("Video", it.third);
|
||||||
|
},
|
||||||
|
invokeParent = false
|
||||||
|
)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.target_bitrate), "Bitrate", listOf(
|
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.target_bitrate), "Bitrate", listOf(
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_movie, container.context.getString(R.string.low_bitrate), "", 1, {
|
SlideUpMenuItem(
|
||||||
targetBitrate = 1;
|
container.context,
|
||||||
menu?.selectOption("Bitrate", 1);
|
R.drawable.ic_movie,
|
||||||
menu?.setOk(container.context.getString(R.string.download));
|
container.context.getString(R.string.low_bitrate),
|
||||||
}, false),
|
"",
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_movie, container.context.getString(R.string.high_bitrate), "", 9999999, {
|
tag = 1,
|
||||||
targetBitrate = 9999999;
|
call = {
|
||||||
menu?.selectOption("Bitrate", 9999999);
|
targetBitrate = 1;
|
||||||
menu?.setOk(container.context.getString(R.string.download));
|
menu?.selectOption("Bitrate", 1);
|
||||||
}, false)
|
menu?.setOk(container.context.getString(R.string.download));
|
||||||
|
},
|
||||||
|
invokeParent = false
|
||||||
|
),
|
||||||
|
SlideUpMenuItem(
|
||||||
|
container.context,
|
||||||
|
R.drawable.ic_movie,
|
||||||
|
container.context.getString(R.string.high_bitrate),
|
||||||
|
"",
|
||||||
|
tag = 9999999,
|
||||||
|
call = {
|
||||||
|
targetBitrate = 9999999;
|
||||||
|
menu?.selectOption("Bitrate", 9999999);
|
||||||
|
menu?.setOk(container.context.getString(R.string.download));
|
||||||
|
},
|
||||||
|
invokeParent = false
|
||||||
|
)
|
||||||
)));
|
)));
|
||||||
|
|
||||||
|
|
||||||
@@ -672,11 +880,21 @@ class UISlideOverlays {
|
|||||||
val items = arrayListOf<View>();
|
val items = arrayListOf<View>();
|
||||||
val lastUpdated = StatePlaylists.instance.getLastUpdatedPlaylist();
|
val lastUpdated = StatePlaylists.instance.getLastUpdatedPlaylist();
|
||||||
|
|
||||||
|
val isLimited = video?.url != null && StatePlatform.instance.getContentClientOrNull(video!!.url)?.let {
|
||||||
|
if (it is JSClient)
|
||||||
|
return@let it.config.reduceFunctionsInLimitedVersion && BuildConfig.IS_PLAYSTORE_BUILD
|
||||||
|
else false;
|
||||||
|
} ?: false;
|
||||||
|
|
||||||
if (lastUpdated != null) {
|
if (lastUpdated != null) {
|
||||||
items.add(
|
items.add(
|
||||||
SlideUpMenuGroup(container.context, container.context.getString(R.string.recently_used_playlist), "recentlyusedplaylist",
|
SlideUpMenuGroup(container.context, container.context.getString(R.string.recently_used_playlist), "recentlyusedplaylist",
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, lastUpdated.name, "${lastUpdated.videos.size} " + container.context.getString(R.string.videos), "",
|
SlideUpMenuItem(container.context,
|
||||||
{
|
R.drawable.ic_playlist_add,
|
||||||
|
lastUpdated.name,
|
||||||
|
"${lastUpdated.videos.size} " + container.context.getString(R.string.videos),
|
||||||
|
tag = "",
|
||||||
|
call = {
|
||||||
StatePlaylists.instance.addToPlaylist(lastUpdated.id, video);
|
StatePlaylists.instance.addToPlaylist(lastUpdated.id, video);
|
||||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||||
}))
|
}))
|
||||||
@@ -688,42 +906,91 @@ 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), "download", {
|
if(!isLimited)
|
||||||
showDownloadVideoOverlay(video, container, true);
|
SlideUpMenuItem(
|
||||||
}, false),
|
container.context,
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_share, container.context.getString(R.string.share), "Share the video", "share", {
|
R.drawable.ic_download,
|
||||||
val url = if(video.shareUrl.isNotEmpty()) video.shareUrl else video.url;
|
container.context.getString(R.string.download),
|
||||||
container.context.startActivity(Intent.createChooser(Intent().apply {
|
container.context.getString(R.string.download_the_video),
|
||||||
action = Intent.ACTION_SEND;
|
tag = "download",
|
||||||
putExtra(Intent.EXTRA_TEXT, url);
|
call = {
|
||||||
type = "text/plain";
|
showDownloadVideoOverlay(video, container, true);
|
||||||
}, null));
|
},
|
||||||
}, false),
|
invokeParent = false
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_visibility_off, container.context.getString(R.string.hide_creator_from_home), "", "hide_creator", {
|
) else null,
|
||||||
StateMeta.instance.addHiddenCreator(video.author.url);
|
SlideUpMenuItem(
|
||||||
UIDialogs.toast(container.context, "[${video.author.name}] hidden, you may need to reload home");
|
container.context,
|
||||||
}))
|
R.drawable.ic_share,
|
||||||
+ actions)
|
container.context.getString(R.string.share),
|
||||||
|
"Share the video",
|
||||||
|
tag = "share",
|
||||||
|
call = {
|
||||||
|
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));
|
||||||
|
},
|
||||||
|
invokeParent = false
|
||||||
|
),
|
||||||
|
SlideUpMenuItem(
|
||||||
|
container.context,
|
||||||
|
R.drawable.ic_visibility_off,
|
||||||
|
container.context.getString(R.string.hide_creator_from_home),
|
||||||
|
"",
|
||||||
|
tag = "hide_creator",
|
||||||
|
call = {
|
||||||
|
StateMeta.instance.addHiddenCreator(video.author.url);
|
||||||
|
UIDialogs.toast(container.context, "[${video.author.name}] hidden, you may need to reload home");
|
||||||
|
}))
|
||||||
|
+ actions).filterNotNull()
|
||||||
));
|
));
|
||||||
items.add(
|
items.add(
|
||||||
SlideUpMenuGroup(container.context, container.context.getString(R.string.add_to), "addto",
|
SlideUpMenuGroup(container.context, container.context.getString(R.string.add_to), "addto",
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_queue_add, container.context.getString(R.string.add_to_queue), "${queue.size} " + container.context.getString(R.string.videos), "queue",
|
SlideUpMenuItem(container.context,
|
||||||
{ StatePlayer.instance.addToQueue(video); }),
|
R.drawable.ic_queue_add,
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_watchlist_add, "${container.context.getString(R.string.add_to)} " + StatePlayer.TYPE_WATCHLATER + "", "${watchLater.size} " + container.context.getString(R.string.videos), "watch later",
|
container.context.getString(R.string.add_to_queue),
|
||||||
{ StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video)); })
|
"${queue.size} " + container.context.getString(R.string.videos),
|
||||||
|
tag = "queue",
|
||||||
|
call = { StatePlayer.instance.addToQueue(video); }),
|
||||||
|
SlideUpMenuItem(container.context,
|
||||||
|
R.drawable.ic_watchlist_add,
|
||||||
|
"${container.context.getString(R.string.add_to)} " + StatePlayer.TYPE_WATCHLATER + "",
|
||||||
|
"${watchLater.size} " + container.context.getString(R.string.videos),
|
||||||
|
tag = "watch later",
|
||||||
|
call = { StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true); }),
|
||||||
|
SlideUpMenuItem(container.context,
|
||||||
|
R.drawable.ic_history,
|
||||||
|
container.context.getString(R.string.add_to_history),
|
||||||
|
"Mark as watched",
|
||||||
|
tag = "history",
|
||||||
|
call = { StateHistory.instance.markAsWatched(video); }),
|
||||||
));
|
));
|
||||||
|
|
||||||
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", {
|
playlistItems.add(SlideUpMenuItem(
|
||||||
showCreatePlaylistOverlay(container) {
|
container.context,
|
||||||
val playlist = Playlist(it, arrayListOf(SerializedPlatformVideo.fromVideo(video)));
|
R.drawable.ic_playlist_add,
|
||||||
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
|
container.context.getString(R.string.new_playlist),
|
||||||
};
|
container.context.getString(R.string.add_to_new_playlist),
|
||||||
}, false))
|
tag = "add_to_new_playlist",
|
||||||
|
call = {
|
||||||
|
showCreatePlaylistOverlay(container) {
|
||||||
|
val playlist = Playlist(it, arrayListOf(SerializedPlatformVideo.fromVideo(video)));
|
||||||
|
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
invokeParent = 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),
|
||||||
|
tag = "",
|
||||||
|
call = {
|
||||||
StatePlaylists.instance.addToPlaylist(playlist.id, video);
|
StatePlaylists.instance.addToPlaylist(playlist.id, video);
|
||||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||||
}));
|
}));
|
||||||
@@ -745,8 +1012,12 @@ class UISlideOverlays {
|
|||||||
if (lastUpdated != null) {
|
if (lastUpdated != null) {
|
||||||
items.add(
|
items.add(
|
||||||
SlideUpMenuGroup(container.context, container.context.getString(R.string.recently_used_playlist), "recentlyusedplaylist",
|
SlideUpMenuGroup(container.context, container.context.getString(R.string.recently_used_playlist), "recentlyusedplaylist",
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, lastUpdated.name, "${lastUpdated.videos.size} " + container.context.getString(R.string.videos), "",
|
SlideUpMenuItem(container.context,
|
||||||
{
|
R.drawable.ic_playlist_add,
|
||||||
|
lastUpdated.name,
|
||||||
|
"${lastUpdated.videos.size} " + container.context.getString(R.string.videos),
|
||||||
|
tag = "",
|
||||||
|
call = {
|
||||||
StatePlaylists.instance.addToPlaylist(lastUpdated.id, video);
|
StatePlaylists.instance.addToPlaylist(lastUpdated.id, video);
|
||||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||||
}))
|
}))
|
||||||
@@ -758,25 +1029,44 @@ class UISlideOverlays {
|
|||||||
val watchLater = StatePlaylists.instance.getWatchLater();
|
val watchLater = StatePlaylists.instance.getWatchLater();
|
||||||
items.add(
|
items.add(
|
||||||
SlideUpMenuGroup(container.context, container.context.getString(R.string.other), "other",
|
SlideUpMenuGroup(container.context, container.context.getString(R.string.other), "other",
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_queue_add, container.context.getString(R.string.queue), "${queue.size} " + container.context.getString(R.string.videos), "queue",
|
SlideUpMenuItem(container.context,
|
||||||
{ StatePlayer.instance.addToQueue(video); }),
|
R.drawable.ic_queue_add,
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_watchlist_add, StatePlayer.TYPE_WATCHLATER, "${watchLater.size} " + container.context.getString(R.string.videos), "watch later",
|
container.context.getString(R.string.queue),
|
||||||
{ StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video)); }),
|
"${queue.size} " + container.context.getString(R.string.videos),
|
||||||
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),
|
tag = "queue",
|
||||||
{ showDownloadVideoOverlay(video, container, true); }, false))
|
call = { StatePlayer.instance.addToQueue(video); }),
|
||||||
|
SlideUpMenuItem(container.context,
|
||||||
|
R.drawable.ic_watchlist_add,
|
||||||
|
StatePlayer.TYPE_WATCHLATER,
|
||||||
|
"${watchLater.size} " + container.context.getString(R.string.videos),
|
||||||
|
tag = "watch later",
|
||||||
|
call = { StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true); }),
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
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", {
|
playlistItems.add(SlideUpMenuItem(
|
||||||
slideUpMenuOverlayUpdated(showCreatePlaylistOverlay(container) {
|
container.context,
|
||||||
val playlist = Playlist(it, arrayListOf(SerializedPlatformVideo.fromVideo(video)));
|
R.drawable.ic_playlist_add,
|
||||||
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
|
container.context.getString(R.string.new_playlist),
|
||||||
});
|
container.context.getString(R.string.add_to_new_playlist),
|
||||||
}, false))
|
tag = "add_to_new_playlist",
|
||||||
|
call = {
|
||||||
|
slideUpMenuOverlayUpdated(showCreatePlaylistOverlay(container) {
|
||||||
|
val playlist = Playlist(it, arrayListOf(SerializedPlatformVideo.fromVideo(video)));
|
||||||
|
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
invokeParent = 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),
|
||||||
|
tag = "",
|
||||||
|
call = {
|
||||||
StatePlaylists.instance.addToPlaylist(playlist.id, video);
|
StatePlaylists.instance.addToPlaylist(playlist.id, video);
|
||||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||||
}));
|
}));
|
||||||
@@ -801,20 +1091,36 @@ class UISlideOverlays {
|
|||||||
|
|
||||||
val views = arrayOf(
|
val views = arrayOf(
|
||||||
hidden
|
hidden
|
||||||
.map { btn -> SlideUpMenuItem(container.context, btn.iconResource, btn.text.text.toString(), "", "", {
|
.map { btn -> SlideUpMenuItem(
|
||||||
btn.handler?.invoke(btn);
|
container.context,
|
||||||
}, invokeParents) as View }.toTypedArray(),
|
btn.iconResource,
|
||||||
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), "", {
|
btn.text.text.toString(),
|
||||||
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
|
tag = "",
|
||||||
.map { x -> visible.find { it.tagRef == x } ?: hidden.find { it.tagRef == x } }
|
call = {
|
||||||
.filter { it != null }
|
btn.handler?.invoke(btn);
|
||||||
.map { it!! }
|
},
|
||||||
.toList();
|
invokeParent = 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),
|
||||||
|
tag = "",
|
||||||
|
call = {
|
||||||
|
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
|
||||||
|
.map { x -> visible.find { it.tagRef == x } ?: hidden.find { it.tagRef == x } }
|
||||||
|
.filter { it != null }
|
||||||
|
.map { it!! }
|
||||||
|
.toList();
|
||||||
|
|
||||||
onPinnedbuttons?.invoke(selected + (visible + hidden).filter { !selected.contains(it) });
|
onPinnedbuttons?.invoke(selected + (visible + hidden).filter { !selected.contains(it) });
|
||||||
}
|
}
|
||||||
}, false))
|
},
|
||||||
|
invokeParent = false
|
||||||
|
))
|
||||||
).flatten().toTypedArray();
|
).flatten().toTypedArray();
|
||||||
|
|
||||||
return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.more_options), null, true, *views).apply { show() };
|
return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.more_options), null, true, *views).apply { show() };
|
||||||
@@ -826,14 +1132,21 @@ class UISlideOverlays {
|
|||||||
var overlay: SlideUpMenuOverlay? = null;
|
var overlay: SlideUpMenuOverlay? = null;
|
||||||
|
|
||||||
overlay = SlideUpMenuOverlay(container.context, container, title, container.context.getString(R.string.save), true,
|
overlay = SlideUpMenuOverlay(container.context, container, title, container.context.getString(R.string.save), true,
|
||||||
options.map { SlideUpMenuItem(container.context, R.drawable.ic_move_up, it.first, "", it.second, {
|
options.map { SlideUpMenuItem(
|
||||||
|
container.context,
|
||||||
|
R.drawable.ic_move_up,
|
||||||
|
it.first,
|
||||||
|
"",
|
||||||
|
tag = it.second,
|
||||||
|
call = {
|
||||||
if(overlay!!.selectOption(null, it.second, true, true)) {
|
if(overlay!!.selectOption(null, it.second, true, true)) {
|
||||||
if(!selection.contains(it.second))
|
if(!selection.contains(it.second))
|
||||||
selection.add(it.second);
|
selection.add(it.second);
|
||||||
}
|
} else
|
||||||
else
|
|
||||||
selection.remove(it.second);
|
selection.remove(it.second);
|
||||||
}, false)
|
},
|
||||||
|
invokeParent = false
|
||||||
|
)
|
||||||
});
|
});
|
||||||
overlay.onOK.subscribe {
|
overlay.onOK.subscribe {
|
||||||
onOrdered.invoke(selection);
|
onOrdered.invoke(selection);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import android.os.OperationCanceledException
|
|||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.WindowInsetsController
|
import android.view.WindowInsetsController
|
||||||
|
import android.view.WindowManager
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
@@ -25,10 +26,13 @@ import com.futo.platformplayer.engine.V8Plugin
|
|||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.PlatformVideoWithTime
|
import com.futo.platformplayer.models.PlatformVideoWithTime
|
||||||
import com.futo.platformplayer.others.PlatformLinkMovementMethod
|
import com.futo.platformplayer.others.PlatformLinkMovementMethod
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.ByteOrder
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.ThreadLocalRandom
|
import java.util.concurrent.ThreadLocalRandom
|
||||||
|
|
||||||
@@ -229,4 +233,49 @@ fun String.decodeUnicode(): String {
|
|||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
return sb.toString()
|
return sb.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun <T> smartMerge(targetArr: List<T>, toMerge: List<T>) : List<T>{
|
||||||
|
val missingToMerge = toMerge.filter { !targetArr.contains(it) }.toList();
|
||||||
|
val newArrResult = targetArr.toMutableList();
|
||||||
|
|
||||||
|
for(missing in missingToMerge) {
|
||||||
|
val newIndex = findNewIndex(toMerge, newArrResult, missing);
|
||||||
|
newArrResult.add(newIndex, missing);
|
||||||
|
}
|
||||||
|
|
||||||
|
return newArrResult;
|
||||||
|
}
|
||||||
|
fun <T> findNewIndex(originalArr: List<T>, newArr: List<T>, item: T): Int{
|
||||||
|
var originalIndex = originalArr.indexOf(item);
|
||||||
|
var newIndex = -1;
|
||||||
|
|
||||||
|
for(i in originalIndex-1 downTo 0) {
|
||||||
|
val previousItem = originalArr[i];
|
||||||
|
val indexInNewArr = newArr.indexOfFirst { it == previousItem };
|
||||||
|
if(indexInNewArr >= 0) {
|
||||||
|
newIndex = indexInNewArr + 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(newIndex < 0) {
|
||||||
|
for(i in originalIndex+1 until originalArr.size) {
|
||||||
|
val previousItem = originalArr[i];
|
||||||
|
val indexInNewArr = newArr.indexOfFirst { it == previousItem };
|
||||||
|
if(indexInNewArr >= 0) {
|
||||||
|
newIndex = indexInNewArr - 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(newIndex < 0)
|
||||||
|
return originalArr.size;
|
||||||
|
else
|
||||||
|
return newIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ByteBuffer.toUtf8String(): String {
|
||||||
|
val remainingBytes = ByteArray(remaining())
|
||||||
|
get(remainingBytes)
|
||||||
|
return String(remainingBytes, Charsets.UTF_8)
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+39
-21
@@ -3,6 +3,7 @@ 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.view.View
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
@@ -17,6 +18,7 @@ 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.StateApp
|
||||||
import com.futo.platformplayer.states.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
|
import com.futo.platformplayer.views.LoaderView
|
||||||
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 kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -27,6 +29,7 @@ class PolycentricCreateProfileActivity : AppCompatActivity() {
|
|||||||
private lateinit var _buttonHelp: ImageButton;
|
private lateinit var _buttonHelp: ImageButton;
|
||||||
private lateinit var _profileName: EditText;
|
private lateinit var _profileName: EditText;
|
||||||
private lateinit var _buttonCreate: LinearLayout;
|
private lateinit var _buttonCreate: LinearLayout;
|
||||||
|
private lateinit var _loader: LoaderView;
|
||||||
private val TAG = "PolycentricCreateProfileActivity";
|
private val TAG = "PolycentricCreateProfileActivity";
|
||||||
|
|
||||||
private var _creating = false;
|
private var _creating = false;
|
||||||
@@ -43,6 +46,7 @@ class PolycentricCreateProfileActivity : AppCompatActivity() {
|
|||||||
_buttonHelp = findViewById(R.id.button_help);
|
_buttonHelp = findViewById(R.id.button_help);
|
||||||
_profileName = findViewById(R.id.edit_profile_name);
|
_profileName = findViewById(R.id.edit_profile_name);
|
||||||
_buttonCreate = findViewById(R.id.button_create_profile);
|
_buttonCreate = findViewById(R.id.button_create_profile);
|
||||||
|
_loader = findViewById(R.id.loader);
|
||||||
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
|
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
|
||||||
finish();
|
finish();
|
||||||
};
|
};
|
||||||
@@ -65,35 +69,49 @@ class PolycentricCreateProfileActivity : AppCompatActivity() {
|
|||||||
return@setOnClickListener;
|
return@setOnClickListener;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_profileName.isEnabled = false;
|
||||||
|
_buttonCreate.visibility = View.GONE;
|
||||||
|
_loader.start();
|
||||||
|
_loader.visibility = View.VISIBLE;
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val processHandle: ProcessHandle;
|
val processHandle: ProcessHandle;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
processHandle = ProcessHandle.create();
|
|
||||||
Store.instance.addProcessSecret(processHandle.processSecret);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
PolycentricStorage.instance.addProcessSecret(processHandle.processSecret)
|
processHandle = ProcessHandle.create();
|
||||||
|
Store.instance.addProcessSecret(processHandle.processSecret);
|
||||||
|
|
||||||
|
try {
|
||||||
|
PolycentricStorage.instance.addProcessSecret(processHandle.processSecret)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
processHandle.addServer(PolycentricCache.SERVER);
|
||||||
|
processHandle.setUsername(username);
|
||||||
|
StatePolycentric.instance.setProcessHandle(processHandle);
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
|
Logger.e(TAG, getString(R.string.failed_to_create_profile), e);
|
||||||
|
return@launch;
|
||||||
|
} finally {
|
||||||
|
_creating = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
processHandle.addServer(PolycentricCache.SERVER);
|
try {
|
||||||
processHandle.setUsername(username);
|
Logger.i(TAG, "Started backfill");
|
||||||
StatePolycentric.instance.setProcessHandle(processHandle);
|
processHandle.fullyBackfillServersAnnounceExceptions();
|
||||||
} catch (e: Throwable) {
|
Logger.i(TAG, "Finished backfill");
|
||||||
Logger.e(TAG, getString(R.string.failed_to_create_profile), e);
|
} catch (e: Throwable) {
|
||||||
return@launch;
|
Logger.e(TAG, getString(R.string.failed_to_fully_backfill_servers), e);
|
||||||
} finally {
|
}
|
||||||
_creating = false;
|
|
||||||
}
|
}
|
||||||
|
finally {
|
||||||
try {
|
withContext(Dispatchers.Main) {
|
||||||
Logger.i(TAG, "Started backfill");
|
_profileName.isEnabled = true;
|
||||||
processHandle.fullyBackfillServersAnnounceExceptions();
|
_buttonCreate.visibility = View.VISIBLE;
|
||||||
Logger.i(TAG, "Finished backfill");
|
_loader.stop();
|
||||||
} catch (e: Throwable) {
|
_loader.visibility = View.GONE;
|
||||||
Logger.e(TAG, getString(R.string.failed_to_fully_backfill_servers), e);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import android.os.Bundle
|
|||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.ScrollView
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.request.target.CustomTarget
|
import com.bumptech.glide.request.target.CustomTarget
|
||||||
@@ -28,6 +29,7 @@ class PolycentricHomeActivity : AppCompatActivity() {
|
|||||||
private lateinit var _buttonNewProfile: BigButton;
|
private lateinit var _buttonNewProfile: BigButton;
|
||||||
private lateinit var _buttonImportProfile: BigButton;
|
private lateinit var _buttonImportProfile: BigButton;
|
||||||
private lateinit var _layoutButtons: LinearLayout;
|
private lateinit var _layoutButtons: LinearLayout;
|
||||||
|
private lateinit var _scroll: ScrollView;
|
||||||
|
|
||||||
override fun attachBaseContext(newBase: Context?) {
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
||||||
@@ -42,6 +44,7 @@ class PolycentricHomeActivity : AppCompatActivity() {
|
|||||||
_buttonNewProfile = findViewById(R.id.button_new_profile);
|
_buttonNewProfile = findViewById(R.id.button_new_profile);
|
||||||
_buttonImportProfile = findViewById(R.id.button_import_profile);
|
_buttonImportProfile = findViewById(R.id.button_import_profile);
|
||||||
_layoutButtons = findViewById(R.id.layout_buttons);
|
_layoutButtons = findViewById(R.id.layout_buttons);
|
||||||
|
_scroll = findViewById(R.id.scroll);
|
||||||
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
|
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
|
||||||
finish();
|
finish();
|
||||||
};
|
};
|
||||||
@@ -78,6 +81,7 @@ class PolycentricHomeActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
_layoutButtons.addView(profileButton, 0);
|
_layoutButtons.addView(profileButton, 0);
|
||||||
}
|
}
|
||||||
|
_scroll.invalidate();
|
||||||
|
|
||||||
_buttonHelp.setOnClickListener {
|
_buttonHelp.setOnClickListener {
|
||||||
startActivity(Intent(this, PolycentricWhyActivity::class.java));
|
startActivity(Intent(this, PolycentricWhyActivity::class.java));
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ 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.polycentric.PolycentricCache
|
||||||
|
import com.futo.platformplayer.polycentric.PolycentricStorage
|
||||||
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
|
||||||
@@ -33,6 +34,7 @@ import com.futo.platformplayer.views.buttons.BigButton
|
|||||||
import com.futo.platformplayer.views.overlays.LoaderOverlay
|
import com.futo.platformplayer.views.overlays.LoaderOverlay
|
||||||
import com.futo.polycentric.core.Store
|
import com.futo.polycentric.core.Store
|
||||||
import com.futo.polycentric.core.SystemState
|
import com.futo.polycentric.core.SystemState
|
||||||
|
import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl
|
||||||
import com.futo.polycentric.core.toBase64Url
|
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
|
||||||
@@ -47,6 +49,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
|||||||
private lateinit var _buttonHelp: ImageButton;
|
private lateinit var _buttonHelp: ImageButton;
|
||||||
private lateinit var _editName: EditText;
|
private lateinit var _editName: EditText;
|
||||||
private lateinit var _buttonExport: BigButton;
|
private lateinit var _buttonExport: BigButton;
|
||||||
|
private lateinit var _buttonOpenHarborProfile: BigButton;
|
||||||
private lateinit var _buttonLogout: BigButton;
|
private lateinit var _buttonLogout: BigButton;
|
||||||
private lateinit var _buttonDelete: BigButton;
|
private lateinit var _buttonDelete: BigButton;
|
||||||
private lateinit var _username: String;
|
private lateinit var _username: String;
|
||||||
@@ -68,10 +71,14 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
|||||||
_imagePolycentric = findViewById(R.id.image_polycentric);
|
_imagePolycentric = findViewById(R.id.image_polycentric);
|
||||||
_editName = findViewById(R.id.edit_profile_name);
|
_editName = findViewById(R.id.edit_profile_name);
|
||||||
_buttonExport = findViewById(R.id.button_export);
|
_buttonExport = findViewById(R.id.button_export);
|
||||||
|
_buttonOpenHarborProfile = findViewById(R.id.button_open_harbor_profile);
|
||||||
_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);
|
_loaderOverlay = findViewById(R.id.loader_overlay);
|
||||||
_textSystem = findViewById(R.id.text_system)
|
_textSystem = findViewById(R.id.text_system)
|
||||||
|
findViewById<TextView>(R.id.text_cta2).setOnClickListener {
|
||||||
|
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://harbor.social")))
|
||||||
|
}
|
||||||
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
|
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
|
||||||
saveIfRequired();
|
saveIfRequired();
|
||||||
finish();
|
finish();
|
||||||
@@ -92,6 +99,16 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
|||||||
startActivity(Intent(this, PolycentricBackupActivity::class.java));
|
startActivity(Intent(this, PolycentricBackupActivity::class.java));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_buttonOpenHarborProfile.onClick.subscribe {
|
||||||
|
val processHandle = StatePolycentric.instance.processHandle!!;
|
||||||
|
processHandle?.let {
|
||||||
|
val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(it.system));
|
||||||
|
val url = it.system.systemToURLInfoSystemLinkUrl(systemState.servers.asIterable());
|
||||||
|
val navUrl = "https://harbor.social/" + url.substring("polycentric://".length)
|
||||||
|
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(navUrl)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_buttonLogout.onClick.subscribe {
|
_buttonLogout.onClick.subscribe {
|
||||||
StatePolycentric.instance.setProcessHandle(null);
|
StatePolycentric.instance.setProcessHandle(null);
|
||||||
startActivity(Intent(this, PolycentricHomeActivity::class.java));
|
startActivity(Intent(this, PolycentricHomeActivity::class.java));
|
||||||
@@ -108,6 +125,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
StatePolycentric.instance.setProcessHandle(null);
|
StatePolycentric.instance.setProcessHandle(null);
|
||||||
Store.instance.removeProcessSecret(processHandle.system);
|
Store.instance.removeProcessSecret(processHandle.system);
|
||||||
|
PolycentricStorage.instance.removeProcessSecret(processHandle.system);
|
||||||
startActivity(Intent(this, PolycentricHomeActivity::class.java));
|
startActivity(Intent(this, PolycentricHomeActivity::class.java));
|
||||||
finish();
|
finish();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import androidx.core.app.ActivityCompat
|
|||||||
import androidx.core.content.ContextCompat
|
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.constructs.Event0
|
||||||
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.LoaderView
|
import com.futo.platformplayer.views.LoaderView
|
||||||
@@ -184,12 +185,19 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
|
|||||||
resultLauncher.launch(intent);
|
resultLauncher.launch(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
settingsActivityClosed.emit()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
//TODO: Temporary for solving Settings issues
|
//TODO: Temporary for solving Settings issues
|
||||||
@SuppressLint("StaticFieldLeak")
|
@SuppressLint("StaticFieldLeak")
|
||||||
private var _lastActivity: SettingsActivity? = null;
|
private var _lastActivity: SettingsActivity? = null;
|
||||||
|
|
||||||
|
val settingsActivityClosed = Event0()
|
||||||
|
|
||||||
fun getActivity(): SettingsActivity? {
|
fun getActivity(): SettingsActivity? {
|
||||||
val act = _lastActivity;
|
val act = _lastActivity;
|
||||||
if(act != null && !act._isFinished)
|
if(act != null && !act._isFinished)
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.futo.platformplayer.states.StateSync
|
||||||
|
import com.futo.platformplayer.sync.internal.LinkType
|
||||||
|
import com.futo.platformplayer.sync.internal.SyncSession
|
||||||
|
import com.futo.platformplayer.views.sync.SyncDeviceView
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class SyncHomeActivity : AppCompatActivity() {
|
||||||
|
private lateinit var _layoutDevices: LinearLayout
|
||||||
|
private lateinit var _layoutEmpty: LinearLayout
|
||||||
|
private val _viewMap: MutableMap<String, SyncDeviceView> = mutableMapOf()
|
||||||
|
|
||||||
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
|
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_sync_home)
|
||||||
|
setNavigationBarColorAndIcons()
|
||||||
|
|
||||||
|
_layoutDevices = findViewById(R.id.layout_devices)
|
||||||
|
_layoutEmpty = findViewById(R.id.layout_empty)
|
||||||
|
|
||||||
|
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
findViewById<LinearLayout>(R.id.button_link_new_device).setOnClickListener {
|
||||||
|
startActivity(Intent(this@SyncHomeActivity, SyncPairActivity::class.java))
|
||||||
|
}
|
||||||
|
|
||||||
|
findViewById<LinearLayout>(R.id.button_show_pairing_code).setOnClickListener {
|
||||||
|
startActivity(Intent(this@SyncHomeActivity, SyncShowPairingCodeActivity::class.java))
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeDevices()
|
||||||
|
|
||||||
|
StateSync.instance.deviceUpdatedOrAdded.subscribe(this) { publicKey, session ->
|
||||||
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
val view = _viewMap[publicKey]
|
||||||
|
if (!session.isAuthorized) {
|
||||||
|
if (view != null) {
|
||||||
|
_layoutDevices.removeView(view)
|
||||||
|
_viewMap.remove(publicKey)
|
||||||
|
}
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
if (view == null) {
|
||||||
|
val syncDeviceView = SyncDeviceView(this@SyncHomeActivity)
|
||||||
|
syncDeviceView.onRemove.subscribe {
|
||||||
|
StateApp.instance.scopeOrNull?.launch {
|
||||||
|
StateSync.instance.delete(publicKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val v = updateDeviceView(syncDeviceView, publicKey, session)
|
||||||
|
_layoutDevices.addView(v, 0)
|
||||||
|
_viewMap[publicKey] = v
|
||||||
|
} else {
|
||||||
|
updateDeviceView(view, publicKey, session)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateEmptyVisibility()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StateSync.instance.deviceRemoved.subscribe(this) {
|
||||||
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
val view = _viewMap[it]
|
||||||
|
if (view != null) {
|
||||||
|
_layoutDevices.removeView(view)
|
||||||
|
_viewMap.remove(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateEmptyVisibility()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
|
||||||
|
StateSync.instance.deviceUpdatedOrAdded.remove(this)
|
||||||
|
StateSync.instance.deviceRemoved.remove(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateDeviceView(syncDeviceView: SyncDeviceView, publicKey: String, session: SyncSession?): SyncDeviceView {
|
||||||
|
val connected = session?.connected ?: false
|
||||||
|
syncDeviceView.setLinkType(if (connected) LinkType.Local else LinkType.None)
|
||||||
|
.setName(publicKey)
|
||||||
|
.setStatus(if (connected) "Connected" else "Disconnected")
|
||||||
|
return syncDeviceView
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateEmptyVisibility() {
|
||||||
|
if (_viewMap.isNotEmpty()) {
|
||||||
|
_layoutEmpty.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
_layoutEmpty.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initializeDevices() {
|
||||||
|
_layoutDevices.removeAllViews()
|
||||||
|
|
||||||
|
for (publicKey in StateSync.instance.getAll()) {
|
||||||
|
val syncDeviceView = SyncDeviceView(this)
|
||||||
|
syncDeviceView.onRemove.subscribe {
|
||||||
|
StateApp.instance.scopeOrNull?.launch {
|
||||||
|
StateSync.instance.delete(publicKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val view = updateDeviceView(syncDeviceView, publicKey, StateSync.instance.getSession(publicKey))
|
||||||
|
_layoutDevices.addView(view)
|
||||||
|
_viewMap[publicKey] = view
|
||||||
|
}
|
||||||
|
|
||||||
|
updateEmptyVisibility()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "SyncHomeActivity"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Base64
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.futo.platformplayer.states.StateSync
|
||||||
|
import com.futo.platformplayer.sync.internal.SyncDeviceInfo
|
||||||
|
import com.google.zxing.integration.android.IntentIntegrator
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
|
class SyncPairActivity : AppCompatActivity() {
|
||||||
|
private lateinit var _editCode: EditText
|
||||||
|
|
||||||
|
private lateinit var _layoutPairing: LinearLayout
|
||||||
|
private lateinit var _textPairingStatus: TextView
|
||||||
|
|
||||||
|
private lateinit var _layoutPairingSuccess: LinearLayout
|
||||||
|
|
||||||
|
private lateinit var _layoutPairingError: LinearLayout
|
||||||
|
private lateinit var _textError: TextView
|
||||||
|
|
||||||
|
private val _qrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
|
val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
|
||||||
|
scanResult?.let {
|
||||||
|
if (it.contents != null) {
|
||||||
|
_editCode.text.clear()
|
||||||
|
_editCode.text.append(it.contents)
|
||||||
|
pair(it.contents)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
|
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_sync_pair)
|
||||||
|
setNavigationBarColorAndIcons()
|
||||||
|
|
||||||
|
_editCode = findViewById(R.id.edit_code)
|
||||||
|
_layoutPairing = findViewById(R.id.layout_pairing)
|
||||||
|
_textPairingStatus = findViewById(R.id.text_pairing_status)
|
||||||
|
_layoutPairingSuccess = findViewById(R.id.layout_pairing_success)
|
||||||
|
_layoutPairingError = findViewById(R.id.layout_pairing_error)
|
||||||
|
_textError = findViewById(R.id.text_error)
|
||||||
|
|
||||||
|
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
findViewById<LinearLayout>(R.id.button_scan_qr).setOnClickListener {
|
||||||
|
val integrator = IntentIntegrator(this)
|
||||||
|
integrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
|
||||||
|
integrator.setPrompt(getString(R.string.scan_a_qr_code))
|
||||||
|
integrator.setOrientationLocked(true);
|
||||||
|
integrator.setCameraId(0)
|
||||||
|
integrator.setBeepEnabled(false)
|
||||||
|
integrator.setBarcodeImageEnabled(true)
|
||||||
|
integrator.setCaptureActivity(QRCaptureActivity::class.java);
|
||||||
|
_qrCodeResultLauncher.launch(integrator.createScanIntent())
|
||||||
|
}
|
||||||
|
|
||||||
|
findViewById<LinearLayout>(R.id.button_link_new_device).setOnClickListener {
|
||||||
|
pair(_editCode.text.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
_layoutPairingSuccess.setOnClickListener {
|
||||||
|
_layoutPairingSuccess.visibility = View.GONE
|
||||||
|
}
|
||||||
|
_layoutPairingError.setOnClickListener {
|
||||||
|
_layoutPairingError.visibility = View.GONE
|
||||||
|
}
|
||||||
|
_layoutPairingSuccess.visibility = View.GONE
|
||||||
|
_layoutPairingError.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
fun pair(url: String) {
|
||||||
|
try {
|
||||||
|
_layoutPairing.visibility = View.VISIBLE
|
||||||
|
_textPairingStatus.text = "Parsing text..."
|
||||||
|
|
||||||
|
if (!url.startsWith("grayjay://sync/")) {
|
||||||
|
throw Exception("Not a valid URL: $url")
|
||||||
|
}
|
||||||
|
|
||||||
|
val deviceInfo: SyncDeviceInfo = Json.decodeFromString<SyncDeviceInfo>(Base64.decode(url.substring("grayjay://sync/".length), Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP).decodeToString())
|
||||||
|
if (StateSync.instance.isAuthorized(deviceInfo.publicKey)) {
|
||||||
|
throw Exception("This device is already paired")
|
||||||
|
}
|
||||||
|
|
||||||
|
_textPairingStatus.text = "Connecting..."
|
||||||
|
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
StateSync.instance.connect(deviceInfo) { session, complete, message ->
|
||||||
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
if (complete) {
|
||||||
|
_layoutPairingSuccess.visibility = View.VISIBLE
|
||||||
|
_layoutPairing.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
_textPairingStatus.text = message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
_layoutPairingError.visibility = View.VISIBLE
|
||||||
|
_textError.text = e.message
|
||||||
|
_layoutPairing.visibility = View.GONE
|
||||||
|
Logger.e(TAG, "Failed to pair", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(e: Throwable) {
|
||||||
|
_layoutPairingError.visibility = View.VISIBLE
|
||||||
|
_textError.text = e.message
|
||||||
|
_layoutPairing.visibility = View.GONE
|
||||||
|
Logger.e(TAG, "Failed to pair", e)
|
||||||
|
} finally {
|
||||||
|
_layoutPairing.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "SyncPairActivity"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Base64
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.futo.platformplayer.states.StateSync
|
||||||
|
import com.futo.platformplayer.sync.internal.SyncDeviceInfo
|
||||||
|
import com.google.zxing.BarcodeFormat
|
||||||
|
import com.google.zxing.MultiFormatWriter
|
||||||
|
import com.google.zxing.common.BitMatrix
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import java.net.NetworkInterface
|
||||||
|
|
||||||
|
class SyncShowPairingCodeActivity : AppCompatActivity() {
|
||||||
|
private lateinit var _textCode: TextView
|
||||||
|
private lateinit var _imageQR: ImageView
|
||||||
|
private lateinit var _textQR: TextView
|
||||||
|
private var _code: String? = null
|
||||||
|
|
||||||
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
|
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
activity = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
activity = this
|
||||||
|
|
||||||
|
setContentView(R.layout.activity_sync_show_pairing_code)
|
||||||
|
setNavigationBarColorAndIcons()
|
||||||
|
|
||||||
|
_textCode = findViewById(R.id.text_code)
|
||||||
|
_imageQR = findViewById(R.id.image_qr)
|
||||||
|
_textQR = findViewById(R.id.text_scan_qr)
|
||||||
|
|
||||||
|
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
findViewById<LinearLayout>(R.id.button_copy).setOnClickListener {
|
||||||
|
val code = _code ?: return@setOnClickListener
|
||||||
|
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager;
|
||||||
|
val clip = ClipData.newPlainText(getString(R.string.copied_text), code);
|
||||||
|
clipboard.setPrimaryClip(clip);
|
||||||
|
UIDialogs.toast(this, "Copied to clipboard")
|
||||||
|
}
|
||||||
|
|
||||||
|
val ips = getIPs()
|
||||||
|
val selfDeviceInfo = SyncDeviceInfo(StateSync.instance.publicKey!!, ips.toTypedArray(), StateSync.PORT)
|
||||||
|
val json = Json.encodeToString(selfDeviceInfo)
|
||||||
|
val base64 = Base64.encodeToString(json.toByteArray(), Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP)
|
||||||
|
val url = "grayjay://sync/${base64}"
|
||||||
|
setCode(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setCode(code: String?) {
|
||||||
|
_code = code
|
||||||
|
|
||||||
|
_textCode.text = code
|
||||||
|
|
||||||
|
if (code == null) {
|
||||||
|
_imageQR.visibility = View.INVISIBLE
|
||||||
|
_textQR.visibility = View.INVISIBLE
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val dimension = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 200f, resources.displayMetrics).toInt()
|
||||||
|
val qrCodeBitmap = generateQRCode(code, dimension, dimension)
|
||||||
|
_imageQR.setImageBitmap(qrCodeBitmap)
|
||||||
|
_imageQR.visibility = View.VISIBLE
|
||||||
|
_textQR.visibility = View.VISIBLE
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.e(TAG, getString(R.string.failed_to_generate_qr_code), e)
|
||||||
|
_imageQR.visibility = View.INVISIBLE
|
||||||
|
_textQR.visibility = View.INVISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateQRCode(content: String, width: Int, height: Int): Bitmap {
|
||||||
|
val bitMatrix = MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height);
|
||||||
|
return bitMatrixToBitmap(bitMatrix);
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bitMatrixToBitmap(matrix: BitMatrix): Bitmap {
|
||||||
|
val width = matrix.width;
|
||||||
|
val height = matrix.height;
|
||||||
|
val bmp = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
|
||||||
|
|
||||||
|
for (x in 0 until width) {
|
||||||
|
for (y in 0 until height) {
|
||||||
|
bmp.setPixel(x, y, if (matrix[x, y]) Color.BLACK else Color.WHITE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getIPs(): List<String> {
|
||||||
|
val ips = arrayListOf<String>()
|
||||||
|
for (intf in NetworkInterface.getNetworkInterfaces()) {
|
||||||
|
for (addr in intf.inetAddresses) {
|
||||||
|
if (addr.isLoopbackAddress) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addr.address.size != 4) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
addr.hostAddress?.let { ips.add(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ips
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "SyncShowPairingCodeActivity"
|
||||||
|
var activity: SyncShowPairingCodeActivity? = null
|
||||||
|
private set
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,13 +17,14 @@ import okhttp3.WebSocket
|
|||||||
import okhttp3.WebSocketListener
|
import okhttp3.WebSocketListener
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.security.cert.X509Certificate
|
import java.security.cert.X509Certificate
|
||||||
|
import java.time.Duration
|
||||||
import javax.net.ssl.SSLContext
|
import javax.net.ssl.SSLContext
|
||||||
import javax.net.ssl.TrustManager
|
import javax.net.ssl.TrustManager
|
||||||
import javax.net.ssl.X509TrustManager
|
import javax.net.ssl.X509TrustManager
|
||||||
import kotlin.system.measureTimeMillis
|
import kotlin.system.measureTimeMillis
|
||||||
|
|
||||||
open class ManagedHttpClient {
|
open class ManagedHttpClient {
|
||||||
protected val _builderTemplate: OkHttpClient.Builder;
|
protected var _builderTemplate: OkHttpClient.Builder;
|
||||||
|
|
||||||
private var client: OkHttpClient;
|
private var client: OkHttpClient;
|
||||||
|
|
||||||
@@ -32,6 +33,15 @@ 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"
|
||||||
|
|
||||||
|
fun setTimeout(timeout: Long) {
|
||||||
|
rebuildClient {
|
||||||
|
it.callTimeout(Duration.ofMillis(client.callTimeoutMillis.toLong()))
|
||||||
|
.writeTimeout(Duration.ofMillis(client.writeTimeoutMillis.toLong()))
|
||||||
|
.readTimeout(Duration.ofMillis(client.readTimeoutMillis.toLong()))
|
||||||
|
.connectTimeout(Duration.ofMillis(timeout));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private val trustAllCerts = arrayOf<TrustManager>(
|
private val trustAllCerts = arrayOf<TrustManager>(
|
||||||
object: X509TrustManager {
|
object: X509TrustManager {
|
||||||
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) { }
|
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) { }
|
||||||
@@ -62,6 +72,15 @@ open class ManagedHttpClient {
|
|||||||
}.build();
|
}.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun rebuildClient(modify: (OkHttpClient.Builder) -> OkHttpClient.Builder) {
|
||||||
|
_builderTemplate = modify(_builderTemplate);
|
||||||
|
client = _builderTemplate.addNetworkInterceptor { chain ->
|
||||||
|
val request = beforeRequest(chain.request());
|
||||||
|
val response = afterRequest(chain.proceed(request));
|
||||||
|
return@addNetworkInterceptor response;
|
||||||
|
}.build();
|
||||||
|
}
|
||||||
|
|
||||||
open fun clone(): ManagedHttpClient {
|
open fun clone(): ManagedHttpClient {
|
||||||
val clonedClient = ManagedHttpClient(_builderTemplate);
|
val clonedClient = ManagedHttpClient(_builderTemplate);
|
||||||
clonedClient.user_agent = user_agent;
|
clonedClient.user_agent = user_agent;
|
||||||
|
|||||||
@@ -210,6 +210,20 @@ class HttpContext : AutoCloseable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fun respondBytes(status: Int, headers: HttpHeaders, body: ByteArray? = null) {
|
||||||
|
if(headers.get("content-length").isNullOrEmpty()) {
|
||||||
|
if (body != null) {
|
||||||
|
headers.put("content-length", body.size.toString());
|
||||||
|
} else {
|
||||||
|
headers.put("content-length", "0")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
respond(status, headers) { responseStream ->
|
||||||
|
if(body != null) {
|
||||||
|
responseStream.write(body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
fun respond(status: Int, headers: HttpHeaders, writing: (OutputStream)->Unit) {
|
fun respond(status: Int, headers: HttpHeaders, writing: (OutputStream)->Unit) {
|
||||||
val responseStream = _responseStream ?: throw IllegalStateException("No response stream set");
|
val responseStream = _responseStream ?: throw IllegalStateException("No response stream set");
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package com.futo.platformplayer.api.http.server
|
|||||||
|
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.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.HttpFunctionHandler
|
||||||
import com.futo.platformplayer.api.http.server.handlers.HttpHandler
|
import com.futo.platformplayer.api.http.server.handlers.HttpHandler
|
||||||
import com.futo.platformplayer.api.http.server.handlers.HttpOptionsAllowHandler
|
import com.futo.platformplayer.api.http.server.handlers.HttpOptionsAllowHandler
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
@@ -208,20 +208,20 @@ class ManagedHttpServer(private val _requestedPort: Int = 0) {
|
|||||||
|
|
||||||
for(getMethod in getMethods)
|
for(getMethod in getMethods)
|
||||||
if(getMethod.first.parameterTypes.firstOrNull() == HttpContext::class.java && getMethod.first.parameterCount == 1)
|
if(getMethod.first.parameterTypes.firstOrNull() == HttpContext::class.java && getMethod.first.parameterCount == 1)
|
||||||
addHandler(HttpFuntionHandler("GET", getMethod.second.path) { getMethod.first.invoke(obj, it) }).apply {
|
addHandler(HttpFunctionHandler("GET", getMethod.second.path) { getMethod.first.invoke(obj, it) }).apply {
|
||||||
if(!getMethod.second.contentType.isEmpty())
|
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(HttpFunctionHandler("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;
|
||||||
addHandler(HttpFuntionHandler("GET", getField.second.path) {
|
addHandler(HttpFunctionHandler("GET", getField.second.path) {
|
||||||
val value = getField.first.get(obj) as String?;
|
val value = getField.first.get(obj) as String?;
|
||||||
if(value != null) {
|
if(value != null) {
|
||||||
val headers = HttpHeaders(
|
val headers = HttpHeaders(
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@ 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 HttpFuntionHandler(method: String, path: String, val handler: (HttpContext)->Unit) : HttpHandler(method, path) {
|
class HttpFunctionHandler(method: String, path: String, val handler: (HttpContext)->Unit) : HttpHandler(method, path) {
|
||||||
override fun handle(httpContext: HttpContext) {
|
override fun handle(httpContext: HttpContext) {
|
||||||
httpContext.setResponseHeaders(this.headers);
|
httpContext.setResponseHeaders(this.headers);
|
||||||
handler(httpContext);
|
handler(httpContext);
|
||||||
|
|||||||
@@ -4,6 +4,6 @@ import kotlinx.serialization.json.Json
|
|||||||
|
|
||||||
class Serializer {
|
class Serializer {
|
||||||
companion object {
|
companion object {
|
||||||
val json = Json { ignoreUnknownKeys = true; encodeDefaults = true; };
|
val json = Json { ignoreUnknownKeys = true; encodeDefaults = true; coerceInputValues = true };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -27,6 +27,8 @@ open class PlatformAuthorLink {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
val UNKNOWN = PlatformAuthorLink(PlatformID.NONE, "Unknown", "", null, null);
|
||||||
|
|
||||||
fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformAuthorLink {
|
fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformAuthorLink {
|
||||||
if(value.has("membershipUrl"))
|
if(value.has("membershipUrl"))
|
||||||
return PlatformAuthorMembershipLink.fromV8(config, value);
|
return PlatformAuthorMembershipLink.fromV8(config, value);
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ enum class ChapterType(val value: Int) {
|
|||||||
companion object {
|
companion object {
|
||||||
fun fromInt(value: Int): ChapterType
|
fun fromInt(value: Int): ChapterType
|
||||||
{
|
{
|
||||||
val result = ChapterType.values().firstOrNull { it.value == value };
|
val result = ChapterType.entries.firstOrNull { it.value == value };
|
||||||
if(result == null)
|
if(result == null)
|
||||||
throw UnknownPlatformException(value.toString());
|
throw UnknownPlatformException(value.toString());
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package com.futo.platformplayer.api.media.models.comments
|
||||||
|
|
||||||
|
import com.futo.platformplayer.api.media.IPlatformClient
|
||||||
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
|
import com.futo.platformplayer.api.media.models.ratings.IRating
|
||||||
|
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||||
|
import com.futo.platformplayer.api.media.models.ratings.RatingType
|
||||||
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import kotlinx.coroutines.Deferred
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
|
class LazyComment: IPlatformComment {
|
||||||
|
private var _commentDeferred: Deferred<IPlatformComment>;
|
||||||
|
private var _commentLoaded: IPlatformComment? = null;
|
||||||
|
private var _commentException: Throwable? = null;
|
||||||
|
|
||||||
|
override val contextUrl: String
|
||||||
|
get() = _commentLoaded?.contextUrl ?: "";
|
||||||
|
override val author: PlatformAuthorLink
|
||||||
|
get() = _commentLoaded?.author ?: PlatformAuthorLink.UNKNOWN;
|
||||||
|
override val message: String
|
||||||
|
get() = _commentLoaded?.message ?: "";
|
||||||
|
override val rating: IRating
|
||||||
|
get() = _commentLoaded?.rating ?: RatingLikes(0);
|
||||||
|
override val date: OffsetDateTime?
|
||||||
|
get() = _commentLoaded?.date ?: OffsetDateTime.MIN;
|
||||||
|
override val replyCount: Int?
|
||||||
|
get() = _commentLoaded?.replyCount ?: 0;
|
||||||
|
|
||||||
|
val isAvailable: Boolean get() = _commentLoaded != null;
|
||||||
|
|
||||||
|
private var _uiHandler: ((LazyComment)->Unit)? = null;
|
||||||
|
|
||||||
|
constructor(commentDeferred: Deferred<IPlatformComment>) {
|
||||||
|
_commentDeferred = commentDeferred;
|
||||||
|
_commentDeferred.invokeOnCompletion {
|
||||||
|
if(it == null) {
|
||||||
|
_commentLoaded = commentDeferred.getCompleted();
|
||||||
|
Logger.i("LazyComment", "Resolved comment");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
_commentException = it;
|
||||||
|
Logger.e("LazyComment", "Resolving comment failed: ${it.message}", it);
|
||||||
|
}
|
||||||
|
|
||||||
|
_uiHandler?.invoke(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getUnderlyingComment(): IPlatformComment? {
|
||||||
|
return _commentLoaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setUIHandler(handler: (LazyComment)->Unit){
|
||||||
|
_uiHandler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getReplies(client: IPlatformClient): IPager<IPlatformComment>? {
|
||||||
|
return _commentLoaded?.getReplies(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -21,7 +21,7 @@ enum class ContentType(val value: Int) {
|
|||||||
companion object {
|
companion object {
|
||||||
fun fromInt(value: Int): ContentType
|
fun fromInt(value: Int): ContentType
|
||||||
{
|
{
|
||||||
val result = ContentType.values().firstOrNull { it.value == value };
|
val result = ContentType.entries.firstOrNull { it.value == value };
|
||||||
if(result == null)
|
if(result == null)
|
||||||
throw UnknownPlatformException(value.toString());
|
throw UnknownPlatformException(value.toString());
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ enum class LiveEventType(val value : Int) {
|
|||||||
|
|
||||||
companion object{
|
companion object{
|
||||||
fun fromInt(value : Int) : LiveEventType{
|
fun fromInt(value : Int) : LiveEventType{
|
||||||
return LiveEventType.values().first { it.value == value };
|
return LiveEventType.entries.first { it.value == value };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,7 +10,7 @@ enum class TextType(val value: Int) {
|
|||||||
companion object {
|
companion object {
|
||||||
fun fromInt(value: Int): TextType
|
fun fromInt(value: Int): TextType
|
||||||
{
|
{
|
||||||
val result = TextType.values().firstOrNull { it.value == value };
|
val result = TextType.entries.firstOrNull { it.value == value };
|
||||||
if(result == null)
|
if(result == null)
|
||||||
throw IllegalArgumentException("Unknown Texttype: $value");
|
throw IllegalArgumentException("Unknown Texttype: $value");
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ enum class RatingType(val value : Int) {
|
|||||||
|
|
||||||
companion object{
|
companion object{
|
||||||
fun fromInt(value : Int) : RatingType{
|
fun fromInt(value : Int) : RatingType{
|
||||||
return RatingType.values().first { it.value == value };
|
return RatingType.entries.first { it.value == value };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+1
-4
@@ -1,6 +1,3 @@
|
|||||||
package com.futo.platformplayer.api.media.models.streams.sources
|
package com.futo.platformplayer.api.media.models.streams.sources
|
||||||
|
|
||||||
interface IAudioUrlWidevineSource : IAudioUrlSource {
|
interface IAudioUrlWidevineSource : IAudioUrlSource, IWidevineSource
|
||||||
val bearerToken: String
|
|
||||||
val licenseUri: String
|
|
||||||
}
|
|
||||||
|
|||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
package com.futo.platformplayer.api.media.models.streams.sources
|
||||||
|
|
||||||
|
interface IDashManifestWidevineSource : IWidevineSource {
|
||||||
|
val url: String
|
||||||
|
}
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
package com.futo.platformplayer.api.media.models.streams.sources
|
||||||
|
|
||||||
|
interface IVideoUrlWidevineSource : IVideoUrlSource, IWidevineSource
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
package com.futo.platformplayer.api.media.models.streams.sources
|
||||||
|
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
|
||||||
|
|
||||||
|
interface IWidevineSource {
|
||||||
|
val licenseUri: String
|
||||||
|
val hasLicenseRequestExecutor: Boolean
|
||||||
|
fun getLicenseRequestExecutor(): JSRequestExecutor?
|
||||||
|
}
|
||||||
+2
-2
@@ -19,9 +19,9 @@ open class SerializedPlatformVideo(
|
|||||||
override val thumbnails: Thumbnails,
|
override val thumbnails: Thumbnails,
|
||||||
override val author: PlatformAuthorLink,
|
override val author: PlatformAuthorLink,
|
||||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
||||||
override val datetime: OffsetDateTime?,
|
override val datetime: OffsetDateTime? = null,
|
||||||
override val url: String,
|
override val url: String,
|
||||||
override val shareUrl: String,
|
override val shareUrl: String = "",
|
||||||
|
|
||||||
override val duration: Long,
|
override val duration: Long,
|
||||||
override val viewCount: Long,
|
override val viewCount: Long,
|
||||||
|
|||||||
@@ -237,7 +237,8 @@ open class JSClient : IPlatformClient {
|
|||||||
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,
|
hasPeekChannelContents = plugin.executeBoolean("!!source.peekChannelContents") ?: false,
|
||||||
hasGetChannelPlaylists = plugin.executeBoolean("!!source.getChannelPlaylists") ?: false
|
hasGetChannelPlaylists = plugin.executeBoolean("!!source.getChannelPlaylists") ?: false,
|
||||||
|
hasGetContentRecommendations = plugin.executeBoolean("!!source.getContentRecommendations") ?: false
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.futo.platformplayer.api.media.platforms.js
|
||||||
|
|
||||||
|
class JSClientConstants {
|
||||||
|
companion object {
|
||||||
|
val PLUGIN_SPEC_VERSION = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
-4
@@ -4,6 +4,7 @@ import android.net.Uri
|
|||||||
import com.futo.platformplayer.SignatureProvider
|
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.matchesDomain
|
||||||
import com.futo.platformplayer.states.StatePlugins
|
import com.futo.platformplayer.states.StatePlugins
|
||||||
import kotlinx.serialization.Contextual
|
import kotlinx.serialization.Contextual
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
@@ -49,6 +50,8 @@ class SourcePluginConfig(
|
|||||||
var primaryClaimFieldType: Int? = null,
|
var primaryClaimFieldType: Int? = null,
|
||||||
var developerSubmitUrl: String? = null,
|
var developerSubmitUrl: String? = null,
|
||||||
var allowAllHttpHeaderAccess: Boolean = false,
|
var allowAllHttpHeaderAccess: Boolean = false,
|
||||||
|
var maxDownloadParallelism: Int = 0,
|
||||||
|
var reduceFunctionsInLimitedVersion: Boolean = false,
|
||||||
) : IV8PluginConfig {
|
) : IV8PluginConfig {
|
||||||
|
|
||||||
val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl);
|
val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl);
|
||||||
@@ -79,7 +82,7 @@ class SourcePluginConfig(
|
|||||||
private val _allowUrlsLower: List<String> get() {
|
private val _allowUrlsLower: List<String> get() {
|
||||||
if(_allowUrlsLowerVal == null)
|
if(_allowUrlsLowerVal == null)
|
||||||
_allowUrlsLowerVal = allowUrls.map { it.lowercase() }
|
_allowUrlsLowerVal = allowUrls.map { it.lowercase() }
|
||||||
.filter { it.length > 0 && (it[0] != '*' || (_allowRegex.matches(it))) };
|
.filter { it.length > 0 };
|
||||||
return _allowUrlsLowerVal!!;
|
return _allowUrlsLowerVal!!;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -172,12 +175,10 @@ class SourcePluginConfig(
|
|||||||
return true;
|
return true;
|
||||||
val uri = Uri.parse(url);
|
val uri = Uri.parse(url);
|
||||||
val host = uri.host?.lowercase() ?: "";
|
val host = uri.host?.lowercase() ?: "";
|
||||||
return _allowUrlsLower.any { it == host || (it.length > 0 && it[0] == '*' && host.endsWith(it.substring(1))) };
|
return _allowUrlsLower.any { it == host || (it.length > 0 && it[0] == '.' && host.matchesDomain(it)) };
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val _allowRegex = Regex("\\*\\.[a-z0-9]+\\.[a-z]+");
|
|
||||||
|
|
||||||
fun fromJson(json: String, sourceUrl: String? = null): SourcePluginConfig {
|
fun fromJson(json: String, sourceUrl: String? = null): SourcePluginConfig {
|
||||||
val obj = Serializer.json.decodeFromString<SourcePluginConfig>(json);
|
val obj = Serializer.json.decodeFromString<SourcePluginConfig>(json);
|
||||||
if(obj.sourceUrl == null)
|
if(obj.sourceUrl == null)
|
||||||
|
|||||||
+1
-1
@@ -38,7 +38,7 @@ class JSHttpClient : ManagedHttpClient {
|
|||||||
|
|
||||||
constructor(jsClient: JSClient?, auth: SourceAuth? = null, captcha: SourceCaptchaData? = null, config: SourcePluginConfig? = null) : super(
|
constructor(jsClient: JSClient?, auth: SourceAuth? = null, captcha: SourceCaptchaData? = null, config: SourcePluginConfig? = null) : super(
|
||||||
//Temporary ugly solution for DevPortal proxy support
|
//Temporary ugly solution for DevPortal proxy support
|
||||||
(if(jsClient?.config?.id == StateDeveloper.DEV_ID && StateDeveloper.instance.devProxy != null)
|
(if((jsClient?.config?.id == StateDeveloper.DEV_ID || jsClient == null) && StateDeveloper.instance.devProxy != null)
|
||||||
OkHttpClient.Builder().proxy(Proxy(Proxy.Type.HTTP,
|
OkHttpClient.Builder().proxy(Proxy(Proxy.Type.HTTP,
|
||||||
InetSocketAddress(StateDeveloper.instance.devProxy!!.url, StateDeveloper.instance.devProxy!!.port)
|
InetSocketAddress(StateDeveloper.instance.devProxy!!.url, StateDeveloper.instance.devProxy!!.port)
|
||||||
))
|
))
|
||||||
|
|||||||
+1
@@ -16,6 +16,7 @@ interface IJSContentDetails: IPlatformContent {
|
|||||||
return when(ContentType.fromInt(type)) {
|
return when(ContentType.fromInt(type)) {
|
||||||
ContentType.MEDIA -> JSVideoDetails(plugin, obj);
|
ContentType.MEDIA -> JSVideoDetails(plugin, obj);
|
||||||
ContentType.POST -> JSPostDetails(plugin.config, obj);
|
ContentType.POST -> JSPostDetails(plugin.config, obj);
|
||||||
|
ContentType.ARTICLE -> JSArticleDetails(plugin, obj);
|
||||||
else -> throw NotImplementedError("Unknown content type ${type}");
|
else -> throw NotImplementedError("Unknown content type ${type}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+162
@@ -0,0 +1,162 @@
|
|||||||
|
package com.futo.platformplayer.api.media.platforms.js.models
|
||||||
|
|
||||||
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
|
import com.futo.platformplayer.api.media.IPlatformClient
|
||||||
|
import com.futo.platformplayer.api.media.IPluginSourced
|
||||||
|
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||||
|
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.IPlatformContent
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
|
||||||
|
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
||||||
|
import com.futo.platformplayer.api.media.models.post.TextType
|
||||||
|
import com.futo.platformplayer.api.media.models.ratings.IRating
|
||||||
|
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
|
import com.futo.platformplayer.getOrDefault
|
||||||
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
import com.futo.platformplayer.getOrThrowNullableList
|
||||||
|
import com.futo.platformplayer.states.StateDeveloper
|
||||||
|
|
||||||
|
open class JSArticleDetails : JSContent, IPluginSourced, IPlatformContentDetails {
|
||||||
|
final override val contentType: ContentType get() = ContentType.ARTICLE;
|
||||||
|
|
||||||
|
private val _hasGetComments: Boolean;
|
||||||
|
private val _hasGetContentRecommendations: Boolean;
|
||||||
|
|
||||||
|
val rating: IRating;
|
||||||
|
|
||||||
|
val summary: String;
|
||||||
|
val thumbnails: Thumbnails?;
|
||||||
|
val segments: List<IJSArticleSegment>;
|
||||||
|
|
||||||
|
constructor(client: JSClient, obj: V8ValueObject): super(client.config, obj) {
|
||||||
|
val contextName = "PlatformPost";
|
||||||
|
|
||||||
|
rating = obj.getOrDefault<V8ValueObject>(client.config, "rating", contextName, null)?.let { IRating.fromV8(client.config, it, contextName) } ?: RatingLikes(0);
|
||||||
|
summary = _content.getOrThrow(client.config, "summary", contextName);
|
||||||
|
if(_content.has("thumbnails"))
|
||||||
|
thumbnails = Thumbnails.fromV8(client.config, _content.getOrThrow(client.config, "thumbnails", contextName));
|
||||||
|
else
|
||||||
|
thumbnails = null;
|
||||||
|
|
||||||
|
|
||||||
|
segments = (obj.getOrThrowNullableList<V8ValueObject>(client.config, "segments", contextName)
|
||||||
|
?.map { fromV8Segment(client, it) }
|
||||||
|
?.filterNotNull() ?: listOf());
|
||||||
|
|
||||||
|
_hasGetComments = _content.has("getComments");
|
||||||
|
_hasGetContentRecommendations = _content.has("getContentRecommendations");
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? {
|
||||||
|
if(!_hasGetComments || _content.isClosed)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if(client is DevJSClient)
|
||||||
|
return StateDeveloper.instance.handleDevCall(client.devID, "videoDetail.getComments()") {
|
||||||
|
return@handleDevCall getCommentsJS(client);
|
||||||
|
}
|
||||||
|
else if(client is JSClient)
|
||||||
|
return getCommentsJS(client);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getPlaybackTracker(): IPlaybackTracker? = null;
|
||||||
|
|
||||||
|
override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? {
|
||||||
|
if(!_hasGetContentRecommendations || _content.isClosed)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if(client is DevJSClient)
|
||||||
|
return StateDeveloper.instance.handleDevCall(client.devID, "postDetail.getContentRecommendations()") {
|
||||||
|
return@handleDevCall getContentRecommendationsJS(client);
|
||||||
|
}
|
||||||
|
else if(client is JSClient)
|
||||||
|
return getContentRecommendationsJS(client);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getContentRecommendationsJS(client: JSClient): JSContentPager {
|
||||||
|
val contentPager = _content.invoke<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
|
||||||
|
return JSContentPager(_pluginConfig, client, contentPager);
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCommentsJS(client: JSClient): JSCommentPager {
|
||||||
|
val commentPager = _content.invoke<V8ValueObject>("getComments", arrayOf<Any>());
|
||||||
|
return JSCommentPager(_pluginConfig, client, commentPager);
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromV8Segment(client: JSClient, obj: V8ValueObject): IJSArticleSegment? {
|
||||||
|
if(!obj.has("type"))
|
||||||
|
throw IllegalArgumentException("Object missing type field");
|
||||||
|
return when(SegmentType.fromInt(obj.getOrThrow(client.config, "type", "JSArticle.Segment"))) {
|
||||||
|
SegmentType.TEXT -> JSTextSegment(client, obj);
|
||||||
|
SegmentType.IMAGES -> JSImagesSegment(client, obj);
|
||||||
|
SegmentType.NESTED -> JSNestedSegment(client, obj);
|
||||||
|
else -> null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class SegmentType(val value: Int) {
|
||||||
|
UNKNOWN(0),
|
||||||
|
TEXT(1),
|
||||||
|
IMAGES(2),
|
||||||
|
|
||||||
|
NESTED(9);
|
||||||
|
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromInt(value: Int): SegmentType
|
||||||
|
{
|
||||||
|
val result = SegmentType.entries.firstOrNull { it.value == value };
|
||||||
|
if(result == null)
|
||||||
|
throw IllegalArgumentException("Unknown Texttype: $value");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IJSArticleSegment {
|
||||||
|
val type: SegmentType;
|
||||||
|
}
|
||||||
|
class JSTextSegment: IJSArticleSegment {
|
||||||
|
override val type = SegmentType.TEXT;
|
||||||
|
val textType: TextType;
|
||||||
|
val content: String;
|
||||||
|
|
||||||
|
constructor(client: JSClient, obj: V8ValueObject) {
|
||||||
|
val contextName = "JSTextSegment";
|
||||||
|
textType = TextType.fromInt((obj.getOrDefault<Int>(client.config, "textType", contextName, null) ?: 0));
|
||||||
|
content = obj.getOrDefault(client.config, "content", contextName, "") ?: "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class JSImagesSegment: IJSArticleSegment {
|
||||||
|
override val type = SegmentType.IMAGES;
|
||||||
|
val images: List<String>;
|
||||||
|
val caption: String;
|
||||||
|
|
||||||
|
constructor(client: JSClient, obj: V8ValueObject) {
|
||||||
|
val contextName = "JSTextSegment";
|
||||||
|
images = obj.getOrThrowNullableList<String>(client.config, "images", contextName) ?: listOf();
|
||||||
|
caption = obj.getOrDefault(client.config, "caption", contextName, "") ?: "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class JSNestedSegment: IJSArticleSegment {
|
||||||
|
override val type = SegmentType.NESTED;
|
||||||
|
val nested: IPlatformContent;
|
||||||
|
|
||||||
|
constructor(client: JSClient, obj: V8ValueObject) {
|
||||||
|
val contextName = "JSNestedSegment";
|
||||||
|
val nestedObj = obj.getOrThrow<V8ValueObject>(client.config, "nested", contextName, false);
|
||||||
|
nested = IJSContent.fromV8(client, nestedObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
+10
-1
@@ -42,7 +42,12 @@ open class JSContent : IPlatformContent, IPluginSourced {
|
|||||||
|
|
||||||
id = PlatformID.fromV8(_pluginConfig, _content.getOrThrow(config, "id", contextName));
|
id = PlatformID.fromV8(_pluginConfig, _content.getOrThrow(config, "id", contextName));
|
||||||
name = HtmlCompat.fromHtml(_content.getOrThrow<String>(config, "name", contextName).decodeUnicode(), HtmlCompat.FROM_HTML_MODE_LEGACY).toString();
|
name = HtmlCompat.fromHtml(_content.getOrThrow<String>(config, "name", contextName).decodeUnicode(), HtmlCompat.FROM_HTML_MODE_LEGACY).toString();
|
||||||
author = PlatformAuthorLink.fromV8(_pluginConfig, _content.getOrThrow(config, "author", contextName));
|
|
||||||
|
val authorObj = _content.getOrDefault<V8ValueObject>(config, "author", contextName, null);
|
||||||
|
if(authorObj != null)
|
||||||
|
author = PlatformAuthorLink.fromV8(_pluginConfig, authorObj);
|
||||||
|
else
|
||||||
|
author = PlatformAuthorLink.UNKNOWN;
|
||||||
|
|
||||||
val datetimeInt = _content.getOrThrow<Int>(config, "datetime", contextName).toLong();
|
val datetimeInt = _content.getOrThrow<Int>(config, "datetime", contextName).toLong();
|
||||||
if(datetimeInt == 0.toLong())
|
if(datetimeInt == 0.toLong())
|
||||||
@@ -54,4 +59,8 @@ open class JSContent : IPlatformContent, IPluginSourced {
|
|||||||
|
|
||||||
_hasGetDetails = _content.has("getDetails");
|
_hasGetDetails = _content.has("getDetails");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getUnderlyingObject(): V8ValueObject? {
|
||||||
|
return _content;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -71,6 +71,8 @@ abstract class JSPager<T> : IPager<T> {
|
|||||||
|
|
||||||
warnIfMainThread("JSPager.getResults");
|
warnIfMainThread("JSPager.getResults");
|
||||||
val items = pager.getOrThrow<V8ValueArray>(config, "results", "JSPager");
|
val items = pager.getOrThrow<V8ValueArray>(config, "results", "JSPager");
|
||||||
|
if(items.v8Runtime.isDead || items.v8Runtime.isClosed)
|
||||||
|
throw IllegalStateException("Runtime closed");
|
||||||
val newResults = items.toArray()
|
val newResults = items.toArray()
|
||||||
.map { convertResult(it as V8ValueObject) }
|
.map { convertResult(it as V8ValueObject) }
|
||||||
.toList();
|
.toList();
|
||||||
|
|||||||
+2
-1
@@ -10,6 +10,7 @@ import com.futo.platformplayer.api.media.structures.IPager
|
|||||||
import com.futo.platformplayer.api.media.structures.ReusablePager
|
import com.futo.platformplayer.api.media.structures.ReusablePager
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
import com.futo.platformplayer.models.Playlist
|
import com.futo.platformplayer.models.Playlist
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
class JSPlaylistDetails: JSPlaylist, IPlatformPlaylistDetails {
|
class JSPlaylistDetails: JSPlaylist, IPlatformPlaylistDetails {
|
||||||
override val contents: IPager<IPlatformVideo>;
|
override val contents: IPager<IPlatformVideo>;
|
||||||
@@ -37,6 +38,6 @@ class JSPlaylistDetails: JSPlaylist, IPlatformPlaylistDetails {
|
|||||||
onProgress?.invoke(videos.size);
|
onProgress?.invoke(videos.size);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Playlist(id.toString(), name, videos.map { SerializedPlatformVideo.fromVideo(it)});
|
return Playlist(UUID.randomUUID().toString(), name, videos.map { SerializedPlatformVideo.fromVideo(it)});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+134
@@ -0,0 +1,134 @@
|
|||||||
|
package com.futo.platformplayer.api.media.platforms.js.models
|
||||||
|
|
||||||
|
import com.caoccao.javet.values.V8Value
|
||||||
|
import com.caoccao.javet.values.primitive.V8ValueString
|
||||||
|
import com.caoccao.javet.values.primitive.V8ValueUndefined
|
||||||
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
|
import com.caoccao.javet.values.reference.V8ValueTypedArray
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
|
import com.futo.platformplayer.engine.exceptions.PluginException
|
||||||
|
import com.futo.platformplayer.engine.exceptions.ScriptException
|
||||||
|
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
||||||
|
import com.futo.platformplayer.getOrDefault
|
||||||
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.states.StateDeveloper
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import java.util.Base64
|
||||||
|
|
||||||
|
class JSRequestExecutor {
|
||||||
|
private val _plugin: JSClient;
|
||||||
|
private val _config: IV8PluginConfig;
|
||||||
|
private var _executor: V8ValueObject;
|
||||||
|
val urlPrefix: String?;
|
||||||
|
|
||||||
|
private val hasCleanup: Boolean;
|
||||||
|
|
||||||
|
constructor(plugin: JSClient, executor: V8ValueObject) {
|
||||||
|
this._plugin = plugin;
|
||||||
|
this._executor = executor;
|
||||||
|
this._config = plugin.config;
|
||||||
|
val config = plugin.config;
|
||||||
|
|
||||||
|
urlPrefix = executor.getOrDefault(config, "urlPrefix", "RequestExecutor", null);
|
||||||
|
|
||||||
|
if(!executor.has("executeRequest"))
|
||||||
|
throw ScriptImplementationException(config, "RequestExecutor is missing executeRequest", null);
|
||||||
|
hasCleanup = executor.has("cleanup");
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: Executor properties?
|
||||||
|
@Throws(ScriptException::class)
|
||||||
|
open fun executeRequest(method: String, url: String, body: ByteArray?, headers: Map<String, String>): ByteArray {
|
||||||
|
if (_executor.isClosed)
|
||||||
|
throw IllegalStateException("Executor object is closed");
|
||||||
|
|
||||||
|
val result = if(_plugin is DevJSClient)
|
||||||
|
StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") {
|
||||||
|
V8Plugin.catchScriptErrors<Any>(
|
||||||
|
_config,
|
||||||
|
"[${_config.name}] JSRequestExecutor",
|
||||||
|
"builder.modifyRequest()"
|
||||||
|
) {
|
||||||
|
_executor.invoke("executeRequest", url, headers, method, body);
|
||||||
|
} as V8Value;
|
||||||
|
}
|
||||||
|
else V8Plugin.catchScriptErrors<Any>(
|
||||||
|
_config,
|
||||||
|
"[${_config.name}] JSRequestExecutor",
|
||||||
|
"builder.modifyRequest()"
|
||||||
|
) {
|
||||||
|
_executor.invoke("executeRequest", url, headers, method, body);
|
||||||
|
} as V8Value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if(result is V8ValueString) {
|
||||||
|
val base64Result = Base64.getDecoder().decode(result.value);
|
||||||
|
return base64Result;
|
||||||
|
}
|
||||||
|
if(result is V8ValueTypedArray) {
|
||||||
|
val buffer = result.buffer;
|
||||||
|
val byteBuffer = buffer.byteBuffer;
|
||||||
|
val bytesResult = ByteArray(result.byteLength);
|
||||||
|
byteBuffer.get(bytesResult, 0, result.byteLength);
|
||||||
|
buffer.close();
|
||||||
|
return bytesResult;
|
||||||
|
}
|
||||||
|
if(result is V8ValueObject && result.has("type")) {
|
||||||
|
val type = result.getOrThrow<Int>(_config, "type", "JSRequestModifier");
|
||||||
|
when(type) {
|
||||||
|
//TODO: Buffer type?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(result is V8ValueUndefined) {
|
||||||
|
if(_plugin is DevJSClient)
|
||||||
|
StateDeveloper.instance.logDevException(_plugin.devID, "JSRequestExecutor.executeRequest returned illegal undefined");
|
||||||
|
throw ScriptImplementationException(_config, "JSRequestExecutor.executeRequest returned illegal undefined", null);
|
||||||
|
}
|
||||||
|
throw NotImplementedError("Executor result type not implemented? " + result.javaClass.name);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
result.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
open fun cleanup() {
|
||||||
|
if (!hasCleanup || _executor.isClosed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if(_plugin is DevJSClient)
|
||||||
|
StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") {
|
||||||
|
V8Plugin.catchScriptErrors<Any>(
|
||||||
|
_config,
|
||||||
|
"[${_config.name}] JSRequestExecutor",
|
||||||
|
"builder.modifyRequest()"
|
||||||
|
) {
|
||||||
|
_executor.invokeVoid("cleanup", null);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else V8Plugin.catchScriptErrors<Any>(
|
||||||
|
_config,
|
||||||
|
"[${_config.name}] JSRequestExecutor",
|
||||||
|
"builder.modifyRequest()"
|
||||||
|
) {
|
||||||
|
_executor.invokeVoid("cleanup", null);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun finalize() {
|
||||||
|
cleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: are these available..?
|
||||||
|
@Serializable
|
||||||
|
class ExecutorParameters {
|
||||||
|
var rangeStart: Int = -1;
|
||||||
|
var rangeEnd: Int = -1;
|
||||||
|
|
||||||
|
var segment: Int = -1;
|
||||||
|
}
|
||||||
+20
-3
@@ -3,22 +3,39 @@ package com.futo.platformplayer.api.media.platforms.js.models.sources
|
|||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlWidevineSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlWidevineSource
|
||||||
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.models.JSRequestExecutor
|
||||||
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
|
||||||
class JSAudioUrlWidevineSource : JSAudioUrlSource, IAudioUrlWidevineSource {
|
class JSAudioUrlWidevineSource : JSAudioUrlSource, IAudioUrlWidevineSource {
|
||||||
override val bearerToken: String
|
|
||||||
override val licenseUri: String
|
override val licenseUri: String
|
||||||
|
override val hasLicenseRequestExecutor: Boolean
|
||||||
|
|
||||||
@Suppress("ConvertSecondaryConstructorToPrimary")
|
@Suppress("ConvertSecondaryConstructorToPrimary")
|
||||||
constructor(plugin: JSClient, obj: V8ValueObject) : super(plugin, obj) {
|
constructor(plugin: JSClient, obj: V8ValueObject) : super(plugin, obj) {
|
||||||
val contextName = "JSAudioUrlWidevineSource"
|
val contextName = "JSAudioUrlWidevineSource"
|
||||||
val config = plugin.config
|
val config = plugin.config
|
||||||
bearerToken = _obj.getOrThrow(config, "bearerToken", contextName)
|
|
||||||
licenseUri = _obj.getOrThrow(config, "licenseUri", contextName)
|
licenseUri = _obj.getOrThrow(config, "licenseUri", contextName)
|
||||||
|
hasLicenseRequestExecutor = obj.has("getLicenseRequestExecutor")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLicenseRequestExecutor(): JSRequestExecutor? {
|
||||||
|
if (!hasLicenseRequestExecutor || _obj.isClosed)
|
||||||
|
return null
|
||||||
|
|
||||||
|
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSAudioUrlWidevineSource", "obj.getLicenseRequestExecutor()") {
|
||||||
|
_obj.invoke("getLicenseRequestExecutor", arrayOf<Any>())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result !is V8ValueObject)
|
||||||
|
return null
|
||||||
|
|
||||||
|
return JSRequestExecutor(_plugin, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
val url = getAudioUrl()
|
val url = getAudioUrl()
|
||||||
return "(name=$name, container=$container, bitrate=$bitrate, codec=$codec, url=$url, language=$language, duration=$duration, bearerToken=$bearerToken, licenseUri=$licenseUri)"
|
return "(name=$name, container=$container, bitrate=$bitrate, codec=$codec, url=$url, language=$language, duration=$duration, hasLicenseRequestExecutor=${hasLicenseRequestExecutor}, licenseUri=$licenseUri)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
package com.futo.platformplayer.api.media.platforms.js.models.sources
|
||||||
|
|
||||||
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
|
import com.futo.platformplayer.getOrDefault
|
||||||
|
import com.futo.platformplayer.getOrNull
|
||||||
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
import com.futo.platformplayer.others.Language
|
||||||
|
import com.futo.platformplayer.states.StateDeveloper
|
||||||
|
|
||||||
|
class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawSource {
|
||||||
|
override val container : String = "application/dash+xml";
|
||||||
|
override val name : String;
|
||||||
|
override val codec: String;
|
||||||
|
override val bitrate: Int;
|
||||||
|
override val duration: Long;
|
||||||
|
override val priority: Boolean;
|
||||||
|
|
||||||
|
override val language: String;
|
||||||
|
|
||||||
|
val url: String;
|
||||||
|
override var manifest: String?;
|
||||||
|
|
||||||
|
override val hasGenerate: Boolean;
|
||||||
|
|
||||||
|
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH_RAW, plugin, obj) {
|
||||||
|
val contextName = "DashRawSource";
|
||||||
|
val config = plugin.config;
|
||||||
|
name = _obj.getOrThrow(config, "name", contextName);
|
||||||
|
url = _obj.getOrThrow(config, "url", contextName);
|
||||||
|
manifest = _obj.getOrThrow(config, "manifest", contextName);
|
||||||
|
codec = _obj.getOrDefault(config, "codec", contextName, "") ?: "";
|
||||||
|
bitrate = _obj.getOrDefault(config, "bitrate", contextName, 0) ?: 0;
|
||||||
|
duration = _obj.getOrDefault(config, "duration", contextName, 0) ?: 0;
|
||||||
|
priority = _obj.getOrDefault(config, "priority", contextName, false) ?: false;
|
||||||
|
language = _obj.getOrDefault(config, "language", contextName, Language.UNKNOWN) ?: Language.UNKNOWN;
|
||||||
|
hasGenerate = _obj.has("generate");
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun generate(): String? {
|
||||||
|
if(!hasGenerate)
|
||||||
|
return manifest;
|
||||||
|
if(_obj.isClosed)
|
||||||
|
throw IllegalStateException("Source object already closed");
|
||||||
|
|
||||||
|
val plugin = _plugin.getUnderlyingPlugin();
|
||||||
|
if(_plugin is DevJSClient)
|
||||||
|
return StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRaw", false) {
|
||||||
|
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
|
||||||
|
_obj.invokeString("generate");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
|
||||||
|
_obj.invokeString("generate");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+114
@@ -0,0 +1,114 @@
|
|||||||
|
package com.futo.platformplayer.api.media.platforms.js.models.sources
|
||||||
|
|
||||||
|
import com.caoccao.javet.values.V8Value
|
||||||
|
import com.caoccao.javet.values.primitive.V8ValueString
|
||||||
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
|
import com.futo.platformplayer.getOrDefault
|
||||||
|
import com.futo.platformplayer.getOrNull
|
||||||
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
import com.futo.platformplayer.states.StateDeveloper
|
||||||
|
|
||||||
|
interface IJSDashManifestRawSource {
|
||||||
|
val hasGenerate: Boolean;
|
||||||
|
var manifest: String?;
|
||||||
|
fun generate(): String?;
|
||||||
|
}
|
||||||
|
open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSource {
|
||||||
|
override val container : String = "application/dash+xml";
|
||||||
|
override val name : String;
|
||||||
|
override val width: Int;
|
||||||
|
override val height: Int;
|
||||||
|
override val codec: String;
|
||||||
|
override val bitrate: Int?;
|
||||||
|
override val duration: Long;
|
||||||
|
override val priority: Boolean;
|
||||||
|
|
||||||
|
var url: String?;
|
||||||
|
override var manifest: String?;
|
||||||
|
|
||||||
|
override val hasGenerate: Boolean;
|
||||||
|
val canMerge: Boolean;
|
||||||
|
|
||||||
|
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH_RAW, plugin, obj) {
|
||||||
|
val contextName = "DashRawSource";
|
||||||
|
val config = plugin.config;
|
||||||
|
name = _obj.getOrThrow(config, "name", contextName);
|
||||||
|
url = _obj.getOrThrow(config, "url", contextName);
|
||||||
|
manifest = _obj.getOrDefault<String>(config, "manifest", contextName, null);
|
||||||
|
width = _obj.getOrDefault(config, "width", contextName, 0) ?: 0;
|
||||||
|
height = _obj.getOrDefault(config, "height", contextName, 0) ?: 0;
|
||||||
|
codec = _obj.getOrDefault(config, "codec", contextName, "") ?: "";
|
||||||
|
bitrate = _obj.getOrDefault(config, "bitrate", contextName, 0) ?: 0;
|
||||||
|
duration = _obj.getOrDefault(config, "duration", contextName, 0) ?: 0;
|
||||||
|
priority = _obj.getOrDefault(config, "priority", contextName, false) ?: false;
|
||||||
|
canMerge = _obj.getOrDefault(config, "canMerge", contextName, false) ?: false;
|
||||||
|
hasGenerate = _obj.has("generate");
|
||||||
|
}
|
||||||
|
|
||||||
|
override open fun generate(): String? {
|
||||||
|
if(!hasGenerate)
|
||||||
|
return manifest;
|
||||||
|
if(_obj.isClosed)
|
||||||
|
throw IllegalStateException("Source object already closed");
|
||||||
|
if(_plugin is DevJSClient) {
|
||||||
|
return StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRawSource.generate()") {
|
||||||
|
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
|
||||||
|
_obj.invokeString("generate");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
|
||||||
|
_obj.invokeString("generate");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class JSDashManifestMergingRawSource(
|
||||||
|
val video: JSDashManifestRawSource,
|
||||||
|
val audio: JSDashManifestRawAudioSource): JSDashManifestRawSource(video.getUnderlyingPlugin()!!, video.getUnderlyingObject()!!), IVideoSource {
|
||||||
|
|
||||||
|
override val name: String
|
||||||
|
get() = video.name;
|
||||||
|
override val bitrate: Int
|
||||||
|
get() = (video.bitrate ?: 0) + audio.bitrate;
|
||||||
|
override val codec: String
|
||||||
|
get() = video.codec
|
||||||
|
override val container: String
|
||||||
|
get() = video.container
|
||||||
|
override val duration: Long
|
||||||
|
get() = video.duration;
|
||||||
|
override val height: Int
|
||||||
|
get() = video.height;
|
||||||
|
override val width: Int
|
||||||
|
get() = video.width;
|
||||||
|
override val priority: Boolean
|
||||||
|
get() = video.priority;
|
||||||
|
|
||||||
|
override fun generate(): String? {
|
||||||
|
val videoDash = video.generate();
|
||||||
|
val audioDash = audio.generate();
|
||||||
|
if(videoDash != null && audioDash == null) return videoDash;
|
||||||
|
if(audioDash != null && videoDash == null) return audioDash;
|
||||||
|
if(videoDash == null) return null;
|
||||||
|
|
||||||
|
//TODO: Temporary simple solution..make more reliable version
|
||||||
|
val audioAdaptationSet = adaptationSetRegex.find(audioDash!!);
|
||||||
|
if(audioAdaptationSet != null) {
|
||||||
|
return videoDash.replace("</AdaptationSet>", "</AdaptationSet>\n" + audioAdaptationSet.value)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return videoDash;
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val adaptationSetRegex = Regex("<AdaptationSet.*?>.*?<\\/AdaptationSet>", RegexOption.DOT_MATCHES_ALL);
|
||||||
|
}
|
||||||
|
}
|
||||||
+60
@@ -0,0 +1,60 @@
|
|||||||
|
package com.futo.platformplayer.api.media.platforms.js.models.sources
|
||||||
|
|
||||||
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestWidevineSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
|
||||||
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
|
import com.futo.platformplayer.getOrNull
|
||||||
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
|
||||||
|
class JSDashManifestWidevineSource : IVideoUrlSource, IDashManifestSource,
|
||||||
|
IDashManifestWidevineSource, JSSource {
|
||||||
|
override val width: Int = 0
|
||||||
|
override val height: Int = 0
|
||||||
|
override val container: String = "application/dash+xml"
|
||||||
|
override val codec: String = "Dash"
|
||||||
|
override val name: String
|
||||||
|
override val bitrate: Int? = null
|
||||||
|
override val url: String
|
||||||
|
override val duration: Long
|
||||||
|
|
||||||
|
override var priority: Boolean = false
|
||||||
|
|
||||||
|
override val licenseUri: String
|
||||||
|
override val hasLicenseRequestExecutor: Boolean
|
||||||
|
|
||||||
|
@Suppress("ConvertSecondaryConstructorToPrimary")
|
||||||
|
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH, plugin, obj) {
|
||||||
|
val contextName = "DashWidevineSource"
|
||||||
|
val config = plugin.config
|
||||||
|
name = _obj.getOrThrow(config, "name", contextName)
|
||||||
|
url = _obj.getOrThrow(config, "url", contextName)
|
||||||
|
duration = _obj.getOrThrow(config, "duration", contextName)
|
||||||
|
|
||||||
|
priority = obj.getOrNull(config, "priority", contextName) ?: false
|
||||||
|
|
||||||
|
licenseUri = _obj.getOrThrow(config, "licenseUri", contextName)
|
||||||
|
hasLicenseRequestExecutor = obj.has("getLicenseRequestExecutor")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLicenseRequestExecutor(): JSRequestExecutor? {
|
||||||
|
if (!hasLicenseRequestExecutor || _obj.isClosed)
|
||||||
|
return null
|
||||||
|
|
||||||
|
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSDashManifestWidevineSource", "obj.getLicenseRequestExecutor()") {
|
||||||
|
_obj.invoke("getLicenseRequestExecutor", arrayOf<Any>())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result !is V8ValueObject)
|
||||||
|
return null
|
||||||
|
|
||||||
|
return JSRequestExecutor(_plugin, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getVideoUrl(): String {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
}
|
||||||
+57
-8
@@ -10,10 +10,12 @@ 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.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSRequest
|
import com.futo.platformplayer.api.media.platforms.js.models.JSRequest
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestModifier
|
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestModifier
|
||||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
import com.futo.platformplayer.engine.V8Plugin
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
import com.futo.platformplayer.getOrDefault
|
import com.futo.platformplayer.getOrDefault
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.orNull
|
import com.futo.platformplayer.orNull
|
||||||
import com.futo.platformplayer.views.video.datasources.JSHttpDataSource
|
import com.futo.platformplayer.views.video.datasources.JSHttpDataSource
|
||||||
|
|
||||||
@@ -21,9 +23,17 @@ abstract class JSSource {
|
|||||||
protected val _plugin: JSClient;
|
protected val _plugin: JSClient;
|
||||||
protected val _config: IV8PluginConfig;
|
protected val _config: IV8PluginConfig;
|
||||||
protected val _obj: V8ValueObject;
|
protected val _obj: V8ValueObject;
|
||||||
|
|
||||||
val hasRequestModifier: Boolean;
|
val hasRequestModifier: Boolean;
|
||||||
private val _requestModifier: JSRequest?;
|
private val _requestModifier: JSRequest?;
|
||||||
|
|
||||||
|
val hasRequestExecutor: Boolean;
|
||||||
|
private val _requestExecutor: JSRequest?;
|
||||||
|
|
||||||
|
val requiresCustomDatasource: Boolean get() {
|
||||||
|
return hasRequestModifier || hasRequestExecutor;
|
||||||
|
}
|
||||||
|
|
||||||
val type : String;
|
val type : String;
|
||||||
|
|
||||||
constructor(type: String, plugin: JSClient, obj: V8ValueObject) {
|
constructor(type: String, plugin: JSClient, obj: V8ValueObject) {
|
||||||
@@ -36,6 +46,11 @@ abstract class JSSource {
|
|||||||
JSRequest(plugin, it, null, null, true);
|
JSRequest(plugin, it, null, null, true);
|
||||||
}
|
}
|
||||||
hasRequestModifier = _requestModifier != null || obj.has("getRequestModifier");
|
hasRequestModifier = _requestModifier != null || obj.has("getRequestModifier");
|
||||||
|
|
||||||
|
_requestExecutor = obj.getOrDefault<V8ValueObject>(_config, "requestExecutor", "JSSource.requestExecutor", null)?.let {
|
||||||
|
JSRequest(plugin, it, null, null, true);
|
||||||
|
}
|
||||||
|
hasRequestExecutor = _requestExecutor != null || obj.has("getRequestExecutor");
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getRequestModifier(): IRequestModifier? {
|
fun getRequestModifier(): IRequestModifier? {
|
||||||
@@ -44,20 +59,38 @@ abstract class JSSource {
|
|||||||
return@AdhocRequestModifier _requestModifier.modify(_plugin, url, headers);
|
return@AdhocRequestModifier _requestModifier.modify(_plugin, url, headers);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!hasRequestModifier || _obj.isClosed) {
|
if (!hasRequestModifier || _obj.isClosed)
|
||||||
return null;
|
return null;
|
||||||
}
|
|
||||||
|
|
||||||
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSVideoUrlSource", "obj.getRequestModifier()") {
|
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSVideoUrlSource", "obj.getRequestModifier()") {
|
||||||
_obj.invoke("getRequestModifier", arrayOf<Any>());
|
_obj.invoke("getRequestModifier", arrayOf<Any>());
|
||||||
};
|
};
|
||||||
|
|
||||||
if (result !is V8ValueObject) {
|
if (result !is V8ValueObject)
|
||||||
return null;
|
return null;
|
||||||
}
|
|
||||||
|
|
||||||
return JSRequestModifier(_plugin, result)
|
return JSRequestModifier(_plugin, result)
|
||||||
}
|
}
|
||||||
|
open fun getRequestExecutor(): JSRequestExecutor? {
|
||||||
|
if (!hasRequestExecutor || _obj.isClosed)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSSource", "obj.getRequestExecutor()") {
|
||||||
|
_obj.invoke("getRequestExecutor", arrayOf<Any>());
|
||||||
|
};
|
||||||
|
|
||||||
|
if (result !is V8ValueObject)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return JSRequestExecutor(_plugin, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getUnderlyingPlugin(): JSClient? {
|
||||||
|
return _plugin;
|
||||||
|
}
|
||||||
|
fun getUnderlyingObject(): V8ValueObject? {
|
||||||
|
return _obj;
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val TYPE_AUDIOURL = "AudioUrlSource";
|
const val TYPE_AUDIOURL = "AudioUrlSource";
|
||||||
@@ -65,33 +98,49 @@ abstract class JSSource {
|
|||||||
const val TYPE_AUDIO_WITH_METADATA = "AudioUrlRangeSource";
|
const val TYPE_AUDIO_WITH_METADATA = "AudioUrlRangeSource";
|
||||||
const val TYPE_VIDEO_WITH_METADATA = "VideoUrlRangeSource";
|
const val TYPE_VIDEO_WITH_METADATA = "VideoUrlRangeSource";
|
||||||
const val TYPE_DASH = "DashSource";
|
const val TYPE_DASH = "DashSource";
|
||||||
|
const val TYPE_DASH_WIDEVINE = "DashWidevineSource";
|
||||||
|
const val TYPE_DASH_RAW = "DashRawSource";
|
||||||
|
const val TYPE_DASH_RAW_AUDIO = "DashRawAudioSource";
|
||||||
const val TYPE_HLS = "HLSSource";
|
const val TYPE_HLS = "HLSSource";
|
||||||
const val TYPE_AUDIOURL_WIDEVINE = "AudioUrlWidevineSource"
|
const val TYPE_AUDIOURL_WIDEVINE = "AudioUrlWidevineSource"
|
||||||
|
const val TYPE_VIDEOURL_WIDEVINE = "VideoUrlWidevineSource"
|
||||||
|
|
||||||
fun fromV8VideoNullable(plugin: JSClient, obj: V8Value?) : IVideoSource? = obj.orNull { fromV8Video(plugin, it as V8ValueObject) };
|
fun fromV8VideoNullable(plugin: JSClient, obj: V8Value?) : IVideoSource? = obj.orNull { fromV8Video(plugin, it as V8ValueObject) };
|
||||||
fun fromV8Video(plugin: JSClient, obj: V8ValueObject) : IVideoSource {
|
fun fromV8Video(plugin: JSClient, obj: V8ValueObject) : IVideoSource? {
|
||||||
val type = obj.getString("plugin_type");
|
val type = obj.getString("plugin_type");
|
||||||
return when(type) {
|
return when(type) {
|
||||||
TYPE_VIDEOURL -> JSVideoUrlSource(plugin, obj);
|
TYPE_VIDEOURL -> JSVideoUrlSource(plugin, obj);
|
||||||
|
TYPE_VIDEOURL_WIDEVINE -> JSVideoUrlWidevineSource(plugin, obj);
|
||||||
TYPE_VIDEO_WITH_METADATA -> JSVideoUrlRangeSource(plugin, obj);
|
TYPE_VIDEO_WITH_METADATA -> JSVideoUrlRangeSource(plugin, obj);
|
||||||
TYPE_HLS -> fromV8HLS(plugin, obj);
|
TYPE_HLS -> fromV8HLS(plugin, obj);
|
||||||
|
TYPE_DASH_WIDEVINE -> JSDashManifestWidevineSource(plugin, obj)
|
||||||
TYPE_DASH -> fromV8Dash(plugin, obj);
|
TYPE_DASH -> fromV8Dash(plugin, obj);
|
||||||
else -> throw NotImplementedError("Unknown type ${type}");
|
TYPE_DASH_RAW -> fromV8DashRaw(plugin, obj);
|
||||||
|
else -> {
|
||||||
|
Logger.w("JSSource", "Unknown video type ${type}");
|
||||||
|
null;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun fromV8DashNullable(plugin: JSClient, obj: V8Value?) : JSDashManifestSource? = obj.orNull { fromV8Dash(plugin, it as V8ValueObject) };
|
fun fromV8DashNullable(plugin: JSClient, obj: V8Value?) : JSDashManifestSource? = obj.orNull { fromV8Dash(plugin, it as V8ValueObject) };
|
||||||
fun fromV8Dash(plugin: JSClient, obj: V8ValueObject) : JSDashManifestSource = JSDashManifestSource(plugin, obj);
|
fun fromV8Dash(plugin: JSClient, obj: V8ValueObject) : JSDashManifestSource = JSDashManifestSource(plugin, obj);
|
||||||
|
fun fromV8DashRaw(plugin: JSClient, obj: V8ValueObject) : JSDashManifestRawSource = JSDashManifestRawSource(plugin, obj);
|
||||||
|
fun fromV8DashRawAudio(plugin: JSClient, obj: V8ValueObject) : JSDashManifestRawAudioSource = JSDashManifestRawAudioSource(plugin, obj);
|
||||||
fun fromV8HLSNullable(plugin: JSClient, obj: V8Value?) : JSHLSManifestSource? = obj.orNull { fromV8HLS(plugin, it as V8ValueObject) };
|
fun fromV8HLSNullable(plugin: JSClient, obj: V8Value?) : JSHLSManifestSource? = obj.orNull { fromV8HLS(plugin, it as V8ValueObject) };
|
||||||
fun fromV8HLS(plugin: JSClient, obj: V8ValueObject) : JSHLSManifestSource = JSHLSManifestSource(plugin, obj);
|
fun fromV8HLS(plugin: JSClient, obj: V8ValueObject) : JSHLSManifestSource = JSHLSManifestSource(plugin, obj);
|
||||||
|
|
||||||
fun fromV8Audio(plugin: JSClient, obj: V8ValueObject) : IAudioSource {
|
fun fromV8Audio(plugin: JSClient, obj: V8ValueObject) : IAudioSource? {
|
||||||
val type = obj.getString("plugin_type");
|
val type = obj.getString("plugin_type");
|
||||||
return when(type) {
|
return when(type) {
|
||||||
TYPE_HLS -> JSHLSManifestAudioSource.fromV8HLS(plugin, obj);
|
TYPE_HLS -> JSHLSManifestAudioSource.fromV8HLS(plugin, obj);
|
||||||
TYPE_AUDIOURL -> JSAudioUrlSource(plugin, obj);
|
TYPE_AUDIOURL -> JSAudioUrlSource(plugin, obj);
|
||||||
|
TYPE_DASH_RAW_AUDIO -> fromV8DashRawAudio(plugin, obj);
|
||||||
TYPE_AUDIOURL_WIDEVINE -> JSAudioUrlWidevineSource(plugin, obj);
|
TYPE_AUDIOURL_WIDEVINE -> JSAudioUrlWidevineSource(plugin, obj);
|
||||||
TYPE_AUDIO_WITH_METADATA -> JSAudioUrlRangeSource(plugin, obj);
|
TYPE_AUDIO_WITH_METADATA -> JSAudioUrlRangeSource(plugin, obj);
|
||||||
else -> throw NotImplementedError("Unknown type ${type}");
|
else -> {
|
||||||
|
Logger.w("JSSource", "Unknown audio type ${type}");
|
||||||
|
null;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -23,9 +23,11 @@ class JSUnMuxVideoSourceDescriptor: VideoUnMuxedSourceDescriptor {
|
|||||||
this.isUnMuxed = obj.getOrThrow(config, "isUnMuxed", contextName);
|
this.isUnMuxed = obj.getOrThrow(config, "isUnMuxed", contextName);
|
||||||
this.videoSources = obj.getOrThrow<V8ValueArray>(config, "videoSources", contextName).toArray()
|
this.videoSources = obj.getOrThrow<V8ValueArray>(config, "videoSources", contextName).toArray()
|
||||||
.map { JSSource.fromV8Video(plugin, it as V8ValueObject) }
|
.map { JSSource.fromV8Video(plugin, it as V8ValueObject) }
|
||||||
|
.filterNotNull()
|
||||||
.toTypedArray();
|
.toTypedArray();
|
||||||
this.audioSources = obj.getOrThrow<V8ValueArray>(config, "audioSources", contextName).toArray()
|
this.audioSources = obj.getOrThrow<V8ValueArray>(config, "audioSources", contextName).toArray()
|
||||||
.map { JSSource.fromV8Audio(plugin, it as V8ValueObject) }
|
.map { JSSource.fromV8Audio(plugin, it as V8ValueObject) }
|
||||||
|
.filterNotNull()
|
||||||
.toTypedArray();
|
.toTypedArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+1
@@ -21,6 +21,7 @@ class JSVideoSourceDescriptor : VideoMuxedSourceDescriptor {
|
|||||||
this.isUnMuxed = obj.getOrThrow(config, "isUnMuxed", contextName);
|
this.isUnMuxed = obj.getOrThrow(config, "isUnMuxed", contextName);
|
||||||
this.videoSources = obj.getOrThrow<V8ValueArray>(config, "videoSources", contextName).toArray()
|
this.videoSources = obj.getOrThrow<V8ValueArray>(config, "videoSources", contextName).toArray()
|
||||||
.map { JSSource.fromV8Video(plugin, it as V8ValueObject) }
|
.map { JSSource.fromV8Video(plugin, it as V8ValueObject) }
|
||||||
|
.filterNotNull()
|
||||||
.toTypedArray();
|
.toTypedArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
package com.futo.platformplayer.api.media.platforms.js.models.sources
|
||||||
|
|
||||||
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlWidevineSource
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
|
||||||
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
|
||||||
|
class JSVideoUrlWidevineSource : JSVideoUrlSource, IVideoUrlWidevineSource {
|
||||||
|
override val licenseUri: String
|
||||||
|
override val hasLicenseRequestExecutor: Boolean
|
||||||
|
|
||||||
|
@Suppress("ConvertSecondaryConstructorToPrimary")
|
||||||
|
constructor(plugin: JSClient, obj: V8ValueObject) : super(plugin, obj) {
|
||||||
|
val contextName = "JSAudioUrlWidevineSource"
|
||||||
|
val config = plugin.config
|
||||||
|
|
||||||
|
licenseUri = _obj.getOrThrow(config, "licenseUri", contextName)
|
||||||
|
hasLicenseRequestExecutor = obj.has("getLicenseRequestExecutor")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLicenseRequestExecutor(): JSRequestExecutor? {
|
||||||
|
if (!hasLicenseRequestExecutor || _obj.isClosed)
|
||||||
|
return null
|
||||||
|
|
||||||
|
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSAudioUrlWidevineSource", "obj.getLicenseRequestExecutor()") {
|
||||||
|
_obj.invoke("getLicenseRequestExecutor", arrayOf<Any>())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result !is V8ValueObject)
|
||||||
|
return null
|
||||||
|
|
||||||
|
return JSRequestExecutor(_plugin, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
val url = getVideoUrl()
|
||||||
|
return "(width=$width, height=$height, container=$container, codec=$codec, name=$name, bitrate=$bitrate, duration=$duration, url=$url, hasLicenseRequestExecutor=$hasLicenseRequestExecutor, licenseUri=$licenseUri)"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -88,7 +88,8 @@ class DashBuilder : XMLBuilder {
|
|||||||
fun withRepresentationOnDemand(id: String, subtitleSource: ISubtitleSource, subtitleUrl: String) {
|
fun withRepresentationOnDemand(id: String, subtitleSource: ISubtitleSource, subtitleUrl: String) {
|
||||||
withRepresentation(id, mapOf(
|
withRepresentation(id, mapOf(
|
||||||
Pair("mimeType", subtitleSource.format ?: "text/vtt"),
|
Pair("mimeType", subtitleSource.format ?: "text/vtt"),
|
||||||
Pair("startWithSAP", "1"),
|
Pair("default", "true"),
|
||||||
|
Pair("lang", "en"),
|
||||||
Pair("bandwidth", "1000")
|
Pair("bandwidth", "1000")
|
||||||
)) {
|
)) {
|
||||||
it.withBaseURL(subtitleUrl)
|
it.withBaseURL(subtitleUrl)
|
||||||
@@ -151,7 +152,7 @@ class DashBuilder : XMLBuilder {
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
//TODO: Verify if & really should be replaced like this?
|
//TODO: Verify if & really should be replaced like this?
|
||||||
it.withRepresentationOnDemand("1", subtitleSource, subtitleUrl.replace("&", "&"))
|
it.withRepresentationOnDemand("caption_en", subtitleSource, subtitleUrl.replace("&", "&"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//Video
|
//Video
|
||||||
@@ -164,7 +165,7 @@ class DashBuilder : XMLBuilder {
|
|||||||
Pair("subsegmentStartsWithSAP", "1")
|
Pair("subsegmentStartsWithSAP", "1")
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
it.withRepresentationOnDemand("1", vidSource, vidUrl.replace("&", "&"));
|
it.withRepresentationOnDemand("2", vidSource, vidUrl.replace("&", "&"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,14 +6,17 @@ import android.net.Uri
|
|||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.futo.platformplayer.BuildConfig
|
import android.util.Xml
|
||||||
|
import androidx.annotation.OptIn
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
|
import com.futo.platformplayer.api.http.server.HttpHeaders
|
||||||
import com.futo.platformplayer.api.http.server.ManagedHttpServer
|
import com.futo.platformplayer.api.http.server.ManagedHttpServer
|
||||||
import com.futo.platformplayer.api.http.server.handlers.HttpConstantHandler
|
import com.futo.platformplayer.api.http.server.handlers.HttpConstantHandler
|
||||||
import com.futo.platformplayer.api.http.server.handlers.HttpFileHandler
|
import com.futo.platformplayer.api.http.server.handlers.HttpFileHandler
|
||||||
import com.futo.platformplayer.api.http.server.handlers.HttpFuntionHandler
|
import com.futo.platformplayer.api.http.server.handlers.HttpFunctionHandler
|
||||||
import com.futo.platformplayer.api.http.server.handlers.HttpProxyHandler
|
import com.futo.platformplayer.api.http.server.handlers.HttpProxyHandler
|
||||||
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.IAudioUrlSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
|
||||||
@@ -26,16 +29,23 @@ import com.futo.platformplayer.api.media.models.streams.sources.LocalSubtitleSou
|
|||||||
import com.futo.platformplayer.api.media.models.streams.sources.LocalVideoSource
|
import com.futo.platformplayer.api.media.models.streams.sources.LocalVideoSource
|
||||||
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.IPlatformVideoDetails
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestMergingRawSource
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawAudioSource
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawSource
|
||||||
import com.futo.platformplayer.builders.DashBuilder
|
import com.futo.platformplayer.builders.DashBuilder
|
||||||
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.exceptions.UnsupportedCastException
|
import com.futo.platformplayer.exceptions.UnsupportedCastException
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.mdns.DnsService
|
||||||
|
import com.futo.platformplayer.mdns.ServiceDiscoverer
|
||||||
import com.futo.platformplayer.models.CastingDeviceInfo
|
import com.futo.platformplayer.models.CastingDeviceInfo
|
||||||
import com.futo.platformplayer.parsers.HLS
|
import com.futo.platformplayer.parsers.HLS
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.stores.CastingDeviceInfoStorage
|
import com.futo.platformplayer.stores.CastingDeviceInfoStorage
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
|
import com.futo.platformplayer.toUrlAddress
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
@@ -43,17 +53,15 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
|
import java.net.URLDecoder
|
||||||
|
import java.net.URLEncoder
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import javax.jmdns.JmDNS
|
|
||||||
import javax.jmdns.ServiceEvent
|
|
||||||
import javax.jmdns.ServiceListener
|
|
||||||
import javax.jmdns.ServiceTypeListener
|
|
||||||
|
|
||||||
class StateCasting {
|
class StateCasting {
|
||||||
private val _scopeIO = CoroutineScope(Dispatchers.IO);
|
private val _scopeIO = CoroutineScope(Dispatchers.IO);
|
||||||
private val _scopeMain = CoroutineScope(Dispatchers.Main);
|
private val _scopeMain = CoroutineScope(Dispatchers.Main);
|
||||||
private var _jmDNS: JmDNS? = null;
|
|
||||||
private val _storage: CastingDeviceInfoStorage = FragmentedStorage.get();
|
private val _storage: CastingDeviceInfoStorage = FragmentedStorage.get();
|
||||||
|
|
||||||
private val _castServer = ManagedHttpServer(9999);
|
private val _castServer = ManagedHttpServer(9999);
|
||||||
@@ -70,105 +78,51 @@ class StateCasting {
|
|||||||
val onActiveDeviceDurationChanged = Event1<Double>();
|
val onActiveDeviceDurationChanged = Event1<Double>();
|
||||||
val onActiveDeviceVolumeChanged = Event1<Double>();
|
val onActiveDeviceVolumeChanged = Event1<Double>();
|
||||||
var activeDevice: CastingDevice? = null;
|
var activeDevice: CastingDevice? = null;
|
||||||
|
private var _videoExecutor: JSRequestExecutor? = null
|
||||||
|
private var _audioExecutor: JSRequestExecutor? = null
|
||||||
private val _client = ManagedHttpClient();
|
private val _client = ManagedHttpClient();
|
||||||
var _resumeCastingDevice: CastingDeviceInfo? = null;
|
var _resumeCastingDevice: CastingDeviceInfo? = null;
|
||||||
|
val _serviceDiscoverer = ServiceDiscoverer(arrayOf(
|
||||||
|
"_googlecast._tcp.local",
|
||||||
|
"_airplay._tcp.local",
|
||||||
|
"_fastcast._tcp.local",
|
||||||
|
"_fcast._tcp.local"
|
||||||
|
)) { handleServiceUpdated(it) }
|
||||||
|
|
||||||
val isCasting: Boolean get() = activeDevice != null;
|
val isCasting: Boolean get() = activeDevice != null;
|
||||||
|
|
||||||
private val _chromecastServiceListener = object : ServiceListener {
|
private fun handleServiceUpdated(services: List<DnsService>) {
|
||||||
override fun serviceAdded(event: ServiceEvent) {
|
for (s in services) {
|
||||||
Logger.i(TAG, "ChromeCast service added: " + event.info);
|
//TODO: Addresses IPv4 only?
|
||||||
addOrUpdateDevice(event);
|
val addresses = s.addresses.toTypedArray()
|
||||||
}
|
val port = s.port.toInt()
|
||||||
|
var name = s.texts.firstOrNull { it.startsWith("md=") }?.substring("md=".length)
|
||||||
override fun serviceRemoved(event: ServiceEvent) {
|
if (s.name.endsWith("._googlecast._tcp.local")) {
|
||||||
Logger.i(TAG, "ChromeCast service removed: " + event.info);
|
if (name == null) {
|
||||||
synchronized(devices) {
|
name = s.name.substring(0, s.name.length - "._googlecast._tcp.local".length)
|
||||||
val device = devices[event.info.name];
|
|
||||||
if (device != null) {
|
|
||||||
onDeviceRemoved.emit(device);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun serviceResolved(event: ServiceEvent) {
|
addOrUpdateChromeCastDevice(name, addresses, port)
|
||||||
Logger.v(TAG, "ChromeCast service resolved: " + event.info);
|
} else if (s.name.endsWith("._airplay._tcp.local")) {
|
||||||
addOrUpdateDevice(event);
|
if (name == null) {
|
||||||
}
|
name = s.name.substring(0, s.name.length - "._airplay._tcp.local".length)
|
||||||
|
|
||||||
fun addOrUpdateDevice(event: ServiceEvent) {
|
|
||||||
addOrUpdateChromeCastDevice(event.info.name, event.info.inetAddresses, event.info.port);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val _airPlayServiceListener = object : ServiceListener {
|
|
||||||
override fun serviceAdded(event: ServiceEvent) {
|
|
||||||
Logger.i(TAG, "AirPlay service added: " + event.info);
|
|
||||||
addOrUpdateDevice(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun serviceRemoved(event: ServiceEvent) {
|
|
||||||
Logger.i(TAG, "AirPlay service removed: " + event.info);
|
|
||||||
synchronized(devices) {
|
|
||||||
val device = devices[event.info.name];
|
|
||||||
if (device != null) {
|
|
||||||
onDeviceRemoved.emit(device);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun serviceResolved(event: ServiceEvent) {
|
addOrUpdateAirPlayDevice(name, addresses, port)
|
||||||
Logger.i(TAG, "AirPlay service resolved: " + event.info);
|
} else if (s.name.endsWith("._fastcast._tcp.local")) {
|
||||||
addOrUpdateDevice(event);
|
if (name == null) {
|
||||||
}
|
name = s.name.substring(0, s.name.length - "._fastcast._tcp.local".length)
|
||||||
|
|
||||||
fun addOrUpdateDevice(event: ServiceEvent) {
|
|
||||||
addOrUpdateAirPlayDevice(event.info.name, event.info.inetAddresses, event.info.port);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val _fastCastServiceListener = object : ServiceListener {
|
|
||||||
override fun serviceAdded(event: ServiceEvent) {
|
|
||||||
Logger.i(TAG, "FastCast service added: " + event.info);
|
|
||||||
addOrUpdateDevice(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun serviceRemoved(event: ServiceEvent) {
|
|
||||||
Logger.i(TAG, "FastCast service removed: " + event.info);
|
|
||||||
synchronized(devices) {
|
|
||||||
val device = devices[event.info.name];
|
|
||||||
if (device != null) {
|
|
||||||
onDeviceRemoved.emit(device);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addOrUpdateFastCastDevice(name, addresses, port)
|
||||||
|
} else if (s.name.endsWith("._fcast._tcp.local")) {
|
||||||
|
if (name == null) {
|
||||||
|
name = s.name.substring(0, s.name.length - "._fcast._tcp.local".length)
|
||||||
|
}
|
||||||
|
|
||||||
|
addOrUpdateFastCastDevice(name, addresses, port)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun serviceResolved(event: ServiceEvent) {
|
|
||||||
Logger.i(TAG, "FastCast service resolved: " + event.info);
|
|
||||||
addOrUpdateDevice(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addOrUpdateDevice(event: ServiceEvent) {
|
|
||||||
addOrUpdateFastCastDevice(event.info.name, event.info.inetAddresses, event.info.port);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val _serviceTypeListener = object : ServiceTypeListener {
|
|
||||||
override fun serviceTypeAdded(event: ServiceEvent?) {
|
|
||||||
if (event == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.i(TAG, "Service type added (name: ${event.name}, type: ${event.type})");
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun subTypeForServiceTypeAdded(event: ServiceEvent?) {
|
|
||||||
if (event == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.i(TAG, "Sub type for service type added (name: ${event.name}, type: ${event.type})");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleUrl(context: Context, url: String) {
|
fun handleUrl(context: Context, url: String) {
|
||||||
@@ -237,29 +191,30 @@ class StateCasting {
|
|||||||
rememberedDevices.clear();
|
rememberedDevices.clear();
|
||||||
rememberedDevices.addAll(_storage.deviceInfos.map { deviceFromCastingDeviceInfo(it) });
|
rememberedDevices.addAll(_storage.deviceInfos.map { deviceFromCastingDeviceInfo(it) });
|
||||||
|
|
||||||
_scopeIO.launch {
|
|
||||||
try {
|
|
||||||
val jmDNS = JmDNS.create(InetAddress.getLocalHost());
|
|
||||||
jmDNS.addServiceListener("_googlecast._tcp.local.", _chromecastServiceListener);
|
|
||||||
jmDNS.addServiceListener("_airplay._tcp.local.", _airPlayServiceListener);
|
|
||||||
jmDNS.addServiceListener("_fastcast._tcp.local.", _fastCastServiceListener);
|
|
||||||
jmDNS.addServiceListener("_fcast._tcp.local.", _fastCastServiceListener);
|
|
||||||
|
|
||||||
if (BuildConfig.DEBUG) {
|
|
||||||
jmDNS.addServiceTypeListener(_serviceTypeListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
_jmDNS = jmDNS;
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.e(TAG, "Failed to start casting service.", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_castServer.start();
|
_castServer.start();
|
||||||
enableDeveloper(true);
|
enableDeveloper(true);
|
||||||
|
|
||||||
Logger.i(TAG, "CastingService started.");
|
Logger.i(TAG, "CastingService started.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun startDiscovering() {
|
||||||
|
try {
|
||||||
|
_serviceDiscoverer.start()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.i(TAG, "Failed to start ServiceDiscoverer", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun stopDiscovering() {
|
||||||
|
try {
|
||||||
|
_serviceDiscoverer.stop()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.i(TAG, "Failed to stop ServiceDiscoverer", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun stop() {
|
fun stop() {
|
||||||
if (!_started)
|
if (!_started)
|
||||||
@@ -269,25 +224,7 @@ class StateCasting {
|
|||||||
|
|
||||||
Logger.i(TAG, "CastingService stopping.")
|
Logger.i(TAG, "CastingService stopping.")
|
||||||
|
|
||||||
val jmDNS = _jmDNS;
|
stopDiscovering()
|
||||||
if (jmDNS != null) {
|
|
||||||
_scopeIO.launch {
|
|
||||||
try {
|
|
||||||
jmDNS.removeServiceListener("_googlecast._tcp.local.", _chromecastServiceListener);
|
|
||||||
jmDNS.removeServiceListener("_airplay._tcp", _airPlayServiceListener);
|
|
||||||
jmDNS.removeServiceListener("_fastcast._tcp.local.", _fastCastServiceListener);
|
|
||||||
|
|
||||||
if (BuildConfig.DEBUG) {
|
|
||||||
jmDNS.removeServiceTypeListener(_serviceTypeListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
jmDNS.close();
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.e(TAG, "Failed to stop mDNS.", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_scopeIO.cancel();
|
_scopeIO.cancel();
|
||||||
_scopeMain.cancel();
|
_scopeMain.cancel();
|
||||||
|
|
||||||
@@ -437,15 +374,26 @@ class StateCasting {
|
|||||||
} else {
|
} else {
|
||||||
StateApp.instance.scope.launch(Dispatchers.IO) {
|
StateApp.instance.scope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
if (ad is FCastCastingDevice) {
|
val isRawDash = videoSource is JSDashManifestRawSource || audioSource is JSDashManifestRawAudioSource
|
||||||
Logger.i(TAG, "Casting as DASH direct");
|
if (isRawDash) {
|
||||||
castDashDirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
|
Logger.i(TAG, "Casting as raw DASH");
|
||||||
} else if (ad is AirPlayCastingDevice) {
|
|
||||||
Logger.i(TAG, "Casting as HLS indirect");
|
try {
|
||||||
castHlsIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
|
castDashRaw(contentResolver, video, videoSource as JSDashManifestRawSource?, audioSource as JSDashManifestRawAudioSource?, subtitleSource, resumePosition, speed);
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to start casting DASH raw videoSource=${videoSource} audioSource=${audioSource}.", e);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Logger.i(TAG, "Casting as DASH indirect");
|
if (ad is FCastCastingDevice) {
|
||||||
castDashIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
|
Logger.i(TAG, "Casting as DASH direct");
|
||||||
|
castDashDirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
|
||||||
|
} else if (ad is AirPlayCastingDevice) {
|
||||||
|
Logger.i(TAG, "Casting as HLS indirect");
|
||||||
|
castHlsIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
|
||||||
|
} else {
|
||||||
|
Logger.i(TAG, "Casting as DASH indirect");
|
||||||
|
castDashIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to start casting DASH videoSource=${videoSource} audioSource=${audioSource}.", e);
|
Logger.e(TAG, "Failed to start casting DASH videoSource=${videoSource} audioSource=${audioSource}.", e);
|
||||||
@@ -454,7 +402,7 @@ class StateCasting {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val proxyStreams = Settings.instance.casting.alwaysProxyRequests;
|
val proxyStreams = Settings.instance.casting.alwaysProxyRequests;
|
||||||
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
|
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
|
|
||||||
if (videoSource is IVideoUrlSource) {
|
if (videoSource is IVideoUrlSource) {
|
||||||
@@ -489,6 +437,26 @@ class StateCasting {
|
|||||||
} else if (audioSource is LocalAudioSource) {
|
} else if (audioSource is LocalAudioSource) {
|
||||||
Logger.i(TAG, "Casting as local audio");
|
Logger.i(TAG, "Casting as local audio");
|
||||||
castLocalAudio(video, audioSource, resumePosition, speed);
|
castLocalAudio(video, audioSource, resumePosition, speed);
|
||||||
|
} else if (videoSource is JSDashManifestRawSource) {
|
||||||
|
Logger.i(TAG, "Casting as JSDashManifestRawSource video");
|
||||||
|
|
||||||
|
StateApp.instance.scope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
castDashRaw(contentResolver, video, videoSource as JSDashManifestRawSource?, null, null, resumePosition, speed);
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to start casting DASH raw videoSource=${videoSource}.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (audioSource is JSDashManifestRawAudioSource) {
|
||||||
|
Logger.i(TAG, "Casting as JSDashManifestRawSource audio");
|
||||||
|
|
||||||
|
StateApp.instance.scope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
castDashRaw(contentResolver, video, null, audioSource as JSDashManifestRawAudioSource?, null, resumePosition, speed);
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to start casting DASH raw audioSource=${audioSource}.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
var str = listOf(
|
var str = listOf(
|
||||||
if(videoSource != null) "Video: ${videoSource::class.java.simpleName}" else null,
|
if(videoSource != null) "Video: ${videoSource::class.java.simpleName}" else null,
|
||||||
@@ -529,7 +497,7 @@ class StateCasting {
|
|||||||
private fun castLocalVideo(video: IPlatformVideoDetails, videoSource: LocalVideoSource, resumePosition: Double, speed: Double?) : List<String> {
|
private fun castLocalVideo(video: IPlatformVideoDetails, videoSource: LocalVideoSource, resumePosition: Double, speed: Double?) : List<String> {
|
||||||
val ad = activeDevice ?: return listOf();
|
val ad = activeDevice ?: return listOf();
|
||||||
|
|
||||||
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
|
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
val videoPath = "/video-${id}"
|
val videoPath = "/video-${id}"
|
||||||
val videoUrl = url + videoPath;
|
val videoUrl = url + videoPath;
|
||||||
@@ -548,7 +516,7 @@ class StateCasting {
|
|||||||
private fun castLocalAudio(video: IPlatformVideoDetails, audioSource: LocalAudioSource, resumePosition: Double, speed: Double?) : List<String> {
|
private fun castLocalAudio(video: IPlatformVideoDetails, audioSource: LocalAudioSource, resumePosition: Double, speed: Double?) : List<String> {
|
||||||
val ad = activeDevice ?: return listOf();
|
val ad = activeDevice ?: return listOf();
|
||||||
|
|
||||||
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
|
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
val audioPath = "/audio-${id}"
|
val audioPath = "/audio-${id}"
|
||||||
val audioUrl = url + audioPath;
|
val audioUrl = url + audioPath;
|
||||||
@@ -567,7 +535,7 @@ class StateCasting {
|
|||||||
private fun castLocalHls(video: IPlatformVideoDetails, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, resumePosition: Double, speed: Double?): List<String> {
|
private fun castLocalHls(video: IPlatformVideoDetails, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, resumePosition: Double, speed: Double?): List<String> {
|
||||||
val ad = activeDevice ?: return listOf()
|
val ad = activeDevice ?: return listOf()
|
||||||
|
|
||||||
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}"
|
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}"
|
||||||
val id = UUID.randomUUID()
|
val id = UUID.randomUUID()
|
||||||
|
|
||||||
val hlsPath = "/hls-${id}"
|
val hlsPath = "/hls-${id}"
|
||||||
@@ -663,7 +631,7 @@ class StateCasting {
|
|||||||
private fun castLocalDash(video: IPlatformVideoDetails, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
private fun castLocalDash(video: IPlatformVideoDetails, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
||||||
val ad = activeDevice ?: return listOf();
|
val ad = activeDevice ?: return listOf();
|
||||||
|
|
||||||
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
|
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
|
|
||||||
val dashPath = "/dash-${id}"
|
val dashPath = "/dash-${id}"
|
||||||
@@ -713,7 +681,7 @@ class StateCasting {
|
|||||||
val ad = activeDevice ?: return listOf();
|
val ad = activeDevice ?: return listOf();
|
||||||
val proxyStreams = Settings.instance.casting.alwaysProxyRequests || ad !is FCastCastingDevice;
|
val proxyStreams = Settings.instance.casting.alwaysProxyRequests || ad !is FCastCastingDevice;
|
||||||
|
|
||||||
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
|
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
|
|
||||||
val videoPath = "/video-${id}"
|
val videoPath = "/video-${id}"
|
||||||
@@ -771,20 +739,21 @@ class StateCasting {
|
|||||||
Logger.v(TAG) { "Dash manifest: $content" };
|
Logger.v(TAG) { "Dash manifest: $content" };
|
||||||
ad.loadContent("application/dash+xml", content, resumePosition, video.duration.toDouble(), speed);
|
ad.loadContent("application/dash+xml", content, resumePosition, video.duration.toDouble(), speed);
|
||||||
|
|
||||||
return listOf(videoUrl ?: "", audioUrl ?: "", subtitlesUrl ?: "", videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "", subtitlesUri.toString()); }
|
return listOf(videoUrl ?: "", audioUrl ?: "", subtitlesUrl ?: "", videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "", subtitlesUri.toString());
|
||||||
|
}
|
||||||
|
|
||||||
private fun castProxiedHls(video: IPlatformVideoDetails, sourceUrl: String, codec: String?, resumePosition: Double, speed: Double?): List<String> {
|
private fun castProxiedHls(video: IPlatformVideoDetails, sourceUrl: String, codec: String?, resumePosition: Double, speed: Double?): List<String> {
|
||||||
_castServer.removeAllHandlers("castProxiedHlsMaster")
|
_castServer.removeAllHandlers("castProxiedHlsMaster")
|
||||||
|
|
||||||
val ad = activeDevice ?: return listOf();
|
val ad = activeDevice ?: return listOf();
|
||||||
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
|
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
||||||
|
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
val hlsPath = "/hls-${id}"
|
val hlsPath = "/hls-${id}"
|
||||||
val hlsUrl = url + hlsPath
|
val hlsUrl = url + hlsPath
|
||||||
Logger.i(TAG, "HLS url: $hlsUrl");
|
Logger.i(TAG, "HLS url: $hlsUrl");
|
||||||
|
|
||||||
_castServer.addHandlerWithAllowAllOptions(HttpFuntionHandler("GET", hlsPath) { masterContext ->
|
_castServer.addHandlerWithAllowAllOptions(HttpFunctionHandler("GET", hlsPath) { masterContext ->
|
||||||
_castServer.removeAllHandlers("castProxiedHlsVariant")
|
_castServer.removeAllHandlers("castProxiedHlsVariant")
|
||||||
|
|
||||||
val headers = masterContext.headers.clone()
|
val headers = masterContext.headers.clone()
|
||||||
@@ -811,7 +780,7 @@ class StateCasting {
|
|||||||
val proxiedVariantPlaylist = proxyVariantPlaylist(url, id, variantPlaylist, video.isLive)
|
val proxiedVariantPlaylist = proxyVariantPlaylist(url, id, variantPlaylist, video.isLive)
|
||||||
val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8()
|
val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8()
|
||||||
masterContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8);
|
masterContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8);
|
||||||
return@HttpFuntionHandler
|
return@HttpFunctionHandler
|
||||||
} else {
|
} else {
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
@@ -828,7 +797,7 @@ class StateCasting {
|
|||||||
val newPlaylistPath = "/hls-playlist-${playlistId}"
|
val newPlaylistPath = "/hls-playlist-${playlistId}"
|
||||||
val newPlaylistUrl = url + newPlaylistPath;
|
val newPlaylistUrl = url + newPlaylistPath;
|
||||||
|
|
||||||
_castServer.addHandlerWithAllowAllOptions(HttpFuntionHandler("GET", newPlaylistPath) { vpContext ->
|
_castServer.addHandlerWithAllowAllOptions(HttpFunctionHandler("GET", newPlaylistPath) { vpContext ->
|
||||||
val vpHeaders = vpContext.headers.clone()
|
val vpHeaders = vpContext.headers.clone()
|
||||||
vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl";
|
vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl";
|
||||||
|
|
||||||
@@ -858,7 +827,7 @@ class StateCasting {
|
|||||||
val newPlaylistPath = "/hls-playlist-${playlistId}"
|
val newPlaylistPath = "/hls-playlist-${playlistId}"
|
||||||
newPlaylistUrl = url + newPlaylistPath
|
newPlaylistUrl = url + newPlaylistPath
|
||||||
|
|
||||||
_castServer.addHandlerWithAllowAllOptions(HttpFuntionHandler("GET", newPlaylistPath) { vpContext ->
|
_castServer.addHandlerWithAllowAllOptions(HttpFunctionHandler("GET", newPlaylistPath) { vpContext ->
|
||||||
val vpHeaders = vpContext.headers.clone()
|
val vpHeaders = vpContext.headers.clone()
|
||||||
vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl";
|
vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl";
|
||||||
|
|
||||||
@@ -947,7 +916,7 @@ class StateCasting {
|
|||||||
|
|
||||||
private suspend fun castHlsIndirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
private suspend fun castHlsIndirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
||||||
val ad = activeDevice ?: return listOf();
|
val ad = activeDevice ?: return listOf();
|
||||||
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
|
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
|
|
||||||
val hlsPath = "/hls-${id}"
|
val hlsPath = "/hls-${id}"
|
||||||
@@ -1077,7 +1046,7 @@ class StateCasting {
|
|||||||
val ad = activeDevice ?: return listOf();
|
val ad = activeDevice ?: return listOf();
|
||||||
val proxyStreams = Settings.instance.casting.alwaysProxyRequests || ad !is FCastCastingDevice;
|
val proxyStreams = Settings.instance.casting.alwaysProxyRequests || ad !is FCastCastingDevice;
|
||||||
|
|
||||||
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
|
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
|
|
||||||
val dashPath = "/dash-${id}"
|
val dashPath = "/dash-${id}"
|
||||||
@@ -1151,6 +1120,166 @@ class StateCasting {
|
|||||||
return listOf(dashUrl, videoUrl ?: "", audioUrl ?: "", subtitlesUrl ?: "", videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "", subtitlesUri.toString());
|
return listOf(dashUrl, videoUrl ?: "", audioUrl ?: "", subtitlesUrl ?: "", videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "", subtitlesUri.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun cleanExecutors() {
|
||||||
|
if (_videoExecutor != null) {
|
||||||
|
_videoExecutor?.cleanup()
|
||||||
|
_videoExecutor = null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_audioExecutor != null) {
|
||||||
|
_audioExecutor?.cleanup()
|
||||||
|
_audioExecutor = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
|
private suspend fun castDashRaw(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: JSDashManifestRawSource?, audioSource: JSDashManifestRawAudioSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
||||||
|
val ad = activeDevice ?: return listOf();
|
||||||
|
|
||||||
|
cleanExecutors()
|
||||||
|
_castServer.removeAllHandlers("castDashRaw")
|
||||||
|
|
||||||
|
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
||||||
|
val id = UUID.randomUUID();
|
||||||
|
|
||||||
|
val dashPath = "/dash-${id}"
|
||||||
|
val videoPath = "/video-${id}"
|
||||||
|
val audioPath = "/audio-${id}"
|
||||||
|
val subtitlePath = "/subtitle-${id}"
|
||||||
|
|
||||||
|
val dashUrl = url + dashPath;
|
||||||
|
Logger.i(TAG, "DASH url: $dashUrl");
|
||||||
|
|
||||||
|
val videoUrl = url + videoPath
|
||||||
|
val audioUrl = url + audioPath
|
||||||
|
|
||||||
|
val subtitlesUri = if (subtitleSource != null) withContext(Dispatchers.IO) {
|
||||||
|
return@withContext subtitleSource.getSubtitlesURI();
|
||||||
|
} else null;
|
||||||
|
|
||||||
|
var subtitlesUrl: String? = null;
|
||||||
|
if (subtitlesUri != null) {
|
||||||
|
if(subtitlesUri.scheme == "file") {
|
||||||
|
var content: String? = null;
|
||||||
|
val inputStream = contentResolver.openInputStream(subtitlesUri);
|
||||||
|
inputStream?.use { stream ->
|
||||||
|
val reader = stream.bufferedReader();
|
||||||
|
content = reader.use { it.readText() };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content != null) {
|
||||||
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
|
HttpConstantHandler("GET", subtitlePath, content!!, subtitleSource?.format ?: "text/vtt")
|
||||||
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
|
).withTag("cast");
|
||||||
|
}
|
||||||
|
|
||||||
|
subtitlesUrl = url + subtitlePath;
|
||||||
|
} else {
|
||||||
|
subtitlesUrl = subtitlesUri.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var dashContent = withContext(Dispatchers.IO) {
|
||||||
|
//TODO: Include subtitlesURl in the future
|
||||||
|
return@withContext if (audioSource != null && videoSource != null) {
|
||||||
|
JSDashManifestMergingRawSource(videoSource, audioSource).generate()
|
||||||
|
} else if (audioSource != null) {
|
||||||
|
audioSource.generate()
|
||||||
|
} else if (videoSource != null) {
|
||||||
|
videoSource.generate()
|
||||||
|
} else {
|
||||||
|
Logger.e(TAG, "Expected at least audio or video to be set")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} ?: throw Exception("Dash is null")
|
||||||
|
|
||||||
|
for (representation in representationRegex.findAll(dashContent)) {
|
||||||
|
val mediaType = representation.groups[1]?.value ?: throw Exception("Media type should be found")
|
||||||
|
dashContent = mediaInitializationRegex.replace(dashContent) {
|
||||||
|
if (it.range.first < representation.range.first || it.range.last > representation.range.last) {
|
||||||
|
return@replace it.value
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mediaType.startsWith("video/")) {
|
||||||
|
return@replace "${it.groups[1]!!.value}=\"${videoUrl}?url=${URLEncoder.encode(it.groups[2]!!.value, "UTF-8").replace("%24Number%24", "\$Number\$")}&mediaType=${URLEncoder.encode(mediaType, "UTF-8")}\""
|
||||||
|
} else if (mediaType.startsWith("audio/")) {
|
||||||
|
return@replace "${it.groups[1]!!.value}=\"${audioUrl}?url=${URLEncoder.encode(it.groups[2]!!.value, "UTF-8").replace("%24Number%24", "\$Number\$")}&mediaType=${URLEncoder.encode(mediaType, "UTF-8")}\""
|
||||||
|
} else {
|
||||||
|
throw Exception("Expected audio or video")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (videoSource != null && !videoSource.hasRequestExecutor) {
|
||||||
|
throw Exception("Video source without request executor not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (audioSource != null && !audioSource.hasRequestExecutor) {
|
||||||
|
throw Exception("Audio source without request executor not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (audioSource != null && audioSource.hasRequestExecutor) {
|
||||||
|
_audioExecutor = audioSource.getRequestExecutor()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (videoSource != null && videoSource.hasRequestExecutor) {
|
||||||
|
_videoExecutor = videoSource.getRequestExecutor()
|
||||||
|
}
|
||||||
|
|
||||||
|
//TOOD: Else also handle the non request executor case, perhaps add ?url=$originalUrl to the query parameters, ... propagate this for all other flows also
|
||||||
|
|
||||||
|
Logger.v(TAG) { "Dash manifest: $dashContent" };
|
||||||
|
|
||||||
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
|
HttpConstantHandler("GET", dashPath, dashContent,
|
||||||
|
"application/dash+xml")
|
||||||
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
|
).withTag("castDashRaw");
|
||||||
|
|
||||||
|
if (videoSource != null) {
|
||||||
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
|
HttpFunctionHandler("GET", videoPath) { httpContext ->
|
||||||
|
val originalUrl = httpContext.query["url"]?.let { URLDecoder.decode(it, "UTF-8") } ?: return@HttpFunctionHandler
|
||||||
|
val mediaType = httpContext.query["mediaType"]?.let { URLDecoder.decode(it, "UTF-8") } ?: return@HttpFunctionHandler
|
||||||
|
|
||||||
|
val videoExecutor = _videoExecutor;
|
||||||
|
if (videoExecutor != null) {
|
||||||
|
val data = videoExecutor.executeRequest("GET", originalUrl, null, httpContext.headers)
|
||||||
|
httpContext.respondBytes(200, HttpHeaders().apply {
|
||||||
|
put("Content-Type", mediaType)
|
||||||
|
}, data);
|
||||||
|
} else {
|
||||||
|
throw NotImplementedError()
|
||||||
|
}
|
||||||
|
}.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
|
).withTag("castDashRaw");
|
||||||
|
}
|
||||||
|
if (audioSource != null) {
|
||||||
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
|
HttpFunctionHandler("GET", audioPath) { httpContext ->
|
||||||
|
val originalUrl = httpContext.query["url"]?.let { URLDecoder.decode(it, "UTF-8") } ?: return@HttpFunctionHandler
|
||||||
|
val mediaType = httpContext.query["mediaType"]?.let { URLDecoder.decode(it, "UTF-8") } ?: return@HttpFunctionHandler
|
||||||
|
|
||||||
|
val audioExecutor = _audioExecutor;
|
||||||
|
if (audioExecutor != null) {
|
||||||
|
val data = audioExecutor.executeRequest("GET", originalUrl, null, httpContext.headers)
|
||||||
|
httpContext.respondBytes(200, HttpHeaders().apply {
|
||||||
|
put("Content-Type", mediaType)
|
||||||
|
}, data);
|
||||||
|
} else {
|
||||||
|
throw NotImplementedError()
|
||||||
|
}
|
||||||
|
}.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
|
).withTag("castDashRaw");
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.i(TAG, "added new castDash handlers (dashPath: $dashPath, videoPath: $videoPath, audioPath: $audioPath).");
|
||||||
|
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", "application/dash+xml", dashUrl, resumePosition, video.duration.toDouble(), speed);
|
||||||
|
|
||||||
|
return listOf()
|
||||||
|
}
|
||||||
|
|
||||||
private fun deviceFromCastingDeviceInfo(deviceInfo: CastingDeviceInfo): CastingDevice {
|
private fun deviceFromCastingDeviceInfo(deviceInfo: CastingDeviceInfo): CastingDevice {
|
||||||
return when (deviceInfo.type) {
|
return when (deviceInfo.type) {
|
||||||
CastProtocolType.CHROMECAST -> {
|
CastProtocolType.CHROMECAST -> {
|
||||||
@@ -1245,7 +1374,7 @@ class StateCasting {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val newDevice = deviceFactory();
|
val newDevice = deviceFactory();
|
||||||
devices[name] = newDevice;
|
this.devices[name] = newDevice;
|
||||||
|
|
||||||
invokeEvents = {
|
invokeEvents = {
|
||||||
onDeviceAdded.emit(newDevice);
|
onDeviceAdded.emit(newDevice);
|
||||||
@@ -1259,7 +1388,7 @@ class StateCasting {
|
|||||||
fun enableDeveloper(enableDev: Boolean){
|
fun enableDeveloper(enableDev: Boolean){
|
||||||
_castServer.removeAllHandlers("dev");
|
_castServer.removeAllHandlers("dev");
|
||||||
if(enableDev) {
|
if(enableDev) {
|
||||||
_castServer.addHandler(HttpFuntionHandler("GET", "/dashPlayer") { context ->
|
_castServer.addHandler(HttpFunctionHandler("GET", "/dashPlayer") { context ->
|
||||||
if (context.query.containsKey("dashUrl")) {
|
if (context.query.containsKey("dashUrl")) {
|
||||||
val dashUrl = context.query["dashUrl"];
|
val dashUrl = context.query["dashUrl"];
|
||||||
val html = "<div>\n" +
|
val html = "<div>\n" +
|
||||||
@@ -1299,6 +1428,9 @@ class StateCasting {
|
|||||||
companion object {
|
companion object {
|
||||||
val instance: StateCasting = StateCasting();
|
val instance: StateCasting = StateCasting();
|
||||||
|
|
||||||
|
private val representationRegex = Regex("<Representation .*?mimeType=\"(.*?)\".*?>(.*?)<\\/Representation>", RegexOption.DOT_MATCHES_ALL)
|
||||||
|
private val mediaInitializationRegex = Regex("(media|initiali[sz]ation)=\"([^\"]+)\"", RegexOption.DOT_MATCHES_ALL);
|
||||||
|
|
||||||
private val TAG = "StateCasting";
|
private val TAG = "StateCasting";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,10 +25,8 @@ import com.futo.platformplayer.states.StateDeveloper
|
|||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.google.gson.ExclusionStrategy
|
import com.google.gson.ExclusionStrategy
|
||||||
import com.google.gson.FieldAttributes
|
import com.google.gson.FieldAttributes
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
import com.google.gson.JsonArray
|
import com.google.gson.JsonArray
|
||||||
import com.google.gson.JsonElement
|
|
||||||
import com.google.gson.JsonParser
|
import com.google.gson.JsonParser
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
@@ -573,7 +571,7 @@ class DeveloperEndpoints(private val context: Context) {
|
|||||||
val resp = _client.get(body.url!!, body.headers);
|
val resp = _client.get(body.url!!, body.headers);
|
||||||
|
|
||||||
context.respondCode(200,
|
context.respondCode(200,
|
||||||
Json.encodeToString(PackageHttp.BridgeHttpResponse(resp.url, resp.code, resp.body?.string())),
|
Json.encodeToString(PackageHttp.BridgeHttpStringResponse(resp.url, resp.code, resp.body?.string())),
|
||||||
context.query.getOrDefault("CT", "text/plain"));
|
context.query.getOrDefault("CT", "text/plain"));
|
||||||
}
|
}
|
||||||
catch(ex: Exception) {
|
catch(ex: Exception) {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ class AutomaticBackupDialog(context: Context) : AlertDialog(context) {
|
|||||||
private lateinit var _buttonCancel: ImageButton;
|
private lateinit var _buttonCancel: ImageButton;
|
||||||
|
|
||||||
private lateinit var _editPassword: EditText;
|
private lateinit var _editPassword: EditText;
|
||||||
|
private lateinit var _editPassword2: EditText;
|
||||||
|
|
||||||
private lateinit var _inputMethodManager: InputMethodManager;
|
private lateinit var _inputMethodManager: InputMethodManager;
|
||||||
|
|
||||||
@@ -34,6 +35,7 @@ class AutomaticBackupDialog(context: Context) : AlertDialog(context) {
|
|||||||
_buttonStop = findViewById(R.id.button_stop);
|
_buttonStop = findViewById(R.id.button_stop);
|
||||||
_buttonStart = findViewById(R.id.button_start);
|
_buttonStart = findViewById(R.id.button_start);
|
||||||
_editPassword = findViewById(R.id.edit_password);
|
_editPassword = findViewById(R.id.edit_password);
|
||||||
|
_editPassword2 = findViewById(R.id.edit_password2);
|
||||||
|
|
||||||
_inputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager;
|
_inputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager;
|
||||||
|
|
||||||
@@ -52,6 +54,13 @@ class AutomaticBackupDialog(context: Context) : AlertDialog(context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_buttonStart.setOnClickListener {
|
_buttonStart.setOnClickListener {
|
||||||
|
val p1 = _editPassword.text.toString();
|
||||||
|
val p2 = _editPassword2.text.toString();
|
||||||
|
if(!(p1?.equals(p2) ?: false)) {
|
||||||
|
UIDialogs.toast(context, "Password fields do not match, confirm that you typed it correctly.");
|
||||||
|
return@setOnClickListener;
|
||||||
|
}
|
||||||
|
|
||||||
val pbytes = _editPassword.text.toString().toByteArray();
|
val pbytes = _editPassword.text.toString().toByteArray();
|
||||||
if(pbytes.size < 4 || pbytes.size > 32) {
|
if(pbytes.size < 4 || pbytes.size > 32) {
|
||||||
UIDialogs.toast(context, "Password needs to be atleast 4 bytes long and smaller than 32 bytes", false);
|
UIDialogs.toast(context, "Password needs to be atleast 4 bytes long and smaller than 32 bytes", false);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import android.graphics.Color
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
|
import android.view.KeyEvent
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
@@ -57,11 +58,21 @@ class CommentDialog(context: Context?, val contextUrl: String, val ref: Protocol
|
|||||||
_editComment = findViewById(R.id.edit_comment);
|
_editComment = findViewById(R.id.edit_comment);
|
||||||
_textCharacterCount = findViewById(R.id.character_count);
|
_textCharacterCount = findViewById(R.id.character_count);
|
||||||
_textCharacterCountMax = findViewById(R.id.character_count_max);
|
_textCharacterCountMax = findViewById(R.id.character_count_max);
|
||||||
|
setCanceledOnTouchOutside(false)
|
||||||
|
setOnKeyListener { _, keyCode, event ->
|
||||||
|
if (keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_UP) {
|
||||||
|
handleCloseAttempt()
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_editComment.addTextChangedListener(object : TextWatcher {
|
_editComment.addTextChangedListener(object : TextWatcher {
|
||||||
override fun afterTextChanged(s: Editable?) = Unit
|
override fun afterTextChanged(s: Editable?) = Unit
|
||||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
|
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
|
||||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, c: Int) {
|
||||||
|
val count = s?.length ?: 0;
|
||||||
_textCharacterCount.text = count.toString();
|
_textCharacterCount.text = count.toString();
|
||||||
|
|
||||||
if (count > PolycentricPlatformComment.MAX_COMMENT_SIZE) {
|
if (count > PolycentricPlatformComment.MAX_COMMENT_SIZE) {
|
||||||
@@ -79,10 +90,13 @@ class CommentDialog(context: Context?, val contextUrl: String, val ref: Protocol
|
|||||||
_inputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager;
|
_inputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager;
|
||||||
|
|
||||||
_buttonCancel.setOnClickListener {
|
_buttonCancel.setOnClickListener {
|
||||||
clearFocus();
|
handleCloseAttempt()
|
||||||
dismiss();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
setOnCancelListener {
|
||||||
|
handleCloseAttempt()
|
||||||
|
}
|
||||||
|
|
||||||
_buttonCreate.setOnClickListener {
|
_buttonCreate.setOnClickListener {
|
||||||
clearFocus();
|
clearFocus();
|
||||||
|
|
||||||
@@ -134,6 +148,22 @@ class CommentDialog(context: Context?, val contextUrl: String, val ref: Protocol
|
|||||||
focus();
|
focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleCloseAttempt() {
|
||||||
|
if (_editComment.text.isEmpty()) {
|
||||||
|
clearFocus()
|
||||||
|
dismiss()
|
||||||
|
} else {
|
||||||
|
UIDialogs.showConfirmationDialog(
|
||||||
|
context,
|
||||||
|
context.resources.getString(R.string.not_empty_close),
|
||||||
|
action = {
|
||||||
|
clearFocus()
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun focus() {
|
private fun focus() {
|
||||||
_editComment.requestFocus();
|
_editComment.requestFocus();
|
||||||
window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
|
window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
|
||||||
|
|||||||
@@ -104,6 +104,8 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
|||||||
super.show();
|
super.show();
|
||||||
Logger.i(TAG, "Dialog shown.");
|
Logger.i(TAG, "Dialog shown.");
|
||||||
|
|
||||||
|
StateCasting.instance.startDiscovering()
|
||||||
|
|
||||||
(_imageLoader.drawable as Animatable?)?.start();
|
(_imageLoader.drawable as Animatable?)?.start();
|
||||||
|
|
||||||
_devices.clear();
|
_devices.clear();
|
||||||
@@ -169,6 +171,7 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
|||||||
|
|
||||||
(_imageLoader.drawable as Animatable?)?.stop();
|
(_imageLoader.drawable as Animatable?)?.stop();
|
||||||
|
|
||||||
|
StateCasting.instance.stopDiscovering()
|
||||||
StateCasting.instance.onDeviceAdded.remove(this);
|
StateCasting.instance.onDeviceAdded.remove(this);
|
||||||
StateCasting.instance.onDeviceChanged.remove(this);
|
StateCasting.instance.onDeviceChanged.remove(this);
|
||||||
StateCasting.instance.onDeviceRemoved.remove(this);
|
StateCasting.instance.onDeviceRemoved.remove(this);
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescri
|
|||||||
import com.futo.platformplayer.api.media.models.streams.sources.AudioUrlSource
|
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.IAudioSource
|
||||||
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.IDashManifestSource
|
||||||
|
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.IHLSManifestSource
|
||||||
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.streams.sources.IVideoUrlSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
||||||
@@ -25,6 +27,14 @@ 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.api.media.models.video.SerializedPlatformVideoDetails
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideoDetails
|
||||||
|
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.models.JSRequestExecutor
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.JSVideo
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.IJSDashManifestRawSource
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawAudioSource
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawSource
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.exceptions.DownloadException
|
import com.futo.platformplayer.exceptions.DownloadException
|
||||||
import com.futo.platformplayer.helpers.FileHelper.Companion.sanitizeFileName
|
import com.futo.platformplayer.helpers.FileHelper.Companion.sanitizeFileName
|
||||||
@@ -34,6 +44,7 @@ import com.futo.platformplayer.parsers.HLS
|
|||||||
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||||
import com.futo.platformplayer.states.StateDownloads
|
import com.futo.platformplayer.states.StateDownloads
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
|
import com.futo.platformplayer.states.StatePlugins
|
||||||
import com.futo.platformplayer.toHumanBitrate
|
import com.futo.platformplayer.toHumanBitrate
|
||||||
import com.futo.platformplayer.toHumanBytesSpeed
|
import com.futo.platformplayer.toHumanBytesSpeed
|
||||||
import hasAnySource
|
import hasAnySource
|
||||||
@@ -46,9 +57,12 @@ import kotlinx.coroutines.awaitAll
|
|||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.serialization.Contextual
|
||||||
|
import kotlinx.serialization.Transient
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
import java.lang.Thread.sleep
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
@@ -56,6 +70,7 @@ import java.util.concurrent.ForkJoinPool
|
|||||||
import java.util.concurrent.ForkJoinTask
|
import java.util.concurrent.ForkJoinTask
|
||||||
import java.util.concurrent.ThreadLocalRandom
|
import java.util.concurrent.ThreadLocalRandom
|
||||||
import kotlin.coroutines.resumeWithException
|
import kotlin.coroutines.resumeWithException
|
||||||
|
import kotlin.time.times
|
||||||
|
|
||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
class VideoDownload {
|
class VideoDownload {
|
||||||
@@ -71,12 +86,51 @@ class VideoDownload {
|
|||||||
|
|
||||||
var targetPixelCount: Long? = null;
|
var targetPixelCount: Long? = null;
|
||||||
var targetBitrate: Long? = null;
|
var targetBitrate: Long? = null;
|
||||||
|
var targetVideoName: String? = null;
|
||||||
|
var targetAudioName: String? = null;
|
||||||
|
|
||||||
var videoSource: VideoUrlSource?;
|
var videoSource: VideoUrlSource?;
|
||||||
var audioSource: AudioUrlSource?;
|
var audioSource: AudioUrlSource?;
|
||||||
|
@Contextual
|
||||||
|
@Transient
|
||||||
|
val videoSourceToUse: IVideoSource? get () = if(requiresLiveVideoSource) videoSourceLive as IVideoSource? else videoSource as IVideoSource?;
|
||||||
|
@Contextual
|
||||||
|
@Transient
|
||||||
|
val audioSourceToUse: IAudioSource? get () = if(requiresLiveAudioSource) audioSourceLive as IAudioSource? else audioSource as IAudioSource?;
|
||||||
|
|
||||||
|
var requireVideoSource: Boolean = false;
|
||||||
|
var requireAudioSource: Boolean = false;
|
||||||
|
var requiredCheck: Boolean = false;
|
||||||
|
|
||||||
|
@Contextual
|
||||||
|
@Transient
|
||||||
|
val isVideoDownloadReady: Boolean get() = !requireVideoSource ||
|
||||||
|
((requiresLiveVideoSource && isLiveVideoSourceValid) || (!requiresLiveVideoSource && videoSource != null));
|
||||||
|
@Contextual
|
||||||
|
@Transient
|
||||||
|
val isAudioDownloadReady: Boolean get() = !requireAudioSource ||
|
||||||
|
((requiresLiveAudioSource && isLiveAudioSourceValid) || (!requiresLiveAudioSource && audioSource != null));
|
||||||
|
|
||||||
|
|
||||||
var subtitleSource: SubtitleRawSource?;
|
var subtitleSource: SubtitleRawSource?;
|
||||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
||||||
var prepareTime: OffsetDateTime? = null;
|
var prepareTime: OffsetDateTime? = null;
|
||||||
|
|
||||||
|
var requiresLiveVideoSource: Boolean = false;
|
||||||
|
@Contextual
|
||||||
|
@kotlinx.serialization.Transient
|
||||||
|
var videoSourceLive: JSSource? = null;
|
||||||
|
val isLiveVideoSourceValid get() = videoSourceLive?.getUnderlyingObject()?.isClosed?.let { !it } ?: false;
|
||||||
|
|
||||||
|
var requiresLiveAudioSource: Boolean = false;
|
||||||
|
@Contextual
|
||||||
|
@kotlinx.serialization.Transient
|
||||||
|
var audioSourceLive: JSSource? = null;
|
||||||
|
val isLiveAudioSourceValid get() = audioSourceLive?.getUnderlyingObject()?.isClosed?.let { !it } ?: false;
|
||||||
|
|
||||||
|
var hasVideoRequestExecutor: Boolean = false;
|
||||||
|
var hasAudioRequestExecutor: Boolean = false;
|
||||||
|
|
||||||
var progress: Double = 0.0;
|
var progress: Double = 0.0;
|
||||||
var isCancelled = false;
|
var isCancelled = false;
|
||||||
|
|
||||||
@@ -111,21 +165,40 @@ class VideoDownload {
|
|||||||
onStateChanged.emit(newState);
|
onStateChanged.emit(newState);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(video: IPlatformVideo, targetPixelCount: Long? = null, targetBitrate: Long? = null) {
|
constructor(video: IPlatformVideo, targetPixelCount: Long? = null, targetBitrate: Long? = null, optionalSources: Boolean = false) {
|
||||||
this.video = SerializedPlatformVideo.fromVideo(video);
|
this.video = SerializedPlatformVideo.fromVideo(video);
|
||||||
this.videoSource = null;
|
this.videoSource = null;
|
||||||
this.audioSource = null;
|
this.audioSource = null;
|
||||||
this.subtitleSource = null;
|
this.subtitleSource = null;
|
||||||
this.targetPixelCount = targetPixelCount;
|
this.targetPixelCount = targetPixelCount;
|
||||||
this.targetBitrate = targetBitrate;
|
this.targetBitrate = targetBitrate;
|
||||||
|
this.hasVideoRequestExecutor = video is JSSource && video.hasRequestExecutor;
|
||||||
|
this.requiresLiveVideoSource = false;
|
||||||
|
this.requiresLiveAudioSource = false;
|
||||||
|
this.targetVideoName = videoSource?.name;
|
||||||
|
this.requireVideoSource = targetPixelCount != null;
|
||||||
|
this.requireAudioSource = targetBitrate != null; //TODO: May not be a valid check.. can only be determined after live fetch?
|
||||||
|
this.requiredCheck = optionalSources;
|
||||||
}
|
}
|
||||||
constructor(video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: SubtitleRawSource?) {
|
constructor(video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: SubtitleRawSource?) {
|
||||||
this.video = SerializedPlatformVideo.fromVideo(video);
|
this.video = SerializedPlatformVideo.fromVideo(video);
|
||||||
this.videoDetails = SerializedPlatformVideoDetails.fromVideo(video, if (subtitleSource != null) listOf(subtitleSource) else listOf());
|
this.videoDetails = SerializedPlatformVideoDetails.fromVideo(video, if (subtitleSource != null) listOf(subtitleSource) else listOf());
|
||||||
this.videoSource = VideoUrlSource.fromUrlSource(videoSource);
|
this.videoSource = if(videoSource is IVideoUrlSource) VideoUrlSource.fromUrlSource(videoSource) else null;
|
||||||
this.audioSource = AudioUrlSource.fromUrlSource(audioSource);
|
this.audioSource = if(audioSource is IAudioUrlSource) AudioUrlSource.fromUrlSource(audioSource) else null;
|
||||||
|
this.videoSourceLive = if(videoSource is JSSource) videoSource else null;
|
||||||
|
this.audioSourceLive = if(audioSource is JSSource) audioSource else null;
|
||||||
this.subtitleSource = subtitleSource;
|
this.subtitleSource = subtitleSource;
|
||||||
this.prepareTime = OffsetDateTime.now();
|
this.prepareTime = OffsetDateTime.now();
|
||||||
|
this.hasVideoRequestExecutor = videoSource is JSSource && videoSource.hasRequestExecutor;
|
||||||
|
this.hasAudioRequestExecutor = audioSource is JSSource && audioSource.hasRequestExecutor;
|
||||||
|
this.requiresLiveVideoSource = this.hasVideoRequestExecutor || (videoSource is JSDashManifestRawSource && videoSource.hasGenerate);
|
||||||
|
this.requiresLiveAudioSource = this.hasAudioRequestExecutor || (audioSource is JSDashManifestRawAudioSource && audioSource.hasGenerate);
|
||||||
|
this.targetVideoName = videoSource?.name;
|
||||||
|
this.targetAudioName = audioSource?.name;
|
||||||
|
this.targetPixelCount = if(videoSource != null) (videoSource.width * videoSource.height).toLong() else null;
|
||||||
|
this.targetBitrate = if(audioSource != null) audioSource.bitrate.toLong() else null;
|
||||||
|
this.requireVideoSource = videoSource != null;
|
||||||
|
this.requireAudioSource = audioSource != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun withGroup(groupType: String, groupID: String): VideoDownload {
|
fun withGroup(groupType: String, groupID: String): VideoDownload {
|
||||||
@@ -156,9 +229,21 @@ class VideoDownload {
|
|||||||
|
|
||||||
suspend fun prepare(client: ManagedHttpClient) {
|
suspend fun prepare(client: ManagedHttpClient) {
|
||||||
Logger.i(TAG, "VideoDownload Prepare [${name}]");
|
Logger.i(TAG, "VideoDownload Prepare [${name}]");
|
||||||
|
|
||||||
|
//If live sources are required, ensure a live object is present
|
||||||
|
if(requiresLiveVideoSource && !isLiveVideoSourceValid) {
|
||||||
|
videoDetails = null;
|
||||||
|
videoSource = null;
|
||||||
|
videoSourceLive = null;
|
||||||
|
}
|
||||||
|
if(requiresLiveAudioSource && !isLiveAudioSourceValid) {
|
||||||
|
videoDetails = null;
|
||||||
|
audioSource = null;
|
||||||
|
videoSourceLive = null;
|
||||||
|
}
|
||||||
if(video == null && videoDetails == null)
|
if(video == null && videoDetails == null)
|
||||||
throw IllegalStateException("Missing information for download to complete");
|
throw IllegalStateException("Missing information for download to complete");
|
||||||
if(targetPixelCount == null && targetBitrate == null && videoSource == null && audioSource == null)
|
if(targetPixelCount == null && targetBitrate == null && videoSource == null && audioSource == null && targetVideoName == null && targetAudioName == null)
|
||||||
throw IllegalStateException("No sources or query values set");
|
throw IllegalStateException("No sources or query values set");
|
||||||
|
|
||||||
//Fetch full video object and determine source
|
//Fetch full video object and determine source
|
||||||
@@ -167,6 +252,30 @@ class VideoDownload {
|
|||||||
if(original !is IPlatformVideoDetails)
|
if(original !is IPlatformVideoDetails)
|
||||||
throw IllegalStateException("Original content is not media?");
|
throw IllegalStateException("Original content is not media?");
|
||||||
|
|
||||||
|
if(requiredCheck) {
|
||||||
|
if(original.video is VideoUnMuxedSourceDescriptor) {
|
||||||
|
if(requireVideoSource) {
|
||||||
|
if((original.video as VideoUnMuxedSourceDescriptor).audioSources.any() && !original.video.videoSources.any()) {
|
||||||
|
requireVideoSource = false;
|
||||||
|
targetPixelCount = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(requireAudioSource) {
|
||||||
|
if(!(original.video as VideoUnMuxedSourceDescriptor).audioSources.any() && original.video.videoSources.any()) {
|
||||||
|
requireAudioSource = false;
|
||||||
|
targetBitrate = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if(requireAudioSource) {
|
||||||
|
requireAudioSource = false;
|
||||||
|
targetBitrate = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
requiredCheck = false;
|
||||||
|
}
|
||||||
|
|
||||||
if(original.video.hasAnySource() && !original.isDownloadable()) {
|
if(original.video.hasAnySource() && !original.isDownloadable()) {
|
||||||
Logger.i(TAG, "Attempted to download unsupported video [${original.name}]:${original.url}");
|
Logger.i(TAG, "Attempted to download unsupported video [${original.name}]:${original.url}");
|
||||||
throw DownloadException("Unsupported video for downloading", false);
|
throw DownloadException("Unsupported video for downloading", false);
|
||||||
@@ -192,23 +301,35 @@ class VideoDownload {
|
|||||||
videoSources.add(source)
|
videoSources.add(source)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
var vsource: IVideoSource? = null;
|
||||||
|
|
||||||
val vsource = VideoHelper.selectBestVideoSource(videoSources, targetPixelCount!!.toInt(), arrayOf())
|
if(targetVideoName != null)
|
||||||
|
vsource = videoSources.find { x -> x.isDownloadable() && x.name == targetVideoName };
|
||||||
|
if(vsource == null && targetPixelCount == null)
|
||||||
|
throw IllegalStateException("Could not find comparable downloadable video stream (No target pixel count)");
|
||||||
|
if(vsource == null)
|
||||||
|
vsource = VideoHelper.selectBestVideoSource(videoSources, targetPixelCount!!.toInt(), arrayOf())
|
||||||
// ?: throw IllegalStateException("Could not find a valid video source for video");
|
// ?: throw IllegalStateException("Could not find a valid video source for video");
|
||||||
if(vsource != null) {
|
|
||||||
if (vsource is IVideoUrlSource)
|
if(vsource == null) {
|
||||||
videoSource = VideoUrlSource.fromUrlSource(vsource)
|
videoSource = null;
|
||||||
else
|
if(original.video.videoSources.size == 0)
|
||||||
throw DownloadException("Video source is not supported for downloading (yet)", false);
|
requireVideoSource = false;
|
||||||
}
|
}
|
||||||
|
else if(vsource is IVideoUrlSource)
|
||||||
|
videoSource = VideoUrlSource.fromUrlSource(vsource)
|
||||||
|
else if(vsource is JSSource && requiresLiveVideoSource)
|
||||||
|
videoSourceLive = vsource;
|
||||||
|
else
|
||||||
|
throw DownloadException("Video source is not supported for downloading (yet) [" + vsource?.javaClass?.name + "]", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(audioSource == null && targetBitrate != null) {
|
if(audioSource == null && targetBitrate != null) {
|
||||||
val audioSources = arrayListOf<IAudioSource>()
|
var audioSources = mutableListOf<IAudioSource>()
|
||||||
val video = original.video
|
val video = original.video
|
||||||
if (video is VideoUnMuxedSourceDescriptor) {
|
if (video is VideoUnMuxedSourceDescriptor) {
|
||||||
for (source in video.audioSources) {
|
for (source in video.audioSources) {
|
||||||
if (source is IHLSManifestSource) {
|
if (source is IHLSManifestAudioSource) {
|
||||||
try {
|
try {
|
||||||
val playlistResponse = client.get(source.url)
|
val playlistResponse = client.get(source.url)
|
||||||
if (playlistResponse.isOk) {
|
if (playlistResponse.isOk) {
|
||||||
@@ -226,25 +347,43 @@ class VideoDownload {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val asource = VideoHelper.selectBestAudioSource(audioSources, arrayOf(), null, targetBitrate)
|
var asource: IAudioSource? = null;
|
||||||
?: if(videoSource != null ) null
|
if(targetAudioName != null) {
|
||||||
else throw DownloadException("Could not find a valid video or audio source for download")
|
val filteredAudioSources = audioSources.filter { x -> x.isDownloadable() && x.name == targetAudioName }.toTypedArray();
|
||||||
|
if(filteredAudioSources.size == 1)
|
||||||
|
asource = filteredAudioSources.first();
|
||||||
|
else if(filteredAudioSources.size > 1)
|
||||||
|
audioSources = filteredAudioSources.toMutableList();
|
||||||
|
}
|
||||||
|
if(asource == null && targetBitrate == null)
|
||||||
|
throw IllegalStateException("Could not find comparable downloadable video stream (No target bitrate)");
|
||||||
if(asource == null)
|
if(asource == null)
|
||||||
|
asource = VideoHelper.selectBestAudioSource(audioSources, arrayOf(), null, targetBitrate)
|
||||||
|
?: if(videoSource != null ) null
|
||||||
|
else throw DownloadException("Could not find a valid video or audio source for download")
|
||||||
|
if(asource == null) {
|
||||||
audioSource = null;
|
audioSource = null;
|
||||||
|
if(!original.video.isUnMuxed || original.video.videoSources.size == 0)
|
||||||
|
requireVideoSource = false;
|
||||||
|
}
|
||||||
else if(asource is IAudioUrlSource)
|
else if(asource is IAudioUrlSource)
|
||||||
audioSource = AudioUrlSource.fromUrlSource(asource)
|
audioSource = AudioUrlSource.fromUrlSource(asource)
|
||||||
|
else if(asource is JSSource && requiresLiveAudioSource)
|
||||||
|
audioSourceLive = asource;
|
||||||
else
|
else
|
||||||
throw DownloadException("Audio source is not supported for downloading (yet)", false);
|
throw DownloadException("Audio source is not supported for downloading (yet) [" + asource?.javaClass?.name + "]", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(videoSource == null && audioSource == null)
|
if(!isVideoDownloadReady)
|
||||||
throw DownloadException("No valid sources found for video/audio");
|
throw DownloadException("No valid sources found for video");
|
||||||
|
if(!isAudioDownloadReady)
|
||||||
|
throw DownloadException("No valid sources found for audio");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun download(context: Context, client: ManagedHttpClient, onProgress: ((Double) -> Unit)? = null) = coroutineScope {
|
suspend fun download(context: Context, client: ManagedHttpClient, onProgress: ((Double) -> Unit)? = null) = coroutineScope {
|
||||||
Logger.i(TAG, "VideoDownload Download [${name}]");
|
Logger.i(TAG, "VideoDownload Download [${name}]");
|
||||||
if(videoDetails == null || (videoSource == null && audioSource == null))
|
if(videoDetails == null || (videoSourceToUse == null && audioSourceToUse == null))
|
||||||
throw IllegalStateException("Missing information for download to complete");
|
throw IllegalStateException("Missing information for download to complete");
|
||||||
val downloadDir = StateDownloads.instance.getDownloadsDirectory();
|
val downloadDir = StateDownloads.instance.getDownloadsDirectory();
|
||||||
|
|
||||||
@@ -253,12 +392,19 @@ class VideoDownload {
|
|||||||
|
|
||||||
if(isCancelled) throw CancellationException("Download got cancelled");
|
if(isCancelled) throw CancellationException("Download got cancelled");
|
||||||
|
|
||||||
if(videoSource != null) {
|
val actualVideoSource = if(requiresLiveVideoSource && videoSourceLive is IVideoSource)
|
||||||
videoFileName = "${videoDetails!!.id.value!!} [${videoSource!!.width}x${videoSource!!.height}].${videoContainerToExtension(videoSource!!.container)}".sanitizeFileName();
|
videoSourceLive as IVideoSource?;
|
||||||
|
else videoSource;
|
||||||
|
val actualAudioSource = if(requiresLiveAudioSource && audioSourceLive is IAudioSource)
|
||||||
|
audioSourceLive as IAudioSource?;
|
||||||
|
else audioSource;
|
||||||
|
|
||||||
|
if(actualVideoSource != null) {
|
||||||
|
videoFileName = "${videoDetails!!.id.value!!} [${actualVideoSource!!.width}x${actualVideoSource!!.height}].${videoContainerToExtension(actualVideoSource!!.container)}".sanitizeFileName();
|
||||||
videoFilePath = File(downloadDir, videoFileName!!).absolutePath;
|
videoFilePath = File(downloadDir, videoFileName!!).absolutePath;
|
||||||
}
|
}
|
||||||
if(audioSource != null) {
|
if(actualAudioSource != null) {
|
||||||
audioFileName = "${videoDetails!!.id.value!!} [${audioSource!!.language}-${audioSource!!.bitrate}].${audioContainerToExtension(audioSource!!.container)}".sanitizeFileName();
|
audioFileName = "${videoDetails!!.id.value!!} [${actualAudioSource!!.language}-${actualAudioSource!!.bitrate}].${audioContainerToExtension(actualAudioSource!!.container)}".sanitizeFileName();
|
||||||
audioFilePath = File(downloadDir, audioFileName!!).absolutePath;
|
audioFilePath = File(downloadDir, audioFileName!!).absolutePath;
|
||||||
}
|
}
|
||||||
if(subtitleSource != null) {
|
if(subtitleSource != null) {
|
||||||
@@ -273,10 +419,11 @@ class VideoDownload {
|
|||||||
var lastAudioLength: Long = 0;
|
var lastAudioLength: Long = 0;
|
||||||
var lastAudioRead: Long = 0;
|
var lastAudioRead: Long = 0;
|
||||||
|
|
||||||
if(videoSource != null) {
|
if(actualVideoSource != null) {
|
||||||
sourcesToDownload.add(async {
|
sourcesToDownload.add(async {
|
||||||
Logger.i(TAG, "Started downloading video");
|
Logger.i(TAG, "Started downloading video");
|
||||||
|
|
||||||
|
var lastEmit = 0L;
|
||||||
val progressCallback = { length: Long, totalRead: Long, speed: Long ->
|
val progressCallback = { length: Long, totalRead: Long, speed: Long ->
|
||||||
synchronized(progressLock) {
|
synchronized(progressLock) {
|
||||||
lastVideoLength = length;
|
lastVideoLength = length;
|
||||||
@@ -289,23 +436,34 @@ class VideoDownload {
|
|||||||
val total = lastVideoRead + lastAudioRead;
|
val total = lastVideoRead + lastAudioRead;
|
||||||
if(totalLength > 0) {
|
if(totalLength > 0) {
|
||||||
val percentage = (total / totalLength.toDouble());
|
val percentage = (total / totalLength.toDouble());
|
||||||
onProgress?.invoke(percentage);
|
|
||||||
progress = percentage;
|
progress = percentage;
|
||||||
onProgressChanged.emit(percentage);
|
|
||||||
|
val now = System.currentTimeMillis();
|
||||||
|
if(now - lastEmit > 200) {
|
||||||
|
lastEmit = System.currentTimeMillis();
|
||||||
|
onProgress?.invoke(percentage);
|
||||||
|
onProgressChanged.emit(percentage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
videoFileSize = when (videoSource!!.container) {
|
if(actualVideoSource is IVideoUrlSource)
|
||||||
"application/vnd.apple.mpegurl" -> downloadHlsSource(context, "Video", client, videoSource!!.getVideoUrl(), File(downloadDir, videoFileName!!), progressCallback)
|
videoFileSize = when (videoSource!!.container) {
|
||||||
else -> downloadFileSource("Video", client, videoSource!!.getVideoUrl(), File(downloadDir, videoFileName!!), progressCallback)
|
"application/vnd.apple.mpegurl" -> downloadHlsSource(context, "Video", client, videoSource!!.getVideoUrl(), File(downloadDir, videoFileName!!), progressCallback)
|
||||||
|
else -> downloadFileSource("Video", client, videoSource!!.getVideoUrl(), File(downloadDir, videoFileName!!), progressCallback)
|
||||||
|
}
|
||||||
|
else if(actualVideoSource is JSDashManifestRawSource) {
|
||||||
|
videoFileSize = downloadDashFileSource("Video", client, actualVideoSource, File(downloadDir, videoFileName!!), progressCallback);
|
||||||
}
|
}
|
||||||
|
else throw NotImplementedError("NotImplemented video download: " + actualVideoSource.javaClass.name);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if(audioSource != null) {
|
if(actualAudioSource != null) {
|
||||||
sourcesToDownload.add(async {
|
sourcesToDownload.add(async {
|
||||||
Logger.i(TAG, "Started downloading audio");
|
Logger.i(TAG, "Started downloading audio");
|
||||||
|
|
||||||
|
var lastEmit = 0L;
|
||||||
val progressCallback = { length: Long, totalRead: Long, speed: Long ->
|
val progressCallback = { length: Long, totalRead: Long, speed: Long ->
|
||||||
synchronized(progressLock) {
|
synchronized(progressLock) {
|
||||||
lastAudioLength = length;
|
lastAudioLength = length;
|
||||||
@@ -318,17 +476,27 @@ class VideoDownload {
|
|||||||
val total = lastVideoRead + lastAudioRead;
|
val total = lastVideoRead + lastAudioRead;
|
||||||
if(totalLength > 0) {
|
if(totalLength > 0) {
|
||||||
val percentage = (total / totalLength.toDouble());
|
val percentage = (total / totalLength.toDouble());
|
||||||
onProgress?.invoke(percentage);
|
|
||||||
progress = percentage;
|
progress = percentage;
|
||||||
onProgressChanged.emit(percentage);
|
|
||||||
|
val now = System.currentTimeMillis();
|
||||||
|
if(now - lastEmit > 200) {
|
||||||
|
lastEmit = System.currentTimeMillis();
|
||||||
|
onProgress?.invoke(percentage);
|
||||||
|
onProgressChanged.emit(percentage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
audioFileSize = when (audioSource!!.container) {
|
if(actualAudioSource is IAudioUrlSource)
|
||||||
"application/vnd.apple.mpegurl" -> downloadHlsSource(context, "Audio", client, audioSource!!.getAudioUrl(), File(downloadDir, audioFileName!!), progressCallback)
|
audioFileSize = when (audioSource!!.container) {
|
||||||
else -> downloadFileSource("Audio", client, audioSource!!.getAudioUrl(), File(downloadDir, audioFileName!!), progressCallback)
|
"application/vnd.apple.mpegurl" -> downloadHlsSource(context, "Audio", client, audioSource!!.getAudioUrl(), File(downloadDir, audioFileName!!), progressCallback)
|
||||||
|
else -> downloadFileSource("Audio", client, audioSource!!.getAudioUrl(), File(downloadDir, audioFileName!!), progressCallback)
|
||||||
|
}
|
||||||
|
else if(actualAudioSource is JSDashManifestRawAudioSource) {
|
||||||
|
audioFileSize = downloadDashFileSource("Audio", client, actualAudioSource, File(downloadDir, audioFileName!!), progressCallback);
|
||||||
}
|
}
|
||||||
|
else throw NotImplementedError("NotImplemented audio download: " + actualAudioSource.javaClass.name);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (subtitleSource != null) {
|
if (subtitleSource != null) {
|
||||||
@@ -398,15 +566,20 @@ class VideoDownload {
|
|||||||
|
|
||||||
Logger.i(TAG, "Download '$name' segment $index Sequential");
|
Logger.i(TAG, "Download '$name' segment $index Sequential");
|
||||||
val segmentFile = File(context.cacheDir, "segment-${UUID.randomUUID()}")
|
val segmentFile = File(context.cacheDir, "segment-${UUID.randomUUID()}")
|
||||||
segmentFiles.add(segmentFile)
|
val outputStream = segmentFile.outputStream()
|
||||||
|
try {
|
||||||
|
segmentFiles.add(segmentFile)
|
||||||
|
|
||||||
val segmentLength = downloadSource_Sequential(client, segmentFile.outputStream(), segment.uri) { segmentLength, totalRead, lastSpeed ->
|
val segmentLength = downloadSource_Sequential(client, outputStream, segment.uri) { segmentLength, totalRead, lastSpeed ->
|
||||||
val averageSegmentLength = if (index == 0) segmentLength else downloadedTotalLength / index
|
val averageSegmentLength = if (index == 0) segmentLength else downloadedTotalLength / index
|
||||||
val expectedTotalLength = averageSegmentLength * (variantPlaylist.segments.size - 1) + segmentLength
|
val expectedTotalLength = averageSegmentLength * (variantPlaylist.segments.size - 1) + segmentLength
|
||||||
onProgress(expectedTotalLength, downloadedTotalLength + totalRead, lastSpeed)
|
onProgress(expectedTotalLength, downloadedTotalLength + totalRead, lastSpeed)
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadedTotalLength += segmentLength
|
||||||
|
} finally {
|
||||||
|
outputStream.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadedTotalLength += segmentLength
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.i(TAG, "Combining segments into $targetFile");
|
Logger.i(TAG, "Combining segments into $targetFile");
|
||||||
@@ -473,6 +646,86 @@ class VideoDownload {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun downloadDashFileSource(name: String, client: ManagedHttpClient, source: IJSDashManifestRawSource, targetFile: File, onProgress: (Long, Long, Long) -> Unit): Long {
|
||||||
|
if(targetFile.exists())
|
||||||
|
targetFile.delete();
|
||||||
|
|
||||||
|
targetFile.createNewFile();
|
||||||
|
|
||||||
|
val sourceLength: Long?;
|
||||||
|
val fileStream = FileOutputStream(targetFile);
|
||||||
|
|
||||||
|
try{
|
||||||
|
var manifest = source.manifest;
|
||||||
|
if(source.hasGenerate)
|
||||||
|
manifest = source.generate();
|
||||||
|
if(manifest == null)
|
||||||
|
throw IllegalStateException("No manifest after generation");
|
||||||
|
|
||||||
|
//TODO: Temporary naive assume single-sourced dash
|
||||||
|
val foundTemplate = REGEX_DASH_TEMPLATE.find(manifest);
|
||||||
|
if(foundTemplate == null || foundTemplate.groupValues.size != 3)
|
||||||
|
throw IllegalStateException("No SegmentTemplate found in manifest (unsupported dash?)");
|
||||||
|
val foundTemplateUrl = foundTemplate.groupValues[1];
|
||||||
|
val foundCues = REGEX_DASH_CUE.findAll(foundTemplate.groupValues[2]);
|
||||||
|
if(foundCues.count() <= 0)
|
||||||
|
throw IllegalStateException("No Cues found in manifest (unsupported dash?)");
|
||||||
|
|
||||||
|
val executor = if(source is JSSource && source.hasRequestExecutor)
|
||||||
|
source.getRequestExecutor();
|
||||||
|
else
|
||||||
|
null;
|
||||||
|
val speedTracker = SpeedTracker(1000);
|
||||||
|
|
||||||
|
Logger.i(TAG, "Download $name Dash, CueCount: " + foundCues.count().toString());
|
||||||
|
|
||||||
|
var written = 0;
|
||||||
|
var indexCounter = 0;
|
||||||
|
onProgress(foundCues.count().toLong(), 0, 0);
|
||||||
|
for(cue in foundCues) {
|
||||||
|
val t = cue.groupValues[1];
|
||||||
|
val d = cue.groupValues[2];
|
||||||
|
|
||||||
|
val url = foundTemplateUrl.replace("\$Number\$", indexCounter.toString());
|
||||||
|
|
||||||
|
val data = if(executor != null)
|
||||||
|
executor.executeRequest("GET", url, null, mapOf());
|
||||||
|
else {
|
||||||
|
val resp = client.get(url, mutableMapOf());
|
||||||
|
if(!resp.isOk)
|
||||||
|
throw IllegalStateException("Dash request failed for index " + indexCounter.toString() + ", with code: " + resp.code.toString());
|
||||||
|
resp.body!!.bytes()
|
||||||
|
}
|
||||||
|
fileStream.write(data, 0, data.size);
|
||||||
|
speedTracker.addWork(data.size.toLong());
|
||||||
|
written += data.size;
|
||||||
|
|
||||||
|
onProgress(foundCues.count().toLong(), indexCounter.toLong(), speedTracker.lastSpeed);
|
||||||
|
|
||||||
|
indexCounter++;
|
||||||
|
}
|
||||||
|
sourceLength = written.toLong();
|
||||||
|
|
||||||
|
Logger.i(TAG, "$name downloadSource Finished");
|
||||||
|
}
|
||||||
|
catch(ioex: IOException) {
|
||||||
|
if(targetFile.exists() ?: false)
|
||||||
|
targetFile.delete();
|
||||||
|
if(ioex.message?.contains("ENOSPC") ?: false)
|
||||||
|
throw Exception("Not enough space on device", ioex);
|
||||||
|
else
|
||||||
|
throw ioex;
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
if(targetFile.exists() ?: false)
|
||||||
|
targetFile.delete();
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
fileStream.close();
|
||||||
|
}
|
||||||
|
return sourceLength!!;
|
||||||
|
}
|
||||||
private fun downloadFileSource(name: String, client: ManagedHttpClient, videoUrl: String, targetFile: File, onProgress: (Long, Long, Long) -> Unit): Long {
|
private fun downloadFileSource(name: String, client: ManagedHttpClient, videoUrl: String, targetFile: File, onProgress: (Long, Long, Long) -> Unit): Long {
|
||||||
if(targetFile.exists())
|
if(targetFile.exists())
|
||||||
targetFile.delete();
|
targetFile.delete();
|
||||||
@@ -484,17 +737,25 @@ class VideoDownload {
|
|||||||
|
|
||||||
try{
|
try{
|
||||||
val head = client.tryHead(videoUrl);
|
val head = client.tryHead(videoUrl);
|
||||||
|
val relatedPlugin = (video?.url ?: videoDetails?.url)?.let { StatePlatform.instance.getContentClient(it) }?.let { if(it is JSClient) it else null };
|
||||||
if(Settings.instance.downloads.byteRangeDownload && head?.containsKey("accept-ranges") == true && head.containsKey("content-length"))
|
if(Settings.instance.downloads.byteRangeDownload && head?.containsKey("accept-ranges") == true && head.containsKey("content-length"))
|
||||||
{
|
{
|
||||||
val concurrency = Settings.instance.downloads.getByteRangeThreadCount();
|
val maxParallel = if(relatedPlugin != null && relatedPlugin.config.maxDownloadParallelism > 0)
|
||||||
Logger.i(TAG, "Download $name ByteRange Parallel (${concurrency})");
|
relatedPlugin.config.maxDownloadParallelism else 99;
|
||||||
|
val concurrency = Math.min(maxParallel, Settings.instance.downloads.getByteRangeThreadCount());
|
||||||
|
Logger.i(TAG, "Download $name ByteRange Parallel (${concurrency}): " + videoUrl);
|
||||||
sourceLength = head["content-length"]!!.toLong();
|
sourceLength = head["content-length"]!!.toLong();
|
||||||
onProgress(sourceLength, 0, 0);
|
onProgress(sourceLength, 0, 0);
|
||||||
downloadSource_Ranges(name, client, fileStream, videoUrl, sourceLength, 1024*512, concurrency, onProgress);
|
downloadSource_Ranges(name, client, fileStream, videoUrl, sourceLength, 1024*512, concurrency, onProgress);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Logger.i(TAG, "Download $name Sequential");
|
Logger.i(TAG, "Download $name Sequential");
|
||||||
sourceLength = downloadSource_Sequential(client, fileStream, videoUrl, onProgress);
|
try {
|
||||||
|
sourceLength = downloadSource_Sequential(client, fileStream, videoUrl, onProgress);
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.w(TAG, "Failed to download sequentially (url = $videoUrl)")
|
||||||
|
throw e
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.i(TAG, "$name downloadSource Finished");
|
Logger.i(TAG, "$name downloadSource Finished");
|
||||||
@@ -518,17 +779,19 @@ class VideoDownload {
|
|||||||
return sourceLength!!;
|
return sourceLength!!;
|
||||||
}
|
}
|
||||||
private fun downloadSource_Sequential(client: ManagedHttpClient, fileStream: FileOutputStream, url: String, onProgress: (Long, Long, Long) -> Unit): Long {
|
private fun downloadSource_Sequential(client: ManagedHttpClient, fileStream: FileOutputStream, url: String, onProgress: (Long, Long, Long) -> Unit): Long {
|
||||||
val progressRate: Int = 4096 * 25;
|
val progressRate: Int = 4096 * 5;
|
||||||
var lastProgressCount: Int = 0;
|
var lastProgressCount: Int = 0;
|
||||||
val speedRate: Int = 4096 * 25;
|
val speedRate: Int = 4096 * 5;
|
||||||
var readSinceLastSpeedTest: Long = 0;
|
var readSinceLastSpeedTest: Long = 0;
|
||||||
var timeSinceLastSpeedTest: Long = System.currentTimeMillis();
|
var timeSinceLastSpeedTest: Long = System.currentTimeMillis();
|
||||||
|
|
||||||
var lastSpeed: Long = 0;
|
var lastSpeed: Long = 0;
|
||||||
|
|
||||||
val result = client.get(url);
|
val result = client.get(url);
|
||||||
if (!result.isOk)
|
if (!result.isOk) {
|
||||||
|
result.body?.close()
|
||||||
throw IllegalStateException("Failed to download source. Web[${result.code}] Error");
|
throw IllegalStateException("Failed to download source. Web[${result.code}] Error");
|
||||||
|
}
|
||||||
if (result.body == null)
|
if (result.body == null)
|
||||||
throw IllegalStateException("Failed to download source. Web[${result.code}] No response");
|
throw IllegalStateException("Failed to download source. Web[${result.code}] No response");
|
||||||
|
|
||||||
@@ -536,41 +799,114 @@ class VideoDownload {
|
|||||||
val sourceStream = result.body.byteStream();
|
val sourceStream = result.body.byteStream();
|
||||||
|
|
||||||
var totalRead: Long = 0;
|
var totalRead: Long = 0;
|
||||||
var read: Int;
|
try {
|
||||||
|
var read: Int;
|
||||||
|
val buffer = ByteArray(4096);
|
||||||
|
|
||||||
val buffer = ByteArray(4096);
|
do {
|
||||||
|
read = sourceStream.read(buffer);
|
||||||
|
if (read < 0)
|
||||||
|
break;
|
||||||
|
|
||||||
do {
|
fileStream.write(buffer, 0, read);
|
||||||
read = sourceStream.read(buffer);
|
|
||||||
if (read < 0)
|
|
||||||
break;
|
|
||||||
|
|
||||||
fileStream.write(buffer, 0, read);
|
totalRead += read;
|
||||||
|
|
||||||
totalRead += read;
|
readSinceLastSpeedTest += read;
|
||||||
|
if (totalRead.toDouble() / progressRate > lastProgressCount) {
|
||||||
|
onProgress(sourceLength, totalRead, lastSpeed);
|
||||||
|
lastProgressCount++;
|
||||||
|
}
|
||||||
|
if (readSinceLastSpeedTest > speedRate) {
|
||||||
|
val lastSpeedTime = timeSinceLastSpeedTest;
|
||||||
|
timeSinceLastSpeedTest = System.currentTimeMillis();
|
||||||
|
val timeSince = timeSinceLastSpeedTest - lastSpeedTime;
|
||||||
|
if (timeSince > 0)
|
||||||
|
lastSpeed = (readSinceLastSpeedTest / (timeSince / 1000.0)).toLong();
|
||||||
|
readSinceLastSpeedTest = 0;
|
||||||
|
}
|
||||||
|
|
||||||
readSinceLastSpeedTest += read;
|
if (isCancelled)
|
||||||
if (totalRead / progressRate > lastProgressCount) {
|
throw CancellationException("Cancelled");
|
||||||
onProgress(sourceLength, totalRead, lastSpeed);
|
} while (read > 0);
|
||||||
lastProgressCount++;
|
} finally {
|
||||||
}
|
sourceStream.close()
|
||||||
if (readSinceLastSpeedTest > speedRate) {
|
result.body.close()
|
||||||
val lastSpeedTime = timeSinceLastSpeedTest;
|
}
|
||||||
timeSinceLastSpeedTest = System.currentTimeMillis();
|
|
||||||
val timeSince = timeSinceLastSpeedTest - lastSpeedTime;
|
|
||||||
if (timeSince > 0)
|
|
||||||
lastSpeed = (readSinceLastSpeedTest / (timeSince / 1000.0)).toLong();
|
|
||||||
readSinceLastSpeedTest = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isCancelled)
|
|
||||||
throw CancellationException("Cancelled");
|
|
||||||
} while (read > 0);
|
|
||||||
|
|
||||||
lastSpeed = 0;
|
|
||||||
onProgress(sourceLength, totalRead, 0);
|
onProgress(sourceLength, totalRead, 0);
|
||||||
return sourceLength;
|
return sourceLength;
|
||||||
}
|
}
|
||||||
|
/*private fun downloadSource_Sequential(client: ManagedHttpClient, fileStream: FileOutputStream, url: String, onProgress: (Long, Long, Long) -> Unit): Long {
|
||||||
|
val progressRate: Int = 4096 * 25
|
||||||
|
var lastProgressCount: Int = 0
|
||||||
|
val speedRate: Int = 4096 * 25
|
||||||
|
var readSinceLastSpeedTest: Long = 0
|
||||||
|
var timeSinceLastSpeedTest: Long = System.currentTimeMillis()
|
||||||
|
|
||||||
|
var lastSpeed: Long = 0
|
||||||
|
|
||||||
|
var totalRead: Long = 0
|
||||||
|
var sourceLength: Long
|
||||||
|
val buffer = ByteArray(4096)
|
||||||
|
|
||||||
|
var isPartialDownload = false
|
||||||
|
var result: ManagedHttpClient.Response? = null
|
||||||
|
do {
|
||||||
|
result = client.get(url, if (isPartialDownload) hashMapOf("Range" to "bytes=$totalRead-") else hashMapOf())
|
||||||
|
if (isPartialDownload) {
|
||||||
|
if (result.code != 206)
|
||||||
|
throw IllegalStateException("Failed to download source, byte range fallback failed. Web[${result.code}] Error")
|
||||||
|
} else {
|
||||||
|
if (!result.isOk)
|
||||||
|
throw IllegalStateException("Failed to download source. Web[${result.code}] Error")
|
||||||
|
}
|
||||||
|
if (result.body == null)
|
||||||
|
throw IllegalStateException("Failed to download source. Web[${result.code}] No response")
|
||||||
|
|
||||||
|
isPartialDownload = true
|
||||||
|
sourceLength = result.body!!.contentLength()
|
||||||
|
val sourceStream = result.body!!.byteStream()
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
val read = sourceStream.read(buffer)
|
||||||
|
if (read <= 0) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
fileStream.write(buffer, 0, read)
|
||||||
|
|
||||||
|
totalRead += read
|
||||||
|
readSinceLastSpeedTest += read
|
||||||
|
|
||||||
|
if (totalRead / progressRate > lastProgressCount) {
|
||||||
|
onProgress(sourceLength, totalRead, lastSpeed)
|
||||||
|
lastProgressCount++
|
||||||
|
}
|
||||||
|
if (readSinceLastSpeedTest > speedRate) {
|
||||||
|
val lastSpeedTime = timeSinceLastSpeedTest
|
||||||
|
timeSinceLastSpeedTest = System.currentTimeMillis()
|
||||||
|
val timeSince = timeSinceLastSpeedTest - lastSpeedTime
|
||||||
|
if (timeSince > 0)
|
||||||
|
lastSpeed = (readSinceLastSpeedTest / (timeSince / 1000.0)).toLong()
|
||||||
|
readSinceLastSpeedTest = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCancelled)
|
||||||
|
throw CancellationException("Cancelled")
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.w(TAG, "Sequential download was interrupted, trying to fallback to byte ranges", e)
|
||||||
|
} finally {
|
||||||
|
sourceStream.close()
|
||||||
|
result.body?.close()
|
||||||
|
}
|
||||||
|
} while (totalRead < sourceLength)
|
||||||
|
|
||||||
|
onProgress(sourceLength, totalRead, 0)
|
||||||
|
return sourceLength
|
||||||
|
}*/
|
||||||
private fun downloadSource_Ranges(name: String, client: ManagedHttpClient, fileStream: FileOutputStream, url: String, sourceLength: Long, rangeSize: Int, concurrency: Int = 1, onProgress: (Long, Long, Long) -> Unit) {
|
private fun downloadSource_Ranges(name: String, client: ManagedHttpClient, fileStream: FileOutputStream, url: String, sourceLength: Long, rangeSize: Int, concurrency: Int = 1, onProgress: (Long, Long, Long) -> Unit) {
|
||||||
val progressRate: Int = 4096 * 5;
|
val progressRate: Int = 4096 * 5;
|
||||||
var lastProgressCount: Int = 0;
|
var lastProgressCount: Int = 0;
|
||||||
@@ -643,23 +979,47 @@ class VideoDownload {
|
|||||||
return tasks.map { it.get() };
|
return tasks.map { it.get() };
|
||||||
}
|
}
|
||||||
private fun requestByteRange(client: ManagedHttpClient, url: String, rangeStart: Long, rangeEnd: Long): Triple<ByteArray, Long, Long> {
|
private fun requestByteRange(client: ManagedHttpClient, url: String, rangeStart: Long, rangeEnd: Long): Triple<ByteArray, Long, Long> {
|
||||||
val toRead = rangeEnd - rangeStart;
|
var retryCount = 0
|
||||||
val req = client.get(url, mutableMapOf(Pair("Range", "bytes=${rangeStart}-${rangeEnd}")));
|
var lastException: Throwable? = null
|
||||||
if(!req.isOk)
|
|
||||||
throw IllegalStateException("Range request failed Code [${req.code}] due to: ${req.message}");
|
|
||||||
if(req.body == null)
|
|
||||||
throw IllegalStateException("Range request failed, No body");
|
|
||||||
val read = req.body.contentLength();
|
|
||||||
|
|
||||||
if(read < toRead)
|
while (retryCount <= 3) {
|
||||||
throw IllegalStateException("Byte-Range request attempted to provide less (${read} < ${toRead})");
|
try {
|
||||||
|
val toRead = rangeEnd - rangeStart;
|
||||||
|
val req = client.get(url, mutableMapOf(Pair("Range", "bytes=${rangeStart}-${rangeEnd}")));
|
||||||
|
if (!req.isOk) {
|
||||||
|
val bodyString = req.body?.string()
|
||||||
|
req.body?.close()
|
||||||
|
throw IllegalStateException("Range request failed Code [${req.code}] due to: ${req.message}");
|
||||||
|
}
|
||||||
|
if (req.body == null)
|
||||||
|
throw IllegalStateException("Range request failed, No body");
|
||||||
|
val read = req.body.contentLength();
|
||||||
|
|
||||||
return Triple(req.body.bytes(), rangeStart, rangeEnd);
|
if (read < toRead)
|
||||||
|
throw IllegalStateException("Byte-Range request attempted to provide less (${read} < ${toRead})");
|
||||||
|
|
||||||
|
return Triple(req.body.bytes(), rangeStart, rangeEnd);
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.w(TAG, "Failed to download range (url=${url} bytes=${rangeStart}-${rangeEnd})", e)
|
||||||
|
|
||||||
|
retryCount++
|
||||||
|
lastException = e
|
||||||
|
|
||||||
|
sleep(when (retryCount) {
|
||||||
|
1 -> 1000 + ((Math.random() * 300.0).toLong() - 150)
|
||||||
|
2 -> 2000 + ((Math.random() * 300.0).toLong() - 150)
|
||||||
|
3 -> 4000 + ((Math.random() * 300.0).toLong() - 150)
|
||||||
|
else -> 1000 + ((Math.random() * 300.0).toLong() - 150)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw lastException!!
|
||||||
}
|
}
|
||||||
|
|
||||||
fun validate() {
|
fun validate() {
|
||||||
Logger.i(TAG, "VideoDownload Validate [${name}]");
|
Logger.i(TAG, "VideoDownload Validate [${name}]");
|
||||||
if(videoSource != null) {
|
if(videoSourceToUse != null) {
|
||||||
if(videoFilePath == null)
|
if(videoFilePath == null)
|
||||||
throw IllegalStateException("Missing video file name after download");
|
throw IllegalStateException("Missing video file name after download");
|
||||||
val expectedFile = File(videoFilePath!!);
|
val expectedFile = File(videoFilePath!!);
|
||||||
@@ -670,7 +1030,7 @@ class VideoDownload {
|
|||||||
throw IllegalStateException("Expected size [${videoFileSize}], but found ${expectedFile.length()}");
|
throw IllegalStateException("Expected size [${videoFileSize}], but found ${expectedFile.length()}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(audioSource != null) {
|
if(audioSourceToUse != null) {
|
||||||
if(audioFilePath == null)
|
if(audioFilePath == null)
|
||||||
throw IllegalStateException("Missing audio file name after download");
|
throw IllegalStateException("Missing audio file name after download");
|
||||||
val expectedFile = File(audioFilePath!!);
|
val expectedFile = File(audioFilePath!!);
|
||||||
@@ -692,15 +1052,15 @@ class VideoDownload {
|
|||||||
fun complete() {
|
fun complete() {
|
||||||
Logger.i(TAG, "VideoDownload Complete [${name}]");
|
Logger.i(TAG, "VideoDownload Complete [${name}]");
|
||||||
val existing = StateDownloads.instance.getCachedVideo(id);
|
val existing = StateDownloads.instance.getCachedVideo(id);
|
||||||
val localVideoSource = videoFilePath?.let { LocalVideoSource.fromSource(videoSource!!, it, videoFileSize ?: 0) };
|
val localVideoSource = videoFilePath?.let { LocalVideoSource.fromSource(videoSourceToUse!!, it, videoFileSize ?: 0) };
|
||||||
val localAudioSource = audioFilePath?.let { LocalAudioSource.fromSource(audioSource!!, it, audioFileSize ?: 0) };
|
val localAudioSource = audioFilePath?.let { LocalAudioSource.fromSource(audioSourceToUse!!, it, audioFileSize ?: 0) };
|
||||||
val localSubtitleSource = subtitleFilePath?.let { LocalSubtitleSource.fromSource(subtitleSource!!, it) };
|
val localSubtitleSource = subtitleFilePath?.let { LocalSubtitleSource.fromSource(subtitleSource!!, it) };
|
||||||
|
|
||||||
if(localVideoSource != null && videoSource != null && videoSource is IStreamMetaDataSource)
|
if(localVideoSource != null && videoSourceToUse != null && videoSourceToUse is IStreamMetaDataSource)
|
||||||
localVideoSource.streamMetaData = (videoSource as IStreamMetaDataSource).streamMetaData;
|
localVideoSource.streamMetaData = (videoSourceToUse as IStreamMetaDataSource).streamMetaData;
|
||||||
|
|
||||||
if(localAudioSource != null && audioSource != null && audioSource is IStreamMetaDataSource)
|
if(localAudioSource != null && audioSourceToUse != null && audioSourceToUse is IStreamMetaDataSource)
|
||||||
localAudioSource.streamMetaData = (audioSource as IStreamMetaDataSource).streamMetaData;
|
localAudioSource.streamMetaData = (audioSourceToUse as IStreamMetaDataSource).streamMetaData;
|
||||||
|
|
||||||
if(existing != null) {
|
if(existing != null) {
|
||||||
existing.videoSerialized = videoDetails!!;
|
existing.videoSerialized = videoDetails!!;
|
||||||
@@ -757,6 +1117,9 @@ class VideoDownload {
|
|||||||
const val GROUP_PLAYLIST = "Playlist";
|
const val GROUP_PLAYLIST = "Playlist";
|
||||||
const val GROUP_WATCHLATER= "WatchLater";
|
const val GROUP_WATCHLATER= "WatchLater";
|
||||||
|
|
||||||
|
val REGEX_DASH_TEMPLATE = Regex("<SegmentTemplate .*?media=\"(.*?)\".*?>(.*?)<\\/SegmentTemplate>", RegexOption.DOT_MATCHES_ALL);
|
||||||
|
val REGEX_DASH_CUE = Regex("<S .*?t=\"([0-9]*?)\".*?d=\"([0-9]*?)\".*?\\/>", RegexOption.DOT_MATCHES_ALL);
|
||||||
|
|
||||||
fun videoContainerToExtension(container: String): String? {
|
fun videoContainerToExtension(container: String): String? {
|
||||||
if (container.contains("video/mp4") || container == "application/vnd.apple.mpegurl")
|
if (container.contains("video/mp4") || container == "application/vnd.apple.mpegurl")
|
||||||
return "mp4";
|
return "mp4";
|
||||||
@@ -803,4 +1166,27 @@ class VideoDownload {
|
|||||||
return "subtitle";
|
return "subtitle";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SpeedTracker {
|
||||||
|
private val segmentStart: Long;
|
||||||
|
private val intervalMs: Long;
|
||||||
|
private var workDone: Long;
|
||||||
|
var lastSpeed: Long;
|
||||||
|
constructor(intervalMs: Long) {
|
||||||
|
segmentStart = System.currentTimeMillis();
|
||||||
|
this.intervalMs = intervalMs;
|
||||||
|
this.workDone = 0;
|
||||||
|
this.lastSpeed = 0;
|
||||||
|
}
|
||||||
|
fun addWork(work: Long) {
|
||||||
|
val now = System.currentTimeMillis();
|
||||||
|
if((now - segmentStart) > intervalMs)
|
||||||
|
{
|
||||||
|
lastSpeed = workDone;
|
||||||
|
workDone = 0;
|
||||||
|
}
|
||||||
|
workDone += work;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,8 @@ import com.caoccao.javet.exceptions.JavetException
|
|||||||
import com.caoccao.javet.exceptions.JavetExecutionException
|
import com.caoccao.javet.exceptions.JavetExecutionException
|
||||||
import com.caoccao.javet.interop.V8Host
|
import com.caoccao.javet.interop.V8Host
|
||||||
import com.caoccao.javet.interop.V8Runtime
|
import com.caoccao.javet.interop.V8Runtime
|
||||||
|
import com.caoccao.javet.interop.options.V8Flags
|
||||||
|
import com.caoccao.javet.interop.options.V8RuntimeOptions
|
||||||
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
|
||||||
@@ -133,9 +135,10 @@ class V8Plugin {
|
|||||||
synchronized(_runtimeLock) {
|
synchronized(_runtimeLock) {
|
||||||
if (_runtime != null)
|
if (_runtime != null)
|
||||||
return;
|
return;
|
||||||
|
//V8RuntimeOptions.V8_FLAGS.setUseStrict(true);
|
||||||
val host = V8Host.getV8Instance();
|
val host = V8Host.getV8Instance();
|
||||||
val options = host.jsRuntimeType.getRuntimeOptions();
|
val options = host.jsRuntimeType.getRuntimeOptions();
|
||||||
|
|
||||||
_runtime = host.createV8Runtime(options);
|
_runtime = host.createV8Runtime(options);
|
||||||
if (!host.isIsolateCreated)
|
if (!host.isIsolateCreated)
|
||||||
throw IllegalStateException("Isolate not created");
|
throw IllegalStateException("Isolate not created");
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ package com.futo.platformplayer.engine.packages
|
|||||||
|
|
||||||
import com.caoccao.javet.annotations.V8Function
|
import com.caoccao.javet.annotations.V8Function
|
||||||
import com.caoccao.javet.annotations.V8Property
|
import com.caoccao.javet.annotations.V8Property
|
||||||
|
import com.caoccao.javet.values.V8Value
|
||||||
import com.futo.platformplayer.BuildConfig
|
import com.futo.platformplayer.BuildConfig
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateDeveloper
|
import com.futo.platformplayer.states.StateDeveloper
|
||||||
import com.futo.platformplayer.UIDialogs
|
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.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.JSClientConstants
|
||||||
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.internal.JSHttpClient
|
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
|
||||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
@@ -49,6 +51,16 @@ class PackageBridge : V8Package {
|
|||||||
fun buildFlavor(): String {
|
fun buildFlavor(): String {
|
||||||
return BuildConfig.FLAVOR;
|
return BuildConfig.FLAVOR;
|
||||||
}
|
}
|
||||||
|
@V8Property
|
||||||
|
fun buildSpecVersion(): Int {
|
||||||
|
return JSClientConstants.PLUGIN_SPEC_VERSION;
|
||||||
|
}
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun dispose(value: V8Value) {
|
||||||
|
Logger.e(TAG, "Manual dispose: " + value.javaClass.name);
|
||||||
|
value.close();
|
||||||
|
}
|
||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
fun toast(str: String) {
|
fun toast(str: String) {
|
||||||
|
|||||||
@@ -68,6 +68,10 @@ class PackageDOMParser : V8Package {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@V8Property
|
@V8Property
|
||||||
|
fun parentElement(): DOMNode? {
|
||||||
|
return parentNode();
|
||||||
|
}
|
||||||
|
@V8Property
|
||||||
fun attributes(): Map<String, String> = _element.attributes().associate { Pair(it.key, it.value) }
|
fun attributes(): Map<String, String> = _element.attributes().associate { Pair(it.key, it.value) }
|
||||||
@V8Property
|
@V8Property
|
||||||
fun innerHTML(): String = _element.html();
|
fun innerHTML(): String = _element.html();
|
||||||
@@ -76,6 +80,8 @@ class PackageDOMParser : V8Package {
|
|||||||
@V8Property
|
@V8Property
|
||||||
fun textContent(): String = _element.text();
|
fun textContent(): String = _element.text();
|
||||||
@V8Property
|
@V8Property
|
||||||
|
fun tagName(): String = _element.tagName().uppercase();
|
||||||
|
@V8Property
|
||||||
fun text(): String = _element.text().ifEmpty { data() };
|
fun text(): String = _element.text().ifEmpty { data() };
|
||||||
@V8Property
|
@V8Property
|
||||||
fun data(): String = _element.data();
|
fun data(): String = _element.data();
|
||||||
|
|||||||
@@ -7,7 +7,11 @@ import com.caoccao.javet.enums.V8ConversionMode
|
|||||||
import com.caoccao.javet.enums.V8ProxyMode
|
import com.caoccao.javet.enums.V8ProxyMode
|
||||||
import com.caoccao.javet.interop.V8Runtime
|
import com.caoccao.javet.interop.V8Runtime
|
||||||
import com.caoccao.javet.values.V8Value
|
import com.caoccao.javet.values.V8Value
|
||||||
|
import com.caoccao.javet.values.primitive.V8ValueString
|
||||||
|
import com.caoccao.javet.values.reference.V8ValueArrayBuffer
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
|
import com.caoccao.javet.values.reference.V8ValueSharedArrayBuffer
|
||||||
|
import com.caoccao.javet.values.reference.V8ValueTypedArray
|
||||||
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.api.media.platforms.js.internal.JSHttpClient
|
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
|
||||||
@@ -16,6 +20,9 @@ import com.futo.platformplayer.engine.V8Plugin
|
|||||||
import com.futo.platformplayer.engine.internal.IV8Convertable
|
import com.futo.platformplayer.engine.internal.IV8Convertable
|
||||||
import com.futo.platformplayer.engine.internal.V8BindObject
|
import com.futo.platformplayer.engine.internal.V8BindObject
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import java.net.SocketTimeoutException
|
import java.net.SocketTimeoutException
|
||||||
import kotlin.streams.asSequence
|
import kotlin.streams.asSequence
|
||||||
|
|
||||||
@@ -64,33 +71,44 @@ class PackageHttp: V8Package {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
fun request(method: String, url: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BridgeHttpResponse {
|
fun request(method: String, url: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false, bytesResult: Boolean = false) : IBridgeHttpResponse {
|
||||||
return if(useAuth)
|
return if(useAuth)
|
||||||
_packageClientAuth.request(method, url, headers)
|
_packageClientAuth.request(method, url, headers, if(bytesResult) ReturnType.BYTES else ReturnType.STRING)
|
||||||
else
|
else
|
||||||
_packageClient.request(method, url, headers);
|
_packageClient.request(method, url, headers, if(bytesResult) ReturnType.BYTES else ReturnType.STRING);
|
||||||
}
|
}
|
||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
fun requestWithBody(method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BridgeHttpResponse {
|
fun requestWithBody(method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false, bytesResult: Boolean = false) : IBridgeHttpResponse {
|
||||||
return if(useAuth)
|
return if(useAuth)
|
||||||
_packageClientAuth.requestWithBody(method, url, body, headers)
|
_packageClientAuth.requestWithBody(method, url, body, headers, if(bytesResult) ReturnType.BYTES else ReturnType.STRING)
|
||||||
else
|
else
|
||||||
_packageClient.requestWithBody(method, url, body, headers);
|
_packageClient.requestWithBody(method, url, body, headers, if(bytesResult) ReturnType.BYTES else ReturnType.STRING);
|
||||||
}
|
}
|
||||||
@V8Function
|
@V8Function
|
||||||
fun GET(url: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BridgeHttpResponse {
|
fun GET(url: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false, useByteResponse: Boolean = false) : IBridgeHttpResponse {
|
||||||
return if(useAuth)
|
return if(useAuth)
|
||||||
_packageClientAuth.GET(url, headers)
|
_packageClientAuth.GET(url, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING)
|
||||||
else
|
else
|
||||||
_packageClient.GET(url, headers);
|
_packageClient.GET(url, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
|
||||||
}
|
}
|
||||||
@V8Function
|
@V8Function
|
||||||
fun POST(url: String, body: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BridgeHttpResponse {
|
fun POST(url: String, body: Any, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false, useByteResponse: Boolean = false) : IBridgeHttpResponse {
|
||||||
return if(useAuth)
|
|
||||||
_packageClientAuth.POST(url, body, headers)
|
val client = if(useAuth) _packageClientAuth else _packageClient;
|
||||||
|
|
||||||
|
if(body is V8ValueString)
|
||||||
|
return client.POST(url, body.value, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
|
||||||
|
else if(body is String)
|
||||||
|
return client.POST(url, body, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
|
||||||
|
else if(body is V8ValueTypedArray)
|
||||||
|
return client.POST(url, body.toBytes(), headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
|
||||||
|
else if(body is ByteArray)
|
||||||
|
return client.POST(url, body, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
|
||||||
|
else if(body is ArrayList<*>) //Avoid this case, used purely for testing
|
||||||
|
return client.POST(url, body.map { (it as Double).toInt().toByte() }.toByteArray(), headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
|
||||||
else
|
else
|
||||||
_packageClient.POST(url, body, headers);
|
throw NotImplementedError("Body type " + body?.javaClass?.name?.toString() + " not implemented for POST");
|
||||||
}
|
}
|
||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
@@ -111,8 +129,19 @@ class PackageHttp: V8Package {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IBridgeHttpResponse {
|
||||||
|
val url: String;
|
||||||
|
val code: Int;
|
||||||
|
val headers: Map<String, List<String>>?;
|
||||||
|
}
|
||||||
|
|
||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
class BridgeHttpResponse(val url: String, val code: Int, val body: String?, val headers: Map<String, List<String>>? = null) : IV8Convertable {
|
class BridgeHttpStringResponse(
|
||||||
|
override val url: String,
|
||||||
|
override val code: Int, val
|
||||||
|
body: String?,
|
||||||
|
override val headers: Map<String, List<String>>? = null) : IV8Convertable, IBridgeHttpResponse {
|
||||||
|
|
||||||
val isOk = code >= 200 && code < 300;
|
val isOk = code >= 200 && code < 300;
|
||||||
|
|
||||||
override fun toV8(runtime: V8Runtime): V8Value? {
|
override fun toV8(runtime: V8Runtime): V8Value? {
|
||||||
@@ -125,6 +154,37 @@ class PackageHttp: V8Package {
|
|||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@kotlinx.serialization.Serializable
|
||||||
|
class BridgeHttpBytesResponse: IV8Convertable, IBridgeHttpResponse {
|
||||||
|
override val url: String;
|
||||||
|
override val code: Int;
|
||||||
|
val body: ByteArray?;
|
||||||
|
override val headers: Map<String, List<String>>?;
|
||||||
|
|
||||||
|
val isOk: Boolean;
|
||||||
|
|
||||||
|
constructor(url: String, code: Int, body: ByteArray? = null, headers: Map<String, List<String>>? = null) {
|
||||||
|
this.url = url;
|
||||||
|
this.code = code;
|
||||||
|
this.body = body;
|
||||||
|
this.headers = headers;
|
||||||
|
this.isOk = code >= 200 && code < 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toV8(runtime: V8Runtime): V8Value? {
|
||||||
|
val obj = runtime.createV8ValueObject();
|
||||||
|
obj.set("url", url);
|
||||||
|
obj.set("code", code);
|
||||||
|
if(body != null) {
|
||||||
|
val buffer = runtime.createV8ValueArrayBuffer(body.size);
|
||||||
|
buffer.fromBytes(body);
|
||||||
|
obj.set("body", body);
|
||||||
|
}
|
||||||
|
obj.set("headers", headers);
|
||||||
|
obj.set("isOk", isOk);
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//TODO: This object is currently re-wrapped each modification, this is due to an issue passing the same object back and forth, should be fixed in future.
|
//TODO: This object is currently re-wrapped each modification, this is due to an issue passing the same object back and forth, should be fixed in future.
|
||||||
@V8Convert(mode = V8ConversionMode.AllowOnly, proxyMode = V8ProxyMode.Class)
|
@V8Convert(mode = V8ConversionMode.AllowOnly, proxyMode = V8ProxyMode.Class)
|
||||||
@@ -147,6 +207,12 @@ class PackageHttp: V8Package {
|
|||||||
fun POST(url: String, body: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BatchBuilder
|
fun POST(url: String, body: String, headers: MutableMap<String, String> = HashMap(), useAuth: Boolean = false) : BatchBuilder
|
||||||
= clientPOST(_package.getDefaultClient(useAuth), url, body, headers);
|
= clientPOST(_package.getDefaultClient(useAuth), url, body, headers);
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun DUMMY(): BatchBuilder {
|
||||||
|
_reqs.add(Pair(_package.getDefaultClient(false), RequestDescriptor("DUMMY", "", mutableMapOf())));
|
||||||
|
return BatchBuilder(_package, _reqs);
|
||||||
|
}
|
||||||
|
|
||||||
//Client-specific
|
//Client-specific
|
||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
@@ -169,12 +235,14 @@ class PackageHttp: V8Package {
|
|||||||
|
|
||||||
//Finalizer
|
//Finalizer
|
||||||
@V8Function
|
@V8Function
|
||||||
fun execute(): List<BridgeHttpResponse> {
|
fun execute(): List<IBridgeHttpResponse?> {
|
||||||
return _reqs.parallelStream().map {
|
return _reqs.parallelStream().map {
|
||||||
|
if(it.second.method == "DUMMY")
|
||||||
|
return@map null;
|
||||||
if(it.second.body != null)
|
if(it.second.body != null)
|
||||||
return@map it.first.requestWithBody(it.second.method, it.second.url, it.second.body!!, it.second.headers);
|
return@map it.first.requestWithBody(it.second.method, it.second.url, it.second.body!!, it.second.headers, it.second.respType);
|
||||||
else
|
else
|
||||||
return@map it.first.request(it.second.method, it.second.url, it.second.headers);
|
return@map it.first.request(it.second.method, it.second.url, it.second.headers, it.second.respType);
|
||||||
}
|
}
|
||||||
.asSequence()
|
.asSequence()
|
||||||
.toList();
|
.toList();
|
||||||
@@ -230,65 +298,116 @@ class PackageHttp: V8Package {
|
|||||||
if(_client is JSHttpClient)
|
if(_client is JSHttpClient)
|
||||||
_client.doAllowNewCookies = allow;
|
_client.doAllowNewCookies = allow;
|
||||||
}
|
}
|
||||||
|
@V8Function
|
||||||
|
fun setTimeout(timeoutMs: Int) {
|
||||||
|
if(_client is JSHttpClient) {
|
||||||
|
_client.setTimeout(timeoutMs.toLong());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
fun request(method: String, url: String, headers: MutableMap<String, String> = HashMap()) : BridgeHttpResponse {
|
fun request(method: String, url: String, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType) : IBridgeHttpResponse {
|
||||||
applyDefaultHeaders(headers);
|
applyDefaultHeaders(headers);
|
||||||
return logExceptions {
|
return logExceptions {
|
||||||
return@logExceptions catchHttp {
|
return@logExceptions catchHttp {
|
||||||
val client = _client;
|
val client = _client;
|
||||||
//logRequest(method, url, headers, null);
|
//logRequest(method, url, headers, null);
|
||||||
val resp = client.requestMethod(method, url, headers);
|
val resp = client.requestMethod(method, url, headers);
|
||||||
val responseBody = resp.body?.string();
|
|
||||||
//logResponse(method, url, resp.code, resp.headers, responseBody);
|
//logResponse(method, url, resp.code, resp.headers, responseBody);
|
||||||
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers,
|
return@catchHttp when(returnType) {
|
||||||
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
ReturnType.STRING -> BridgeHttpStringResponse(resp.url, resp.code, resp.body?.string(), sanitizeResponseHeaders(resp.headers,
|
||||||
|
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
||||||
|
ReturnType.BYTES -> BridgeHttpBytesResponse(resp.url, resp.code, resp.body?.bytes(), sanitizeResponseHeaders(resp.headers,
|
||||||
|
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
||||||
|
else -> throw NotImplementedError("Return type " + returnType.toString() + " not implemented");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@V8Function
|
@V8Function
|
||||||
fun requestWithBody(method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap()) : BridgeHttpResponse {
|
fun requestWithBody(method: String, url: String, body:String, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType) : IBridgeHttpResponse {
|
||||||
applyDefaultHeaders(headers);
|
applyDefaultHeaders(headers);
|
||||||
return logExceptions {
|
return logExceptions {
|
||||||
catchHttp {
|
catchHttp {
|
||||||
val client = _client;
|
val client = _client;
|
||||||
//logRequest(method, url, headers, body);
|
//logRequest(method, url, headers, body);
|
||||||
val resp = client.requestMethod(method, url, body, headers);
|
val resp = client.requestMethod(method, url, body, headers);
|
||||||
val responseBody = resp.body?.string();
|
|
||||||
//logResponse(method, url, resp.code, resp.headers, responseBody);
|
//logResponse(method, url, resp.code, resp.headers, responseBody);
|
||||||
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers,
|
|
||||||
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
return@catchHttp when(returnType) {
|
||||||
|
ReturnType.STRING -> BridgeHttpStringResponse(resp.url, resp.code, resp.body?.string(), sanitizeResponseHeaders(resp.headers,
|
||||||
|
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
||||||
|
ReturnType.BYTES -> BridgeHttpBytesResponse(resp.url, resp.code, resp.body?.bytes(), sanitizeResponseHeaders(resp.headers,
|
||||||
|
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
||||||
|
else -> throw NotImplementedError("Return type " + returnType.toString() + " not implemented");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
fun GET(url: String, headers: MutableMap<String, String> = HashMap()) : BridgeHttpResponse {
|
fun GET(url: String, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType = ReturnType.STRING) : IBridgeHttpResponse {
|
||||||
applyDefaultHeaders(headers);
|
applyDefaultHeaders(headers);
|
||||||
return logExceptions {
|
return logExceptions {
|
||||||
catchHttp {
|
catchHttp {
|
||||||
val client = _client;
|
val client = _client;
|
||||||
//logRequest("GET", url, headers, null);
|
//logRequest("GET", url, headers, null);
|
||||||
val resp = client.get(url, headers);
|
val resp = client.get(url, headers);
|
||||||
val responseBody = resp.body?.string();
|
//val responseBody = resp.body?.string();
|
||||||
//logResponse("GET", url, resp.code, resp.headers, responseBody);
|
//logResponse("GET", url, resp.code, resp.headers, responseBody);
|
||||||
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers,
|
|
||||||
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
|
||||||
|
return@catchHttp when(returnType) {
|
||||||
|
ReturnType.STRING -> BridgeHttpStringResponse(resp.url, resp.code, resp.body?.string(), sanitizeResponseHeaders(resp.headers,
|
||||||
|
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
||||||
|
ReturnType.BYTES -> BridgeHttpBytesResponse(resp.url, resp.code, resp.body?.bytes(), sanitizeResponseHeaders(resp.headers,
|
||||||
|
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
||||||
|
else -> throw NotImplementedError("Return type " + returnType.toString() + " not implemented");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@V8Function
|
@V8Function
|
||||||
fun POST(url: String, body: String, headers: MutableMap<String, String> = HashMap()) : BridgeHttpResponse {
|
fun POST(url: String, body: String, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType = ReturnType.STRING) : IBridgeHttpResponse {
|
||||||
applyDefaultHeaders(headers);
|
applyDefaultHeaders(headers);
|
||||||
return logExceptions {
|
return logExceptions {
|
||||||
catchHttp {
|
catchHttp {
|
||||||
val client = _client;
|
val client = _client;
|
||||||
//logRequest("POST", url, headers, body);
|
//logRequest("POST", url, headers, body);
|
||||||
val resp = client.post(url, body, headers);
|
val resp = client.post(url, body, headers);
|
||||||
val responseBody = resp.body?.string();
|
//val responseBody = resp.body?.string();
|
||||||
//logResponse("POST", url, resp.code, resp.headers, responseBody);
|
//logResponse("POST", url, resp.code, resp.headers, responseBody);
|
||||||
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers,
|
|
||||||
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
|
||||||
|
return@catchHttp when(returnType) {
|
||||||
|
ReturnType.STRING -> BridgeHttpStringResponse(resp.url, resp.code, resp.body?.string(), sanitizeResponseHeaders(resp.headers,
|
||||||
|
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
||||||
|
ReturnType.BYTES -> BridgeHttpBytesResponse(resp.url, resp.code, resp.body?.bytes(), sanitizeResponseHeaders(resp.headers,
|
||||||
|
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
||||||
|
else -> throw NotImplementedError("Return type " + returnType.toString() + " not implemented");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@V8Function
|
||||||
|
fun POST(url: String, body: ByteArray, headers: MutableMap<String, String> = HashMap(), returnType: ReturnType = ReturnType.STRING) : IBridgeHttpResponse {
|
||||||
|
applyDefaultHeaders(headers);
|
||||||
|
return logExceptions {
|
||||||
|
catchHttp {
|
||||||
|
val client = _client;
|
||||||
|
//logRequest("POST", url, headers, body);
|
||||||
|
val resp = client.post(url, body, headers);
|
||||||
|
//val responseBody = resp.body?.string();
|
||||||
|
//logResponse("POST", url, resp.code, resp.headers, responseBody);
|
||||||
|
|
||||||
|
|
||||||
|
return@catchHttp when(returnType) {
|
||||||
|
ReturnType.STRING -> BridgeHttpStringResponse(resp.url, resp.code, resp.body?.string(), sanitizeResponseHeaders(resp.headers,
|
||||||
|
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
||||||
|
ReturnType.BYTES -> BridgeHttpBytesResponse(resp.url, resp.code, resp.body?.bytes(), sanitizeResponseHeaders(resp.headers,
|
||||||
|
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
||||||
|
else -> throw NotImplementedError("Return type " + returnType.toString() + " not implemented");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -388,13 +507,13 @@ class PackageHttp: V8Package {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun catchHttp(handle: ()->BridgeHttpResponse): BridgeHttpResponse {
|
private fun catchHttp(handle: ()->IBridgeHttpResponse): IBridgeHttpResponse {
|
||||||
try{
|
try{
|
||||||
return handle();
|
return handle();
|
||||||
}
|
}
|
||||||
//Forward timeouts
|
//Forward timeouts
|
||||||
catch(ex: SocketTimeoutException) {
|
catch(ex: SocketTimeoutException) {
|
||||||
return BridgeHttpResponse("", 408, null);
|
return BridgeHttpStringResponse("", 408, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -514,20 +633,25 @@ class PackageHttp: V8Package {
|
|||||||
val url: String,
|
val url: String,
|
||||||
val headers: MutableMap<String, String>,
|
val headers: MutableMap<String, String>,
|
||||||
val body: String? = null,
|
val body: String? = null,
|
||||||
val contentType: String? = null
|
val contentType: String? = null,
|
||||||
|
val respType: ReturnType = ReturnType.STRING
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun catchHttp(handle: ()->BridgeHttpResponse): BridgeHttpResponse {
|
private fun catchHttp(handle: ()->BridgeHttpStringResponse): BridgeHttpStringResponse {
|
||||||
try{
|
try{
|
||||||
return handle();
|
return handle();
|
||||||
}
|
}
|
||||||
//Forward timeouts
|
//Forward timeouts
|
||||||
catch(ex: SocketTimeoutException) {
|
catch(ex: SocketTimeoutException) {
|
||||||
return BridgeHttpResponse("", 408, null);
|
return BridgeHttpStringResponse("", 408, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
enum class ReturnType(val value: Int) {
|
||||||
|
STRING(0),
|
||||||
|
BYTES(1);
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "PackageHttp";
|
private const val TAG = "PackageHttp";
|
||||||
|
|||||||
@@ -133,6 +133,10 @@ class ChannelAboutFragment : Fragment, IChannelTabFragment {
|
|||||||
Logger.w(TAG, "Failed to parse claim=$c", e)
|
Logger.w(TAG, "Failed to parse claim=$c", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if(!map.containsKey("Harbor"))
|
||||||
|
this.context?.let {
|
||||||
|
map.set("Harbor", polycentricProfile.getHarborUrl(it));
|
||||||
|
}
|
||||||
|
|
||||||
if (map.isNotEmpty())
|
if (map.isNotEmpty())
|
||||||
setLinks(map, if (polycentricProfile.systemState.username.isNotBlank()) polycentricProfile.systemState.username else _lastChannel?.name ?: "")
|
setLinks(map, if (polycentricProfile.systemState.username.isNotBlank()) polycentricProfile.systemState.username else _lastChannel?.name ?: "")
|
||||||
|
|||||||
+15
-6
@@ -1,12 +1,13 @@
|
|||||||
package com.futo.platformplayer.fragment.channel.tab
|
package com.futo.platformplayer.fragment.channel.tab
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
@@ -15,7 +16,6 @@ import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
|||||||
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.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.video.IPlatformVideo
|
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSPager
|
import com.futo.platformplayer.api.media.platforms.js.models.JSPager
|
||||||
import com.futo.platformplayer.api.media.structures.IAsyncPager
|
import com.futo.platformplayer.api.media.structures.IAsyncPager
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
@@ -41,10 +41,11 @@ import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
|||||||
import com.futo.platformplayer.views.adapters.feedtypes.PreviewContentListAdapter
|
import com.futo.platformplayer.views.adapters.feedtypes.PreviewContentListAdapter
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
||||||
private var _recyclerResults: RecyclerView? = null;
|
private var _recyclerResults: RecyclerView? = null;
|
||||||
private var _llmVideo: LinearLayoutManager? = null;
|
private var _glmVideo: GridLayoutManager? = null;
|
||||||
private var _loading = false;
|
private var _loading = false;
|
||||||
private var _pager_parent: IPager<IPlatformContent>? = null;
|
private var _pager_parent: IPager<IPlatformContent>? = null;
|
||||||
private var _pager: IPager<IPlatformContent>? = null;
|
private var _pager: IPager<IPlatformContent>? = null;
|
||||||
@@ -118,7 +119,7 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
|||||||
super.onScrolled(recyclerView, dx, dy);
|
super.onScrolled(recyclerView, dx, dy);
|
||||||
|
|
||||||
val recyclerResults = _recyclerResults ?: return;
|
val recyclerResults = _recyclerResults ?: return;
|
||||||
val llmVideo = _llmVideo ?: return;
|
val llmVideo = _glmVideo ?: return;
|
||||||
|
|
||||||
val visibleItemCount = recyclerResults.childCount;
|
val visibleItemCount = recyclerResults.childCount;
|
||||||
val firstVisibleItem = llmVideo.findFirstVisibleItemPosition();
|
val firstVisibleItem = llmVideo.findFirstVisibleItemPosition();
|
||||||
@@ -163,9 +164,10 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
|||||||
this.onLongPress.subscribe(this@ChannelContentsFragment.onLongPress::emit);
|
this.onLongPress.subscribe(this@ChannelContentsFragment.onLongPress::emit);
|
||||||
}
|
}
|
||||||
|
|
||||||
_llmVideo = LinearLayoutManager(view.context);
|
val numColumns = max((resources.configuration.screenWidthDp.toDouble() / resources.getInteger(R.integer.column_width_dp)).toInt(), 1)
|
||||||
|
_glmVideo = GridLayoutManager(view.context, numColumns);
|
||||||
_recyclerResults?.adapter = _adapterResults;
|
_recyclerResults?.adapter = _adapterResults;
|
||||||
_recyclerResults?.layoutManager = _llmVideo;
|
_recyclerResults?.layoutManager = _glmVideo;
|
||||||
_recyclerResults?.addOnScrollListener(_scrollListener);
|
_recyclerResults?.addOnScrollListener(_scrollListener);
|
||||||
|
|
||||||
return view;
|
return view;
|
||||||
@@ -181,6 +183,13 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
|||||||
_nextPageHandler.cancel();
|
_nextPageHandler.cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
|
super.onConfigurationChanged(newConfig)
|
||||||
|
|
||||||
|
_glmVideo?.spanCount =
|
||||||
|
max((resources.configuration.screenWidthDp.toDouble() / resources.getInteger(R.integer.column_width_dp)).toInt(), 1)
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
private fun setPager(pager: IPager<IPlatformContent>, cache: FeedFragment.ItemCache<IPlatformContent>? = null) {
|
private fun setPager(pager: IPager<IPlatformContent>, cache: FeedFragment.ItemCache<IPlatformContent>? = null) {
|
||||||
if (_pager_parent != null && _pager_parent is IRefreshPager<*>) {
|
if (_pager_parent != null && _pager_parent is IRefreshPager<*>) {
|
||||||
|
|||||||
+15
-5
@@ -1,12 +1,13 @@
|
|||||||
package com.futo.platformplayer.fragment.channel.tab
|
package com.futo.platformplayer.fragment.channel.tab
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
@@ -36,10 +37,11 @@ import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
|||||||
import com.futo.platformplayer.views.adapters.feedtypes.PreviewContentListAdapter
|
import com.futo.platformplayer.views.adapters.feedtypes.PreviewContentListAdapter
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
|
class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
|
||||||
private var _recyclerResults: RecyclerView? = null
|
private var _recyclerResults: RecyclerView? = null
|
||||||
private var _llmPlaylist: LinearLayoutManager? = null
|
private var _glmPlaylist: GridLayoutManager? = null
|
||||||
private var _loading = false
|
private var _loading = false
|
||||||
private var _pagerParent: IPager<IPlatformPlaylist>? = null
|
private var _pagerParent: IPager<IPlatformPlaylist>? = null
|
||||||
private var _pager: IPager<IPlatformPlaylist>? = null
|
private var _pager: IPager<IPlatformPlaylist>? = null
|
||||||
@@ -109,7 +111,7 @@ class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
|
|||||||
super.onScrolled(recyclerView, dx, dy)
|
super.onScrolled(recyclerView, dx, dy)
|
||||||
|
|
||||||
val recyclerResults = _recyclerResults ?: return
|
val recyclerResults = _recyclerResults ?: return
|
||||||
val llmPlaylist = _llmPlaylist ?: return
|
val llmPlaylist = _glmPlaylist ?: return
|
||||||
|
|
||||||
val visibleItemCount = recyclerResults.childCount
|
val visibleItemCount = recyclerResults.childCount
|
||||||
val firstVisibleItem = llmPlaylist.findFirstVisibleItemPosition()
|
val firstVisibleItem = llmPlaylist.findFirstVisibleItemPosition()
|
||||||
@@ -158,9 +160,10 @@ class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
|
|||||||
this.onLongPress.subscribe(this@ChannelPlaylistsFragment.onLongPress::emit)
|
this.onLongPress.subscribe(this@ChannelPlaylistsFragment.onLongPress::emit)
|
||||||
}
|
}
|
||||||
|
|
||||||
_llmPlaylist = LinearLayoutManager(view.context)
|
val numColumns = max((resources.configuration.screenWidthDp.toDouble() / resources.getInteger(R.integer.column_width_dp)).toInt(), 1)
|
||||||
|
_glmPlaylist = GridLayoutManager(view.context, numColumns)
|
||||||
_recyclerResults?.adapter = _adapterResults
|
_recyclerResults?.adapter = _adapterResults
|
||||||
_recyclerResults?.layoutManager = _llmPlaylist
|
_recyclerResults?.layoutManager = _glmPlaylist
|
||||||
_recyclerResults?.addOnScrollListener(_scrollListener)
|
_recyclerResults?.addOnScrollListener(_scrollListener)
|
||||||
|
|
||||||
return view
|
return view
|
||||||
@@ -176,6 +179,13 @@ class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
|
|||||||
_nextPageHandler.cancel()
|
_nextPageHandler.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
|
super.onConfigurationChanged(newConfig)
|
||||||
|
|
||||||
|
_glmPlaylist?.spanCount =
|
||||||
|
max((resources.configuration.screenWidthDp.toDouble() / resources.getInteger(R.integer.column_width_dp)).toInt(), 1)
|
||||||
|
}
|
||||||
|
|
||||||
private fun setPager(
|
private fun setPager(
|
||||||
pager: IPager<IPlatformPlaylist>
|
pager: IPager<IPlatformPlaylist>
|
||||||
) {
|
) {
|
||||||
|
|||||||
+55
-25
@@ -7,6 +7,7 @@ import android.annotation.SuppressLint
|
|||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.res.Configuration
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@@ -34,7 +35,7 @@ import kotlin.math.roundToInt
|
|||||||
class MenuBottomBarFragment : MainActivityFragment() {
|
class MenuBottomBarFragment : MainActivityFragment() {
|
||||||
private var _view: MenuBottomBarView? = null;
|
private var _view: MenuBottomBarView? = null;
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
val view = MenuBottomBarView(this, inflater);
|
val view = MenuBottomBarView(this, inflater);
|
||||||
_view = view;
|
_view = view;
|
||||||
return view;
|
return view;
|
||||||
@@ -56,7 +57,13 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
return _view?.onBackPressed() ?: false;
|
return _view?.onBackPressed() ?: false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("ViewConstructor")
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
|
super.onConfigurationChanged(newConfig)
|
||||||
|
|
||||||
|
_view?.updateAllButtonVisibility()
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ViewConstructor")
|
||||||
class MenuBottomBarView : LinearLayout {
|
class MenuBottomBarView : LinearLayout {
|
||||||
private val _fragment: MenuBottomBarFragment;
|
private val _fragment: MenuBottomBarFragment;
|
||||||
private val _inflater: LayoutInflater;
|
private val _inflater: LayoutInflater;
|
||||||
@@ -76,7 +83,7 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
private var _buttonsVisible = 0;
|
private var _buttonsVisible = 0;
|
||||||
private var _subscriptionsVisible = true;
|
private var _subscriptionsVisible = true;
|
||||||
|
|
||||||
var currentButtonDefinitions: List<ButtonDefinition>? = null;
|
private var currentButtonDefinitions: List<ButtonDefinition>? = null;
|
||||||
|
|
||||||
constructor(fragment: MenuBottomBarFragment, inflater: LayoutInflater) : super(inflater.context) {
|
constructor(fragment: MenuBottomBarFragment, inflater: LayoutInflater) : super(inflater.context) {
|
||||||
_fragment = fragment;
|
_fragment = fragment;
|
||||||
@@ -132,7 +139,7 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
val staggerFactor = 3.0f
|
val staggerFactor = 3.0f
|
||||||
|
|
||||||
if (visible) {
|
if (visible) {
|
||||||
moreOverlay.visibility = LinearLayout.VISIBLE
|
moreOverlay.visibility = VISIBLE
|
||||||
val animations = arrayListOf<Animator>()
|
val animations = arrayListOf<Animator>()
|
||||||
animations.add(ObjectAnimator.ofFloat(moreOverlayBackground, "alpha", 0.0f, 1.0f).setDuration(duration))
|
animations.add(ObjectAnimator.ofFloat(moreOverlayBackground, "alpha", 0.0f, 1.0f).setDuration(duration))
|
||||||
|
|
||||||
@@ -161,7 +168,7 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
animatorSet.doOnEnd {
|
animatorSet.doOnEnd {
|
||||||
_moreVisibleAnimating = false
|
_moreVisibleAnimating = false
|
||||||
_moreVisible = false
|
_moreVisible = false
|
||||||
moreOverlay.visibility = LinearLayout.INVISIBLE
|
moreOverlay.visibility = INVISIBLE
|
||||||
}
|
}
|
||||||
animatorSet.playTogether(animations)
|
animatorSet.playTogether(animations)
|
||||||
animatorSet.start()
|
animatorSet.start()
|
||||||
@@ -178,7 +185,7 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
_layoutBottomBarButtons.removeAllViews();
|
_layoutBottomBarButtons.removeAllViews();
|
||||||
|
|
||||||
_layoutBottomBarButtons.addView(Space(context).apply {
|
_layoutBottomBarButtons.addView(Space(context).apply {
|
||||||
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
|
layoutParams = LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f)
|
||||||
})
|
})
|
||||||
|
|
||||||
for ((index, button) in buttons.withIndex()) {
|
for ((index, button) in buttons.withIndex()) {
|
||||||
@@ -192,7 +199,7 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
_layoutBottomBarButtons.addView(menuButton)
|
_layoutBottomBarButtons.addView(menuButton)
|
||||||
if (index < buttonDefinitions.size - 1) {
|
if (index < buttonDefinitions.size - 1) {
|
||||||
_layoutBottomBarButtons.addView(Space(context).apply {
|
_layoutBottomBarButtons.addView(Space(context).apply {
|
||||||
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
|
layoutParams = LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,7 +207,7 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_layoutBottomBarButtons.addView(Space(context).apply {
|
_layoutBottomBarButtons.addView(Space(context).apply {
|
||||||
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
|
layoutParams = LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,26 +216,30 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
_moreButtons.clear();
|
_moreButtons.clear();
|
||||||
_layoutMoreButtons.removeAllViews();
|
_layoutMoreButtons.removeAllViews();
|
||||||
|
|
||||||
|
var insertedButtons = 0;
|
||||||
//Force buy to be on top for more buttons
|
//Force buy to be on top for more buttons
|
||||||
val buyIndex = buttons.indexOfFirst { b -> b.id == 98 };
|
val buyIndex = buttons.indexOfFirst { b -> b.id == 98 };
|
||||||
if (buyIndex != -1) {
|
if (buyIndex != -1) {
|
||||||
val button = buttons[buyIndex]
|
val button = buttons[buyIndex]
|
||||||
buttons.removeAt(buyIndex)
|
buttons.removeAt(buyIndex)
|
||||||
buttons.add(0, button)
|
buttons.add(0, button)
|
||||||
|
insertedButtons++;
|
||||||
}
|
}
|
||||||
//Force faq to be second
|
//Force faq to be second
|
||||||
val faqIndex = buttons.indexOfFirst { b -> b.id == 97 };
|
val faqIndex = buttons.indexOfFirst { b -> b.id == 97 };
|
||||||
if (faqIndex != -1) {
|
if (faqIndex != -1) {
|
||||||
val button = buttons[faqIndex]
|
val button = buttons[faqIndex]
|
||||||
buttons.removeAt(faqIndex)
|
buttons.removeAt(faqIndex)
|
||||||
buttons.add(if (buttons.size == 1) 1 else 0, button)
|
buttons.add(if (insertedButtons == 1) 1 else 0, button)
|
||||||
|
insertedButtons++;
|
||||||
}
|
}
|
||||||
//Force privacy to be third
|
//Force privacy to be third
|
||||||
val privacyIndex = buttons.indexOfFirst { b -> b.id == 96 };
|
val privacyIndex = buttons.indexOfFirst { b -> b.id == 96 };
|
||||||
if (privacyIndex != -1) {
|
if (privacyIndex != -1) {
|
||||||
val button = buttons[privacyIndex]
|
val button = buttons[privacyIndex]
|
||||||
buttons.removeAt(privacyIndex)
|
buttons.removeAt(privacyIndex)
|
||||||
buttons.add(if (buttons.size == 2) 2 else 1, button)
|
buttons.add(if (insertedButtons == 2) 2 else (if(insertedButtons == 1) 1 else 0), button)
|
||||||
|
insertedButtons++;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (data in buttons) {
|
for (data in buttons) {
|
||||||
@@ -251,9 +262,20 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
button.updateActive(_fragment);
|
button.updateActive(_fragment);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onConfigurationChanged(newConfig: Configuration?) {
|
||||||
|
super.onConfigurationChanged(newConfig)
|
||||||
|
|
||||||
|
updateAllButtonVisibility()
|
||||||
|
}
|
||||||
|
|
||||||
fun updateAllButtonVisibility() {
|
fun updateAllButtonVisibility() {
|
||||||
|
// if the more fly-out menu is open the we should close it
|
||||||
|
if(_moreVisible) {
|
||||||
|
setMoreVisible(false)
|
||||||
|
}
|
||||||
|
|
||||||
val defs = currentButtonDefinitions?.toMutableList() ?: return
|
val defs = currentButtonDefinitions?.toMutableList() ?: return
|
||||||
val metrics = StateApp.instance.displayMetrics ?: resources.displayMetrics;
|
val metrics = resources.displayMetrics
|
||||||
_buttonsVisible = floor(metrics.widthPixels.toDouble() / 65.dp(resources).toDouble()).roundToInt();
|
_buttonsVisible = floor(metrics.widthPixels.toDouble() / 65.dp(resources).toDouble()).roundToInt();
|
||||||
if (_buttonsVisible >= defs.size) {
|
if (_buttonsVisible >= defs.size) {
|
||||||
updateBottomMenuButtons(defs.toMutableList(), false);
|
updateBottomMenuButtons(defs.toMutableList(), false);
|
||||||
@@ -310,19 +332,6 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
if (!StatePayment.instance.hasPaid) {
|
if (!StatePayment.instance.hasPaid) {
|
||||||
newCurrentButtonDefinitions.add(ButtonDefinition(98, R.drawable.ic_paid, R.drawable.ic_paid_filled, R.string.buy, canToggle = false, { it.currentMain is BuyFragment }, { it.navigate<BuyFragment>() }))
|
newCurrentButtonDefinitions.add(ButtonDefinition(98, R.drawable.ic_paid, R.drawable.ic_paid_filled, R.string.buy, canToggle = false, { it.currentMain is BuyFragment }, { it.navigate<BuyFragment>() }))
|
||||||
}
|
}
|
||||||
newCurrentButtonDefinitions.add(ButtonDefinition(97, R.drawable.ic_quiz, R.drawable.ic_quiz_fill, R.string.faq, canToggle = false, { false }, {
|
|
||||||
it.navigate<BrowserFragment>(Settings.URL_FAQ);
|
|
||||||
}))
|
|
||||||
newCurrentButtonDefinitions.add(ButtonDefinition(96, R.drawable.ic_disabled_visible, R.drawable.ic_disabled_visible, R.string.privacy_mode, canToggle = false, { false }, {
|
|
||||||
UIDialogs.showDialog(context, R.drawable.ic_disabled_visible_purple, "Privacy Mode",
|
|
||||||
"All requests will be processed anonymously (unauthenticated), playback and history tracking will be disabled.\n\nTap the icon to disable.", null, 0,
|
|
||||||
UIDialogs.Action("Cancel", {
|
|
||||||
StateApp.instance.setPrivacyMode(false);
|
|
||||||
}, UIDialogs.ActionStyle.NONE),
|
|
||||||
UIDialogs.Action("Enable", {
|
|
||||||
StateApp.instance.setPrivacyMode(true);
|
|
||||||
}, UIDialogs.ActionStyle.PRIMARY));
|
|
||||||
}))
|
|
||||||
|
|
||||||
//Add conditional buttons here, when you add a conditional button, be sure to add the register and unregister events for when the button needs to be updated
|
//Add conditional buttons here, when you add a conditional button, be sure to add the register and unregister events for when the button needs to be updated
|
||||||
|
|
||||||
@@ -368,7 +377,15 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
|
|
||||||
//Add configurable buttons here
|
//Add configurable buttons here
|
||||||
var buttonDefinitions = listOf(
|
var buttonDefinitions = listOf(
|
||||||
ButtonDefinition(0, R.drawable.ic_home, R.drawable.ic_home_filled, R.string.home, canToggle = true, { it.currentMain is HomeFragment }, { it.navigate<HomeFragment>() }),
|
ButtonDefinition(0, R.drawable.ic_home, R.drawable.ic_home_filled, R.string.home, canToggle = true, { it.currentMain is HomeFragment }, {
|
||||||
|
val currentMain = it.currentMain
|
||||||
|
if (currentMain is HomeFragment) {
|
||||||
|
currentMain.scrollToTop(false)
|
||||||
|
currentMain.reloadFeed()
|
||||||
|
} else {
|
||||||
|
it.navigate<HomeFragment>()
|
||||||
|
}
|
||||||
|
}),
|
||||||
ButtonDefinition(1, R.drawable.ic_subscriptions, R.drawable.ic_subscriptions_filled, R.string.subscriptions, canToggle = true, { it.currentMain is SubscriptionsFeedFragment }, { it.navigate<SubscriptionsFeedFragment>() }),
|
ButtonDefinition(1, R.drawable.ic_subscriptions, R.drawable.ic_subscriptions_filled, R.string.subscriptions, canToggle = true, { it.currentMain is SubscriptionsFeedFragment }, { it.navigate<SubscriptionsFeedFragment>() }),
|
||||||
ButtonDefinition(2, R.drawable.ic_creators, R.drawable.ic_creators_filled, R.string.creators, canToggle = false, { it.currentMain is CreatorsFragment }, { it.navigate<CreatorsFragment>() }),
|
ButtonDefinition(2, R.drawable.ic_creators, R.drawable.ic_creators_filled, R.string.creators, canToggle = false, { it.currentMain is CreatorsFragment }, { it.navigate<CreatorsFragment>() }),
|
||||||
ButtonDefinition(3, R.drawable.ic_sources, R.drawable.ic_sources_filled, R.string.sources, canToggle = false, { it.currentMain is SourcesFragment }, { it.navigate<SourcesFragment>() }),
|
ButtonDefinition(3, R.drawable.ic_sources, R.drawable.ic_sources_filled, R.string.sources, canToggle = false, { it.currentMain is SourcesFragment }, { it.navigate<SourcesFragment>() }),
|
||||||
@@ -387,6 +404,19 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
if (c is Activity) {
|
if (c is Activity) {
|
||||||
c.overridePendingTransition(R.anim.slide_in_up, R.anim.slide_darken);
|
c.overridePendingTransition(R.anim.slide_in_up, R.anim.slide_darken);
|
||||||
}
|
}
|
||||||
|
}),
|
||||||
|
ButtonDefinition(96, R.drawable.ic_disabled_visible, R.drawable.ic_disabled_visible, R.string.privacy_mode, canToggle = true, { false }, {
|
||||||
|
UIDialogs.showDialog(it.context ?: return@ButtonDefinition, R.drawable.ic_disabled_visible_purple, "Privacy Mode",
|
||||||
|
"All requests will be processed anonymously (any logins will be disabled except for the personalized home page), local playback and history tracking will also be disabled.\n\nTap the icon to disable.", null, 0,
|
||||||
|
UIDialogs.Action("Cancel", {
|
||||||
|
StateApp.instance.setPrivacyMode(false);
|
||||||
|
}, UIDialogs.ActionStyle.NONE),
|
||||||
|
UIDialogs.Action("Enable", {
|
||||||
|
StateApp.instance.setPrivacyMode(true);
|
||||||
|
}, UIDialogs.ActionStyle.PRIMARY));
|
||||||
|
}),
|
||||||
|
ButtonDefinition(97, R.drawable.ic_quiz, R.drawable.ic_quiz_fill, R.string.faq, canToggle = true, { false }, {
|
||||||
|
it.navigate<BrowserFragment>(Settings.URL_FAQ);
|
||||||
})
|
})
|
||||||
//96 is reserved for privacy button
|
//96 is reserved for privacy button
|
||||||
//98 is reserved for buy button
|
//98 is reserved for buy button
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import android.widget.TextView
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.futopay.PaymentConfigurations
|
import com.futo.futopay.PaymentConfigurations
|
||||||
import com.futo.futopay.PaymentManager
|
import com.futo.futopay.PaymentManager
|
||||||
|
import com.futo.futopay.formatMoney
|
||||||
import com.futo.platformplayer.BuildConfig
|
import com.futo.platformplayer.BuildConfig
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
@@ -89,14 +90,13 @@ class BuyFragment : MainFragment() {
|
|||||||
try {
|
try {
|
||||||
val currencies = StatePayment.instance.getAvailableCurrencies("grayjay");
|
val currencies = StatePayment.instance.getAvailableCurrencies("grayjay");
|
||||||
val prices = StatePayment.instance.getAvailableCurrencyPrices("grayjay");
|
val prices = StatePayment.instance.getAvailableCurrencyPrices("grayjay");
|
||||||
val country = StatePayment.instance.getPaymentCountryFromIP()?.let { c -> PaymentConfigurations.COUNTRIES.find { it.id.equals(c, ignoreCase = true) } };
|
val country = StatePayment.instance.getPaymentCountryFromIP(true)?.let { c -> PaymentConfigurations.COUNTRIES.find { it.id.equals(c, ignoreCase = true) } };
|
||||||
val currency = country?.let { c -> PaymentConfigurations.CURRENCIES.find { it.id == c.defaultCurrencyId && (currencies.contains(it.id)) } };
|
val currency = country?.let { c -> PaymentConfigurations.CURRENCIES.find { it.id == c.defaultCurrencyId && (currencies.contains(it.id)) } };
|
||||||
|
|
||||||
if(currency != null && prices.containsKey(currency.id)) {
|
if(currency != null && prices.containsKey(currency.id)) {
|
||||||
val price = prices[currency.id]!!;
|
val price = prices[currency.id]!!;
|
||||||
val priceDecimal = (price.toDouble() / 100);
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
_buttonBuyText.text = currency.symbol + String.format("%.2f", priceDecimal) + context.getString(R.string.plus_tax);
|
_buttonBuyText.text = formatMoney(country.id, currency.id, price) + context.getString(R.string.plus_tax);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-6
@@ -1,7 +1,10 @@
|
|||||||
package com.futo.platformplayer.fragment.mainactivity.main
|
package com.futo.platformplayer.fragment.mainactivity.main
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.graphics.drawable.Animatable
|
import android.graphics.drawable.Animatable
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@@ -12,6 +15,7 @@ import android.widget.ImageView
|
|||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.appcompat.widget.AppCompatImageView
|
import androidx.appcompat.widget.AppCompatImageView
|
||||||
|
import androidx.core.content.ContextCompat.startActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.viewpager2.widget.ViewPager2
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
@@ -52,7 +56,9 @@ import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
|||||||
import com.futo.platformplayer.views.subscriptions.SubscribeButton
|
import com.futo.platformplayer.views.subscriptions.SubscribeButton
|
||||||
import com.futo.polycentric.core.OwnedClaim
|
import com.futo.polycentric.core.OwnedClaim
|
||||||
import com.futo.polycentric.core.PublicKey
|
import com.futo.polycentric.core.PublicKey
|
||||||
|
import com.futo.polycentric.core.Store
|
||||||
import com.futo.polycentric.core.SystemState
|
import com.futo.polycentric.core.SystemState
|
||||||
|
import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl
|
||||||
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||||
import com.google.android.material.tabs.TabLayout
|
import com.google.android.material.tabs.TabLayout
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
@@ -64,7 +70,13 @@ import kotlinx.serialization.Serializable
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class PolycentricProfile(
|
data class PolycentricProfile(
|
||||||
val system: PublicKey, val systemState: SystemState, val ownedClaims: List<OwnedClaim>
|
val system: PublicKey, val systemState: SystemState, val ownedClaims: List<OwnedClaim>
|
||||||
)
|
) {
|
||||||
|
fun getHarborUrl(context: Context): String{
|
||||||
|
val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(system));
|
||||||
|
val url = system.systemToURLInfoSystemLinkUrl(systemState.servers.asIterable());
|
||||||
|
return "https://harbor.social/" + url.substring("polycentric://".length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ChannelFragment : MainFragment() {
|
class ChannelFragment : MainFragment() {
|
||||||
override val isMainView: Boolean = true
|
override val isMainView: Boolean = true
|
||||||
@@ -225,11 +237,7 @@ class ChannelFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
adapter.onAddToWatchLaterClicked.subscribe { content ->
|
adapter.onAddToWatchLaterClicked.subscribe { content ->
|
||||||
if (content is IPlatformVideo) {
|
if (content is IPlatformVideo) {
|
||||||
StatePlaylists.instance.addToWatchLater(
|
StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(content), true)
|
||||||
SerializedPlatformVideo.fromVideo(
|
|
||||||
content
|
|
||||||
)
|
|
||||||
)
|
|
||||||
UIDialogs.toast("Added to watch later\n[${content.name}]")
|
UIDialogs.toast("Added to watch later\n[${content.name}]")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -221,8 +221,8 @@ class CommentsFragment : MainFragment() {
|
|||||||
|
|
||||||
Logger.i(TAG, "onAuthorClick: " + c.author.id.value);
|
Logger.i(TAG, "onAuthorClick: " + c.author.id.value);
|
||||||
if(c.author.id.value?.startsWith("polycentric://") ?: false) {
|
if(c.author.id.value?.startsWith("polycentric://") ?: false) {
|
||||||
//val navUrl = "https://harbor.social/" + c.author.id.value?.substring("polycentric://".length);
|
val navUrl = "https://harbor.social/" + c.author.id.value?.substring("polycentric://".length);
|
||||||
val navUrl = "https://polycentric.io/user/" + c.author.id.value?.substring("polycentric://".length);
|
//val navUrl = "https://polycentric.io/user/" + c.author.id.value?.substring("polycentric://".length);
|
||||||
_fragment.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(navUrl)))
|
_fragment.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(navUrl)))
|
||||||
//_fragment.navigate<BrowserFragment>(navUrl);
|
//_fragment.navigate<BrowserFragment>(navUrl);
|
||||||
}
|
}
|
||||||
|
|||||||
+40
-34
@@ -4,7 +4,7 @@ import android.content.Context
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
@@ -33,6 +33,7 @@ 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.withTimestamp
|
import com.futo.platformplayer.withTimestamp
|
||||||
import kotlin.math.floor
|
import kotlin.math.floor
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent, IPlatformContent, IPager<IPlatformContent>, ContentPreviewViewHolder> where TFragment : MainFragment {
|
abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent, IPlatformContent, IPager<IPlatformContent>, ContentPreviewViewHolder> where TFragment : MainFragment {
|
||||||
private var _exoPlayer: PlayerManager? = null;
|
private var _exoPlayer: PlayerManager? = null;
|
||||||
@@ -45,9 +46,7 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
|||||||
private var _videoOptionsOverlay: SlideUpMenuOverlay? = null;
|
private var _videoOptionsOverlay: SlideUpMenuOverlay? = null;
|
||||||
protected open val shouldShowTimeBar: Boolean get() = true
|
protected open val shouldShowTimeBar: Boolean get() = true
|
||||||
|
|
||||||
constructor(fragment: TFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData) {
|
constructor(fragment: TFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, GridLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData)
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun filterResults(results: List<IPlatformContent>): List<IPlatformContent> {
|
override fun filterResults(results: List<IPlatformContent>): List<IPlatformContent> {
|
||||||
return results;
|
return results;
|
||||||
@@ -55,16 +54,10 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
|||||||
|
|
||||||
override fun createAdapter(recyclerResults: RecyclerView, context: Context, dataset: ArrayList<IPlatformContent>): InsertedViewAdapterWithLoader<ContentPreviewViewHolder> {
|
override fun createAdapter(recyclerResults: RecyclerView, context: Context, dataset: ArrayList<IPlatformContent>): InsertedViewAdapterWithLoader<ContentPreviewViewHolder> {
|
||||||
val player = StatePlayer.instance.getThumbnailPlayerOrCreate(context);
|
val player = StatePlayer.instance.getThumbnailPlayerOrCreate(context);
|
||||||
player.modifyState("ThumbnailPlayer", { state -> state.muted = true });
|
player.modifyState("ThumbnailPlayer") { state -> state.muted = true };
|
||||||
_exoPlayer = player;
|
_exoPlayer = player;
|
||||||
|
|
||||||
val v = LinearLayout(context).apply {
|
return PreviewContentListAdapter(context, feedStyle, dataset, player, _previewsEnabled, arrayListOf(), arrayListOf(), shouldShowTimeBar).apply {
|
||||||
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
|
||||||
orientation = LinearLayout.VERTICAL;
|
|
||||||
};
|
|
||||||
headerView = v;
|
|
||||||
|
|
||||||
return PreviewContentListAdapter(context, feedStyle, dataset, player, _previewsEnabled, arrayListOf(v), arrayListOf(), shouldShowTimeBar).apply {
|
|
||||||
attachAdapterEvents(this);
|
attachAdapterEvents(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -89,7 +82,7 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
|||||||
};
|
};
|
||||||
adapter.onAddToWatchLaterClicked.subscribe(this) {
|
adapter.onAddToWatchLaterClicked.subscribe(this) {
|
||||||
if(it is IPlatformVideo) {
|
if(it is IPlatformVideo) {
|
||||||
StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it));
|
StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it), true);
|
||||||
UIDialogs.toast("Added to watch later\n[${it.name}]");
|
UIDialogs.toast("Added to watch later\n[${it.name}]");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -118,8 +111,13 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
|||||||
|
|
||||||
private fun showVideoOptionsOverlay(content: IPlatformVideo) {
|
private fun showVideoOptionsOverlay(content: IPlatformVideo) {
|
||||||
_overlayContainer.let {
|
_overlayContainer.let {
|
||||||
_videoOptionsOverlay = UISlideOverlays.showVideoOptionsOverlay(content, it, SlideUpMenuItem(context, R.drawable.ic_visibility_off, context.getString(R.string.hide), context.getString(R.string.hide_from_home), "hide",
|
_videoOptionsOverlay = UISlideOverlays.showVideoOptionsOverlay(content, it, SlideUpMenuItem(
|
||||||
{ StateMeta.instance.addHiddenVideo(content.url);
|
context,
|
||||||
|
R.drawable.ic_visibility_off,
|
||||||
|
context.getString(R.string.hide),
|
||||||
|
context.getString(R.string.hide_from_home),
|
||||||
|
tag = "hide",
|
||||||
|
call = { StateMeta.instance.addHiddenVideo(content.url);
|
||||||
if (fragment is HomeFragment) {
|
if (fragment is HomeFragment) {
|
||||||
val removeIndex = recyclerData.results.indexOf(content);
|
val removeIndex = recyclerData.results.indexOf(content);
|
||||||
if (removeIndex >= 0) {
|
if (removeIndex >= 0) {
|
||||||
@@ -128,12 +126,19 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
SlideUpMenuItem(context, R.drawable.ic_playlist, context.getString(R.string.play_feed_as_queue), context.getString(R.string.play_entire_feed), "playFeed",
|
SlideUpMenuItem(context,
|
||||||
{
|
R.drawable.ic_playlist,
|
||||||
|
context.getString(R.string.play_feed_as_queue),
|
||||||
|
context.getString(R.string.play_entire_feed),
|
||||||
|
tag = "playFeed",
|
||||||
|
call = {
|
||||||
val newQueue = listOf(content) + recyclerData.results
|
val newQueue = listOf(content) + recyclerData.results
|
||||||
.filterIsInstance<IPlatformVideo>()
|
.filterIsInstance<IPlatformVideo>()
|
||||||
.filter { it != content };
|
.filter { it != content };
|
||||||
StatePlayer.instance.setQueue(newQueue, StatePlayer.TYPE_QUEUE, "Feed Queue", true, false);
|
StatePlayer.instance.setQueue(newQueue, StatePlayer.TYPE_QUEUE, "Feed Queue",
|
||||||
|
focus = true,
|
||||||
|
shuffle = false
|
||||||
|
);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -151,21 +156,22 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
|||||||
adapter.onLongPress.remove(this);
|
adapter.onLongPress.remove(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRestoreCachedData(cachedData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>) {
|
override fun onRestoreCachedData(cachedData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, GridLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>) {
|
||||||
super.onRestoreCachedData(cachedData)
|
super.onRestoreCachedData(cachedData)
|
||||||
val v = LinearLayout(context).apply {
|
|
||||||
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
|
||||||
orientation = LinearLayout.VERTICAL;
|
|
||||||
};
|
|
||||||
headerView = v;
|
|
||||||
cachedData.adapter.viewsToPrepend.add(v);
|
|
||||||
(cachedData.adapter as PreviewContentListAdapter?)?.let { attachAdapterEvents(it) };
|
(cachedData.adapter as PreviewContentListAdapter?)?.let { attachAdapterEvents(it) };
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createLayoutManager(recyclerResults: RecyclerView, context: Context): LinearLayoutManager {
|
override fun createLayoutManager(
|
||||||
val llmResults = LinearLayoutManager(context);
|
recyclerResults: RecyclerView,
|
||||||
llmResults.orientation = LinearLayoutManager.VERTICAL;
|
context: Context
|
||||||
return llmResults;
|
): GridLayoutManager {
|
||||||
|
val glmResults =
|
||||||
|
GridLayoutManager(
|
||||||
|
context,
|
||||||
|
max((resources.configuration.screenWidthDp.toDouble() / resources.getInteger(R.integer.column_width_dp)).toInt(), 1)
|
||||||
|
);
|
||||||
|
return glmResults
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onScrollStateChanged(newState: Int) {
|
override fun onScrollStateChanged(newState: Int) {
|
||||||
@@ -208,11 +214,11 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun playPreview() {
|
private fun playPreview() {
|
||||||
if(feedStyle == FeedStyle.THUMBNAIL)
|
if(feedStyle == FeedStyle.THUMBNAIL || recyclerData.layoutManager.spanCount > 1)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
val firstVisible = recyclerData.layoutManager.findFirstVisibleItemPosition();
|
val firstVisible = recyclerData.layoutManager.findFirstVisibleItemPosition()
|
||||||
val lastVisible = recyclerData.layoutManager.findLastVisibleItemPosition();
|
val lastVisible = recyclerData.layoutManager.findLastVisibleItemPosition()
|
||||||
val itemsVisible = lastVisible - firstVisible + 1;
|
val itemsVisible = lastVisible - firstVisible + 1;
|
||||||
val autoPlayIndex = (firstVisible + floor(itemsVisible / 2.0 + 0.49).toInt()).coerceAtLeast(0).coerceAtMost((recyclerData.results.size - 1));
|
val autoPlayIndex = (firstVisible + floor(itemsVisible / 2.0 + 0.49).toInt()).coerceAtLeast(0).coerceAtMost((recyclerData.results.size - 1));
|
||||||
|
|
||||||
@@ -232,7 +238,7 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
|||||||
(recyclerData.adapter as PreviewContentListAdapter?)?.preview(viewHolder.childViewHolder)
|
(recyclerData.adapter as PreviewContentListAdapter?)?.preview(viewHolder.childViewHolder)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopVideo() {
|
private fun stopVideo() {
|
||||||
//TODO: Is this still necessary?
|
//TODO: Is this still necessary?
|
||||||
(recyclerData.adapter as PreviewContentListAdapter?)?.stopPreview();
|
(recyclerData.adapter as PreviewContentListAdapter?)?.stopPreview();
|
||||||
}
|
}
|
||||||
@@ -260,6 +266,6 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = "ContentFeedView";
|
private const val TAG = "ContentFeedView";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+21
-14
@@ -3,13 +3,9 @@ package com.futo.platformplayer.fragment.mainactivity.main
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup.MarginLayoutParams
|
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.futo.platformplayer.*
|
|
||||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
|
||||||
import com.futo.platformplayer.api.media.structures.*
|
import com.futo.platformplayer.api.media.structures.*
|
||||||
import com.futo.platformplayer.views.FeedStyle
|
import com.futo.platformplayer.views.FeedStyle
|
||||||
import com.futo.platformplayer.views.adapters.*
|
import com.futo.platformplayer.views.adapters.*
|
||||||
@@ -18,9 +14,7 @@ import com.futo.platformplayer.views.adapters.viewholders.CreatorViewHolder
|
|||||||
abstract class CreatorFeedView<TFragment> : FeedView<TFragment, PlatformAuthorLink, PlatformAuthorLink, IPager<PlatformAuthorLink>, CreatorViewHolder> where TFragment : MainFragment {
|
abstract class CreatorFeedView<TFragment> : FeedView<TFragment, PlatformAuthorLink, PlatformAuthorLink, IPager<PlatformAuthorLink>, CreatorViewHolder> where TFragment : MainFragment {
|
||||||
override val feedStyle: FeedStyle = FeedStyle.THUMBNAIL; //R.layout.list_creator;
|
override val feedStyle: FeedStyle = FeedStyle.THUMBNAIL; //R.layout.list_creator;
|
||||||
|
|
||||||
constructor(fragment: TFragment, inflater: LayoutInflater) : super(fragment, inflater) {
|
constructor(fragment: TFragment, inflater: LayoutInflater) : super(fragment, inflater)
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createAdapter(recyclerResults: RecyclerView, context: Context, dataset: ArrayList<PlatformAuthorLink>): InsertedViewAdapterWithLoader<CreatorViewHolder> {
|
override fun createAdapter(recyclerResults: RecyclerView, context: Context, dataset: ArrayList<PlatformAuthorLink>): InsertedViewAdapterWithLoader<CreatorViewHolder> {
|
||||||
return InsertedViewAdapterWithLoader(context, arrayListOf(), arrayListOf(),
|
return InsertedViewAdapterWithLoader(context, arrayListOf(), arrayListOf(),
|
||||||
@@ -34,18 +28,31 @@ abstract class CreatorFeedView<TFragment> : FeedView<TFragment, PlatformAuthorLi
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createLayoutManager(recyclerResults: RecyclerView, context: Context): LinearLayoutManager {
|
/*
|
||||||
val glmResults = GridLayoutManager(context, 2);
|
* An empty override to remove the inherited span count update functionality
|
||||||
glmResults.orientation = LinearLayoutManager.VERTICAL;
|
*/
|
||||||
|
override fun updateSpanCount(){
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createLayoutManager(
|
||||||
|
recyclerResults: RecyclerView,
|
||||||
|
context: Context
|
||||||
|
): GridLayoutManager {
|
||||||
|
val glmResults = GridLayoutManager(context, 2)
|
||||||
|
|
||||||
_swipeRefresh.layoutParams = (_swipeRefresh.layoutParams as MarginLayoutParams?)?.apply {
|
_swipeRefresh.layoutParams = (_swipeRefresh.layoutParams as MarginLayoutParams?)?.apply {
|
||||||
rightMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8.0f, context.resources.displayMetrics).toInt();
|
rightMargin = TypedValue.applyDimension(
|
||||||
};
|
TypedValue.COMPLEX_UNIT_DIP,
|
||||||
|
8.0f,
|
||||||
|
context.resources.displayMetrics
|
||||||
|
).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
return glmResults;
|
return glmResults
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = "CreatorFeedView";
|
private const val TAG = "CreatorFeedView";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+17
-2
@@ -8,6 +8,7 @@ import android.widget.AdapterView
|
|||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.ImageButton
|
||||||
import android.widget.Spinner
|
import android.widget.Spinner
|
||||||
import androidx.core.widget.addTextChangedListener
|
import androidx.core.widget.addTextChangedListener
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
@@ -25,11 +26,20 @@ class CreatorsFragment : MainFragment() {
|
|||||||
private var _overlayContainer: FrameLayout? = null;
|
private var _overlayContainer: FrameLayout? = null;
|
||||||
private var _containerSearch: FrameLayout? = null;
|
private var _containerSearch: FrameLayout? = null;
|
||||||
private var _editSearch: EditText? = null;
|
private var _editSearch: EditText? = null;
|
||||||
|
private var _buttonClearSearch: ImageButton? = null
|
||||||
|
|
||||||
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
val view = inflater.inflate(R.layout.fragment_creators, container, false);
|
val view = inflater.inflate(R.layout.fragment_creators, container, false);
|
||||||
_containerSearch = view.findViewById(R.id.container_search);
|
_containerSearch = view.findViewById(R.id.container_search);
|
||||||
_editSearch = view.findViewById(R.id.edit_search);
|
val editSearch: EditText = view.findViewById(R.id.edit_search);
|
||||||
|
val buttonClearSearch: ImageButton = view.findViewById(R.id.button_clear_search)
|
||||||
|
_editSearch = editSearch
|
||||||
|
_buttonClearSearch = buttonClearSearch
|
||||||
|
buttonClearSearch.setOnClickListener {
|
||||||
|
editSearch.text.clear()
|
||||||
|
editSearch.requestFocus()
|
||||||
|
_buttonClearSearch?.visibility = View.INVISIBLE;
|
||||||
|
}
|
||||||
|
|
||||||
val adapter = SubscriptionAdapter(inflater, getString(R.string.confirm_delete_subscription));
|
val adapter = SubscriptionAdapter(inflater, getString(R.string.confirm_delete_subscription));
|
||||||
adapter.onClick.subscribe { platformUser -> navigate<ChannelFragment>(platformUser) };
|
adapter.onClick.subscribe { platformUser -> navigate<ChannelFragment>(platformUser) };
|
||||||
@@ -51,7 +61,12 @@ class CreatorsFragment : MainFragment() {
|
|||||||
_spinnerSortBy = spinnerSortBy;
|
_spinnerSortBy = spinnerSortBy;
|
||||||
|
|
||||||
_editSearch?.addTextChangedListener {
|
_editSearch?.addTextChangedListener {
|
||||||
adapter.query = it.toString();
|
adapter.query = it.toString()
|
||||||
|
if (it?.isEmpty() == true) {
|
||||||
|
_buttonClearSearch?.visibility = View.INVISIBLE
|
||||||
|
} else {
|
||||||
|
_buttonClearSearch?.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val recyclerView = view.findViewById<RecyclerView>(R.id.recycler_subscriptions);
|
val recyclerView = view.findViewById<RecyclerView>(R.id.recycler_subscriptions);
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
package com.futo.platformplayer.fragment.mainactivity.main
|
package com.futo.platformplayer.fragment.mainactivity.main
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.res.Configuration
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.recyclerview.widget.RecyclerView.LayoutManager
|
import androidx.recyclerview.widget.RecyclerView.LayoutManager
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
@@ -24,18 +25,21 @@ import com.futo.platformplayer.views.others.ProgressBar
|
|||||||
import com.futo.platformplayer.views.others.TagsView
|
import com.futo.platformplayer.views.others.TagsView
|
||||||
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||||
import com.futo.platformplayer.views.adapters.InsertedViewHolder
|
import com.futo.platformplayer.views.adapters.InsertedViewHolder
|
||||||
|
import com.futo.platformplayer.views.announcements.AnnouncementView
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : LinearLayout where TPager : IPager<TResult>, TViewHolder : RecyclerView.ViewHolder, TFragment : MainFragment {
|
abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : LinearLayout where TPager : IPager<TResult>, TViewHolder : RecyclerView.ViewHolder, TFragment : MainFragment {
|
||||||
protected val _recyclerResults: RecyclerView;
|
protected val _recyclerResults: RecyclerView;
|
||||||
protected val _overlayContainer: FrameLayout;
|
protected val _overlayContainer: FrameLayout;
|
||||||
protected val _swipeRefresh: SwipeRefreshLayout;
|
protected val _swipeRefresh: SwipeRefreshLayout;
|
||||||
private val _progress_bar: ProgressBar;
|
private val _progressBar: ProgressBar;
|
||||||
private val _spinnerSortBy: Spinner;
|
private val _spinnerSortBy: Spinner;
|
||||||
private val _containerSortBy: LinearLayout;
|
private val _containerSortBy: LinearLayout;
|
||||||
|
private val _announcementView: AnnouncementView;
|
||||||
private val _tagsView: TagsView;
|
private val _tagsView: TagsView;
|
||||||
private val _textCentered: TextView;
|
private val _textCentered: TextView;
|
||||||
private val _emptyPagerContainer: FrameLayout;
|
private val _emptyPagerContainer: FrameLayout;
|
||||||
@@ -44,7 +48,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||||||
|
|
||||||
private var _loading: Boolean = true;
|
private var _loading: Boolean = true;
|
||||||
|
|
||||||
private val _pager_lock = Object();
|
private val _pagerLock = Object();
|
||||||
private var _cache: ItemCache<TResult>? = null;
|
private var _cache: ItemCache<TResult>? = null;
|
||||||
|
|
||||||
open val visibleThreshold = 15;
|
open val visibleThreshold = 15;
|
||||||
@@ -58,21 +62,22 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||||||
private var _activeTags: List<String>? = null;
|
private var _activeTags: List<String>? = null;
|
||||||
|
|
||||||
private var _nextPageHandler: TaskHandler<TPager, List<TResult>>;
|
private var _nextPageHandler: TaskHandler<TPager, List<TResult>>;
|
||||||
val recyclerData: RecyclerData<InsertedViewAdapterWithLoader<TViewHolder>, LinearLayoutManager, TPager, TResult, TConverted, InsertedViewHolder<TViewHolder>>;
|
val recyclerData: RecyclerData<InsertedViewAdapterWithLoader<TViewHolder>, GridLayoutManager, TPager, TResult, TConverted, InsertedViewHolder<TViewHolder>>;
|
||||||
|
|
||||||
val fragment: TFragment;
|
val fragment: TFragment;
|
||||||
|
|
||||||
private val _scrollListener: RecyclerView.OnScrollListener;
|
private val _scrollListener: RecyclerView.OnScrollListener;
|
||||||
private var _automaticNextPageCounter = 0;
|
private var _automaticNextPageCounter = 0;
|
||||||
|
|
||||||
constructor(fragment: TFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<TViewHolder>, LinearLayoutManager, TPager, TResult, TConverted, InsertedViewHolder<TViewHolder>>? = null) : super(inflater.context) {
|
constructor(fragment: TFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<TViewHolder>, GridLayoutManager, TPager, TResult, TConverted, InsertedViewHolder<TViewHolder>>? = null) : super(inflater.context) {
|
||||||
this.fragment = fragment;
|
this.fragment = fragment;
|
||||||
inflater.inflate(R.layout.fragment_feed, this);
|
inflater.inflate(R.layout.fragment_feed, this);
|
||||||
|
|
||||||
_textCentered = findViewById(R.id.text_centered);
|
_textCentered = findViewById(R.id.text_centered);
|
||||||
_emptyPagerContainer = findViewById(R.id.empty_pager_container);
|
_emptyPagerContainer = findViewById(R.id.empty_pager_container);
|
||||||
_progress_bar = findViewById(R.id.progress_bar);
|
_progressBar = findViewById(R.id.progress_bar);
|
||||||
_progress_bar.inactiveColor = Color.TRANSPARENT;
|
_announcementView = findViewById(R.id.announcement_view)
|
||||||
|
_progressBar.inactiveColor = Color.TRANSPARENT;
|
||||||
|
|
||||||
_swipeRefresh = findViewById(R.id.swipe_refresh);
|
_swipeRefresh = findViewById(R.id.swipe_refresh);
|
||||||
val recyclerResults: RecyclerView = findViewById(R.id.list_results);
|
val recyclerResults: RecyclerView = findViewById(R.id.list_results);
|
||||||
@@ -158,7 +163,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||||||
super.onScrolled(recyclerView, dx, dy);
|
super.onScrolled(recyclerView, dx, dy);
|
||||||
|
|
||||||
val visibleItemCount = _recyclerResults.childCount;
|
val visibleItemCount = _recyclerResults.childCount;
|
||||||
val firstVisibleItem = recyclerData.layoutManager.findFirstVisibleItemPosition();
|
val firstVisibleItem = recyclerData.layoutManager.findFirstVisibleItemPosition()
|
||||||
//Logger.i(TAG, "onScrolled loadNextPage visibleItemCount=$visibleItemCount firstVisibleItem=$visibleItemCount")
|
//Logger.i(TAG, "onScrolled loadNextPage visibleItemCount=$visibleItemCount firstVisibleItem=$visibleItemCount")
|
||||||
|
|
||||||
if (!_loading && firstVisibleItem + visibleItemCount + visibleThreshold >= recyclerData.results.size && firstVisibleItem > 0) {
|
if (!_loading && firstVisibleItem + visibleItemCount + visibleThreshold >= recyclerData.results.size && firstVisibleItem > 0) {
|
||||||
@@ -171,6 +176,10 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||||||
_recyclerResults.addOnScrollListener(_scrollListener);
|
_recyclerResults.addOnScrollListener(_scrollListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected fun showAnnouncementView() {
|
||||||
|
_announcementView.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
private fun ensureEnoughContentVisible(filteredResults: List<TConverted>) {
|
private fun ensureEnoughContentVisible(filteredResults: List<TConverted>) {
|
||||||
val canScroll = if (recyclerData.results.isEmpty()) false else {
|
val canScroll = if (recyclerData.results.isEmpty()) false else {
|
||||||
val layoutManager = recyclerData.layoutManager
|
val layoutManager = recyclerData.layoutManager
|
||||||
@@ -179,14 +188,13 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||||||
if (firstVisibleItemPosition != RecyclerView.NO_POSITION) {
|
if (firstVisibleItemPosition != RecyclerView.NO_POSITION) {
|
||||||
val firstVisibleView = layoutManager.findViewByPosition(firstVisibleItemPosition)
|
val firstVisibleView = layoutManager.findViewByPosition(firstVisibleItemPosition)
|
||||||
val itemHeight = firstVisibleView?.height ?: 0
|
val itemHeight = firstVisibleView?.height ?: 0
|
||||||
val occupiedSpace = recyclerData.results.size * itemHeight
|
val occupiedSpace = recyclerData.results.size / recyclerData.layoutManager.spanCount * itemHeight
|
||||||
val recyclerViewHeight = _recyclerResults.height
|
val recyclerViewHeight = _recyclerResults.height
|
||||||
Logger.i(TAG, "ensureEnoughContentVisible loadNextPage occupiedSpace=$occupiedSpace recyclerViewHeight=$recyclerViewHeight")
|
Logger.i(TAG, "ensureEnoughContentVisible loadNextPage occupiedSpace=$occupiedSpace recyclerViewHeight=$recyclerViewHeight")
|
||||||
occupiedSpace >= recyclerViewHeight
|
occupiedSpace >= recyclerViewHeight
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
Logger.i(TAG, "ensureEnoughContentVisible loadNextPage canScroll=$canScroll _automaticNextPageCounter=$_automaticNextPageCounter")
|
Logger.i(TAG, "ensureEnoughContentVisible loadNextPage canScroll=$canScroll _automaticNextPageCounter=$_automaticNextPageCounter")
|
||||||
if (!canScroll || filteredResults.isEmpty()) {
|
if (!canScroll || filteredResults.isEmpty()) {
|
||||||
@@ -226,7 +234,20 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open fun updateSpanCount() {
|
||||||
|
recyclerData.layoutManager.spanCount =
|
||||||
|
max((resources.configuration.screenWidthDp.toDouble() / resources.getInteger(R.integer.column_width_dp)).toInt(), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onConfigurationChanged(newConfig: Configuration?) {
|
||||||
|
super.onConfigurationChanged(newConfig)
|
||||||
|
|
||||||
|
updateSpanCount()
|
||||||
|
}
|
||||||
|
|
||||||
fun onResume() {
|
fun onResume() {
|
||||||
|
updateSpanCount()
|
||||||
|
|
||||||
//Reload the pager if the plugin was killed
|
//Reload the pager if the plugin was killed
|
||||||
val pager = recyclerData.pager;
|
val pager = recyclerData.pager;
|
||||||
if((pager is MultiPager<*> && pager.findPager { it is JSPager<*> && !it.isAvailable } != null) ||
|
if((pager is MultiPager<*> && pager.findPager { it is JSPager<*> && !it.isAvailable } != null) ||
|
||||||
@@ -252,7 +273,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||||||
protected open fun setActiveTags(activeTags: List<String>?) {
|
protected open fun setActiveTags(activeTags: List<String>?) {
|
||||||
_activeTags = activeTags;
|
_activeTags = activeTags;
|
||||||
|
|
||||||
if (activeTags != null && activeTags.isNotEmpty()) {
|
if (!activeTags.isNullOrEmpty()) {
|
||||||
_tagsView.setTags(activeTags);
|
_tagsView.setTags(activeTags);
|
||||||
_tagsView.visibility = View.VISIBLE;
|
_tagsView.visibility = View.VISIBLE;
|
||||||
} else {
|
} else {
|
||||||
@@ -262,7 +283,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||||||
protected open fun setSortByOptions(options: List<String>?) {
|
protected open fun setSortByOptions(options: List<String>?) {
|
||||||
_sortByOptions = options;
|
_sortByOptions = options;
|
||||||
|
|
||||||
if (options != null && options.isNotEmpty()) {
|
if (!options.isNullOrEmpty()) {
|
||||||
val allOptions = arrayListOf<String>();
|
val allOptions = arrayListOf<String>();
|
||||||
allOptions.add("Default");
|
allOptions.add("Default");
|
||||||
allOptions.addAll(options);
|
allOptions.addAll(options);
|
||||||
@@ -277,19 +298,19 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
protected abstract fun createAdapter(recyclerResults: RecyclerView, context: Context, dataset: ArrayList<TConverted>): InsertedViewAdapterWithLoader<TViewHolder>;
|
protected abstract fun createAdapter(recyclerResults: RecyclerView, context: Context, dataset: ArrayList<TConverted>): InsertedViewAdapterWithLoader<TViewHolder>;
|
||||||
protected abstract fun createLayoutManager(recyclerResults: RecyclerView, context: Context): LinearLayoutManager;
|
protected abstract fun createLayoutManager(recyclerResults: RecyclerView, context: Context): GridLayoutManager;
|
||||||
protected open fun onRestoreCachedData(cachedData: RecyclerData<InsertedViewAdapterWithLoader<TViewHolder>, LinearLayoutManager, TPager, TResult, TConverted, InsertedViewHolder<TViewHolder>>) {}
|
protected open fun onRestoreCachedData(cachedData: RecyclerData<InsertedViewAdapterWithLoader<TViewHolder>, GridLayoutManager, TPager, TResult, TConverted, InsertedViewHolder<TViewHolder>>) {}
|
||||||
|
|
||||||
protected fun setProgress(fin: Int, total: Int) {
|
protected fun setProgress(fin: Int, total: Int) {
|
||||||
val progress = (fin.toFloat() / total);
|
val progress = (fin.toFloat() / total);
|
||||||
_progress_bar.progress = progress;
|
_progressBar.progress = progress;
|
||||||
if(progress > 0 && progress < 1)
|
if(progress > 0 && progress < 1)
|
||||||
{
|
{
|
||||||
if(_progress_bar.height == 0)
|
if(_progressBar.height == 0)
|
||||||
_progress_bar.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 5);
|
_progressBar.layoutParams = LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 5);
|
||||||
}
|
}
|
||||||
else if(_progress_bar.height > 0) {
|
else if(_progressBar.height > 0) {
|
||||||
_progress_bar.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0);
|
_progressBar.layoutParams = LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,7 +366,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||||||
//insertPagerResults(_cache!!.cachePager.getResults(), false);
|
//insertPagerResults(_cache!!.cachePager.getResults(), false);
|
||||||
}
|
}
|
||||||
fun setPager(pager: TPager, cache: ItemCache<TResult>? = null) {
|
fun setPager(pager: TPager, cache: ItemCache<TResult>? = null) {
|
||||||
synchronized(_pager_lock) {
|
synchronized(_pagerLock) {
|
||||||
detachParentPagerEvents();
|
detachParentPagerEvents();
|
||||||
detachPagerEvents();
|
detachPagerEvents();
|
||||||
|
|
||||||
@@ -425,7 +446,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||||||
val p = recyclerData.pager;
|
val p = recyclerData.pager;
|
||||||
if(p is IReplacerPager<*>) {
|
if(p is IReplacerPager<*>) {
|
||||||
p.onReplaced.subscribe(this) { _, newItem ->
|
p.onReplaced.subscribe(this) { _, newItem ->
|
||||||
synchronized(_pager_lock) {
|
synchronized(_pagerLock) {
|
||||||
val filtered = filterResults(listOf(newItem as TResult));
|
val filtered = filterResults(listOf(newItem as TResult));
|
||||||
if(filtered.isEmpty())
|
if(filtered.isEmpty())
|
||||||
return@subscribe;
|
return@subscribe;
|
||||||
@@ -443,7 +464,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||||||
|
|
||||||
var _lastNextPage = false;
|
var _lastNextPage = false;
|
||||||
private fun loadNextPage() {
|
private fun loadNextPage() {
|
||||||
synchronized(_pager_lock) {
|
synchronized(_pagerLock) {
|
||||||
val pager: TPager = recyclerData.pager ?: return;
|
val pager: TPager = recyclerData.pager ?: return;
|
||||||
val hasMorePages = pager.hasMorePages();
|
val hasMorePages = pager.hasMorePages();
|
||||||
Logger.i(TAG, "loadNextPage() hasMorePages=$hasMorePages, page size=${pager.getResults().size}");
|
Logger.i(TAG, "loadNextPage() hasMorePages=$hasMorePages, page size=${pager.getResults().size}");
|
||||||
@@ -468,7 +489,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = "FeedView";
|
private const val TAG = "FeedView";
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class ItemCache<TResult>(val cachePager: IPager<TResult>) {
|
abstract class ItemCache<TResult>(val cachePager: IPager<TResult>) {
|
||||||
|
|||||||
+34
-29
@@ -6,7 +6,7 @@ import android.view.LayoutInflater
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
@@ -18,13 +18,9 @@ import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
|||||||
import com.futo.platformplayer.engine.exceptions.ScriptExecutionException
|
import com.futo.platformplayer.engine.exceptions.ScriptExecutionException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.SearchType
|
|
||||||
import com.futo.platformplayer.states.AnnouncementType
|
|
||||||
import com.futo.platformplayer.states.StateAnnouncement
|
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StateMeta
|
import com.futo.platformplayer.states.StateMeta
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.platformplayer.states.StateSubscriptions
|
|
||||||
import com.futo.platformplayer.views.FeedStyle
|
import com.futo.platformplayer.views.FeedStyle
|
||||||
import com.futo.platformplayer.views.NoResultsView
|
import com.futo.platformplayer.views.NoResultsView
|
||||||
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
|
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
|
||||||
@@ -32,11 +28,8 @@ import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
|||||||
import com.futo.platformplayer.views.adapters.InsertedViewHolder
|
import com.futo.platformplayer.views.adapters.InsertedViewHolder
|
||||||
import com.futo.platformplayer.views.announcements.AnnouncementView
|
import com.futo.platformplayer.views.announcements.AnnouncementView
|
||||||
import com.futo.platformplayer.views.buttons.BigButton
|
import com.futo.platformplayer.views.buttons.BigButton
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.util.UUID
|
|
||||||
|
|
||||||
class HomeFragment : MainFragment() {
|
class HomeFragment : MainFragment() {
|
||||||
override val isMainView : Boolean = true;
|
override val isMainView : Boolean = true;
|
||||||
@@ -44,7 +37,15 @@ class HomeFragment : MainFragment() {
|
|||||||
override val hasBottomBar: Boolean get() = true;
|
override val hasBottomBar: Boolean get() = true;
|
||||||
|
|
||||||
private var _view: HomeView? = null;
|
private var _view: HomeView? = null;
|
||||||
private var _cachedRecyclerData: FeedView.RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null;
|
private var _cachedRecyclerData: FeedView.RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, GridLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null;
|
||||||
|
|
||||||
|
fun reloadFeed() {
|
||||||
|
_view?.reloadFeed()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun scrollToTop(smooth: Boolean) {
|
||||||
|
_view?.scrollToTop(smooth)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||||
super.onShownWithView(parameter, isBack);
|
super.onShownWithView(parameter, isBack);
|
||||||
@@ -93,16 +94,10 @@ class HomeFragment : MainFragment() {
|
|||||||
class HomeView : ContentFeedView<HomeFragment> {
|
class HomeView : ContentFeedView<HomeFragment> {
|
||||||
override val feedStyle: FeedStyle get() = Settings.instance.home.getHomeFeedStyle();
|
override val feedStyle: FeedStyle get() = Settings.instance.home.getHomeFeedStyle();
|
||||||
|
|
||||||
private var _announcementsView: AnnouncementView;
|
|
||||||
|
|
||||||
private val _taskGetPager: TaskHandler<Boolean, IPager<IPlatformContent>>;
|
private val _taskGetPager: TaskHandler<Boolean, IPager<IPlatformContent>>;
|
||||||
override val shouldShowTimeBar: Boolean get() = Settings.instance.home.progressBar
|
override val shouldShowTimeBar: Boolean get() = Settings.instance.home.progressBar
|
||||||
|
|
||||||
constructor(fragment: HomeFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData) {
|
constructor(fragment: HomeFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, GridLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData) {
|
||||||
_announcementsView = AnnouncementView(context, null).apply {
|
|
||||||
headerView.addView(this);
|
|
||||||
};
|
|
||||||
|
|
||||||
_taskGetPager = TaskHandler<Boolean, IPager<IPlatformContent>>({ fragment.lifecycleScope }, {
|
_taskGetPager = TaskHandler<Boolean, IPager<IPlatformContent>>({ fragment.lifecycleScope }, {
|
||||||
StatePlatform.instance.getHomeRefresh(fragment.lifecycleScope)
|
StatePlatform.instance.getHomeRefresh(fragment.lifecycleScope)
|
||||||
})
|
})
|
||||||
@@ -133,22 +128,18 @@ class HomeFragment : MainFragment() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
setPreviewsEnabled(Settings.instance.home.previewFeedItems);
|
setPreviewsEnabled(Settings.instance.home.previewFeedItems);
|
||||||
|
showAnnouncementView()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onShown() {
|
fun onShown() {
|
||||||
val lastClients = recyclerData.lastClients;
|
val lastClients = recyclerData.lastClients;
|
||||||
val clients = StatePlatform.instance.getSortedEnabledClient().filter { if (it is JSClient) it.enableInHome else true };
|
val clients = StatePlatform.instance.getSortedEnabledClient().filter { if (it is JSClient) it.enableInHome else true };
|
||||||
|
|
||||||
val feedstyleChanged = recyclerData.loadedFeedStyle != feedStyle;
|
val feedstyleChanged = recyclerData.loadedFeedStyle != feedStyle;
|
||||||
val clientsChanged = lastClients == null || lastClients.size != clients.size || !lastClients.containsAll(clients);
|
val clientsChanged = lastClients == null || lastClients.size != clients.size || !lastClients.containsAll(clients);
|
||||||
val outdated = recyclerData.lastLoad.getNowDiffSeconds() > 60;
|
Logger.i(TAG, "onShown (recyclerData.loadedFeedStyle=${recyclerData.loadedFeedStyle}, recyclerData.lastLoad=${recyclerData.lastLoad}, feedstyleChanged=$feedstyleChanged, clientsChanged=$clientsChanged)")
|
||||||
Logger.i(TAG, "onShown (recyclerData.loadedFeedStyle=${recyclerData.loadedFeedStyle}, recyclerData.lastLoad=${recyclerData.lastLoad}, feedstyleChanged=$feedstyleChanged, clientsChanged=$clientsChanged, outdated=$outdated)")
|
|
||||||
|
|
||||||
if(feedstyleChanged || outdated || clientsChanged) {
|
if(feedstyleChanged || clientsChanged) {
|
||||||
recyclerData.lastLoad = OffsetDateTime.now();
|
reloadFeed()
|
||||||
recyclerData.loadedFeedStyle = feedStyle;
|
|
||||||
recyclerData.lastClients = clients;
|
|
||||||
loadResults();
|
|
||||||
} else {
|
} else {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -156,7 +147,22 @@ class HomeFragment : MainFragment() {
|
|||||||
finishRefreshLayoutLoader();
|
finishRefreshLayoutLoader();
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getEmptyPagerView(): View? {
|
fun scrollToTop(smooth: Boolean) {
|
||||||
|
if (smooth) {
|
||||||
|
_recyclerResults.smoothScrollToPosition(0)
|
||||||
|
} else {
|
||||||
|
_recyclerResults.scrollToPosition(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reloadFeed() {
|
||||||
|
recyclerData.lastLoad = OffsetDateTime.now();
|
||||||
|
recyclerData.loadedFeedStyle = feedStyle;
|
||||||
|
recyclerData.lastClients = StatePlatform.instance.getSortedEnabledClient().filter { if (it is JSClient) it.enableInHome else true };
|
||||||
|
loadResults();
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getEmptyPagerView(): View {
|
||||||
val dp10 = 10.dp(resources);
|
val dp10 = 10.dp(resources);
|
||||||
val dp30 = 30.dp(resources);
|
val dp30 = 30.dp(resources);
|
||||||
|
|
||||||
@@ -188,8 +194,7 @@ class HomeFragment : MainFragment() {
|
|||||||
listOf(BigButton(context, "Sources", "Go to the sources tab", R.drawable.ic_creators) {
|
listOf(BigButton(context, "Sources", "Go to the sources tab", R.drawable.ic_creators) {
|
||||||
fragment.navigate<SourcesFragment>();
|
fragment.navigate<SourcesFragment>();
|
||||||
}.withMargin(dp10, dp30))
|
}.withMargin(dp10, dp30))
|
||||||
);
|
)
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun reload() {
|
override fun reload() {
|
||||||
@@ -209,7 +214,7 @@ class HomeFragment : MainFragment() {
|
|||||||
//StateAnnouncement.instance.registerAnnouncement(UUID.randomUUID().toString(), context.getString(R.string.no_home_available), context.getString(R.string.no_home_page_is_available_please_check_if_you_are_connected_to_the_internet_and_refresh), AnnouncementType.SESSION);
|
//StateAnnouncement.instance.registerAnnouncement(UUID.randomUUID().toString(), context.getString(R.string.no_home_available), context.getString(R.string.no_home_page_is_available_please_check_if_you_are_connected_to_the_internet_and_refresh), AnnouncementType.SESSION);
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.i(TAG, "Got new home pager ${pager}");
|
Logger.i(TAG, "Got new home pager $pager");
|
||||||
finishRefreshLayoutLoader();
|
finishRefreshLayoutLoader();
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setPager(pager);
|
setPager(pager);
|
||||||
@@ -219,7 +224,7 @@ class HomeFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val TAG = "HomeFragment";
|
const val TAG = "HomeFragment";
|
||||||
|
|
||||||
fun newInstance() = HomeFragment().apply {}
|
fun newInstance() = HomeFragment().apply {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import com.futo.platformplayer.activities.MainActivity
|
|||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.fragment.mainactivity.MainActivityFragment
|
import com.futo.platformplayer.fragment.mainactivity.MainActivityFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.topbar.TopFragment
|
import com.futo.platformplayer.fragment.mainactivity.topbar.TopFragment
|
||||||
import com.futo.platformplayer.listeners.OrientationManager
|
|
||||||
|
|
||||||
abstract class MainFragment : MainActivityFragment() {
|
abstract class MainFragment : MainActivityFragment() {
|
||||||
open val isMainView: Boolean = false;
|
open val isMainView: Boolean = false;
|
||||||
@@ -46,10 +45,6 @@ abstract class MainFragment : MainActivityFragment() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun onOrientationChanged(orientation: OrientationManager.Orientation) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun onBackPressed(): Boolean {
|
open fun onBackPressed(): Boolean {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
+58
-24
@@ -70,7 +70,7 @@ class PlaylistFragment : MainFragment() {
|
|||||||
private var _editPlaylistOverlay: SlideUpMenuOverlay? = null;
|
private var _editPlaylistOverlay: SlideUpMenuOverlay? = null;
|
||||||
private var _url: String? = null;
|
private var _url: String? = null;
|
||||||
|
|
||||||
private val _taskLoadPlaylist: TaskHandler<String, IPlatformPlaylistDetails>;
|
private val _taskLoadPlaylist: TaskHandler<String, Playlist>;
|
||||||
|
|
||||||
constructor(fragment: PlaylistFragment, inflater: LayoutInflater) : super(inflater) {
|
constructor(fragment: PlaylistFragment, inflater: LayoutInflater) : super(inflater) {
|
||||||
_fragment = fragment;
|
_fragment = fragment;
|
||||||
@@ -109,32 +109,44 @@ class PlaylistFragment : MainFragment() {
|
|||||||
val reconstruction = StatePlaylists.instance.playlistStore.getReconstructionString(playlist);
|
val reconstruction = StatePlaylists.instance.playlistStore.getReconstructionString(playlist);
|
||||||
|
|
||||||
UISlideOverlays.showOverlay(overlayContainer, context.getString(R.string.playlist) + " [${playlist.name}]", null, {},
|
UISlideOverlays.showOverlay(overlayContainer, context.getString(R.string.playlist) + " [${playlist.name}]", null, {},
|
||||||
SlideUpMenuItem(context, R.drawable.ic_list, context.getString(R.string.share_as_text), context.getString(R.string.share_as_a_list_of_video_urls), 1, {
|
SlideUpMenuItem(
|
||||||
_fragment.startActivity(ShareCompat.IntentBuilder(context)
|
context,
|
||||||
.setType("text/plain")
|
R.drawable.ic_list,
|
||||||
.setText(reconstruction)
|
context.getString(R.string.share_as_text),
|
||||||
.intent);
|
context.getString(R.string.share_as_a_list_of_video_urls),
|
||||||
}),
|
tag = 1,
|
||||||
SlideUpMenuItem(context, R.drawable.ic_move_up, context.getString(R.string.share_as_import), context.getString(R.string.share_as_a_import_file_for_grayjay), 2, {
|
call = {
|
||||||
val shareUri = StatePlaylists.instance.createPlaylistShareJsonUri(context, playlist);
|
_fragment.startActivity(ShareCompat.IntentBuilder(context)
|
||||||
_fragment.startActivity(ShareCompat.IntentBuilder(context)
|
.setType("text/plain")
|
||||||
.setType("application/json")
|
.setText(reconstruction)
|
||||||
.setStream(shareUri)
|
.intent);
|
||||||
.intent);
|
}),
|
||||||
})
|
SlideUpMenuItem(
|
||||||
|
context,
|
||||||
|
R.drawable.ic_move_up,
|
||||||
|
context.getString(R.string.share_as_import),
|
||||||
|
context.getString(R.string.share_as_a_import_file_for_grayjay),
|
||||||
|
tag = 2,
|
||||||
|
call = {
|
||||||
|
val shareUri = StatePlaylists.instance.createPlaylistShareJsonUri(context, playlist);
|
||||||
|
_fragment.startActivity(ShareCompat.IntentBuilder(context)
|
||||||
|
.setType("application/json")
|
||||||
|
.setStream(shareUri)
|
||||||
|
.intent);
|
||||||
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
_taskLoadPlaylist = TaskHandler<String, IPlatformPlaylistDetails>(
|
_taskLoadPlaylist = TaskHandler<String, Playlist>(
|
||||||
StateApp.instance.scopeGetter,
|
StateApp.instance.scopeGetter,
|
||||||
{
|
{
|
||||||
return@TaskHandler StatePlatform.instance.getPlaylist(it);
|
return@TaskHandler StatePlatform.instance.getPlaylist(it).toPlaylist();
|
||||||
})
|
})
|
||||||
.success {
|
.success {
|
||||||
setName(it.name);
|
setName(it.name);
|
||||||
//TODO: Implement support for pagination
|
//TODO: Implement support for pagination
|
||||||
setVideos(it.toPlaylist().videos, false);
|
setVideos(it.videos, false);
|
||||||
setVideoCount(it.videoCount);
|
setVideoCount(it.videos.size);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
@@ -144,6 +156,14 @@ class PlaylistFragment : MainFragment() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun copyPlaylist(playlist: Playlist) {
|
||||||
|
StatePlaylists.instance.playlistStore.save(playlist)
|
||||||
|
_fragment.topBar?.assume<NavigationTopBarFragment>()?.setMenuItems(
|
||||||
|
arrayListOf()
|
||||||
|
)
|
||||||
|
UIDialogs.toast("Playlist saved")
|
||||||
|
}
|
||||||
|
|
||||||
fun onShown(parameter: Any?) {
|
fun onShown(parameter: Any?) {
|
||||||
_taskLoadPlaylist.cancel()
|
_taskLoadPlaylist.cancel()
|
||||||
|
|
||||||
@@ -158,14 +178,10 @@ class PlaylistFragment : MainFragment() {
|
|||||||
setButtonDownloadVisible(true)
|
setButtonDownloadVisible(true)
|
||||||
setButtonEditVisible(true)
|
setButtonEditVisible(true)
|
||||||
|
|
||||||
if (!StatePlaylists.instance.playlistStore.getItems().contains(parameter)) {
|
if (!StatePlaylists.instance.playlistStore.hasItem { it.id == parameter.id }) {
|
||||||
_fragment.topBar?.assume<NavigationTopBarFragment>()
|
_fragment.topBar?.assume<NavigationTopBarFragment>()
|
||||||
?.setMenuItems(arrayListOf(Pair(R.drawable.ic_copy) {
|
?.setMenuItems(arrayListOf(Pair(R.drawable.ic_copy) {
|
||||||
StatePlaylists.instance.playlistStore.save(parameter)
|
copyPlaylist(parameter)
|
||||||
_fragment.topBar?.assume<NavigationTopBarFragment>()?.setMenuItems(
|
|
||||||
arrayListOf()
|
|
||||||
)
|
|
||||||
UIDialogs.toast("Playlist saved")
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -230,6 +246,15 @@ class PlaylistFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun download() {
|
private fun download() {
|
||||||
|
val playlist = _playlist ?: return
|
||||||
|
if (!StatePlaylists.instance.playlistStore.hasItem { it.id == playlist.id }) {
|
||||||
|
UIDialogs.showConfirmationDialog(context, "Playlist must be saved to download", {
|
||||||
|
copyPlaylist(playlist)
|
||||||
|
download()
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
_playlist?.let {
|
_playlist?.let {
|
||||||
UISlideOverlays.showDownloadPlaylistOverlay(it, overlayContainer);
|
UISlideOverlays.showDownloadPlaylistOverlay(it, overlayContainer);
|
||||||
}
|
}
|
||||||
@@ -254,6 +279,15 @@ class PlaylistFragment : MainFragment() {
|
|||||||
override fun canEdit(): Boolean { return _playlist != null; }
|
override fun canEdit(): Boolean { return _playlist != null; }
|
||||||
|
|
||||||
override fun onEditClick() {
|
override fun onEditClick() {
|
||||||
|
val playlist = _playlist ?: return
|
||||||
|
if (!StatePlaylists.instance.playlistStore.hasItem { it.id == playlist.id }) {
|
||||||
|
UIDialogs.showConfirmationDialog(context, "Playlist must be saved to edit the name", {
|
||||||
|
copyPlaylist(playlist)
|
||||||
|
onEditClick()
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
_editPlaylistNameInput?.activate();
|
_editPlaylistNameInput?.activate();
|
||||||
_editPlaylistOverlay?.show();
|
_editPlaylistOverlay?.show();
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -12,6 +12,7 @@ import android.widget.LinearLayout
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
@@ -23,6 +24,8 @@ import com.futo.platformplayer.states.StatePlaylists
|
|||||||
import com.futo.platformplayer.views.adapters.*
|
import com.futo.platformplayer.views.adapters.*
|
||||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
|
||||||
class PlaylistsFragment : MainFragment() {
|
class PlaylistsFragment : MainFragment() {
|
||||||
@@ -119,7 +122,9 @@ class PlaylistsFragment : MainFragment() {
|
|||||||
|
|
||||||
findViewById<TextView>(R.id.text_view_all).setOnClickListener { _fragment.navigate<WatchLaterFragment>(context.getString(R.string.watch_later)); };
|
findViewById<TextView>(R.id.text_view_all).setOnClickListener { _fragment.navigate<WatchLaterFragment>(context.getString(R.string.watch_later)); };
|
||||||
StatePlaylists.instance.onWatchLaterChanged.subscribe(this) {
|
StatePlaylists.instance.onWatchLaterChanged.subscribe(this) {
|
||||||
updateWatchLater();
|
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
updateWatchLater();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+46
-31
@@ -9,6 +9,7 @@ import android.view.LayoutInflater
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.ViewPropertyAnimator
|
import android.view.ViewPropertyAnimator
|
||||||
|
import android.widget.Button
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
@@ -19,6 +20,7 @@ import androidx.core.view.children
|
|||||||
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.Settings
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.api.media.PlatformID
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
import com.futo.platformplayer.api.media.models.Thumbnails
|
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||||
@@ -135,10 +137,7 @@ class PostDetailFragment : MainFragment {
|
|||||||
private val _imageDislikeIcon: ImageView;
|
private val _imageDislikeIcon: ImageView;
|
||||||
private val _textDislikes: TextView;
|
private val _textDislikes: TextView;
|
||||||
|
|
||||||
private val _textComments: TextView;
|
|
||||||
private val _textCommentType: TextView;
|
|
||||||
private val _addCommentView: AddCommentView;
|
private val _addCommentView: AddCommentView;
|
||||||
private val _toggleCommentType: Toggle;
|
|
||||||
|
|
||||||
private val _rating: PillRatingLikesDislikes;
|
private val _rating: PillRatingLikesDislikes;
|
||||||
|
|
||||||
@@ -152,6 +151,10 @@ class PostDetailFragment : MainFragment {
|
|||||||
|
|
||||||
private val _commentsList: CommentsList;
|
private val _commentsList: CommentsList;
|
||||||
|
|
||||||
|
private var _commentType: Boolean? = null;
|
||||||
|
private val _buttonPolycentric: Button
|
||||||
|
private val _buttonPlatform: Button
|
||||||
|
|
||||||
private val _taskLoadPost = if(!isInEditMode) TaskHandler<String, IPlatformPostDetails>(
|
private val _taskLoadPost = if(!isInEditMode) TaskHandler<String, IPlatformPostDetails>(
|
||||||
StateApp.instance.scopeGetter,
|
StateApp.instance.scopeGetter,
|
||||||
{
|
{
|
||||||
@@ -198,9 +201,6 @@ class PostDetailFragment : MainFragment {
|
|||||||
_textDislikes = findViewById(R.id.text_dislikes);
|
_textDislikes = findViewById(R.id.text_dislikes);
|
||||||
|
|
||||||
_commentsList = findViewById(R.id.comments_list);
|
_commentsList = findViewById(R.id.comments_list);
|
||||||
_textCommentType = findViewById(R.id.text_comment_type);
|
|
||||||
_toggleCommentType = findViewById(R.id.toggle_comment_type);
|
|
||||||
_textComments = findViewById(R.id.text_comments);
|
|
||||||
_addCommentView = findViewById(R.id.add_comment_view);
|
_addCommentView = findViewById(R.id.add_comment_view);
|
||||||
|
|
||||||
_rating = findViewById(R.id.rating);
|
_rating = findViewById(R.id.rating);
|
||||||
@@ -213,6 +213,9 @@ class PostDetailFragment : MainFragment {
|
|||||||
|
|
||||||
_repliesOverlay = findViewById(R.id.replies_overlay);
|
_repliesOverlay = findViewById(R.id.replies_overlay);
|
||||||
|
|
||||||
|
_buttonPolycentric = findViewById(R.id.button_polycentric)
|
||||||
|
_buttonPlatform = findViewById(R.id.button_platform)
|
||||||
|
|
||||||
_textContent.setPlatformPlayerLinkMovementMethod(context);
|
_textContent.setPlatformPlayerLinkMovementMethod(context);
|
||||||
|
|
||||||
_buttonSubscribe.onSubscribed.subscribe {
|
_buttonSubscribe.onSubscribed.subscribe {
|
||||||
@@ -224,9 +227,10 @@ class PostDetailFragment : MainFragment {
|
|||||||
root.removeView(layoutTop);
|
root.removeView(layoutTop);
|
||||||
_commentsList.setPrependedView(layoutTop);
|
_commentsList.setPrependedView(layoutTop);
|
||||||
|
|
||||||
|
/*TODO: Why is this here?
|
||||||
_commentsList.onCommentsLoaded.subscribe {
|
_commentsList.onCommentsLoaded.subscribe {
|
||||||
updateCommentType(false);
|
updateCommentType(false);
|
||||||
};
|
};*/
|
||||||
|
|
||||||
_commentsList.onRepliesClick.subscribe { c ->
|
_commentsList.onRepliesClick.subscribe { c ->
|
||||||
val replyCount = c.replyCount ?: 0;
|
val replyCount = c.replyCount ?: 0;
|
||||||
@@ -237,7 +241,7 @@ class PostDetailFragment : MainFragment {
|
|||||||
|
|
||||||
if (c is PolycentricPlatformComment) {
|
if (c is PolycentricPlatformComment) {
|
||||||
var parentComment: PolycentricPlatformComment = c;
|
var parentComment: PolycentricPlatformComment = c;
|
||||||
_repliesOverlay.load(_toggleCommentType.value, metadata, c.contextUrl, c.reference, c,
|
_repliesOverlay.load(_commentType!!, metadata, c.contextUrl, c.reference, c,
|
||||||
{ StatePolycentric.instance.getCommentPager(c.contextUrl, c.reference) },
|
{ StatePolycentric.instance.getCommentPager(c.contextUrl, c.reference) },
|
||||||
{
|
{
|
||||||
val newComment = parentComment.cloneWithUpdatedReplyCount((parentComment.replyCount ?: 0) + 1);
|
val newComment = parentComment.cloneWithUpdatedReplyCount((parentComment.replyCount ?: 0) + 1);
|
||||||
@@ -245,22 +249,23 @@ class PostDetailFragment : MainFragment {
|
|||||||
parentComment = newComment;
|
parentComment = newComment;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
_repliesOverlay.load(_toggleCommentType.value, metadata, null, null, c, { StatePlatform.instance.getSubComments(c) });
|
_repliesOverlay.load(_commentType!!, metadata, null, null, c, { StatePlatform.instance.getSubComments(c) });
|
||||||
}
|
}
|
||||||
|
|
||||||
setRepliesOverlayVisible(isVisible = true, animate = true);
|
setRepliesOverlayVisible(isVisible = true, animate = true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (StatePolycentric.instance.enabled) {
|
||||||
|
_buttonPolycentric.setOnClickListener {
|
||||||
|
updateCommentType(false)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_buttonPolycentric.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
_toggleCommentType.onValueChanged.subscribe {
|
_buttonPlatform.setOnClickListener {
|
||||||
updateCommentType(true);
|
updateCommentType(true)
|
||||||
};
|
}
|
||||||
|
|
||||||
_textCommentType.setOnClickListener {
|
|
||||||
_toggleCommentType.setValue(!_toggleCommentType.value, true);
|
|
||||||
updateCommentType(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
_layoutMonetization.visibility = View.GONE;
|
_layoutMonetization.visibility = View.GONE;
|
||||||
|
|
||||||
_buttonSupport.setOnClickListener {
|
_buttonSupport.setOnClickListener {
|
||||||
@@ -432,7 +437,7 @@ class PostDetailFragment : MainFragment {
|
|||||||
_taskLoadPolycentricProfile.cancel();
|
_taskLoadPolycentricProfile.cancel();
|
||||||
_version++;
|
_version++;
|
||||||
|
|
||||||
_toggleCommentType.setValue(false, false);
|
updateCommentType(null)
|
||||||
_url = null;
|
_url = null;
|
||||||
_post = null;
|
_post = null;
|
||||||
_postOverview = null;
|
_postOverview = null;
|
||||||
@@ -476,7 +481,8 @@ class PostDetailFragment : MainFragment {
|
|||||||
_addCommentView.setContext(value.url, Models.referenceFromBuffer(value.url.toByteArray()));
|
_addCommentView.setContext(value.url, Models.referenceFromBuffer(value.url.toByteArray()));
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCommentType(true);
|
val commentType = !Settings.instance.other.polycentricEnabled || Settings.instance.comments.defaultCommentSection == 1
|
||||||
|
updateCommentType(commentType, true);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -679,20 +685,29 @@ class PostDetailFragment : MainFragment {
|
|||||||
_commentsList.load(false) { StatePolycentric.instance.getCommentPager(post!!.url, ref, listOfNotNull(extraBytesRef)); };
|
_commentsList.load(false) { StatePolycentric.instance.getCommentPager(post!!.url, ref, listOfNotNull(extraBytesRef)); };
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateCommentType(reloadComments: Boolean) {
|
private fun updateCommentType(commentType: Boolean?, forceReload: Boolean = false) {
|
||||||
if (_toggleCommentType.value) {
|
val changed = commentType != _commentType
|
||||||
_textCommentType.text = "Platform";
|
_commentType = commentType
|
||||||
_addCommentView.visibility = View.GONE;
|
|
||||||
|
|
||||||
if (reloadComments) {
|
if (commentType == null) {
|
||||||
fetchComments();
|
_buttonPlatform.setTextColor(resources.getColor(R.color.gray_ac))
|
||||||
}
|
_buttonPolycentric.setTextColor(resources.getColor(R.color.gray_ac))
|
||||||
} else {
|
} else {
|
||||||
_textCommentType.text = "Polycentric";
|
_buttonPlatform.setTextColor(resources.getColor(if (commentType) R.color.white else R.color.gray_ac))
|
||||||
_addCommentView.visibility = View.VISIBLE;
|
_buttonPolycentric.setTextColor(resources.getColor(if (!commentType) R.color.white else R.color.gray_ac))
|
||||||
|
|
||||||
if (reloadComments) {
|
if (commentType) {
|
||||||
fetchPolycentricComments()
|
_addCommentView.visibility = View.GONE;
|
||||||
|
|
||||||
|
if (forceReload || changed) {
|
||||||
|
fetchComments();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_addCommentView.visibility = View.VISIBLE;
|
||||||
|
|
||||||
|
if (forceReload || changed) {
|
||||||
|
fetchPolycentricComments()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-7
@@ -397,23 +397,43 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
UIDialogs.Action("Cancel", {}, UIDialogs.ActionStyle.NONE),
|
UIDialogs.Action("Cancel", {}, UIDialogs.ActionStyle.NONE),
|
||||||
UIDialogs.Action("Login", {
|
UIDialogs.Action("Login", {
|
||||||
LoginActivity.showLogin(StateApp.instance.context, config) {
|
LoginActivity.showLogin(StateApp.instance.context, config) {
|
||||||
StatePlugins.instance.setPluginAuth(config.id, it);
|
try {
|
||||||
reloadSource(config.id);
|
StatePlugins.instance.setPluginAuth(config.id, it);
|
||||||
|
reloadSource(config.id);
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
||||||
|
context?.let { c -> UIDialogs.showGeneralErrorDialog(c, "Failed to set plugin authentication (loginSource, loginWarning)", e) }
|
||||||
|
}
|
||||||
|
Logger.e(TAG, "Failed to set plugin authentication (loginSource, loginWarning)", e)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, UIDialogs.ActionStyle.PRIMARY))
|
}, UIDialogs.ActionStyle.PRIMARY))
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
LoginActivity.showLogin(StateApp.instance.context, config) {
|
LoginActivity.showLogin(StateApp.instance.context, config) {
|
||||||
StatePlugins.instance.setPluginAuth(config.id, it);
|
try {
|
||||||
reloadSource(config.id);
|
StatePlugins.instance.setPluginAuth(config.id, it);
|
||||||
|
reloadSource(config.id);
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
||||||
|
context?.let { c -> UIDialogs.showGeneralErrorDialog(c, "Failed to set plugin authentication (loginSource)", e) }
|
||||||
|
}
|
||||||
|
Logger.e(TAG, "Failed to set plugin authentication (loginSource)", e)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
private fun logoutSource(clear: Boolean = true) {
|
private fun logoutSource(clear: Boolean = true) {
|
||||||
val config = _config ?: return;
|
val config = _config ?: return;
|
||||||
|
|
||||||
StatePlugins.instance.setPluginAuth(config.id, null);
|
try {
|
||||||
reloadSource(config.id);
|
StatePlugins.instance.setPluginAuth(config.id, null);
|
||||||
|
reloadSource(config.id);
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
||||||
|
context?.let { c -> UIDialogs.showGeneralErrorDialog(c, "Failed to clear plugin authentication", e) }
|
||||||
|
}
|
||||||
|
Logger.e(TAG, "Failed to clear plugin authentication", e)
|
||||||
|
}
|
||||||
|
|
||||||
//TODO: Maybe add a dialog option..
|
//TODO: Maybe add a dialog option..
|
||||||
if(Settings.instance.plugins.clearCookiesOnLogout && clear) {
|
if(Settings.instance.plugins.clearCookiesOnLogout && clear) {
|
||||||
|
|||||||
+6
-4
@@ -180,7 +180,7 @@ class SubscriptionGroupFragment : MainFragment() {
|
|||||||
UIDialogs.showDialog(context, R.drawable.ic_trash, "Delete Group", "Are you sure you want to this group?\n[${g.name}]?", null, 0,
|
UIDialogs.showDialog(context, R.drawable.ic_trash, "Delete Group", "Are you sure you want to this group?\n[${g.name}]?", null, 0,
|
||||||
UIDialogs.Action("Cancel", {}),
|
UIDialogs.Action("Cancel", {}),
|
||||||
UIDialogs.Action("Delete", {
|
UIDialogs.Action("Delete", {
|
||||||
StateSubscriptionGroups.instance.deleteSubscriptionGroup(g.id);
|
StateSubscriptionGroups.instance.deleteSubscriptionGroup(g.id, true);
|
||||||
_didDelete = true;
|
_didDelete = true;
|
||||||
fragment.close(true);
|
fragment.close(true);
|
||||||
}, UIDialogs.ActionStyle.DANGEROUS))
|
}, UIDialogs.ActionStyle.DANGEROUS))
|
||||||
@@ -253,7 +253,7 @@ class SubscriptionGroupFragment : MainFragment() {
|
|||||||
if(g.urls.isEmpty() && g.image == null) {
|
if(g.urls.isEmpty() && g.image == null) {
|
||||||
//Obtain image
|
//Obtain image
|
||||||
for(sub in it) {
|
for(sub in it) {
|
||||||
val sub = StateSubscriptions.instance.getSubscription(sub);
|
val sub = StateSubscriptions.instance.getSubscription(sub) ?: StateSubscriptions.instance.getSubscriptionOther(sub);
|
||||||
if(sub != null && sub.channel.thumbnail != null) {
|
if(sub != null && sub.channel.thumbnail != null) {
|
||||||
g.image = ImageVariable.fromUrl(sub.channel.thumbnail!!);
|
g.image = ImageVariable.fromUrl(sub.channel.thumbnail!!);
|
||||||
g.image?.setImageView(_imageGroup);
|
g.image?.setImageView(_imageGroup);
|
||||||
@@ -308,8 +308,10 @@ class SubscriptionGroupFragment : MainFragment() {
|
|||||||
|
|
||||||
if(group != null) {
|
if(group != null) {
|
||||||
val urls = group.urls.toList();
|
val urls = group.urls.toList();
|
||||||
val subs = StateSubscriptions.instance.getSubscriptions().map { it.channel }
|
val subs = urls.map {
|
||||||
_enabledCreators.addAll(subs.filter { urls.contains(it.url) });
|
(StateSubscriptions.instance.getSubscription(it) ?: StateSubscriptions.instance.getSubscriptionOther(it))?.channel
|
||||||
|
}.filterNotNull();
|
||||||
|
_enabledCreators.addAll(subs);
|
||||||
}
|
}
|
||||||
updateMeta();
|
updateMeta();
|
||||||
filterCreators();
|
filterCreators();
|
||||||
|
|||||||
+14
-4
@@ -14,6 +14,7 @@ import androidx.lifecycle.lifecycleScope
|
|||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.UISlideOverlays
|
import com.futo.platformplayer.UISlideOverlays
|
||||||
import com.futo.platformplayer.activities.AddSourceOptionsActivity
|
import com.futo.platformplayer.activities.AddSourceOptionsActivity
|
||||||
import com.futo.platformplayer.fragment.mainactivity.topbar.AddTopBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.topbar.AddTopBarFragment
|
||||||
@@ -57,10 +58,19 @@ class SubscriptionGroupListFragment : MainFragment() {
|
|||||||
|
|
||||||
};
|
};
|
||||||
it.onDelete.subscribe { group ->
|
it.onDelete.subscribe { group ->
|
||||||
val loc = _subs.indexOf(group);
|
context?.let { context ->
|
||||||
_subs.remove(group);
|
UIDialogs.showDialog(context, R.drawable.ic_trash, "Delete Group", "Are you sure you want to this group?\n[${group.name}]?", null, 0,
|
||||||
_list?.adapter?.notifyItemRangeRemoved(loc);
|
UIDialogs.Action("Cancel", {}),
|
||||||
StateSubscriptionGroups.instance.deleteSubscriptionGroup(group.id);
|
UIDialogs.Action("Delete", {
|
||||||
|
StateSubscriptionGroups.instance.deleteSubscriptionGroup(group.id, true);
|
||||||
|
|
||||||
|
val loc = _subs.indexOf(group);
|
||||||
|
_subs.remove(group);
|
||||||
|
_list?.adapter?.notifyItemRangeRemoved(loc);
|
||||||
|
StateSubscriptionGroups.instance.deleteSubscriptionGroup(group.id, true);
|
||||||
|
|
||||||
|
}, UIDialogs.ActionStyle.DANGEROUS));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
it.onDragDrop.subscribe {
|
it.onDragDrop.subscribe {
|
||||||
_touchHelper?.startDrag(it);
|
_touchHelper?.startDrag(it);
|
||||||
|
|||||||
+15
-33
@@ -5,12 +5,10 @@ import android.os.Bundle
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.LinearLayout
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.api.media.IPlatformClient
|
|
||||||
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.video.IPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
@@ -27,6 +25,7 @@ import com.futo.platformplayer.states.StateApp
|
|||||||
import com.futo.platformplayer.states.StateCache
|
import com.futo.platformplayer.states.StateCache
|
||||||
import com.futo.platformplayer.states.StateHistory
|
import com.futo.platformplayer.states.StateHistory
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
|
import com.futo.platformplayer.states.StatePlugins
|
||||||
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
|
||||||
@@ -36,7 +35,6 @@ import com.futo.platformplayer.views.ToastView
|
|||||||
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
|
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
|
||||||
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||||
import com.futo.platformplayer.views.adapters.InsertedViewHolder
|
import com.futo.platformplayer.views.adapters.InsertedViewHolder
|
||||||
import com.futo.platformplayer.views.announcements.AnnouncementView
|
|
||||||
import com.futo.platformplayer.views.buttons.BigButton
|
import com.futo.platformplayer.views.buttons.BigButton
|
||||||
import com.futo.platformplayer.views.subscriptions.SubscriptionBar
|
import com.futo.platformplayer.views.subscriptions.SubscriptionBar
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
@@ -46,7 +44,6 @@ import kotlinx.coroutines.withContext
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.nio.channels.Channel
|
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import kotlin.system.measureTimeMillis
|
import kotlin.system.measureTimeMillis
|
||||||
|
|
||||||
@@ -57,7 +54,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
|
|
||||||
private var _view: SubscriptionsFeedView? = null;
|
private var _view: SubscriptionsFeedView? = null;
|
||||||
private var _group: SubscriptionGroup? = null;
|
private var _group: SubscriptionGroup? = null;
|
||||||
private var _cachedRecyclerData: FeedView.RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null;
|
private var _cachedRecyclerData: FeedView.RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, GridLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null;
|
||||||
|
|
||||||
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||||
super.onShownWithView(parameter, isBack);
|
super.onShownWithView(parameter, isBack);
|
||||||
@@ -110,7 +107,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
|
|
||||||
var subGroup: SubscriptionGroup? = null;
|
var subGroup: SubscriptionGroup? = null;
|
||||||
|
|
||||||
constructor(fragment: SubscriptionsFeedFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData) {
|
constructor(fragment: SubscriptionsFeedFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, GridLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData) {
|
||||||
Logger.i(TAG, "SubscriptionsFeedFragment constructor()");
|
Logger.i(TAG, "SubscriptionsFeedFragment constructor()");
|
||||||
StateSubscriptions.instance.global.onUpdateProgress.subscribe(this) { progress, total ->
|
StateSubscriptions.instance.global.onUpdateProgress.subscribe(this) { progress, total ->
|
||||||
};
|
};
|
||||||
@@ -127,6 +124,9 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
initializeToolbarContent();
|
initializeToolbarContent();
|
||||||
|
|
||||||
setPreviewsEnabled(Settings.instance.subscriptions.previewFeedItems);
|
setPreviewsEnabled(Settings.instance.subscriptions.previewFeedItems);
|
||||||
|
if (Settings.instance.tabs.find { it.id == 0 }?.enabled != true) {
|
||||||
|
showAnnouncementView()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onShown() {
|
fun onShown() {
|
||||||
@@ -147,23 +147,6 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val announcementsView = _announcementsView;
|
|
||||||
val homeTab = Settings.instance.tabs.find { it.id == 0 };
|
|
||||||
val isHomeEnabled = homeTab?.enabled == true;
|
|
||||||
if (announcementsView != null && isHomeEnabled) {
|
|
||||||
headerView.removeView(announcementsView);
|
|
||||||
_announcementsView = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (announcementsView == null && !isHomeEnabled) {
|
|
||||||
val c = context;
|
|
||||||
if (c != null) {
|
|
||||||
_announcementsView = AnnouncementView(c, null).apply {
|
|
||||||
headerView.addView(this)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!StateSubscriptions.instance.global.isGlobalUpdating) {
|
if (!StateSubscriptions.instance.global.isGlobalUpdating) {
|
||||||
finishRefreshLayoutLoader();
|
finishRefreshLayoutLoader();
|
||||||
}
|
}
|
||||||
@@ -191,8 +174,6 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
|
|
||||||
private var _subscriptionBar: SubscriptionBar? = null;
|
private var _subscriptionBar: SubscriptionBar? = null;
|
||||||
|
|
||||||
private var _announcementsView: AnnouncementView? = null;
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class FeedFilterSettings: FragmentedStorageFileJson() {
|
class FeedFilterSettings: FragmentedStorageFileJson() {
|
||||||
val allowContentTypes: MutableList<ContentType> = mutableListOf(ContentType.MEDIA, ContentType.POST);
|
val allowContentTypes: MutableList<ContentType> = mutableListOf(ContentType.MEDIA, ContentType.POST);
|
||||||
@@ -214,7 +195,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
val subRequestCounts = StateSubscriptions.instance.getSubscriptionRequestCount(group);
|
val subRequestCounts = StateSubscriptions.instance.getSubscriptionRequestCount(group);
|
||||||
val reqCountStr = subRequestCounts.map { " ${it.key.config.name}: ${it.value}/${it.key.getSubscriptionRateLimit()}" }.joinToString("\n");
|
val reqCountStr = subRequestCounts.map { " ${it.key.config.name}: ${it.value}/${it.key.getSubscriptionRateLimit()}" }.joinToString("\n");
|
||||||
val rateLimitPlugins = subRequestCounts.filter { clientCount -> clientCount.key.getSubscriptionRateLimit()?.let { rateLimit -> clientCount.value > rateLimit } == true }
|
val rateLimitPlugins = subRequestCounts.filter { clientCount -> clientCount.key.getSubscriptionRateLimit()?.let { rateLimit -> clientCount.value > rateLimit } == true }
|
||||||
Logger.w(TAG, "Trying to refreshing subscriptions with requests:\n" + reqCountStr);
|
Logger.w(TAG, "Trying to refreshing subscriptions with requests:\n$reqCountStr");
|
||||||
if(rateLimitPlugins.any())
|
if(rateLimitPlugins.any())
|
||||||
throw RateLimitException(rateLimitPlugins.map { it.key.id });
|
throw RateLimitException(rateLimitPlugins.map { it.key.id });
|
||||||
}
|
}
|
||||||
@@ -276,7 +257,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
|
|
||||||
private fun initializeToolbarContent() {
|
private fun initializeToolbarContent() {
|
||||||
_subscriptionBar = SubscriptionBar(context).apply {
|
_subscriptionBar = SubscriptionBar(context).apply {
|
||||||
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
|
||||||
};
|
};
|
||||||
_subscriptionBar?.onClickChannel?.subscribe { c -> fragment.navigate<ChannelFragment>(c); };
|
_subscriptionBar?.onClickChannel?.subscribe { c -> fragment.navigate<ChannelFragment>(c); };
|
||||||
_subscriptionBar?.onToggleGroup?.subscribe { g ->
|
_subscriptionBar?.onToggleGroup?.subscribe { g ->
|
||||||
@@ -364,6 +345,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun reload() {
|
override fun reload() {
|
||||||
|
StatePlugins.instance.clearUpdating(); //Fallback in case it doesnt clear, UI should be blocked.
|
||||||
loadResults(true);
|
loadResults(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,7 +377,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
_taskGetPager.run(withRefetch);
|
_taskGetPager.run(withRefetch);
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRestoreCachedData(cachedData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>) {
|
override fun onRestoreCachedData(cachedData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, GridLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>) {
|
||||||
super.onRestoreCachedData(cachedData);
|
super.onRestoreCachedData(cachedData);
|
||||||
setEmptyPager(cachedData.results.isEmpty());
|
setEmptyPager(cachedData.results.isEmpty());
|
||||||
}
|
}
|
||||||
@@ -450,7 +432,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
if (toShow is PluginException)
|
if (toShow is PluginException)
|
||||||
UIDialogs.appToast(ToastView.Toast(
|
UIDialogs.appToast(ToastView.Toast(
|
||||||
toShow.message +
|
toShow.message +
|
||||||
(if(channel != null) "\nChannel: " + channel else ""), false, null,
|
(if(channel != null) "\nChannel: $channel" else ""), false, null,
|
||||||
"Plugin ${toShow.config.name} failed")
|
"Plugin ${toShow.config.name} failed")
|
||||||
);
|
);
|
||||||
else
|
else
|
||||||
@@ -461,14 +443,14 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
val failedChannels = exs.filterIsInstance<ChannelException>().map { it.channelNameOrUrl }.distinct().toList();
|
val failedChannels = exs.filterIsInstance<ChannelException>().map { it.channelNameOrUrl }.distinct().toList();
|
||||||
val failedPlugins = exs.filter { it is PluginException || (it is ChannelException && it.cause is PluginException) }
|
val failedPlugins = exs.filter { it is PluginException || (it is ChannelException && it.cause is PluginException) }
|
||||||
.map { if(it is ChannelException) (it.cause as PluginException) else if(it is PluginException) it else null }
|
.map { if(it is ChannelException) (it.cause as PluginException) else if(it is PluginException) it else null }
|
||||||
.filter { it != null }
|
.filterNotNull()
|
||||||
.distinctBy { it?.config?.name }
|
.distinctBy { it?.config?.name }
|
||||||
.map { it!! }
|
.map { it!! }
|
||||||
.toList();
|
.toList();
|
||||||
for(distinctPluginFail in failedPlugins)
|
for(distinctPluginFail in failedPlugins)
|
||||||
UIDialogs.appToast(context.getString(R.string.plugin_pluginname_failed_message).replace("{pluginName}", distinctPluginFail.config.name).replace("{message}", distinctPluginFail.message ?: ""));
|
UIDialogs.appToast(context.getString(R.string.plugin_pluginname_failed_message).replace("{pluginName}", distinctPluginFail.config.name).replace("{message}", distinctPluginFail.message ?: ""));
|
||||||
if(failedChannels.isNotEmpty())
|
if(failedChannels.isNotEmpty())
|
||||||
UIDialogs.appToast(ToastView.Toast(failedChannels.take(3).map { "- ${it}" }.joinToString("\n") +
|
UIDialogs.appToast(ToastView.Toast(failedChannels.take(3).map { "- $it" }.joinToString("\n") +
|
||||||
(if(failedChannels.size >= 3) "\nAnd ${failedChannels.size - 3} more" else ""), false, null, "Failed Channels"));
|
(if(failedChannels.size >= 3) "\nAnd ${failedChannels.size - 3} more" else ""), false, null, "Failed Channels"));
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
@@ -480,7 +462,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val TAG = "SubscriptionsFeedFragment";
|
const val TAG = "SubscriptionsFeedFragment";
|
||||||
|
|
||||||
fun newInstance() = SubscriptionsFeedFragment().apply {}
|
fun newInstance() = SubscriptionsFeedFragment().apply {}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-2
@@ -117,8 +117,14 @@ class SuggestionsFragment : MainFragment {
|
|||||||
} else if (_searchType == SearchType.PLAYLIST) {
|
} else if (_searchType == SearchType.PLAYLIST) {
|
||||||
navigate<PlaylistSearchResultsFragment>(it);
|
navigate<PlaylistSearchResultsFragment>(it);
|
||||||
} else {
|
} else {
|
||||||
if(it.isHttpUrl())
|
if(it.isHttpUrl()) {
|
||||||
navigate<VideoDetailFragment>(it);
|
if(StatePlatform.instance.hasEnabledPlaylistClient(it))
|
||||||
|
navigate<RemotePlaylistFragment>(it);
|
||||||
|
else if(StatePlatform.instance.hasEnabledChannelClient(it))
|
||||||
|
navigate<ChannelFragment>(it);
|
||||||
|
else
|
||||||
|
navigate<VideoDetailFragment>(it);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
navigate<ContentSearchResultsFragment>(SuggestionsFragmentData(it, SearchType.VIDEO, _channelUrl));
|
navigate<ContentSearchResultsFragment>(SuggestionsFragmentData(it, SearchType.VIDEO, _channelUrl));
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-3
@@ -6,6 +6,7 @@ import android.view.LayoutInflater
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.ScrollView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
import com.futo.platformplayer.api.media.IPlatformClient
|
import com.futo.platformplayer.api.media.IPlatformClient
|
||||||
@@ -58,7 +59,15 @@ class TutorialFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("ViewConstructor")
|
@SuppressLint("ViewConstructor")
|
||||||
class TutorialView : LinearLayout {
|
class TutorialView(fragment: TutorialFragment, inflater: LayoutInflater) :
|
||||||
|
ScrollView(inflater.context) {
|
||||||
|
init {
|
||||||
|
addView(TutorialContainer(fragment, inflater))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ViewConstructor")
|
||||||
|
class TutorialContainer : LinearLayout {
|
||||||
val fragment: TutorialFragment
|
val fragment: TutorialFragment
|
||||||
|
|
||||||
constructor(fragment: TutorialFragment, inflater: LayoutInflater) : super(inflater.context) {
|
constructor(fragment: TutorialFragment, inflater: LayoutInflater) : super(inflater.context) {
|
||||||
@@ -150,7 +159,7 @@ class TutorialFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val TAG = "HomeFragment";
|
const val TAG = "HomeFragment";
|
||||||
|
|
||||||
fun newInstance() = TutorialFragment().apply {}
|
fun newInstance() = TutorialFragment().apply {}
|
||||||
val initialSetupVideos = listOf(
|
val initialSetupVideos = listOf(
|
||||||
@@ -200,7 +209,7 @@ class TutorialFragment : MainFragment() {
|
|||||||
TutorialVideo(
|
TutorialVideo(
|
||||||
uuid = "94d36959-e3fc-4c24-a988-89147067a179",
|
uuid = "94d36959-e3fc-4c24-a988-89147067a179",
|
||||||
name = "Casting",
|
name = "Casting",
|
||||||
description = "Learn about casting in Grayjay. How do I show video on my TV?",
|
description = "Learn about casting in Grayjay. How do I show video on my TV?\nhttps://fcast.org/",
|
||||||
thumbnailUrl = "https://releases.grayjay.app/tutorials/how-to-cast.jpg",
|
thumbnailUrl = "https://releases.grayjay.app/tutorials/how-to-cast.jpg",
|
||||||
videoUrl = "https://releases.grayjay.app/tutorials/how-to-cast.mp4",
|
videoUrl = "https://releases.grayjay.app/tutorials/how-to-cast.mp4",
|
||||||
duration = 79
|
duration = 79
|
||||||
|
|||||||
+174
-125
@@ -1,32 +1,36 @@
|
|||||||
package com.futo.platformplayer.fragment.mainactivity.main
|
package com.futo.platformplayer.fragment.mainactivity.main
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.content.pm.ActivityInfo
|
import android.content.pm.ActivityInfo
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.view.WindowInsets
|
||||||
|
import android.view.WindowInsetsController
|
||||||
|
import android.view.WindowManager
|
||||||
import androidx.constraintlayout.motion.widget.MotionLayout
|
import androidx.constraintlayout.motion.widget.MotionLayout
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.core.view.WindowInsetsControllerCompat
|
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
import com.futo.platformplayer.activities.SettingsActivity
|
||||||
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.casting.CastConnectionState
|
|
||||||
import com.futo.platformplayer.casting.StateCasting
|
import com.futo.platformplayer.casting.StateCasting
|
||||||
import com.futo.platformplayer.constructs.Event0
|
import com.futo.platformplayer.constructs.Event0
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.listeners.OrientationManager
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.PlatformVideoWithTime
|
import com.futo.platformplayer.models.PlatformVideoWithTime
|
||||||
import com.futo.platformplayer.models.UrlVideoWithTime
|
import com.futo.platformplayer.models.UrlVideoWithTime
|
||||||
import com.futo.platformplayer.states.StatePlayer
|
import com.futo.platformplayer.states.StatePlayer
|
||||||
import com.futo.platformplayer.views.containers.SingleViewTouchableMotionLayout
|
import com.futo.platformplayer.views.containers.SingleViewTouchableMotionLayout
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
@UnstableApi
|
||||||
class VideoDetailFragment : MainFragment {
|
class VideoDetailFragment : MainFragment {
|
||||||
override val isMainView : Boolean = false;
|
override val isMainView : Boolean = false;
|
||||||
override val hasBottomBar: Boolean = true;
|
override val hasBottomBar: Boolean = true;
|
||||||
@@ -39,22 +43,32 @@ class VideoDetailFragment : MainFragment {
|
|||||||
private var _view : SingleViewTouchableMotionLayout? = null;
|
private var _view : SingleViewTouchableMotionLayout? = null;
|
||||||
|
|
||||||
var isFullscreen : Boolean = false;
|
var isFullscreen : Boolean = false;
|
||||||
|
/**
|
||||||
|
* whether the view is in the process of switching from full-screen maximized to minimized
|
||||||
|
* this is used to detect that the app is skipping the non full-screen maximized state
|
||||||
|
*/
|
||||||
|
var isMinimizingFromFullScreen : Boolean = false;
|
||||||
val onFullscreenChanged = Event1<Boolean>();
|
val onFullscreenChanged = Event1<Boolean>();
|
||||||
var isTransitioning : Boolean = false
|
var isTransitioning : Boolean = false
|
||||||
private set;
|
private set;
|
||||||
var isInPictureInPicture : Boolean = false
|
var isInPictureInPicture : Boolean = false
|
||||||
private set;
|
private set;
|
||||||
|
|
||||||
var state: State = State.CLOSED;
|
private var _state: State = State.CLOSED
|
||||||
|
|
||||||
|
var state: State
|
||||||
|
get() = _state
|
||||||
|
set(value) {
|
||||||
|
_state = value
|
||||||
|
onStateChanged(value)
|
||||||
|
}
|
||||||
|
|
||||||
val currentUrl get() = _viewDetail?.currentUrl;
|
val currentUrl get() = _viewDetail?.currentUrl;
|
||||||
|
|
||||||
val onMinimize = Event0();
|
val onMinimize = Event0();
|
||||||
val onTransitioning = Event1<Boolean>();
|
val onTransitioning = Event1<Boolean>();
|
||||||
val onMaximized = Event0();
|
val onMaximized = Event0();
|
||||||
|
|
||||||
var lastOrientation : OrientationManager.Orientation = OrientationManager.Orientation.PORTRAIT
|
|
||||||
private set;
|
|
||||||
|
|
||||||
private var _isInitialMaximize = true;
|
private var _isInitialMaximize = true;
|
||||||
|
|
||||||
private val _maximizeProgress get() = _view?.progress ?: 0.0f;
|
private val _maximizeProgress get() = _view?.progress ?: 0.0f;
|
||||||
@@ -63,8 +77,7 @@ class VideoDetailFragment : MainFragment {
|
|||||||
private var _leavingPiP = false;
|
private var _leavingPiP = false;
|
||||||
|
|
||||||
//region Fragment
|
//region Fragment
|
||||||
constructor() : super() {
|
constructor() : super()
|
||||||
}
|
|
||||||
|
|
||||||
fun nextVideo() {
|
fun nextVideo() {
|
||||||
_viewDetail?.nextVideo(true, true, true);
|
_viewDetail?.nextVideo(true, true, true);
|
||||||
@@ -74,6 +87,94 @@ class VideoDetailFragment : MainFragment {
|
|||||||
_viewDetail?.prevVideo(true);
|
_viewDetail?.prevVideo(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isSmallWindow(): Boolean {
|
||||||
|
return min(
|
||||||
|
resources.configuration.screenWidthDp,
|
||||||
|
resources.configuration.screenHeightDp
|
||||||
|
) < resources.getInteger(R.integer.column_width_dp) * 2
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
|
super.onConfigurationChanged(newConfig)
|
||||||
|
|
||||||
|
val isLandscapeVideo: Boolean = _viewDetail?.isLandscapeVideo() ?: false
|
||||||
|
|
||||||
|
val isSmallWindow = isSmallWindow()
|
||||||
|
|
||||||
|
if (
|
||||||
|
isSmallWindow
|
||||||
|
&& newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||||
|
&& !isFullscreen
|
||||||
|
&& state == State.MAXIMIZED
|
||||||
|
) {
|
||||||
|
_viewDetail?.setFullscreen(true)
|
||||||
|
} else if (
|
||||||
|
isSmallWindow
|
||||||
|
&& isFullscreen
|
||||||
|
&& !Settings.instance.playback.fullscreenPortrait
|
||||||
|
&& newConfig.orientation == Configuration.ORIENTATION_PORTRAIT
|
||||||
|
&& isLandscapeVideo
|
||||||
|
) {
|
||||||
|
_viewDetail?.setFullscreen(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onStateChanged(state: State) {
|
||||||
|
if (
|
||||||
|
isSmallWindow()
|
||||||
|
&& state == State.MAXIMIZED
|
||||||
|
&& !isFullscreen
|
||||||
|
&& resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||||
|
) {
|
||||||
|
_viewDetail?.setFullscreen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateOrientation()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onVideoChanged(videoWidth : Int, videoHeight: Int) {
|
||||||
|
if (
|
||||||
|
isSmallWindow()
|
||||||
|
&& state == State.MAXIMIZED
|
||||||
|
&& !isFullscreen
|
||||||
|
&& videoHeight > videoWidth
|
||||||
|
) {
|
||||||
|
_viewDetail?.setFullscreen(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateOrientation() {
|
||||||
|
val a = activity ?: return
|
||||||
|
val isFullScreenPortraitAllowed = Settings.instance.playback.fullscreenPortrait
|
||||||
|
val isReversePortraitAllowed = Settings.instance.playback.reversePortrait
|
||||||
|
val rotationLock = StatePlayer.instance.rotationLock
|
||||||
|
|
||||||
|
val isLandscapeVideo: Boolean = _viewDetail?.isLandscapeVideo() ?: false
|
||||||
|
|
||||||
|
val isSmallWindow = isSmallWindow()
|
||||||
|
|
||||||
|
// For small windows if the device isn't landscape right now and full screen portrait isn't allowed then we should force landscape
|
||||||
|
if (isSmallWindow && isFullscreen && !isFullScreenPortraitAllowed && resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT && !rotationLock && isLandscapeVideo) {
|
||||||
|
if(Settings.instance.playback.forceAllowFullScreenRotation){
|
||||||
|
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|
||||||
|
}else{
|
||||||
|
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// For small windows if the device isn't in a portrait orientation and we're in the maximized state then we should force portrait
|
||||||
|
else if (isSmallWindow && !isMinimizingFromFullScreen && !isFullscreen && state == State.MAXIMIZED && resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||||
|
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
|
||||||
|
} else if (rotationLock) {
|
||||||
|
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED
|
||||||
|
} else {
|
||||||
|
a.requestedOrientation = if (isReversePortraitAllowed) {
|
||||||
|
ActivityInfo.SCREEN_ORIENTATION_FULL_USER
|
||||||
|
} else {
|
||||||
|
ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||||
super.onShownWithView(parameter, isBack);
|
super.onShownWithView(parameter, isBack);
|
||||||
Logger.i(TAG, "onShownWithView parameter=$parameter")
|
Logger.i(TAG, "onShownWithView parameter=$parameter")
|
||||||
@@ -99,49 +200,6 @@ class VideoDetailFragment : MainFragment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOrientationChanged(orientation: OrientationManager.Orientation) {
|
|
||||||
super.onOrientationChanged(orientation);
|
|
||||||
|
|
||||||
if(!_isActive || state != State.MAXIMIZED)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var newOrientation = orientation;
|
|
||||||
val d = StateCasting.instance.activeDevice;
|
|
||||||
if (d != null && d.connectionState == CastConnectionState.CONNECTED) {
|
|
||||||
newOrientation = OrientationManager.Orientation.PORTRAIT;
|
|
||||||
} else if(StatePlayer.instance.rotationLock) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(Settings.instance.other.bypassRotationPrevention && orientation == OrientationManager.Orientation.PORTRAIT)
|
|
||||||
changeOrientation(OrientationManager.Orientation.PORTRAIT);
|
|
||||||
|
|
||||||
if(lastOrientation == newOrientation)
|
|
||||||
return;
|
|
||||||
|
|
||||||
activity?.let {
|
|
||||||
if (isFullscreen) {
|
|
||||||
if (Settings.instance.playback.fullscreenPortrait) {
|
|
||||||
changeOrientation(newOrientation);
|
|
||||||
} else {
|
|
||||||
if(newOrientation == OrientationManager.Orientation.REVERSED_LANDSCAPE && it.requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE)
|
|
||||||
changeOrientation(OrientationManager.Orientation.REVERSED_LANDSCAPE);
|
|
||||||
else if(newOrientation == OrientationManager.Orientation.LANDSCAPE && it.requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE)
|
|
||||||
changeOrientation(OrientationManager.Orientation.LANDSCAPE);
|
|
||||||
else if(Settings.instance.playback.isAutoRotate() && (newOrientation == OrientationManager.Orientation.PORTRAIT || newOrientation == OrientationManager.Orientation.REVERSED_PORTRAIT)) {
|
|
||||||
_viewDetail?.setFullscreen(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if(Settings.instance.playback.isAutoRotate() && (lastOrientation == OrientationManager.Orientation.PORTRAIT || lastOrientation == OrientationManager.Orientation.REVERSED_PORTRAIT)) {
|
|
||||||
lastOrientation = newOrientation;
|
|
||||||
_viewDetail?.setFullscreen(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lastOrientation = newOrientation;
|
|
||||||
}
|
|
||||||
override fun onBackPressed(): Boolean {
|
override fun onBackPressed(): Boolean {
|
||||||
Logger.i(TAG, "onBackPressed")
|
Logger.i(TAG, "onBackPressed")
|
||||||
|
|
||||||
@@ -155,16 +213,13 @@ class VideoDetailFragment : MainFragment {
|
|||||||
closeVideoDetails();
|
closeVideoDetails();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
override fun onHide() {
|
|
||||||
super.onHide();
|
|
||||||
}
|
|
||||||
|
|
||||||
fun preventPictureInPicture() {
|
fun preventPictureInPicture() {
|
||||||
Logger.i(TAG, "preventPictureInPicture() preventPictureInPicture = true");
|
Logger.i(TAG, "preventPictureInPicture() preventPictureInPicture = true");
|
||||||
_viewDetail?.preventPictureInPicture = true;
|
_viewDetail?.preventPictureInPicture = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun minimizeVideoDetail(){
|
fun minimizeVideoDetail() {
|
||||||
_viewDetail?.setFullscreen(false);
|
_viewDetail?.setFullscreen(false);
|
||||||
if(_view != null)
|
if(_view != null)
|
||||||
_view!!.transitionToStart();
|
_view!!.transitionToStart();
|
||||||
@@ -198,7 +253,9 @@ class VideoDetailFragment : MainFragment {
|
|||||||
_viewDetail = _view!!.findViewById<VideoDetailView>(R.id.fragview_videodetail).also {
|
_viewDetail = _view!!.findViewById<VideoDetailView>(R.id.fragview_videodetail).also {
|
||||||
it.applyFragment(this);
|
it.applyFragment(this);
|
||||||
it.onFullscreenChanged.subscribe(::onFullscreenChanged);
|
it.onFullscreenChanged.subscribe(::onFullscreenChanged);
|
||||||
|
it.onVideoChanged.subscribe(::onVideoChanged)
|
||||||
it.onMinimize.subscribe {
|
it.onMinimize.subscribe {
|
||||||
|
isMinimizingFromFullScreen = true
|
||||||
_view!!.transitionToStart();
|
_view!!.transitionToStart();
|
||||||
};
|
};
|
||||||
it.onClose.subscribe {
|
it.onClose.subscribe {
|
||||||
@@ -235,6 +292,7 @@ class VideoDetailFragment : MainFragment {
|
|||||||
|
|
||||||
if (state != State.MINIMIZED && progress < 0.1) {
|
if (state != State.MINIMIZED && progress < 0.1) {
|
||||||
state = State.MINIMIZED;
|
state = State.MINIMIZED;
|
||||||
|
isMinimizingFromFullScreen = false
|
||||||
onMinimize.emit();
|
onMinimize.emit();
|
||||||
}
|
}
|
||||||
else if (state != State.MAXIMIZED && progress > 0.9) {
|
else if (state != State.MAXIMIZED && progress > 0.9) {
|
||||||
@@ -266,7 +324,6 @@ class VideoDetailFragment : MainFragment {
|
|||||||
override fun onTransitionTrigger(p0: MotionLayout?, p1: Int, p2: Boolean, p3: Float) { }
|
override fun onTransitionTrigger(p0: MotionLayout?, p1: Int, p2: Boolean, p3: Float) { }
|
||||||
});
|
});
|
||||||
|
|
||||||
context
|
|
||||||
_view?.let {
|
_view?.let {
|
||||||
if (it.progress >= 0.5 && it.progress < 1.0)
|
if (it.progress >= 0.5 && it.progress < 1.0)
|
||||||
maximizeVideoDetail();
|
maximizeVideoDetail();
|
||||||
@@ -275,8 +332,15 @@ class VideoDetailFragment : MainFragment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_loadUrlOnCreate?.let { _viewDetail?.setVideo(it.url, it.timeSeconds, it.playWhenReady) };
|
_loadUrlOnCreate?.let { _viewDetail?.setVideo(it.url, it.timeSeconds, it.playWhenReady) };
|
||||||
|
|
||||||
maximizeVideoDetail();
|
maximizeVideoDetail();
|
||||||
|
|
||||||
|
SettingsActivity.settingsActivityClosed.subscribe(this) {
|
||||||
|
updateOrientation()
|
||||||
|
}
|
||||||
|
|
||||||
|
StatePlayer.instance.onRotationLockChanged.subscribe(this) {
|
||||||
|
updateOrientation()
|
||||||
|
}
|
||||||
return _view!!;
|
return _view!!;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,11 +398,6 @@ class VideoDetailFragment : MainFragment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val realOrientation = if(activity is MainActivity) (activity as MainActivity).orientation else lastOrientation;
|
|
||||||
Logger.i(TAG, "Real orientation on boot ${realOrientation}, lastOrientation: ${lastOrientation}");
|
|
||||||
if(realOrientation != lastOrientation)
|
|
||||||
onOrientationChanged(realOrientation);
|
|
||||||
|
|
||||||
StateCasting.instance.onResume();
|
StateCasting.instance.onResume();
|
||||||
}
|
}
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
@@ -373,13 +432,16 @@ class VideoDetailFragment : MainFragment {
|
|||||||
if(shouldStop) {
|
if(shouldStop) {
|
||||||
_viewDetail?.onStop();
|
_viewDetail?.onStop();
|
||||||
StateCasting.instance.onStop();
|
StateCasting.instance.onStop();
|
||||||
Logger.v(TAG, "called onStop() shouldStop: $shouldStop");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyMainView() {
|
override fun onDestroyMainView() {
|
||||||
super.onDestroyMainView();
|
super.onDestroyMainView();
|
||||||
Logger.v(TAG, "onDestroyMainView");
|
Logger.v(TAG, "onDestroyMainView");
|
||||||
|
|
||||||
|
SettingsActivity.settingsActivityClosed.remove(this)
|
||||||
|
StatePlayer.instance.onRotationLockChanged.remove(this)
|
||||||
|
|
||||||
_viewDetail?.let {
|
_viewDetail?.let {
|
||||||
_viewDetail = null;
|
_viewDetail = null;
|
||||||
it.onDestroy();
|
it.onDestroy();
|
||||||
@@ -387,13 +449,6 @@ class VideoDetailFragment : MainFragment {
|
|||||||
_view = null;
|
_view = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
StateCasting.instance.onActiveDeviceConnectionStateChanged.subscribe(this) { _, _ ->
|
|
||||||
onOrientationChanged(lastOrientation);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
|
||||||
@@ -409,67 +464,61 @@ class VideoDetailFragment : MainFragment {
|
|||||||
onMaximized.clear();
|
onMaximized.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onFullscreenChanged(fullscreen : Boolean) {
|
private fun hideSystemUI() {
|
||||||
activity?.let {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
if (fullscreen) {
|
WindowCompat.setDecorFitsSystemWindows(requireActivity().window, false)
|
||||||
if (Settings.instance.playback.fullscreenPortrait) {
|
activity?.window?.insetsController?.let { controller ->
|
||||||
changeOrientation(lastOrientation);
|
controller.hide(WindowInsets.Type.statusBars())
|
||||||
} else {
|
controller.hide(WindowInsets.Type.systemBars())
|
||||||
var orient = lastOrientation;
|
controller.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||||
if(orient == OrientationManager.Orientation.PORTRAIT || orient == OrientationManager.Orientation.REVERSED_PORTRAIT)
|
|
||||||
orient = OrientationManager.Orientation.LANDSCAPE;
|
|
||||||
changeOrientation(orient);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
} else {
|
||||||
changeOrientation(OrientationManager.Orientation.PORTRAIT);
|
@Suppress("DEPRECATION")
|
||||||
|
activity?.window?.setFlags(
|
||||||
|
WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
||||||
|
WindowManager.LayoutParams.FLAG_FULLSCREEN
|
||||||
|
)
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
activity?.window?.decorView?.systemUiVisibility = (
|
||||||
|
View.SYSTEM_UI_FLAG_FULLSCREEN
|
||||||
|
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||||
|
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showSystemUI() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(requireActivity().window, true)
|
||||||
|
activity?.window?.insetsController?.let { controller ->
|
||||||
|
controller.show(WindowInsets.Type.statusBars())
|
||||||
|
controller.show(WindowInsets.Type.systemBars())
|
||||||
|
controller.systemBarsBehavior = WindowInsetsController.BEHAVIOR_DEFAULT
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
activity?.window?.decorView?.systemUiVisibility = View.SYSTEM_UI_FLAG_VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onFullscreenChanged(fullscreen : Boolean) {
|
||||||
isFullscreen = fullscreen;
|
isFullscreen = fullscreen;
|
||||||
onFullscreenChanged.emit(isFullscreen);
|
onFullscreenChanged.emit(isFullscreen);
|
||||||
_view?.allowMotion = !fullscreen;
|
|
||||||
}
|
|
||||||
private fun changeOrientation(orientation: OrientationManager.Orientation) {
|
|
||||||
Logger.i(TAG, "Orientation Change:" + orientation.name);
|
|
||||||
activity?.let {
|
|
||||||
when (orientation) {
|
|
||||||
OrientationManager.Orientation.LANDSCAPE -> {
|
|
||||||
it.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
|
|
||||||
_view?.allowMotion = false;
|
|
||||||
|
|
||||||
WindowCompat.setDecorFitsSystemWindows(requireActivity().window, false)
|
if (isFullscreen) {
|
||||||
WindowInsetsControllerCompat(it.window, _viewDetail!!).let { controller ->
|
hideSystemUI()
|
||||||
controller.hide(WindowInsetsCompat.Type.statusBars());
|
} else {
|
||||||
controller.hide(WindowInsetsCompat.Type.systemBars());
|
showSystemUI()
|
||||||
controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
OrientationManager.Orientation.REVERSED_LANDSCAPE -> {
|
|
||||||
it.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
|
|
||||||
_view?.allowMotion = false;
|
|
||||||
|
|
||||||
WindowCompat.setDecorFitsSystemWindows(requireActivity().window, false)
|
|
||||||
WindowInsetsControllerCompat(it.window, _viewDetail!!).let { controller ->
|
|
||||||
controller.hide(WindowInsetsCompat.Type.statusBars());
|
|
||||||
controller.hide(WindowInsetsCompat.Type.systemBars());
|
|
||||||
controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
it.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
|
|
||||||
_view?.allowMotion = true;
|
|
||||||
|
|
||||||
WindowCompat.setDecorFitsSystemWindows(it.window, true)
|
|
||||||
WindowInsetsControllerCompat(it.window, _viewDetail!!).let { controller ->
|
|
||||||
controller.show(WindowInsetsCompat.Type.statusBars());
|
|
||||||
controller.show(WindowInsetsCompat.Type.systemBars())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateOrientation();
|
||||||
|
_view?.allowMotion = !fullscreen;
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = "VideoDetailFragment";
|
private const val TAG = "VideoDetailFragment";
|
||||||
|
|
||||||
fun newInstance() = VideoDetailFragment().apply {}
|
fun newInstance() = VideoDetailFragment().apply {}
|
||||||
}
|
}
|
||||||
|
|||||||
+513
-175
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -96,11 +96,11 @@ class WatchLaterFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onVideoOrderChanged(videos: List<IPlatformVideo>) {
|
override fun onVideoOrderChanged(videos: List<IPlatformVideo>) {
|
||||||
StatePlaylists.instance.updateWatchLater(ArrayList(videos.map { it as SerializedPlatformVideo }));
|
StatePlaylists.instance.updateWatchLater(ArrayList(videos.map { it as SerializedPlatformVideo }), true);
|
||||||
}
|
}
|
||||||
override fun onVideoRemoved(video: IPlatformVideo) {
|
override fun onVideoRemoved(video: IPlatformVideo) {
|
||||||
if (video is SerializedPlatformVideo) {
|
if (video is SerializedPlatformVideo) {
|
||||||
StatePlaylists.instance.removeFromWatchLater(video);
|
StatePlaylists.instance.removeFromWatchLater(video, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+16
-12
@@ -26,6 +26,7 @@ import com.futo.platformplayer.stores.FragmentedStorage
|
|||||||
import com.futo.platformplayer.stores.SearchHistoryStorage
|
import com.futo.platformplayer.stores.SearchHistoryStorage
|
||||||
|
|
||||||
class SearchTopBarFragment : TopFragment() {
|
class SearchTopBarFragment : TopFragment() {
|
||||||
|
@Suppress("PrivatePropertyName")
|
||||||
private val TAG = "SearchTopBarFragment"
|
private val TAG = "SearchTopBarFragment"
|
||||||
|
|
||||||
private var _editSearch: EditText? = null;
|
private var _editSearch: EditText? = null;
|
||||||
@@ -191,29 +192,32 @@ class SearchTopBarFragment : TopFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun onDone() {
|
private fun onDone() {
|
||||||
val editSearch = _editSearch;
|
val editSearch = _editSearch
|
||||||
if (editSearch != null) {
|
if (editSearch != null) {
|
||||||
val text = editSearch.text.toString();
|
val text = editSearch.text.toString()
|
||||||
if (text.length < 3) {
|
if (text.isEmpty()) {
|
||||||
UIDialogs.toast(getString(R.string.please_use_at_least_3_characters));
|
UIDialogs.toast(getString(R.string.please_use_at_least_1_character))
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
editSearch.clearFocus();
|
editSearch.clearFocus()
|
||||||
_inputMethodManager?.hideSoftInputFromWindow(editSearch.windowToken, 0);
|
_inputMethodManager?.hideSoftInputFromWindow(editSearch.windowToken, 0)
|
||||||
|
|
||||||
if (Settings.instance.search.searchHistory) {
|
if (Settings.instance.search.searchHistory) {
|
||||||
val storage = FragmentedStorage.get<SearchHistoryStorage>();
|
val storage = FragmentedStorage.get<SearchHistoryStorage>()
|
||||||
storage.add(text);
|
storage.add(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_searchType == SearchType.CREATOR) {
|
if (_searchType == SearchType.CREATOR) {
|
||||||
onSearch.emit(text);
|
onSearch.emit(text)
|
||||||
} else {
|
} else {
|
||||||
onSearch.emit(text);
|
onSearch.emit(text)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Logger.w(TAG, "Unexpected condition happened where done is edit search is null but done is triggered.");
|
Logger.w(
|
||||||
|
TAG,
|
||||||
|
"Unexpected condition happened where done is edit search is null but done is triggered."
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
|||||||
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.video.IPlatformVideoDetails
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSAudioUrlRangeSource
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSAudioUrlRangeSource
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawAudioSource
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawSource
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSVideoUrlRangeSource
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSVideoUrlRangeSource
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.others.Language
|
import com.futo.platformplayer.others.Language
|
||||||
@@ -44,8 +47,8 @@ class VideoHelper {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isDownloadable(source: IVideoSource) = source is IVideoUrlSource || source is IHLSManifestSource;
|
fun isDownloadable(source: IVideoSource) = source is IVideoUrlSource || source is IHLSManifestSource || source is JSDashManifestRawSource;
|
||||||
fun isDownloadable(source: IAudioSource) = (source is IAudioUrlSource || source is IHLSManifestAudioSource) && source !is IAudioUrlWidevineSource
|
fun isDownloadable(source: IAudioSource) = (source is IAudioUrlSource || source is IHLSManifestAudioSource || source is JSDashManifestRawAudioSource) && source !is IAudioUrlWidevineSource
|
||||||
|
|
||||||
fun selectBestVideoSource(desc: IVideoSourceDescriptor, desiredPixelCount : Int, prefContainers : Array<String>) : IVideoSource? = selectBestVideoSource(desc.videoSources.toList(), desiredPixelCount, prefContainers);
|
fun selectBestVideoSource(desc: IVideoSourceDescriptor, desiredPixelCount : Int, prefContainers : Array<String>) : IVideoSource? = selectBestVideoSource(desc.videoSources.toList(), desiredPixelCount, prefContainers);
|
||||||
fun selectBestVideoSource(sources: Iterable<IVideoSource>, desiredPixelCount : Int, prefContainers : Array<String>) : IVideoSource? {
|
fun selectBestVideoSource(sources: Iterable<IVideoSource>, desiredPixelCount : Int, prefContainers : Array<String>) : IVideoSource? {
|
||||||
@@ -186,5 +189,25 @@ class VideoHelper {
|
|||||||
return@Resolver dataSpec;
|
return@Resolver dataSpec;
|
||||||
})).createMediaSource(manifest, MediaItem.Builder().setUri(Uri.parse(audioSource.getAudioUrl())).build())
|
})).createMediaSource(manifest, MediaItem.Builder().setUri(Uri.parse(audioSource.getAudioUrl())).build())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun estimateSourceSize(source: IVideoSource?): Long {
|
||||||
|
if(source == null) return 0;
|
||||||
|
if(source is IVideoSource) {
|
||||||
|
if(source.bitrate ?: 0 <= 0 || source.duration.toInt() == 0)
|
||||||
|
return 0;
|
||||||
|
return (source.duration / 8) * source.bitrate!!;
|
||||||
|
}
|
||||||
|
else return 0;
|
||||||
|
}
|
||||||
|
fun estimateSourceSize(source: IAudioSource?): Long {
|
||||||
|
if(source == null) return 0;
|
||||||
|
if(source is IAudioSource) {
|
||||||
|
if(source.bitrate <= 0 || source.duration?.toInt() ?: 0 == 0)
|
||||||
|
return 0;
|
||||||
|
return (source.duration!! / 8) * source.bitrate;
|
||||||
|
}
|
||||||
|
else return 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
package com.futo.platformplayer.listeners
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.view.OrientationEventListener
|
|
||||||
import com.futo.platformplayer.Settings
|
|
||||||
import com.futo.platformplayer.constructs.Event1
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
|
||||||
|
|
||||||
class OrientationManager : OrientationEventListener {
|
|
||||||
|
|
||||||
val onOrientationChanged = Event1<Orientation>();
|
|
||||||
|
|
||||||
var orientation : Orientation = Orientation.PORTRAIT;
|
|
||||||
|
|
||||||
constructor(context: Context) : super(context) { }
|
|
||||||
|
|
||||||
//TODO: Something weird is going on here
|
|
||||||
//TODO: Old implementation felt pretty good for me, but now with 0 deadzone still feels bad, even though code should be identical?
|
|
||||||
override fun onOrientationChanged(orientationAnglep: Int) {
|
|
||||||
if (orientationAnglep == -1) return
|
|
||||||
|
|
||||||
val deadZone = Settings.instance.playback.getAutoRotateDeadZoneDegrees()
|
|
||||||
val isInDeadZone = when (orientation) {
|
|
||||||
Orientation.PORTRAIT -> orientationAnglep in 0 until (60 - deadZone) || orientationAnglep in (300 + deadZone) .. 360
|
|
||||||
Orientation.REVERSED_LANDSCAPE -> orientationAnglep in (60 + deadZone) until (140 - deadZone)
|
|
||||||
Orientation.REVERSED_PORTRAIT -> orientationAnglep in (140 + deadZone) until (220 - deadZone)
|
|
||||||
Orientation.LANDSCAPE -> orientationAnglep in (220 + deadZone) until (300 - deadZone)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isInDeadZone) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
val newOrientation = when (orientationAnglep) {
|
|
||||||
in 60 until 140 -> Orientation.REVERSED_LANDSCAPE
|
|
||||||
in 140 until 220 -> Orientation.REVERSED_PORTRAIT
|
|
||||||
in 220 until 300 -> Orientation.LANDSCAPE
|
|
||||||
else -> Orientation.PORTRAIT
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.i("OrientationManager", "Orientation=$newOrientation orientationAnglep=$orientationAnglep");
|
|
||||||
|
|
||||||
if (newOrientation != orientation) {
|
|
||||||
orientation = newOrientation
|
|
||||||
onOrientationChanged.emit(newOrientation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//TODO: Perhaps just use ActivityInfo orientations instead..
|
|
||||||
enum class Orientation {
|
|
||||||
PORTRAIT,
|
|
||||||
LANDSCAPE,
|
|
||||||
REVERSED_PORTRAIT,
|
|
||||||
REVERSED_LANDSCAPE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.futo.platformplayer.mdns
|
||||||
|
|
||||||
|
data class BroadcastService(
|
||||||
|
val deviceName: String,
|
||||||
|
val serviceName: String,
|
||||||
|
val port: UShort,
|
||||||
|
val ttl: UInt,
|
||||||
|
val weight: UShort,
|
||||||
|
val priority: UShort,
|
||||||
|
val texts: List<String>? = null
|
||||||
|
)
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user