mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-29 11:03:01 +02:00
Compare commits
286 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0630ec1d46 | |||
| 4dce8d6a80 | |||
| 3b62f999bf | |||
| 65ae8610fd | |||
| c1c2000c98 | |||
| 287c2d82a1 | |||
| 5cde1650f4 | |||
| a4b90f14ab | |||
| 4826b40136 | |||
| 62618224da | |||
| 49f15e1637 | |||
| e36047c890 | |||
| 8f1199bd08 | |||
| d6e045ea4e | |||
| 304e48996b | |||
| f350dc83b8 | |||
| ebb7beda8c | |||
| a01f3da66e | |||
| 72f5b5fbc0 | |||
| 330aa495c8 | |||
| 0b529ae94d | |||
| 83b35183d0 | |||
| 2cd01eb1fe | |||
| 07378f665a | |||
| bfd5f24f4c | |||
| 3d617187af | |||
| d040b93ca9 | |||
| a410e2962a | |||
| f5aa8f37bb | |||
| 7e932df450 | |||
| 3d4741727e | |||
| a03b63ef74 | |||
| 15ce3e9f20 | |||
| da58b72f9d | |||
| 1639bd7af1 | |||
| d474121f85 | |||
| 978f76ffb6 | |||
| 084bac00f5 | |||
| 94454172dd | |||
| 891d3cf966 | |||
| 561d5ec7ab | |||
| 7ce437d50a | |||
| 4b02d4ce90 | |||
| 3107185869 | |||
| 2e3584a353 | |||
| e5b1be195c | |||
| dde30c9d76 | |||
| 3830e65de8 | |||
| c589cf167e | |||
| 2fde367c82 | |||
| 8fd188268e | |||
| b65257df42 | |||
| aaa2d7f08d | |||
| f73e25ece6 | |||
| 78d427f208 | |||
| eaeaf3538f | |||
| 85e381a85e | |||
| 1b7ee8231b | |||
| 1b8b8f5738 | |||
| 53df19b477 | |||
| ccf21b7580 | |||
| 4189d62a57 | |||
| 9a3e3af614 | |||
| f7187400dc | |||
| f55a7f0a7b | |||
| d6d35a645e | |||
| e719dcc7f5 | |||
| bc5bc5450c | |||
| f4bade0c2e | |||
| 9be59c674d | |||
| a1dec23c20 | |||
| ed926c4e37 | |||
| ab360ed6f6 | |||
| 569ba3d651 | |||
| 60fe28c2fe | |||
| 2787e29a07 | |||
| c77a4d08d6 | |||
| 9b3f90f922 | |||
| c88d457021 | |||
| b20b625820 | |||
| fd95311920 | |||
| 6da5c11731 | |||
| 4e58231308 | |||
| ef0ecf249a | |||
| 4981617f7a | |||
| 2070bc7007 | |||
| 231d2461b3 | |||
| 3b457f87c4 | |||
| de3ced4d3c | |||
| 891777e89e | |||
| 287239dd1c | |||
| 7cdded8fd7 | |||
| 8c9d045e1d | |||
| 620f5a0459 | |||
| 178d874ba0 | |||
| d44f30c8a6 | |||
| ce66937429 | |||
| 9823337375 | |||
| 11f5f0dfe1 | |||
| e1882f19e8 | |||
| 6a8b9f06c2 | |||
| 752fc8787d | |||
| 90a1cd8280 | |||
| aa570ac29d | |||
| fb7b6363f9 | |||
| 23afe7994c | |||
| 7557e6f6ba | |||
| 86b6938911 | |||
| 8f30a45fa8 | |||
| 7c9e9d5f52 | |||
| 4066ce73a8 | |||
| b5722dba1a | |||
| 81765ecafc | |||
| 84b42e9d19 | |||
| ed319a0e5f | |||
| dd55d10194 | |||
| 2084b46090 | |||
| 53443a6cf2 | |||
| 92715b5642 | |||
| 6166392515 | |||
| 49d0dead7d | |||
| 6f004830ff | |||
| e2e5e36bad | |||
| f267d264d3 | |||
| be1a77bfd7 | |||
| 41a980e826 | |||
| 09c09f3d64 | |||
| 2404399ec5 | |||
| b45d4c0557 | |||
| a41b138d3c | |||
| 1e46949dd6 | |||
| 3ed2c1ba5d | |||
| 809b99c9c9 | |||
| 4d3acdb5fb | |||
| ca9e321ef2 | |||
| da27517fcf | |||
| 192df0a3b8 | |||
| a965003a9d | |||
| 9ea26c821f | |||
| 14b699485a | |||
| 1684edc43f | |||
| 580c4418b9 | |||
| 4a65fc2358 | |||
| 71ba131fb3 | |||
| 9693b50719 | |||
| 102e2c54bb | |||
| e989590c08 | |||
| 6cee33b449 | |||
| f32498a444 | |||
| c85f71b601 | |||
| 196e55899e | |||
| ebec45076d | |||
| 561d9ae987 | |||
| 8950bd94cb | |||
| f416f197bc | |||
| 65afe5a0e6 | |||
| 4b5d347413 | |||
| 4dcc2dd0ca | |||
| 2a7a332160 | |||
| 27ee1eabda | |||
| 0034665965 | |||
| a69692be18 | |||
| dc76152166 | |||
| d7f3ae696c | |||
| 71f5449d34 | |||
| 0e64fa8d4c | |||
| 73b048d4c5 | |||
| 1c05b39861 | |||
| 7cfa6c163f | |||
| 2d4af2e867 | |||
| 1eeaffc442 | |||
| 82125b33ed | |||
| 42cbbc28fd | |||
| a7cbb0e93c | |||
| fde6148ece | |||
| df1661d75a | |||
| f938f79a35 | |||
| 333f00235b | |||
| c06475bfb3 | |||
| d1a54d0cf3 | |||
| 349437c06b | |||
| 1b03c83c84 | |||
| bb749aacf1 | |||
| 3a41b89e52 | |||
| 70cbc77381 | |||
| 3a99f5dfaa | |||
| f24435ecf4 | |||
| 4a708e316a | |||
| c2b47c998d | |||
| 534f7b3134 | |||
| d5d2692317 | |||
| dc9cc7b00f | |||
| 965e74c7e2 | |||
| 096ba54eb1 | |||
| f4e38f9e50 | |||
| c0d9409176 | |||
| 7d1f565749 | |||
| dfec4ada3b | |||
| cd695cf265 | |||
| 47ff2e0c38 | |||
| db7c09291f | |||
| 01f10c49ba | |||
| 1ff0692a72 | |||
| 116e6099d5 | |||
| 18ccaadc5b | |||
| 8f6eac7ca2 | |||
| f4610d0df5 | |||
| bf1a6b7d0a | |||
| b3fd05e62e | |||
| f7ce365618 | |||
| 77a558dbe5 | |||
| cc0c400b28 | |||
| 2bcd59cbfa | |||
| 5139acc7f1 | |||
| 1564433e02 | |||
| 1339beb7cd | |||
| cd9698ea48 | |||
| c8f8e4c5eb | |||
| 0b4ab46563 | |||
| ea1ac86134 | |||
| 790331e798 | |||
| f5d9b2ba41 | |||
| 7f26ac00b1 | |||
| fcbab10434 | |||
| c4061cc6ac | |||
| 12ac4d6b6f | |||
| 3d06e62cd4 | |||
| d7d23e1048 | |||
| 1fe9b70176 | |||
| a9cf8dd71a | |||
| 3299261db3 | |||
| e465ec8278 | |||
| d0e4a0aa1f | |||
| 74efec3235 | |||
| 13516087f2 | |||
| 0a0c16524a | |||
| 9b843a155e | |||
| cb085acbff | |||
| c3d7df166b | |||
| d312062125 | |||
| e2453192aa | |||
| 68eb0cc8f2 | |||
| cb9cecfa5d | |||
| 0f4e4a7d97 | |||
| f20a708b36 | |||
| 8c4e511883 | |||
| a4a3b8d664 | |||
| bf6530ea81 | |||
| 4a80c2aab1 | |||
| 527bbfe43f | |||
| d8e1edb60b | |||
| 245b5f74c0 | |||
| e9a1f63415 | |||
| ec370dd94b | |||
| e39d862ef3 | |||
| 7b065654aa | |||
| 918b2bbe96 | |||
| e529a3d34d | |||
| 5475778d67 | |||
| c6a3ff0a53 | |||
| cf3587f504 | |||
| d42f104884 | |||
| 6a43568369 | |||
| 85c9cd0a6e | |||
| be5920cfae | |||
| 3d25d94a77 | |||
| fe97850835 | |||
| dab9decd89 | |||
| 854651aa71 | |||
| fdd1af3287 | |||
| 0bf92b6aff | |||
| d9403bf4da | |||
| 716d8caf4d | |||
| 0f0f368a75 | |||
| ff8d7558d4 | |||
| 66f9824b68 | |||
| 44a6e5da38 | |||
| de5a4aa5f3 | |||
| e8007082a7 | |||
| 3c70c5a366 | |||
| eb6e79b055 | |||
| ea59f8dccb | |||
| aef1c584e5 | |||
| c4ce671a87 | |||
| e8a79c87ab | |||
| da6eef905c |
@@ -1,19 +1,19 @@
|
|||||||
name: Bug Report
|
name: Bug Report
|
||||||
description: Let us know about an unexpected error, a crash, or an incorrect behavior.
|
description: Let us know about an unexpected error, a crash, or an incorrect behavior.
|
||||||
labels: ["bug", "new"]
|
labels: ["Bug"]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
# Thank you for taking the time to fill out this bug report.
|
# Thank you for taking the time to fill out this bug report.
|
||||||
|
|
||||||
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay Android Application
|
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay Android Application
|
||||||
|
|
||||||
For general usage questions, please see: [The Official FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)
|
For general usage questions, please see: [The Official FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)
|
||||||
|
|
||||||
## Filing a bug report
|
## Filing a bug report
|
||||||
|
|
||||||
To fix your issues faster, we need clear reproduction cases - ideally allowing us to make it happen locally.
|
To fix your issues faster, we need clear reproduction cases - ideally allowing us to make it happen locally.
|
||||||
* Please include all needed context. For example, Device, OS, Application, your Grayjay Configurations and Plugin versioning info.
|
* Please include all needed context. For example, Device, OS, Application, your Grayjay Configurations and Plugin versioning info.
|
||||||
* if you've found out a particular series of UI interactions can introduce buggy behavior, please label those steps 1-n with markdown
|
* if you've found out a particular series of UI interactions can introduce buggy behavior, please label those steps 1-n with markdown
|
||||||
|
|
||||||
@@ -41,18 +41,21 @@ body:
|
|||||||
label: What plugins are you seeing the problem on?
|
label: What plugins are you seeing the problem on?
|
||||||
multiple: true
|
multiple: true
|
||||||
options:
|
options:
|
||||||
- All
|
- "All"
|
||||||
- Youtube
|
- "Youtube"
|
||||||
- BiliBili (CN)
|
- "Odysee"
|
||||||
- Twitch
|
- "Rumble"
|
||||||
- Odysee
|
- "Kick"
|
||||||
- Rumble
|
- "Twitch"
|
||||||
- Kick
|
- "PeerTube"
|
||||||
- PeerTube
|
- "Patreon"
|
||||||
- Patreon
|
- "Nebula"
|
||||||
- Nebula
|
- "BiliBili (CN)"
|
||||||
- SoundCloud
|
- "Bitchute"
|
||||||
- Other
|
- "SoundCloud"
|
||||||
|
- "Dailymotion"
|
||||||
|
- "Apple Podcasts"
|
||||||
|
- "Other"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
@@ -72,6 +75,17 @@ body:
|
|||||||
- label: While logged out
|
- label: While logged out
|
||||||
- label: N/A
|
- label: N/A
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: vpn
|
||||||
|
attributes:
|
||||||
|
label: Are you using a VPN?
|
||||||
|
multiple: false
|
||||||
|
options:
|
||||||
|
- "No"
|
||||||
|
- "Yes"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: logs
|
id: logs
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
name: Documentation Issue
|
name: Documentation Issue
|
||||||
description: Report an issue or suggest a change in the documentation.
|
description: Report an issue or suggest a change in the documentation.
|
||||||
labels: ["documentation", "new"]
|
labels: ["Documentation"]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
# Thank you for opening a documentation change request.
|
# Thank you for opening a documentation change request.
|
||||||
|
|
||||||
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay Android Application. Use the `Documentation` issue type to report problems with the documentation in our code repositories, inside the application, or on [https://grayjay.app](https://grayjay.app)
|
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay Android Application. Use the `Documentation` issue type to report problems with the documentation in our code repositories, inside the application, or on [https://grayjay.app](https://grayjay.app)
|
||||||
Technical writers monitor this issue type, so report Grayjay bugs or feature requests with the `Bug report` or `Feature Request` issue types instead to get engineering attention.
|
Technical writers monitor this issue type, so report Grayjay bugs or feature requests with the `Bug report` or `Feature Request` issue types instead to get engineering attention.
|
||||||
|
|
||||||
For general usage questions, please see: [The Official FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)
|
For general usage questions, please see: [The Official FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
name: Feature Request
|
name: Feature Request
|
||||||
description: Suggest a new feature or other enhancement.
|
description: Suggest a new feature or other enhancement.
|
||||||
labels: ["enhancement", "new"]
|
labels: ["Enhancement"]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
@@ -9,8 +9,6 @@ body:
|
|||||||
|
|
||||||
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay Android Application
|
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay Android Application
|
||||||
|
|
||||||
[External Contributions are closed at this time](https://github.com/tom-futo/grayjay-android/blob/master/CONTRIBUTION.md#contributing-to-core)
|
|
||||||
|
|
||||||
For discussion related to enhancements, please see: [The FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)
|
For discussion related to enhancements, please see: [The FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
@@ -55,4 +53,4 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
**Note:** If the submit button is disabled and you have filled out all required fields, please check that you did not forget a **Title** for the issue.
|
**Note:** If the submit button is disabled and you have filled out all required fields, please check that you did not forget a **Title** for the issue.
|
||||||
|
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
name: Issue labeler
|
|
||||||
on:
|
|
||||||
issues:
|
|
||||||
types: [ opened ]
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
label-component:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
# required for all workflows
|
|
||||||
issues: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Parse issue form
|
|
||||||
uses: stefanbuck/github-issue-parser@v3
|
|
||||||
id: issue-parser
|
|
||||||
with:
|
|
||||||
template-path: .github/ISSUE_TEMPLATE/bug_report.yml
|
|
||||||
|
|
||||||
- name: Set labels based on plugin field
|
|
||||||
uses: redhat-plumbers-in-action/advanced-issue-labeler@v2
|
|
||||||
with:
|
|
||||||
issue-form: ${{ steps.issue-parser.outputs.jsonString }}
|
|
||||||
section: plugin
|
|
||||||
block-list: |
|
|
||||||
None
|
|
||||||
Other
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
@@ -82,3 +82,9 @@
|
|||||||
[submodule "app/src/stable/assets/sources/dailymotion"]
|
[submodule "app/src/stable/assets/sources/dailymotion"]
|
||||||
path = app/src/stable/assets/sources/dailymotion
|
path = app/src/stable/assets/sources/dailymotion
|
||||||
url = ../plugins/dailymotion.git
|
url = ../plugins/dailymotion.git
|
||||||
|
[submodule "app/src/stable/assets/sources/apple-podcast"]
|
||||||
|
path = app/src/stable/assets/sources/apple-podcast
|
||||||
|
url = ../plugins/apple-podcasts.git
|
||||||
|
[submodule "app/src/unstable/assets/sources/apple-podcasts"]
|
||||||
|
path = app/src/unstable/assets/sources/apple-podcasts
|
||||||
|
url = ../plugins/apple-podcasts.git
|
||||||
|
|||||||
+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).
|
||||||
|
|||||||
@@ -36,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" />
|
||||||
@@ -51,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="sensorPortrait"
|
|
||||||
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">
|
||||||
|
|
||||||
@@ -146,11 +151,9 @@
|
|||||||
<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="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
@@ -173,7 +176,6 @@
|
|||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.AddSourceActivity"
|
android:name=".activities.AddSourceActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar">
|
android:theme="@style/Theme.FutoVideo.NoActionBar">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
@@ -217,7 +219,6 @@
|
|||||||
android:name=".activities.ManageTabsActivity"
|
android:name=".activities.ManageTabsActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
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="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
@@ -226,5 +227,17 @@
|
|||||||
android:name=".activities.FCastGuideActivity"
|
android:name=".activities.FCastGuideActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
|
<activity
|
||||||
|
android:name=".activities.SyncHomeActivity"
|
||||||
|
android:screenOrientation="sensorPortrait"
|
||||||
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
|
<activity
|
||||||
|
android:name=".activities.SyncPairActivity"
|
||||||
|
android:screenOrientation="sensorPortrait"
|
||||||
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
|
<activity
|
||||||
|
android:name=".activities.SyncShowPairingCodeActivity"
|
||||||
|
android:screenOrientation="sensorPortrait"
|
||||||
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
File diff suppressed because one or more lines are too long
@@ -11,7 +11,8 @@ let Type = {
|
|||||||
Streams: "STREAMS",
|
Streams: "STREAMS",
|
||||||
Mixed: "MIXED",
|
Mixed: "MIXED",
|
||||||
Live: "LIVE",
|
Live: "LIVE",
|
||||||
Subscriptions: "SUBSCRIPTIONS"
|
Subscriptions: "SUBSCRIPTIONS",
|
||||||
|
Shorts: "SHORTS"
|
||||||
},
|
},
|
||||||
Order: {
|
Order: {
|
||||||
Chronological: "CHRONOLOGICAL"
|
Chronological: "CHRONOLOGICAL"
|
||||||
@@ -201,7 +202,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
|
||||||
@@ -244,6 +245,7 @@ class PlatformVideo extends PlatformContent {
|
|||||||
this.viewCount = obj.viewCount ?? -1; //Long
|
this.viewCount = obj.viewCount ?? -1; //Long
|
||||||
|
|
||||||
this.isLive = obj.isLive ?? false; //Boolean
|
this.isLive = obj.isLive ?? false; //Boolean
|
||||||
|
this.isShort = !!obj.isShort ?? false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class PlatformVideoDetails extends PlatformVideo {
|
class PlatformVideoDetails extends PlatformVideo {
|
||||||
@@ -260,6 +262,7 @@ class PlatformVideoDetails extends PlatformVideo {
|
|||||||
|
|
||||||
this.rating = obj.rating ?? null; //IRating
|
this.rating = obj.rating ?? null; //IRating
|
||||||
this.subtitles = obj.subtitles ?? [];
|
this.subtitles = obj.subtitles ?? [];
|
||||||
|
this.isShort = !!obj.isShort ?? false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,12 +281,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 +370,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 +412,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 +474,16 @@ 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 {
|
class DashManifestRawSource {
|
||||||
constructor(obj) {
|
constructor(obj) {
|
||||||
obj = obj ?? {};
|
obj = obj ?? {};
|
||||||
|
|||||||
@@ -1,114 +0,0 @@
|
|||||||
import android.app.Activity
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.pm.ActivityInfo
|
|
||||||
import android.hardware.Sensor
|
|
||||||
import android.hardware.SensorEvent
|
|
||||||
import android.hardware.SensorEventListener
|
|
||||||
import android.hardware.SensorManager
|
|
||||||
import com.futo.platformplayer.constructs.Event1
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
|
|
||||||
class AdvancedOrientationListener(private val activity: Activity, private val lifecycleScope: CoroutineScope) {
|
|
||||||
private val sensorManager: SensorManager = activity.getSystemService(Context.SENSOR_SERVICE) as SensorManager
|
|
||||||
private val accelerometer: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
|
|
||||||
private val magnetometer: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)
|
|
||||||
|
|
||||||
private var lastOrientation: Int = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
|
||||||
private var lastStableOrientation: Int = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
|
||||||
private var lastOrientationChangeTime = 0L
|
|
||||||
private val debounceTime = 200L
|
|
||||||
private val stabilityThresholdTime = 800L
|
|
||||||
private var deviceAspectRatio: Float = 1.0f
|
|
||||||
|
|
||||||
private val gravity = FloatArray(3)
|
|
||||||
private val geomagnetic = FloatArray(3)
|
|
||||||
private val rotationMatrix = FloatArray(9)
|
|
||||||
private val orientationAngles = FloatArray(3)
|
|
||||||
|
|
||||||
val onOrientationChanged = Event1<Int>()
|
|
||||||
|
|
||||||
private val sensorListener = object : SensorEventListener {
|
|
||||||
override fun onSensorChanged(event: SensorEvent) {
|
|
||||||
when (event.sensor.type) {
|
|
||||||
Sensor.TYPE_ACCELEROMETER -> {
|
|
||||||
System.arraycopy(event.values, 0, gravity, 0, gravity.size)
|
|
||||||
}
|
|
||||||
Sensor.TYPE_MAGNETIC_FIELD -> {
|
|
||||||
System.arraycopy(event.values, 0, geomagnetic, 0, geomagnetic.size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (gravity.isNotEmpty() && geomagnetic.isNotEmpty()) {
|
|
||||||
val success = SensorManager.getRotationMatrix(rotationMatrix, null, gravity, geomagnetic)
|
|
||||||
if (success) {
|
|
||||||
SensorManager.getOrientation(rotationMatrix, orientationAngles)
|
|
||||||
|
|
||||||
val azimuth = Math.toDegrees(orientationAngles[0].toDouble()).toFloat()
|
|
||||||
val pitch = Math.toDegrees(orientationAngles[1].toDouble()).toFloat()
|
|
||||||
val roll = Math.toDegrees(orientationAngles[2].toDouble()).toFloat()
|
|
||||||
|
|
||||||
val newOrientation = when {
|
|
||||||
roll in -155f .. -15f && isWithinThreshold(pitch, 0f, 30.0) -> {
|
|
||||||
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
|
||||||
}
|
|
||||||
roll in 15f .. 155f && isWithinThreshold(pitch, 0f, 30.0) -> {
|
|
||||||
ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
|
|
||||||
}
|
|
||||||
isWithinThreshold(pitch, -90f, 30.0 * deviceAspectRatio) && roll in -15f .. 15f -> {
|
|
||||||
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
|
||||||
}
|
|
||||||
isWithinThreshold(pitch, 90f, 30.0 * deviceAspectRatio) && roll in -15f .. 15f -> {
|
|
||||||
ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT
|
|
||||||
}
|
|
||||||
else -> lastOrientation
|
|
||||||
}
|
|
||||||
|
|
||||||
//Logger.i("AdvancedOrientationListener", "newOrientation = ${newOrientation}, roll = ${roll}, pitch = ${pitch}, azimuth = ${azimuth}")
|
|
||||||
|
|
||||||
if (newOrientation != lastStableOrientation) {
|
|
||||||
val currentTime = System.currentTimeMillis()
|
|
||||||
if (currentTime - lastOrientationChangeTime > debounceTime) {
|
|
||||||
lastOrientationChangeTime = currentTime
|
|
||||||
lastStableOrientation = newOrientation
|
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
|
||||||
delay(stabilityThresholdTime)
|
|
||||||
if (newOrientation == lastStableOrientation) {
|
|
||||||
lastOrientation = newOrientation
|
|
||||||
onOrientationChanged.emit(newOrientation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isWithinThreshold(value: Float, target: Float, threshold: Double): Boolean {
|
|
||||||
return Math.abs(value - target) <= threshold
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
sensorManager.registerListener(sensorListener, accelerometer, SensorManager.SENSOR_DELAY_GAME)
|
|
||||||
sensorManager.registerListener(sensorListener, magnetometer, SensorManager.SENSOR_DELAY_GAME)
|
|
||||||
|
|
||||||
val metrics = activity.resources.displayMetrics
|
|
||||||
deviceAspectRatio = (metrics.heightPixels.toFloat() / metrics.widthPixels.toFloat())
|
|
||||||
if (deviceAspectRatio == 0.0f)
|
|
||||||
deviceAspectRatio = 1.0f
|
|
||||||
|
|
||||||
lastOrientation = activity.resources.configuration.orientation
|
|
||||||
}
|
|
||||||
|
|
||||||
fun stopListening() {
|
|
||||||
sensorManager.unregisterListener(sensorListener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -226,6 +226,25 @@ fun Long.toHumanTime(isMs: Boolean): String {
|
|||||||
else
|
else
|
||||||
return "${prefix}${minsStr}:${secsStr}"
|
return "${prefix}${minsStr}:${secsStr}"
|
||||||
}
|
}
|
||||||
|
fun Long.toHumanDuration(isMs: Boolean): String {
|
||||||
|
var scaler = 1;
|
||||||
|
if(isMs)
|
||||||
|
scaler = 1000;
|
||||||
|
val v = Math.abs(this);
|
||||||
|
val hours = Math.max(v/(secondsInHour*scaler), 0);
|
||||||
|
val mins = Math.max((v % (secondsInHour*scaler)) / (secondsInMinute * scaler), 0);
|
||||||
|
val minsStr = mins.toString();
|
||||||
|
val seconds = Math.max(((v % (secondsInHour*scaler)) % (secondsInMinute * scaler))/scaler, 0);
|
||||||
|
val secsStr = seconds.toString().padStart(2, '0');
|
||||||
|
val prefix = if (this < 0) { "-" } else { "" };
|
||||||
|
|
||||||
|
return listOf(
|
||||||
|
if(hours > 0) "${hours}h" else null,
|
||||||
|
if(mins > 0) "${mins}m" else null ,
|
||||||
|
if(seconds > 0) "${seconds}s" else null
|
||||||
|
).filterNotNull().joinToString(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//TODO: Determine if below stuff should have its own proper class, seems a bit too complex for a utility method
|
//TODO: Determine if below stuff should have its own proper class, seems a bit too complex for a utility method
|
||||||
fun String.fixHtmlWhitespace(): Spanned {
|
fun String.fixHtmlWhitespace(): Spanned {
|
||||||
@@ -360,11 +379,11 @@ fun String.matchesDomain(queryDomain: String): Boolean {
|
|||||||
|
|
||||||
val parts = queryDomain.lowercase().split(".");
|
val parts = queryDomain.lowercase().split(".");
|
||||||
if(parts.size < 3)
|
if(parts.size < 3)
|
||||||
throw IllegalStateException("Illegal use of wildcards on First-Level-Domain");
|
throw IllegalStateException("Illegal use of wildcards on First-Level-Domain (" + queryDomain + ")");
|
||||||
if(parts.size >= 3){
|
if(parts.size >= 3){
|
||||||
val isSLD = slds.contains("." + parts[parts.size - 2] + "." + parts[parts.size - 1]);
|
val isSLD = slds.contains("." + parts[parts.size - 2] + "." + parts[parts.size - 1]);
|
||||||
if(isSLD && parts.size <= 3)
|
if(isSLD && parts.size <= 3)
|
||||||
throw IllegalStateException("Illegal use of wildcards on Second-Level-Domain");
|
throw IllegalStateException("Illegal use of wildcards on Second-Level-Domain (" + queryDomain + ")");
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: Should be safe, but double verify if can't be exploited
|
//TODO: Should be safe, but double verify if can't be exploited
|
||||||
@@ -372,4 +391,13 @@ fun String.matchesDomain(queryDomain: String): Boolean {
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
return this == queryDomain;
|
return this == queryDomain;
|
||||||
|
}
|
||||||
|
|
||||||
|
fun String.getSubdomainWildcardQuery(): String {
|
||||||
|
val domainParts = this.split(".");
|
||||||
|
val sldParts = "." + domainParts[domainParts.size - 2].lowercase() + "." + domainParts[domainParts.size - 1].lowercase();
|
||||||
|
if(slds.contains(sldParts))
|
||||||
|
return "." + domainParts.drop(domainParts.size - 3).joinToString(".");
|
||||||
|
else
|
||||||
|
return "." + domainParts.drop(domainParts.size - 2).joinToString(".");
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@ import java.io.ByteArrayOutputStream
|
|||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.net.Inet4Address
|
import java.net.Inet4Address
|
||||||
|
import java.net.Inet6Address
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import java.net.InetSocketAddress
|
import java.net.InetSocketAddress
|
||||||
import java.net.Socket
|
import java.net.Socket
|
||||||
|
|||||||
@@ -33,10 +33,10 @@ fun Boolean?.toYesNo(): String {
|
|||||||
fun InetAddress?.toUrlAddress(): String {
|
fun InetAddress?.toUrlAddress(): String {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
is Inet6Address -> {
|
is Inet6Address -> {
|
||||||
"[${toString()}]"
|
"[${hostAddress}]"
|
||||||
}
|
}
|
||||||
is Inet4Address -> {
|
is Inet4Address -> {
|
||||||
toString()
|
hostAddress
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
throw Exception("Invalid address type")
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,11 +2,8 @@ package com.futo.platformplayer
|
|||||||
|
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Context.POWER_SERVICE
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
|
||||||
import android.os.PowerManager
|
|
||||||
import android.webkit.CookieManager
|
import android.webkit.CookieManager
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
@@ -14,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
|
||||||
@@ -26,7 +24,6 @@ import com.futo.platformplayer.states.StateBackup
|
|||||||
import com.futo.platformplayer.states.StateCache
|
import com.futo.platformplayer.states.StateCache
|
||||||
import com.futo.platformplayer.states.StateMeta
|
import com.futo.platformplayer.states.StateMeta
|
||||||
import com.futo.platformplayer.states.StatePayment
|
import com.futo.platformplayer.states.StatePayment
|
||||||
import com.futo.platformplayer.states.StatePlayer
|
|
||||||
import com.futo.platformplayer.states.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
import com.futo.platformplayer.states.StateUpdate
|
import com.futo.platformplayer.states.StateUpdate
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
@@ -36,9 +33,7 @@ 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 com.stripe.android.customersheet.injection.CustomerSheetViewModelModule_Companion_ContextFactory.context
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -63,6 +58,15 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@Transient
|
@Transient
|
||||||
val onTabsChanged = Event0();
|
val onTabsChanged = Event0();
|
||||||
|
|
||||||
|
@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)
|
@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() {
|
||||||
@@ -140,7 +144,6 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,6 +254,9 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
|
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
|
||||||
var progressBar: Boolean = true;
|
var progressBar: Boolean = true;
|
||||||
|
|
||||||
|
@FormField(R.string.hide_hidden_from_search, FieldForm.TOGGLE, R.string.hide_hidden_from_search_description, 7)
|
||||||
|
var hidefromSearch: Boolean = false;
|
||||||
|
|
||||||
|
|
||||||
fun getSearchFeedStyle(): FeedStyle {
|
fun getSearchFeedStyle(): FeedStyle {
|
||||||
if(searchFeedStyle == 0)
|
if(searchFeedStyle == 0)
|
||||||
@@ -409,17 +415,13 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
var preferredPreviewQuality: Int = 5;
|
var preferredPreviewQuality: Int = 5;
|
||||||
fun getPreferredPreviewQualityPixelCount(): Int = preferedQualityToPixels(preferredPreviewQuality);
|
fun getPreferredPreviewQualityPixelCount(): Int = preferedQualityToPixels(preferredPreviewQuality);
|
||||||
|
|
||||||
|
|
||||||
@FormField(R.string.simplify_sources, FieldForm.TOGGLE, R.string.simplify_sources_description, 4)
|
@FormField(R.string.simplify_sources, FieldForm.TOGGLE, R.string.simplify_sources_description, 4)
|
||||||
var simplifySources: Boolean = true;
|
var simplifySources: Boolean = true;
|
||||||
|
|
||||||
@FormField(R.string.auto_rotate, FieldForm.DROPDOWN, -1, 5)
|
@FormField(R.string.always_allow_reverse_landscape_auto_rotate, FieldForm.TOGGLE, R.string.always_allow_reverse_landscape_auto_rotate_description, 5)
|
||||||
@DropdownFieldOptionsId(R.array.system_enabled_disabled_array)
|
var alwaysAllowReverseLandscapeAutoRotate: Boolean = true
|
||||||
var autoRotate: Int = 2;
|
|
||||||
|
|
||||||
fun isAutoRotate() = (autoRotate == 1 && !StatePlayer.instance.rotationLock) || (autoRotate == 2 && StateApp.instance.getCurrentSystemAutoRotate() && !StatePlayer.instance.rotationLock);
|
@FormField(R.string.background_behavior, FieldForm.DROPDOWN, -1, 6)
|
||||||
|
|
||||||
@FormField(R.string.background_behavior, FieldForm.DROPDOWN, -1, 7)
|
|
||||||
@DropdownFieldOptionsId(R.array.player_background_behavior)
|
@DropdownFieldOptionsId(R.array.player_background_behavior)
|
||||||
var backgroundPlay: Int = 2;
|
var backgroundPlay: Int = 2;
|
||||||
|
|
||||||
@@ -471,26 +473,43 @@ 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, 14)
|
@FormField(R.string.prefer_webm, FieldForm.TOGGLE, R.string.prefer_webm_description, 18)
|
||||||
var preferWebmVideo: Boolean = false;
|
var preferWebmVideo: Boolean = false;
|
||||||
@FormField(R.string.prefer_webm_audio, FieldForm.TOGGLE, R.string.prefer_webm_audio_description, 15)
|
@FormField(R.string.prefer_webm_audio, FieldForm.TOGGLE, R.string.prefer_webm_audio_description, 19)
|
||||||
var preferWebmAudio: Boolean = false;
|
var preferWebmAudio: Boolean = false;
|
||||||
|
|
||||||
@FormField(R.string.allow_under_cutout, FieldForm.TOGGLE, R.string.allow_under_cutout_description, 16)
|
@FormField(R.string.allow_under_cutout, FieldForm.TOGGLE, R.string.allow_under_cutout_description, 20)
|
||||||
var allowVideoToGoUnderCutout: Boolean = true;
|
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)
|
||||||
@@ -539,7 +558,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)
|
||||||
@@ -827,10 +846,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();
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -839,12 +862,16 @@ 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.playlist_allow_dups, FieldForm.TOGGLE, R.string.playlist_allow_dups_description, 3)
|
||||||
|
var playlistAllowDups: Boolean = true;
|
||||||
|
|
||||||
@FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 1)
|
@FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 4)
|
||||||
var polycentricEnabled: Boolean = true;
|
var polycentricEnabled: Boolean = true;
|
||||||
|
|
||||||
|
@FormField(R.string.polycentric_local_cache, FieldForm.TOGGLE, R.string.polycentric_local_cache_description, 5)
|
||||||
|
var polycentricLocalCache: Boolean = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.gesture_controls, FieldForm.GROUP, -1, 19)
|
@FormField(R.string.gesture_controls, FieldForm.GROUP, -1, 19)
|
||||||
@@ -876,7 +903,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 {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.futo.platformplayer
|
package com.futo.platformplayer
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.webkit.CookieManager
|
import android.webkit.CookieManager
|
||||||
import androidx.work.Data
|
import androidx.work.Data
|
||||||
import androidx.work.OneTimeWorkRequestBuilder
|
import androidx.work.OneTimeWorkRequestBuilder
|
||||||
@@ -235,13 +236,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)
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
package com.futo.platformplayer
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.pm.ActivityInfo
|
|
||||||
import android.hardware.SensorManager
|
|
||||||
import android.view.OrientationEventListener
|
|
||||||
import com.futo.platformplayer.constructs.Event1
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class SimpleOrientationListener(
|
|
||||||
private val activity: Activity,
|
|
||||||
private val lifecycleScope: CoroutineScope
|
|
||||||
) {
|
|
||||||
private var lastOrientation: Int = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
|
||||||
private var lastStableOrientation: Int = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
|
||||||
private val stabilityThresholdTime = 500L
|
|
||||||
|
|
||||||
val onOrientationChanged = Event1<Int>()
|
|
||||||
|
|
||||||
private val orientationListener = object : OrientationEventListener(activity, SensorManager.SENSOR_DELAY_UI) {
|
|
||||||
override fun onOrientationChanged(orientation: Int) {
|
|
||||||
val newOrientation = when {
|
|
||||||
orientation in 45..134 -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
|
|
||||||
orientation in 135..224 -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT
|
|
||||||
orientation in 225..314 -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
|
||||||
orientation in 315..360 || orientation in 0..44 -> ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
|
||||||
else -> lastOrientation
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newOrientation != lastStableOrientation) {
|
|
||||||
lastStableOrientation = newOrientation
|
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
|
||||||
delay(stabilityThresholdTime)
|
|
||||||
if (newOrientation == lastStableOrientation) {
|
|
||||||
lastOrientation = newOrientation
|
|
||||||
onOrientationChanged.emit(newOrientation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
orientationListener.enable()
|
|
||||||
lastOrientation = activity.resources.configuration.orientation
|
|
||||||
}
|
|
||||||
|
|
||||||
fun stopListening() {
|
|
||||||
orientationListener.disable()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
||||||
@@ -357,8 +368,8 @@ class UIDialogs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showChangelogDialog(context: Context, lastVersion: Int) {
|
fun showChangelogDialog(context: Context, lastVersion: Int, changelogs: Map<Int, String>? = null) {
|
||||||
val dialog = ChangelogDialog(context);
|
val dialog = ChangelogDialog(context, changelogs);
|
||||||
registerDialogOpened(dialog);
|
registerDialogOpened(dialog);
|
||||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||||
dialog.show();
|
dialog.show();
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ 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.JSDashManifestRawAudioSource
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawSource
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawSource
|
||||||
import com.futo.platformplayer.downloads.VideoLocal
|
import com.futo.platformplayer.downloads.VideoLocal
|
||||||
@@ -334,7 +335,9 @@ class UISlideOverlays {
|
|||||||
call = {
|
call = {
|
||||||
selectedVideoVariant = it
|
selectedVideoVariant = it
|
||||||
slideUpMenuOverlay.selectOption(videoButtons, it)
|
slideUpMenuOverlay.selectOption(videoButtons, it)
|
||||||
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
if (audioButtons.isEmpty()){
|
||||||
|
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
||||||
|
}
|
||||||
},
|
},
|
||||||
invokeParent = false
|
invokeParent = false
|
||||||
))
|
))
|
||||||
@@ -416,7 +419,7 @@ 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(
|
listOf((if (audioSources != null) listOf(SlideUpMenuItem(
|
||||||
container.context,
|
container.context,
|
||||||
R.drawable.ic_movie,
|
R.drawable.ic_movie,
|
||||||
container.context.getString(R.string.none),
|
container.context.getString(R.string.none),
|
||||||
@@ -429,7 +432,7 @@ class UISlideOverlays {
|
|||||||
menu?.setOk(container.context.getString(R.string.download));
|
menu?.setOk(container.context.getString(R.string.download));
|
||||||
},
|
},
|
||||||
invokeParent = false
|
invokeParent = false
|
||||||
)) +
|
)) else listOf()) +
|
||||||
videoSources
|
videoSources
|
||||||
.filter { it.isDownloadable() }
|
.filter { it.isDownloadable() }
|
||||||
.map {
|
.map {
|
||||||
@@ -879,6 +882,12 @@ 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",
|
||||||
@@ -888,7 +897,8 @@ class UISlideOverlays {
|
|||||||
"${lastUpdated.videos.size} " + container.context.getString(R.string.videos),
|
"${lastUpdated.videos.size} " + container.context.getString(R.string.videos),
|
||||||
tag = "",
|
tag = "",
|
||||||
call = {
|
call = {
|
||||||
StatePlaylists.instance.addToPlaylist(lastUpdated.id, video);
|
if(StatePlaylists.instance.addToPlaylist(lastUpdated.id, video))
|
||||||
|
UIDialogs.appToast("Added to playlist [${lastUpdated?.name}]", false);
|
||||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
@@ -899,17 +909,18 @@ 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(
|
if(!isLimited && !video.isLive)
|
||||||
container.context,
|
SlideUpMenuItem(
|
||||||
R.drawable.ic_download,
|
container.context,
|
||||||
container.context.getString(R.string.download),
|
R.drawable.ic_download,
|
||||||
container.context.getString(R.string.download_the_video),
|
container.context.getString(R.string.download),
|
||||||
tag = "download",
|
container.context.getString(R.string.download_the_video),
|
||||||
call = {
|
tag = "download",
|
||||||
showDownloadVideoOverlay(video, container, true);
|
call = {
|
||||||
},
|
showDownloadVideoOverlay(video, container, true);
|
||||||
invokeParent = false
|
},
|
||||||
),
|
invokeParent = false
|
||||||
|
) else null,
|
||||||
SlideUpMenuItem(
|
SlideUpMenuItem(
|
||||||
container.context,
|
container.context,
|
||||||
R.drawable.ic_share,
|
R.drawable.ic_share,
|
||||||
@@ -936,7 +947,7 @@ class UISlideOverlays {
|
|||||||
StateMeta.instance.addHiddenCreator(video.author.url);
|
StateMeta.instance.addHiddenCreator(video.author.url);
|
||||||
UIDialogs.toast(container.context, "[${video.author.name}] hidden, you may need to reload home");
|
UIDialogs.toast(container.context, "[${video.author.name}] hidden, you may need to reload home");
|
||||||
}))
|
}))
|
||||||
+ actions)
|
+ 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",
|
||||||
@@ -951,7 +962,7 @@ class UISlideOverlays {
|
|||||||
"${container.context.getString(R.string.add_to)} " + StatePlayer.TYPE_WATCHLATER + "",
|
"${container.context.getString(R.string.add_to)} " + StatePlayer.TYPE_WATCHLATER + "",
|
||||||
"${watchLater.size} " + container.context.getString(R.string.videos),
|
"${watchLater.size} " + container.context.getString(R.string.videos),
|
||||||
tag = "watch later",
|
tag = "watch later",
|
||||||
call = { StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video)); }),
|
call = { StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true); }),
|
||||||
SlideUpMenuItem(container.context,
|
SlideUpMenuItem(container.context,
|
||||||
R.drawable.ic_history,
|
R.drawable.ic_history,
|
||||||
container.context.getString(R.string.add_to_history),
|
container.context.getString(R.string.add_to_history),
|
||||||
@@ -983,7 +994,8 @@ class UISlideOverlays {
|
|||||||
"${playlist.videos.size} " + container.context.getString(R.string.videos),
|
"${playlist.videos.size} " + container.context.getString(R.string.videos),
|
||||||
tag = "",
|
tag = "",
|
||||||
call = {
|
call = {
|
||||||
StatePlaylists.instance.addToPlaylist(playlist.id, video);
|
if(StatePlaylists.instance.addToPlaylist(playlist.id, video))
|
||||||
|
UIDialogs.appToast("Added to playlist [${playlist.name}]", false);
|
||||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -1010,7 +1022,8 @@ class UISlideOverlays {
|
|||||||
"${lastUpdated.videos.size} " + container.context.getString(R.string.videos),
|
"${lastUpdated.videos.size} " + container.context.getString(R.string.videos),
|
||||||
tag = "",
|
tag = "",
|
||||||
call = {
|
call = {
|
||||||
StatePlaylists.instance.addToPlaylist(lastUpdated.id, video);
|
if(StatePlaylists.instance.addToPlaylist(lastUpdated.id, video))
|
||||||
|
UIDialogs.appToast("Added to playlist [${lastUpdated?.name}]", false);
|
||||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
@@ -1032,16 +1045,10 @@ class UISlideOverlays {
|
|||||||
StatePlayer.TYPE_WATCHLATER,
|
StatePlayer.TYPE_WATCHLATER,
|
||||||
"${watchLater.size} " + container.context.getString(R.string.videos),
|
"${watchLater.size} " + container.context.getString(R.string.videos),
|
||||||
tag = "watch later",
|
tag = "watch later",
|
||||||
call = { StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video)); }),
|
call = { StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true);
|
||||||
SlideUpMenuItem(
|
UIDialogs.appToast("Added to watch later", false);
|
||||||
container.context,
|
}),
|
||||||
R.drawable.ic_download,
|
)
|
||||||
container.context.getString(R.string.download),
|
|
||||||
container.context.getString(R.string.download_the_video),
|
|
||||||
tag = container.context.getString(R.string.download),
|
|
||||||
call = { showDownloadVideoOverlay(video, container, true); },
|
|
||||||
invokeParent = false
|
|
||||||
))
|
|
||||||
);
|
);
|
||||||
|
|
||||||
val playlistItems = arrayListOf<SlideUpMenuItem>();
|
val playlistItems = arrayListOf<SlideUpMenuItem>();
|
||||||
@@ -1067,7 +1074,8 @@ class UISlideOverlays {
|
|||||||
"${playlist.videos.size} " + container.context.getString(R.string.videos),
|
"${playlist.videos.size} " + container.context.getString(R.string.videos),
|
||||||
tag = "",
|
tag = "",
|
||||||
call = {
|
call = {
|
||||||
StatePlaylists.instance.addToPlaylist(playlist.id, video);
|
if(StatePlaylists.instance.addToPlaylist(playlist.id, video))
|
||||||
|
UIDialogs.appToast("Added to playlist [${playlist.name}]", false);
|
||||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,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
|
||||||
|
|
||||||
@@ -147,8 +150,6 @@ fun InputStream.copyToOutputStream(inputStreamLength: Long, outputStream: Output
|
|||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
fun Activity.setNavigationBarColorAndIcons() {
|
fun Activity.setNavigationBarColorAndIcons() {
|
||||||
window.navigationBarColor = ContextCompat.getColor(this, android.R.color.black);
|
window.navigationBarColor = ContextCompat.getColor(this, android.R.color.black);
|
||||||
if (Settings.instance.playback.allowVideoToGoUnderCutout)
|
|
||||||
window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
window.insetsController?.setSystemBarsAppearance(0, WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS);
|
window.insetsController?.setSystemBarsAppearance(0, WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS);
|
||||||
@@ -232,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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
package com.futo.platformplayer.activities
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
|
import android.media.AudioManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.net.wifi.WifiManager
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.StrictMode
|
import android.os.StrictMode
|
||||||
import android.os.StrictMode.VmPolicy
|
import android.os.StrictMode.VmPolicy
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.WindowManager
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import androidx.activity.result.ActivityResult
|
import androidx.activity.result.ActivityResult
|
||||||
@@ -28,10 +30,12 @@ import androidx.fragment.app.Fragment
|
|||||||
import androidx.fragment.app.FragmentContainerView
|
import androidx.fragment.app.FragmentContainerView
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
import com.futo.platformplayer.BuildConfig
|
import com.futo.platformplayer.BuildConfig
|
||||||
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.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.casting.StateCasting
|
import com.futo.platformplayer.casting.StateCasting
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
|
||||||
@@ -69,6 +73,8 @@ import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFrag
|
|||||||
import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.ImportCache
|
import com.futo.platformplayer.models.ImportCache
|
||||||
|
import com.futo.platformplayer.models.UrlVideoWithTime
|
||||||
|
import com.futo.platformplayer.receivers.MediaButtonReceiver
|
||||||
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.StateBackup
|
import com.futo.platformplayer.states.StateBackup
|
||||||
@@ -79,6 +85,7 @@ import com.futo.platformplayer.states.StatePlayer
|
|||||||
import com.futo.platformplayer.states.StatePlaylists
|
import com.futo.platformplayer.states.StatePlaylists
|
||||||
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.StringStorage
|
||||||
import com.futo.platformplayer.stores.SubscriptionStorage
|
import com.futo.platformplayer.stores.SubscriptionStorage
|
||||||
import com.futo.platformplayer.stores.v2.ManagedStore
|
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||||
import com.futo.platformplayer.views.ToastView
|
import com.futo.platformplayer.views.ToastView
|
||||||
@@ -86,11 +93,14 @@ import com.futo.polycentric.core.ApiMethods
|
|||||||
import com.google.gson.JsonParser
|
import com.google.gson.JsonParser
|
||||||
import com.google.zxing.integration.android.IntentIntegrator
|
import com.google.zxing.integration.android.IntentIntegrator
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.PrintWriter
|
import java.io.PrintWriter
|
||||||
@@ -108,7 +118,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
private val HEIGHT_VIDEO_MINIMIZED_DP = 60f;
|
private val HEIGHT_VIDEO_MINIMIZED_DP = 60f;
|
||||||
|
|
||||||
//Containers
|
//Containers
|
||||||
lateinit var rootView : MotionLayout;
|
lateinit var rootView: MotionLayout;
|
||||||
|
|
||||||
private lateinit var _overlayContainer: FrameLayout;
|
private lateinit var _overlayContainer: FrameLayout;
|
||||||
private lateinit var _toastView: ToastView;
|
private lateinit var _toastView: ToastView;
|
||||||
@@ -165,11 +175,11 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
lateinit var _fragVideoDetail: VideoDetailFragment;
|
lateinit var _fragVideoDetail: VideoDetailFragment;
|
||||||
|
|
||||||
//State
|
//State
|
||||||
private val _queue : Queue<Pair<MainFragment, Any?>> = LinkedList();
|
private val _queue: Queue<Pair<MainFragment, Any?>> = LinkedList();
|
||||||
lateinit var fragCurrent : MainFragment private set;
|
lateinit var fragCurrent: MainFragment private set;
|
||||||
private var _parameterCurrent: Any? = null;
|
private var _parameterCurrent: Any? = null;
|
||||||
|
|
||||||
var fragBeforeOverlay : MainFragment? = null; private set;
|
var fragBeforeOverlay: MainFragment? = null; private set;
|
||||||
|
|
||||||
val onNavigated = Event1<MainFragment>();
|
val onNavigated = Event1<MainFragment>();
|
||||||
|
|
||||||
@@ -215,15 +225,15 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
Logger.e("Application", "Uncaught", excp);
|
Logger.e("Application", "Uncaught", excp);
|
||||||
|
|
||||||
//Resolve invocation chains
|
//Resolve invocation chains
|
||||||
while(excp is InvocationTargetException || excp is java.lang.RuntimeException) {
|
while (excp is InvocationTargetException || excp is java.lang.RuntimeException) {
|
||||||
val before = excp;
|
val before = excp;
|
||||||
|
|
||||||
if(excp is InvocationTargetException)
|
if (excp is InvocationTargetException)
|
||||||
excp = excp.targetException ?: excp.cause ?: excp;
|
excp = excp.targetException ?: excp.cause ?: excp;
|
||||||
else if(excp is java.lang.RuntimeException)
|
else if (excp is java.lang.RuntimeException)
|
||||||
excp = excp.cause ?: excp;
|
excp = excp.cause ?: excp;
|
||||||
|
|
||||||
if(excp == before)
|
if (excp == before)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
writer.write((excp.message ?: "Empty error") + "\n\n");
|
writer.write((excp.message ?: "Empty error") + "\n\n");
|
||||||
@@ -245,6 +255,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UnstableApi
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
Logger.i(TAG, "MainActivity Starting");
|
Logger.i(TAG, "MainActivity Starting");
|
||||||
StateApp.instance.setGlobalContext(this, lifecycleScope);
|
StateApp.instance.setGlobalContext(this, lifecycleScope);
|
||||||
@@ -253,6 +264,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.activity_main);
|
setContentView(R.layout.activity_main);
|
||||||
setNavigationBarColorAndIcons();
|
setNavigationBarColorAndIcons();
|
||||||
|
if (Settings.instance.playback.allowVideoToGoUnderCutout)
|
||||||
|
window.attributes.layoutInDisplayCutoutMode =
|
||||||
|
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
||||||
|
|
||||||
runBlocking {
|
runBlocking {
|
||||||
StatePlatform.instance.updateAvailableClients(this@MainActivity);
|
StatePlatform.instance.updateAvailableClients(this@MainActivity);
|
||||||
@@ -326,10 +340,12 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
updateSegmentPaddings();
|
updateSegmentPaddings();
|
||||||
};
|
};
|
||||||
_fragVideoDetail.onTransitioning.subscribe {
|
_fragVideoDetail.onTransitioning.subscribe {
|
||||||
if(it || _fragVideoDetail.state != VideoDetailFragment.State.MINIMIZED)
|
if (it || _fragVideoDetail.state != VideoDetailFragment.State.MINIMIZED)
|
||||||
_fragContainerOverlay.elevation = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 15f, resources.displayMetrics);
|
_fragContainerOverlay.elevation =
|
||||||
|
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 15f, resources.displayMetrics);
|
||||||
else
|
else
|
||||||
_fragContainerOverlay.elevation = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics);
|
_fragContainerOverlay.elevation =
|
||||||
|
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics);
|
||||||
}
|
}
|
||||||
|
|
||||||
_fragVideoDetail.onCloseEvent.subscribe {
|
_fragVideoDetail.onCloseEvent.subscribe {
|
||||||
@@ -346,40 +362,39 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
_buttonIncognito.alpha = 0f;
|
_buttonIncognito.alpha = 0f;
|
||||||
StateApp.instance.privateModeChanged.subscribe {
|
StateApp.instance.privateModeChanged.subscribe {
|
||||||
//Messing with visibility causes some issues with layout ordering?
|
//Messing with visibility causes some issues with layout ordering?
|
||||||
if(it) {
|
if (it) {
|
||||||
_buttonIncognito.elevation = 99f;
|
_buttonIncognito.elevation = 99f;
|
||||||
_buttonIncognito.alpha = 1f;
|
_buttonIncognito.alpha = 1f;
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
_buttonIncognito.elevation = -99f;
|
_buttonIncognito.elevation = -99f;
|
||||||
_buttonIncognito.alpha = 0f;
|
_buttonIncognito.alpha = 0f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_buttonIncognito.setOnClickListener {
|
_buttonIncognito.setOnClickListener {
|
||||||
if(!StateApp.instance.privateMode)
|
if (!StateApp.instance.privateMode)
|
||||||
return@setOnClickListener;
|
return@setOnClickListener;
|
||||||
UIDialogs.showDialog(this, R.drawable.ic_disabled_visible_purple, "Disable Privacy Mode",
|
UIDialogs.showDialog(
|
||||||
|
this, R.drawable.ic_disabled_visible_purple, "Disable Privacy Mode",
|
||||||
"Do you want to disable privacy mode? New videos will be tracked again.", null, 0,
|
"Do you want to disable privacy mode? New videos will be tracked again.", null, 0,
|
||||||
UIDialogs.Action("Cancel", {
|
UIDialogs.Action("Cancel", {
|
||||||
StateApp.instance.setPrivacyMode(true);
|
StateApp.instance.setPrivacyMode(true);
|
||||||
}, UIDialogs.ActionStyle.NONE),
|
}, UIDialogs.ActionStyle.NONE),
|
||||||
UIDialogs.Action("Disable", {
|
UIDialogs.Action("Disable", {
|
||||||
StateApp.instance.setPrivacyMode(false);
|
StateApp.instance.setPrivacyMode(false);
|
||||||
}, UIDialogs.ActionStyle.DANGEROUS));
|
}, UIDialogs.ActionStyle.DANGEROUS)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
_fragVideoDetail.onFullscreenChanged.subscribe {
|
_fragVideoDetail.onFullscreenChanged.subscribe {
|
||||||
Logger.i(TAG, "onFullscreenChanged ${it}");
|
Logger.i(TAG, "onFullscreenChanged ${it}");
|
||||||
|
|
||||||
if(it) {
|
if (it) {
|
||||||
_buttonIncognito.elevation = -99f;
|
_buttonIncognito.elevation = -99f;
|
||||||
_buttonIncognito.alpha = 0f;
|
_buttonIncognito.alpha = 0f;
|
||||||
}
|
} else {
|
||||||
else {
|
if (StateApp.instance.privateMode) {
|
||||||
if(StateApp.instance.privateMode) {
|
|
||||||
_buttonIncognito.elevation = 99f;
|
_buttonIncognito.elevation = 99f;
|
||||||
_buttonIncognito.alpha = 1f;
|
_buttonIncognito.alpha = 1f;
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
_buttonIncognito.elevation = -99f;
|
_buttonIncognito.elevation = -99f;
|
||||||
_buttonIncognito.alpha = 0f;
|
_buttonIncognito.alpha = 0f;
|
||||||
}
|
}
|
||||||
@@ -392,7 +407,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
return@subscribe;
|
return@subscribe;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(_fragVideoDetail.state == VideoDetailFragment.State.CLOSED) {
|
if (_fragVideoDetail.state == VideoDetailFragment.State.CLOSED) {
|
||||||
if (fragCurrent !is VideoDetailFragment) {
|
if (fragCurrent !is VideoDetailFragment) {
|
||||||
val toPlay = StatePlayer.instance.getCurrentQueueItem();
|
val toPlay = StatePlayer.instance.getCurrentQueueItem();
|
||||||
navigate(_fragVideoDetail, toPlay);
|
navigate(_fragVideoDetail, toPlay);
|
||||||
@@ -440,11 +455,12 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
_fragSubGroupList.topBar = _fragTopBarAdd;
|
_fragSubGroupList.topBar = _fragTopBarAdd;
|
||||||
|
|
||||||
_fragBrowser.topBar = _fragTopBarNavigation;
|
_fragBrowser.topBar = _fragTopBarNavigation;
|
||||||
|
|
||||||
fragCurrent = _fragMainHome;
|
fragCurrent = _fragMainHome;
|
||||||
|
|
||||||
val defaultTab = Settings.instance.tabs.mapNotNull {
|
val defaultTab = Settings.instance.tabs.mapNotNull {
|
||||||
val buttonDefinition = MenuBottomBarFragment.buttonDefinitions.firstOrNull { bd -> it.id == bd.id };
|
val buttonDefinition =
|
||||||
|
MenuBottomBarFragment.buttonDefinitions.firstOrNull { bd -> it.id == bd.id };
|
||||||
if (buttonDefinition == null) {
|
if (buttonDefinition == null) {
|
||||||
return@mapNotNull null;
|
return@mapNotNull null;
|
||||||
} else {
|
} else {
|
||||||
@@ -503,7 +519,11 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
|
|
||||||
//startActivity(Intent(this, TestActivity::class.java));
|
//startActivity(Intent(this, TestActivity::class.java));
|
||||||
|
|
||||||
val sharedPreferences = getSharedPreferences("GrayjayFirstBoot", Context.MODE_PRIVATE)
|
// updates the requestedOrientation based on user settings
|
||||||
|
_fragVideoDetail.updateOrientation()
|
||||||
|
|
||||||
|
val sharedPreferences =
|
||||||
|
getSharedPreferences("GrayjayFirstBoot", Context.MODE_PRIVATE)
|
||||||
val isFirstBoot = sharedPreferences.getBoolean("IsFirstBoot", true)
|
val isFirstBoot = sharedPreferences.getBoolean("IsFirstBoot", true)
|
||||||
if (isFirstBoot) {
|
if (isFirstBoot) {
|
||||||
UIDialogs.showConfirmationDialog(this, getString(R.string.do_you_want_to_see_the_tutorials_you_can_find_them_at_any_time_through_the_more_button), {
|
UIDialogs.showConfirmationDialog(this, getString(R.string.do_you_want_to_see_the_tutorials_you_can_find_them_at_any_time_through_the_more_button), {
|
||||||
@@ -512,6 +532,64 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
|
|
||||||
sharedPreferences.edit().putBoolean("IsFirstBoot", false).apply()
|
sharedPreferences.edit().putBoolean("IsFirstBoot", false).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val submissionStatus = FragmentedStorage.get<StringStorage>("subscriptionSubmissionStatus")
|
||||||
|
|
||||||
|
val numSubscriptions = StateSubscriptions.instance.getSubscriptionCount()
|
||||||
|
|
||||||
|
val subscriptionsThreshold = 20
|
||||||
|
|
||||||
|
if (
|
||||||
|
submissionStatus.value == ""
|
||||||
|
&& StateApp.instance.getCurrentNetworkState() != StateApp.NetworkState.DISCONNECTED
|
||||||
|
&& numSubscriptions >= subscriptionsThreshold
|
||||||
|
) {
|
||||||
|
|
||||||
|
UIDialogs.showDialog(
|
||||||
|
this,
|
||||||
|
R.drawable.ic_internet,
|
||||||
|
getString(R.string.contribute_personal_subscriptions_list),
|
||||||
|
getString(R.string.contribute_personal_subscriptions_list_description),
|
||||||
|
null,
|
||||||
|
0,
|
||||||
|
UIDialogs.Action("Cancel", {
|
||||||
|
submissionStatus.setAndSave("dismissed")
|
||||||
|
}, UIDialogs.ActionStyle.NONE),
|
||||||
|
UIDialogs.Action("Upload", {
|
||||||
|
submissionStatus.setAndSave("submitted")
|
||||||
|
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
@Serializable
|
||||||
|
data class CreatorInfo(val pluginId: String, val url: String)
|
||||||
|
|
||||||
|
val subscriptions =
|
||||||
|
StateSubscriptions.instance.getSubscriptions().map { original ->
|
||||||
|
CreatorInfo(
|
||||||
|
pluginId = original.channel.id.pluginId ?: "",
|
||||||
|
url = original.channel.url
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val json = Json.encodeToString(subscriptions)
|
||||||
|
|
||||||
|
val url = "https://data.grayjay.app/donate-subscription-list"
|
||||||
|
val client = ManagedHttpClient();
|
||||||
|
val headers = hashMapOf(
|
||||||
|
"Content-Type" to "application/json"
|
||||||
|
)
|
||||||
|
try {
|
||||||
|
val response = client.post(url, json, headers)
|
||||||
|
// if it failed retry one time
|
||||||
|
if (!response.isOk) {
|
||||||
|
client.post(url, json, headers)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.i(TAG, "Failed to submit subscription list.", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, UIDialogs.ActionStyle.PRIMARY)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -576,39 +654,45 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleIntent(intent: Intent?) {
|
private fun handleIntent(intent: Intent?) {
|
||||||
if(intent == null)
|
if (intent == null)
|
||||||
return;
|
return;
|
||||||
Logger.i(TAG, "handleIntent started by " + intent.action);
|
Logger.i(TAG, "handleIntent started by " + intent.action);
|
||||||
|
|
||||||
|
|
||||||
var targetData: String? = null;
|
var targetData: String? = null;
|
||||||
|
|
||||||
when(intent.action) {
|
when (intent.action) {
|
||||||
Intent.ACTION_SEND -> {
|
Intent.ACTION_SEND -> {
|
||||||
targetData = intent.getStringExtra(Intent.EXTRA_STREAM) ?: intent.getStringExtra(Intent.EXTRA_TEXT);
|
targetData = intent.getStringExtra(Intent.EXTRA_STREAM)
|
||||||
|
?: intent.getStringExtra(Intent.EXTRA_TEXT);
|
||||||
Logger.i(TAG, "Share Received: " + targetData);
|
Logger.i(TAG, "Share Received: " + targetData);
|
||||||
}
|
}
|
||||||
|
|
||||||
Intent.ACTION_VIEW -> {
|
Intent.ACTION_VIEW -> {
|
||||||
targetData = intent.dataString
|
targetData = intent.dataString
|
||||||
|
|
||||||
if(!targetData.isNullOrEmpty()) {
|
if (!targetData.isNullOrEmpty()) {
|
||||||
Logger.i(TAG, "View Received: " + targetData);
|
Logger.i(TAG, "View Received: " + targetData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"VIDEO" -> {
|
"VIDEO" -> {
|
||||||
val url = intent.getStringExtra("VIDEO");
|
val url = intent.getStringExtra("VIDEO");
|
||||||
navigate(_fragVideoDetail, url);
|
navigate(_fragVideoDetail, url);
|
||||||
}
|
}
|
||||||
|
|
||||||
"IMPORT_OPTIONS" -> {
|
"IMPORT_OPTIONS" -> {
|
||||||
UIDialogs.showImportOptionsDialog(this);
|
UIDialogs.showImportOptionsDialog(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
"ACTION" -> {
|
"ACTION" -> {
|
||||||
val action = intent.getStringExtra("ACTION");
|
val action = intent.getStringExtra("ACTION");
|
||||||
StateDeveloper.instance.testState = "TestPlayback";
|
StateDeveloper.instance.testState = "TestPlayback";
|
||||||
StateDeveloper.instance.testPlayback();
|
StateDeveloper.instance.testPlayback();
|
||||||
}
|
}
|
||||||
|
|
||||||
"TAB" -> {
|
"TAB" -> {
|
||||||
when(intent.getStringExtra("TAB")){
|
when (intent.getStringExtra("TAB")) {
|
||||||
"Sources" -> {
|
"Sources" -> {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
StatePlatform.instance.updateAvailableClients(this@MainActivity, true) //Ideally this is not needed..
|
StatePlatform.instance.updateAvailableClients(this@MainActivity, true) //Ideally this is not needed..
|
||||||
@@ -619,7 +703,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
navigate(_fragBrowser, BrowserFragment.NavigateOptions("https://plugins.grayjay.app/phone.html", mapOf(
|
navigate(_fragBrowser, BrowserFragment.NavigateOptions("https://plugins.grayjay.app/phone.html", mapOf(
|
||||||
Pair("grayjay") { req ->
|
Pair("grayjay") { req ->
|
||||||
StateApp.instance.contextOrNull?.let {
|
StateApp.instance.contextOrNull?.let {
|
||||||
if(it is MainActivity) {
|
if (it is MainActivity) {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
it.handleUrlAll(req.url.toString());
|
it.handleUrlAll(req.url.toString());
|
||||||
}
|
}
|
||||||
@@ -638,8 +722,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
handleUrlAll(targetData)
|
handleUrlAll(targetData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} catch (ex: Throwable) {
|
||||||
catch(ex: Throwable) {
|
|
||||||
UIDialogs.showGeneralErrorDialog(this, getString(R.string.failed_to_handle_file), ex);
|
UIDialogs.showGeneralErrorDialog(this, getString(R.string.failed_to_handle_file), ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -648,35 +731,31 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
val uri = Uri.parse(url)
|
val uri = Uri.parse(url)
|
||||||
when (uri.scheme) {
|
when (uri.scheme) {
|
||||||
"grayjay" -> {
|
"grayjay" -> {
|
||||||
if(url.startsWith("grayjay://license/")) {
|
if (url.startsWith("grayjay://license/")) {
|
||||||
if(StatePayment.instance.setPaymentLicenseUrl(url))
|
if (StatePayment.instance.setPaymentLicenseUrl(url)) {
|
||||||
{
|
|
||||||
UIDialogs.showDialogOk(this, R.drawable.ic_check, getString(R.string.your_license_key_has_been_set_an_app_restart_might_be_required));
|
UIDialogs.showDialogOk(this, R.drawable.ic_check, getString(R.string.your_license_key_has_been_set_an_app_restart_might_be_required));
|
||||||
|
|
||||||
if(fragCurrent is BuyFragment)
|
if (fragCurrent is BuyFragment)
|
||||||
closeSegment(fragCurrent);
|
closeSegment(fragCurrent);
|
||||||
}
|
} else
|
||||||
else
|
|
||||||
UIDialogs.toast(getString(R.string.invalid_license_format));
|
UIDialogs.toast(getString(R.string.invalid_license_format));
|
||||||
|
|
||||||
}
|
} else if (url.startsWith("grayjay://plugin/")) {
|
||||||
else if(url.startsWith("grayjay://plugin/")) {
|
|
||||||
val intent = Intent(this, AddSourceActivity::class.java).apply {
|
val intent = Intent(this, AddSourceActivity::class.java).apply {
|
||||||
data = Uri.parse(url.substring("grayjay://plugin/".length));
|
data = Uri.parse(url.substring("grayjay://plugin/".length));
|
||||||
};
|
};
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
}
|
} else if (url.startsWith("grayjay://video/")) {
|
||||||
else if(url.startsWith("grayjay://video/")) {
|
|
||||||
val videoUrl = url.substring("grayjay://video/".length);
|
val videoUrl = url.substring("grayjay://video/".length);
|
||||||
navigate(_fragVideoDetail, videoUrl);
|
navigate(_fragVideoDetail, videoUrl);
|
||||||
}
|
} else if (url.startsWith("grayjay://channel/")) {
|
||||||
else if(url.startsWith("grayjay://channel/")) {
|
|
||||||
val channelUrl = url.substring("grayjay://channel/".length);
|
val channelUrl = url.substring("grayjay://channel/".length);
|
||||||
navigate(_fragMainChannel, channelUrl);
|
navigate(_fragMainChannel, channelUrl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"content" -> {
|
"content" -> {
|
||||||
if(!handleContent(url, intent.type)) {
|
if (!handleContent(url, intent.type)) {
|
||||||
UIDialogs.showSingleButtonDialog(
|
UIDialogs.showSingleButtonDialog(
|
||||||
this,
|
this,
|
||||||
R.drawable.ic_play,
|
R.drawable.ic_play,
|
||||||
@@ -685,8 +764,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
{ });
|
{ });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"file" -> {
|
"file" -> {
|
||||||
if(!handleFile(url)) {
|
if (!handleFile(url)) {
|
||||||
UIDialogs.showSingleButtonDialog(
|
UIDialogs.showSingleButtonDialog(
|
||||||
this,
|
this,
|
||||||
R.drawable.ic_play,
|
R.drawable.ic_play,
|
||||||
@@ -695,8 +775,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
{ });
|
{ });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"polycentric" -> {
|
"polycentric" -> {
|
||||||
if(!handlePolycentric(url)) {
|
if (!handlePolycentric(url)) {
|
||||||
UIDialogs.showSingleButtonDialog(
|
UIDialogs.showSingleButtonDialog(
|
||||||
this,
|
this,
|
||||||
R.drawable.ic_play,
|
R.drawable.ic_play,
|
||||||
@@ -705,8 +786,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
{ });
|
{ });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"fcast" -> {
|
"fcast" -> {
|
||||||
if(!handleFCast(url)) {
|
if (!handleFCast(url)) {
|
||||||
UIDialogs.showSingleButtonDialog(
|
UIDialogs.showSingleButtonDialog(
|
||||||
this,
|
this,
|
||||||
R.drawable.ic_cast,
|
R.drawable.ic_cast,
|
||||||
@@ -715,6 +797,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
{ });
|
{ });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
if (!handleUrl(url)) {
|
if (!handleUrl(url)) {
|
||||||
UIDialogs.showSingleButtonDialog(
|
UIDialogs.showSingleButtonDialog(
|
||||||
@@ -728,7 +811,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun handleUrl(url: String): Boolean {
|
suspend fun handleUrl(url: String, position: Int = 0): Boolean {
|
||||||
Logger.i(TAG, "handleUrl(url=$url)")
|
Logger.i(TAG, "handleUrl(url=$url)")
|
||||||
|
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
@@ -736,7 +819,10 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
if (StatePlatform.instance.hasEnabledVideoClient(url)) {
|
if (StatePlatform.instance.hasEnabledVideoClient(url)) {
|
||||||
Logger.i(TAG, "handleUrl(url=$url) found video client");
|
Logger.i(TAG, "handleUrl(url=$url) found video client");
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
navigate(_fragVideoDetail, url);
|
if (position > 0)
|
||||||
|
navigate(_fragVideoDetail, UrlVideoWithTime(url, position.toLong(), true));
|
||||||
|
else
|
||||||
|
navigate(_fragVideoDetail, url);
|
||||||
|
|
||||||
_fragVideoDetail.maximizeVideoDetail(true);
|
_fragVideoDetail.maximizeVideoDetail(true);
|
||||||
}
|
}
|
||||||
@@ -752,7 +838,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
} else if (StatePlatform.instance.hasEnabledPlaylistClient(url)) {
|
} else if (StatePlatform.instance.hasEnabledPlaylistClient(url)) {
|
||||||
Logger.i(TAG, "handleUrl(url=$url) found playlist client");
|
Logger.i(TAG, "handleUrl(url=$url) found playlist client");
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
navigate(_fragMainPlaylist, url);
|
navigate(_fragMainRemotePlaylist, url);
|
||||||
delay(100);
|
delay(100);
|
||||||
_fragVideoDetail.minimizeVideoDetail();
|
_fragVideoDetail.minimizeVideoDetail();
|
||||||
};
|
};
|
||||||
@@ -761,24 +847,25 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
return@withContext false;
|
return@withContext false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleContent(file: String, mime: String? = null): Boolean {
|
fun handleContent(file: String, mime: String? = null): Boolean {
|
||||||
Logger.i(TAG, "handleContent(url=$file)");
|
Logger.i(TAG, "handleContent(url=$file)");
|
||||||
|
|
||||||
val data = readSharedContent(file);
|
val data = readSharedContent(file);
|
||||||
if(file.lowercase().endsWith(".json") || mime == "application/json") {
|
if (file.lowercase().endsWith(".json") || mime == "application/json") {
|
||||||
var recon = String(data);
|
var recon = String(data);
|
||||||
if(!recon.trim().startsWith("["))
|
if (!recon.trim().startsWith("["))
|
||||||
return handleUnknownJson(recon);
|
return handleUnknownJson(recon);
|
||||||
|
|
||||||
var reconLines = Json.decodeFromString<List<String>>(recon);
|
var reconLines = Json.decodeFromString<List<String>>(recon);
|
||||||
val cacheStr = reconLines.find { it.startsWith("__CACHE:") }?.substring("__CACHE:".length);
|
val cacheStr =
|
||||||
|
reconLines.find { it.startsWith("__CACHE:") }?.substring("__CACHE:".length);
|
||||||
reconLines = reconLines.filter { !it.startsWith("__CACHE:") }; //TODO: constant prefix
|
reconLines = reconLines.filter { !it.startsWith("__CACHE:") }; //TODO: constant prefix
|
||||||
var cache: ImportCache? = null;
|
var cache: ImportCache? = null;
|
||||||
try {
|
try {
|
||||||
if(cacheStr != null)
|
if (cacheStr != null)
|
||||||
cache = Json.decodeFromString(cacheStr);
|
cache = Json.decodeFromString(cacheStr);
|
||||||
}
|
} catch (ex: Throwable) {
|
||||||
catch(ex: Throwable) {
|
|
||||||
Logger.e(TAG, "Failed to deserialize cache");
|
Logger.e(TAG, "Failed to deserialize cache");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -787,32 +874,31 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
Logger.i(TAG, "Opened shared playlist reconstruction\n${recon}");
|
Logger.i(TAG, "Opened shared playlist reconstruction\n${recon}");
|
||||||
handleReconstruction(recon, cache);
|
handleReconstruction(recon, cache);
|
||||||
return true;
|
return true;
|
||||||
}
|
} else if (file.lowercase().endsWith(".zip") || mime == "application/zip") {
|
||||||
else if(file.lowercase().endsWith(".zip") || mime == "application/zip") {
|
|
||||||
StateBackup.importZipBytes(this, lifecycleScope, data);
|
StateBackup.importZipBytes(this, lifecycleScope, data);
|
||||||
return true;
|
return true;
|
||||||
}
|
} else if (file.lowercase().endsWith(".txt") || mime == "text/plain") {
|
||||||
else if(file.lowercase().endsWith(".txt") || mime == "text/plain") {
|
|
||||||
return handleUnknownText(String(data));
|
return handleUnknownText(String(data));
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleFile(file: String): Boolean {
|
fun handleFile(file: String): Boolean {
|
||||||
Logger.i(TAG, "handleFile(url=$file)");
|
Logger.i(TAG, "handleFile(url=$file)");
|
||||||
if(file.lowercase().endsWith(".json")) {
|
if (file.lowercase().endsWith(".json")) {
|
||||||
var recon = String(readSharedFile(file));
|
var recon = String(readSharedFile(file));
|
||||||
if(!recon.startsWith("["))
|
if (!recon.startsWith("["))
|
||||||
return handleUnknownJson(recon);
|
return handleUnknownJson(recon);
|
||||||
|
|
||||||
var reconLines = Json.decodeFromString<List<String>>(recon);
|
var reconLines = Json.decodeFromString<List<String>>(recon);
|
||||||
val cacheStr = reconLines.find { it.startsWith("__CACHE:") }?.substring("__CACHE:".length);
|
val cacheStr =
|
||||||
|
reconLines.find { it.startsWith("__CACHE:") }?.substring("__CACHE:".length);
|
||||||
reconLines = reconLines.filter { !it.startsWith("__CACHE:") }; //TODO: constant prefix
|
reconLines = reconLines.filter { !it.startsWith("__CACHE:") }; //TODO: constant prefix
|
||||||
var cache: ImportCache? = null;
|
var cache: ImportCache? = null;
|
||||||
try {
|
try {
|
||||||
if(cacheStr != null)
|
if (cacheStr != null)
|
||||||
cache = Json.decodeFromString(cacheStr);
|
cache = Json.decodeFromString(cacheStr);
|
||||||
}
|
} catch (ex: Throwable) {
|
||||||
catch(ex: Throwable) {
|
|
||||||
Logger.e(TAG, "Failed to deserialize cache");
|
Logger.e(TAG, "Failed to deserialize cache");
|
||||||
}
|
}
|
||||||
recon = reconLines.joinToString("\n");
|
recon = reconLines.joinToString("\n");
|
||||||
@@ -820,19 +906,18 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
Logger.i(TAG, "Opened shared playlist reconstruction\n${recon}");
|
Logger.i(TAG, "Opened shared playlist reconstruction\n${recon}");
|
||||||
handleReconstruction(recon, cache);
|
handleReconstruction(recon, cache);
|
||||||
return true;
|
return true;
|
||||||
}
|
} else if (file.lowercase().endsWith(".zip")) {
|
||||||
else if(file.lowercase().endsWith(".zip")) {
|
|
||||||
StateBackup.importZipBytes(this, lifecycleScope, readSharedFile(file));
|
StateBackup.importZipBytes(this, lifecycleScope, readSharedFile(file));
|
||||||
return true;
|
return true;
|
||||||
}
|
} else if (file.lowercase().endsWith(".txt")) {
|
||||||
else if(file.lowercase().endsWith(".txt")) {
|
|
||||||
return handleUnknownText(String(readSharedFile(file)));
|
return handleUnknownText(String(readSharedFile(file)));
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleReconstruction(recon: String, cache: ImportCache? = null) {
|
fun handleReconstruction(recon: String, cache: ImportCache? = null) {
|
||||||
val type = ManagedStore.getReconstructionIdentifier(recon);
|
val type = ManagedStore.getReconstructionIdentifier(recon);
|
||||||
val store: ManagedStore<*> = when(type) {
|
val store: ManagedStore<*> = when (type) {
|
||||||
"Playlist" -> StatePlaylists.instance.playlistStore
|
"Playlist" -> StatePlaylists.instance.playlistStore
|
||||||
else -> {
|
else -> {
|
||||||
UIDialogs.toast(getString(R.string.unknown_reconstruction_type) + " ${type}", false);
|
UIDialogs.toast(getString(R.string.unknown_reconstruction_type) + " ${type}", false);
|
||||||
@@ -840,13 +925,15 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
val name = when(type) {
|
val name = when (type) {
|
||||||
"Playlist" -> recon.split("\n").filter { !it.startsWith(ManagedStore.RECONSTRUCTION_HEADER_OPERATOR) }.firstOrNull() ?: type;
|
"Playlist" -> recon.split("\n")
|
||||||
|
.filter { !it.startsWith(ManagedStore.RECONSTRUCTION_HEADER_OPERATOR) }
|
||||||
|
.firstOrNull() ?: type;
|
||||||
else -> type
|
else -> type
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if(!type.isNullOrEmpty()) {
|
if (!type.isNullOrEmpty()) {
|
||||||
UIDialogs.showImportDialog(this, store, name, listOf(recon), cache) {
|
UIDialogs.showImportDialog(this, store, name, listOf(recon), cache) {
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -855,18 +942,18 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
|
|
||||||
fun handleUnknownText(text: String): Boolean {
|
fun handleUnknownText(text: String): Boolean {
|
||||||
try {
|
try {
|
||||||
if(text.startsWith("@/Subscription") || text.startsWith("Subscriptions")) {
|
if (text.startsWith("@/Subscription") || text.startsWith("Subscriptions")) {
|
||||||
val lines = text.split("\n").map { it.trim() }.drop(1).filter { it.isNotEmpty() };
|
val lines = text.split("\n").map { it.trim() }.drop(1).filter { it.isNotEmpty() };
|
||||||
navigate(_fragImportSubscriptions, lines);
|
navigate(_fragImportSubscriptions, lines);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
} catch (ex: Throwable) {
|
||||||
catch(ex: Throwable) {
|
|
||||||
Logger.e(TAG, ex.message, ex);
|
Logger.e(TAG, ex.message, ex);
|
||||||
UIDialogs.showGeneralErrorDialog(this, getString(R.string.failed_to_parse_text_file), ex);
|
UIDialogs.showGeneralErrorDialog(this, getString(R.string.failed_to_parse_text_file), ex);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleUnknownJson(json: String): Boolean {
|
fun handleUnknownJson(json: String): Boolean {
|
||||||
|
|
||||||
val context = this;
|
val context = this;
|
||||||
@@ -878,8 +965,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
return false;//throw IllegalArgumentException("Invalid NewPipe json structure found");
|
return false;//throw IllegalArgumentException("Invalid NewPipe json structure found");
|
||||||
|
|
||||||
StateBackup.importNewPipeSubs(this, newPipeSubsParsed);
|
StateBackup.importNewPipeSubs(this, newPipeSubsParsed);
|
||||||
}
|
} catch (ex: Exception) {
|
||||||
catch(ex: Exception) {
|
|
||||||
Logger.e(TAG, ex.message, ex);
|
Logger.e(TAG, ex.message, ex);
|
||||||
UIDialogs.showGeneralErrorDialog(context, getString(R.string.failed_to_parse_newpipe_subscriptions), ex);
|
UIDialogs.showGeneralErrorDialog(context, getString(R.string.failed_to_parse_newpipe_subscriptions), ex);
|
||||||
}
|
}
|
||||||
@@ -925,7 +1011,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
|
|
||||||
private fun readSharedFile(filePath: String): ByteArray {
|
private fun readSharedFile(filePath: String): ByteArray {
|
||||||
val dataFile = File(filePath);
|
val dataFile = File(filePath);
|
||||||
if(!dataFile.exists())
|
if (!dataFile.exists())
|
||||||
throw IllegalArgumentException("Opened file does not exist or not permitted");
|
throw IllegalArgumentException("Opened file does not exist or not permitted");
|
||||||
val data = dataFile.readBytes();
|
val data = dataFile.readBytes();
|
||||||
return data;
|
return data;
|
||||||
@@ -934,13 +1020,13 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
Logger.i(TAG, "onBackPressed")
|
Logger.i(TAG, "onBackPressed")
|
||||||
|
|
||||||
if(_fragBotBarMenu.onBackPressed())
|
if (_fragBotBarMenu.onBackPressed())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if(_fragVideoDetail.state == VideoDetailFragment.State.MAXIMIZED && _fragVideoDetail.onBackPressed())
|
if (_fragVideoDetail.state == VideoDetailFragment.State.MAXIMIZED && _fragVideoDetail.onBackPressed())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if(!fragCurrent.onBackPressed())
|
if (!fragCurrent.onBackPressed())
|
||||||
closeSegment();
|
closeSegment();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -948,7 +1034,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
super.onUserLeaveHint();
|
super.onUserLeaveHint();
|
||||||
Logger.i(TAG, "onUserLeaveHint")
|
Logger.i(TAG, "onUserLeaveHint")
|
||||||
|
|
||||||
if(_fragVideoDetail.state == VideoDetailFragment.State.MAXIMIZED || _fragVideoDetail.state == VideoDetailFragment.State.MINIMIZED)
|
if (_fragVideoDetail.state == VideoDetailFragment.State.MAXIMIZED || _fragVideoDetail.state == VideoDetailFragment.State.MINIMIZED)
|
||||||
_fragVideoDetail.onUserLeaveHint();
|
_fragVideoDetail.onUserLeaveHint();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -984,12 +1070,12 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
fun navigate(segment: MainFragment, parameter: Any? = null, withHistory: Boolean = true, isBack: Boolean = false) {
|
fun navigate(segment: MainFragment, parameter: Any? = null, withHistory: Boolean = true, isBack: Boolean = false) {
|
||||||
Logger.i(TAG, "Navigate to $segment (parameter=$parameter, withHistory=$withHistory, isBack=$isBack)")
|
Logger.i(TAG, "Navigate to $segment (parameter=$parameter, withHistory=$withHistory, isBack=$isBack)")
|
||||||
|
|
||||||
if(segment != fragCurrent) {
|
if (segment != fragCurrent) {
|
||||||
|
|
||||||
if(segment is VideoDetailFragment) {
|
if (segment is VideoDetailFragment) {
|
||||||
if(_fragContainerVideoDetail.visibility != View.VISIBLE)
|
if (_fragContainerVideoDetail.visibility != View.VISIBLE)
|
||||||
_fragContainerVideoDetail.visibility = View.VISIBLE;
|
_fragContainerVideoDetail.visibility = View.VISIBLE;
|
||||||
when(segment.state) {
|
when (segment.state) {
|
||||||
VideoDetailFragment.State.MINIMIZED -> segment.maximizeVideoDetail()
|
VideoDetailFragment.State.MINIMIZED -> segment.maximizeVideoDetail()
|
||||||
VideoDetailFragment.State.CLOSED -> segment.maximizeVideoDetail()
|
VideoDetailFragment.State.CLOSED -> segment.maximizeVideoDetail()
|
||||||
else -> {}
|
else -> {}
|
||||||
@@ -997,11 +1083,10 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
segment.onShown(parameter, isBack);
|
segment.onShown(parameter, isBack);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fragCurrent.onHide();
|
fragCurrent.onHide();
|
||||||
|
|
||||||
if(segment.isMainView) {
|
if (segment.isMainView) {
|
||||||
var transaction = supportFragmentManager.beginTransaction();
|
var transaction = supportFragmentManager.beginTransaction();
|
||||||
if (segment.topBar != null) {
|
if (segment.topBar != null) {
|
||||||
if (segment.topBar != fragCurrent.topBar) {
|
if (segment.topBar != fragCurrent.topBar) {
|
||||||
@@ -1010,8 +1095,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
.replace(R.id.fragment_top_bar, segment.topBar as Fragment);
|
.replace(R.id.fragment_top_bar, segment.topBar as Fragment);
|
||||||
fragCurrent.topBar?.onHide();
|
fragCurrent.topBar?.onHide();
|
||||||
}
|
}
|
||||||
}
|
} else if (fragCurrent.topBar != null)
|
||||||
else if(fragCurrent.topBar != null)
|
|
||||||
transaction.hide(fragCurrent.topBar as Fragment);
|
transaction.hide(fragCurrent.topBar as Fragment);
|
||||||
|
|
||||||
transaction = transaction.replace(R.id.fragment_main, segment);
|
transaction = transaction.replace(R.id.fragment_main, segment);
|
||||||
@@ -1019,25 +1103,24 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
if (segment.hasBottomBar) {
|
if (segment.hasBottomBar) {
|
||||||
if (!fragCurrent.hasBottomBar)
|
if (!fragCurrent.hasBottomBar)
|
||||||
transaction = transaction.show(_fragBotBarMenu);
|
transaction = transaction.show(_fragBotBarMenu);
|
||||||
}
|
} else {
|
||||||
else {
|
if (fragCurrent.hasBottomBar)
|
||||||
if(fragCurrent.hasBottomBar)
|
|
||||||
transaction = transaction.hide(_fragBotBarMenu);
|
transaction = transaction.hide(_fragBotBarMenu);
|
||||||
}
|
}
|
||||||
transaction.commitNow();
|
transaction.commitNow();
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
if(!segment.hasBottomBar) {
|
if (!segment.hasBottomBar) {
|
||||||
supportFragmentManager.beginTransaction()
|
supportFragmentManager.beginTransaction()
|
||||||
.hide(_fragBotBarMenu)
|
.hide(_fragBotBarMenu)
|
||||||
.commitNow();
|
.commitNow();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(fragCurrent.isHistory && withHistory && _queue.lastOrNull() != fragCurrent)
|
if (fragCurrent.isHistory && withHistory && _queue.lastOrNull() != fragCurrent)
|
||||||
_queue.add(Pair(fragCurrent, _parameterCurrent));
|
_queue.add(Pair(fragCurrent, _parameterCurrent));
|
||||||
|
|
||||||
if(segment.isOverlay && !fragCurrent.isOverlay && withHistory)// && fragCurrent.isHistory)
|
if (segment.isOverlay && !fragCurrent.isOverlay && withHistory)// && fragCurrent.isHistory)
|
||||||
fragBeforeOverlay = fragCurrent;
|
fragBeforeOverlay = fragCurrent;
|
||||||
|
|
||||||
|
|
||||||
@@ -1055,12 +1138,12 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
* If called with a non-null fragment, it will only close if the current fragment is the provided one
|
* If called with a non-null fragment, it will only close if the current fragment is the provided one
|
||||||
*/
|
*/
|
||||||
fun closeSegment(fragment: MainFragment? = null) {
|
fun closeSegment(fragment: MainFragment? = null) {
|
||||||
if(fragment is VideoDetailFragment) {
|
if (fragment is VideoDetailFragment) {
|
||||||
fragment.onHide();
|
fragment.onHide();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if((fragment?.isOverlay ?: false) && fragBeforeOverlay != null) {
|
if ((fragment?.isOverlay ?: false) && fragBeforeOverlay != null) {
|
||||||
navigate(fragBeforeOverlay!!, null, false, true);
|
navigate(fragBeforeOverlay!!, null, false, true);
|
||||||
} else {
|
} else {
|
||||||
val last = _queue.lastOrNull();
|
val last = _queue.lastOrNull();
|
||||||
@@ -1082,8 +1165,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
/**
|
/**
|
||||||
* Provides the fragment instance for the provided fragment class
|
* Provides the fragment instance for the provided fragment class
|
||||||
*/
|
*/
|
||||||
inline fun <reified T : Fragment> getFragment() : T {
|
inline fun <reified T : Fragment> getFragment(): T {
|
||||||
return when(T::class) {
|
return when (T::class) {
|
||||||
HomeFragment::class -> _fragMainHome as T;
|
HomeFragment::class -> _fragMainHome as T;
|
||||||
TutorialFragment::class -> _fragMainTutorial as T;
|
TutorialFragment::class -> _fragMainTutorial as T;
|
||||||
ContentSearchResultsFragment::class -> _fragMainVideoSearchResults as T;
|
ContentSearchResultsFragment::class -> _fragMainVideoSearchResults as T;
|
||||||
@@ -1120,15 +1203,21 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
|
|
||||||
private fun updateSegmentPaddings() {
|
private fun updateSegmentPaddings() {
|
||||||
var paddingBottom = 0f;
|
var paddingBottom = 0f;
|
||||||
if(fragCurrent.hasBottomBar)
|
if (fragCurrent.hasBottomBar)
|
||||||
paddingBottom += HEIGHT_MENU_DP;
|
paddingBottom += HEIGHT_MENU_DP;
|
||||||
|
|
||||||
_fragContainerOverlay.setPadding(0,0,0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, paddingBottom - HEIGHT_MENU_DP, resources.displayMetrics).toInt());
|
_fragContainerOverlay.setPadding(
|
||||||
|
0, 0, 0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, paddingBottom - HEIGHT_MENU_DP, resources.displayMetrics)
|
||||||
|
.toInt()
|
||||||
|
);
|
||||||
|
|
||||||
if(_fragVideoDetail.state == VideoDetailFragment.State.MINIMIZED)
|
if (_fragVideoDetail.state == VideoDetailFragment.State.MINIMIZED)
|
||||||
paddingBottom += HEIGHT_VIDEO_MINIMIZED_DP;
|
paddingBottom += HEIGHT_VIDEO_MINIMIZED_DP;
|
||||||
|
|
||||||
_fragContainerMain.setPadding(0,0,0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, paddingBottom, resources.displayMetrics).toInt());
|
_fragContainerMain.setPadding(
|
||||||
|
0, 0, 0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, paddingBottom, resources.displayMetrics)
|
||||||
|
.toInt()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1144,14 +1233,18 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
ContextCompat.checkSelfPermission(this, notifPermission) == PackageManager.PERMISSION_GRANTED -> {
|
ContextCompat.checkSelfPermission(this, notifPermission) == PackageManager.PERMISSION_GRANTED -> {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ActivityCompat.shouldShowRequestPermissionRationale(this, notifPermission) -> {
|
ActivityCompat.shouldShowRequestPermissionRationale(this, notifPermission) -> {
|
||||||
UIDialogs.showDialog(this, R.drawable.ic_notifications, "Notifications Required",
|
UIDialogs.showDialog(
|
||||||
|
this, R.drawable.ic_notifications, "Notifications Required",
|
||||||
reason, null, 0,
|
reason, null, 0,
|
||||||
UIDialogs.Action("Cancel", {}),
|
UIDialogs.Action("Cancel", {}),
|
||||||
UIDialogs.Action("Enable", {
|
UIDialogs.Action("Enable", {
|
||||||
requestPermissionLauncher.launch(notifPermission);
|
requestPermissionLauncher.launch(notifPermission);
|
||||||
}, UIDialogs.ActionStyle.PRIMARY));
|
}, UIDialogs.ActionStyle.PRIMARY)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
requestPermissionLauncher.launch(notifPermission);
|
requestPermissionLauncher.launch(notifPermission);
|
||||||
}
|
}
|
||||||
@@ -1163,15 +1256,16 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
fun showAppToast(toast: ToastView.Toast) {
|
fun showAppToast(toast: ToastView.Toast) {
|
||||||
synchronized(_toastQueue) {
|
synchronized(_toastQueue) {
|
||||||
_toastQueue.add(toast);
|
_toastQueue.add(toast);
|
||||||
if(_toastJob?.isActive != true)
|
if (_toastJob?.isActive != true)
|
||||||
_toastJob = lifecycleScope.launch(Dispatchers.Default) {
|
_toastJob = lifecycleScope.launch(Dispatchers.Default) {
|
||||||
launchAppToastJob();
|
launchAppToastJob();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun launchAppToastJob() {
|
private suspend fun launchAppToastJob() {
|
||||||
Logger.i(TAG, "Starting appToast loop");
|
Logger.i(TAG, "Starting appToast loop");
|
||||||
while(!_toastQueue.isEmpty()) {
|
while (!_toastQueue.isEmpty()) {
|
||||||
val toast = _toastQueue.poll() ?: continue;
|
val toast = _toastQueue.poll() ?: continue;
|
||||||
Logger.i(TAG, "Showing next toast (${toast.msg})");
|
Logger.i(TAG, "Showing next toast (${toast.msg})");
|
||||||
|
|
||||||
@@ -1184,10 +1278,10 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
_toastView.setToastAnimated(toast);
|
_toastView.setToastAnimated(toast);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(toast.long)
|
if (toast.long)
|
||||||
delay(5000);
|
delay(5000);
|
||||||
else
|
else
|
||||||
delay(3000);
|
delay(2500);
|
||||||
}
|
}
|
||||||
Logger.i(TAG, "Ending appToast loop");
|
Logger.i(TAG, "Ending appToast loop");
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
@@ -1198,18 +1292,19 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
|
|
||||||
|
|
||||||
//TODO: Only calls last handler due to missing request codes on ActivityResultLaunchers.
|
//TODO: Only calls last handler due to missing request codes on ActivityResultLaunchers.
|
||||||
private var resultLauncherMap = mutableMapOf<Int, (ActivityResult)->Unit>();
|
private var resultLauncherMap = mutableMapOf<Int, (ActivityResult) -> Unit>();
|
||||||
private var requestCode: Int? = -1;
|
private var requestCode: Int? = -1;
|
||||||
private val resultLauncher: ActivityResultLauncher<Intent> = registerForActivityResult(
|
private val resultLauncher: ActivityResultLauncher<Intent> = registerForActivityResult(
|
||||||
ActivityResultContracts.StartActivityForResult()) {
|
ActivityResultContracts.StartActivityForResult()
|
||||||
result: ActivityResult ->
|
) { result: ActivityResult ->
|
||||||
val handler = synchronized(resultLauncherMap) {
|
val handler = synchronized(resultLauncherMap) {
|
||||||
resultLauncherMap.remove(requestCode);
|
resultLauncherMap.remove(requestCode);
|
||||||
}
|
}
|
||||||
if(handler != null)
|
if (handler != null)
|
||||||
handler(result);
|
handler(result);
|
||||||
};
|
};
|
||||||
override fun launchForResult(intent: Intent, code: Int, handler: (ActivityResult)->Unit) {
|
|
||||||
|
override fun launchForResult(intent: Intent, code: Int, handler: (ActivityResult) -> Unit) {
|
||||||
synchronized(resultLauncherMap) {
|
synchronized(resultLauncherMap) {
|
||||||
resultLauncherMap[code] = handler;
|
resultLauncherMap[code] = handler;
|
||||||
}
|
}
|
||||||
@@ -1220,32 +1315,34 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
companion object {
|
companion object {
|
||||||
private val TAG = "MainActivity"
|
private val TAG = "MainActivity"
|
||||||
|
|
||||||
fun getTabIntent(context: Context, tab: String) : Intent {
|
fun getTabIntent(context: Context, tab: String): Intent {
|
||||||
val sourcesIntent = Intent(context, MainActivity::class.java);
|
val sourcesIntent = Intent(context, MainActivity::class.java);
|
||||||
sourcesIntent.action = "TAB";
|
sourcesIntent.action = "TAB";
|
||||||
sourcesIntent.putExtra("TAB", tab);
|
sourcesIntent.putExtra("TAB", tab);
|
||||||
sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||||
return sourcesIntent;
|
return sourcesIntent;
|
||||||
}
|
}
|
||||||
fun getVideoIntent(context: Context, videoUrl: String) : Intent {
|
|
||||||
|
fun getVideoIntent(context: Context, videoUrl: String): Intent {
|
||||||
val sourcesIntent = Intent(context, MainActivity::class.java);
|
val sourcesIntent = Intent(context, MainActivity::class.java);
|
||||||
sourcesIntent.action = "VIDEO";
|
sourcesIntent.action = "VIDEO";
|
||||||
sourcesIntent.putExtra("VIDEO", videoUrl);
|
sourcesIntent.putExtra("VIDEO", videoUrl);
|
||||||
sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||||
return sourcesIntent;
|
return sourcesIntent;
|
||||||
}
|
}
|
||||||
fun getActionIntent(context: Context, action: String) : Intent {
|
|
||||||
|
fun getActionIntent(context: Context, action: String): Intent {
|
||||||
val sourcesIntent = Intent(context, MainActivity::class.java);
|
val sourcesIntent = Intent(context, MainActivity::class.java);
|
||||||
sourcesIntent.action = "ACTION";
|
sourcesIntent.action = "ACTION";
|
||||||
sourcesIntent.putExtra("ACTION", action);
|
sourcesIntent.putExtra("ACTION", action);
|
||||||
sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||||
return sourcesIntent;
|
return sourcesIntent;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getImportOptionsIntent(context: Context): Intent {
|
fun getImportOptionsIntent(context: Context): Intent {
|
||||||
val sourcesIntent = Intent(context, MainActivity::class.java);
|
val sourcesIntent = Intent(context, MainActivity::class.java);
|
||||||
sourcesIntent.action = "IMPORT_OPTIONS";
|
sourcesIntent.action = "IMPORT_OPTIONS";
|
||||||
sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||||
return sourcesIntent;
|
return sourcesIntent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+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();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,148 @@
|
|||||||
|
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
|
||||||
|
if(e.message == "Failed to connect") {
|
||||||
|
_textError.text = "Failed to connect.\n\nThis may be due to not being on the same network, due to firewall, or vpn.\nSync currently operates only over local direct connections."
|
||||||
|
}
|
||||||
|
else
|
||||||
|
_textError.text = e.message
|
||||||
|
_layoutPairing.visibility = View.GONE
|
||||||
|
Logger.e(TAG, "Failed to pair", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(e: Throwable) {
|
||||||
|
_layoutPairingError.visibility = View.VISIBLE
|
||||||
|
_textError.text = e.message
|
||||||
|
_layoutPairing.visibility = View.GONE
|
||||||
|
Logger.e(TAG, "Failed to pair", e)
|
||||||
|
} 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,8 @@ import com.futo.platformplayer.SettingsDev
|
|||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.ensureNotMainThread
|
import com.futo.platformplayer.ensureNotMainThread
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
import okhttp3.Call
|
import okhttp3.Call
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
@@ -63,7 +65,7 @@ open class ManagedHttpClient {
|
|||||||
|
|
||||||
constructor(builder: OkHttpClient.Builder = OkHttpClient.Builder()) {
|
constructor(builder: OkHttpClient.Builder = OkHttpClient.Builder()) {
|
||||||
_builderTemplate = builder;
|
_builderTemplate = builder;
|
||||||
if(SettingsDev.instance.developerMode && SettingsDev.instance.networking.allowAllCertificates)
|
if(FragmentedStorage.isInitialized && StateApp.instance.isMainActive && SettingsDev.instance.developerMode && SettingsDev.instance.networking.allowAllCertificates)
|
||||||
trustAllCertificates(builder);
|
trustAllCertificates(builder);
|
||||||
client = builder.addNetworkInterceptor { chain ->
|
client = builder.addNetworkInterceptor { chain ->
|
||||||
val request = beforeRequest(chain.request());
|
val request = beforeRequest(chain.request());
|
||||||
|
|||||||
+1
-1
@@ -73,7 +73,7 @@ class HttpFileHandler(method: String, path: String, private val contentType: Str
|
|||||||
Logger.v(TAG, "Sent bytes $current-${current + bytesToSend}, totalBytesSent=$totalBytesSent")
|
Logger.v(TAG, "Sent bytes $current-${current + bytesToSend}, totalBytesSent=$totalBytesSent")
|
||||||
|
|
||||||
current += bytesToSend.toLong()
|
current += bytesToSend.toLong()
|
||||||
if (current >= end) {
|
if (current > end) {
|
||||||
Logger.i(TAG, "Expected amount of bytes sent")
|
Logger.i(TAG, "Expected amount of bytes sent")
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class ResultCapabilities(
|
|||||||
const val TYPE_POSTS = "POSTS";
|
const val TYPE_POSTS = "POSTS";
|
||||||
const val TYPE_MIXED = "MIXED";
|
const val TYPE_MIXED = "MIXED";
|
||||||
const val TYPE_SUBSCRIPTIONS = "SUBSCRIPTIONS";
|
const val TYPE_SUBSCRIPTIONS = "SUBSCRIPTIONS";
|
||||||
|
const val TYPE_SHORTS = "SHORTS";
|
||||||
|
|
||||||
const val ORDER_CHONOLOGICAL = "CHRONOLOGICAL";
|
const val ORDER_CHONOLOGICAL = "CHRONOLOGICAL";
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+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
@@ -33,13 +33,13 @@ class LocalAudioSource : IAudioSource, IStreamMetaDataSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromSource(source: IAudioSource, path: String, fileSize: Long): LocalAudioSource {
|
fun fromSource(source: IAudioSource, path: String, fileSize: Long, overrideContainer: String? = null): LocalAudioSource {
|
||||||
return LocalAudioSource(
|
return LocalAudioSource(
|
||||||
source.name,
|
source.name,
|
||||||
path,
|
path,
|
||||||
fileSize,
|
fileSize,
|
||||||
source.bitrate,
|
source.bitrate,
|
||||||
source.container,
|
overrideContainer ?: source.container,
|
||||||
source.codec,
|
source.codec,
|
||||||
source.language
|
source.language
|
||||||
);
|
);
|
||||||
|
|||||||
+2
-2
@@ -35,7 +35,7 @@ class LocalVideoSource : IVideoSource, IStreamMetaDataSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromSource(source: IVideoSource, path: String, fileSize: Long): LocalVideoSource {
|
fun fromSource(source: IVideoSource, path: String, fileSize: Long, overrideContainer: String? = null): LocalVideoSource {
|
||||||
return LocalVideoSource(
|
return LocalVideoSource(
|
||||||
source.name,
|
source.name,
|
||||||
path,
|
path,
|
||||||
@@ -43,7 +43,7 @@ class LocalVideoSource : IVideoSource, IStreamMetaDataSource {
|
|||||||
source.width,
|
source.width,
|
||||||
source.height,
|
source.height,
|
||||||
source.duration,
|
source.duration,
|
||||||
source.container,
|
overrideContainer ?: source.container,
|
||||||
source.codec,
|
source.codec,
|
||||||
source.bitrate?:0
|
source.bitrate?:0
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,4 +13,6 @@ interface IPlatformVideo : IPlatformContent {
|
|||||||
val viewCount: Long;
|
val viewCount: Long;
|
||||||
|
|
||||||
val isLive : Boolean;
|
val isLive : Boolean;
|
||||||
|
|
||||||
|
val isShort: Boolean;
|
||||||
}
|
}
|
||||||
+3
-2
@@ -19,12 +19,13 @@ 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,
|
||||||
|
override val isShort: Boolean = false
|
||||||
) : IPlatformVideo, SerializedPlatformContent {
|
) : IPlatformVideo, SerializedPlatformContent {
|
||||||
override val contentType: ContentType = ContentType.MEDIA;
|
override val contentType: ContentType = ContentType.MEDIA;
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -38,7 +38,8 @@ open class SerializedPlatformVideoDetails(
|
|||||||
override val video: ISerializedVideoSourceDescriptor,
|
override val video: ISerializedVideoSourceDescriptor,
|
||||||
override val preview: ISerializedVideoSourceDescriptor?,
|
override val preview: ISerializedVideoSourceDescriptor?,
|
||||||
|
|
||||||
override val subtitles: List<SubtitleRawSource> = listOf()
|
override val subtitles: List<SubtitleRawSource> = listOf(),
|
||||||
|
override val isShort: Boolean = false
|
||||||
) : IPlatformVideo, IPlatformVideoDetails {
|
) : IPlatformVideo, IPlatformVideoDetails {
|
||||||
final override val contentType: ContentType get() = ContentType.MEDIA;
|
final override val contentType: ContentType get() = ContentType.MEDIA;
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
+22
-2
@@ -33,6 +33,7 @@ class SourcePluginConfig(
|
|||||||
override val allowEval: Boolean = false,
|
override val allowEval: Boolean = false,
|
||||||
override val allowUrls: List<String> = listOf(),
|
override val allowUrls: List<String> = listOf(),
|
||||||
override val packages: List<String> = listOf(),
|
override val packages: List<String> = listOf(),
|
||||||
|
override val packagesOptional: List<String> = listOf(),
|
||||||
|
|
||||||
val settings: List<Setting> = listOf(),
|
val settings: List<Setting> = listOf(),
|
||||||
|
|
||||||
@@ -50,7 +51,9 @@ 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 maxDownloadParallelism: Int = 0,
|
||||||
|
var reduceFunctionsInLimitedVersion: Boolean = false,
|
||||||
|
var changelog: HashMap<String, List<String>>? = null
|
||||||
) : IV8PluginConfig {
|
) : IV8PluginConfig {
|
||||||
|
|
||||||
val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl);
|
val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl);
|
||||||
@@ -100,6 +103,10 @@ class SourcePluginConfig(
|
|||||||
if(!packages.contains(pack))
|
if(!packages.contains(pack))
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
for(pack in newConfig.packagesOptional) {
|
||||||
|
if(!packagesOptional.contains(pack))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
//Developer Submit Url should be same or empty
|
//Developer Submit Url should be same or empty
|
||||||
if(!newConfig.developerSubmitUrl.isNullOrEmpty() && developerSubmitUrl != newConfig.developerSubmitUrl)
|
if(!newConfig.developerSubmitUrl.isNullOrEmpty() && developerSubmitUrl != newConfig.developerSubmitUrl)
|
||||||
return false;
|
return false;
|
||||||
@@ -128,7 +135,7 @@ class SourcePluginConfig(
|
|||||||
|
|
||||||
val currentlyInstalledPlugin = StatePlugins.instance.getPlugin(id);
|
val currentlyInstalledPlugin = StatePlugins.instance.getPlugin(id);
|
||||||
if (currentlyInstalledPlugin != null) {
|
if (currentlyInstalledPlugin != null) {
|
||||||
if (currentlyInstalledPlugin.config.scriptPublicKey != scriptPublicKey) {
|
if (currentlyInstalledPlugin.config.scriptPublicKey != scriptPublicKey && !currentlyInstalledPlugin.config.scriptPublicKey.isNullOrEmpty()) {
|
||||||
list.add(Pair(
|
list.add(Pair(
|
||||||
"Different Author",
|
"Different Author",
|
||||||
"This plugin was signed by a different author. Please ensure that this is correct and that the plugin was not provided by a malicious actor."));
|
"This plugin was signed by a different author. Please ensure that this is correct and that the plugin was not provided by a malicious actor."));
|
||||||
@@ -177,6 +184,19 @@ class SourcePluginConfig(
|
|||||||
return _allowUrlsLower.any { it == host || (it.length > 0 && it[0] == '.' && host.matchesDomain(it)) };
|
return _allowUrlsLower.any { it == host || (it.length > 0 && it[0] == '.' && host.matchesDomain(it)) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getChangelogString(version: String): String?{
|
||||||
|
if(changelog == null || !changelog!!.containsKey(version))
|
||||||
|
return null;
|
||||||
|
val changelog = changelog!![version]!!;
|
||||||
|
if(changelog.size > 1) {
|
||||||
|
return "Changelog (${version})\n" + changelog.map { " - " + it.trim() }.joinToString("\n");
|
||||||
|
}
|
||||||
|
else if(changelog.size == 1) {
|
||||||
|
return "Changelog (${version})\n" + changelog[0].trim();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
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);
|
||||||
|
|||||||
+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}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
package com.futo.platformplayer.api.media.platforms.js.models
|
|
||||||
|
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
|
||||||
import com.futo.platformplayer.api.media.IPluginSourced
|
|
||||||
import com.futo.platformplayer.api.media.models.Thumbnails
|
|
||||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
|
||||||
import com.futo.platformplayer.api.media.models.post.TextType
|
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
|
||||||
import com.futo.platformplayer.getOrDefault
|
|
||||||
import com.futo.platformplayer.getOrThrow
|
|
||||||
import com.futo.platformplayer.getOrThrowNullableList
|
|
||||||
import kotlin.streams.toList
|
|
||||||
|
|
||||||
open class JSArticle : JSContent, IPluginSourced {
|
|
||||||
final override val contentType: ContentType get() = ContentType.POST;
|
|
||||||
|
|
||||||
val summary: String;
|
|
||||||
val thumbnails: Thumbnails?;
|
|
||||||
val segments: List<IJSArticleSegment>;
|
|
||||||
|
|
||||||
constructor(config: SourcePluginConfig, obj: V8ValueObject): super(config, obj) {
|
|
||||||
val contextName = "PlatformPost";
|
|
||||||
|
|
||||||
summary = _content.getOrThrow(config, "summary", contextName);
|
|
||||||
if(_content.has("thumbnails"))
|
|
||||||
thumbnails = Thumbnails.fromV8(config, _content.getOrThrow(config, "thumbnails", contextName));
|
|
||||||
else
|
|
||||||
thumbnails = null;
|
|
||||||
|
|
||||||
|
|
||||||
segments = (obj.getOrThrowNullableList<V8ValueObject>(config, "segments", contextName)
|
|
||||||
?.map { fromV8Segment(config, it) }
|
|
||||||
?.filterNotNull() ?: listOf());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun fromV8Segment(config: SourcePluginConfig, obj: V8ValueObject): IJSArticleSegment? {
|
|
||||||
if(!obj.has("type"))
|
|
||||||
throw IllegalArgumentException("Object missing type field");
|
|
||||||
return when(obj.getOrThrow<SegmentType>(config, "type", "JSArticle.Segment")) {
|
|
||||||
SegmentType.TEXT -> JSTextSegment(config, obj);
|
|
||||||
SegmentType.IMAGES -> JSImagesSegment(config, obj);
|
|
||||||
else -> null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class SegmentType(i: Int) {
|
|
||||||
UNKNOWN(0),
|
|
||||||
TEXT(1),
|
|
||||||
IMAGES(2)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IJSArticleSegment {
|
|
||||||
val type: SegmentType;
|
|
||||||
}
|
|
||||||
class JSTextSegment: IJSArticleSegment {
|
|
||||||
override val type = SegmentType.TEXT;
|
|
||||||
val textType: TextType;
|
|
||||||
val content: String;
|
|
||||||
|
|
||||||
constructor(config: SourcePluginConfig, obj: V8ValueObject) {
|
|
||||||
val contextName = "JSTextSegment";
|
|
||||||
textType = TextType.fromInt((obj.getOrDefault<Int>(config, "textType", contextName, null) ?: 0));
|
|
||||||
content = obj.getOrDefault(config, "content", contextName, "") ?: "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
class JSImagesSegment: IJSArticleSegment {
|
|
||||||
override val type = SegmentType.IMAGES;
|
|
||||||
val images: List<String>;
|
|
||||||
|
|
||||||
constructor(config: SourcePluginConfig, obj: V8ValueObject) {
|
|
||||||
val contextName = "JSTextSegment";
|
|
||||||
images = obj.getOrThrowNullableList<String>(config, "images", contextName) ?: listOf();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
-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())
|
||||||
|
|||||||
@@ -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)});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+3
-3
@@ -42,7 +42,7 @@ class JSRequestExecutor {
|
|||||||
|
|
||||||
//TODO: Executor properties?
|
//TODO: Executor properties?
|
||||||
@Throws(ScriptException::class)
|
@Throws(ScriptException::class)
|
||||||
open fun executeRequest(url: String, headers: Map<String, String>): ByteArray {
|
open fun executeRequest(method: String, url: String, body: ByteArray?, headers: Map<String, String>): ByteArray {
|
||||||
if (_executor.isClosed)
|
if (_executor.isClosed)
|
||||||
throw IllegalStateException("Executor object is closed");
|
throw IllegalStateException("Executor object is closed");
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ class JSRequestExecutor {
|
|||||||
"[${_config.name}] JSRequestExecutor",
|
"[${_config.name}] JSRequestExecutor",
|
||||||
"builder.modifyRequest()"
|
"builder.modifyRequest()"
|
||||||
) {
|
) {
|
||||||
_executor.invoke("executeRequest", url, headers);
|
_executor.invoke("executeRequest", url, headers, method, body);
|
||||||
} as V8Value;
|
} as V8Value;
|
||||||
}
|
}
|
||||||
else V8Plugin.catchScriptErrors<Any>(
|
else V8Plugin.catchScriptErrors<Any>(
|
||||||
@@ -61,7 +61,7 @@ class JSRequestExecutor {
|
|||||||
"[${_config.name}] JSRequestExecutor",
|
"[${_config.name}] JSRequestExecutor",
|
||||||
"builder.modifyRequest()"
|
"builder.modifyRequest()"
|
||||||
) {
|
) {
|
||||||
_executor.invoke("executeRequest", url, headers);
|
_executor.invoke("executeRequest", url, headers, method, body);
|
||||||
} as V8Value;
|
} as V8Value;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import com.futo.platformplayer.api.media.models.Thumbnails
|
|||||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
|
import com.futo.platformplayer.getOrDefault
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
|
||||||
open class JSVideo : JSContent, IPlatformVideo, IPluginSourced {
|
open class JSVideo : JSContent, IPlatformVideo, IPluginSourced {
|
||||||
@@ -17,6 +18,7 @@ open class JSVideo : JSContent, IPlatformVideo, IPluginSourced {
|
|||||||
final override val viewCount: Long;
|
final override val viewCount: Long;
|
||||||
|
|
||||||
final override val isLive: Boolean;
|
final override val isLive: Boolean;
|
||||||
|
final override val isShort: Boolean;
|
||||||
|
|
||||||
constructor(config: SourcePluginConfig, obj: V8ValueObject): super(config, obj) {
|
constructor(config: SourcePluginConfig, obj: V8ValueObject): super(config, obj) {
|
||||||
val contextName = "PlatformVideo";
|
val contextName = "PlatformVideo";
|
||||||
@@ -26,5 +28,6 @@ open class JSVideo : JSContent, IPlatformVideo, IPluginSourced {
|
|||||||
duration = _content.getOrThrow<Int>(config, "duration", contextName).toLong();
|
duration = _content.getOrThrow<Int>(config, "duration", contextName).toLong();
|
||||||
viewCount = _content.getOrThrow(config, "viewCount", contextName);
|
viewCount = _content.getOrThrow(config, "viewCount", contextName);
|
||||||
isLive = _content.getOrThrow(config, "isLive", contextName);
|
isLive = _content.getOrThrow(config, "isLive", contextName);
|
||||||
|
isShort = _content.getOrDefault(config, "isShort", contextName, false) ?: false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+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)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-4
@@ -4,6 +4,8 @@ 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.IAudioSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
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.models.streams.sources.IVideoUrlSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.other.IStreamMetaDataSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.other.StreamMetaData
|
||||||
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
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.JSClient
|
||||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
@@ -14,8 +16,8 @@ import com.futo.platformplayer.getOrThrow
|
|||||||
import com.futo.platformplayer.others.Language
|
import com.futo.platformplayer.others.Language
|
||||||
import com.futo.platformplayer.states.StateDeveloper
|
import com.futo.platformplayer.states.StateDeveloper
|
||||||
|
|
||||||
class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawSource {
|
class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawSource, IStreamMetaDataSource {
|
||||||
override val container : String = "application/dash+xml";
|
override val container : String;
|
||||||
override val name : String;
|
override val name : String;
|
||||||
override val codec: String;
|
override val codec: String;
|
||||||
override val bitrate: Int;
|
override val bitrate: Int;
|
||||||
@@ -29,11 +31,14 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS
|
|||||||
|
|
||||||
override val hasGenerate: Boolean;
|
override val hasGenerate: Boolean;
|
||||||
|
|
||||||
|
override var streamMetaData: StreamMetaData? = null;
|
||||||
|
|
||||||
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH_RAW, plugin, obj) {
|
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH_RAW, plugin, obj) {
|
||||||
val contextName = "DashRawSource";
|
val contextName = "DashRawSource";
|
||||||
val config = plugin.config;
|
val config = plugin.config;
|
||||||
name = _obj.getOrThrow(config, "name", contextName);
|
name = _obj.getOrThrow(config, "name", contextName);
|
||||||
url = _obj.getOrThrow(config, "url", contextName);
|
url = _obj.getOrThrow(config, "url", contextName);
|
||||||
|
container = _obj.getOrDefault<String>(config, "container", contextName, null) ?: "application/dash+xml";
|
||||||
manifest = _obj.getOrThrow(config, "manifest", contextName);
|
manifest = _obj.getOrThrow(config, "manifest", contextName);
|
||||||
codec = _obj.getOrDefault(config, "codec", contextName, "") ?: "";
|
codec = _obj.getOrDefault(config, "codec", contextName, "") ?: "";
|
||||||
bitrate = _obj.getOrDefault(config, "bitrate", contextName, 0) ?: 0;
|
bitrate = _obj.getOrDefault(config, "bitrate", contextName, 0) ?: 0;
|
||||||
@@ -50,15 +55,28 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS
|
|||||||
throw IllegalStateException("Source object already closed");
|
throw IllegalStateException("Source object already closed");
|
||||||
|
|
||||||
val plugin = _plugin.getUnderlyingPlugin();
|
val plugin = _plugin.getUnderlyingPlugin();
|
||||||
|
|
||||||
|
var result: String? = null;
|
||||||
if(_plugin is DevJSClient)
|
if(_plugin is DevJSClient)
|
||||||
return StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRaw", false) {
|
result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRaw", false) {
|
||||||
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
|
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
|
||||||
_obj.invokeString("generate");
|
_obj.invokeString("generate");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
return _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
|
result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
|
||||||
_obj.invokeString("generate");
|
_obj.invokeString("generate");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(result != null){
|
||||||
|
val initStart = _obj.getOrDefault<Int>(_config, "initStart", "JSDashManifestRawSource", null) ?: 0;
|
||||||
|
val initEnd = _obj.getOrDefault<Int>(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0;
|
||||||
|
val indexStart = _obj.getOrDefault<Int>(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0;
|
||||||
|
val indexEnd = _obj.getOrDefault<Int>(_config, "indexEnd", "JSDashManifestRawSource", null) ?: 0;
|
||||||
|
if(initEnd > 0 && indexStart > 0 && indexEnd > 0) {
|
||||||
|
streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+28
-6
@@ -6,6 +6,8 @@ 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.IDashManifestSource
|
||||||
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
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.other.IStreamMetaDataSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.other.StreamMetaData
|
||||||
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
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.JSClient
|
||||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
@@ -20,8 +22,8 @@ interface IJSDashManifestRawSource {
|
|||||||
var manifest: String?;
|
var manifest: String?;
|
||||||
fun generate(): String?;
|
fun generate(): String?;
|
||||||
}
|
}
|
||||||
open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSource {
|
open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSource, IStreamMetaDataSource {
|
||||||
override val container : String = "application/dash+xml";
|
override val container : String;
|
||||||
override val name : String;
|
override val name : String;
|
||||||
override val width: Int;
|
override val width: Int;
|
||||||
override val height: Int;
|
override val height: Int;
|
||||||
@@ -36,11 +38,14 @@ open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSo
|
|||||||
override val hasGenerate: Boolean;
|
override val hasGenerate: Boolean;
|
||||||
val canMerge: Boolean;
|
val canMerge: Boolean;
|
||||||
|
|
||||||
|
override var streamMetaData: StreamMetaData? = null;
|
||||||
|
|
||||||
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH_RAW, plugin, obj) {
|
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH_RAW, plugin, obj) {
|
||||||
val contextName = "DashRawSource";
|
val contextName = "DashRawSource";
|
||||||
val config = plugin.config;
|
val config = plugin.config;
|
||||||
name = _obj.getOrThrow(config, "name", contextName);
|
name = _obj.getOrThrow(config, "name", contextName);
|
||||||
url = _obj.getOrThrow(config, "url", contextName);
|
url = _obj.getOrThrow(config, "url", contextName);
|
||||||
|
container = _obj.getOrDefault<String>(config, "container", contextName, null) ?: "application/dash+xml";
|
||||||
manifest = _obj.getOrDefault<String>(config, "manifest", contextName, null);
|
manifest = _obj.getOrDefault<String>(config, "manifest", contextName, null);
|
||||||
width = _obj.getOrDefault(config, "width", contextName, 0) ?: 0;
|
width = _obj.getOrDefault(config, "width", contextName, 0) ?: 0;
|
||||||
height = _obj.getOrDefault(config, "height", contextName, 0) ?: 0;
|
height = _obj.getOrDefault(config, "height", contextName, 0) ?: 0;
|
||||||
@@ -57,17 +62,30 @@ open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSo
|
|||||||
return manifest;
|
return manifest;
|
||||||
if(_obj.isClosed)
|
if(_obj.isClosed)
|
||||||
throw IllegalStateException("Source object already closed");
|
throw IllegalStateException("Source object already closed");
|
||||||
|
|
||||||
|
var result: String? = null;
|
||||||
if(_plugin is DevJSClient) {
|
if(_plugin is DevJSClient) {
|
||||||
return StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRawSource.generate()") {
|
result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRawSource.generate()") {
|
||||||
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
|
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
|
||||||
_obj.invokeString("generate");
|
_obj.invokeString("generate");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
return _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
|
result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
|
||||||
_obj.invokeString("generate");
|
_obj.invokeString("generate");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if(result != null){
|
||||||
|
val initStart = _obj.getOrDefault<Int>(_config, "initStart", "JSDashManifestRawSource", null) ?: 0;
|
||||||
|
val initEnd = _obj.getOrDefault<Int>(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0;
|
||||||
|
val indexStart = _obj.getOrDefault<Int>(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0;
|
||||||
|
val indexEnd = _obj.getOrDefault<Int>(_config, "indexEnd", "JSDashManifestRawSource", null) ?: 0;
|
||||||
|
if(initEnd > 0 && indexStart > 0 && indexEnd > 0) {
|
||||||
|
streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,12 +118,16 @@ class JSDashManifestMergingRawSource(
|
|||||||
if(videoDash == null) return null;
|
if(videoDash == null) return null;
|
||||||
|
|
||||||
//TODO: Temporary simple solution..make more reliable version
|
//TODO: Temporary simple solution..make more reliable version
|
||||||
|
|
||||||
|
var result: String? = null;
|
||||||
val audioAdaptationSet = adaptationSetRegex.find(audioDash!!);
|
val audioAdaptationSet = adaptationSetRegex.find(audioDash!!);
|
||||||
if(audioAdaptationSet != null) {
|
if(audioAdaptationSet != null) {
|
||||||
return videoDash.replace("</AdaptationSet>", "</AdaptationSet>\n" + audioAdaptationSet.value)
|
result = videoDash.replace("</AdaptationSet>", "</AdaptationSet>\n" + audioAdaptationSet.value)
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
return videoDash;
|
result = videoDash;
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
+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
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
@@ -98,18 +98,22 @@ 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 = "DashRawSource";
|
||||||
const val TYPE_DASH_RAW_AUDIO = "DashRawAudioSource";
|
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);
|
||||||
TYPE_DASH_RAW -> fromV8DashRaw(plugin, obj);
|
TYPE_DASH_RAW -> fromV8DashRaw(plugin, obj);
|
||||||
else -> {
|
else -> {
|
||||||
|
|||||||
+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("&", "&"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ class StateCasting {
|
|||||||
private val _scopeMain = CoroutineScope(Dispatchers.Main);
|
private val _scopeMain = CoroutineScope(Dispatchers.Main);
|
||||||
private val _storage: CastingDeviceInfoStorage = FragmentedStorage.get();
|
private val _storage: CastingDeviceInfoStorage = FragmentedStorage.get();
|
||||||
|
|
||||||
private val _castServer = ManagedHttpServer(9999);
|
private val _castServer = ManagedHttpServer();
|
||||||
private var _started = false;
|
private var _started = false;
|
||||||
|
|
||||||
var devices: HashMap<String, CastingDevice> = hashMapOf();
|
var devices: HashMap<String, CastingDevice> = hashMapOf();
|
||||||
@@ -1245,7 +1245,7 @@ class StateCasting {
|
|||||||
|
|
||||||
val videoExecutor = _videoExecutor;
|
val videoExecutor = _videoExecutor;
|
||||||
if (videoExecutor != null) {
|
if (videoExecutor != null) {
|
||||||
val data = videoExecutor.executeRequest(originalUrl, httpContext.headers)
|
val data = videoExecutor.executeRequest("GET", originalUrl, null, httpContext.headers)
|
||||||
httpContext.respondBytes(200, HttpHeaders().apply {
|
httpContext.respondBytes(200, HttpHeaders().apply {
|
||||||
put("Content-Type", mediaType)
|
put("Content-Type", mediaType)
|
||||||
}, data);
|
}, data);
|
||||||
@@ -1263,7 +1263,7 @@ class StateCasting {
|
|||||||
|
|
||||||
val audioExecutor = _audioExecutor;
|
val audioExecutor = _audioExecutor;
|
||||||
if (audioExecutor != null) {
|
if (audioExecutor != null) {
|
||||||
val data = audioExecutor.executeRequest(originalUrl, httpContext.headers)
|
val data = audioExecutor.executeRequest("GET", originalUrl, null, httpContext.headers)
|
||||||
httpContext.respondBytes(200, HttpHeaders().apply {
|
httpContext.respondBytes(200, HttpHeaders().apply {
|
||||||
put("Content-Type", mediaType)
|
put("Content-Type", mediaType)
|
||||||
}, data);
|
}, data);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -1,37 +1,24 @@
|
|||||||
package com.futo.platformplayer.dialogs
|
package com.futo.platformplayer.dialogs
|
||||||
|
|
||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
import android.app.PendingIntent.*
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.PackageInstaller
|
|
||||||
import android.graphics.drawable.Animatable
|
import android.graphics.drawable.Animatable
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.method.ScrollingMovementMethod
|
import android.text.method.ScrollingMovementMethod
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.WindowManager
|
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
import com.futo.platformplayer.receivers.InstallReceiver
|
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
|
||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.ChannelFragment
|
|
||||||
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.states.StatePlatform
|
|
||||||
import com.futo.platformplayer.states.StateUpdate
|
import com.futo.platformplayer.states.StateUpdate
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import java.io.File
|
|
||||||
import java.io.InputStream
|
|
||||||
|
|
||||||
class ChangelogDialog(context: Context?) : AlertDialog(context) {
|
class ChangelogDialog(context: Context?, val changelogs: Map<Int, String>? = null) : AlertDialog(context) {
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = "ChangelogDialog";
|
private val TAG = "ChangelogDialog";
|
||||||
}
|
}
|
||||||
@@ -48,7 +35,11 @@ class ChangelogDialog(context: Context?) : AlertDialog(context) {
|
|||||||
private var _maxVersion: Int = 0;
|
private var _maxVersion: Int = 0;
|
||||||
private var _managedHttpClient = ManagedHttpClient();
|
private var _managedHttpClient = ManagedHttpClient();
|
||||||
|
|
||||||
private val _taskDownloadChangelog = TaskHandler<Int, String?>(StateApp.instance.scopeGetter, { version -> StateUpdate.instance.downloadChangelog(_managedHttpClient, version) })
|
private val _taskDownloadChangelog = TaskHandler<Int, String?>(StateApp.instance.scopeGetter, { version -> if(changelogs == null)
|
||||||
|
StateUpdate.instance.downloadChangelog(_managedHttpClient, version)
|
||||||
|
else
|
||||||
|
changelogs[version]
|
||||||
|
})
|
||||||
.success { setChangelog(it); }
|
.success { setChangelog(it); }
|
||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
Logger.w(TAG, "Failed to load changelog.", it);
|
Logger.w(TAG, "Failed to load changelog.", it);
|
||||||
@@ -97,7 +88,7 @@ class ChangelogDialog(context: Context?) : AlertDialog(context) {
|
|||||||
setVersion(version);
|
setVersion(version);
|
||||||
|
|
||||||
val currentVersion = BuildConfig.VERSION_CODE;
|
val currentVersion = BuildConfig.VERSION_CODE;
|
||||||
_buttonUpdate.visibility = if (currentVersion == _maxVersion) View.GONE else View.VISIBLE;
|
_buttonUpdate.visibility = if (currentVersion == _maxVersion || changelogs != null) View.GONE else View.VISIBLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setVersion(version: Int) {
|
private fun setVersion(version: Int) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ class PluginUpdateDialog : AlertDialog {
|
|||||||
private lateinit var _buttonInstall: LinearLayout;
|
private lateinit var _buttonInstall: LinearLayout;
|
||||||
|
|
||||||
private lateinit var _textPlugin: TextView;
|
private lateinit var _textPlugin: TextView;
|
||||||
|
private lateinit var _textChangelog: TextView;
|
||||||
private lateinit var _textProgres: TextView;
|
private lateinit var _textProgres: TextView;
|
||||||
private lateinit var _textError: TextView;
|
private lateinit var _textError: TextView;
|
||||||
private lateinit var _textResult: TextView;
|
private lateinit var _textResult: TextView;
|
||||||
@@ -94,6 +95,7 @@ class PluginUpdateDialog : AlertDialog {
|
|||||||
_buttonInstall = findViewById(R.id.button_install);
|
_buttonInstall = findViewById(R.id.button_install);
|
||||||
|
|
||||||
_textPlugin = findViewById(R.id.text_plugin);
|
_textPlugin = findViewById(R.id.text_plugin);
|
||||||
|
_textChangelog = findViewById(R.id.text_changelog);
|
||||||
_textProgres = findViewById(R.id.text_progress);
|
_textProgres = findViewById(R.id.text_progress);
|
||||||
_textError = findViewById(R.id.text_error);
|
_textError = findViewById(R.id.text_error);
|
||||||
_textResult = findViewById(R.id.text_result);
|
_textResult = findViewById(R.id.text_result);
|
||||||
@@ -110,6 +112,27 @@ class PluginUpdateDialog : AlertDialog {
|
|||||||
_updateSpinner = findViewById(R.id.update_spinner);
|
_updateSpinner = findViewById(R.id.update_spinner);
|
||||||
_iconPlugin = findViewById(R.id.icon_plugin);
|
_iconPlugin = findViewById(R.id.icon_plugin);
|
||||||
|
|
||||||
|
try {
|
||||||
|
var changelogVersion = _newConfig.version.toString();
|
||||||
|
if (_newConfig.changelog != null && _newConfig.changelog?.containsKey(changelogVersion) == true) {
|
||||||
|
_textChangelog.movementMethod = ScrollingMovementMethod();
|
||||||
|
val changelog = _newConfig.changelog!![changelogVersion]!!;
|
||||||
|
if(changelog.size > 1) {
|
||||||
|
_textChangelog.text = "Changelog (${_newConfig.version})\n" + changelog.map { " - " + it.trim() }.joinToString("\n");
|
||||||
|
}
|
||||||
|
else if(changelog.size == 1) {
|
||||||
|
_textChangelog.text = "Changelog (${_newConfig.version})\n" + changelog[0].trim();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
_textChangelog.visibility = View.GONE;
|
||||||
|
} else
|
||||||
|
_textChangelog.visibility = View.GONE;
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
_textChangelog.visibility = View.GONE;
|
||||||
|
Logger.e(TAG, "Invalid changelog? ", ex);
|
||||||
|
}
|
||||||
|
|
||||||
_buttonCancel1.setOnClickListener {
|
_buttonCancel1.setOnClickListener {
|
||||||
dismiss();
|
dismiss();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ class VideoDownload {
|
|||||||
|
|
||||||
var requireVideoSource: Boolean = false;
|
var requireVideoSource: Boolean = false;
|
||||||
var requireAudioSource: Boolean = false;
|
var requireAudioSource: Boolean = false;
|
||||||
|
var requiredCheck: Boolean = false;
|
||||||
|
|
||||||
@Contextual
|
@Contextual
|
||||||
@Transient
|
@Transient
|
||||||
@@ -140,11 +141,17 @@ class VideoDownload {
|
|||||||
var error: String? = null;
|
var error: String? = null;
|
||||||
|
|
||||||
var videoFilePath: String? = null;
|
var videoFilePath: String? = null;
|
||||||
var videoFileName: String? = null;
|
var videoFileNameBase: String? = null;
|
||||||
|
var videoFileNameExt: String? = null;
|
||||||
|
val videoFileName: String? get() = if(videoFileNameBase.isNullOrEmpty()) null else videoFileNameBase + (if(!videoFileNameExt.isNullOrEmpty()) "." + videoFileNameExt else "");
|
||||||
|
var videoOverrideContainer: String? = null;
|
||||||
var videoFileSize: Long? = null;
|
var videoFileSize: Long? = null;
|
||||||
|
|
||||||
var audioFilePath: String? = null;
|
var audioFilePath: String? = null;
|
||||||
var audioFileName: String? = null;
|
var audioFileNameBase: String? = null;
|
||||||
|
var audioFileNameExt: String? = null;
|
||||||
|
val audioFileName: String? get() = if(audioFileNameBase.isNullOrEmpty()) null else audioFileNameBase + (if(!audioFileNameExt.isNullOrEmpty()) "." + audioFileNameExt else "");
|
||||||
|
var audioOverrideContainer: String? = null;
|
||||||
var audioFileSize: Long? = null;
|
var audioFileSize: Long? = null;
|
||||||
|
|
||||||
var subtitleFilePath: String? = null;
|
var subtitleFilePath: String? = null;
|
||||||
@@ -164,7 +171,7 @@ 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;
|
||||||
@@ -175,8 +182,9 @@ class VideoDownload {
|
|||||||
this.requiresLiveVideoSource = false;
|
this.requiresLiveVideoSource = false;
|
||||||
this.requiresLiveAudioSource = false;
|
this.requiresLiveAudioSource = false;
|
||||||
this.targetVideoName = videoSource?.name;
|
this.targetVideoName = videoSource?.name;
|
||||||
this.requireVideoSource = targetPixelCount != null
|
this.requireVideoSource = targetPixelCount != null;
|
||||||
this.requireAudioSource = targetBitrate != null; //TODO: May not be a valid check.. can only be determined after live fetch?
|
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: IVideoSource?, audioSource: IAudioSource?, subtitleSource: SubtitleRawSource?) {
|
constructor(video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: SubtitleRawSource?) {
|
||||||
this.video = SerializedPlatformVideo.fromVideo(video);
|
this.video = SerializedPlatformVideo.fromVideo(video);
|
||||||
@@ -233,11 +241,13 @@ class VideoDownload {
|
|||||||
videoDetails = null;
|
videoDetails = null;
|
||||||
videoSource = null;
|
videoSource = null;
|
||||||
videoSourceLive = null;
|
videoSourceLive = null;
|
||||||
|
videoOverrideContainer = null;
|
||||||
}
|
}
|
||||||
if(requiresLiveAudioSource && !isLiveAudioSourceValid) {
|
if(requiresLiveAudioSource && !isLiveAudioSourceValid) {
|
||||||
videoDetails = null;
|
videoDetails = null;
|
||||||
audioSource = null;
|
audioSource = null;
|
||||||
videoSourceLive = null;
|
videoSourceLive = null;
|
||||||
|
audioOverrideContainer = 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");
|
||||||
@@ -250,6 +260,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);
|
||||||
@@ -284,6 +318,10 @@ class VideoDownload {
|
|||||||
if(vsource == null)
|
if(vsource == null)
|
||||||
vsource = VideoHelper.selectBestVideoSource(videoSources, targetPixelCount!!.toInt(), arrayOf())
|
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 is JSSource) {
|
||||||
|
this.hasVideoRequestExecutor = this.hasVideoRequestExecutor || vsource.hasRequestExecutor;
|
||||||
|
this.requiresLiveVideoSource = this.hasVideoRequestExecutor || (vsource is JSDashManifestRawSource && vsource.hasGenerate);
|
||||||
|
}
|
||||||
|
|
||||||
if(vsource == null) {
|
if(vsource == null) {
|
||||||
videoSource = null;
|
videoSource = null;
|
||||||
@@ -335,6 +373,12 @@ class VideoDownload {
|
|||||||
asource = VideoHelper.selectBestAudioSource(audioSources, arrayOf(), null, targetBitrate)
|
asource = VideoHelper.selectBestAudioSource(audioSources, arrayOf(), null, targetBitrate)
|
||||||
?: if(videoSource != null ) null
|
?: if(videoSource != null ) null
|
||||||
else throw DownloadException("Could not find a valid video or audio source for download")
|
else throw DownloadException("Could not find a valid video or audio source for download")
|
||||||
|
|
||||||
|
if(asource is JSSource) {
|
||||||
|
this.hasAudioRequestExecutor = this.hasAudioRequestExecutor || asource.hasRequestExecutor;
|
||||||
|
this.requiresLiveAudioSource = this.hasAudioRequestExecutor || (asource is JSDashManifestRawSource && asource.hasGenerate);
|
||||||
|
}
|
||||||
|
|
||||||
if(asource == null) {
|
if(asource == null) {
|
||||||
audioSource = null;
|
audioSource = null;
|
||||||
if(!original.video.isUnMuxed || original.video.videoSources.size == 0)
|
if(!original.video.isUnMuxed || original.video.videoSources.size == 0)
|
||||||
@@ -374,11 +418,13 @@ class VideoDownload {
|
|||||||
else audioSource;
|
else audioSource;
|
||||||
|
|
||||||
if(actualVideoSource != null) {
|
if(actualVideoSource != null) {
|
||||||
videoFileName = "${videoDetails!!.id.value!!} [${actualVideoSource!!.width}x${actualVideoSource!!.height}].${videoContainerToExtension(actualVideoSource!!.container)}".sanitizeFileName();
|
videoFileNameBase = "${videoDetails!!.id.value!!} [${actualVideoSource!!.width}x${actualVideoSource!!.height}]".sanitizeFileName();
|
||||||
|
videoFileNameExt = videoContainerToExtension(actualVideoSource!!.container);
|
||||||
videoFilePath = File(downloadDir, videoFileName!!).absolutePath;
|
videoFilePath = File(downloadDir, videoFileName!!).absolutePath;
|
||||||
}
|
}
|
||||||
if(actualAudioSource != null) {
|
if(actualAudioSource != null) {
|
||||||
audioFileName = "${videoDetails!!.id.value!!} [${actualAudioSource!!.language}-${actualAudioSource!!.bitrate}].${audioContainerToExtension(actualAudioSource!!.container)}".sanitizeFileName();
|
audioFileNameBase = "${videoDetails!!.id.value!!} [${actualAudioSource!!.language}-${actualAudioSource!!.bitrate}]".sanitizeFileName();
|
||||||
|
audioFileNameExt = audioContainerToExtension(actualAudioSource!!.container);
|
||||||
audioFilePath = File(downloadDir, audioFileName!!).absolutePath;
|
audioFilePath = File(downloadDir, audioFileName!!).absolutePath;
|
||||||
}
|
}
|
||||||
if(subtitleSource != null) {
|
if(subtitleSource != null) {
|
||||||
@@ -663,7 +709,7 @@ class VideoDownload {
|
|||||||
val url = foundTemplateUrl.replace("\$Number\$", indexCounter.toString());
|
val url = foundTemplateUrl.replace("\$Number\$", indexCounter.toString());
|
||||||
|
|
||||||
val data = if(executor != null)
|
val data = if(executor != null)
|
||||||
executor.executeRequest(url, mapOf());
|
executor.executeRequest("GET", url, null, mapOf());
|
||||||
else {
|
else {
|
||||||
val resp = client.get(url, mutableMapOf());
|
val resp = client.get(url, mutableMapOf());
|
||||||
if(!resp.isOk)
|
if(!resp.isOk)
|
||||||
@@ -1026,8 +1072,8 @@ 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(videoSourceToUse!!, it, videoFileSize ?: 0) };
|
val localVideoSource = videoFilePath?.let { LocalVideoSource.fromSource(videoSourceToUse!!, it, videoFileSize ?: 0, videoOverrideContainer) };
|
||||||
val localAudioSource = audioFilePath?.let { LocalAudioSource.fromSource(audioSourceToUse!!, it, audioFileSize ?: 0) };
|
val localAudioSource = audioFilePath?.let { LocalAudioSource.fromSource(audioSourceToUse!!, it, audioFileSize ?: 0, audioOverrideContainer) };
|
||||||
val localSubtitleSource = subtitleFilePath?.let { LocalSubtitleSource.fromSource(subtitleSource!!, it) };
|
val localSubtitleSource = subtitleFilePath?.let { LocalSubtitleSource.fromSource(subtitleSource!!, it) };
|
||||||
|
|
||||||
if(localVideoSource != null && videoSourceToUse != null && videoSourceToUse is IStreamMetaDataSource)
|
if(localVideoSource != null && videoSourceToUse != null && videoSourceToUse is IStreamMetaDataSource)
|
||||||
@@ -1056,7 +1102,7 @@ class VideoDownload {
|
|||||||
StateDownloads.instance.updateCachedVideo(existing);
|
StateDownloads.instance.updateCachedVideo(existing);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
val newVideo = VideoLocal(videoDetails!!);
|
val newVideo = VideoLocal(videoDetails!!, OffsetDateTime.now());
|
||||||
if(localVideoSource != null)
|
if(localVideoSource != null)
|
||||||
newVideo.videoSource.add(localVideoSource);
|
newVideo.videoSource.add(localVideoSource);
|
||||||
if(localAudioSource != null)
|
if(localAudioSource != null)
|
||||||
@@ -1108,7 +1154,7 @@ class VideoDownload {
|
|||||||
else if (container.contains("video/x-matroska"))
|
else if (container.contains("video/x-matroska"))
|
||||||
return "mkv";
|
return "mkv";
|
||||||
else
|
else
|
||||||
return "video";
|
return "video";//throw IllegalStateException("Unknown container: " + container)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun audioContainerToExtension(container: String): String {
|
fun audioContainerToExtension(container: String): String {
|
||||||
@@ -1119,11 +1165,11 @@ class VideoDownload {
|
|||||||
else if (container.contains("audio/mp3"))
|
else if (container.contains("audio/mp3"))
|
||||||
return "mp3";
|
return "mp3";
|
||||||
else if (container.contains("audio/webm"))
|
else if (container.contains("audio/webm"))
|
||||||
return "webma";
|
return "webm";
|
||||||
else if (container == "application/vnd.apple.mpegurl")
|
else if (container == "application/vnd.apple.mpegurl")
|
||||||
return "mp4";
|
return "mp4a";
|
||||||
else
|
else
|
||||||
return "audio";
|
return "audio";// throw IllegalStateException("Unknown container: " + container)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun subtitleContainerToExtension(container: String?): String {
|
fun subtitleContainerToExtension(container: String?): String {
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class VideoExport {
|
|||||||
this.subtitleSource = subtitleSource;
|
this.subtitleSource = subtitleSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun export(context: Context, onProgress: ((Double) -> Unit)? = null): DocumentFile = coroutineScope {
|
suspend fun export(context: Context, onProgress: ((Double) -> Unit)? = null, documentRoot: DocumentFile? = null): DocumentFile = coroutineScope {
|
||||||
val v = videoSource;
|
val v = videoSource;
|
||||||
val a = audioSource;
|
val a = audioSource;
|
||||||
val s = subtitleSource;
|
val s = subtitleSource;
|
||||||
@@ -50,7 +50,7 @@ class VideoExport {
|
|||||||
if (s != null) sourceCount++;
|
if (s != null) sourceCount++;
|
||||||
|
|
||||||
val outputFile: DocumentFile?;
|
val outputFile: DocumentFile?;
|
||||||
val downloadRoot = StateApp.instance.getExternalDownloadDirectory(context) ?: throw Exception("External download directory is not set");
|
val downloadRoot = documentRoot ?: StateApp.instance.getExternalDownloadDirectory(context) ?: throw Exception("External download directory is not set");
|
||||||
if (sourceCount > 1) {
|
if (sourceCount > 1) {
|
||||||
val outputFileName = videoLocal.name.sanitizeFileName(true) + ".mp4"// + VideoDownload.videoContainerToExtension(v.container);
|
val outputFileName = videoLocal.name.sanitizeFileName(true) + ".mp4"// + VideoDownload.videoContainerToExtension(v.container);
|
||||||
val f = downloadRoot.createFile("video/mp4", outputFileName)
|
val f = downloadRoot.createFile("video/mp4", outputFileName)
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ 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.models.video.SerializedPlatformVideoDetails
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideoDetails
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
|
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||||
import com.futo.platformplayer.stores.v2.IStoreItem
|
import com.futo.platformplayer.stores.v2.IStoreItem
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
@@ -70,14 +71,21 @@ class VideoLocal: IPlatformVideoDetails, IStoreItem {
|
|||||||
|
|
||||||
override val isLive: Boolean get() = videoSerialized.isLive;
|
override val isLive: Boolean get() = videoSerialized.isLive;
|
||||||
|
|
||||||
|
override val isShort: Boolean get() = videoSerialized.isShort;
|
||||||
|
|
||||||
//TODO: Offline subtitles
|
//TODO: Offline subtitles
|
||||||
override val subtitles: List<ISubtitleSource> = listOf();
|
override val subtitles: List<ISubtitleSource> = listOf();
|
||||||
|
|
||||||
constructor(video: SerializedPlatformVideoDetails) {
|
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
||||||
|
var downloadDate: OffsetDateTime? = null;
|
||||||
|
|
||||||
|
constructor(video: SerializedPlatformVideoDetails, downloadDate: OffsetDateTime? = null) {
|
||||||
this.videoSerialized = video;
|
this.videoSerialized = video;
|
||||||
|
this.downloadDate = downloadDate;
|
||||||
}
|
}
|
||||||
constructor(video: IPlatformVideoDetails, subtitleSources: List<SubtitleRawSource>) {
|
constructor(video: IPlatformVideoDetails, subtitleSources: List<SubtitleRawSource>) {
|
||||||
this.videoSerialized = SerializedPlatformVideoDetails.fromVideo(video, subtitleSources);
|
this.videoSerialized = SerializedPlatformVideoDetails.fromVideo(video, subtitleSources);
|
||||||
|
downloadDate = OffsetDateTime.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? = null;
|
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? = null;
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import com.futo.platformplayer.engine.internal.V8Converter
|
|||||||
import com.futo.platformplayer.engine.packages.PackageBridge
|
import com.futo.platformplayer.engine.packages.PackageBridge
|
||||||
import com.futo.platformplayer.engine.packages.PackageDOMParser
|
import com.futo.platformplayer.engine.packages.PackageDOMParser
|
||||||
import com.futo.platformplayer.engine.packages.PackageHttp
|
import com.futo.platformplayer.engine.packages.PackageHttp
|
||||||
|
import com.futo.platformplayer.engine.packages.PackageJSDOM
|
||||||
import com.futo.platformplayer.engine.packages.PackageUtilities
|
import com.futo.platformplayer.engine.packages.PackageUtilities
|
||||||
import com.futo.platformplayer.engine.packages.V8Package
|
import com.futo.platformplayer.engine.packages.V8Package
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
@@ -94,7 +95,11 @@ class V8Plugin {
|
|||||||
withDependency(PackageBridge(this, config));
|
withDependency(PackageBridge(this, config));
|
||||||
|
|
||||||
for(pack in config.packages)
|
for(pack in config.packages)
|
||||||
withDependency(getPackage(pack));
|
withDependency(getPackage(pack)!!);
|
||||||
|
for(pack in config.packagesOptional)
|
||||||
|
getPackage(pack, true)?.let {
|
||||||
|
withDependency(it);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun changeAllowDevSubmit(isAllowed: Boolean) {
|
fun changeAllowDevSubmit(isAllowed: Boolean) {
|
||||||
@@ -254,13 +259,14 @@ class V8Plugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getPackage(packageName: String): V8Package {
|
private fun getPackage(packageName: String, allowNull: Boolean = false): V8Package? {
|
||||||
//TODO: Auto get all package types?
|
//TODO: Auto get all package types?
|
||||||
return when(packageName) {
|
return when(packageName) {
|
||||||
"DOMParser" -> PackageDOMParser(this)
|
"DOMParser" -> PackageDOMParser(this)
|
||||||
"Http" -> PackageHttp(this, config)
|
"Http" -> PackageHttp(this, config)
|
||||||
"Utilities" -> PackageUtilities(this, config)
|
"Utilities" -> PackageUtilities(this, config)
|
||||||
else -> throw ScriptCompilationException(config, "Unknown package [${packageName}] required for plugin ${config.name}");
|
"JSDOM" -> PackageJSDOM(this, config)
|
||||||
|
else -> if(allowNull) null else throw ScriptCompilationException(config, "Unknown package [${packageName}] required for plugin ${config.name}");
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ interface IV8PluginConfig {
|
|||||||
val allowEval: Boolean;
|
val allowEval: Boolean;
|
||||||
val allowUrls: List<String>;
|
val allowUrls: List<String>;
|
||||||
val packages: List<String>;
|
val packages: List<String>;
|
||||||
|
val packagesOptional: List<String>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
@@ -13,17 +14,20 @@ class V8PluginConfig : IV8PluginConfig {
|
|||||||
override val allowEval: Boolean;
|
override val allowEval: Boolean;
|
||||||
override val allowUrls: List<String>;
|
override val allowUrls: List<String>;
|
||||||
override val packages: List<String>;
|
override val packages: List<String>;
|
||||||
|
override val packagesOptional: List<String>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
name = "Unknown";
|
name = "Unknown";
|
||||||
allowEval = false;
|
allowEval = false;
|
||||||
allowUrls = listOf();
|
allowUrls = listOf();
|
||||||
packages = listOf();
|
packages = listOf();
|
||||||
|
packagesOptional = listOf();
|
||||||
}
|
}
|
||||||
constructor(name: String, allowEval: Boolean, allowUrls: List<String>, packages: List<String> = listOf()) {
|
constructor(name: String, allowEval: Boolean, allowUrls: List<String>, packages: List<String> = listOf(), packagesOptional: List<String> = listOf()) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.allowEval = allowEval;
|
this.allowEval = allowEval;
|
||||||
this.allowUrls = allowUrls;
|
this.allowUrls = allowUrls;
|
||||||
this.packages = packages;
|
this.packages = packages;
|
||||||
|
this.packagesOptional = packagesOptional;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,12 @@
|
|||||||
package com.futo.platformplayer.engine.packages
|
package com.futo.platformplayer.engine.packages
|
||||||
|
|
||||||
|
import android.media.MediaCodec
|
||||||
|
import android.media.MediaCodecList
|
||||||
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.utils.JavetResourceUtils
|
||||||
import com.caoccao.javet.values.V8Value
|
import com.caoccao.javet.values.V8Value
|
||||||
|
import com.caoccao.javet.values.reference.V8ValueFunction
|
||||||
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
|
||||||
@@ -16,6 +20,7 @@ import com.futo.platformplayer.engine.IV8PluginConfig
|
|||||||
import com.futo.platformplayer.engine.V8Plugin
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
@@ -37,6 +42,18 @@ class PackageBridge : V8Package {
|
|||||||
_config = config;
|
_config = config;
|
||||||
_client = plugin.httpClient;
|
_client = plugin.httpClient;
|
||||||
_clientAuth = plugin.httpClientAuth;
|
_clientAuth = plugin.httpClientAuth;
|
||||||
|
|
||||||
|
withScript("""
|
||||||
|
function setTimeout(func, delay) {
|
||||||
|
let args = Array.prototype.slice.call(arguments, 2);
|
||||||
|
return bridge.setTimeout(func.bind(globalThis, ...args), delay || 0);
|
||||||
|
}
|
||||||
|
""".trimIndent());
|
||||||
|
withScript("""
|
||||||
|
function clearTimeout(id) {
|
||||||
|
bridge.clearTimeout(id);
|
||||||
|
}
|
||||||
|
""".trimIndent());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -62,6 +79,48 @@ class PackageBridge : V8Package {
|
|||||||
value.close();
|
value.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var timeoutCounter = 0;
|
||||||
|
var timeoutMap = HashSet<Int>();
|
||||||
|
@V8Function
|
||||||
|
fun setTimeout(func: V8ValueFunction, timeout: Long): Int {
|
||||||
|
val id = timeoutCounter++;
|
||||||
|
|
||||||
|
val funcClone = func.toClone<V8ValueFunction>()
|
||||||
|
|
||||||
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
|
delay(timeout);
|
||||||
|
synchronized(timeoutMap) {
|
||||||
|
if(!timeoutMap.contains(id)) {
|
||||||
|
JavetResourceUtils.safeClose(funcClone);
|
||||||
|
return@launch;
|
||||||
|
}
|
||||||
|
timeoutMap.remove(id);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
_plugin.whenNotBusy {
|
||||||
|
funcClone.callVoid(null, arrayOf<Any>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed timeout callback", ex);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
JavetResourceUtils.safeClose(funcClone);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
synchronized(timeoutMap) {
|
||||||
|
timeoutMap.add(id);
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
@V8Function
|
||||||
|
fun clearTimeout(id: Int) {
|
||||||
|
synchronized(timeoutMap) {
|
||||||
|
if(timeoutMap.contains(id))
|
||||||
|
timeoutMap.remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
fun toast(str: String) {
|
fun toast(str: String) {
|
||||||
Logger.i(TAG, "Plugin toast [${_config.name}]: ${str}");
|
Logger.i(TAG, "Plugin toast [${_config.name}]: ${str}");
|
||||||
@@ -130,7 +189,44 @@ class PackageBridge : V8Package {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun getHardwareCodecs(): List<String>{
|
||||||
|
return getSupportedHardwareMediaCodecs();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "PackageBridge";
|
private const val TAG = "PackageBridge";
|
||||||
|
|
||||||
|
private var _mediaCodecList: MutableList<String> = mutableListOf();
|
||||||
|
private var _mediaCodecListHardware: MutableList<String> = mutableListOf();
|
||||||
|
|
||||||
|
fun getSupportedMediaCodecs(): List<String>{
|
||||||
|
synchronized(_mediaCodecList) {
|
||||||
|
if(_mediaCodecList.size <= 0)
|
||||||
|
updateMediaCodecList();
|
||||||
|
return _mediaCodecList;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun getSupportedHardwareMediaCodecs(): List<String>{
|
||||||
|
synchronized(_mediaCodecList) {
|
||||||
|
if(_mediaCodecList.size <= 0)
|
||||||
|
updateMediaCodecList();
|
||||||
|
return _mediaCodecListHardware;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private fun updateMediaCodecList() {
|
||||||
|
_mediaCodecList.clear();
|
||||||
|
_mediaCodecListHardware.clear();
|
||||||
|
for(codec in MediaCodecList(MediaCodecList.ALL_CODECS).codecInfos) {
|
||||||
|
if(!codec.isEncoder) {
|
||||||
|
_mediaCodecList.add(codec.canonicalName);
|
||||||
|
if (codec.isHardwareAccelerated)
|
||||||
|
_mediaCodecListHardware.add(codec.canonicalName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -21,9 +21,13 @@ 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 com.futo.platformplayer.states.StateApp
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.net.SocketTimeoutException
|
import java.net.SocketTimeoutException
|
||||||
|
import java.util.concurrent.ForkJoinPool
|
||||||
|
import java.util.concurrent.ForkJoinTask
|
||||||
|
import kotlin.concurrent.thread
|
||||||
import kotlin.streams.asSequence
|
import kotlin.streams.asSequence
|
||||||
|
|
||||||
class PackageHttp: V8Package {
|
class PackageHttp: V8Package {
|
||||||
@@ -42,6 +46,9 @@ class PackageHttp: V8Package {
|
|||||||
override val name: String get() = "Http";
|
override val name: String get() = "Http";
|
||||||
override val variableName: String get() = "http";
|
override val variableName: String get() = "http";
|
||||||
|
|
||||||
|
private var _batchPoolLock: Any = Any();
|
||||||
|
private var _batchPool: ForkJoinPool? = null;
|
||||||
|
|
||||||
|
|
||||||
constructor(plugin: V8Plugin, config: IV8PluginConfig): super(plugin) {
|
constructor(plugin: V8Plugin, config: IV8PluginConfig): super(plugin) {
|
||||||
_config = config;
|
_config = config;
|
||||||
@@ -51,6 +58,37 @@ class PackageHttp: V8Package {
|
|||||||
_packageClientAuth = PackageHttpClient(this, _clientAuth);
|
_packageClientAuth = PackageHttpClient(this, _clientAuth);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
Automatically adjusting threadpool dedicated per PackageHttp for batch requests.
|
||||||
|
*/
|
||||||
|
private fun <T, R> autoParallelPool(data: List<T>, parallelism: Int, handle: (T)->R): List<Pair<R?, Throwable?>> {
|
||||||
|
synchronized(_batchPoolLock) {
|
||||||
|
val threadsToUse = if (parallelism <= 0) data.size else Math.min(parallelism, data.size);
|
||||||
|
if(_batchPool == null)
|
||||||
|
_batchPool = ForkJoinPool(threadsToUse);
|
||||||
|
var pool = _batchPool ?: return listOf();
|
||||||
|
if(pool.poolSize < threadsToUse) { //Resize pool
|
||||||
|
pool.shutdown();
|
||||||
|
_batchPool = ForkJoinPool(threadsToUse);
|
||||||
|
pool = _batchPool ?: return listOf();
|
||||||
|
}
|
||||||
|
|
||||||
|
val resultTasks = mutableListOf<ForkJoinTask<Pair<R?, Throwable?>>>();
|
||||||
|
for(item in data){
|
||||||
|
resultTasks.add(pool.submit<Pair<R?, Throwable?>> {
|
||||||
|
try {
|
||||||
|
return@submit Pair<R?, Throwable?>(handle(item), null);
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
return@submit Pair<R?, Throwable?>(null, ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return resultTasks.map { it.join() };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
fun newClient(withAuth: Boolean): PackageHttpClient {
|
fun newClient(withAuth: Boolean): PackageHttpClient {
|
||||||
val httpClient = if(withAuth) _clientAuth.clone() else _client.clone();
|
val httpClient = if(withAuth) _clientAuth.clone() else _client.clone();
|
||||||
@@ -176,8 +214,6 @@ class PackageHttp: V8Package {
|
|||||||
obj.set("url", url);
|
obj.set("url", url);
|
||||||
obj.set("code", code);
|
obj.set("code", code);
|
||||||
if(body != null) {
|
if(body != null) {
|
||||||
val buffer = runtime.createV8ValueArrayBuffer(body.size);
|
|
||||||
buffer.fromBytes(body);
|
|
||||||
obj.set("body", body);
|
obj.set("body", body);
|
||||||
}
|
}
|
||||||
obj.set("headers", headers);
|
obj.set("headers", headers);
|
||||||
@@ -236,16 +272,19 @@ class PackageHttp: V8Package {
|
|||||||
//Finalizer
|
//Finalizer
|
||||||
@V8Function
|
@V8Function
|
||||||
fun execute(): List<IBridgeHttpResponse?> {
|
fun execute(): List<IBridgeHttpResponse?> {
|
||||||
return _reqs.parallelStream().map {
|
return _package.autoParallelPool(_reqs, -1) {
|
||||||
if(it.second.method == "DUMMY")
|
if(it.second.method == "DUMMY")
|
||||||
return@map null;
|
return@autoParallelPool 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, it.second.respType);
|
return@autoParallelPool 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, it.second.respType);
|
return@autoParallelPool it.first.request(it.second.method, it.second.url, it.second.headers, it.second.respType);
|
||||||
}
|
}.map {
|
||||||
.asSequence()
|
if(it.second != null)
|
||||||
.toList();
|
throw it.second!!;
|
||||||
|
else
|
||||||
|
return@map it.first;
|
||||||
|
}.toList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -439,11 +478,8 @@ class PackageHttp: V8Package {
|
|||||||
else {
|
else {
|
||||||
headers?.forEach { (header, values) ->
|
headers?.forEach { (header, values) ->
|
||||||
val lowerCaseHeader = header.lowercase()
|
val lowerCaseHeader = header.lowercase()
|
||||||
if(lowerCaseHeader == "set-cookie") {
|
if(lowerCaseHeader == "set-cookie" && !values.any { it.lowercase().contains("httponly") })
|
||||||
result[lowerCaseHeader] = values.filter{
|
result[lowerCaseHeader] = values;
|
||||||
!it.lowercase().contains("httponly")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
result[lowerCaseHeader] = values;
|
result[lowerCaseHeader] = values;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.futo.platformplayer.engine.packages
|
||||||
|
|
||||||
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
|
||||||
|
|
||||||
|
class PackageJSDOM : V8Package {
|
||||||
|
@Transient
|
||||||
|
private val _config: IV8PluginConfig;
|
||||||
|
|
||||||
|
override val name: String get() = "JSDOM";
|
||||||
|
override val variableName: String get() = "packageJSDOM";
|
||||||
|
|
||||||
|
constructor(plugin: V8Plugin, config: IV8PluginConfig): super(plugin) {
|
||||||
|
_config = config;
|
||||||
|
plugin.withDependency(StateApp.instance.contextOrNull ?: return, "scripts/JSDOM.js");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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 ?: "")
|
||||||
|
|||||||
+23
-11
@@ -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(private val subType: String? = null) : 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;
|
||||||
@@ -72,9 +73,12 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
|||||||
if (lastPolycentricProfile != null)
|
if (lastPolycentricProfile != null)
|
||||||
pager= StatePolycentric.instance.getChannelContent(lifecycleScope, lastPolycentricProfile);
|
pager= StatePolycentric.instance.getChannelContent(lifecycleScope, lastPolycentricProfile);
|
||||||
|
|
||||||
if(pager == null)
|
if(pager == null) {
|
||||||
pager = StatePlatform.instance.getChannelContent(channel.url);
|
if(subType != null)
|
||||||
|
pager = StatePlatform.instance.getChannelContent(channel.url, subType);
|
||||||
|
else
|
||||||
|
pager = StatePlatform.instance.getChannelContent(channel.url);
|
||||||
|
}
|
||||||
return pager;
|
return pager;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +122,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 +167,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 +186,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<*>) {
|
||||||
@@ -358,6 +370,6 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val TAG = "VideoListFragment";
|
val TAG = "VideoListFragment";
|
||||||
fun newInstance() = ChannelContentsFragment().apply { }
|
fun newInstance(subType: String? = null) = ChannelContentsFragment(subType).apply { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+28
-8
@@ -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
|
||||||
@@ -21,6 +25,7 @@ import com.futo.platformplayer.UISlideOverlays
|
|||||||
import com.futo.platformplayer.api.media.PlatformID
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
|
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
|
||||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
|
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
|
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
|
||||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||||
@@ -52,7 +57,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 +71,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 +238,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}]")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -449,6 +458,12 @@ class ChannelFragment : MainFragment() {
|
|||||||
|
|
||||||
_fragment.topBar?.assume<NavigationTopBarFragment>()?.setMenuItems(buttons)
|
_fragment.topBar?.assume<NavigationTopBarFragment>()?.setMenuItems(buttons)
|
||||||
}
|
}
|
||||||
|
if(plugin != null && plugin.capabilities.hasGetChannelCapabilities) {
|
||||||
|
if(plugin.getChannelCapabilities()?.types?.contains(ResultCapabilities.TYPE_SHORTS) ?: false &&
|
||||||
|
!(_viewPager.adapter as ChannelViewPagerAdapter).containsItem(ChannelTab.SHORTS.ordinal.toLong())) {
|
||||||
|
(_viewPager.adapter as ChannelViewPagerAdapter).insert(1, ChannelTab.SHORTS);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -461,8 +476,13 @@ class ChannelFragment : MainFragment() {
|
|||||||
R.string.subscribers
|
R.string.subscribers
|
||||||
).lowercase() else ""
|
).lowercase() else ""
|
||||||
|
|
||||||
val supportsPlaylists =
|
var supportsPlaylists = false;
|
||||||
StatePlatform.instance.getChannelClient(channel.url).capabilities.hasGetChannelPlaylists
|
try {
|
||||||
|
supportsPlaylists = StatePlatform.instance.getChannelClient(channel.url).capabilities.hasGetChannelPlaylists
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
//Ignore error
|
||||||
|
Logger.e(TAG, "Failed to check if supports playlists", ex);
|
||||||
|
}
|
||||||
val playlistPosition = 1
|
val playlistPosition = 1
|
||||||
if (supportsPlaylists && !(_viewPager.adapter as ChannelViewPagerAdapter).containsItem(
|
if (supportsPlaylists && !(_viewPager.adapter as ChannelViewPagerAdapter).containsItem(
|
||||||
ChannelTab.PLAYLISTS.ordinal.toLong()
|
ChannelTab.PLAYLISTS.ordinal.toLong()
|
||||||
|
|||||||
+27
-30
@@ -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}]");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -142,7 +135,10 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
|||||||
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
|
||||||
|
);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -160,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) {
|
||||||
@@ -217,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));
|
||||||
|
|
||||||
@@ -241,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();
|
||||||
}
|
}
|
||||||
@@ -269,6 +266,6 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = "ContentFeedView";
|
private const val TAG = "ContentFeedView";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+7
@@ -17,6 +17,7 @@ import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
|||||||
import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
|
||||||
import com.futo.platformplayer.isHttpUrl
|
import com.futo.platformplayer.isHttpUrl
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.states.StateMeta
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.platformplayer.views.FeedStyle
|
import com.futo.platformplayer.views.FeedStyle
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -222,6 +223,12 @@ class ContentSearchResultsFragment : MainFragment() {
|
|||||||
setSortByOptions(null);
|
setSortByOptions(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun filterResults(results: List<IPlatformContent>): List<IPlatformContent> {
|
||||||
|
if(Settings.instance.search.hidefromSearch)
|
||||||
|
return super.filterResults(results.filter { !StateMeta.instance.isVideoHidden(it.url) && !StateMeta.instance.isCreatorHidden(it.author.url) });
|
||||||
|
return super.filterResults(results)
|
||||||
|
}
|
||||||
|
|
||||||
override fun reload() {
|
override fun reload() {
|
||||||
loadResults();
|
loadResults();
|
||||||
}
|
}
|
||||||
|
|||||||
+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);
|
||||||
|
|||||||
+58
-2
@@ -4,8 +4,13 @@ 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.AdapterView
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
|
import android.widget.EditText
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.Spinner
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.core.widget.addTextChangedListener
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
@@ -25,6 +30,7 @@ import com.futo.platformplayer.views.items.PlaylistDownloadItem
|
|||||||
import com.futo.platformplayer.views.others.ProgressBar
|
import com.futo.platformplayer.views.others.ProgressBar
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
class DownloadsFragment : MainFragment() {
|
class DownloadsFragment : MainFragment() {
|
||||||
private val TAG = "DownloadsFragment";
|
private val TAG = "DownloadsFragment";
|
||||||
@@ -92,8 +98,12 @@ class DownloadsFragment : MainFragment() {
|
|||||||
|
|
||||||
private val _listDownloadedHeader: LinearLayout;
|
private val _listDownloadedHeader: LinearLayout;
|
||||||
private val _listDownloadedMeta: TextView;
|
private val _listDownloadedMeta: TextView;
|
||||||
|
private val _listDownloadSearch: EditText;
|
||||||
private val _listDownloaded: AnyInsertedAdapterView<VideoLocal, VideoDownloadViewHolder>;
|
private val _listDownloaded: AnyInsertedAdapterView<VideoLocal, VideoDownloadViewHolder>;
|
||||||
|
|
||||||
|
private var lastDownloads: List<VideoLocal>? = null;
|
||||||
|
private var ordering: String? = "nameAsc";
|
||||||
|
|
||||||
constructor(frag: DownloadsFragment, inflater: LayoutInflater): super(frag.requireContext()) {
|
constructor(frag: DownloadsFragment, inflater: LayoutInflater): super(frag.requireContext()) {
|
||||||
inflater.inflate(R.layout.fragment_downloads, this);
|
inflater.inflate(R.layout.fragment_downloads, this);
|
||||||
_frag = frag;
|
_frag = frag;
|
||||||
@@ -104,6 +114,7 @@ class DownloadsFragment : MainFragment() {
|
|||||||
|
|
||||||
_listActiveDownloadsContainer = findViewById(R.id.downloads_active_downloads_container);
|
_listActiveDownloadsContainer = findViewById(R.id.downloads_active_downloads_container);
|
||||||
_listActiveDownloadsMeta = findViewById(R.id.downloads_active_downloads_meta);
|
_listActiveDownloadsMeta = findViewById(R.id.downloads_active_downloads_meta);
|
||||||
|
_listDownloadSearch = findViewById(R.id.downloads_search);
|
||||||
_listActiveDownloads = findViewById(R.id.downloads_active_downloads_list);
|
_listActiveDownloads = findViewById(R.id.downloads_active_downloads_list);
|
||||||
|
|
||||||
_listPlaylistsContainer = findViewById(R.id.downloads_playlist_container);
|
_listPlaylistsContainer = findViewById(R.id.downloads_playlist_container);
|
||||||
@@ -113,6 +124,30 @@ class DownloadsFragment : MainFragment() {
|
|||||||
_listDownloadedHeader = findViewById(R.id.downloads_videos_header);
|
_listDownloadedHeader = findViewById(R.id.downloads_videos_header);
|
||||||
_listDownloadedMeta = findViewById(R.id.downloads_videos_meta);
|
_listDownloadedMeta = findViewById(R.id.downloads_videos_meta);
|
||||||
|
|
||||||
|
_listDownloadSearch.addTextChangedListener {
|
||||||
|
updateContentFilters();
|
||||||
|
}
|
||||||
|
val spinnerSortBy: Spinner = findViewById(R.id.spinner_sortby);
|
||||||
|
spinnerSortBy.adapter = ArrayAdapter(context, R.layout.spinner_item_simple, resources.getStringArray(R.array.downloads_sortby_array)).also {
|
||||||
|
it.setDropDownViewResource(R.layout.spinner_dropdownitem_simple);
|
||||||
|
};
|
||||||
|
spinnerSortBy.setSelection(0);
|
||||||
|
spinnerSortBy.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||||
|
override fun onItemSelected(parent: AdapterView<*>, view: View?, pos: Int, id: Long) {
|
||||||
|
when(pos) {
|
||||||
|
0 -> ordering = "nameAsc"
|
||||||
|
1 -> ordering = "nameDesc"
|
||||||
|
2 -> ordering = "downloadDateAsc"
|
||||||
|
3 -> ordering = "downloadDateDesc"
|
||||||
|
4 -> ordering = "releasedAsc"
|
||||||
|
5 -> ordering = "releasedDesc"
|
||||||
|
else -> ordering = null
|
||||||
|
}
|
||||||
|
updateContentFilters()
|
||||||
|
}
|
||||||
|
override fun onNothingSelected(parent: AdapterView<*>?) = Unit
|
||||||
|
};
|
||||||
|
|
||||||
_listDownloaded = findViewById<RecyclerView>(R.id.list_downloaded)
|
_listDownloaded = findViewById<RecyclerView>(R.id.list_downloaded)
|
||||||
.asAnyWithTop(findViewById(R.id.downloads_top)) {
|
.asAnyWithTop(findViewById(R.id.downloads_top)) {
|
||||||
it.onClick.subscribe {
|
it.onClick.subscribe {
|
||||||
@@ -125,7 +160,6 @@ class DownloadsFragment : MainFragment() {
|
|||||||
reloadUI();
|
reloadUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun reloadUI() {
|
fun reloadUI() {
|
||||||
val usage = StateDownloads.instance.getTotalUsage(true);
|
val usage = StateDownloads.instance.getTotalUsage(true);
|
||||||
_usageUsed.text = "${usage.usage.toHumanBytesSize()} " + context.getString(R.string.used);
|
_usageUsed.text = "${usage.usage.toHumanBytesSize()} " + context.getString(R.string.used);
|
||||||
@@ -184,7 +218,29 @@ class DownloadsFragment : MainFragment() {
|
|||||||
_listDownloadedMeta.text = "(${downloaded.size} ${context.getString(R.string.videos).lowercase()})";
|
_listDownloadedMeta.text = "(${downloaded.size} ${context.getString(R.string.videos).lowercase()})";
|
||||||
}
|
}
|
||||||
|
|
||||||
_listDownloaded.setData(downloaded);
|
lastDownloads = downloaded;
|
||||||
|
_listDownloaded.setData(filterDownloads(downloaded));
|
||||||
|
}
|
||||||
|
fun updateContentFilters(){
|
||||||
|
val toFilter = lastDownloads ?: return;
|
||||||
|
_listDownloaded.setData(filterDownloads(toFilter));
|
||||||
|
}
|
||||||
|
fun filterDownloads(vids: List<VideoLocal>): List<VideoLocal>{
|
||||||
|
var vidsToReturn = vids;
|
||||||
|
if(!_listDownloadSearch.text.isNullOrEmpty())
|
||||||
|
vidsToReturn = vids.filter { it.name.contains(_listDownloadSearch.text, true) };
|
||||||
|
if(!ordering.isNullOrEmpty()) {
|
||||||
|
vidsToReturn = when(ordering){
|
||||||
|
"downloadDateAsc" -> vidsToReturn.sortedBy { it.downloadDate ?: OffsetDateTime.MAX };
|
||||||
|
"downloadDateDesc" -> vidsToReturn.sortedByDescending { it.downloadDate ?: OffsetDateTime.MIN };
|
||||||
|
"nameAsc" -> vidsToReturn.sortedBy { it.name.lowercase() }
|
||||||
|
"nameDesc" -> vidsToReturn.sortedByDescending { it.name.lowercase() }
|
||||||
|
"releasedAsc" -> vidsToReturn.sortedBy { it.datetime ?: OffsetDateTime.MAX }
|
||||||
|
"releasedDesc" -> vidsToReturn.sortedByDescending { it.datetime ?: OffsetDateTime.MIN }
|
||||||
|
else -> vidsToReturn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return vidsToReturn;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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 {}
|
||||||
}
|
}
|
||||||
|
|||||||
+47
-15
@@ -8,6 +8,7 @@ import android.view.ViewGroup
|
|||||||
import androidx.core.app.ShareCompat
|
import androidx.core.app.ShareCompat
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
|
import com.futo.platformplayer.activities.IWithResultLauncher
|
||||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
||||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
@@ -70,7 +71,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;
|
||||||
@@ -78,6 +79,14 @@ class PlaylistFragment : MainFragment() {
|
|||||||
val nameInput = SlideUpMenuTextInput(context, context.getString(R.string.name));
|
val nameInput = SlideUpMenuTextInput(context, context.getString(R.string.name));
|
||||||
val editPlaylistOverlay = SlideUpMenuOverlay(context, overlayContainer, context.getString(R.string.edit_playlist), context.getString(R.string.ok), false, nameInput);
|
val editPlaylistOverlay = SlideUpMenuOverlay(context, overlayContainer, context.getString(R.string.edit_playlist), context.getString(R.string.ok), false, nameInput);
|
||||||
|
|
||||||
|
_buttonExport.setOnClickListener {
|
||||||
|
_playlist?.let {
|
||||||
|
val context = StateApp.instance.contextOrNull ?: return@let;
|
||||||
|
if(context is IWithResultLauncher)
|
||||||
|
StateDownloads.instance.exportPlaylist(context, it.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
_buttonDownload.visibility = View.VISIBLE;
|
_buttonDownload.visibility = View.VISIBLE;
|
||||||
editPlaylistOverlay.onOK.subscribe {
|
editPlaylistOverlay.onOK.subscribe {
|
||||||
val text = nameInput.text;
|
val text = nameInput.text;
|
||||||
@@ -137,16 +146,16 @@ class PlaylistFragment : MainFragment() {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
_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);
|
setMetadata(it.videos.size, it.videos.sumOf { it.duration });
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
@@ -156,6 +165,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()
|
||||||
|
|
||||||
@@ -166,24 +183,21 @@ class PlaylistFragment : MainFragment() {
|
|||||||
if (parameter != null) {
|
if (parameter != null) {
|
||||||
setName(parameter.name)
|
setName(parameter.name)
|
||||||
setVideos(parameter.videos, true)
|
setVideos(parameter.videos, true)
|
||||||
setVideoCount(parameter.videos.size)
|
setMetadata(parameter.videos.size, parameter.videos.sumOf { it.duration })
|
||||||
setButtonDownloadVisible(true)
|
setButtonDownloadVisible(true)
|
||||||
|
setButtonExportVisible(false)
|
||||||
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 {
|
||||||
setName(null)
|
setName(null)
|
||||||
setVideos(null, false)
|
setVideos(null, false)
|
||||||
setVideoCount(-1)
|
setMetadata(-1, -1);
|
||||||
setButtonDownloadVisible(false)
|
setButtonDownloadVisible(false)
|
||||||
setButtonEditVisible(false)
|
setButtonEditVisible(false)
|
||||||
}
|
}
|
||||||
@@ -191,7 +205,7 @@ class PlaylistFragment : MainFragment() {
|
|||||||
_playlist = null
|
_playlist = null
|
||||||
_url = parameter.url
|
_url = parameter.url
|
||||||
|
|
||||||
setVideoCount(parameter.videoCount)
|
setMetadata(parameter.videoCount, -1);
|
||||||
setName(parameter.name)
|
setName(parameter.name)
|
||||||
setVideos(null, false)
|
setVideos(null, false)
|
||||||
setButtonDownloadVisible(false)
|
setButtonDownloadVisible(false)
|
||||||
@@ -204,7 +218,7 @@ class PlaylistFragment : MainFragment() {
|
|||||||
|
|
||||||
setName(null)
|
setName(null)
|
||||||
setVideos(null, false)
|
setVideos(null, false)
|
||||||
setVideoCount(-1)
|
setMetadata(-1, -1);
|
||||||
setButtonDownloadVisible(false)
|
setButtonDownloadVisible(false)
|
||||||
setButtonEditVisible(false)
|
setButtonEditVisible(false)
|
||||||
|
|
||||||
@@ -242,6 +256,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);
|
||||||
}
|
}
|
||||||
@@ -266,6 +289,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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+41
-9
@@ -237,7 +237,19 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
BigButtonGroup(c, context.getString(R.string.update),
|
BigButtonGroup(c, context.getString(R.string.update),
|
||||||
BigButton(c, context.getString(R.string.check_for_updates), context.getString(R.string.checks_for_new_versions_of_the_source), R.drawable.ic_update) {
|
BigButton(c, context.getString(R.string.check_for_updates), context.getString(R.string.checks_for_new_versions_of_the_source), R.drawable.ic_update) {
|
||||||
checkForUpdatesSource();
|
checkForUpdatesSource();
|
||||||
}
|
},
|
||||||
|
if(config.changelog?.any() == true)
|
||||||
|
BigButton(c, context.getString(R.string.changelog), context.getString(R.string.changelog_plugin_description), R.drawable.ic_list) {
|
||||||
|
UIDialogs.showChangelogDialog(context, config.version, config.changelog!!.filterKeys { it.toIntOrNull() != null }
|
||||||
|
.mapKeys { it.key.toInt() }
|
||||||
|
.mapValues { config.getChangelogString(it.key.toString()) ?: "" });
|
||||||
|
}.apply {
|
||||||
|
this.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).apply {
|
||||||
|
setMargins(0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics).toInt(), 0, 0);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
null
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -397,23 +409,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) {
|
||||||
@@ -524,7 +556,7 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
Logger.i(TAG, "Downloaded source config ($sourceUrl):\n${configJson}");
|
Logger.i(TAG, "Downloaded source config ($sourceUrl):\n${configJson}");
|
||||||
|
|
||||||
val config = SourcePluginConfig.fromJson(configJson);
|
val config = SourcePluginConfig.fromJson(configJson);
|
||||||
if (config.version <= c.version) {
|
if (config.version <= c.version && config.name != "Youtube") {
|
||||||
Logger.i(TAG, "Plugin is up to date.");
|
Logger.i(TAG, "Plugin is up to date.");
|
||||||
withContext(Dispatchers.Main) { UIDialogs.toast(context.getString(R.string.plugin_is_fully_up_to_date)); };
|
withContext(Dispatchers.Main) { UIDialogs.toast(context.getString(R.string.plugin_is_fully_up_to_date)); };
|
||||||
return@launch;
|
return@launch;
|
||||||
|
|||||||
+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);
|
||||||
|
|||||||
+19
-35
@@ -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 });
|
||||||
}
|
}
|
||||||
@@ -223,8 +204,10 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
val feed = StateSubscriptions.instance.getFeed(group?.id);
|
val feed = StateSubscriptions.instance.getFeed(group?.id);
|
||||||
|
|
||||||
val currentExs = feed?.exceptions ?: listOf();
|
val currentExs = feed?.exceptions ?: listOf();
|
||||||
if(currentExs != _lastExceptions && currentExs.any())
|
if(currentExs != _lastExceptions && currentExs.any()) {
|
||||||
handleExceptions(currentExs);
|
handleExceptions(currentExs)
|
||||||
|
feed?.exceptions = listOf()
|
||||||
|
}
|
||||||
|
|
||||||
return@TaskHandler resp;
|
return@TaskHandler resp;
|
||||||
})
|
})
|
||||||
@@ -276,7 +259,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 +347,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 +379,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 +434,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 +445,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 +464,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));
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-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) {
|
||||||
@@ -142,6 +151,7 @@ class TutorialFragment : MainFragment() {
|
|||||||
override val rating: IRating = RatingLikes(-1)
|
override val rating: IRating = RatingLikes(-1)
|
||||||
override val viewCount: Long = -1
|
override val viewCount: Long = -1
|
||||||
override val video: IVideoSourceDescriptor = TutorialVideoSourceDescriptor(videoUrl, duration, width, height)
|
override val video: IVideoSourceDescriptor = TutorialVideoSourceDescriptor(videoUrl, duration, width, height)
|
||||||
|
override val isShort: Boolean = false;
|
||||||
override fun getComments(client: IPlatformClient): IPager<IPlatformComment> {
|
override fun getComments(client: IPlatformClient): IPager<IPlatformComment> {
|
||||||
return EmptyPager()
|
return EmptyPager()
|
||||||
}
|
}
|
||||||
@@ -150,7 +160,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 +210,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
|
||||||
|
|||||||
+317
-68
@@ -1,21 +1,28 @@
|
|||||||
package com.futo.platformplayer.fragment.mainactivity.main
|
package com.futo.platformplayer.fragment.mainactivity.main
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
import android.content.pm.ActivityInfo
|
import android.content.pm.ActivityInfo
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
|
import android.database.ContentObserver
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.util.Log
|
import android.os.Looper
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
import android.view.OrientationEventListener
|
||||||
|
import android.view.Surface
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.WindowInsets
|
import android.view.WindowInsets
|
||||||
import android.view.WindowInsetsController
|
import android.view.WindowInsetsController
|
||||||
|
import android.view.WindowManager
|
||||||
import androidx.constraintlayout.motion.widget.MotionLayout
|
import androidx.constraintlayout.motion.widget.MotionLayout
|
||||||
|
import androidx.core.view.ViewCompat.getDisplay
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.media3.common.util.UnstableApi
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.SimpleOrientationListener
|
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.activities.SettingsActivity
|
import com.futo.platformplayer.activities.SettingsActivity
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
@@ -23,29 +30,36 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
|||||||
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.AutoRotateChangeListener
|
|
||||||
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 kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
|
||||||
class VideoDetailFragment : MainFragment {
|
//region Fragment
|
||||||
override val isMainView : Boolean = false;
|
@UnstableApi
|
||||||
|
class VideoDetailFragment() : MainFragment() {
|
||||||
|
override val isMainView: Boolean = false;
|
||||||
override val hasBottomBar: Boolean = true;
|
override val hasBottomBar: Boolean = true;
|
||||||
override val isOverlay : Boolean = true;
|
override val isOverlay: Boolean = true;
|
||||||
override val isHistory: Boolean = false;
|
override val isHistory: Boolean = false;
|
||||||
|
|
||||||
private var _isActive: Boolean = false;
|
private var _isActive: Boolean = false;
|
||||||
|
|
||||||
private var _viewDetail : VideoDetailView? = null;
|
private var _viewDetail : VideoDetailView? = null;
|
||||||
private var _view : SingleViewTouchableMotionLayout? = null;
|
private var _view : SingleViewTouchableMotionLayout? = null;
|
||||||
private lateinit var _autoRotateChangeListener: AutoRotateChangeListener
|
|
||||||
private lateinit var _orientationListener: SimpleOrientationListener
|
|
||||||
private var _currentOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
|
||||||
|
|
||||||
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;
|
||||||
@@ -74,9 +88,9 @@ class VideoDetailFragment : MainFragment {
|
|||||||
private var _loadUrlOnCreate: UrlVideoWithTime? = null;
|
private var _loadUrlOnCreate: UrlVideoWithTime? = null;
|
||||||
private var _leavingPiP = false;
|
private var _leavingPiP = false;
|
||||||
|
|
||||||
//region Fragment
|
private var _landscapeOrientationListener: LandscapeOrientationListener? = null
|
||||||
constructor() : super() {
|
private var _portraitOrientationListener: PortraitOrientationListener? = null
|
||||||
}
|
private var _autoRotateObserver: AutoRotateObserver? = null
|
||||||
|
|
||||||
fun nextVideo() {
|
fun nextVideo() {
|
||||||
_viewDetail?.nextVideo(true, true, true);
|
_viewDetail?.nextVideo(true, true, true);
|
||||||
@@ -86,27 +100,153 @@ class VideoDetailFragment : MainFragment {
|
|||||||
_viewDetail?.prevVideo(true);
|
_viewDetail?.prevVideo(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onStateChanged(state: VideoDetailFragment.State) {
|
private fun isSmallWindow(): Boolean {
|
||||||
|
return resources.configuration.smallestScreenWidthDp < resources.getInteger(R.integer.column_width_dp) * 2
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isAutoRotateEnabled(): Boolean {
|
||||||
|
return android.provider.Settings.System.getInt(
|
||||||
|
context?.contentResolver,
|
||||||
|
android.provider.Settings.System.ACCELEROMETER_ROTATION, 0
|
||||||
|
) == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
&& !isInPictureInPicture
|
||||||
|
&& 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()
|
updateOrientation()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateOrientation() {
|
private fun onVideoChanged(videoWidth : Int, videoHeight: Int) {
|
||||||
val isMaximized = state == State.MAXIMIZED
|
if (
|
||||||
val isFullScreenPortraitAllowed = Settings.instance.playback.fullscreenPortrait;
|
isSmallWindow()
|
||||||
val currentOrientation = _currentOrientation
|
&& state == State.MAXIMIZED
|
||||||
val isFs = isFullscreen
|
&& !isFullscreen
|
||||||
|
&& videoHeight > videoWidth
|
||||||
if (isFs && isMaximized) {
|
) {
|
||||||
if (isFullScreenPortraitAllowed) {
|
_viewDetail?.setFullscreen(true)
|
||||||
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR
|
|
||||||
} else {
|
|
||||||
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.i(TAG, "updateOrientation (isFs = ${isFs}, currentOrientation = ${currentOrientation}, isMaximized = ${isMaximized}, isFullScreenPortraitAllowed = ${isFullScreenPortraitAllowed}) resulted in requested orientation ${activity?.requestedOrientation}");
|
updateOrientation()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateOrientation() {
|
||||||
|
val a = activity ?: return
|
||||||
|
val isFullScreenPortraitAllowed = Settings.instance.playback.fullscreenPortrait
|
||||||
|
val isReversePortraitAllowed = Settings.instance.playback.reversePortrait
|
||||||
|
val rotationLock = StatePlayer.instance.rotationLock
|
||||||
|
val alwaysAllowReverseLandscapeAutoRotate = Settings.instance.playback.alwaysAllowReverseLandscapeAutoRotate
|
||||||
|
|
||||||
|
val isLandscapeVideo: Boolean = _viewDetail?.isLandscapeVideo() ?: true
|
||||||
|
|
||||||
|
val isSmallWindow = isSmallWindow()
|
||||||
|
val autoRotateEnabled = isAutoRotateEnabled()
|
||||||
|
|
||||||
|
// 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 (alwaysAllowReverseLandscapeAutoRotate){
|
||||||
|
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|
||||||
|
} else {
|
||||||
|
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
|
||||||
|
}
|
||||||
|
if (autoRotateEnabled
|
||||||
|
) {
|
||||||
|
// start listening for the device to rotate to landscape
|
||||||
|
// at which point we'll be able to set requestedOrientation to back to UNSPECIFIED
|
||||||
|
_landscapeOrientationListener?.enableListener()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// For small windows if always all reverse landscape then we'll lock the orientation to landscape when system auto-rotate is off to make sure that locking
|
||||||
|
// and unlockiung in the player settings keep orientation in landscape
|
||||||
|
else if (isSmallWindow && isFullscreen && !isFullScreenPortraitAllowed && alwaysAllowReverseLandscapeAutoRotate && !rotationLock && isLandscapeVideo && !autoRotateEnabled) {
|
||||||
|
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_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
|
||||||
|
// only do this if auto-rotate is on portrait is forced when leaving full screen for autorotate off
|
||||||
|
else if (isSmallWindow && !isMinimizingFromFullScreen && !isFullscreen && state == State.MAXIMIZED && resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||||
|
@SuppressLint("SourceLockedOrientationActivity")
|
||||||
|
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
|
||||||
|
if (autoRotateEnabled
|
||||||
|
) {
|
||||||
|
// start listening for the device to rotate to portrait
|
||||||
|
// at which point we'll be able to set requestedOrientation to back to UNSPECIFIED
|
||||||
|
_portraitOrientationListener?.enableListener()
|
||||||
|
}
|
||||||
|
} else if (rotationLock) {
|
||||||
|
_portraitOrientationListener?.disableListener()
|
||||||
|
_landscapeOrientationListener?.disableListener()
|
||||||
|
val display = getDisplay(_viewDetail!!)
|
||||||
|
val rotation = display!!.rotation
|
||||||
|
val orientation = resources.configuration.orientation
|
||||||
|
|
||||||
|
a.requestedOrientation = when (orientation) {
|
||||||
|
Configuration.ORIENTATION_PORTRAIT -> {
|
||||||
|
if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) {
|
||||||
|
if (rotation == Surface.ROTATION_0) {
|
||||||
|
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||||
|
} else {
|
||||||
|
ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Configuration.ORIENTATION_LANDSCAPE -> {
|
||||||
|
if (rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) {
|
||||||
|
if (rotation == Surface.ROTATION_90) {
|
||||||
|
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||||
|
} else {
|
||||||
|
ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_portraitOrientationListener?.disableListener()
|
||||||
|
_landscapeOrientationListener?.disableListener()
|
||||||
|
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) {
|
||||||
@@ -148,10 +288,6 @@ class VideoDetailFragment : MainFragment {
|
|||||||
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;
|
||||||
@@ -191,7 +327,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 {
|
||||||
@@ -228,6 +366,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,10 +405,6 @@ class VideoDetailFragment : MainFragment {
|
|||||||
minimizeVideoDetail();
|
minimizeVideoDetail();
|
||||||
}
|
}
|
||||||
|
|
||||||
_autoRotateChangeListener = AutoRotateChangeListener(requireContext(), Handler()) { _ ->
|
|
||||||
updateOrientation()
|
|
||||||
}
|
|
||||||
|
|
||||||
_loadUrlOnCreate?.let { _viewDetail?.setVideo(it.url, it.timeSeconds, it.playWhenReady) };
|
_loadUrlOnCreate?.let { _viewDetail?.setVideo(it.url, it.timeSeconds, it.playWhenReady) };
|
||||||
maximizeVideoDetail();
|
maximizeVideoDetail();
|
||||||
|
|
||||||
@@ -281,25 +416,29 @@ class VideoDetailFragment : MainFragment {
|
|||||||
updateOrientation()
|
updateOrientation()
|
||||||
}
|
}
|
||||||
|
|
||||||
_orientationListener = SimpleOrientationListener(requireActivity(), lifecycleScope)
|
val delayBeforeRemoveRotationLock = 800L
|
||||||
_orientationListener.onOrientationChanged.subscribe {
|
|
||||||
_currentOrientation = it
|
|
||||||
Logger.i(TAG, "Current orientation changed (_currentOrientation = ${_currentOrientation})")
|
|
||||||
|
|
||||||
if (Settings.instance.playback.isAutoRotate()) {
|
_landscapeOrientationListener = LandscapeOrientationListener(requireContext())
|
||||||
if (state == State.MAXIMIZED && !isFullscreen && (it == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE || it == ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE)) {
|
{
|
||||||
_viewDetail?.setFullscreen(true)
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
return@subscribe
|
// delay to make sure that the system auto rotate updates
|
||||||
}
|
delay(delayBeforeRemoveRotationLock)
|
||||||
|
updateOrientation()
|
||||||
if (state == State.MAXIMIZED && isFullscreen && !Settings.instance.playback.fullscreenPortrait && (it == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT || it == ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT)) {
|
|
||||||
_viewDetail?.setFullscreen(false)
|
|
||||||
return@subscribe
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
_portraitOrientationListener = PortraitOrientationListener(requireContext())
|
||||||
|
{
|
||||||
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
|
// delay to make sure that the system auto rotate updates
|
||||||
|
delay(delayBeforeRemoveRotationLock)
|
||||||
|
updateOrientation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_autoRotateObserver = AutoRotateObserver(requireContext(), Handler(Looper.getMainLooper())) {
|
||||||
updateOrientation()
|
updateOrientation()
|
||||||
}
|
}
|
||||||
|
_autoRotateObserver?.startObserving()
|
||||||
|
|
||||||
return _view!!;
|
return _view!!;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -391,19 +530,20 @@ 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");
|
||||||
_autoRotateChangeListener?.unregister()
|
|
||||||
_orientationListener.stopListening()
|
|
||||||
|
|
||||||
SettingsActivity.settingsActivityClosed.remove(this)
|
SettingsActivity.settingsActivityClosed.remove(this)
|
||||||
StatePlayer.instance.onRotationLockChanged.remove(this)
|
StatePlayer.instance.onRotationLockChanged.remove(this)
|
||||||
|
|
||||||
|
_landscapeOrientationListener?.disableListener()
|
||||||
|
_portraitOrientationListener?.disableListener()
|
||||||
|
_autoRotateObserver?.stopObserving()
|
||||||
|
|
||||||
_viewDetail?.let {
|
_viewDetail?.let {
|
||||||
_viewDetail = null;
|
_viewDetail = null;
|
||||||
it.onDestroy();
|
it.onDestroy();
|
||||||
@@ -426,22 +566,42 @@ class VideoDetailFragment : MainFragment {
|
|||||||
onMaximized.clear();
|
onMaximized.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun hideSystemUI() {
|
private fun hideSystemUI() {
|
||||||
WindowCompat.setDecorFitsSystemWindows(requireActivity().window, false)
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
activity?.window?.insetsController?.let { controller ->
|
WindowCompat.setDecorFitsSystemWindows(requireActivity().window, false)
|
||||||
controller.hide(WindowInsets.Type.statusBars())
|
activity?.window?.insetsController?.let { controller ->
|
||||||
controller.hide(WindowInsets.Type.systemBars())
|
controller.hide(WindowInsets.Type.statusBars())
|
||||||
controller.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
controller.hide(WindowInsets.Type.systemBars())
|
||||||
|
controller.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
@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() {
|
private fun showSystemUI() {
|
||||||
WindowCompat.setDecorFitsSystemWindows(requireActivity().window, true)
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
activity?.window?.insetsController?.let { controller ->
|
WindowCompat.setDecorFitsSystemWindows(requireActivity().window, true)
|
||||||
controller.show(WindowInsets.Type.statusBars())
|
activity?.window?.insetsController?.let { controller ->
|
||||||
controller.show(WindowInsets.Type.systemBars())
|
controller.show(WindowInsets.Type.statusBars())
|
||||||
controller.systemBarsBehavior = WindowInsetsController.BEHAVIOR_DEFAULT
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -455,12 +615,17 @@ class VideoDetailFragment : MainFragment {
|
|||||||
showSystemUI()
|
showSystemUI()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// temporarily force the device to portrait if auto-rotate is disabled to prevent landscape when exiting full screen on a small device
|
||||||
|
// @SuppressLint("SourceLockedOrientationActivity")
|
||||||
|
// if (!isFullscreen && isSmallWindow() && !isAutoRotateEnabled() && !isMinimizingFromFullScreen) {
|
||||||
|
// activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
|
||||||
|
// }
|
||||||
updateOrientation();
|
updateOrientation();
|
||||||
_view?.allowMotion = !fullscreen;
|
_view?.allowMotion = !fullscreen;
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = "VideoDetailFragment";
|
private const val TAG = "VideoDetailFragment";
|
||||||
|
|
||||||
fun newInstance() = VideoDetailFragment().apply {}
|
fun newInstance() = VideoDetailFragment().apply {}
|
||||||
}
|
}
|
||||||
@@ -476,4 +641,88 @@ class VideoDetailFragment : MainFragment {
|
|||||||
//region View
|
//region View
|
||||||
//TODO: Determine if encapsulated would be readable enough
|
//TODO: Determine if encapsulated would be readable enough
|
||||||
//endregion
|
//endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class LandscapeOrientationListener(
|
||||||
|
context: Context,
|
||||||
|
private val onLandscapeDetected: () -> Unit
|
||||||
|
) : OrientationEventListener(context) {
|
||||||
|
|
||||||
|
private var isListening = false
|
||||||
|
|
||||||
|
override fun onOrientationChanged(orientation: Int) {
|
||||||
|
if (!isListening) return
|
||||||
|
|
||||||
|
if (orientation in 60..120 || orientation in 240..300) {
|
||||||
|
onLandscapeDetected()
|
||||||
|
disableListener()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun enableListener() {
|
||||||
|
if (!isListening) {
|
||||||
|
isListening = true
|
||||||
|
enable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun disableListener() {
|
||||||
|
if (isListening) {
|
||||||
|
isListening = false
|
||||||
|
disable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PortraitOrientationListener(
|
||||||
|
context: Context,
|
||||||
|
private val onPortraitDetected: () -> Unit
|
||||||
|
) : OrientationEventListener(context) {
|
||||||
|
|
||||||
|
private var isListening = false
|
||||||
|
|
||||||
|
override fun onOrientationChanged(orientation: Int) {
|
||||||
|
if (!isListening) return
|
||||||
|
|
||||||
|
if (orientation in 0..30 || orientation in 330..360 || orientation in 150..210) {
|
||||||
|
onPortraitDetected()
|
||||||
|
disableListener()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun enableListener() {
|
||||||
|
if (!isListening) {
|
||||||
|
isListening = true
|
||||||
|
enable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun disableListener() {
|
||||||
|
if (isListening) {
|
||||||
|
isListening = false
|
||||||
|
disable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AutoRotateObserver(context: Context, handler: Handler, private val onAutoRotateChanged: () -> Unit) : ContentObserver(handler) {
|
||||||
|
private val contentResolver = context.contentResolver
|
||||||
|
|
||||||
|
override fun onChange(selfChange: Boolean) {
|
||||||
|
super.onChange(selfChange)
|
||||||
|
|
||||||
|
onAutoRotateChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startObserving() {
|
||||||
|
contentResolver.registerContentObserver(
|
||||||
|
android.provider.Settings.System.getUriFor(android.provider.Settings.System.ACCELEROMETER_ROTATION),
|
||||||
|
false,
|
||||||
|
this
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stopObserving() {
|
||||||
|
contentResolver.unregisterContentObserver(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+500
-159
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user