mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-04-24 21:44:49 +00:00
Compare commits
No commits in common. "master" and "240" have entirely different histories.
543 changed files with 5156 additions and 34404 deletions
94
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
94
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
@ -1,94 +0,0 @@
|
||||||
name: Bug Report
|
|
||||||
description: Let us know about an unexpected error, a crash, or an incorrect behavior.
|
|
||||||
labels: ["Bug"]
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
# Thank you for taking the time to fill out this bug report.
|
|
||||||
|
|
||||||
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay Android Application
|
|
||||||
|
|
||||||
For general usage questions, please see: [The Official FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)
|
|
||||||
|
|
||||||
## Filing a bug report
|
|
||||||
|
|
||||||
To fix your issues faster, we need clear reproduction cases - ideally allowing us to make it happen locally.
|
|
||||||
* Please include all needed context. For example, Device, OS, Application, your Grayjay Configurations and Plugin versioning info.
|
|
||||||
* if you've found out a particular series of UI interactions can introduce buggy behavior, please label those steps 1-n with markdown
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: what-happened
|
|
||||||
attributes:
|
|
||||||
label: What happened?
|
|
||||||
description: What did you expect to happen?
|
|
||||||
placeholder: Tell us what you see!
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: grayjay-version
|
|
||||||
attributes:
|
|
||||||
label: Grayjay Version
|
|
||||||
description: In the application, select More > Settings, scroll to the bottom and locate the value next to "Version Name".
|
|
||||||
placeholder: "242"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: dropdown
|
|
||||||
id: plugin
|
|
||||||
attributes:
|
|
||||||
label: What plugins are you seeing the problem on?
|
|
||||||
multiple: true
|
|
||||||
options:
|
|
||||||
- "All"
|
|
||||||
- "Youtube"
|
|
||||||
- "Odysee"
|
|
||||||
- "Rumble"
|
|
||||||
- "Kick"
|
|
||||||
- "Twitch"
|
|
||||||
- "PeerTube"
|
|
||||||
- "Patreon"
|
|
||||||
- "Nebula"
|
|
||||||
- "BiliBili (CN)"
|
|
||||||
- "Bitchute"
|
|
||||||
- "SoundCloud"
|
|
||||||
- "Dailymotion"
|
|
||||||
- "Apple Podcasts"
|
|
||||||
- "Other"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: plugin-version
|
|
||||||
attributes:
|
|
||||||
label: Plugin Version
|
|
||||||
description: In the application, select Sources > [the broken plugin], write down the value under "Version".
|
|
||||||
placeholder: "12"
|
|
||||||
|
|
||||||
- type: checkboxes
|
|
||||||
id: login
|
|
||||||
attributes:
|
|
||||||
label: When do you experience the issue?
|
|
||||||
options:
|
|
||||||
- label: While logged in
|
|
||||||
- label: While logged out
|
|
||||||
- label: N/A
|
|
||||||
|
|
||||||
- type: dropdown
|
|
||||||
id: vpn
|
|
||||||
attributes:
|
|
||||||
label: Are you using a VPN?
|
|
||||||
multiple: false
|
|
||||||
options:
|
|
||||||
- "No"
|
|
||||||
- "Yes"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: logs
|
|
||||||
attributes:
|
|
||||||
label: Relevant log output
|
|
||||||
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
|
||||||
render: shell
|
|
8
.github/ISSUE_TEMPLATE/config.yml
vendored
8
.github/ISSUE_TEMPLATE/config.yml
vendored
|
@ -1,8 +0,0 @@
|
||||||
blank_issues_enabled: false
|
|
||||||
contact_links:
|
|
||||||
- name: Need a Grayjay License?
|
|
||||||
url: https://pay.futo.org/api/PaymentPortal
|
|
||||||
about: Purchase a Grayjay license with FutoPay
|
|
||||||
- name: Plugin Building, Usage, or other Questions
|
|
||||||
url: https://chat.futo.org/#narrow/stream/46-Grayjay
|
|
||||||
about: Grayjays Community Chat
|
|
63
.github/ISSUE_TEMPLATE/documentation_issue.yml
vendored
63
.github/ISSUE_TEMPLATE/documentation_issue.yml
vendored
|
@ -1,63 +0,0 @@
|
||||||
name: Documentation Issue
|
|
||||||
description: Report an issue or suggest a change in the documentation.
|
|
||||||
labels: ["Documentation"]
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
# Thank you for opening a documentation change request.
|
|
||||||
|
|
||||||
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay Android Application. Use the `Documentation` issue type to report problems with the documentation in our code repositories, inside the application, or on [https://grayjay.app](https://grayjay.app)
|
|
||||||
Technical writers monitor this issue type, so report Grayjay bugs or feature requests with the `Bug report` or `Feature Request` issue types instead to get engineering attention.
|
|
||||||
|
|
||||||
For general usage questions, please see: [The Official FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: grayjay-affected-pages
|
|
||||||
attributes:
|
|
||||||
label: Affected Pages
|
|
||||||
description: |
|
|
||||||
Link to or describe the pages relevant to your documentation change request.
|
|
||||||
placeholder:
|
|
||||||
value:
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: grayjay-problem
|
|
||||||
attributes:
|
|
||||||
label: What is the docs issue?
|
|
||||||
description: What problems or suggestions do you have about the documentation?
|
|
||||||
placeholder:
|
|
||||||
value:
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: grayjay-proposal
|
|
||||||
attributes:
|
|
||||||
label: Proposal
|
|
||||||
description: What documentation changes would fix this issue and where would you expect to find them? Are one or more page headings unclear? Do one or more pages need additional context, examples, or warnings? Do we need a new page or section dedicated to a specific topic? Your ideas help us understand what you and other users need from our documentation and how we can improve the content.
|
|
||||||
placeholder:
|
|
||||||
value:
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: grayjay-references
|
|
||||||
attributes:
|
|
||||||
label: References
|
|
||||||
description: |
|
|
||||||
Are there any other open or closed GitLab/GitHub issues related to the problem or solution you described? If so, list them below. For example:
|
|
||||||
```
|
|
||||||
- #6017
|
|
||||||
```
|
|
||||||
placeholder:
|
|
||||||
value:
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
**Note:** If the submit button is disabled and you have filled out all required fields, please check that you did not forget a **Title** for the issue.
|
|
56
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
56
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
|
@ -1,56 +0,0 @@
|
||||||
name: Feature Request
|
|
||||||
description: Suggest a new feature or other enhancement.
|
|
||||||
labels: ["Enhancement"]
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
# Thank you for opening a feature request.
|
|
||||||
|
|
||||||
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay Android Application
|
|
||||||
|
|
||||||
For discussion related to enhancements, please see: [The FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: grayjay-use-case
|
|
||||||
attributes:
|
|
||||||
label: Use Cases
|
|
||||||
description: |
|
|
||||||
In order to properly evaluate a feature request, it is necessary to understand the use cases for it. Please describe below the _end goal_ you are trying to achieve that has led you to request this feature. Please keep this section focused on the problem and not on the suggested solution.
|
|
||||||
placeholder:
|
|
||||||
value:
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: grayjay-proposal
|
|
||||||
attributes:
|
|
||||||
label: Proposal
|
|
||||||
description: |
|
|
||||||
If you have an idea for a way to address the problem via a change to Grayjay features, please describe it below.
|
|
||||||
|
|
||||||
In this section, it's helpful to include specific examples of how what you are suggesting might look in the application, this allows us to understand the full picture of what you are proposing. If you're not sure of some details, don't worry! When we evaluate the feature request we may suggest modifications as necessary to work within the design constraints of the Grayjay Core Application.
|
|
||||||
placeholder:
|
|
||||||
value:
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: grayjay-references
|
|
||||||
attributes:
|
|
||||||
label: References
|
|
||||||
description: |
|
|
||||||
Are there any other GitHub issues, whether open or closed, that are related to the problem you've described above or to the suggested solution? If so, please create a list below that mentions each of them. For example:
|
|
||||||
```
|
|
||||||
- #10
|
|
||||||
```
|
|
||||||
placeholder:
|
|
||||||
value:
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
**Note:** If the submit button is disabled and you have filled out all required fields, please check that you did not forget a **Title** for the issue.
|
|
||||||
|
|
30
.gitmodules
vendored
30
.gitmodules
vendored
|
@ -64,33 +64,3 @@
|
||||||
[submodule "app/src/stable/assets/sources/bilibili"]
|
[submodule "app/src/stable/assets/sources/bilibili"]
|
||||||
path = app/src/stable/assets/sources/bilibili
|
path = app/src/stable/assets/sources/bilibili
|
||||||
url = ../plugins/bilibili.git
|
url = ../plugins/bilibili.git
|
||||||
[submodule "app/src/stable/assets/sources/spotify"]
|
|
||||||
path = app/src/stable/assets/sources/spotify
|
|
||||||
url = ../plugins/spotify.git
|
|
||||||
[submodule "app/src/unstable/assets/sources/spotify"]
|
|
||||||
path = app/src/unstable/assets/sources/spotify
|
|
||||||
url = ../plugins/spotify.git
|
|
||||||
[submodule "app/src/stable/assets/sources/bitchute"]
|
|
||||||
path = app/src/stable/assets/sources/bitchute
|
|
||||||
url = ../plugins/bitchute.git
|
|
||||||
[submodule "app/src/unstable/assets/sources/bitchute"]
|
|
||||||
path = app/src/unstable/assets/sources/bitchute
|
|
||||||
url = ../plugins/bitchute.git
|
|
||||||
[submodule "app/src/unstable/assets/sources/dailymotion"]
|
|
||||||
path = app/src/unstable/assets/sources/dailymotion
|
|
||||||
url = ../plugins/dailymotion.git
|
|
||||||
[submodule "app/src/stable/assets/sources/dailymotion"]
|
|
||||||
path = app/src/stable/assets/sources/dailymotion
|
|
||||||
url = ../plugins/dailymotion.git
|
|
||||||
[submodule "app/src/stable/assets/sources/apple-podcast"]
|
|
||||||
path = app/src/stable/assets/sources/apple-podcasts
|
|
||||||
url = ../plugins/apple-podcasts.git
|
|
||||||
[submodule "app/src/unstable/assets/sources/apple-podcasts"]
|
|
||||||
path = app/src/unstable/assets/sources/apple-podcasts
|
|
||||||
url = ../plugins/apple-podcasts.git
|
|
||||||
[submodule "app/src/stable/assets/sources/tedtalks"]
|
|
||||||
path = app/src/stable/assets/sources/tedtalks
|
|
||||||
url = ../plugins/tedtalks.git
|
|
||||||
[submodule "app/src/unstable/assets/sources/tedtalks"]
|
|
||||||
path = app/src/unstable/assets/sources/tedtalks
|
|
||||||
url = ../plugins/tedtalks.git
|
|
||||||
|
|
|
@ -49,23 +49,9 @@ 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.**
|
||||||
|
|
||||||
### License
|
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.
|
||||||
|
|
||||||
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).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
32
LICENSE
Normal file
32
LICENSE
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
# FUTO TEMPORARY LICENSE
|
||||||
|
This license grants you the rights, and only the rights, set out below in respect of the source code provided. If you take advantage of these rights, you accept this license. If you do not accept the license, do not access the code.
|
||||||
|
|
||||||
|
Words used in the Terms of Service have the same meaning in this license. Where there is any inconsistency between this license and those Terms of Service, these terms prevail.
|
||||||
|
|
||||||
|
## Section 1: Definitions
|
||||||
|
- "code" means the source code made available from time, in our sole discretion, for access under this license. Reference to code in this license means the code and any part of it and any derivative of it.
|
||||||
|
- “compilation” means to compile the code from ‘source code’ to ‘machine code’.
|
||||||
|
- "defect" means a defect, bug, backdoor, security issue or other deficiency in the code.
|
||||||
|
- “non-commercial distribution” means distribution of the code or any compilation of the code, or of any other application or program containing the code or any compilation of the code, where such distribution is not intended for or directed towards commercial advantage or monetary compensation.
|
||||||
|
- "review" means to access, analyse, test and otherwise review the code as a reference, for the sole purpose of analysing it for defects.
|
||||||
|
- "you" means the licensee of rights set out in this license.
|
||||||
|
|
||||||
|
## Section 2: Grant of Rights
|
||||||
|
1. Subject to the terms of this license, we grant you a non-transferable, non-exclusive, worldwide, royalty-free license to access and use the code solely for the purposes of review, compilation and non-commercial distribution.
|
||||||
|
2. You may provide the code to anyone else and publish excerpts of it for the purposes of review, compilation and non-commercial distribution, provided that when you do so you make any recipient of the code aware of the terms of this license, they must agree to be bound by the terms of this license and you must attribute the code to the provider.
|
||||||
|
3. Other than in respect of those parts of the code that were developed by other parties and as specified strictly in accordance with the open source and other licenses under which those parts of the code have been made available, as set out on our website or in those items of code, you are not entitled to use or do anything with the code for any commercial or other purpose, other than review, compilation and non-commercial distribution in accordance with the terms of this license.
|
||||||
|
4. Subject to the terms of this license, you must at all times comply with and shall be bound by our Terms of Use, Privacy and Data Policy.
|
||||||
|
|
||||||
|
## Section 3: Limitations
|
||||||
|
1. This license does not grant you any rights to use the provider's name, logo, or trademarks and you must not in any way indicate you are authorised to speak on behalf of the provider.
|
||||||
|
2. If you issue proceedings in any jurisdiction against the provider because you consider the provider has infringed copyright or any patent right in respect of the code (including any joinder or counterclaim), your license to the code is automatically terminated.
|
||||||
|
3. THE CODE IS MADE AVAILABLE "AS-IS" AND WITHOUT ANY EXPRESS OR IMPLIED GUARANTEES AS TO FITNESS, MERCHANTABILITY, NON-INFRINGEMENT OR OTHERWISE. IT IS NOT BEING PROVIDED IN TRADE BUT ON A VOLUNTARY BASIS ON OUR PART AND IS NOT MADE AVAILABLE FOR ANY USE OUTSIDE THE TERMS OF THIS LICENSE. ANYONE ACCESSING THE CODE MUST ENSURE THEY HAVE THE REQUISITE EXPERTISE TO SECURE THEIR OWN SYSTEM AND DEVICES AND TO ACCESS AND USE THE CODE IN ACCORDANCE WITH THE TERMS OF THIS LICENSE. YOU BEAR THE RISK OF ACCESSING AND USING THE CODE. IN PARTICULAR, THE PROVIDER BEARS NO LIABILITY FOR ANY INTERFERENCE WITH OR ADVERSE EFFECT ON YOUR SYSTEM OR DEVICES AS A RESULT OF YOUR ACCESSING AND USING THE CODE IN ACCORDANCE WITH THE TERMS OF THIS LICENSE OR OTHERWISE.
|
||||||
|
|
||||||
|
## Section 4: Termination, suspension and variation
|
||||||
|
1. We may suspend, terminate or vary the terms of this license and any access to the code at any time, without notice, for any reason or no reason, in respect of any licensee, group of licensees or all licensees including as may be applicable any sub-licensees.
|
||||||
|
|
||||||
|
## Section 5: General
|
||||||
|
1. This license and its interpretation and operation are governed solely by the local law. You agree to submit to the exclusive jurisdiction of the local arbitral tribunals as further described in our Terms of Service and you agree not to raise any jurisdictional issue if we need to enforce an arbitral award or judgment in our jurisdiction or another country.
|
||||||
|
2. Questions and comments regarding this license are welcomed and should be addressed at https://chat.futo.org/login/.
|
||||||
|
|
||||||
|
Last updated 7 June 2023.
|
43
LICENSE.md
43
LICENSE.md
|
@ -1,43 +0,0 @@
|
||||||
# Source First License 1.1
|
|
||||||
|
|
||||||
## Acceptance
|
|
||||||
By using the software, you agree to all of the terms and conditions below.
|
|
||||||
|
|
||||||
## Copyright License
|
|
||||||
FUTO Holdings, Inc. (the “Licensor”) grants you a non-exclusive, royalty-free, worldwide, non-sublicensable, non-transferable license to use, copy, distribute, make available, and prepare derivative works of the software, in each case subject to the limitations below.
|
|
||||||
|
|
||||||
## Limitations
|
|
||||||
You may use or modify the software only for non-commercial purposes such as personal use for research, experiment, and testing for the benefit of public knowledge, personal study, private entertainment, hobby projects, amateur pursuits, or religious observance, all without any anticipated commercial application.
|
|
||||||
|
|
||||||
You may distribute the software or provide it to others only if you do so free of charge for non-commercial purposes.
|
|
||||||
|
|
||||||
Notwithstanding the above, you may not remove or obscure any functionality in the software related to payment to the Licensor in any copy you distribute to others.
|
|
||||||
|
|
||||||
You may not alter, remove, or obscure any licensing, copyright, or other notices of the Licensor in the software. Any use of the Licensor’s trademarks is subject to applicable law.
|
|
||||||
|
|
||||||
## Patents
|
|
||||||
If you make any written claim that the software infringes or contributes to infringement of any patent, your license for the software granted under these terms ends immediately. If your company makes such a claim, your license ends immediately for work on behalf of your company.
|
|
||||||
|
|
||||||
## Notices
|
|
||||||
You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms. If you modify the software, you must include in any modified copies of the software a prominent notice stating that you have modified the software, such as but not limited to, a statement in a readme file or an in-application about section.
|
|
||||||
|
|
||||||
## Fair Use
|
|
||||||
You may have "fair use" rights for the software under the law. These terms do not limit them.
|
|
||||||
|
|
||||||
## No Other Rights
|
|
||||||
These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the Licensor from granting licenses to anyone else. These terms do not imply any other licenses.
|
|
||||||
|
|
||||||
## Termination
|
|
||||||
If you use the software in violation of these terms, such use is not licensed, and your license will automatically terminate. If the licensor provides you with a notice of your violation, and you cease all violation of this license no later than 30 days after you receive that notice, your license will be reinstated retroactively. However, if you violate these terms after such reinstatement, any additional violation of these terms will cause your license to terminate automatically and permanently.
|
|
||||||
|
|
||||||
## No Liability
|
|
||||||
As far as the law allows, the software comes as is, without any warranty or condition, and the Licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.
|
|
||||||
|
|
||||||
## Definitions
|
|
||||||
- The “Licensor” is the entity offering these terms, FUTO Holdings, Inc.
|
|
||||||
- The “software” is the software the licensor makes available under these terms, including any portion of it.
|
|
||||||
- “You” refers to the individual or entity agreeing to these terms.
|
|
||||||
- “Your company” is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. Control means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
|
|
||||||
- “Your license” is the license granted to you for the software under these terms.
|
|
||||||
- “Use” means anything you do with the software requiring your license.
|
|
||||||
- “Trademark” means trademarks, service marks, and similar rights.
|
|
35
README.md
35
README.md
|
@ -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.png" height="700" /></b></td>
|
<td><b style="font-size:30px"><img src="images/video.jpg" height="700" /></b></td>
|
||||||
<td><b style="font-size:30px"><img src="images/video-details.png" height="700" /></b></td>
|
<td><b style="font-size:30px"><img src="images/video-details.jpg" height="700" /></b></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Video</td>
|
<td>Video</td>
|
||||||
|
@ -24,10 +24,12 @@ 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/source.png" height="700" /></b></td>
|
<td><b style="font-size:30px"><img src="images/sources.jpg" height="700" /></b></td>
|
||||||
|
<td><b style="font-size:30px"><img src="images/sources-disabled.jpg" height="700" /></b></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Sources</td>
|
<td>Sources (all enabled)</td>
|
||||||
|
<td>Sources (one disabled)</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
@ -36,7 +38,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.png" height="700" /></b></td>
|
<td><b style="font-size:30px"><img src="images/source-settings.jpg" height="700" /></b></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Install a new source</td>
|
<td>Install a new source</td>
|
||||||
|
@ -52,8 +54,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.png" height="700" /></b></td>
|
<td><b style="font-size:30px"><img src="images/search-list.jpg" height="700" /></b></td>
|
||||||
<td><b style="font-size:30px"><img src="images/search-preview.png" height="700" /></b></td>
|
<td><b style="font-size:30px"><img src="images/search-preview.jpg" height="700" /></b></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Search (list)</td>
|
<td>Search (list)</td>
|
||||||
|
@ -69,7 +71,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.png" height="700" /></b></td>
|
<td><b style="font-size:30px"><img src="images/channel.jpg" height="700" /></b></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Channel</td>
|
<td>Channel</td>
|
||||||
|
@ -110,7 +112,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.png" height="700" /></b></td>
|
<td><b style="font-size:30px"><img src="images/settings.jpg" height="700" /></b></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Settings</td>
|
<td>Settings</td>
|
||||||
|
@ -123,8 +125,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.png" height="700" /></b></td>
|
<td><b style="font-size:30px"><img src="images/playlists.jpg" height="700" /></b></td>
|
||||||
<td><b style="font-size:30px"><img src="images/playlist.png" height="700" /></b></td>
|
<td><b style="font-size:30px"><img src="images/playlist.jpg" height="700" /></b></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Playlists</td>
|
<td>Playlists</td>
|
||||||
|
@ -140,7 +142,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.png" height="700" /></b></td>
|
<td><b style="font-size:30px"><img src="images/downloads.jpg" height="700" /></b></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Downloads</td>
|
<td>Downloads</td>
|
||||||
|
@ -155,7 +157,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.png" height="700" /></b></td>
|
<td><b style="font-size:30px"><img src="images/casting.jpg" height="700" /></b></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Casting</td>
|
<td>Casting</td>
|
||||||
|
@ -180,12 +182,6 @@ 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.
|
||||||
|
|
||||||
|
@ -203,6 +199,7 @@ 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).
|
||||||
|
|
|
@ -2,7 +2,7 @@ plugins {
|
||||||
id 'com.android.application'
|
id 'com.android.application'
|
||||||
id 'org.jetbrains.kotlin.android'
|
id 'org.jetbrains.kotlin.android'
|
||||||
id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.21'
|
id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.21'
|
||||||
id 'org.ajoberstar.grgit' version '5.2.2'
|
id 'org.ajoberstar.grgit' version '1.7.2'
|
||||||
id 'com.google.protobuf'
|
id 'com.google.protobuf'
|
||||||
id 'kotlin-parcelize'
|
id 'kotlin-parcelize'
|
||||||
id 'com.google.devtools.ksp'
|
id 'com.google.devtools.ksp'
|
||||||
|
@ -144,19 +144,9 @@ android {
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
buildConfig true
|
buildConfig true
|
||||||
}
|
}
|
||||||
sourceSets {
|
|
||||||
main {
|
|
||||||
assets {
|
|
||||||
srcDirs 'src/main/assets', 'src/tests/assets', 'src/test/assets'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'com.google.dagger:dagger:2.48'
|
|
||||||
implementation 'androidx.test:monitor:1.7.2'
|
|
||||||
annotationProcessor 'com.google.dagger:dagger-compiler:2.48'
|
|
||||||
|
|
||||||
//Core
|
//Core
|
||||||
implementation 'androidx.core:core-ktx:1.12.0'
|
implementation 'androidx.core:core-ktx:1.12.0'
|
||||||
|
@ -194,10 +184,11 @@ dependencies {
|
||||||
implementation 'androidx.media:media:1.7.0'
|
implementation 'androidx.media:media:1.7.0'
|
||||||
|
|
||||||
//Other
|
//Other
|
||||||
|
implementation 'org.jmdns:jmdns:3.5.1'
|
||||||
implementation 'org.jsoup:jsoup:1.15.3'
|
implementation 'org.jsoup:jsoup:1.15.3'
|
||||||
implementation 'com.google.android.flexbox:flexbox:3.0.0'
|
implementation 'com.google.android.flexbox:flexbox:3.0.0'
|
||||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||||
implementation 'com.arthenica:ffmpeg-kit-full:6.0-2.LTS'
|
implementation 'com.arthenica:ffmpeg-kit-full:5.1'
|
||||||
implementation 'org.jetbrains.kotlin:kotlin-reflect:1.9.0'
|
implementation 'org.jetbrains.kotlin:kotlin-reflect:1.9.0'
|
||||||
implementation 'com.github.dhaval2404:imagepicker:2.1'
|
implementation 'com.github.dhaval2404:imagepicker:2.1'
|
||||||
implementation 'com.google.zxing:core:3.4.1'
|
implementation 'com.google.zxing:core:3.4.1'
|
||||||
|
|
|
@ -1,266 +0,0 @@
|
||||||
package com.futo.platformplayer
|
|
||||||
|
|
||||||
import com.futo.platformplayer.noise.protocol.Noise
|
|
||||||
import com.futo.platformplayer.sync.internal.*
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import org.junit.Assert.*
|
|
||||||
import org.junit.Test
|
|
||||||
import java.net.Socket
|
|
||||||
import java.nio.ByteBuffer
|
|
||||||
import kotlin.random.Random
|
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
|
||||||
|
|
||||||
class SyncServerTests {
|
|
||||||
|
|
||||||
//private val relayHost = "relay.grayjay.app"
|
|
||||||
//private val relayKey = "xGbHRzDOvE6plRbQaFgSen82eijF+gxS0yeUaeEErkw="
|
|
||||||
private val relayKey = "XlUaSpIlRaCg0TGzZ7JYmPupgUHDqTZXUUBco2K7ejw="
|
|
||||||
private val relayHost = "192.168.1.175"
|
|
||||||
private val relayPort = 9000
|
|
||||||
|
|
||||||
/** Creates a client connected to the live relay server. */
|
|
||||||
private suspend fun createClient(
|
|
||||||
onHandshakeComplete: ((SyncSocketSession) -> Unit)? = null,
|
|
||||||
onData: ((SyncSocketSession, UByte, UByte, ByteBuffer) -> Unit)? = null,
|
|
||||||
onNewChannel: ((SyncSocketSession, ChannelRelayed) -> Unit)? = null,
|
|
||||||
isHandshakeAllowed: ((SyncSocketSession, String, String?) -> Boolean)? = null
|
|
||||||
): SyncSocketSession = withContext(Dispatchers.IO) {
|
|
||||||
val p = Noise.createDH("25519")
|
|
||||||
p.generateKeyPair()
|
|
||||||
val socket = Socket(relayHost, relayPort)
|
|
||||||
val inputStream = LittleEndianDataInputStream(socket.getInputStream())
|
|
||||||
val outputStream = LittleEndianDataOutputStream(socket.getOutputStream())
|
|
||||||
val tcs = CompletableDeferred<Boolean>()
|
|
||||||
val socketSession = SyncSocketSession(
|
|
||||||
relayHost,
|
|
||||||
p,
|
|
||||||
inputStream,
|
|
||||||
outputStream,
|
|
||||||
onClose = { socket.close() },
|
|
||||||
onHandshakeComplete = { s ->
|
|
||||||
onHandshakeComplete?.invoke(s)
|
|
||||||
tcs.complete(true)
|
|
||||||
},
|
|
||||||
onData = onData ?: { _, _, _, _ -> },
|
|
||||||
onNewChannel = onNewChannel ?: { _, _ -> },
|
|
||||||
isHandshakeAllowed = isHandshakeAllowed ?: { _, _, _ -> true }
|
|
||||||
)
|
|
||||||
socketSession.authorizable = AlwaysAuthorized()
|
|
||||||
socketSession.startAsInitiator(relayKey)
|
|
||||||
withTimeout(5000.milliseconds) { tcs.await() }
|
|
||||||
return@withContext socketSession
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun multipleClientsHandshake_Success() = runBlocking {
|
|
||||||
val client1 = createClient()
|
|
||||||
val client2 = createClient()
|
|
||||||
assertNotNull(client1.remotePublicKey, "Client 1 handshake failed")
|
|
||||||
assertNotNull(client2.remotePublicKey, "Client 2 handshake failed")
|
|
||||||
client1.stop()
|
|
||||||
client2.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun publishAndRequestConnectionInfo_Authorized_Success() = runBlocking {
|
|
||||||
val clientA = createClient()
|
|
||||||
val clientB = createClient()
|
|
||||||
val clientC = createClient()
|
|
||||||
clientA.publishConnectionInformation(arrayOf(clientB.localPublicKey), 12345, true, true, true, true)
|
|
||||||
delay(100.milliseconds)
|
|
||||||
val infoB = clientB.requestConnectionInfo(clientA.localPublicKey)
|
|
||||||
val infoC = clientC.requestConnectionInfo(clientA.localPublicKey)
|
|
||||||
assertNotNull("Client B should receive connection info", infoB)
|
|
||||||
assertEquals(12345.toUShort(), infoB!!.port)
|
|
||||||
assertNull("Client C should not receive connection info (unauthorized)", infoC)
|
|
||||||
clientA.stop()
|
|
||||||
clientB.stop()
|
|
||||||
clientC.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun relayedTransport_Bidirectional_Success() = runBlocking {
|
|
||||||
val tcsA = CompletableDeferred<ChannelRelayed>()
|
|
||||||
val tcsB = CompletableDeferred<ChannelRelayed>()
|
|
||||||
val clientA = createClient(onNewChannel = { _, c -> tcsA.complete(c) })
|
|
||||||
val clientB = createClient(onNewChannel = { _, c -> tcsB.complete(c) })
|
|
||||||
val channelTask = async { clientA.startRelayedChannel(clientB.localPublicKey) }
|
|
||||||
val channelA = withTimeout(5000.milliseconds) { tcsA.await() }
|
|
||||||
channelA.authorizable = AlwaysAuthorized()
|
|
||||||
val channelB = withTimeout(5000.milliseconds) { tcsB.await() }
|
|
||||||
channelB.authorizable = AlwaysAuthorized()
|
|
||||||
channelTask.await()
|
|
||||||
|
|
||||||
val tcsDataB = CompletableDeferred<ByteArray>()
|
|
||||||
channelB.setDataHandler { _, _, o, so, d ->
|
|
||||||
val b = ByteArray(d.remaining())
|
|
||||||
d.get(b)
|
|
||||||
if (o == Opcode.DATA.value && so == 0u.toUByte()) tcsDataB.complete(b)
|
|
||||||
}
|
|
||||||
channelA.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(byteArrayOf(1, 2, 3)))
|
|
||||||
|
|
||||||
val tcsDataA = CompletableDeferred<ByteArray>()
|
|
||||||
channelA.setDataHandler { _, _, o, so, d ->
|
|
||||||
val b = ByteArray(d.remaining())
|
|
||||||
d.get(b)
|
|
||||||
if (o == Opcode.DATA.value && so == 0u.toUByte()) tcsDataA.complete(b)
|
|
||||||
}
|
|
||||||
channelB.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(byteArrayOf(4, 5, 6)))
|
|
||||||
|
|
||||||
val receivedB = withTimeout(5000.milliseconds) { tcsDataB.await() }
|
|
||||||
val receivedA = withTimeout(5000.milliseconds) { tcsDataA.await() }
|
|
||||||
assertArrayEquals(byteArrayOf(1, 2, 3), receivedB)
|
|
||||||
assertArrayEquals(byteArrayOf(4, 5, 6), receivedA)
|
|
||||||
clientA.stop()
|
|
||||||
clientB.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun relayedTransport_MaximumMessageSize_Success() = runBlocking {
|
|
||||||
val MAX_DATA_PER_PACKET = SyncSocketSession.MAXIMUM_PACKET_SIZE - SyncSocketSession.HEADER_SIZE - 8 - 16 - 16
|
|
||||||
val maxSizeData = ByteArray(MAX_DATA_PER_PACKET).apply { Random.nextBytes(this) }
|
|
||||||
val tcsA = CompletableDeferred<ChannelRelayed>()
|
|
||||||
val tcsB = CompletableDeferred<ChannelRelayed>()
|
|
||||||
val clientA = createClient(onNewChannel = { _, c -> tcsA.complete(c) })
|
|
||||||
val clientB = createClient(onNewChannel = { _, c -> tcsB.complete(c) })
|
|
||||||
val channelTask = async { clientA.startRelayedChannel(clientB.localPublicKey) }
|
|
||||||
val channelA = withTimeout(5000.milliseconds) { tcsA.await() }
|
|
||||||
channelA.authorizable = AlwaysAuthorized()
|
|
||||||
val channelB = withTimeout(5000.milliseconds) { tcsB.await() }
|
|
||||||
channelB.authorizable = AlwaysAuthorized()
|
|
||||||
channelTask.await()
|
|
||||||
|
|
||||||
val tcsDataB = CompletableDeferred<ByteArray>()
|
|
||||||
channelB.setDataHandler { _, _, o, so, d ->
|
|
||||||
val b = ByteArray(d.remaining())
|
|
||||||
d.get(b)
|
|
||||||
if (o == Opcode.DATA.value && so == 0u.toUByte()) tcsDataB.complete(b)
|
|
||||||
}
|
|
||||||
channelA.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(maxSizeData))
|
|
||||||
val receivedData = withTimeout(5000.milliseconds) { tcsDataB.await() }
|
|
||||||
assertArrayEquals(maxSizeData, receivedData)
|
|
||||||
clientA.stop()
|
|
||||||
clientB.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun publishAndGetRecord_Success() = runBlocking {
|
|
||||||
val clientA = createClient()
|
|
||||||
val clientB = createClient()
|
|
||||||
val clientC = createClient()
|
|
||||||
val data = byteArrayOf(1, 2, 3)
|
|
||||||
val success = clientA.publishRecords(listOf(clientB.localPublicKey), "testKey", data)
|
|
||||||
val recordB = clientB.getRecord(clientA.localPublicKey, "testKey")
|
|
||||||
val recordC = clientC.getRecord(clientA.localPublicKey, "testKey")
|
|
||||||
assertTrue(success)
|
|
||||||
assertNotNull(recordB)
|
|
||||||
assertArrayEquals(data, recordB!!.first)
|
|
||||||
assertNull("Unauthorized client should not access record", recordC)
|
|
||||||
clientA.stop()
|
|
||||||
clientB.stop()
|
|
||||||
clientC.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun getNonExistentRecord_ReturnsNull() = runBlocking {
|
|
||||||
val clientA = createClient()
|
|
||||||
val clientB = createClient()
|
|
||||||
val record = clientB.getRecord(clientA.localPublicKey, "nonExistentKey")
|
|
||||||
assertNull("Getting non-existent record should return null", record)
|
|
||||||
clientA.stop()
|
|
||||||
clientB.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun updateRecord_TimestampUpdated() = runBlocking {
|
|
||||||
val clientA = createClient()
|
|
||||||
val clientB = createClient()
|
|
||||||
val key = "updateKey"
|
|
||||||
val data1 = byteArrayOf(1)
|
|
||||||
val data2 = byteArrayOf(2)
|
|
||||||
clientA.publishRecords(listOf(clientB.localPublicKey), key, data1)
|
|
||||||
val record1 = clientB.getRecord(clientA.localPublicKey, key)
|
|
||||||
delay(1000.milliseconds)
|
|
||||||
clientA.publishRecords(listOf(clientB.localPublicKey), key, data2)
|
|
||||||
val record2 = clientB.getRecord(clientA.localPublicKey, key)
|
|
||||||
assertNotNull(record1)
|
|
||||||
assertNotNull(record2)
|
|
||||||
assertTrue(record2!!.second > record1!!.second)
|
|
||||||
assertArrayEquals(data2, record2.first)
|
|
||||||
clientA.stop()
|
|
||||||
clientB.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun deleteRecord_Success() = runBlocking {
|
|
||||||
val clientA = createClient()
|
|
||||||
val clientB = createClient()
|
|
||||||
val data = byteArrayOf(1, 2, 3)
|
|
||||||
clientA.publishRecords(listOf(clientB.localPublicKey), "toDelete", data)
|
|
||||||
val success = clientB.deleteRecords(clientA.localPublicKey, clientB.localPublicKey, listOf("toDelete"))
|
|
||||||
val record = clientB.getRecord(clientA.localPublicKey, "toDelete")
|
|
||||||
assertTrue(success)
|
|
||||||
assertNull(record)
|
|
||||||
clientA.stop()
|
|
||||||
clientB.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun listRecordKeys_Success() = runBlocking {
|
|
||||||
val clientA = createClient()
|
|
||||||
val clientB = createClient()
|
|
||||||
val keys = arrayOf("key1", "key2", "key3")
|
|
||||||
keys.forEach { key ->
|
|
||||||
clientA.publishRecords(listOf(clientB.localPublicKey), key, byteArrayOf(1))
|
|
||||||
}
|
|
||||||
val listedKeys = clientB.listRecordKeys(clientA.localPublicKey, clientB.localPublicKey)
|
|
||||||
assertArrayEquals(keys, listedKeys.map { it.first }.toTypedArray())
|
|
||||||
clientA.stop()
|
|
||||||
clientB.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun singleLargeMessageViaRelayedChannel_Success() = runBlocking {
|
|
||||||
val largeData = ByteArray(100000).apply { Random.nextBytes(this) }
|
|
||||||
val tcsA = CompletableDeferred<ChannelRelayed>()
|
|
||||||
val tcsB = CompletableDeferred<ChannelRelayed>()
|
|
||||||
val clientA = createClient(onNewChannel = { _, c -> tcsA.complete(c) })
|
|
||||||
val clientB = createClient(onNewChannel = { _, c -> tcsB.complete(c) })
|
|
||||||
val channelTask = async { clientA.startRelayedChannel(clientB.localPublicKey) }
|
|
||||||
val channelA = withTimeout(5000.milliseconds) { tcsA.await() }
|
|
||||||
channelA.authorizable = AlwaysAuthorized()
|
|
||||||
val channelB = withTimeout(5000.milliseconds) { tcsB.await() }
|
|
||||||
channelB.authorizable = AlwaysAuthorized()
|
|
||||||
channelTask.await()
|
|
||||||
|
|
||||||
val tcsDataB = CompletableDeferred<ByteArray>()
|
|
||||||
channelB.setDataHandler { _, _, o, so, d ->
|
|
||||||
val b = ByteArray(d.remaining())
|
|
||||||
d.get(b)
|
|
||||||
if (o == Opcode.DATA.value && so == 0u.toUByte()) tcsDataB.complete(b)
|
|
||||||
}
|
|
||||||
channelA.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(largeData))
|
|
||||||
val receivedData = withTimeout(10000.milliseconds) { tcsDataB.await() }
|
|
||||||
assertArrayEquals(largeData, receivedData)
|
|
||||||
clientA.stop()
|
|
||||||
clientB.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun publishAndGetLargeRecord_Success() = runBlocking {
|
|
||||||
val largeData = ByteArray(1000000).apply { Random.nextBytes(this) }
|
|
||||||
val clientA = createClient()
|
|
||||||
val clientB = createClient()
|
|
||||||
val success = clientA.publishRecords(listOf(clientB.localPublicKey), "largeRecord", largeData)
|
|
||||||
val record = clientB.getRecord(clientA.localPublicKey, "largeRecord")
|
|
||||||
assertTrue(success)
|
|
||||||
assertNotNull(record)
|
|
||||||
assertArrayEquals(largeData, record!!.first)
|
|
||||||
clientA.stop()
|
|
||||||
clientB.stop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class AlwaysAuthorized : IAuthorizable {
|
|
||||||
override val isAuthorized: Boolean get() = true
|
|
||||||
}
|
|
|
@ -11,7 +11,6 @@
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
|
||||||
<uses-permission android:name="com.android.alarm.permission.SET_ALARM"/>
|
<uses-permission android:name="com.android.alarm.permission.SET_ALARM"/>
|
||||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
|
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
|
||||||
<!--<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>-->
|
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
|
||||||
<uses-permission android:name="android.permission.WRITE_SETTINGS" tools:ignore="ProtectedPermissions"/>
|
<uses-permission android:name="android.permission.WRITE_SETTINGS" tools:ignore="ProtectedPermissions"/>
|
||||||
|
@ -36,18 +35,15 @@
|
||||||
<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" />
|
||||||
<service android:name=".services.DownloadService"
|
<service android:name=".services.DownloadService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:foregroundServiceType="dataSync" />
|
android:foregroundServiceType="dataSync" />
|
||||||
|
<service android:name=".services.ExportingService"
|
||||||
|
android:enabled="true"
|
||||||
|
android:foregroundServiceType="dataSync" />
|
||||||
|
|
||||||
<receiver android:name=".receivers.MediaControlReceiver" />
|
<receiver android:name=".receivers.MediaControlReceiver" />
|
||||||
<receiver android:name=".receivers.AudioNoisyReceiver" />
|
<receiver android:name=".receivers.AudioNoisyReceiver" />
|
||||||
|
@ -57,8 +53,9 @@
|
||||||
android:name=".activities.MainActivity"
|
android:name=".activities.MainActivity"
|
||||||
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
|
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
android:screenOrientation="portrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar"
|
android:theme="@style/Theme.FutoVideo.NoActionBar"
|
||||||
android:launchMode="singleInstance"
|
android:launchMode="singleTask"
|
||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
android:supportsPictureInPicture="true">
|
android:supportsPictureInPicture="true">
|
||||||
|
|
||||||
|
@ -151,30 +148,34 @@
|
||||||
<data android:scheme="polycentric" />
|
<data android:scheme="polycentric" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.TestActivity"
|
android:name=".activities.TestActivity"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.SettingsActivity"
|
android:name=".activities.SettingsActivity"
|
||||||
|
android:screenOrientation="portrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.DeveloperActivity"
|
android:name=".activities.DeveloperActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="portrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.ExceptionActivity"
|
android:name=".activities.ExceptionActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="portrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.CaptchaActivity"
|
android:name=".activities.CaptchaActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="portrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.LoginActivity"
|
android:name=".activities.LoginActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="portrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.AddSourceActivity"
|
android:name=".activities.AddSourceActivity"
|
||||||
|
android:screenOrientation="portrait"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar">
|
android:theme="@style/Theme.FutoVideo.NoActionBar">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
|
@ -188,55 +189,44 @@
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.AddSourceOptionsActivity"
|
android:name=".activities.AddSourceOptionsActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="portrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.PolycentricHomeActivity"
|
android:name=".activities.PolycentricHomeActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="portrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.PolycentricBackupActivity"
|
android:name=".activities.PolycentricBackupActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="portrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.PolycentricCreateProfileActivity"
|
android:name=".activities.PolycentricCreateProfileActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="portrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.PolycentricProfileActivity"
|
android:name=".activities.PolycentricProfileActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="portrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.PolycentricWhyActivity"
|
android:name=".activities.PolycentricWhyActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="portrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.PolycentricImportProfileActivity"
|
android:name=".activities.PolycentricImportProfileActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="portrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.ManageTabsActivity"
|
android:name=".activities.ManageTabsActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="portrait"
|
||||||
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="portrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.FCastGuideActivity"
|
android:name=".activities.FCastGuideActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="portrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
|
||||||
<activity
|
|
||||||
android:name=".activities.SyncHomeActivity"
|
|
||||||
android:screenOrientation="sensorPortrait"
|
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
|
||||||
<activity
|
|
||||||
android:name=".activities.SyncPairActivity"
|
|
||||||
android:screenOrientation="sensorPortrait"
|
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
|
||||||
<activity
|
|
||||||
android:name=".activities.SyncShowPairingCodeActivity"
|
|
||||||
android:screenOrientation="sensorPortrait"
|
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
|
@ -1,15 +0,0 @@
|
||||||
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g clip-path="url(#clip0_287_2206)">
|
|
||||||
<path d="M22.0557 38.25L43.1117 6H1L22.0557 38.25Z" fill="url(#paint0_linear_287_2206)"/>
|
|
||||||
<path d="M6 28.2444C6.85811 27.3291 8.98625 25.2353 10.6338 24.1827C12.2814 23.13 14.257 20.1209 15.0388 18.7479C17.4224 15.2392 22.7618 7.91286 25.0501 6.67716C25.462 6.35678 26.0608 5.85718 26.3087 5.64745C27.1668 3.7405 30.0844 0.498738 34.8898 2.78706C35.3017 2.64974 36.32 2.61542 36.7777 2.61542C36.4153 2.86334 35.6564 3.58795 35.5191 4.50328C35.153 7.02039 33.7647 8.48874 33.1164 8.90825C32.6587 11.8259 32.0294 14.4002 30.6564 15.3155L31.915 17.5466C33.8029 19.5489 37.7159 23.8737 38.2649 25.1552C36.4344 24.5603 35.2521 23.992 34.8898 23.7822L38.2649 28.416C36.2818 28.2635 31.8235 26.9744 29.8556 23.0385C30.6336 25.1438 31.4001 27.7677 31.6862 28.8165C30.6183 27.9393 28.3224 25.3955 27.6816 22.2376C27.8647 25.304 27.8342 27.4816 27.7961 28.1872C27.2812 27.7105 26.0913 26.2307 25.4505 24.1255V27.6723C24.6821 26.604 23.1363 24.0104 22.9967 22.0533C23.1255 24.2716 23.047 25.3115 22.9906 25.5556L20.0731 22.8097C19.2912 23.2292 17.1898 24.1827 15.0388 24.6403C13.5743 25.876 11.797 28.969 11.0915 30.3611V28.5877L9.14643 30.5327L9.83291 28.4733L8.57433 29.5602C8.28828 29.7318 7.62468 30.0751 7.25857 30.0751C7.39585 29.7547 7.65904 29.4076 7.77345 29.2741L6.11441 29.9034C6.3051 29.3504 6.90388 28.13 7.77345 27.6723C6.58351 28.13 6.09536 28.2444 6 28.2444Z" fill="white"/>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="paint0_linear_287_2206" x1="22.0557" y1="38.25" x2="22.0557" y2="-4.75" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stop-color="#01D6E6"/>
|
|
||||||
<stop offset="1" stop-color="#0182E7"/>
|
|
||||||
</linearGradient>
|
|
||||||
<clipPath id="clip0_287_2206">
|
|
||||||
<rect width="44" height="44" fill="white"/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 1.8 KiB |
|
@ -262,17 +262,6 @@ function getDevLogs(lastIndex, cb) {
|
||||||
.then(x=>x.json())
|
.then(x=>x.json())
|
||||||
.then(y=> cb && cb(y));
|
.then(y=> cb && cb(y));
|
||||||
}
|
}
|
||||||
function getDevHttpExchanges(cb) {
|
|
||||||
fetch("/plugin/getDevHttpExchanges", {
|
|
||||||
timeout: 1000
|
|
||||||
})
|
|
||||||
.then(x=>x.json())
|
|
||||||
.then(y=> cb && cb(y));
|
|
||||||
}
|
|
||||||
function setDevHttpProxy(url, port) {
|
|
||||||
return fetch("/dev/setDevProxy?url=" + encodeURIComponent(url) + "&port=" + port)
|
|
||||||
.then(x=>x.json());
|
|
||||||
}
|
|
||||||
function sendFakeDevLog(devId, msg) {
|
function sendFakeDevLog(devId, msg) {
|
||||||
return syncGET("/plugin/fakeDevLog?devId=" + devId + "&msg=" + msg, {});
|
return syncGET("/plugin/fakeDevLog?devId=" + devId + "&msg=" + msg, {});
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,9 +7,6 @@
|
||||||
<!--<link href="./dependencies/vuetify.min.css" rel="stylesheet">-->
|
<!--<link href="./dependencies/vuetify.min.css" rel="stylesheet">-->
|
||||||
<link href="https://cdn.jsdelivr.net/npm/vuetify@2.7.1/dist/vuetify.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/vuetify@2.7.1/dist/vuetify.min.css" rel="stylesheet">
|
||||||
|
|
||||||
<title>DevPortal</title>
|
|
||||||
<link rel="icon" type="image/x-icon" href="/favicon.svg">
|
|
||||||
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -153,7 +150,7 @@
|
||||||
.pastPluginUrl {
|
.pastPluginUrl {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
width: 700px;
|
width: 500px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
@ -163,122 +160,13 @@
|
||||||
box-shadow: 0px 1px 2px #131313;
|
box-shadow: 0px 1px 2px #131313;
|
||||||
font-weight: lighter;
|
font-weight: lighter;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.pastPluginUrl .deleteButton {
|
|
||||||
position: absolute;
|
|
||||||
right: 15px;
|
|
||||||
height: 100%;
|
|
||||||
width: 30px;
|
|
||||||
top: 0px;
|
|
||||||
padding-top: 2px;
|
|
||||||
display: grid;
|
|
||||||
justify-items: center;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: 400;
|
|
||||||
transform: scaleX(1.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
[v-cloak] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
#cloakLoader {
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
text-align: center;
|
|
||||||
left: 0px;
|
|
||||||
top: 0px;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background-color: black;
|
|
||||||
color: white;
|
|
||||||
padding-top: 50px;
|
|
||||||
font-family: sans-serif;
|
|
||||||
}
|
|
||||||
.httpContainer {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.httpLine {
|
|
||||||
}
|
|
||||||
.httpLine .request {
|
|
||||||
height: 50px;
|
|
||||||
position: relative;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.httpLine .request .status {
|
|
||||||
position: absolute;
|
|
||||||
left: 10px;
|
|
||||||
width: 40px;
|
|
||||||
top: 10px;
|
|
||||||
padding: 5px;
|
|
||||||
background-color: #333;
|
|
||||||
border-radius: 5px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.httpLine .request .status.error {
|
|
||||||
background-color: #880000;
|
|
||||||
}
|
|
||||||
.httpLine .request .status.success {
|
|
||||||
background-color: #008800;
|
|
||||||
}
|
|
||||||
.httpLine .request .status.warn {
|
|
||||||
background-color: #803500;
|
|
||||||
}
|
|
||||||
.httpLine .request .method {
|
|
||||||
position: absolute;
|
|
||||||
left: 55px;
|
|
||||||
top: 10px;
|
|
||||||
padding: 5px;
|
|
||||||
background-color: #333;
|
|
||||||
border-radius: 5px;
|
|
||||||
width: 50px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.httpLine .request .url {
|
|
||||||
position: absolute;
|
|
||||||
left: 110px;
|
|
||||||
top: 10px;
|
|
||||||
padding: 5px;
|
|
||||||
background-color: #333;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
.httpLine .response {
|
|
||||||
background-color: #111;
|
|
||||||
margin-left: 55px;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
.httpLine .response .body{
|
|
||||||
white-space: pre-wrap;
|
|
||||||
font-family: monospace;
|
|
||||||
background-color: black;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
.httpLine .response .headers {
|
|
||||||
margin: 10px;
|
|
||||||
}
|
|
||||||
.httpLine .response .headers .key {
|
|
||||||
display: inline-block;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #FFF;
|
|
||||||
}
|
|
||||||
.httpLine .response .headers .value {
|
|
||||||
display: inline-block;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #AAA;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<v-app>
|
<v-app>
|
||||||
<div v-cloak id="cloakLoader" v-if="!page">
|
<v-main>
|
||||||
<h2>Loading..</h2>
|
|
||||||
First load may take longer
|
|
||||||
</div>
|
|
||||||
<v-main v-cloak>
|
|
||||||
<div id="topMenu">
|
<div id="topMenu">
|
||||||
<div style="height: 100%; display: inline-block; padding-left: 10px; padding-right: 20px;">
|
<div style="height: 100%; display: inline-block; padding-left: 10px; padding-right: 20px;">
|
||||||
<img src="./dependencies/FutoMainLogo.svg"
|
<img src="./dependencies/FutoMainLogo.svg"
|
||||||
|
@ -362,13 +250,10 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div v-if="pastPluginUrls" style="margin-top: 60px; margin-left: 25px;">
|
<div v-if="pastPluginUrls" style="margin-top: 60px;">
|
||||||
<h2 style="font-weight: lighter; text-align: center;">Past Plugins</h2>
|
<h2 style="font-weight: lighter; text-align: center;">Past Plugins</h2>
|
||||||
<div class="pastPluginUrl" v-for="pastPluginUrl in pastPluginUrls" @click="this.Plugin.newPluginUrl = pastPluginUrl; loadPlugin(pastPluginUrl)">
|
<div class="pastPluginUrl" v-for="pastPluginUrl in pastPluginUrls" @click="this.Plugin.newPluginUrl = pastPluginUrl; loadPlugin(pastPluginUrl)">
|
||||||
{{pastPluginUrl}}
|
{{pastPluginUrl}}
|
||||||
<div class="deleteButton" @click="(ev)=>{ev.stopPropagation(); deletePastPlugin(pastPluginUrl)}">
|
|
||||||
X
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -620,62 +505,7 @@
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-card-actions>
|
<v-card-actions>
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
<v-btn @click="Integration.logs = []">Clear</v-btn>
|
<v-btn>Clear</v-btn>
|
||||||
</v-card-actions>
|
|
||||||
</v-card>
|
|
||||||
<v-card style="margin: 20px;" v-if="Plugin.currentPlugin && Integration.httpExchanges">
|
|
||||||
<v-card-title>
|
|
||||||
Http Logs
|
|
||||||
</v-card-title>
|
|
||||||
</v-card-header>
|
|
||||||
<v-card-text>
|
|
||||||
<div style="position: absolute; top: 0px; right: 15px;">
|
|
||||||
<v-checkbox v-model="Integration.showHttpRequests" label="Show Http Requests"></v-checkbox>
|
|
||||||
</div>
|
|
||||||
<div class="httpContainer" v-if="Integration.showHttpRequests">
|
|
||||||
<div class="httpLine" v-for="exchange of Integration.httpExchanges">
|
|
||||||
<div class="request" @click="toggleHttpExchange(exchange)">
|
|
||||||
<div :class="[{ success: exchange.response.status < 300, warn: exchange.response.status >= 300 && exchange.response.status < 400, error: exchange.response.status >= 400 }, 'status']">
|
|
||||||
{{exchange.response.status}}
|
|
||||||
</div>
|
|
||||||
<div class="method">
|
|
||||||
{{exchange.request.method}}
|
|
||||||
</div>
|
|
||||||
<div class="url">
|
|
||||||
{{exchange.request.url}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="response" v-if="exchange.response.show">
|
|
||||||
<h2>Request Headers</h2>
|
|
||||||
<div class="headers">
|
|
||||||
<div class="header" v-for="(headerValue, header) in exchange.request.headers">
|
|
||||||
<div class="key">
|
|
||||||
{{header}}
|
|
||||||
</div>
|
|
||||||
<div class="value">
|
|
||||||
{{headerValue}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h2>Response</h2>
|
|
||||||
<div class="headers">
|
|
||||||
<div class="header" v-for="(headerValue, header) in exchange.response.headers">
|
|
||||||
<div class="key">
|
|
||||||
{{header}}
|
|
||||||
</div>
|
|
||||||
<div class="value">
|
|
||||||
{{headerValue}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="body">{{exchange.response.body}}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</v-card-text>
|
|
||||||
<v-card-actions>
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
<v-btn v-if="Integration.showHttpRequests" @click="Integration.httpExchanges = []">Clear</v-btn>
|
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
</v-card>
|
</v-card>
|
||||||
</div>
|
</div>
|
||||||
|
@ -732,9 +562,7 @@
|
||||||
lastLogIndex: -1,
|
lastLogIndex: -1,
|
||||||
lastLogDevID: "",
|
lastLogDevID: "",
|
||||||
logs: [],
|
logs: [],
|
||||||
httpExchanges: [],
|
lastInjectTime: ""
|
||||||
lastInjectTime: "",
|
|
||||||
showHttpRequests: false
|
|
||||||
},
|
},
|
||||||
Plugin: {
|
Plugin: {
|
||||||
loadUsingTag: false,
|
loadUsingTag: false,
|
||||||
|
@ -818,16 +646,6 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if(this.Integration.showHttpRequests) {
|
|
||||||
getDevHttpExchanges((exchanges)=>{
|
|
||||||
Vue.nextTick(()=>{
|
|
||||||
for(i = 0; i < exchanges.length; i++) {
|
|
||||||
exchanges[i].response.show = false;
|
|
||||||
this.Integration.httpExchanges.unshift(exchanges[i]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch(ex) {
|
catch(ex) {
|
||||||
console.error("Failed update", ex);
|
console.error("Failed update", ex);
|
||||||
|
@ -869,12 +687,6 @@
|
||||||
this.reloadPlugin();
|
this.reloadPlugin();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
deletePastPlugin(url) {
|
|
||||||
let currentPastPlugins = this.pastPluginUrls;
|
|
||||||
currentPastPlugins = currentPastPlugins.filter(x=>x.toLowerCase() != url.toLowerCase());
|
|
||||||
this.pastPluginUrls = currentPastPlugins;
|
|
||||||
localStorage.setItem("pastPlugins", JSON.stringify(currentPastPlugins));
|
|
||||||
},
|
|
||||||
loginTestPlugin() {
|
loginTestPlugin() {
|
||||||
pluginLoginTestPlugin();
|
pluginLoginTestPlugin();
|
||||||
setTimeout(()=>{
|
setTimeout(()=>{
|
||||||
|
@ -1110,9 +922,6 @@
|
||||||
},
|
},
|
||||||
showTestResults(results) {
|
showTestResults(results) {
|
||||||
|
|
||||||
},
|
|
||||||
toggleHttpExchange(exchange) {
|
|
||||||
exchange.response.show = !exchange.response.show;
|
|
||||||
},
|
},
|
||||||
copyClipboard(cpy) {
|
copyClipboard(cpy) {
|
||||||
if(navigator.clipboard)
|
if(navigator.clipboard)
|
||||||
|
|
2
app/src/main/assets/devportal/plugin.d.ts
vendored
2
app/src/main/assets/devportal/plugin.d.ts
vendored
|
@ -127,7 +127,7 @@ declare class PlatformVideoDetails extends PlatformVideo {
|
||||||
}
|
}
|
||||||
|
|
||||||
declare interface PlatformPostDef extends PlatformContentDef {
|
declare interface PlatformPostDef extends PlatformContentDef {
|
||||||
thumbnails: Thumbnails[],
|
thumbnails: string[],
|
||||||
images: string[],
|
images: string[],
|
||||||
description: string
|
description: string
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -11,8 +11,7 @@ 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"
|
||||||
|
@ -202,7 +201,7 @@ class PlatformContent {
|
||||||
obj = obj ?? {};
|
obj = obj ?? {};
|
||||||
this.id = obj.id ?? PlatformID(); //PlatformID
|
this.id = obj.id ?? PlatformID(); //PlatformID
|
||||||
this.name = obj.name ?? ""; //string
|
this.name = obj.name ?? ""; //string
|
||||||
this.thumbnails = obj.thumbnails ?? new Thumbnails([]); //Thumbnail[]
|
this.thumbnails = obj.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
|
||||||
|
@ -245,7 +244,6 @@ 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 {
|
||||||
|
@ -262,11 +260,6 @@ 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;
|
|
||||||
|
|
||||||
if (obj.getContentRecommendations) {
|
|
||||||
this.getContentRecommendations = obj.getContentRecommendations
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -285,49 +278,12 @@ class PlatformPostDetails extends PlatformPost {
|
||||||
super(obj);
|
super(obj);
|
||||||
obj = obj ?? {};
|
obj = obj ?? {};
|
||||||
this.plugin_type = "PlatformPostDetails";
|
this.plugin_type = "PlatformPostDetails";
|
||||||
this.rating = obj.rating ?? new RatingLikes(-1);
|
this.rating = obj.rating ?? 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) {
|
||||||
|
@ -374,16 +330,6 @@ 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);
|
||||||
|
@ -411,33 +357,6 @@ class AudioUrlSource {
|
||||||
this.requestModifier = obj.requestModifier;
|
this.requestModifier = obj.requestModifier;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class AudioUrlWidevineSource extends AudioUrlSource {
|
|
||||||
constructor(obj) {
|
|
||||||
super(obj);
|
|
||||||
this.plugin_type = "AudioUrlWidevineSource";
|
|
||||||
|
|
||||||
this.licenseUri = obj.licenseUri;
|
|
||||||
if(obj.getLicenseRequestExecutor)
|
|
||||||
this.getLicenseRequestExecutor = obj.getLicenseRequestExecutor;
|
|
||||||
|
|
||||||
// deprecated api conversion
|
|
||||||
if(obj.bearerToken) {
|
|
||||||
this.getLicenseRequestExecutor = () => {
|
|
||||||
return {
|
|
||||||
executeRequest: (url, _headers, _method, license_request_data) => {
|
|
||||||
return http.POST(
|
|
||||||
url,
|
|
||||||
license_request_data,
|
|
||||||
{ Authorization: `Bearer ${obj.bearerToken}` },
|
|
||||||
false,
|
|
||||||
true
|
|
||||||
).body
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
class AudioUrlRangeSource extends AudioUrlSource {
|
class AudioUrlRangeSource extends AudioUrlSource {
|
||||||
constructor(obj) {
|
constructor(obj) {
|
||||||
super(obj);
|
super(obj);
|
||||||
|
@ -478,49 +397,6 @@ class DashSource {
|
||||||
this.requestModifier = obj.requestModifier;
|
this.requestModifier = obj.requestModifier;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class DashWidevineSource extends DashSource {
|
|
||||||
constructor(obj) {
|
|
||||||
super(obj);
|
|
||||||
this.plugin_type = "DashWidevineSource";
|
|
||||||
|
|
||||||
this.licenseUri = obj.licenseUri;
|
|
||||||
if(obj.getLicenseRequestExecutor)
|
|
||||||
this.getLicenseRequestExecutor = obj.getLicenseRequestExecutor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
class DashManifestRawSource {
|
|
||||||
constructor(obj) {
|
|
||||||
obj = obj ?? {};
|
|
||||||
this.plugin_type = "DashRawSource";
|
|
||||||
this.name = obj.name ?? "";
|
|
||||||
this.bitrate = obj.bitrate ?? 0;
|
|
||||||
this.container = obj.container ?? "";
|
|
||||||
this.codec = obj.codec ?? "";
|
|
||||||
this.duration = obj.duration ?? 0;
|
|
||||||
this.url = obj.url;
|
|
||||||
this.language = obj.language ?? Language.UNKNOWN;
|
|
||||||
if(obj.requestModifier)
|
|
||||||
this.requestModifier = obj.requestModifier;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class DashManifestRawAudioSource {
|
|
||||||
constructor(obj) {
|
|
||||||
obj = obj ?? {};
|
|
||||||
this.plugin_type = "DashRawAudioSource";
|
|
||||||
this.name = obj.name ?? "";
|
|
||||||
this.bitrate = obj.bitrate ?? 0;
|
|
||||||
this.container = obj.container ?? "";
|
|
||||||
this.codec = obj.codec ?? "";
|
|
||||||
this.duration = obj.duration ?? 0;
|
|
||||||
this.url = obj.url;
|
|
||||||
this.language = obj.language ?? Language.UNKNOWN;
|
|
||||||
this.manifest = obj.manifest ?? null;
|
|
||||||
if(obj.requestModifier)
|
|
||||||
this.requestModifier = obj.requestModifier;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class RequestModifier {
|
class RequestModifier {
|
||||||
constructor(obj) {
|
constructor(obj) {
|
||||||
|
@ -551,7 +427,7 @@ class PlatformPlaylist extends PlatformContent {
|
||||||
constructor(obj) {
|
constructor(obj) {
|
||||||
super(obj, 4);
|
super(obj, 4);
|
||||||
this.plugin_type = "PlatformPlaylist";
|
this.plugin_type = "PlatformPlaylist";
|
||||||
this.videoCount = obj.videoCount ?? -1;
|
this.videoCount = obj.videoCount ?? 0;
|
||||||
this.thumbnail = obj.thumbnail;
|
this.thumbnail = obj.thumbnail;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -877,99 +753,3 @@ class URLSearchParams {
|
||||||
return searchString;
|
return searchString;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var __REGEX_SPACE_CHARACTERS = /<%= spaceCharacters %>/g;
|
|
||||||
var __btoa_TABLE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
||||||
function btoa(input) {
|
|
||||||
input = String(input);
|
|
||||||
if (/[^\0-\xFF]/.test(input)) {
|
|
||||||
// Note: no need to special-case astral symbols here, as surrogates are
|
|
||||||
// matched, and the input is supposed to only contain ASCII anyway.
|
|
||||||
error(
|
|
||||||
'The string to be encoded contains characters outside of the ' +
|
|
||||||
'Latin1 range.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
var padding = input.length % 3;
|
|
||||||
var output = '';
|
|
||||||
var position = -1;
|
|
||||||
var a;
|
|
||||||
var b;
|
|
||||||
var c;
|
|
||||||
var buffer;
|
|
||||||
// Make sure any padding is handled outside of the loop.
|
|
||||||
var length = input.length - padding;
|
|
||||||
|
|
||||||
while (++position < length) {
|
|
||||||
// Read three bytes, i.e. 24 bits.
|
|
||||||
a = input.charCodeAt(position) << 16;
|
|
||||||
b = input.charCodeAt(++position) << 8;
|
|
||||||
c = input.charCodeAt(++position);
|
|
||||||
buffer = a + b + c;
|
|
||||||
// Turn the 24 bits into four chunks of 6 bits each, and append the
|
|
||||||
// matching character for each of them to the output.
|
|
||||||
output += (
|
|
||||||
__btoa_TABLE.charAt(buffer >> 18 & 0x3F) +
|
|
||||||
__btoa_TABLE.charAt(buffer >> 12 & 0x3F) +
|
|
||||||
__btoa_TABLE.charAt(buffer >> 6 & 0x3F) +
|
|
||||||
__btoa_TABLE.charAt(buffer & 0x3F)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (padding == 2) {
|
|
||||||
a = input.charCodeAt(position) << 8;
|
|
||||||
b = input.charCodeAt(++position);
|
|
||||||
buffer = a + b;
|
|
||||||
output += (
|
|
||||||
__btoa_TABLE.charAt(buffer >> 10) +
|
|
||||||
__btoa_TABLE.charAt((buffer >> 4) & 0x3F) +
|
|
||||||
__btoa_TABLE.charAt((buffer << 2) & 0x3F) +
|
|
||||||
'='
|
|
||||||
);
|
|
||||||
} else if (padding == 1) {
|
|
||||||
buffer = input.charCodeAt(position);
|
|
||||||
output += (
|
|
||||||
__btoa_TABLE.charAt(buffer >> 2) +
|
|
||||||
__btoa_TABLE.charAt((buffer << 4) & 0x3F) +
|
|
||||||
'=='
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return output;
|
|
||||||
};
|
|
||||||
function atob(input) {
|
|
||||||
input = String(input)
|
|
||||||
.replace(__REGEX_SPACE_CHARACTERS, '');
|
|
||||||
var length = input.length;
|
|
||||||
if (length % 4 == 0) {
|
|
||||||
input = input.replace(/==?$/, '');
|
|
||||||
length = input.length;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
length % 4 == 1 ||
|
|
||||||
// http://whatwg.org/C#alphanumeric-ascii-characters
|
|
||||||
/[^+a-zA-Z0-9/]/.test(input)
|
|
||||||
) {
|
|
||||||
error(
|
|
||||||
'Invalid character: the string to be decoded is not correctly encoded.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
var bitCounter = 0;
|
|
||||||
var bitStorage;
|
|
||||||
var buffer;
|
|
||||||
var output = '';
|
|
||||||
var position = -1;
|
|
||||||
while (++position < length) {
|
|
||||||
buffer = __btoa_TABLE.indexOf(input.charAt(position));
|
|
||||||
bitStorage = bitCounter % 4 ? bitStorage * 64 + buffer : buffer;
|
|
||||||
// Unless this is the first of a group of 4 characters…
|
|
||||||
if (bitCounter++ % 4) {
|
|
||||||
// …convert the first 8 bits to a single ASCII character.
|
|
||||||
output += String.fromCharCode(
|
|
||||||
0xFF & bitStorage >> (-2 * bitCounter & 6)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return output;
|
|
||||||
};
|
|
||||||
|
|
|
@ -18,10 +18,7 @@ fun IAudioSource.isDownloadable(): Boolean = VideoHelper.isDownloadable(this);
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
fun JSSource.getHttpDataSourceFactory(): HttpDataSource.Factory {
|
fun JSSource.getHttpDataSourceFactory(): HttpDataSource.Factory {
|
||||||
val requestModifier = getRequestModifier();
|
val requestModifier = getRequestModifier();
|
||||||
val requestExecutor = getRequestExecutor();
|
return if (requestModifier != null) {
|
||||||
return if (requestExecutor != null) {
|
|
||||||
JSHttpDataSource.Factory().setRequestExecutor(requestExecutor);
|
|
||||||
} else if (requestModifier != null) {
|
|
||||||
JSHttpDataSource.Factory().setRequestModifier(requestModifier);
|
JSHttpDataSource.Factory().setRequestModifier(requestModifier);
|
||||||
} else {
|
} else {
|
||||||
DefaultHttpDataSource.Factory();
|
DefaultHttpDataSource.Factory();
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -6,7 +6,6 @@ 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
|
||||||
|
@ -216,14 +215,9 @@ private fun ByteArray.toInetAddress(): InetAddress {
|
||||||
return InetAddress.getByAddress(this);
|
return InetAddress.getByAddress(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int): Socket? {
|
fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? {
|
||||||
val timeout = 2000
|
val timeout = 2000
|
||||||
|
|
||||||
|
|
||||||
val addresses = if(!Settings.instance.casting.allowIpv6) attemptAddresses.filterIsInstance<Inet4Address>() else attemptAddresses;
|
|
||||||
if(addresses.isEmpty())
|
|
||||||
throw IllegalStateException("No valid addresses found (ipv6: ${(if(Settings.instance.casting.allowIpv6) "enabled" else "disabled")})");
|
|
||||||
|
|
||||||
if (addresses.isEmpty()) {
|
if (addresses.isEmpty()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
package com.futo.platformplayer
|
package com.futo.platformplayer
|
||||||
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||||
import com.futo.platformplayer.states.AnnouncementType
|
import com.futo.platformplayer.states.AnnouncementType
|
||||||
import com.futo.platformplayer.states.StateAnnouncement
|
import com.futo.platformplayer.states.StateAnnouncement
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.polycentric.core.ProcessHandle
|
import com.futo.polycentric.core.ProcessHandle
|
||||||
import com.futo.polycentric.core.Store
|
import com.futo.polycentric.core.Store
|
||||||
import com.futo.polycentric.core.SystemState
|
import com.futo.polycentric.core.SystemState
|
||||||
import com.futo.polycentric.core.base64UrlToByteArray
|
|
||||||
import userpackage.Protocol
|
import userpackage.Protocol
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
@ -40,21 +40,6 @@ fun Protocol.ImageBundle?.selectHighestResolutionImage(): Protocol.ImageManifest
|
||||||
return imageManifestsList.filter { it.byteCount < maximumFileSize }.maxByOrNull { abs(it.width * it.height) }
|
return imageManifestsList.filter { it.byteCount < maximumFileSize }.maxByOrNull { abs(it.width * it.height) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun String.getDataLinkFromUrl(): Protocol.URLInfoDataLink? {
|
|
||||||
val urlData = if (this.startsWith("polycentric://")) {
|
|
||||||
this.substring("polycentric://".length)
|
|
||||||
} else this;
|
|
||||||
|
|
||||||
val urlBytes = urlData.base64UrlToByteArray();
|
|
||||||
val urlInfo = Protocol.URLInfo.parseFrom(urlBytes);
|
|
||||||
if (urlInfo.urlType != 4L) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
val dataLink = Protocol.URLInfoDataLink.parseFrom(urlInfo.body);
|
|
||||||
return dataLink
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Protocol.Claim.resolveChannelUrl(): String? {
|
fun Protocol.Claim.resolveChannelUrl(): String? {
|
||||||
return StatePlatform.instance.resolveChannelUrlByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) })
|
return StatePlatform.instance.resolveChannelUrlByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) })
|
||||||
}
|
}
|
||||||
|
@ -62,3 +47,29 @@ fun Protocol.Claim.resolveChannelUrl(): String? {
|
||||||
fun Protocol.Claim.resolveChannelUrls(): List<String> {
|
fun Protocol.Claim.resolveChannelUrls(): List<String> {
|
||||||
return StatePlatform.instance.resolveChannelUrlsByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) })
|
return StatePlatform.instance.resolveChannelUrlsByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun ProcessHandle.fullyBackfillServersAnnounceExceptions() {
|
||||||
|
val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(system))
|
||||||
|
if (!systemState.servers.contains(PolycentricCache.STAGING_SERVER)) {
|
||||||
|
removeServer(PolycentricCache.STAGING_SERVER)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!systemState.servers.contains(PolycentricCache.SERVER)) {
|
||||||
|
removeServer(PolycentricCache.SERVER)
|
||||||
|
}
|
||||||
|
|
||||||
|
val exceptions = fullyBackfillServers()
|
||||||
|
for (pair in exceptions) {
|
||||||
|
val server = pair.key
|
||||||
|
val exception = pair.value
|
||||||
|
|
||||||
|
StateAnnouncement.instance.registerAnnouncement(
|
||||||
|
"backfill-failed",
|
||||||
|
"Backfill failed",
|
||||||
|
"Failed to backfill server $server. $exception",
|
||||||
|
AnnouncementType.SESSION_RECURRING
|
||||||
|
);
|
||||||
|
|
||||||
|
Logger.e("Backfill", "Failed to backfill server $server.", exception)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,6 @@
|
||||||
package com.futo.platformplayer
|
package com.futo.platformplayer
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import java.net.Inet4Address
|
|
||||||
import java.net.Inet6Address
|
|
||||||
import java.net.InetAddress
|
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.net.URISyntaxException
|
import java.net.URISyntaxException
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
|
@ -29,17 +26,3 @@ fun String?.yesNoToBoolean(): Boolean {
|
||||||
fun Boolean?.toYesNo(): String {
|
fun Boolean?.toYesNo(): String {
|
||||||
return if (this == true) "YES" else "NO"
|
return if (this == true) "YES" else "NO"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun InetAddress?.toUrlAddress(): String {
|
|
||||||
return when (this) {
|
|
||||||
is Inet6Address -> {
|
|
||||||
"[${hostAddress}]"
|
|
||||||
}
|
|
||||||
is Inet4Address -> {
|
|
||||||
hostAddress
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
throw Exception("Invalid address type")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,192 +0,0 @@
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,144 +0,0 @@
|
||||||
package com.futo.platformplayer
|
|
||||||
|
|
||||||
import com.google.common.base.Preconditions
|
|
||||||
import com.google.common.primitives.Longs
|
|
||||||
import java.io.*
|
|
||||||
|
|
||||||
class LittleEndianDataOutputStream
|
|
||||||
/**
|
|
||||||
* Creates a `LittleEndianDataOutputStream` that wraps the given stream.
|
|
||||||
*
|
|
||||||
* @param out the stream to delegate to
|
|
||||||
*/
|
|
||||||
(out: OutputStream?) : FilterOutputStream(DataOutputStream(Preconditions.checkNotNull(out))),
|
|
||||||
DataOutput {
|
|
||||||
@Throws(IOException::class)
|
|
||||||
override fun write(b: ByteArray, off: Int, len: Int) {
|
|
||||||
// Override slow FilterOutputStream impl
|
|
||||||
out.write(b, off, len)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
override fun writeBoolean(v: Boolean) {
|
|
||||||
(out as DataOutputStream).writeBoolean(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
override fun writeByte(v: Int) {
|
|
||||||
(out as DataOutputStream).writeByte(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Deprecated(
|
|
||||||
"""The semantics of {@code writeBytes(String s)} are considered dangerous. Please use
|
|
||||||
{@link #writeUTF(String s)}, {@link #writeChars(String s)} or another write method instead."""
|
|
||||||
)
|
|
||||||
@Throws(
|
|
||||||
IOException::class
|
|
||||||
)
|
|
||||||
override fun writeBytes(s: String) {
|
|
||||||
(out as DataOutputStream).writeBytes(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Writes a char as specified by [DataOutputStream.writeChar], except using
|
|
||||||
* little-endian byte order.
|
|
||||||
*
|
|
||||||
* @throws IOException if an I/O error occurs
|
|
||||||
*/
|
|
||||||
@Throws(IOException::class)
|
|
||||||
override fun writeChar(v: Int) {
|
|
||||||
writeShort(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Writes a `String` as specified by [DataOutputStream.writeChars], except
|
|
||||||
* each character is written using little-endian byte order.
|
|
||||||
*
|
|
||||||
* @throws IOException if an I/O error occurs
|
|
||||||
*/
|
|
||||||
@Throws(IOException::class)
|
|
||||||
override fun writeChars(s: String) {
|
|
||||||
for (i in 0 until s.length) {
|
|
||||||
writeChar(s[i].code)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Writes a `double` as specified by [DataOutputStream.writeDouble], except
|
|
||||||
* using little-endian byte order.
|
|
||||||
*
|
|
||||||
* @throws IOException if an I/O error occurs
|
|
||||||
*/
|
|
||||||
@Throws(IOException::class)
|
|
||||||
override fun writeDouble(v: Double) {
|
|
||||||
writeLong(java.lang.Double.doubleToLongBits(v))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Writes a `float` as specified by [DataOutputStream.writeFloat], except using
|
|
||||||
* little-endian byte order.
|
|
||||||
*
|
|
||||||
* @throws IOException if an I/O error occurs
|
|
||||||
*/
|
|
||||||
@Throws(IOException::class)
|
|
||||||
override fun writeFloat(v: Float) {
|
|
||||||
writeInt(java.lang.Float.floatToIntBits(v))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Writes an `int` as specified by [DataOutputStream.writeInt], except using
|
|
||||||
* little-endian byte order.
|
|
||||||
*
|
|
||||||
* @throws IOException if an I/O error occurs
|
|
||||||
*/
|
|
||||||
@Throws(IOException::class)
|
|
||||||
override fun writeInt(v: Int) {
|
|
||||||
val bytes = byteArrayOf(
|
|
||||||
(0xFF and v).toByte(),
|
|
||||||
(0xFF and (v shr 8)).toByte(),
|
|
||||||
(0xFF and (v shr 16)).toByte(),
|
|
||||||
(0xFF and (v shr 24)).toByte()
|
|
||||||
)
|
|
||||||
out.write(bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Writes a `long` as specified by [DataOutputStream.writeLong], except using
|
|
||||||
* little-endian byte order.
|
|
||||||
*
|
|
||||||
* @throws IOException if an I/O error occurs
|
|
||||||
*/
|
|
||||||
@Throws(IOException::class)
|
|
||||||
override fun writeLong(v: Long) {
|
|
||||||
val bytes = Longs.toByteArray(java.lang.Long.reverseBytes(v))
|
|
||||||
write(bytes, 0, bytes.size)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Writes a `short` as specified by [DataOutputStream.writeShort], except using
|
|
||||||
* little-endian byte order.
|
|
||||||
*
|
|
||||||
* @throws IOException if an I/O error occurs
|
|
||||||
*/
|
|
||||||
@Throws(IOException::class)
|
|
||||||
override fun writeShort(v: Int) {
|
|
||||||
val bytes = byteArrayOf(
|
|
||||||
(0xFF and v).toByte(),
|
|
||||||
(0xFF and (v shr 8)).toByte()
|
|
||||||
)
|
|
||||||
out.write(bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
override fun writeUTF(str: String) {
|
|
||||||
(out as DataOutputStream).writeUTF(str)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Overriding close() because FilterOutputStream's close() method pre-JDK8 has bad behavior:
|
|
||||||
// it silently ignores any exception thrown by flush(). Instead, just close the delegate stream.
|
|
||||||
// It should flush itself if necessary.
|
|
||||||
@Throws(IOException::class)
|
|
||||||
override fun close() {
|
|
||||||
out.close()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -11,7 +11,6 @@ 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
|
||||||
|
@ -33,6 +32,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 kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@ -44,7 +44,6 @@ import kotlinx.serialization.json.Json
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class MenuBottomBarSetting(val id: Int, var enabled: Boolean);
|
data class MenuBottomBarSetting(val id: Int, var enabled: Boolean);
|
||||||
|
|
||||||
|
@ -58,16 +57,7 @@ class Settings : FragmentedStorageFileJson() {
|
||||||
@Transient
|
@Transient
|
||||||
val onTabsChanged = Event0();
|
val onTabsChanged = Event0();
|
||||||
|
|
||||||
@FormField(R.string.sync_grayjay, FieldForm.BUTTON, R.string.sync_grayjay_description, -8)
|
@FormField(R.string.manage_polycentric_identity, FieldForm.BUTTON, R.string.manage_your_polycentric_identity, -6)
|
||||||
@FormFieldButton(R.drawable.ic_update)
|
|
||||||
fun syncGrayjay() {
|
|
||||||
SettingsActivity.getActivity()?.let {
|
|
||||||
it.startActivity(Intent(it, SyncHomeActivity::class.java))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@FormField(R.string.manage_polycentric_identity, FieldForm.BUTTON, R.string.manage_your_polycentric_identity, -7)
|
|
||||||
@FormFieldButton(R.drawable.ic_person)
|
@FormFieldButton(R.drawable.ic_person)
|
||||||
fun managePolycentricIdentity() {
|
fun managePolycentricIdentity() {
|
||||||
SettingsActivity.getActivity()?.let {
|
SettingsActivity.getActivity()?.let {
|
||||||
|
@ -83,7 +73,7 @@ class Settings : FragmentedStorageFileJson() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.show_faq, FieldForm.BUTTON, R.string.get_answers_to_common_questions, -6)
|
@FormField(R.string.show_faq, FieldForm.BUTTON, R.string.get_answers_to_common_questions, -5)
|
||||||
@FormFieldButton(R.drawable.ic_quiz)
|
@FormFieldButton(R.drawable.ic_quiz)
|
||||||
fun openFAQ() {
|
fun openFAQ() {
|
||||||
try {
|
try {
|
||||||
|
@ -93,7 +83,7 @@ class Settings : FragmentedStorageFileJson() {
|
||||||
//Ignored
|
//Ignored
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@FormField(R.string.show_issues, FieldForm.BUTTON, R.string.a_list_of_user_reported_and_self_reported_issues, -5)
|
@FormField(R.string.show_issues, FieldForm.BUTTON, R.string.a_list_of_user_reported_and_self_reported_issues, -4)
|
||||||
@FormFieldButton(R.drawable.ic_data_alert)
|
@FormFieldButton(R.drawable.ic_data_alert)
|
||||||
fun openIssues() {
|
fun openIssues() {
|
||||||
try {
|
try {
|
||||||
|
@ -125,7 +115,7 @@ class Settings : FragmentedStorageFileJson() {
|
||||||
}
|
}
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
@FormField(R.string.manage_tabs, FieldForm.BUTTON, R.string.change_tabs_visible_on_the_home_screen, -4)
|
@FormField(R.string.manage_tabs, FieldForm.BUTTON, R.string.change_tabs_visible_on_the_home_screen, -3)
|
||||||
@FormFieldButton(R.drawable.ic_tabs)
|
@FormFieldButton(R.drawable.ic_tabs)
|
||||||
fun manageTabs() {
|
fun manageTabs() {
|
||||||
try {
|
try {
|
||||||
|
@ -139,15 +129,16 @@ class Settings : FragmentedStorageFileJson() {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@FormField(R.string.import_data, FieldForm.BUTTON, R.string.import_data_description, -3)
|
@FormField(R.string.import_data, FieldForm.BUTTON, R.string.import_data_description, -2)
|
||||||
@FormFieldButton(R.drawable.ic_move_up)
|
@FormFieldButton(R.drawable.ic_move_up)
|
||||||
fun import() {
|
fun import() {
|
||||||
val act = SettingsActivity.getActivity() ?: return;
|
val act = SettingsActivity.getActivity() ?: return;
|
||||||
val intent = MainActivity.getImportOptionsIntent(act);
|
val intent = MainActivity.getImportOptionsIntent(act);
|
||||||
|
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK;
|
||||||
act.startActivity(intent);
|
act.startActivity(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.link_handling, FieldForm.BUTTON, R.string.allow_grayjay_to_handle_links, -2)
|
@FormField(R.string.link_handling, FieldForm.BUTTON, R.string.allow_grayjay_to_handle_links, -1)
|
||||||
@FormFieldButton(R.drawable.ic_link)
|
@FormFieldButton(R.drawable.ic_link)
|
||||||
fun manageLinks() {
|
fun manageLinks() {
|
||||||
try {
|
try {
|
||||||
|
@ -157,24 +148,6 @@ class Settings : FragmentedStorageFileJson() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*@FormField(R.string.disable_battery_optimization, FieldForm.BUTTON, R.string.click_to_go_to_battery_optimization_settings_disabling_battery_optimization_will_prevent_the_os_from_killing_media_sessions, -1)
|
|
||||||
@FormFieldButton(R.drawable.battery_full_24px)
|
|
||||||
fun ignoreBatteryOptimization() {
|
|
||||||
SettingsActivity.getActivity()?.let {
|
|
||||||
val intent = Intent()
|
|
||||||
val packageName = it.packageName
|
|
||||||
val pm = it.getSystemService(POWER_SERVICE) as PowerManager;
|
|
||||||
if (!pm.isIgnoringBatteryOptimizations(packageName)) {
|
|
||||||
intent.setAction(android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)
|
|
||||||
intent.setData(Uri.parse("package:$packageName"))
|
|
||||||
it.startActivity(intent)
|
|
||||||
UIDialogs.toast(it, "Please ignore battery optimizations for Grayjay")
|
|
||||||
} else {
|
|
||||||
UIDialogs.toast(it, "Battery optimizations already disabled for Grayjay")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
|
|
||||||
@FormField(R.string.language, "group", -1, 0)
|
@FormField(R.string.language, "group", -1, 0)
|
||||||
var language = LanguageSettings();
|
var language = LanguageSettings();
|
||||||
@Serializable
|
@Serializable
|
||||||
|
@ -205,7 +178,7 @@ class Settings : FragmentedStorageFileJson() {
|
||||||
var home = HomeSettings();
|
var home = HomeSettings();
|
||||||
@Serializable
|
@Serializable
|
||||||
class HomeSettings {
|
class HomeSettings {
|
||||||
@FormField(R.string.feed_style, FieldForm.DROPDOWN, R.string.may_require_restart, 3)
|
@FormField(R.string.feed_style, FieldForm.DROPDOWN, R.string.may_require_restart, 5)
|
||||||
@DropdownFieldOptionsId(R.array.feed_style)
|
@DropdownFieldOptionsId(R.array.feed_style)
|
||||||
var homeFeedStyle: Int = 1;
|
var homeFeedStyle: Int = 1;
|
||||||
|
|
||||||
|
@ -216,11 +189,6 @@ class Settings : FragmentedStorageFileJson() {
|
||||||
return FeedStyle.THUMBNAIL;
|
return FeedStyle.THUMBNAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.show_home_filters, FieldForm.TOGGLE, R.string.show_home_filters_description, 4)
|
|
||||||
var showHomeFilters: Boolean = true;
|
|
||||||
@FormField(R.string.show_home_filters_plugin_names, FieldForm.TOGGLE, R.string.show_home_filters_plugin_names_description, 5)
|
|
||||||
var showHomeFiltersPluginNames: Boolean = false;
|
|
||||||
|
|
||||||
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
|
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
|
||||||
var previewFeedItems: Boolean = true;
|
var previewFeedItems: Boolean = true;
|
||||||
|
|
||||||
|
@ -259,9 +227,6 @@ 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)
|
||||||
|
@ -299,9 +264,6 @@ class Settings : FragmentedStorageFileJson() {
|
||||||
@FormField(R.string.show_subscription_group, FieldForm.TOGGLE, R.string.show_subscription_group_description, 5)
|
@FormField(R.string.show_subscription_group, FieldForm.TOGGLE, R.string.show_subscription_group_description, 5)
|
||||||
var showSubscriptionGroups: Boolean = true;
|
var showSubscriptionGroups: Boolean = true;
|
||||||
|
|
||||||
@FormField(R.string.use_subscription_exchange, FieldForm.TOGGLE, R.string.use_subscription_exchange_description, 6)
|
|
||||||
var useSubscriptionExchange: Boolean = false;
|
|
||||||
|
|
||||||
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
|
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
|
||||||
var previewFeedItems: Boolean = true;
|
var previewFeedItems: Boolean = true;
|
||||||
|
|
||||||
|
@ -364,7 +326,7 @@ class Settings : FragmentedStorageFileJson() {
|
||||||
var playback = PlaybackSettings();
|
var playback = PlaybackSettings();
|
||||||
@Serializable
|
@Serializable
|
||||||
class PlaybackSettings {
|
class PlaybackSettings {
|
||||||
@FormField(R.string.primary_language, FieldForm.DROPDOWN, -1, -2)
|
@FormField(R.string.primary_language, FieldForm.DROPDOWN, -1, 0)
|
||||||
@DropdownFieldOptionsId(R.array.audio_languages)
|
@DropdownFieldOptionsId(R.array.audio_languages)
|
||||||
var primaryLanguage: Int = 0;
|
var primaryLanguage: Int = 0;
|
||||||
|
|
||||||
|
@ -388,12 +350,10 @@ class Settings : FragmentedStorageFileJson() {
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@FormField(R.string.prefer_original_audio, FieldForm.TOGGLE, R.string.prefer_original_audio_description, -1)
|
|
||||||
var preferOriginalAudio: Boolean = true;
|
|
||||||
|
|
||||||
//= context.resources.getStringArray(R.array.audio_languages)[primaryLanguage];
|
//= context.resources.getStringArray(R.array.audio_languages)[primaryLanguage];
|
||||||
|
|
||||||
@FormField(R.string.default_playback_speed, FieldForm.DROPDOWN, -1, 0)
|
@FormField(R.string.default_playback_speed, FieldForm.DROPDOWN, -1, 1)
|
||||||
@DropdownFieldOptionsId(R.array.playback_speeds)
|
@DropdownFieldOptionsId(R.array.playback_speeds)
|
||||||
var defaultPlaybackSpeed: Int = 3;
|
var defaultPlaybackSpeed: Int = 3;
|
||||||
fun getDefaultPlaybackSpeed(): Float = when(defaultPlaybackSpeed) {
|
fun getDefaultPlaybackSpeed(): Float = when(defaultPlaybackSpeed) {
|
||||||
|
@ -409,29 +369,37 @@ class Settings : FragmentedStorageFileJson() {
|
||||||
else -> 1.0f;
|
else -> 1.0f;
|
||||||
};
|
};
|
||||||
|
|
||||||
@FormField(R.string.preferred_quality, FieldForm.DROPDOWN, R.string.preferred_quality_description, 1)
|
@FormField(R.string.preferred_quality, FieldForm.DROPDOWN, R.string.preferred_quality_description, 2)
|
||||||
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
||||||
var preferredQuality: Int = 0;
|
var preferredQuality: Int = 0;
|
||||||
|
|
||||||
@FormField(R.string.preferred_metered_quality, FieldForm.DROPDOWN, R.string.preferred_metered_quality_description, 2)
|
@FormField(R.string.preferred_metered_quality, FieldForm.DROPDOWN, R.string.preferred_metered_quality_description, 3)
|
||||||
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
||||||
var preferredMeteredQuality: Int = 0;
|
var preferredMeteredQuality: Int = 0;
|
||||||
fun getPreferredQualityPixelCount(): Int = preferedQualityToPixels(preferredQuality);
|
fun getPreferredQualityPixelCount(): Int = preferedQualityToPixels(preferredQuality);
|
||||||
fun getPreferredMeteredQualityPixelCount(): Int = preferedQualityToPixels(preferredMeteredQuality);
|
fun getPreferredMeteredQualityPixelCount(): Int = preferedQualityToPixels(preferredMeteredQuality);
|
||||||
fun getCurrentPreferredQualityPixelCount(): Int = if(!StateApp.instance.isCurrentMetered()) getPreferredQualityPixelCount() else getPreferredMeteredQualityPixelCount();
|
fun getCurrentPreferredQualityPixelCount(): Int = if(!StateApp.instance.isCurrentMetered()) getPreferredQualityPixelCount() else getPreferredMeteredQualityPixelCount();
|
||||||
|
|
||||||
@FormField(R.string.preferred_preview_quality, FieldForm.DROPDOWN, R.string.preferred_preview_quality_description, 3)
|
@FormField(R.string.preferred_preview_quality, FieldForm.DROPDOWN, R.string.preferred_preview_quality_description, 4)
|
||||||
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
||||||
var preferredPreviewQuality: Int = 5;
|
var preferredPreviewQuality: Int = 5;
|
||||||
fun getPreferredPreviewQualityPixelCount(): Int = preferedQualityToPixels(preferredPreviewQuality);
|
fun getPreferredPreviewQualityPixelCount(): Int = preferedQualityToPixels(preferredPreviewQuality);
|
||||||
|
|
||||||
@FormField(R.string.simplify_sources, FieldForm.TOGGLE, R.string.simplify_sources_description, 4)
|
@FormField(R.string.auto_rotate, FieldForm.DROPDOWN, -1, 5)
|
||||||
var simplifySources: Boolean = true;
|
@DropdownFieldOptionsId(R.array.system_enabled_disabled_array)
|
||||||
|
var autoRotate: Int = 2;
|
||||||
|
|
||||||
@FormField(R.string.always_allow_reverse_landscape_auto_rotate, FieldForm.TOGGLE, R.string.always_allow_reverse_landscape_auto_rotate_description, 5)
|
fun isAutoRotate() = autoRotate == 1 || (autoRotate == 2 && StateApp.instance.getCurrentSystemAutoRotate());
|
||||||
var alwaysAllowReverseLandscapeAutoRotate: Boolean = true
|
|
||||||
|
|
||||||
@FormField(R.string.background_behavior, FieldForm.DROPDOWN, -1, 6)
|
@FormField(R.string.auto_rotate_dead_zone, FieldForm.DROPDOWN, R.string.this_prevents_the_device_from_rotating_within_the_given_amount_of_degrees, 6)
|
||||||
|
@DropdownFieldOptionsId(R.array.auto_rotate_dead_zone)
|
||||||
|
var autoRotateDeadZone: Int = 0;
|
||||||
|
|
||||||
|
fun getAutoRotateDeadZoneDegrees(): Int {
|
||||||
|
return autoRotateDeadZone * 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
@FormField(R.string.background_behavior, FieldForm.DROPDOWN, -1, 7)
|
||||||
@DropdownFieldOptionsId(R.array.player_background_behavior)
|
@DropdownFieldOptionsId(R.array.player_background_behavior)
|
||||||
var backgroundPlay: Int = 2;
|
var backgroundPlay: Int = 2;
|
||||||
|
|
||||||
|
@ -482,44 +450,18 @@ class Settings : FragmentedStorageFileJson() {
|
||||||
|
|
||||||
@FormField(R.string.full_screen_portrait, FieldForm.TOGGLE, R.string.allow_full_screen_portrait, 13)
|
@FormField(R.string.full_screen_portrait, FieldForm.TOGGLE, R.string.allow_full_screen_portrait, 13)
|
||||||
var fullscreenPortrait: Boolean = false;
|
var fullscreenPortrait: Boolean = false;
|
||||||
|
|
||||||
@FormField(R.string.reverse_portrait, FieldForm.TOGGLE, R.string.reverse_portrait_description, 14)
|
|
||||||
var reversePortrait: Boolean = false;
|
|
||||||
|
|
||||||
@FormField(R.string.prefer_webm, FieldForm.TOGGLE, R.string.prefer_webm_description, 18)
|
|
||||||
var preferWebmVideo: Boolean = false;
|
|
||||||
@FormField(R.string.prefer_webm_audio, FieldForm.TOGGLE, R.string.prefer_webm_audio_description, 19)
|
|
||||||
var preferWebmAudio: Boolean = false;
|
|
||||||
|
|
||||||
@FormField(R.string.allow_under_cutout, FieldForm.TOGGLE, R.string.allow_under_cutout_description, 20)
|
|
||||||
var allowVideoToGoUnderCutout: Boolean = true;
|
|
||||||
|
|
||||||
@FormField(R.string.autoplay, FieldForm.TOGGLE, R.string.autoplay, 21)
|
|
||||||
var autoplay: Boolean = false;
|
|
||||||
|
|
||||||
@FormField(R.string.delete_watchlist_on_finish, FieldForm.TOGGLE, R.string.delete_watchlist_on_finish_description, 22)
|
|
||||||
var deleteFromWatchLaterAuto: Boolean = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.comments, "group", R.string.comments_description, 6)
|
@FormField(R.string.comments, "group", R.string.comments_description, 6)
|
||||||
var comments = CommentSettings();
|
var comments = CommentSettings();
|
||||||
@Serializable
|
@Serializable
|
||||||
class CommentSettings {
|
class CommentSettings {
|
||||||
var didAskPolycentricDefault: Boolean = false;
|
|
||||||
|
|
||||||
@FormField(R.string.default_comment_section, FieldForm.DROPDOWN, -1, 0)
|
@FormField(R.string.default_comment_section, FieldForm.DROPDOWN, -1, 0)
|
||||||
@DropdownFieldOptionsId(R.array.comment_sections)
|
@DropdownFieldOptionsId(R.array.comment_sections)
|
||||||
var defaultCommentSection: Int = 2;
|
var defaultCommentSection: Int = 0;
|
||||||
|
|
||||||
@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)
|
||||||
|
@ -568,7 +510,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 = false; //Temporary default disabled to prevent ui freeze?
|
var videoCache: Boolean = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.casting, "group", R.string.configure_casting, 9)
|
@FormField(R.string.casting, "group", R.string.configure_casting, 9)
|
||||||
|
@ -583,15 +525,6 @@ class Settings : FragmentedStorageFileJson() {
|
||||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||||
var keepScreenOn: Boolean = true;
|
var keepScreenOn: Boolean = true;
|
||||||
|
|
||||||
@FormField(R.string.always_proxy_requests, FieldForm.TOGGLE, R.string.always_proxy_requests_description, 3)
|
|
||||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
|
||||||
var alwaysProxyRequests: Boolean = false;
|
|
||||||
|
|
||||||
|
|
||||||
@FormField(R.string.allow_ipv6, FieldForm.TOGGLE, R.string.allow_ipv6_description, 4)
|
|
||||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
|
||||||
var allowIpv6: Boolean = false;
|
|
||||||
|
|
||||||
/*TODO: Should we have a different casting quality?
|
/*TODO: Should we have a different casting quality?
|
||||||
@FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3)
|
@FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3)
|
||||||
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
||||||
|
@ -659,9 +592,6 @@ class Settings : FragmentedStorageFileJson() {
|
||||||
@Serializable
|
@Serializable
|
||||||
class Plugins {
|
class Plugins {
|
||||||
|
|
||||||
@FormField(R.string.check_disabled_plugin_updates, FieldForm.TOGGLE, R.string.check_disabled_plugin_updates_description, -1)
|
|
||||||
var checkDisabledPluginsForUpdates: Boolean = false;
|
|
||||||
|
|
||||||
@FormField(R.string.clear_cookies_on_logout, FieldForm.TOGGLE, R.string.clears_cookies_when_you_log_out, 0)
|
@FormField(R.string.clear_cookies_on_logout, FieldForm.TOGGLE, R.string.clears_cookies_when_you_log_out, 0)
|
||||||
var clearCookiesOnLogout: Boolean = true;
|
var clearCookiesOnLogout: Boolean = true;
|
||||||
|
|
||||||
|
@ -845,10 +775,10 @@ class Settings : FragmentedStorageFileJson() {
|
||||||
fun export() {
|
fun export() {
|
||||||
val activity = SettingsActivity.getActivity() ?: return;
|
val activity = SettingsActivity.getActivity() ?: return;
|
||||||
UISlideOverlays.showOverlay(activity.overlay, "Select export type", null, {},
|
UISlideOverlays.showOverlay(activity.overlay, "Select export type", null, {},
|
||||||
SlideUpMenuItem(activity, R.drawable.ic_share, "Share", "", tag = null, call = {
|
SlideUpMenuItem(activity, R.drawable.ic_share, "Share", "", null, {
|
||||||
StateBackup.shareExternalBackup();
|
StateBackup.shareExternalBackup();
|
||||||
}),
|
}),
|
||||||
SlideUpMenuItem(activity, R.drawable.ic_download, "File", "", tag = null, call = {
|
SlideUpMenuItem(activity, R.drawable.ic_download, "File", "", null, {
|
||||||
StateBackup.saveExternalBackup(activity);
|
StateBackup.saveExternalBackup(activity);
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
@ -864,14 +794,10 @@ 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() {
|
||||||
SettingsActivity.getActivity()?.let { context ->
|
StatePayment.instance.clearLicenses();
|
||||||
UIDialogs.showConfirmationDialog(context, "Are you sure you want to delete your license?", {
|
SettingsActivity.getActivity()?.let {
|
||||||
StatePayment.instance.clearLicenses();
|
UIDialogs.toast(it, it.getString(R.string.licenses_cleared_might_require_app_restart));
|
||||||
SettingsActivity.getActivity()?.let {
|
it.reloadSettings();
|
||||||
UIDialogs.toast(it, it.getString(R.string.licenses_cleared_might_require_app_restart));
|
|
||||||
it.reloadSettings();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -880,16 +806,12 @@ class Settings : FragmentedStorageFileJson() {
|
||||||
var other = Other();
|
var other = Other();
|
||||||
@Serializable
|
@Serializable
|
||||||
class Other {
|
class Other {
|
||||||
@FormField(R.string.playlist_delete_confirmation, FieldForm.TOGGLE, R.string.playlist_delete_confirmation_description, 2)
|
@FormField(R.string.bypass_rotation_prevention, FieldForm.TOGGLE, R.string.bypass_rotation_prevention_description, 1)
|
||||||
var playlistDeleteConfirmation: Boolean = true;
|
@FormFieldWarning(R.string.bypass_rotation_prevention_warning)
|
||||||
@FormField(R.string.playlist_allow_dups, FieldForm.TOGGLE, R.string.playlist_allow_dups_description, 3)
|
var bypassRotationPrevention: Boolean = false;
|
||||||
var playlistAllowDups: Boolean = true;
|
|
||||||
|
|
||||||
@FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 4)
|
@FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 1)
|
||||||
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)
|
||||||
|
@ -921,33 +843,7 @@ class Settings : FragmentedStorageFileJson() {
|
||||||
var pan: Boolean = true;
|
var pan: Boolean = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.synchronization, FieldForm.GROUP, -1, 20)
|
@FormField(R.string.info, 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.discover_through_relay, FieldForm.TOGGLE, R.string.discover_through_relay_description, 3)
|
|
||||||
var discoverThroughRelay: Boolean = true;
|
|
||||||
|
|
||||||
@FormField(R.string.pair_through_relay, FieldForm.TOGGLE, R.string.pair_through_relay_description, 3)
|
|
||||||
var pairThroughRelay: Boolean = true;
|
|
||||||
|
|
||||||
@FormField(R.string.connect_through_relay, FieldForm.TOGGLE, R.string.connect_through_relay_description, 3)
|
|
||||||
var connectThroughRelay: Boolean = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@FormField(R.string.info, FieldForm.GROUP, -1, 21)
|
|
||||||
var info = Info();
|
var info = Info();
|
||||||
@Serializable
|
@Serializable
|
||||||
class Info {
|
class Info {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
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
|
||||||
|
@ -9,7 +8,6 @@ import androidx.work.WorkManager
|
||||||
import com.caoccao.javet.values.primitive.V8ValueInteger
|
import com.caoccao.javet.values.primitive.V8ValueInteger
|
||||||
import com.caoccao.javet.values.primitive.V8ValueString
|
import com.caoccao.javet.values.primitive.V8ValueString
|
||||||
import com.futo.platformplayer.activities.DeveloperActivity
|
import com.futo.platformplayer.activities.DeveloperActivity
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
|
||||||
import com.futo.platformplayer.activities.SettingsActivity
|
import com.futo.platformplayer.activities.SettingsActivity
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
|
@ -35,7 +33,6 @@ import com.futo.platformplayer.stores.FragmentedStorageFileJson
|
||||||
import com.futo.platformplayer.views.fields.ButtonField
|
import com.futo.platformplayer.views.fields.ButtonField
|
||||||
import com.futo.platformplayer.views.fields.FieldForm
|
import com.futo.platformplayer.views.fields.FieldForm
|
||||||
import com.futo.platformplayer.views.fields.FormField
|
import com.futo.platformplayer.views.fields.FormField
|
||||||
import com.futo.platformplayer.views.fields.FormFieldWarning
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@ -236,17 +233,13 @@ 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()!!;
|
||||||
try {
|
UIDialogs.toast(SettingsActivity.getActivity()!!, "Starting test background worker");
|
||||||
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)
|
||||||
|
@ -497,24 +490,6 @@ class SettingsDev : FragmentedStorageFileJson() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@FormField(R.string.test_playback, FieldForm.BUTTON,
|
|
||||||
R.string.test_playback, 1)
|
|
||||||
fun testPlayback(context: Context) {
|
|
||||||
context.startActivity(MainActivity.getActionIntent(context, "TEST_PLAYBACK"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@FormField(R.string.networking, FieldForm.GROUP, -1, 18)
|
|
||||||
var networking = Networking();
|
|
||||||
@Serializable
|
|
||||||
class Networking {
|
|
||||||
@FormField(R.string.allow_all_certificates, FieldForm.TOGGLE, -1, 0)
|
|
||||||
@FormFieldWarning(R.string.allow_all_certificates_warning)
|
|
||||||
var allowAllCertificates: Boolean = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -528,8 +503,6 @@ class SettingsDev : FragmentedStorageFileJson() {
|
||||||
var channelCacheStartupCount = StateCache.instance.channelCacheStartupCount;
|
var channelCacheStartupCount = StateCache.instance.channelCacheStartupCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//region BOILERPLATE
|
//region BOILERPLATE
|
||||||
override fun encode(): String {
|
override fun encode(): String {
|
||||||
return Json.encodeToString(this);
|
return Json.encodeToString(this);
|
||||||
|
|
|
@ -5,10 +5,7 @@ import android.app.AlertDialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.drawable.Animatable
|
|
||||||
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
|
||||||
|
@ -21,7 +18,6 @@ import android.widget.Toast
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
|
||||||
import com.futo.platformplayer.casting.StateCasting
|
import com.futo.platformplayer.casting.StateCasting
|
||||||
import com.futo.platformplayer.dialogs.AutoUpdateDialog
|
import com.futo.platformplayer.dialogs.AutoUpdateDialog
|
||||||
import com.futo.platformplayer.dialogs.AutomaticBackupDialog
|
import com.futo.platformplayer.dialogs.AutomaticBackupDialog
|
||||||
|
@ -35,17 +31,12 @@ import com.futo.platformplayer.dialogs.ConnectedCastingDialog
|
||||||
import com.futo.platformplayer.dialogs.ImportDialog
|
import com.futo.platformplayer.dialogs.ImportDialog
|
||||||
import com.futo.platformplayer.dialogs.ImportOptionsDialog
|
import com.futo.platformplayer.dialogs.ImportOptionsDialog
|
||||||
import com.futo.platformplayer.dialogs.MigrateDialog
|
import com.futo.platformplayer.dialogs.MigrateDialog
|
||||||
import com.futo.platformplayer.dialogs.PluginUpdateDialog
|
|
||||||
import com.futo.platformplayer.dialogs.ProgressDialog
|
import com.futo.platformplayer.dialogs.ProgressDialog
|
||||||
import com.futo.platformplayer.engine.exceptions.PluginException
|
import com.futo.platformplayer.engine.exceptions.PluginException
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.MainFragment
|
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.SourceDetailFragment
|
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.VideoDetailFragment
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.ImportCache
|
import com.futo.platformplayer.models.ImportCache
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StateBackup
|
import com.futo.platformplayer.states.StateBackup
|
||||||
import com.futo.platformplayer.states.StatePlugins
|
|
||||||
import com.futo.platformplayer.stores.v2.ManagedStore
|
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||||
import com.futo.platformplayer.views.ToastView
|
import com.futo.platformplayer.views.ToastView
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
@ -193,58 +184,43 @@ class UIDialogs {
|
||||||
dialog.show();
|
dialog.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showPluginUpdateDialog(context: Context, oldConfig: SourcePluginConfig, newConfig: SourcePluginConfig) {
|
fun showDialog(context: Context, icon: Int, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action) {
|
||||||
val dialog = PluginUpdateDialog(context, oldConfig, newConfig);
|
|
||||||
registerDialogOpened(dialog);
|
|
||||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
|
||||||
dialog.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
fun showDialog(context: Context, icon: Int, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action): AlertDialog {
|
|
||||||
return showDialog(context, icon, false, text, textDetails, code, defaultCloseAction, *actions);
|
|
||||||
}
|
|
||||||
fun showDialog(context: Context, icon: Int, animated: Boolean, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action): AlertDialog {
|
|
||||||
val builder = AlertDialog.Builder(context);
|
val 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);
|
||||||
builder.setView(view);
|
builder.setView(view);
|
||||||
builder.setCancelable(defaultCloseAction > -2);
|
|
||||||
val dialog = builder.create();
|
val dialog = builder.create();
|
||||||
registerDialogOpened(dialog);
|
registerDialogOpened(dialog);
|
||||||
|
|
||||||
view.findViewById<ImageView>(R.id.dialog_icon).apply {
|
view.findViewById<ImageView>(R.id.dialog_icon).apply {
|
||||||
this.setImageResource(icon);
|
this.setImageResource(icon);
|
||||||
if(animated)
|
|
||||||
this.drawable.assume<Animatable, Unit> { it.start() };
|
|
||||||
}
|
}
|
||||||
view.findViewById<TextView>(R.id.dialog_text).apply {
|
view.findViewById<TextView>(R.id.dialog_text).apply {
|
||||||
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;
|
this.visibility = View.GONE;
|
||||||
else {
|
else
|
||||||
this.text = textDetails;
|
this.text = textDetails;
|
||||||
}
|
|
||||||
};
|
};
|
||||||
view.findViewById<TextView>(R.id.dialog_text_code).apply {
|
view.findViewById<TextView>(R.id.dialog_text_code).apply {
|
||||||
if (code == null) this.visibility = View.GONE;
|
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 {
|
||||||
this.marginStart = if(actions.size >= 2) dp14 / 2 else dp28 / 2;
|
if(actions.size > 1)
|
||||||
this.marginEnd = if(actions.size >= 2) dp14 / 2 else dp28 / 2;
|
this.marginEnd = if(actions.size > 2) dp14 else dp28;
|
||||||
};
|
};
|
||||||
buttonView.setTextColor(Color.WHITE);
|
buttonView.setTextColor(Color.WHITE);
|
||||||
buttonView.textSize = 14f;
|
buttonView.textSize = 14f;
|
||||||
|
@ -266,7 +242,7 @@ class UIDialogs {
|
||||||
|
|
||||||
return@map buttonView;
|
return@map buttonView;
|
||||||
};
|
};
|
||||||
if(actions.size <= 1 || center)
|
if(actions.size <= 1)
|
||||||
this.gravity = Gravity.CENTER;
|
this.gravity = Gravity.CENTER;
|
||||||
else
|
else
|
||||||
this.gravity = Gravity.END;
|
this.gravity = Gravity.END;
|
||||||
|
@ -281,7 +257,6 @@ class UIDialogs {
|
||||||
registerDialogClosed(dialog);
|
registerDialogClosed(dialog);
|
||||||
}
|
}
|
||||||
dialog.show();
|
dialog.show();
|
||||||
return dialog;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showGeneralErrorDialog(context: Context, msg: String, ex: Throwable? = null, button: String = "Ok", onOk: (()->Unit)? = null) {
|
fun showGeneralErrorDialog(context: Context, msg: String, ex: Throwable? = null, button: String = "Ok", onOk: (()->Unit)? = null) {
|
||||||
|
@ -294,48 +269,22 @@ class UIDialogs {
|
||||||
}, UIDialogs.ActionStyle.PRIMARY)
|
}, UIDialogs.ActionStyle.PRIMARY)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
fun showGeneralRetryErrorDialog(context: Context, msg: String, ex: Throwable? = null, retryAction: (() -> Unit)? = null, closeAction: (() -> Unit)? = null, mainFragment: MainFragment? = null) {
|
fun showGeneralRetryErrorDialog(context: Context, msg: String, ex: Throwable? = null, retryAction: (() -> Unit)? = null, closeAction: (() -> Unit)? = null) {
|
||||||
val pluginConfig = if(ex is PluginException) ex.config else null;
|
|
||||||
val pluginInfo = if(ex is PluginException)
|
val pluginInfo = if(ex is PluginException)
|
||||||
"\nPlugin [${ex.config.name}]" else "";
|
"\nPlugin [${ex.config.name}]" else "";
|
||||||
|
showDialog(context,
|
||||||
var exMsg = if(ex != null ) "${ex.message}" else "";
|
R.drawable.ic_error_pred,
|
||||||
if(pluginConfig != null && pluginConfig is SourcePluginConfig && StatePlugins.instance.hasUpdateAvailable(pluginConfig))
|
"${msg}${pluginInfo}",
|
||||||
exMsg += "\n\nAn update is available"
|
(if(ex != null ) "${ex.message}" else ""),
|
||||||
|
if(ex is PluginException) ex.code else null,
|
||||||
if(mainFragment != null && pluginConfig != null && pluginConfig is SourcePluginConfig && StatePlugins.instance.hasUpdateAvailable(pluginConfig))
|
0,
|
||||||
showDialog(context,
|
UIDialogs.Action(context.getString(R.string.retry), {
|
||||||
R.drawable.ic_error_pred,
|
retryAction?.invoke();
|
||||||
"${msg}${pluginInfo}",
|
}, UIDialogs.ActionStyle.PRIMARY),
|
||||||
exMsg,
|
UIDialogs.Action(context.getString(R.string.close), {
|
||||||
if(ex is PluginException) ex.code else null,
|
closeAction?.invoke()
|
||||||
1,
|
}, UIDialogs.ActionStyle.NONE)
|
||||||
UIDialogs.Action(context.getString(R.string.update), {
|
);
|
||||||
mainFragment.navigate<SourceDetailFragment>(SourceDetailFragment.UpdatePluginAction(pluginConfig));
|
|
||||||
if(mainFragment is VideoDetailFragment)
|
|
||||||
mainFragment.minimizeVideoDetail();
|
|
||||||
}, UIDialogs.ActionStyle.ACCENT),
|
|
||||||
UIDialogs.Action(context.getString(R.string.close), {
|
|
||||||
closeAction?.invoke()
|
|
||||||
}, UIDialogs.ActionStyle.NONE),
|
|
||||||
UIDialogs.Action(context.getString(R.string.retry), {
|
|
||||||
retryAction?.invoke();
|
|
||||||
}, UIDialogs.ActionStyle.PRIMARY)
|
|
||||||
);
|
|
||||||
else
|
|
||||||
showDialog(context,
|
|
||||||
R.drawable.ic_error_pred,
|
|
||||||
"${msg}${pluginInfo}",
|
|
||||||
exMsg,
|
|
||||||
if(ex is PluginException) ex.code else null,
|
|
||||||
0,
|
|
||||||
UIDialogs.Action(context.getString(R.string.close), {
|
|
||||||
closeAction?.invoke()
|
|
||||||
}, UIDialogs.ActionStyle.NONE),
|
|
||||||
UIDialogs.Action(context.getString(R.string.retry), {
|
|
||||||
retryAction?.invoke();
|
|
||||||
}, UIDialogs.ActionStyle.PRIMARY)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showSingleButtonDialog(context: Context, icon: Int, text: String, buttonText: String, action: (() -> Unit)) {
|
fun showSingleButtonDialog(context: Context, icon: Int, text: String, buttonText: String, action: (() -> Unit)) {
|
||||||
|
@ -356,13 +305,6 @@ 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);
|
||||||
|
@ -375,8 +317,8 @@ class UIDialogs {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showChangelogDialog(context: Context, lastVersion: Int, changelogs: Map<Int, String>? = null) {
|
fun showChangelogDialog(context: Context, lastVersion: Int) {
|
||||||
val dialog = ChangelogDialog(context, changelogs);
|
val dialog = ChangelogDialog(context);
|
||||||
registerDialogOpened(dialog);
|
registerDialogOpened(dialog);
|
||||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||||
dialog.show();
|
dialog.show();
|
||||||
|
@ -525,13 +467,11 @@ 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, center: Boolean = false) {
|
constructor(text: String, action: ()->Unit, style: ActionStyle = ActionStyle.NONE) {
|
||||||
this.text = text;
|
this.text = text;
|
||||||
this.action = action;
|
this.action = action;
|
||||||
this.style = style;
|
this.style = style;
|
||||||
this.center = center;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
enum class ActionStyle {
|
enum class ActionStyle {
|
||||||
|
|
|
@ -15,19 +15,14 @@ import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
|
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantAudioUrlSource
|
import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantAudioUrlSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantVideoUrlSource
|
import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantVideoUrlSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudioSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudioSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
||||||
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawAudioSource
|
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawSource
|
|
||||||
import com.futo.platformplayer.downloads.VideoLocal
|
import com.futo.platformplayer.downloads.VideoLocal
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionGroupFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionGroupFragment
|
||||||
import com.futo.platformplayer.helpers.VideoHelper
|
import com.futo.platformplayer.helpers.VideoHelper
|
||||||
|
@ -39,12 +34,12 @@ import com.futo.platformplayer.models.SubscriptionGroup
|
||||||
import com.futo.platformplayer.parsers.HLS
|
import com.futo.platformplayer.parsers.HLS
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StateDownloads
|
import com.futo.platformplayer.states.StateDownloads
|
||||||
import com.futo.platformplayer.states.StateHistory
|
|
||||||
import com.futo.platformplayer.states.StateMeta
|
import com.futo.platformplayer.states.StateMeta
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.platformplayer.states.StatePlayer
|
import com.futo.platformplayer.states.StatePlayer
|
||||||
import com.futo.platformplayer.states.StatePlaylists
|
import com.futo.platformplayer.states.StatePlaylists
|
||||||
import com.futo.platformplayer.states.StateSubscriptionGroups
|
import com.futo.platformplayer.states.StateSubscriptionGroups
|
||||||
|
import com.futo.platformplayer.states.StateSubscriptions
|
||||||
import com.futo.platformplayer.views.AnyAdapterView
|
import com.futo.platformplayer.views.AnyAdapterView
|
||||||
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
|
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
|
||||||
import com.futo.platformplayer.views.LoaderView
|
import com.futo.platformplayer.views.LoaderView
|
||||||
|
@ -79,36 +74,6 @@ class UISlideOverlays {
|
||||||
return menu;
|
return menu;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showQueueOptionsOverlay(context: Context, container: ViewGroup) {
|
|
||||||
UISlideOverlays.showOverlay(container, "Queue options", null, {
|
|
||||||
|
|
||||||
}, SlideUpMenuItem(context, R.drawable.ic_playlist, "Save as playlist", "", "Creates a new playlist with queue as videos", null, {
|
|
||||||
val nameInput = SlideUpMenuTextInput(container.context, container.context.getString(R.string.name));
|
|
||||||
val addPlaylistOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.create_new_playlist), container.context.getString(R.string.ok), false, nameInput);
|
|
||||||
|
|
||||||
addPlaylistOverlay.onOK.subscribe {
|
|
||||||
val text = nameInput.text.trim()
|
|
||||||
if (text.isBlank()) {
|
|
||||||
return@subscribe;
|
|
||||||
}
|
|
||||||
|
|
||||||
addPlaylistOverlay.hide();
|
|
||||||
nameInput.deactivate();
|
|
||||||
nameInput.clear();
|
|
||||||
StatePlayer.instance.saveQueueAsPlaylist(text);
|
|
||||||
UIDialogs.appToast("Playlist [${text}] created");
|
|
||||||
};
|
|
||||||
|
|
||||||
addPlaylistOverlay.onCancel.subscribe {
|
|
||||||
nameInput.deactivate();
|
|
||||||
nameInput.clear();
|
|
||||||
};
|
|
||||||
|
|
||||||
addPlaylistOverlay.show();
|
|
||||||
nameInput.activate();
|
|
||||||
}, false));
|
|
||||||
}
|
|
||||||
|
|
||||||
fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup): SlideUpMenuOverlay {
|
fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup): SlideUpMenuOverlay {
|
||||||
val items = arrayListOf<View>();
|
val items = arrayListOf<View>();
|
||||||
|
|
||||||
|
@ -126,17 +91,9 @@ class UISlideOverlays {
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
items.addAll(listOf(
|
items.addAll(listOf(
|
||||||
SlideUpMenuItem(
|
SlideUpMenuItem(container.context, R.drawable.ic_notifications, "Notifications", "", "notifications", {
|
||||||
container.context,
|
subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications;
|
||||||
R.drawable.ic_notifications,
|
}, false),
|
||||||
"Notifications",
|
|
||||||
"",
|
|
||||||
tag = "notifications",
|
|
||||||
call = {
|
|
||||||
subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications;
|
|
||||||
},
|
|
||||||
invokeParent = false
|
|
||||||
),
|
|
||||||
if(StateSubscriptionGroups.instance.getSubscriptionGroups().isNotEmpty())
|
if(StateSubscriptionGroups.instance.getSubscriptionGroups().isNotEmpty())
|
||||||
SlideUpMenuGroup(container.context, "Subscription Groups",
|
SlideUpMenuGroup(container.context, "Subscription Groups",
|
||||||
"You can select which groups this subscription is part of.",
|
"You can select which groups this subscription is part of.",
|
||||||
|
@ -171,62 +128,22 @@ class UISlideOverlays {
|
||||||
SlideUpMenuGroup(container.context, "Fetch Settings",
|
SlideUpMenuGroup(container.context, "Fetch Settings",
|
||||||
"Depending on the platform you might not need to enable a type for it to be available.",
|
"Depending on the platform you might not need to enable a type for it to be available.",
|
||||||
-1, listOf()),
|
-1, listOf()),
|
||||||
if(capabilities.hasType(ResultCapabilities.TYPE_LIVE)) SlideUpMenuItem(
|
if(capabilities.hasType(ResultCapabilities.TYPE_LIVE)) SlideUpMenuItem(container.context, R.drawable.ic_live_tv, "Livestreams", "Check for livestreams", "fetchLive", {
|
||||||
container.context,
|
subscription.doFetchLive = menu?.selectOption(null, "fetchLive", true, true) ?: subscription.doFetchLive;
|
||||||
R.drawable.ic_live_tv,
|
}, false) else null,
|
||||||
"Livestreams",
|
if(capabilities.hasType(ResultCapabilities.TYPE_STREAMS)) SlideUpMenuItem(container.context, R.drawable.ic_play, "Streams", "Check for streams", "fetchStreams", {
|
||||||
"Check for livestreams",
|
subscription.doFetchStreams = menu?.selectOption(null, "fetchStreams", true, true) ?: subscription.doFetchStreams;
|
||||||
tag = "fetchLive",
|
}, false) else null,
|
||||||
call = {
|
|
||||||
subscription.doFetchLive = menu?.selectOption(null, "fetchLive", true, true) ?: subscription.doFetchLive;
|
|
||||||
},
|
|
||||||
invokeParent = false
|
|
||||||
) else null,
|
|
||||||
if(capabilities.hasType(ResultCapabilities.TYPE_STREAMS)) SlideUpMenuItem(
|
|
||||||
container.context,
|
|
||||||
R.drawable.ic_play,
|
|
||||||
"Streams",
|
|
||||||
"Check for streams",
|
|
||||||
tag = "fetchStreams",
|
|
||||||
call = {
|
|
||||||
subscription.doFetchStreams = menu?.selectOption(null, "fetchStreams", true, true) ?: subscription.doFetchStreams;
|
|
||||||
},
|
|
||||||
invokeParent = false
|
|
||||||
) else null,
|
|
||||||
if(capabilities.hasType(ResultCapabilities.TYPE_VIDEOS))
|
if(capabilities.hasType(ResultCapabilities.TYPE_VIDEOS))
|
||||||
SlideUpMenuItem(
|
SlideUpMenuItem(container.context, R.drawable.ic_play, "Videos", "Check for videos", "fetchVideos", {
|
||||||
container.context,
|
subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos;
|
||||||
R.drawable.ic_play,
|
}, false) else if(capabilities.hasType(ResultCapabilities.TYPE_MIXED) || capabilities.types.isEmpty())
|
||||||
"Videos",
|
SlideUpMenuItem(container.context, R.drawable.ic_play, "Content", "Check for content", "fetchVideos", {
|
||||||
"Check for videos",
|
subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos;
|
||||||
tag = "fetchVideos",
|
}, false) else null,
|
||||||
call = {
|
if(capabilities.hasType(ResultCapabilities.TYPE_POSTS)) SlideUpMenuItem(container.context, R.drawable.ic_chat, "Posts", "Check for posts", "fetchPosts", {
|
||||||
subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos;
|
subscription.doFetchPosts = menu?.selectOption(null, "fetchPosts", true, true) ?: subscription.doFetchPosts;
|
||||||
},
|
}, false) else null/*,,
|
||||||
invokeParent = false
|
|
||||||
) else if(capabilities.hasType(ResultCapabilities.TYPE_MIXED) || capabilities.types.isEmpty())
|
|
||||||
SlideUpMenuItem(
|
|
||||||
container.context,
|
|
||||||
R.drawable.ic_play,
|
|
||||||
"Content",
|
|
||||||
"Check for content",
|
|
||||||
tag = "fetchVideos",
|
|
||||||
call = {
|
|
||||||
subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos;
|
|
||||||
},
|
|
||||||
invokeParent = false
|
|
||||||
) else null,
|
|
||||||
if(capabilities.hasType(ResultCapabilities.TYPE_POSTS)) SlideUpMenuItem(
|
|
||||||
container.context,
|
|
||||||
R.drawable.ic_chat,
|
|
||||||
"Posts",
|
|
||||||
"Check for posts",
|
|
||||||
tag = "fetchPosts",
|
|
||||||
call = {
|
|
||||||
subscription.doFetchPosts = menu?.selectOption(null, "fetchPosts", true, true) ?: subscription.doFetchPosts;
|
|
||||||
},
|
|
||||||
invokeParent = false
|
|
||||||
) else null/*,,
|
|
||||||
|
|
||||||
SlideUpMenuGroup(container.context, "Actions",
|
SlideUpMenuGroup(container.context, "Actions",
|
||||||
"Various things you can do with this subscription",
|
"Various things you can do with this subscription",
|
||||||
|
@ -325,23 +242,11 @@ class UISlideOverlays {
|
||||||
masterPlaylist = HLS.parseMasterPlaylist(masterPlaylistContent, sourceUrl)
|
masterPlaylist = HLS.parseMasterPlaylist(masterPlaylistContent, sourceUrl)
|
||||||
|
|
||||||
masterPlaylist.getAudioSources().forEach { it ->
|
masterPlaylist.getAudioSources().forEach { it ->
|
||||||
|
audioButtons.add(SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, listOf(it.language, it.codec).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "), it, {
|
||||||
val estSize = VideoHelper.estimateSourceSize(it);
|
selectedAudioVariant = it
|
||||||
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
slideUpMenuOverlay.selectOption(audioButtons, it)
|
||||||
audioButtons.add(SlideUpMenuItem(
|
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
||||||
container.context,
|
}, false))
|
||||||
R.drawable.ic_music,
|
|
||||||
it.name,
|
|
||||||
listOf(it.language, it.codec).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "),
|
|
||||||
(prefix + it.codec).trim(),
|
|
||||||
tag = it,
|
|
||||||
call = {
|
|
||||||
selectedAudioVariant = it
|
|
||||||
slideUpMenuOverlay.selectOption(audioButtons, it)
|
|
||||||
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
|
||||||
},
|
|
||||||
invokeParent = false
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*masterPlaylist.getSubtitleSources().forEach { it ->
|
/*masterPlaylist.getSubtitleSources().forEach { it ->
|
||||||
|
@ -353,24 +258,11 @@ class UISlideOverlays {
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
masterPlaylist.getVideoSources().forEach {
|
masterPlaylist.getVideoSources().forEach {
|
||||||
val estSize = VideoHelper.estimateSourceSize(it);
|
videoButtons.add(SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "${it.width}x${it.height}", it, {
|
||||||
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
selectedVideoVariant = it
|
||||||
videoButtons.add(SlideUpMenuItem(
|
slideUpMenuOverlay.selectOption(videoButtons, it)
|
||||||
container.context,
|
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
||||||
R.drawable.ic_movie,
|
}, false))
|
||||||
it.name,
|
|
||||||
"${it.width}x${it.height}",
|
|
||||||
(prefix + it.codec).trim(),
|
|
||||||
tag = it,
|
|
||||||
call = {
|
|
||||||
selectedVideoVariant = it
|
|
||||||
slideUpMenuOverlay.selectOption(videoButtons, it)
|
|
||||||
if (audioButtons.isEmpty()){
|
|
||||||
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
invokeParent = false
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val newItems = arrayListOf<View>()
|
val newItems = arrayListOf<View>()
|
||||||
|
@ -402,7 +294,7 @@ class UISlideOverlays {
|
||||||
UIDialogs.toast(container.context, "Variant video HLS playlist download started")
|
UIDialogs.toast(container.context, "Variant video HLS playlist download started")
|
||||||
slideUpMenuOverlay.hide()
|
slideUpMenuOverlay.hide()
|
||||||
} else if (source is IHLSManifestAudioSource) {
|
} else if (source is IHLSManifestAudioSource) {
|
||||||
StateDownloads.instance.download(video, null, HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, false, sourceUrl), null)
|
StateDownloads.instance.download(video, null, HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, sourceUrl), null)
|
||||||
UIDialogs.toast(container.context, "Variant audio HLS playlist download started")
|
UIDialogs.toast(container.context, "Variant audio HLS playlist download started")
|
||||||
slideUpMenuOverlay.hide()
|
slideUpMenuOverlay.hide()
|
||||||
} else {
|
} else {
|
||||||
|
@ -429,8 +321,8 @@ class UISlideOverlays {
|
||||||
|
|
||||||
|
|
||||||
val requiresAudio = descriptor is VideoUnMuxedSourceDescriptor;
|
val requiresAudio = descriptor is VideoUnMuxedSourceDescriptor;
|
||||||
var selectedVideo: IVideoSource? = null;
|
var selectedVideo: IVideoUrlSource? = null;
|
||||||
var selectedAudio: IAudioSource? = null;
|
var selectedAudio: IAudioUrlSource? = null;
|
||||||
var selectedSubtitle: ISubtitleSource? = null;
|
var selectedSubtitle: ISubtitleSource? = null;
|
||||||
|
|
||||||
val videoSources = descriptor.videoSources;
|
val videoSources = descriptor.videoSources;
|
||||||
|
@ -449,93 +341,45 @@ 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((if (audioSources != null) listOf(SlideUpMenuItem(
|
listOf(listOf(SlideUpMenuItem(container.context, R.drawable.ic_movie, container.context.getString(R.string.none), container.context.getString(R.string.audio_only), "none", {
|
||||||
container.context,
|
selectedVideo = null;
|
||||||
R.drawable.ic_movie,
|
menu?.selectOption(videoSources, "none");
|
||||||
container.context.getString(R.string.none),
|
if(selectedAudio != null || !requiresAudio)
|
||||||
container.context.getString(R.string.audio_only),
|
menu?.setOk(container.context.getString(R.string.download));
|
||||||
tag = "none",
|
}, false)) +
|
||||||
call = {
|
|
||||||
selectedVideo = null;
|
|
||||||
menu?.selectOption(videoSources, "none");
|
|
||||||
if(selectedAudio != null || !requiresAudio)
|
|
||||||
menu?.setOk(container.context.getString(R.string.download));
|
|
||||||
},
|
|
||||||
invokeParent = false
|
|
||||||
)) else listOf()) +
|
|
||||||
videoSources
|
videoSources
|
||||||
.filter { it.isDownloadable() }
|
.filter { it.isDownloadable() }
|
||||||
.map {
|
.map {
|
||||||
when (it) {
|
when (it) {
|
||||||
is IVideoUrlSource -> {
|
is IVideoUrlSource -> {
|
||||||
val estSize = VideoHelper.estimateSourceSize(it);
|
SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "${it.width}x${it.height}", it, {
|
||||||
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
selectedVideo = it
|
||||||
SlideUpMenuItem(
|
menu?.selectOption(videoSources, it);
|
||||||
container.context,
|
if(selectedAudio != null || !requiresAudio)
|
||||||
R.drawable.ic_movie,
|
menu?.setOk(container.context.getString(R.string.download));
|
||||||
it.name,
|
}, false)
|
||||||
"${it.width}x${it.height}",
|
|
||||||
(prefix + it.codec).trim(),
|
|
||||||
tag = it,
|
|
||||||
call = {
|
|
||||||
selectedVideo = it
|
|
||||||
menu?.selectOption(videoSources, it);
|
|
||||||
if(selectedAudio != null || !requiresAudio)
|
|
||||||
menu?.setOk(container.context.getString(R.string.download));
|
|
||||||
},
|
|
||||||
invokeParent = false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
is JSDashManifestRawSource -> {
|
|
||||||
val estSize = VideoHelper.estimateSourceSize(it);
|
|
||||||
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
|
||||||
SlideUpMenuItem(
|
|
||||||
container.context,
|
|
||||||
R.drawable.ic_movie,
|
|
||||||
it.name,
|
|
||||||
"${it.width}x${it.height}",
|
|
||||||
(prefix + it.codec).trim(),
|
|
||||||
tag = it,
|
|
||||||
call = {
|
|
||||||
selectedVideo = it
|
|
||||||
menu?.selectOption(videoSources, it);
|
|
||||||
if(selectedAudio != null || !requiresAudio)
|
|
||||||
menu?.setOk(container.context.getString(R.string.download));
|
|
||||||
},
|
|
||||||
invokeParent = false
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is IHLSManifestSource -> {
|
is IHLSManifestSource -> {
|
||||||
SlideUpMenuItem(
|
SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "HLS", it, {
|
||||||
container.context,
|
showHlsPicker(video, it, it.url, container)
|
||||||
R.drawable.ic_movie,
|
}, false)
|
||||||
it.name,
|
|
||||||
"HLS",
|
|
||||||
tag = it,
|
|
||||||
call = {
|
|
||||||
showHlsPicker(video, it, it.url, container)
|
|
||||||
},
|
|
||||||
invokeParent = false
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
Logger.w(TAG, "Unhandled source type for UISlideOverlay download items");
|
throw Exception("Unhandled source type")
|
||||||
null;//throw Exception("Unhandled source type")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.filterNotNull()).flatten().toList()
|
}).flatten().toList()
|
||||||
));
|
));
|
||||||
|
|
||||||
if(Settings.instance.downloads.getDefaultVideoQualityPixels() > 0 && videoSources.isNotEmpty()) {
|
if(Settings.instance.downloads.getDefaultVideoQualityPixels() > 0 && videoSources.isNotEmpty()) {
|
||||||
//TODO: Add HLS support here
|
//TODO: Add HLS support here
|
||||||
selectedVideo = VideoHelper.selectBestVideoSource(
|
selectedVideo = VideoHelper.selectBestVideoSource(
|
||||||
videoSources.filter { it is IVideoSource && it.isDownloadable() }.asIterable(),
|
videoSources.filter { it is IVideoUrlSource && it.isDownloadable() }.asIterable(),
|
||||||
Settings.instance.downloads.getDefaultVideoQualityPixels(),
|
Settings.instance.downloads.getDefaultVideoQualityPixels(),
|
||||||
FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS
|
FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS
|
||||||
) as IVideoSource?;
|
) as IVideoUrlSource?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (audioSources != null) {
|
if (audioSources != null) {
|
||||||
|
@ -544,90 +388,43 @@ class UISlideOverlays {
|
||||||
.map {
|
.map {
|
||||||
when (it) {
|
when (it) {
|
||||||
is IAudioUrlSource -> {
|
is IAudioUrlSource -> {
|
||||||
val estSize = VideoHelper.estimateSourceSize(it);
|
SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, "${it.bitrate}", it, {
|
||||||
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
selectedAudio = it
|
||||||
SlideUpMenuItem(
|
menu?.selectOption(audioSources, it);
|
||||||
container.context,
|
menu?.setOk(container.context.getString(R.string.download));
|
||||||
R.drawable.ic_music,
|
}, false);
|
||||||
it.name,
|
|
||||||
"${it.bitrate}",
|
|
||||||
(prefix + it.codec).trim(),
|
|
||||||
tag = it,
|
|
||||||
call = {
|
|
||||||
selectedAudio = it
|
|
||||||
menu?.selectOption(audioSources, it);
|
|
||||||
menu?.setOk(container.context.getString(R.string.download));
|
|
||||||
},
|
|
||||||
invokeParent = false
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
is JSDashManifestRawAudioSource -> {
|
|
||||||
val estSize = VideoHelper.estimateSourceSize(it);
|
|
||||||
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
|
||||||
SlideUpMenuItem(
|
|
||||||
container.context,
|
|
||||||
R.drawable.ic_music,
|
|
||||||
it.name,
|
|
||||||
"${it.bitrate}",
|
|
||||||
(prefix + it.codec).trim(),
|
|
||||||
tag = it,
|
|
||||||
call = {
|
|
||||||
selectedAudio = it
|
|
||||||
menu?.selectOption(audioSources, it);
|
|
||||||
menu?.setOk(container.context.getString(R.string.download));
|
|
||||||
},
|
|
||||||
invokeParent = false
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is IHLSManifestAudioSource -> {
|
is IHLSManifestAudioSource -> {
|
||||||
SlideUpMenuItem(
|
SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "HLS Audio", it, {
|
||||||
container.context,
|
showHlsPicker(video, it, it.url, container)
|
||||||
R.drawable.ic_movie,
|
}, false)
|
||||||
it.name,
|
|
||||||
"HLS Audio",
|
|
||||||
tag = it,
|
|
||||||
call = {
|
|
||||||
showHlsPicker(video, it, it.url, container)
|
|
||||||
},
|
|
||||||
invokeParent = false
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
Logger.w(TAG, "Unhandled source type for UISlideOverlay download items");
|
throw Exception("Unhandled source type")
|
||||||
null;//throw Exception("Unhandled source type")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.filterNotNull()));
|
}));
|
||||||
|
|
||||||
//TODO: Add HLS support here
|
//TODO: Add HLS support here
|
||||||
selectedAudio = VideoHelper.selectBestAudioSource(audioSources.filter { it is IAudioSource && it.isDownloadable() }.asIterable(),
|
selectedAudio = VideoHelper.selectBestAudioSource(audioSources.filter { it is IAudioUrlSource && it.isDownloadable() }.asIterable(),
|
||||||
FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS,
|
FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS,
|
||||||
Settings.instance.playback.getPrimaryLanguage(container.context),
|
Settings.instance.playback.getPrimaryLanguage(container.context),
|
||||||
if(Settings.instance.downloads.isHighBitrateDefault()) 9999999 else 1) as IAudioSource?;
|
if(Settings.instance.downloads.isHighBitrateDefault()) 9999999 else 1) as IAudioUrlSource?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(contentResolver != null && subtitleSources.isNotEmpty()) {
|
if(contentResolver != null && subtitleSources.isNotEmpty()) {
|
||||||
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.subtitles), subtitleSources, subtitleSources.map {
|
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.subtitles), subtitleSources, subtitleSources.map {
|
||||||
SlideUpMenuItem(
|
SlideUpMenuItem(container.context, R.drawable.ic_edit, it.name, "", it, {
|
||||||
container.context,
|
if (selectedSubtitle == it) {
|
||||||
R.drawable.ic_edit,
|
selectedSubtitle = null;
|
||||||
it.name,
|
menu?.selectOption(subtitleSources, null);
|
||||||
"",
|
} else {
|
||||||
tag = it,
|
selectedSubtitle = it;
|
||||||
call = {
|
menu?.selectOption(subtitleSources, it);
|
||||||
if (selectedSubtitle == it) {
|
}
|
||||||
selectedSubtitle = null;
|
}, false);
|
||||||
menu?.selectOption(subtitleSources, null);
|
|
||||||
} else {
|
|
||||||
selectedSubtitle = it;
|
|
||||||
menu?.selectOption(subtitleSources, it);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
invokeParent = false
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -645,18 +442,6 @@ class UISlideOverlays {
|
||||||
}
|
}
|
||||||
|
|
||||||
menu.onOK.subscribe {
|
menu.onOK.subscribe {
|
||||||
val sv = selectedVideo
|
|
||||||
if (sv is IHLSManifestSource) {
|
|
||||||
showHlsPicker(video, sv, sv.url, container)
|
|
||||||
return@subscribe
|
|
||||||
}
|
|
||||||
|
|
||||||
val sa = selectedAudio
|
|
||||||
if (sa is IHLSManifestAudioSource) {
|
|
||||||
showHlsPicker(video, sa, sa.url, container)
|
|
||||||
return@subscribe
|
|
||||||
}
|
|
||||||
|
|
||||||
menu.hide();
|
menu.hide();
|
||||||
val subtitleToDownload = selectedSubtitle;
|
val subtitleToDownload = selectedSubtitle;
|
||||||
if(selectedAudio != null || !requiresAudio) {
|
if(selectedAudio != null || !requiresAudio) {
|
||||||
|
@ -713,9 +498,8 @@ class UISlideOverlays {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch(ex: Throwable) {
|
catch(ex: Throwable) {
|
||||||
Logger.e(TAG, "Fetching details for download failed due to: " + ex.message, ex);
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
UIDialogs.toast(container.context.getString(R.string.failed_to_fetch_details_for_download) + "\n" + ex.message);
|
UIDialogs.toast(container.context.getString(R.string.failed_to_fetch_details_for_download));
|
||||||
handleUnknownDownload();
|
handleUnknownDownload();
|
||||||
loader.hide(true);
|
loader.hide(true);
|
||||||
}
|
}
|
||||||
|
@ -752,47 +536,23 @@ class UISlideOverlays {
|
||||||
);
|
);
|
||||||
|
|
||||||
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.target_resolution), "Video", resolutions.map {
|
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.target_resolution), "Video", resolutions.map {
|
||||||
SlideUpMenuItem(
|
SlideUpMenuItem(container.context, R.drawable.ic_movie, it.first, it.second, it.third, {
|
||||||
container.context,
|
targetPxSize = it.third;
|
||||||
R.drawable.ic_movie,
|
menu?.selectOption("Video", it.third);
|
||||||
it.first,
|
}, false)
|
||||||
it.second,
|
|
||||||
tag = it.third,
|
|
||||||
call = {
|
|
||||||
targetPxSize = it.third;
|
|
||||||
menu?.selectOption("Video", it.third);
|
|
||||||
},
|
|
||||||
invokeParent = false
|
|
||||||
)
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.target_bitrate), "Bitrate", listOf(
|
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.target_bitrate), "Bitrate", listOf(
|
||||||
SlideUpMenuItem(
|
SlideUpMenuItem(container.context, R.drawable.ic_movie, container.context.getString(R.string.low_bitrate), "", 1, {
|
||||||
container.context,
|
targetBitrate = 1;
|
||||||
R.drawable.ic_movie,
|
menu?.selectOption("Bitrate", 1);
|
||||||
container.context.getString(R.string.low_bitrate),
|
menu?.setOk(container.context.getString(R.string.download));
|
||||||
"",
|
}, false),
|
||||||
tag = 1,
|
SlideUpMenuItem(container.context, R.drawable.ic_movie, container.context.getString(R.string.high_bitrate), "", 9999999, {
|
||||||
call = {
|
targetBitrate = 9999999;
|
||||||
targetBitrate = 1;
|
menu?.selectOption("Bitrate", 9999999);
|
||||||
menu?.selectOption("Bitrate", 1);
|
menu?.setOk(container.context.getString(R.string.download));
|
||||||
menu?.setOk(container.context.getString(R.string.download));
|
}, false)
|
||||||
},
|
|
||||||
invokeParent = false
|
|
||||||
),
|
|
||||||
SlideUpMenuItem(
|
|
||||||
container.context,
|
|
||||||
R.drawable.ic_movie,
|
|
||||||
container.context.getString(R.string.high_bitrate),
|
|
||||||
"",
|
|
||||||
tag = 9999999,
|
|
||||||
call = {
|
|
||||||
targetBitrate = 9999999;
|
|
||||||
menu?.selectOption("Bitrate", 9999999);
|
|
||||||
menu?.setOk(container.context.getString(R.string.download));
|
|
||||||
},
|
|
||||||
invokeParent = false
|
|
||||||
)
|
|
||||||
)));
|
)));
|
||||||
|
|
||||||
|
|
||||||
|
@ -912,23 +672,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",
|
||||||
SlideUpMenuItem(container.context,
|
SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, lastUpdated.name, "${lastUpdated.videos.size} " + container.context.getString(R.string.videos), "",
|
||||||
R.drawable.ic_playlist_add,
|
{
|
||||||
lastUpdated.name,
|
StatePlaylists.instance.addToPlaylist(lastUpdated.id, video);
|
||||||
"${lastUpdated.videos.size} " + container.context.getString(R.string.videos),
|
|
||||||
tag = "",
|
|
||||||
call = {
|
|
||||||
if(StatePlaylists.instance.addToPlaylist(lastUpdated.id, video))
|
|
||||||
UIDialogs.appToast("Added to playlist [${lastUpdated?.name}]", false);
|
|
||||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
@ -939,93 +688,43 @@ 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(
|
||||||
if(!isLimited && !video.isLive)
|
SlideUpMenuItem(container.context, R.drawable.ic_download, container.context.getString(R.string.download), container.context.getString(R.string.download_the_video), "download", {
|
||||||
SlideUpMenuItem(
|
showDownloadVideoOverlay(video, container, true);
|
||||||
container.context,
|
}, false),
|
||||||
R.drawable.ic_download,
|
SlideUpMenuItem(container.context, R.drawable.ic_share, container.context.getString(R.string.share), "Share the video", "share", {
|
||||||
container.context.getString(R.string.download),
|
val url = if(video.shareUrl.isNotEmpty()) video.shareUrl else video.url;
|
||||||
container.context.getString(R.string.download_the_video),
|
container.context.startActivity(Intent.createChooser(Intent().apply {
|
||||||
tag = "download",
|
action = Intent.ACTION_SEND;
|
||||||
call = {
|
putExtra(Intent.EXTRA_TEXT, url);
|
||||||
showDownloadVideoOverlay(video, container, true);
|
type = "text/plain";
|
||||||
},
|
}, null));
|
||||||
invokeParent = false
|
}, false),
|
||||||
) else null,
|
SlideUpMenuItem(container.context, R.drawable.ic_visibility_off, container.context.getString(R.string.hide_creator_from_home), "", "hide_creator", {
|
||||||
SlideUpMenuItem(
|
StateMeta.instance.addHiddenCreator(video.author.url);
|
||||||
container.context,
|
UIDialogs.toast(container.context, "[${video.author.name}] hidden, you may need to reload home");
|
||||||
R.drawable.ic_share,
|
}))
|
||||||
container.context.getString(R.string.share),
|
+ actions)
|
||||||
"Share the video",
|
|
||||||
tag = "share",
|
|
||||||
call = {
|
|
||||||
val url = if(video.shareUrl.isNotEmpty()) video.shareUrl else video.url;
|
|
||||||
container.context.startActivity(Intent.createChooser(Intent().apply {
|
|
||||||
action = Intent.ACTION_SEND;
|
|
||||||
putExtra(Intent.EXTRA_TEXT, url);
|
|
||||||
type = "text/plain";
|
|
||||||
}, null));
|
|
||||||
},
|
|
||||||
invokeParent = false
|
|
||||||
),
|
|
||||||
SlideUpMenuItem(
|
|
||||||
container.context,
|
|
||||||
R.drawable.ic_visibility_off,
|
|
||||||
container.context.getString(R.string.hide_creator_from_home),
|
|
||||||
"",
|
|
||||||
tag = "hide_creator",
|
|
||||||
call = {
|
|
||||||
StateMeta.instance.addHiddenCreator(video.author.url);
|
|
||||||
UIDialogs.toast(container.context, "[${video.author.name}] hidden, you may need to reload home");
|
|
||||||
}))
|
|
||||||
+ actions).filterNotNull()
|
|
||||||
));
|
));
|
||||||
items.add(
|
items.add(
|
||||||
SlideUpMenuGroup(container.context, container.context.getString(R.string.add_to), "addto",
|
SlideUpMenuGroup(container.context, container.context.getString(R.string.add_to), "addto",
|
||||||
SlideUpMenuItem(container.context,
|
SlideUpMenuItem(container.context, R.drawable.ic_queue_add, container.context.getString(R.string.add_to_queue), "${queue.size} " + container.context.getString(R.string.videos), "queue",
|
||||||
R.drawable.ic_queue_add,
|
{ StatePlayer.instance.addToQueue(video); }),
|
||||||
container.context.getString(R.string.add_to_queue),
|
SlideUpMenuItem(container.context, R.drawable.ic_watchlist_add, "${container.context.getString(R.string.add_to)} " + StatePlayer.TYPE_WATCHLATER + "", "${watchLater.size} " + container.context.getString(R.string.videos), "watch later",
|
||||||
"${queue.size} " + container.context.getString(R.string.videos),
|
{ StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video)); })
|
||||||
tag = "queue",
|
|
||||||
call = { StatePlayer.instance.addToQueue(video); }),
|
|
||||||
SlideUpMenuItem(container.context,
|
|
||||||
R.drawable.ic_watchlist_add,
|
|
||||||
"${container.context.getString(R.string.add_to)} " + StatePlayer.TYPE_WATCHLATER + "",
|
|
||||||
"${watchLater.size} " + container.context.getString(R.string.videos),
|
|
||||||
tag = "watch later",
|
|
||||||
call = { StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true); }),
|
|
||||||
SlideUpMenuItem(container.context,
|
|
||||||
R.drawable.ic_history,
|
|
||||||
container.context.getString(R.string.add_to_history),
|
|
||||||
"Mark as watched",
|
|
||||||
tag = "history",
|
|
||||||
call = { StateHistory.instance.markAsWatched(video); }),
|
|
||||||
));
|
));
|
||||||
|
|
||||||
val playlistItems = arrayListOf<SlideUpMenuItem>();
|
val playlistItems = arrayListOf<SlideUpMenuItem>();
|
||||||
playlistItems.add(SlideUpMenuItem(
|
playlistItems.add(SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, container.context.getString(R.string.new_playlist), container.context.getString(R.string.add_to_new_playlist), "add_to_new_playlist", {
|
||||||
container.context,
|
showCreatePlaylistOverlay(container) {
|
||||||
R.drawable.ic_playlist_add,
|
val playlist = Playlist(it, arrayListOf(SerializedPlatformVideo.fromVideo(video)));
|
||||||
container.context.getString(R.string.new_playlist),
|
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
|
||||||
container.context.getString(R.string.add_to_new_playlist),
|
};
|
||||||
tag = "add_to_new_playlist",
|
}, false))
|
||||||
call = {
|
|
||||||
showCreatePlaylistOverlay(container) {
|
|
||||||
val playlist = Playlist(it, arrayListOf(SerializedPlatformVideo.fromVideo(video)));
|
|
||||||
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
invokeParent = false
|
|
||||||
))
|
|
||||||
|
|
||||||
for (playlist in allPlaylists) {
|
for (playlist in allPlaylists) {
|
||||||
playlistItems.add(SlideUpMenuItem(container.context,
|
playlistItems.add(SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, "${container.context.getString(R.string.add_to)} " + playlist.name + "", "${playlist.videos.size} " + container.context.getString(R.string.videos), "",
|
||||||
R.drawable.ic_playlist_add,
|
{
|
||||||
"${container.context.getString(R.string.add_to)} " + playlist.name + "",
|
StatePlaylists.instance.addToPlaylist(playlist.id, video);
|
||||||
"${playlist.videos.size} " + container.context.getString(R.string.videos),
|
|
||||||
tag = "",
|
|
||||||
call = {
|
|
||||||
if(StatePlaylists.instance.addToPlaylist(playlist.id, video))
|
|
||||||
UIDialogs.appToast("Added to playlist [${playlist.name}]", false);
|
|
||||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
@ -1046,14 +745,9 @@ class UISlideOverlays {
|
||||||
if (lastUpdated != null) {
|
if (lastUpdated != null) {
|
||||||
items.add(
|
items.add(
|
||||||
SlideUpMenuGroup(container.context, container.context.getString(R.string.recently_used_playlist), "recentlyusedplaylist",
|
SlideUpMenuGroup(container.context, container.context.getString(R.string.recently_used_playlist), "recentlyusedplaylist",
|
||||||
SlideUpMenuItem(container.context,
|
SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, lastUpdated.name, "${lastUpdated.videos.size} " + container.context.getString(R.string.videos), "",
|
||||||
R.drawable.ic_playlist_add,
|
{
|
||||||
lastUpdated.name,
|
StatePlaylists.instance.addToPlaylist(lastUpdated.id, video);
|
||||||
"${lastUpdated.videos.size} " + container.context.getString(R.string.videos),
|
|
||||||
tag = "",
|
|
||||||
call = {
|
|
||||||
if(StatePlaylists.instance.addToPlaylist(lastUpdated.id, video))
|
|
||||||
UIDialogs.appToast("Added to playlist [${lastUpdated?.name}]", false);
|
|
||||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
@ -1064,49 +758,26 @@ class UISlideOverlays {
|
||||||
val watchLater = StatePlaylists.instance.getWatchLater();
|
val watchLater = StatePlaylists.instance.getWatchLater();
|
||||||
items.add(
|
items.add(
|
||||||
SlideUpMenuGroup(container.context, container.context.getString(R.string.other), "other",
|
SlideUpMenuGroup(container.context, container.context.getString(R.string.other), "other",
|
||||||
SlideUpMenuItem(container.context,
|
SlideUpMenuItem(container.context, R.drawable.ic_queue_add, container.context.getString(R.string.queue), "${queue.size} " + container.context.getString(R.string.videos), "queue",
|
||||||
R.drawable.ic_queue_add,
|
{ StatePlayer.instance.addToQueue(video); }),
|
||||||
container.context.getString(R.string.queue),
|
SlideUpMenuItem(container.context, R.drawable.ic_watchlist_add, StatePlayer.TYPE_WATCHLATER, "${watchLater.size} " + container.context.getString(R.string.videos), "watch later",
|
||||||
"${queue.size} " + container.context.getString(R.string.videos),
|
{ StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video)); }),
|
||||||
tag = "queue",
|
SlideUpMenuItem(container.context, R.drawable.ic_download, container.context.getString(R.string.download), container.context.getString(R.string.download_the_video), container.context.getString(R.string.download),
|
||||||
call = { StatePlayer.instance.addToQueue(video); }),
|
{ showDownloadVideoOverlay(video, container, true); }, false))
|
||||||
SlideUpMenuItem(container.context,
|
|
||||||
R.drawable.ic_watchlist_add,
|
|
||||||
StatePlayer.TYPE_WATCHLATER,
|
|
||||||
"${watchLater.size} " + container.context.getString(R.string.videos),
|
|
||||||
tag = "watch later",
|
|
||||||
call = {
|
|
||||||
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true))
|
|
||||||
UIDialogs.appToast("Added to watch later", false);
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
val playlistItems = arrayListOf<SlideUpMenuItem>();
|
val playlistItems = arrayListOf<SlideUpMenuItem>();
|
||||||
playlistItems.add(SlideUpMenuItem(
|
playlistItems.add(SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, container.context.getString(R.string.new_playlist), container.context.getString(R.string.add_to_new_playlist), "add_to_new_playlist", {
|
||||||
container.context,
|
slideUpMenuOverlayUpdated(showCreatePlaylistOverlay(container) {
|
||||||
R.drawable.ic_playlist_add,
|
val playlist = Playlist(it, arrayListOf(SerializedPlatformVideo.fromVideo(video)));
|
||||||
container.context.getString(R.string.new_playlist),
|
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
|
||||||
container.context.getString(R.string.add_to_new_playlist),
|
});
|
||||||
tag = "add_to_new_playlist",
|
}, false))
|
||||||
call = {
|
|
||||||
slideUpMenuOverlayUpdated(showCreatePlaylistOverlay(container) {
|
|
||||||
val playlist = Playlist(it, arrayListOf(SerializedPlatformVideo.fromVideo(video)));
|
|
||||||
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
invokeParent = false
|
|
||||||
))
|
|
||||||
|
|
||||||
for (playlist in allPlaylists) {
|
for (playlist in allPlaylists) {
|
||||||
playlistItems.add(SlideUpMenuItem(container.context,
|
playlistItems.add(SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, playlist.name, "${playlist.videos.size} " + container.context.getString(R.string.videos), "",
|
||||||
R.drawable.ic_playlist_add,
|
{
|
||||||
playlist.name,
|
StatePlaylists.instance.addToPlaylist(playlist.id, video);
|
||||||
"${playlist.videos.size} " + container.context.getString(R.string.videos),
|
|
||||||
tag = "",
|
|
||||||
call = {
|
|
||||||
if(StatePlaylists.instance.addToPlaylist(playlist.id, video))
|
|
||||||
UIDialogs.appToast("Added to playlist [${playlist.name}]", false);
|
|
||||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
@ -1130,74 +801,40 @@ class UISlideOverlays {
|
||||||
|
|
||||||
val views = arrayOf(
|
val views = arrayOf(
|
||||||
hidden
|
hidden
|
||||||
.map { btn -> SlideUpMenuItem(
|
.map { btn -> SlideUpMenuItem(container.context, btn.iconResource, btn.text.text.toString(), "", "", {
|
||||||
container.context,
|
btn.handler?.invoke(btn);
|
||||||
btn.iconResource,
|
}, invokeParents) as View }.toTypedArray(),
|
||||||
btn.text.text.toString(),
|
arrayOf(SlideUpMenuItem(container.context, R.drawable.ic_pin, container.context.getString(R.string.change_pins), container.context.getString(R.string.decide_which_buttons_should_be_pinned), "", {
|
||||||
"",
|
showOrderOverlay(container, container.context.getString(R.string.select_your_pins_in_order), (visible + hidden).map { Pair(it.text.text.toString(), it.tagRef!!) }) {
|
||||||
tag = "",
|
val selected = it
|
||||||
call = {
|
.map { x -> visible.find { it.tagRef == x } ?: hidden.find { it.tagRef == x } }
|
||||||
btn.handler?.invoke(btn);
|
.filter { it != null }
|
||||||
},
|
.map { it!! }
|
||||||
invokeParent = invokeParents
|
.toList();
|
||||||
) as View }.toTypedArray(),
|
|
||||||
arrayOf(SlideUpMenuItem(
|
|
||||||
container.context,
|
|
||||||
R.drawable.ic_pin,
|
|
||||||
container.context.getString(R.string.change_pins),
|
|
||||||
container.context.getString(R.string.decide_which_buttons_should_be_pinned),
|
|
||||||
tag = "",
|
|
||||||
call = {
|
|
||||||
showOrderOverlay(container, container.context.getString(R.string.select_your_pins_in_order), (visible + hidden).map { Pair(it.text.text.toString(), it.tagRef!!) }, {
|
|
||||||
val selected = it
|
|
||||||
.map { x -> visible.find { it.tagRef == x } ?: hidden.find { it.tagRef == x } }
|
|
||||||
.filter { it != null }
|
|
||||||
.map { it!! }
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
onPinnedbuttons?.invoke(selected + (visible + hidden).filter { !selected.contains(it) });
|
onPinnedbuttons?.invoke(selected + (visible + hidden).filter { !selected.contains(it) });
|
||||||
});
|
}
|
||||||
},
|
}, false))
|
||||||
invokeParent = false
|
|
||||||
))
|
|
||||||
).flatten().toTypedArray();
|
).flatten().toTypedArray();
|
||||||
|
|
||||||
return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.more_options), null, true, *views).apply { show() };
|
return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.more_options), null, true, *views).apply { show() };
|
||||||
}
|
}
|
||||||
fun showOrderOverlay(container: ViewGroup, title: String, options: List<Pair<String, Any>>, onOrdered: (List<Any>)->Unit, description: String? = null) {
|
|
||||||
|
fun showOrderOverlay(container: ViewGroup, title: String, options: List<Pair<String, Any>>, onOrdered: (List<Any>)->Unit) {
|
||||||
val selection: MutableList<Any> = mutableListOf();
|
val selection: MutableList<Any> = mutableListOf();
|
||||||
|
|
||||||
var overlay: SlideUpMenuOverlay? = null;
|
var overlay: SlideUpMenuOverlay? = null;
|
||||||
|
|
||||||
overlay = SlideUpMenuOverlay(container.context, container, title, container.context.getString(R.string.save), true,
|
overlay = SlideUpMenuOverlay(container.context, container, title, container.context.getString(R.string.save), true,
|
||||||
listOf(
|
options.map { SlideUpMenuItem(container.context, R.drawable.ic_move_up, it.first, "", it.second, {
|
||||||
if(!description.isNullOrEmpty()) SlideUpMenuGroup(container.context, "", description, "", listOf()) else null,
|
|
||||||
).filterNotNull() +
|
|
||||||
(options.map { SlideUpMenuItem(
|
|
||||||
container.context,
|
|
||||||
R.drawable.ic_move_up,
|
|
||||||
it.first,
|
|
||||||
"",
|
|
||||||
tag = it.second,
|
|
||||||
call = {
|
|
||||||
val overlayItem = overlay?.getSlideUpItemByTag(it.second);
|
|
||||||
if(overlay!!.selectOption(null, it.second, true, true)) {
|
if(overlay!!.selectOption(null, it.second, true, true)) {
|
||||||
if(!selection.contains(it.second)) {
|
if(!selection.contains(it.second))
|
||||||
selection.add(it.second);
|
selection.add(it.second);
|
||||||
if(overlayItem != null) {
|
|
||||||
overlayItem.setSubText(selection.indexOf(it.second).toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
selection.remove(it.second);
|
|
||||||
if(overlayItem != null) {
|
|
||||||
overlayItem.setSubText("");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
else
|
||||||
invokeParent = false
|
selection.remove(it.second);
|
||||||
)
|
}, false)
|
||||||
}));
|
});
|
||||||
overlay.onOK.subscribe {
|
overlay.onOK.subscribe {
|
||||||
onOrdered.invoke(selection);
|
onOrdered.invoke(selection);
|
||||||
overlay.hide();
|
overlay.hide();
|
||||||
|
|
|
@ -13,7 +13,6 @@ import android.os.OperationCanceledException
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.WindowInsetsController
|
import android.view.WindowInsetsController
|
||||||
import android.view.WindowManager
|
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
|
@ -26,18 +25,12 @@ 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.ByteArrayOutputStream
|
|
||||||
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.security.SecureRandom
|
|
||||||
import java.time.OffsetDateTime
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.ThreadLocalRandom
|
import java.util.concurrent.ThreadLocalRandom
|
||||||
import java.util.zip.GZIPInputStream
|
|
||||||
import java.util.zip.GZIPOutputStream
|
|
||||||
|
|
||||||
private val _allowedCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz ";
|
private val _allowedCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz ";
|
||||||
fun getRandomString(sizeOfRandomString: Int): String {
|
fun getRandomString(sizeOfRandomString: Int): String {
|
||||||
|
@ -237,91 +230,3 @@ fun String.decodeUnicode(): String {
|
||||||
}
|
}
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun generateReadablePassword(length: Int): String {
|
|
||||||
val validChars = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789"
|
|
||||||
val secureRandom = SecureRandom()
|
|
||||||
val randomBytes = ByteArray(length)
|
|
||||||
secureRandom.nextBytes(randomBytes)
|
|
||||||
val sb = StringBuilder(length)
|
|
||||||
for (byte in randomBytes) {
|
|
||||||
val index = (byte.toInt() and 0xFF) % validChars.length
|
|
||||||
sb.append(validChars[index])
|
|
||||||
}
|
|
||||||
return sb.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun ByteArray.toGzip(): ByteArray {
|
|
||||||
if (this == null || this.isEmpty()) return ByteArray(0)
|
|
||||||
|
|
||||||
val gzipTimeStart = OffsetDateTime.now();
|
|
||||||
|
|
||||||
val outputStream = ByteArrayOutputStream()
|
|
||||||
GZIPOutputStream(outputStream).use { gzip ->
|
|
||||||
gzip.write(this)
|
|
||||||
}
|
|
||||||
val result = outputStream.toByteArray();
|
|
||||||
Logger.i("Utility", "Gzip compression time: ${gzipTimeStart.getNowDiffMiliseconds()}ms");
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
fun ByteArray.fromGzip(): ByteArray {
|
|
||||||
if (this == null || this.isEmpty()) return ByteArray(0)
|
|
||||||
|
|
||||||
val inputStream = ByteArrayInputStream(this)
|
|
||||||
val outputStream = ByteArrayOutputStream()
|
|
||||||
|
|
||||||
GZIPInputStream(inputStream).use { gzip ->
|
|
||||||
val buffer = ByteArray(1024)
|
|
||||||
var bytesRead: Int
|
|
||||||
while (gzip.read(buffer).also { bytesRead = it } != -1) {
|
|
||||||
outputStream.write(buffer, 0, bytesRead)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return outputStream.toByteArray()
|
|
||||||
}
|
|
|
@ -224,7 +224,7 @@ class AddSourceActivity : AppCompatActivity() {
|
||||||
val isNew = !StatePlatform.instance.getAvailableClients().any { it.id == config.id };
|
val isNew = !StatePlatform.instance.getAvailableClients().any { it.id == config.id };
|
||||||
StatePlugins.instance.installPlugin(this, lifecycleScope, config, script) {
|
StatePlugins.instance.installPlugin(this, lifecycleScope, config, script) {
|
||||||
if(it) {
|
if(it) {
|
||||||
StatePlugins.instance.clearUpdateAvailable(config)
|
StatePlatform.instance.clearUpdateAvailable(config)
|
||||||
if(isNew)
|
if(isNew)
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
StatePlatform.instance.enableClient(listOf(config.id));
|
StatePlatform.instance.enableClient(listOf(config.id));
|
||||||
|
|
|
@ -10,13 +10,11 @@ import androidx.appcompat.app.AppCompatActivity
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.views.buttons.BigButton
|
import com.futo.platformplayer.views.buttons.BigButton
|
||||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTextInput
|
|
||||||
import com.google.zxing.integration.android.IntentIntegrator
|
import com.google.zxing.integration.android.IntentIntegrator
|
||||||
|
|
||||||
class AddSourceOptionsActivity : AppCompatActivity() {
|
class AddSourceOptionsActivity : AppCompatActivity() {
|
||||||
lateinit var _buttonBack: ImageButton;
|
lateinit var _buttonBack: ImageButton;
|
||||||
|
|
||||||
lateinit var _overlayContainer: FrameLayout;
|
|
||||||
lateinit var _buttonQR: BigButton;
|
lateinit var _buttonQR: BigButton;
|
||||||
lateinit var _buttonBrowse: BigButton;
|
lateinit var _buttonBrowse: BigButton;
|
||||||
lateinit var _buttonURL: BigButton;
|
lateinit var _buttonURL: BigButton;
|
||||||
|
@ -56,7 +54,6 @@ class AddSourceOptionsActivity : AppCompatActivity() {
|
||||||
setContentView(R.layout.activity_add_source_options);
|
setContentView(R.layout.activity_add_source_options);
|
||||||
setNavigationBarColorAndIcons();
|
setNavigationBarColorAndIcons();
|
||||||
|
|
||||||
_overlayContainer = findViewById(R.id.overlay_container);
|
|
||||||
_buttonBack = findViewById(R.id.button_back);
|
_buttonBack = findViewById(R.id.button_back);
|
||||||
|
|
||||||
_buttonQR = findViewById(R.id.option_qr);
|
_buttonQR = findViewById(R.id.option_qr);
|
||||||
|
@ -84,25 +81,7 @@ class AddSourceOptionsActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
_buttonURL.onClick.subscribe {
|
_buttonURL.onClick.subscribe {
|
||||||
val nameInput = SlideUpMenuTextInput(this, "ex. https://yourplugin.com/config.json");
|
UIDialogs.toast(this, getString(R.string.not_implemented_yet));
|
||||||
UISlideOverlays.showOverlay(_overlayContainer, "Enter your url", "Install", {
|
|
||||||
|
|
||||||
val content = nameInput.text;
|
|
||||||
|
|
||||||
val url = if (content.startsWith("https://")) {
|
|
||||||
content
|
|
||||||
} else if (content.startsWith("grayjay://plugin/")) {
|
|
||||||
content.substring("grayjay://plugin/".length)
|
|
||||||
} else {
|
|
||||||
UIDialogs.toast(this, getString(R.string.not_a_plugin_url))
|
|
||||||
return@showOverlay;
|
|
||||||
}
|
|
||||||
|
|
||||||
val intent = Intent(this, AddSourceActivity::class.java).apply {
|
|
||||||
data = Uri.parse(url);
|
|
||||||
};
|
|
||||||
startActivity(intent);
|
|
||||||
}, nameInput)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -113,7 +113,7 @@ class LoginActivity : AppCompatActivity() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = "LoginActivity";
|
private val TAG = "LoginActivity";
|
||||||
private val REGEX_LOGIN_BUTTON = Regex("[a-zA-Z\\-\\.#:_ ]*");
|
private val REGEX_LOGIN_BUTTON = Regex("[a-zA-Z\\-\\.#_ ]*");
|
||||||
|
|
||||||
private var _callback: ((SourceAuth?) -> Unit)? = null;
|
private var _callback: ((SourceAuth?) -> Unit)? = null;
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -6,7 +6,6 @@ import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
@ -20,12 +19,7 @@ import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
import com.futo.platformplayer.views.buttons.BigButton
|
import com.futo.platformplayer.views.buttons.BigButton
|
||||||
import com.futo.polycentric.core.ContentType
|
import com.futo.polycentric.core.*
|
||||||
import com.futo.polycentric.core.SignedEvent
|
|
||||||
import com.futo.polycentric.core.StorageTypeCRDTItem
|
|
||||||
import com.futo.polycentric.core.StorageTypeCRDTSetItem
|
|
||||||
import com.futo.polycentric.core.Store
|
|
||||||
import com.futo.polycentric.core.toBase64Url
|
|
||||||
import com.google.zxing.BarcodeFormat
|
import com.google.zxing.BarcodeFormat
|
||||||
import com.google.zxing.MultiFormatWriter
|
import com.google.zxing.MultiFormatWriter
|
||||||
import com.google.zxing.common.BitMatrix
|
import com.google.zxing.common.BitMatrix
|
||||||
|
@ -70,8 +64,11 @@ class PolycentricBackupActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
_buttonShare.onClick.subscribe {
|
_buttonShare.onClick.subscribe {
|
||||||
val shareIntent = Intent(Intent.ACTION_VIEW, Uri.parse(_exportBundle))
|
val shareIntent = Intent(Intent.ACTION_SEND).apply {
|
||||||
startActivity(Intent.createChooser(shareIntent, "Share ID"));
|
type = "text/plain";
|
||||||
|
putExtra(Intent.EXTRA_TEXT, _exportBundle);
|
||||||
|
}
|
||||||
|
startActivity(Intent.createChooser(shareIntent, getString(R.string.share_text)));
|
||||||
};
|
};
|
||||||
|
|
||||||
_buttonCopy.onClick.subscribe {
|
_buttonCopy.onClick.subscribe {
|
||||||
|
|
|
@ -3,7 +3,6 @@ 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
|
||||||
|
@ -11,16 +10,14 @@ import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
|
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.polycentric.PolycentricStorage
|
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.ApiMethods
|
|
||||||
import com.futo.polycentric.core.ProcessHandle
|
import com.futo.polycentric.core.ProcessHandle
|
||||||
import com.futo.polycentric.core.Store
|
import com.futo.polycentric.core.Store
|
||||||
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
@ -29,7 +26,6 @@ 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;
|
||||||
|
@ -46,7 +42,6 @@ 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();
|
||||||
};
|
};
|
||||||
|
@ -69,49 +64,35 @@ 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 {
|
||||||
try {
|
processHandle = ProcessHandle.create();
|
||||||
processHandle = ProcessHandle.create();
|
Store.instance.addProcessSecret(processHandle.processSecret);
|
||||||
Store.instance.addProcessSecret(processHandle.processSecret);
|
|
||||||
|
|
||||||
try {
|
|
||||||
PolycentricStorage.instance.addProcessSecret(processHandle.processSecret)
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
|
|
||||||
}
|
|
||||||
|
|
||||||
processHandle.addServer(ApiMethods.SERVER);
|
|
||||||
processHandle.setUsername(username);
|
|
||||||
StatePolycentric.instance.setProcessHandle(processHandle);
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.e(TAG, getString(R.string.failed_to_create_profile), e);
|
|
||||||
return@launch;
|
|
||||||
} finally {
|
|
||||||
_creating = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Logger.i(TAG, "Started backfill");
|
PolycentricStorage.instance.addProcessSecret(processHandle.processSecret)
|
||||||
processHandle.fullyBackfillServersAnnounceExceptions();
|
|
||||||
Logger.i(TAG, "Finished backfill");
|
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, getString(R.string.failed_to_fully_backfill_servers), e);
|
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
processHandle.addServer("https://srv1-stg.polycentric.io");
|
||||||
|
processHandle.setUsername(username);
|
||||||
|
StatePolycentric.instance.setProcessHandle(processHandle);
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, getString(R.string.failed_to_create_profile), e);
|
||||||
|
return@launch;
|
||||||
|
} finally {
|
||||||
|
_creating = false;
|
||||||
}
|
}
|
||||||
finally {
|
|
||||||
withContext(Dispatchers.Main) {
|
try {
|
||||||
_profileName.isEnabled = true;
|
Logger.i(TAG, "Started backfill");
|
||||||
_buttonCreate.visibility = View.VISIBLE;
|
processHandle.fullyBackfillServersAnnounceExceptions();
|
||||||
_loader.stop();
|
Logger.i(TAG, "Finished backfill");
|
||||||
_loader.visibility = View.GONE;
|
} catch (e: Throwable) {
|
||||||
}
|
Logger.e(TAG, getString(R.string.failed_to_fully_backfill_servers), e);
|
||||||
}
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
|
|
|
@ -8,7 +8,6 @@ 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
|
||||||
|
@ -29,7 +28,6 @@ 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))
|
||||||
|
@ -44,7 +42,6 @@ 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();
|
||||||
};
|
};
|
||||||
|
@ -81,7 +78,6 @@ 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));
|
||||||
|
|
|
@ -12,12 +12,12 @@ import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||||
import com.futo.platformplayer.polycentric.PolycentricStorage
|
import com.futo.platformplayer.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.overlays.LoaderOverlay
|
import com.futo.platformplayer.views.overlays.LoaderOverlay
|
||||||
import com.futo.polycentric.core.ApiMethods
|
|
||||||
import com.futo.polycentric.core.KeyPair
|
import com.futo.polycentric.core.KeyPair
|
||||||
import com.futo.polycentric.core.Process
|
import com.futo.polycentric.core.Process
|
||||||
import com.futo.polycentric.core.ProcessSecret
|
import com.futo.polycentric.core.ProcessSecret
|
||||||
|
@ -145,7 +145,7 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
StatePolycentric.instance.setProcessHandle(processHandle);
|
StatePolycentric.instance.setProcessHandle(processHandle);
|
||||||
processHandle.fullyBackfillClient(ApiMethods.SERVER);
|
processHandle.fullyBackfillClient(PolycentricCache.SERVER);
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
startActivity(Intent(this@PolycentricImportProfileActivity, PolycentricProfileActivity::class.java));
|
startActivity(Intent(this@PolycentricImportProfileActivity, PolycentricProfileActivity::class.java));
|
||||||
finish();
|
finish();
|
||||||
|
|
|
@ -21,20 +21,18 @@ import com.bumptech.glide.Glide
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.dp
|
import com.futo.platformplayer.dp
|
||||||
|
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
||||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.polycentric.PolycentricStorage
|
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||||
import com.futo.platformplayer.selectBestImage
|
import com.futo.platformplayer.selectBestImage
|
||||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
import com.futo.platformplayer.views.buttons.BigButton
|
import com.futo.platformplayer.views.buttons.BigButton
|
||||||
import com.futo.platformplayer.views.overlays.LoaderOverlay
|
import com.futo.platformplayer.views.overlays.LoaderOverlay
|
||||||
import com.futo.polycentric.core.ApiMethods
|
|
||||||
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.fullyBackfillServersAnnounceExceptions
|
|
||||||
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
|
||||||
|
@ -49,7 +47,6 @@ 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;
|
||||||
|
@ -71,14 +68,10 @@ 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();
|
||||||
|
@ -99,16 +92,6 @@ 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));
|
||||||
|
@ -125,7 +108,6 @@ 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();
|
||||||
});
|
});
|
||||||
|
@ -145,7 +127,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
processHandle.fullyBackfillClient(ApiMethods.SERVER)
|
processHandle.fullyBackfillClient(PolycentricCache.SERVER)
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
updateUI();
|
updateUI();
|
||||||
|
|
|
@ -18,7 +18,6 @@ import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
import com.futo.platformplayer.constructs.Event0
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.views.LoaderView
|
import com.futo.platformplayer.views.LoaderView
|
||||||
|
@ -185,19 +184,12 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
|
||||||
resultLauncher.launch(intent);
|
resultLauncher.launch(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
settingsActivityClosed.emit()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
//TODO: Temporary for solving Settings issues
|
//TODO: Temporary for solving Settings issues
|
||||||
@SuppressLint("StaticFieldLeak")
|
@SuppressLint("StaticFieldLeak")
|
||||||
private var _lastActivity: SettingsActivity? = null;
|
private var _lastActivity: SettingsActivity? = null;
|
||||||
|
|
||||||
val settingsActivityClosed = Event0()
|
|
||||||
|
|
||||||
fun getActivity(): SettingsActivity? {
|
fun getActivity(): SettingsActivity? {
|
||||||
val act = _lastActivity;
|
val act = _lastActivity;
|
||||||
if(act != null && !act._isFinished)
|
if(act != null && !act._isFinished)
|
||||||
|
|
|
@ -1,140 +0,0 @@
|
||||||
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(session?.linkType ?: LinkType.None)
|
|
||||||
.setName(session?.displayName ?: StateSync.instance.getCachedName(publicKey) ?: publicKey)
|
|
||||||
//TODO: also display public key?
|
|
||||||
.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"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,148 +0,0 @@
|
||||||
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) { complete, message ->
|
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
|
||||||
if (complete != null && 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"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,142 +0,0 @@
|
||||||
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, StateSync.instance.pairingCode)
|
|
||||||
val json = Json.encodeToString(selfDeviceInfo)
|
|
||||||
val base64 = Base64.encodeToString(json.toByteArray(), Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP)
|
|
||||||
val url = "grayjay://sync/${base64}"
|
|
||||||
setCode(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setCode(code: String?) {
|
|
||||||
_code = code
|
|
||||||
|
|
||||||
_textCode.text = code
|
|
||||||
|
|
||||||
if (code == null) {
|
|
||||||
_imageQR.visibility = View.INVISIBLE
|
|
||||||
_textQR.visibility = View.INVISIBLE
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
val dimension = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 200f, resources.displayMetrics).toInt()
|
|
||||||
val qrCodeBitmap = generateQRCode(code, dimension, dimension)
|
|
||||||
_imageQR.setImageBitmap(qrCodeBitmap)
|
|
||||||
_imageQR.visibility = View.VISIBLE
|
|
||||||
_textQR.visibility = View.VISIBLE
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Logger.e(TAG, getString(R.string.failed_to_generate_qr_code), e)
|
|
||||||
_imageQR.visibility = View.INVISIBLE
|
|
||||||
_textQR.visibility = View.INVISIBLE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun generateQRCode(content: String, width: Int, height: Int): Bitmap {
|
|
||||||
val bitMatrix = MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height);
|
|
||||||
return bitMatrixToBitmap(bitMatrix);
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun bitMatrixToBitmap(matrix: BitMatrix): Bitmap {
|
|
||||||
val width = matrix.width;
|
|
||||||
val height = matrix.height;
|
|
||||||
val bmp = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
|
|
||||||
|
|
||||||
for (x in 0 until width) {
|
|
||||||
for (y in 0 until height) {
|
|
||||||
bmp.setPixel(x, y, if (matrix[x, y]) Color.BLACK else Color.WHITE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return bmp;
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getIPs(): List<String> {
|
|
||||||
val ips = arrayListOf<String>()
|
|
||||||
for (intf in NetworkInterface.getNetworkInterfaces()) {
|
|
||||||
for (addr in intf.inetAddresses) {
|
|
||||||
if (addr.isLoopbackAddress) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (addr.address.size != 4) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
addr.hostAddress?.let { ips.add(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ips
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val TAG = "SyncShowPairingCodeActivity"
|
|
||||||
var activity: SyncShowPairingCodeActivity? = null
|
|
||||||
private set
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,12 +1,8 @@
|
||||||
package com.futo.platformplayer.api.http
|
package com.futo.platformplayer.api.http
|
||||||
|
|
||||||
import androidx.collection.arrayMapOf
|
|
||||||
import com.futo.platformplayer.SettingsDev
|
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.ensureNotMainThread
|
import com.futo.platformplayer.ensureNotMainThread
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
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
|
||||||
|
@ -17,16 +13,10 @@ import okhttp3.Response
|
||||||
import okhttp3.ResponseBody
|
import okhttp3.ResponseBody
|
||||||
import okhttp3.WebSocket
|
import okhttp3.WebSocket
|
||||||
import okhttp3.WebSocketListener
|
import okhttp3.WebSocketListener
|
||||||
import java.security.SecureRandom
|
|
||||||
import java.security.cert.X509Certificate
|
|
||||||
import java.time.Duration
|
|
||||||
import javax.net.ssl.SSLContext
|
|
||||||
import javax.net.ssl.TrustManager
|
|
||||||
import javax.net.ssl.X509TrustManager
|
|
||||||
import kotlin.system.measureTimeMillis
|
import kotlin.system.measureTimeMillis
|
||||||
|
|
||||||
open class ManagedHttpClient {
|
open class ManagedHttpClient {
|
||||||
protected var _builderTemplate: OkHttpClient.Builder;
|
protected val _builderTemplate: OkHttpClient.Builder;
|
||||||
|
|
||||||
private var client: OkHttpClient;
|
private var client: OkHttpClient;
|
||||||
|
|
||||||
|
@ -35,38 +25,8 @@ open class ManagedHttpClient {
|
||||||
|
|
||||||
var user_agent = "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0"
|
var user_agent = "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0"
|
||||||
|
|
||||||
fun setTimeout(timeout: Long) {
|
|
||||||
rebuildClient {
|
|
||||||
it.callTimeout(Duration.ofMillis(client.callTimeoutMillis.toLong()))
|
|
||||||
.writeTimeout(Duration.ofMillis(client.writeTimeoutMillis.toLong()))
|
|
||||||
.readTimeout(Duration.ofMillis(client.readTimeoutMillis.toLong()))
|
|
||||||
.connectTimeout(Duration.ofMillis(timeout));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val trustAllCerts = arrayOf<TrustManager>(
|
|
||||||
object: X509TrustManager {
|
|
||||||
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) { }
|
|
||||||
override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) { }
|
|
||||||
override fun getAcceptedIssuers(): Array<X509Certificate> {
|
|
||||||
return arrayOf();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
private fun trustAllCertificates(builder: OkHttpClient.Builder) {
|
|
||||||
val sslContext = SSLContext.getInstance("SSL");
|
|
||||||
sslContext.init(null, trustAllCerts, SecureRandom());
|
|
||||||
builder.sslSocketFactory(sslContext.socketFactory, trustAllCerts[0] as X509TrustManager);
|
|
||||||
builder.hostnameVerifier { a, b ->
|
|
||||||
return@hostnameVerifier true;
|
|
||||||
}
|
|
||||||
Logger.w(TAG, "Creating INSECURE client (TrustAll)");
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(builder: OkHttpClient.Builder = OkHttpClient.Builder()) {
|
constructor(builder: OkHttpClient.Builder = OkHttpClient.Builder()) {
|
||||||
_builderTemplate = builder;
|
_builderTemplate = builder;
|
||||||
if(FragmentedStorage.isInitialized && StateApp.instance.isMainActive && SettingsDev.instance.developerMode && SettingsDev.instance.networking.allowAllCertificates)
|
|
||||||
trustAllCertificates(builder);
|
|
||||||
client = builder.addNetworkInterceptor { chain ->
|
client = builder.addNetworkInterceptor { chain ->
|
||||||
val request = beforeRequest(chain.request());
|
val request = beforeRequest(chain.request());
|
||||||
val response = afterRequest(chain.proceed(request));
|
val response = afterRequest(chain.proceed(request));
|
||||||
|
@ -74,15 +34,6 @@ open class ManagedHttpClient {
|
||||||
}.build();
|
}.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
fun rebuildClient(modify: (OkHttpClient.Builder) -> OkHttpClient.Builder) {
|
|
||||||
_builderTemplate = modify(_builderTemplate);
|
|
||||||
client = _builderTemplate.addNetworkInterceptor { chain ->
|
|
||||||
val request = beforeRequest(chain.request());
|
|
||||||
val response = afterRequest(chain.proceed(request));
|
|
||||||
return@addNetworkInterceptor response;
|
|
||||||
}.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun clone(): ManagedHttpClient {
|
open fun clone(): ManagedHttpClient {
|
||||||
val clonedClient = ManagedHttpClient(_builderTemplate);
|
val clonedClient = ManagedHttpClient(_builderTemplate);
|
||||||
clonedClient.user_agent = user_agent;
|
clonedClient.user_agent = user_agent;
|
||||||
|
|
|
@ -210,20 +210,6 @@ class HttpContext : AutoCloseable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun respondBytes(status: Int, headers: HttpHeaders, body: ByteArray? = null) {
|
|
||||||
if(headers.get("content-length").isNullOrEmpty()) {
|
|
||||||
if (body != null) {
|
|
||||||
headers.put("content-length", body.size.toString());
|
|
||||||
} else {
|
|
||||||
headers.put("content-length", "0")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
respond(status, headers) { responseStream ->
|
|
||||||
if(body != null) {
|
|
||||||
responseStream.write(body);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fun respond(status: Int, headers: HttpHeaders, writing: (OutputStream)->Unit) {
|
fun respond(status: Int, headers: HttpHeaders, writing: (OutputStream)->Unit) {
|
||||||
val responseStream = _responseStream ?: throw IllegalStateException("No response stream set");
|
val responseStream = _responseStream ?: throw IllegalStateException("No response stream set");
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ package com.futo.platformplayer.api.http.server
|
||||||
|
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.api.http.server.exceptions.EmptyRequestException
|
import com.futo.platformplayer.api.http.server.exceptions.EmptyRequestException
|
||||||
import com.futo.platformplayer.api.http.server.handlers.HttpFunctionHandler
|
import com.futo.platformplayer.api.http.server.handlers.HttpFuntionHandler
|
||||||
import com.futo.platformplayer.api.http.server.handlers.HttpHandler
|
import com.futo.platformplayer.api.http.server.handlers.HttpHandler
|
||||||
import com.futo.platformplayer.api.http.server.handlers.HttpOptionsAllowHandler
|
import com.futo.platformplayer.api.http.server.handlers.HttpOptionsAllowHandler
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
@ -208,20 +208,20 @@ class ManagedHttpServer(private val _requestedPort: Int = 0) {
|
||||||
|
|
||||||
for(getMethod in getMethods)
|
for(getMethod in getMethods)
|
||||||
if(getMethod.first.parameterTypes.firstOrNull() == HttpContext::class.java && getMethod.first.parameterCount == 1)
|
if(getMethod.first.parameterTypes.firstOrNull() == HttpContext::class.java && getMethod.first.parameterCount == 1)
|
||||||
addHandler(HttpFunctionHandler("GET", getMethod.second.path) { getMethod.first.invoke(obj, it) }).apply {
|
addHandler(HttpFuntionHandler("GET", getMethod.second.path) { getMethod.first.invoke(obj, it) }).apply {
|
||||||
if(!getMethod.second.contentType.isEmpty())
|
if(!getMethod.second.contentType.isEmpty())
|
||||||
this.withContentType(getMethod.second.contentType);
|
this.withContentType(getMethod.second.contentType);
|
||||||
}.withContentType(getMethod.second.contentType);
|
}.withContentType(getMethod.second.contentType);
|
||||||
for(postMethod in postMethods)
|
for(postMethod in postMethods)
|
||||||
if(postMethod.first.parameterTypes.firstOrNull() == HttpContext::class.java && postMethod.first.parameterCount == 1)
|
if(postMethod.first.parameterTypes.firstOrNull() == HttpContext::class.java && postMethod.first.parameterCount == 1)
|
||||||
addHandler(HttpFunctionHandler("POST", postMethod.second.path) { postMethod.first.invoke(obj, it) }).apply {
|
addHandler(HttpFuntionHandler("POST", postMethod.second.path) { postMethod.first.invoke(obj, it) }).apply {
|
||||||
if(!postMethod.second.contentType.isEmpty())
|
if(!postMethod.second.contentType.isEmpty())
|
||||||
this.withContentType(postMethod.second.contentType);
|
this.withContentType(postMethod.second.contentType);
|
||||||
}.withContentType(postMethod.second.contentType);
|
}.withContentType(postMethod.second.contentType);
|
||||||
|
|
||||||
for(getField in getFields) {
|
for(getField in getFields) {
|
||||||
getField.first.isAccessible = true;
|
getField.first.isAccessible = true;
|
||||||
addHandler(HttpFunctionHandler("GET", getField.second.path) {
|
addHandler(HttpFuntionHandler("GET", getField.second.path) {
|
||||||
val value = getField.first.get(obj) as String?;
|
val value = getField.first.get(obj) as String?;
|
||||||
if(value != null) {
|
if(value != null) {
|
||||||
val headers = HttpHeaders(
|
val headers = HttpHeaders(
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ package com.futo.platformplayer.api.http.server.handlers
|
||||||
|
|
||||||
import com.futo.platformplayer.api.http.server.HttpContext
|
import com.futo.platformplayer.api.http.server.HttpContext
|
||||||
|
|
||||||
class HttpFunctionHandler(method: String, path: String, val handler: (HttpContext)->Unit) : HttpHandler(method, path) {
|
class HttpFuntionHandler(method: String, path: String, val handler: (HttpContext)->Unit) : HttpHandler(method, path) {
|
||||||
override fun handle(httpContext: HttpContext) {
|
override fun handle(httpContext: HttpContext) {
|
||||||
httpContext.setResponseHeaders(this.headers);
|
httpContext.setResponseHeaders(this.headers);
|
||||||
handler(httpContext);
|
handler(httpContext);
|
||||||
|
|
|
@ -0,0 +1,106 @@
|
||||||
|
package com.futo.platformplayer.api.media
|
||||||
|
|
||||||
|
import androidx.collection.LruCache
|
||||||
|
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
||||||
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
|
import com.futo.platformplayer.api.media.models.chapters.IChapter
|
||||||
|
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
|
||||||
|
import com.futo.platformplayer.api.media.models.live.ILiveChatWindowDescriptor
|
||||||
|
import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
|
||||||
|
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
||||||
|
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
||||||
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
|
import com.futo.platformplayer.models.ImageVariable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A temporary class that caches video results
|
||||||
|
* In future this should be part of a bigger system
|
||||||
|
*/
|
||||||
|
class CachedPlatformClient : IPlatformClient {
|
||||||
|
private val _client : IPlatformClient;
|
||||||
|
override val id: String get() = _client.id;
|
||||||
|
override val name: String get() = _client.name;
|
||||||
|
override val icon: ImageVariable? get() = _client.icon;
|
||||||
|
|
||||||
|
private val _cache: LruCache<String, IPlatformContentDetails>;
|
||||||
|
|
||||||
|
override val capabilities: PlatformClientCapabilities
|
||||||
|
get() = _client.capabilities;
|
||||||
|
|
||||||
|
constructor(client : IPlatformClient, cacheSize : Int = 10 * 1024 * 1024) {
|
||||||
|
this._client = client;
|
||||||
|
this._cache = LruCache<String, IPlatformContentDetails>(cacheSize);
|
||||||
|
}
|
||||||
|
override fun initialize() { _client.initialize() }
|
||||||
|
override fun disable() { _client.disable() }
|
||||||
|
|
||||||
|
override fun isContentDetailsUrl(url: String): Boolean = _client.isContentDetailsUrl(url);
|
||||||
|
override fun getContentDetails(url: String): IPlatformContentDetails {
|
||||||
|
var result = _cache.get(url);
|
||||||
|
if(result == null) {
|
||||||
|
result = _client.getContentDetails(url);
|
||||||
|
_cache.put(url, result);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getContentChapters(url: String): List<IChapter> = _client.getContentChapters(url);
|
||||||
|
override fun getPlaybackTracker(url: String): IPlaybackTracker? = _client.getPlaybackTracker(url);
|
||||||
|
|
||||||
|
override fun isChannelUrl(url: String): Boolean = _client.isChannelUrl(url);
|
||||||
|
override fun getChannel(channelUrl: String): IPlatformChannel = _client.getChannel(channelUrl);
|
||||||
|
|
||||||
|
override fun getChannelCapabilities(): ResultCapabilities = _client.getChannelCapabilities();
|
||||||
|
override fun getChannelContents(
|
||||||
|
channelUrl: String,
|
||||||
|
type: String?,
|
||||||
|
order: String?,
|
||||||
|
filters: Map<String, List<String>>?
|
||||||
|
): IPager<IPlatformContent> = _client.getChannelContents(channelUrl);
|
||||||
|
|
||||||
|
override fun getPeekChannelTypes(): List<String> = _client.getPeekChannelTypes();
|
||||||
|
override fun peekChannelContents(channelUrl: String, type: String?): List<IPlatformContent> = _client.peekChannelContents(channelUrl, type);
|
||||||
|
|
||||||
|
override fun getChannelUrlByClaim(claimType: Int, claimValues: Map<Int, String>): String? = _client.getChannelUrlByClaim(claimType, claimValues)
|
||||||
|
|
||||||
|
override fun searchSuggestions(query: String): Array<String> = _client.searchSuggestions(query);
|
||||||
|
override fun getSearchCapabilities(): ResultCapabilities = _client.getSearchCapabilities();
|
||||||
|
override fun search(
|
||||||
|
query: String,
|
||||||
|
type: String?,
|
||||||
|
order: String?,
|
||||||
|
filters: Map<String, List<String>>?
|
||||||
|
): IPager<IPlatformContent> = _client.search(query, type, order, filters);
|
||||||
|
|
||||||
|
override fun getSearchChannelContentsCapabilities(): ResultCapabilities = _client.getSearchChannelContentsCapabilities();
|
||||||
|
override fun searchChannelContents(
|
||||||
|
channelUrl: String,
|
||||||
|
query: String,
|
||||||
|
type: String?,
|
||||||
|
order: String?,
|
||||||
|
filters: Map<String, List<String>>?
|
||||||
|
): IPager<IPlatformContent> = _client.searchChannelContents(channelUrl, query, type, order, filters);
|
||||||
|
|
||||||
|
override fun searchChannels(query: String) = _client.searchChannels(query);
|
||||||
|
|
||||||
|
override fun getComments(url: String): IPager<IPlatformComment> = _client.getComments(url);
|
||||||
|
override fun getSubComments(comment: IPlatformComment): IPager<IPlatformComment> = _client.getSubComments(comment);
|
||||||
|
|
||||||
|
override fun getLiveChatWindow(url: String): ILiveChatWindowDescriptor? = _client.getLiveChatWindow(url);
|
||||||
|
override fun getLiveEvents(url: String): IPager<IPlatformLiveEvent>? = _client.getLiveEvents(url);
|
||||||
|
|
||||||
|
override fun getHome(): IPager<IPlatformContent> = _client.getHome();
|
||||||
|
|
||||||
|
override fun getUserSubscriptions(): Array<String> { return arrayOf(); };
|
||||||
|
|
||||||
|
override fun searchPlaylists(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = _client.searchPlaylists(query, type, order, filters);
|
||||||
|
override fun isPlaylistUrl(url: String): Boolean = _client.isPlaylistUrl(url);
|
||||||
|
override fun getPlaylist(url: String): IPlatformPlaylistDetails = _client.getPlaylist(url);
|
||||||
|
override fun getUserPlaylists(): Array<String> { return arrayOf(); };
|
||||||
|
|
||||||
|
override fun isClaimTypeSupported(claimType: Int): Boolean {
|
||||||
|
return _client.isClaimTypeSupported(claimType);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
package com.futo.platformplayer.api.media
|
package com.futo.platformplayer.api.media
|
||||||
|
|
||||||
import com.futo.platformplayer.api.media.models.IPlatformChannelContent
|
|
||||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
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
|
||||||
|
@ -11,7 +10,6 @@ import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
|
||||||
import com.futo.platformplayer.api.media.models.live.ILiveChatWindowDescriptor
|
import com.futo.platformplayer.api.media.models.live.ILiveChatWindowDescriptor
|
||||||
import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
|
import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
|
||||||
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
||||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
|
||||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.models.ImageVariable
|
import com.futo.platformplayer.models.ImageVariable
|
||||||
|
@ -67,11 +65,6 @@ interface IPlatformClient {
|
||||||
*/
|
*/
|
||||||
fun searchChannels(query: String): IPager<PlatformAuthorLink>;
|
fun searchChannels(query: String): IPager<PlatformAuthorLink>;
|
||||||
|
|
||||||
/**
|
|
||||||
* Searches for channels and returns a content pager
|
|
||||||
*/
|
|
||||||
fun searchChannelsAsContent(query: String): IPager<IPlatformContent>;
|
|
||||||
|
|
||||||
|
|
||||||
//Video Pages
|
//Video Pages
|
||||||
/**
|
/**
|
||||||
|
@ -100,11 +93,6 @@ interface IPlatformClient {
|
||||||
*/
|
*/
|
||||||
fun peekChannelContents(channelUrl: String, type: String? = null): List<IPlatformContent>
|
fun peekChannelContents(channelUrl: String, type: String? = null): List<IPlatformContent>
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets all playlists of a channel
|
|
||||||
*/
|
|
||||||
fun getChannelPlaylists(channelUrl: String): IPager<IPlatformPlaylist>
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the channel url associated with a claimType
|
* Gets the channel url associated with a claimType
|
||||||
*/
|
*/
|
||||||
|
@ -127,11 +115,6 @@ interface IPlatformClient {
|
||||||
*/
|
*/
|
||||||
fun getPlaybackTracker(url: String): IPlaybackTracker?;
|
fun getPlaybackTracker(url: String): IPlaybackTracker?;
|
||||||
|
|
||||||
/**
|
|
||||||
* Get content recommendations
|
|
||||||
*/
|
|
||||||
fun getContentRecommendations(url: String): IPager<IPlatformContent>?;
|
|
||||||
|
|
||||||
|
|
||||||
//Comments
|
//Comments
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -18,9 +18,7 @@ data class PlatformClientCapabilities(
|
||||||
val hasGetLiveEvents: Boolean = false,
|
val hasGetLiveEvents: Boolean = false,
|
||||||
val hasGetLiveChatWindow: Boolean = false,
|
val hasGetLiveChatWindow: Boolean = false,
|
||||||
val hasGetContentChapters: Boolean = false,
|
val hasGetContentChapters: Boolean = false,
|
||||||
val hasPeekChannelContents: Boolean = false,
|
val hasPeekChannelContents: Boolean = false
|
||||||
val hasGetChannelPlaylists: Boolean = false,
|
|
||||||
val hasGetContentRecommendations: Boolean = false
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
}
|
}
|
|
@ -13,15 +13,13 @@ class PlatformClientPool {
|
||||||
private val _pool: HashMap<JSClient, Int> = hashMapOf();
|
private val _pool: HashMap<JSClient, Int> = hashMapOf();
|
||||||
private var _poolCounter = 0;
|
private var _poolCounter = 0;
|
||||||
private val _poolName: String?;
|
private val _poolName: String?;
|
||||||
private val _privatePool: Boolean;
|
|
||||||
|
|
||||||
var isDead: Boolean = false
|
var isDead: Boolean = false
|
||||||
private set;
|
private set;
|
||||||
val onDead = Event2<JSClient, PlatformClientPool>();
|
val onDead = Event2<JSClient, PlatformClientPool>();
|
||||||
|
|
||||||
constructor(parentClient: IPlatformClient, name: String? = null, privatePool: Boolean = false) {
|
constructor(parentClient: IPlatformClient, name: String? = null) {
|
||||||
_poolName = name;
|
_poolName = name;
|
||||||
_privatePool = privatePool;
|
|
||||||
if(parentClient !is JSClient)
|
if(parentClient !is JSClient)
|
||||||
throw IllegalArgumentException("Pooling only supported for JSClients right now");
|
throw IllegalArgumentException("Pooling only supported for JSClients right now");
|
||||||
Logger.i(TAG, "Pool for ${parentClient.name} was started");
|
Logger.i(TAG, "Pool for ${parentClient.name} was started");
|
||||||
|
@ -53,7 +51,7 @@ class PlatformClientPool {
|
||||||
reserved = _pool.keys.find { !it.isBusy };
|
reserved = _pool.keys.find { !it.isBusy };
|
||||||
if(reserved == null && _pool.size < capacity) {
|
if(reserved == null && _pool.size < capacity) {
|
||||||
Logger.i(TAG, "Started additional [${_parent.name}] client in pool [${_poolName}] (${_pool.size + 1}/${capacity})");
|
Logger.i(TAG, "Started additional [${_parent.name}] client in pool [${_poolName}] (${_pool.size + 1}/${capacity})");
|
||||||
reserved = _parent.getCopy(_privatePool);
|
reserved = _parent.getCopy();
|
||||||
|
|
||||||
reserved?.onCaptchaException?.subscribe { client, ex ->
|
reserved?.onCaptchaException?.subscribe { client, ex ->
|
||||||
StateApp.instance.handleCaptchaException(client, ex);
|
StateApp.instance.handleCaptchaException(client, ex);
|
||||||
|
|
|
@ -6,14 +6,12 @@ class PlatformMultiClientPool {
|
||||||
private val _clientPools: HashMap<IPlatformClient, PlatformClientPool> = hashMapOf();
|
private val _clientPools: HashMap<IPlatformClient, PlatformClientPool> = hashMapOf();
|
||||||
|
|
||||||
private var _isFake = false;
|
private var _isFake = false;
|
||||||
private var _privatePool = false;
|
|
||||||
|
|
||||||
constructor(name: String, maxCap: Int = -1, isPrivatePool: Boolean = false) {
|
constructor(name: String, maxCap: Int = -1) {
|
||||||
_name = name;
|
_name = name;
|
||||||
_maxCap = if(maxCap > 0)
|
_maxCap = if(maxCap > 0)
|
||||||
maxCap
|
maxCap
|
||||||
else 99;
|
else 99;
|
||||||
_privatePool = isPrivatePool;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getClientPooled(parentClient: IPlatformClient, capacity: Int = _maxCap): IPlatformClient {
|
fun getClientPooled(parentClient: IPlatformClient, capacity: Int = _maxCap): IPlatformClient {
|
||||||
|
@ -21,7 +19,7 @@ class PlatformMultiClientPool {
|
||||||
return parentClient;
|
return parentClient;
|
||||||
val pool = synchronized(_clientPools) {
|
val pool = synchronized(_clientPools) {
|
||||||
if(!_clientPools.containsKey(parentClient))
|
if(!_clientPools.containsKey(parentClient))
|
||||||
_clientPools[parentClient] = PlatformClientPool(parentClient, _name, _privatePool).apply {
|
_clientPools[parentClient] = PlatformClientPool(parentClient, _name).apply {
|
||||||
this.onDead.subscribe { _, pool ->
|
this.onDead.subscribe { _, pool ->
|
||||||
synchronized(_clientPools) {
|
synchronized(_clientPools) {
|
||||||
if(_clientPools[parentClient] == pool)
|
if(_clientPools[parentClient] == pool)
|
||||||
|
|
|
@ -4,6 +4,6 @@ import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
class Serializer {
|
class Serializer {
|
||||||
companion object {
|
companion object {
|
||||||
val json = Json { ignoreUnknownKeys = true; encodeDefaults = true; coerceInputValues = true };
|
val json = Json { ignoreUnknownKeys = true; encodeDefaults = true; };
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -2,10 +2,7 @@ package com.futo.platformplayer.api.media.models
|
||||||
|
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
import com.futo.platformplayer.api.media.PlatformID
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSContent
|
|
||||||
import com.futo.platformplayer.getOrDefault
|
import com.futo.platformplayer.getOrDefault
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
|
||||||
|
@ -30,8 +27,6 @@ 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);
|
||||||
|
@ -46,20 +41,3 @@ open class PlatformAuthorLink {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IPlatformChannelContent : IPlatformContent {
|
|
||||||
val thumbnail: String?
|
|
||||||
val subscribers: Long?
|
|
||||||
}
|
|
||||||
|
|
||||||
open class JSChannelContent : JSContent, IPlatformChannelContent {
|
|
||||||
override val contentType: ContentType get() = ContentType.CHANNEL
|
|
||||||
override val thumbnail: String?
|
|
||||||
override val subscribers: Long?
|
|
||||||
|
|
||||||
constructor(config: SourcePluginConfig, obj: V8ValueObject) : super(config, obj) {
|
|
||||||
val contextName = "Channel";
|
|
||||||
thumbnail = obj.getOrDefault<String>(config, "thumbnail", contextName, null)
|
|
||||||
subscribers = if(obj.has("subscribers")) obj.getOrThrow(config,"subscribers", contextName) else null
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -30,7 +30,6 @@ 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";
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@ enum class ChapterType(val value: Int) {
|
||||||
companion object {
|
companion object {
|
||||||
fun fromInt(value: Int): ChapterType
|
fun fromInt(value: Int): ChapterType
|
||||||
{
|
{
|
||||||
val result = ChapterType.entries.firstOrNull { it.value == value };
|
val result = ChapterType.values().firstOrNull { it.value == value };
|
||||||
if(result == null)
|
if(result == null)
|
||||||
throw UnknownPlatformException(value.toString());
|
throw UnknownPlatformException(value.toString());
|
||||||
return result;
|
return result;
|
||||||
|
|
|
@ -1,63 +0,0 @@
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -12,7 +12,6 @@ enum class ContentType(val value: Int) {
|
||||||
URL(9),
|
URL(9),
|
||||||
|
|
||||||
NESTED_VIDEO(11),
|
NESTED_VIDEO(11),
|
||||||
CHANNEL(60),
|
|
||||||
|
|
||||||
LOCKED(70),
|
LOCKED(70),
|
||||||
|
|
||||||
|
@ -22,7 +21,7 @@ enum class ContentType(val value: Int) {
|
||||||
companion object {
|
companion object {
|
||||||
fun fromInt(value: Int): ContentType
|
fun fromInt(value: Int): ContentType
|
||||||
{
|
{
|
||||||
val result = ContentType.entries.firstOrNull { it.value == value };
|
val result = ContentType.values().firstOrNull { it.value == value };
|
||||||
if(result == null)
|
if(result == null)
|
||||||
throw UnknownPlatformException(value.toString());
|
throw UnknownPlatformException(value.toString());
|
||||||
return result;
|
return result;
|
||||||
|
|
|
@ -2,8 +2,6 @@ package com.futo.platformplayer.api.media.models.contents
|
||||||
|
|
||||||
import com.futo.platformplayer.api.media.PlatformID
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.json.JsonNames
|
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
interface IPlatformContent {
|
interface IPlatformContent {
|
||||||
|
|
|
@ -10,6 +10,4 @@ interface IPlatformContentDetails : IPlatformContent {
|
||||||
|
|
||||||
fun getComments(client: IPlatformClient): IPager<IPlatformComment>?;
|
fun getComments(client: IPlatformClient): IPager<IPlatformComment>?;
|
||||||
fun getPlaybackTracker(): IPlaybackTracker?;
|
fun getPlaybackTracker(): IPlaybackTracker?;
|
||||||
|
|
||||||
fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>?;
|
|
||||||
}
|
}
|
|
@ -3,5 +3,4 @@ package com.futo.platformplayer.api.media.models.live
|
||||||
interface ILiveChatWindowDescriptor {
|
interface ILiveChatWindowDescriptor {
|
||||||
val url: String;
|
val url: String;
|
||||||
val removeElements: List<String>;
|
val removeElements: List<String>;
|
||||||
val removeElementsInterval: List<String>;
|
|
||||||
}
|
}
|
|
@ -10,7 +10,7 @@ enum class LiveEventType(val value : Int) {
|
||||||
|
|
||||||
companion object{
|
companion object{
|
||||||
fun fromInt(value : Int) : LiveEventType{
|
fun fromInt(value : Int) : LiveEventType{
|
||||||
return LiveEventType.entries.first { it.value == value };
|
return LiveEventType.values().first { it.value == value };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -7,5 +7,4 @@ interface IPlaybackTracker {
|
||||||
|
|
||||||
fun onInit(seconds: Double);
|
fun onInit(seconds: Double);
|
||||||
fun onProgress(seconds: Double, isPlaying: Boolean);
|
fun onProgress(seconds: Double, isPlaying: Boolean);
|
||||||
fun onConcluded();
|
|
||||||
}
|
}
|
|
@ -8,5 +8,5 @@ interface IPlatformPlaylistDetails: IPlatformPlaylist {
|
||||||
//TODO: Determine if this should be IPlatformContent (probably not?)
|
//TODO: Determine if this should be IPlatformContent (probably not?)
|
||||||
val contents: IPager<IPlatformVideo>;
|
val contents: IPager<IPlatformVideo>;
|
||||||
|
|
||||||
fun toPlaylist(onProgress: ((progress: Int) -> Unit)? = null): Playlist;
|
fun toPlaylist(): Playlist;
|
||||||
}
|
}
|
|
@ -10,7 +10,7 @@ enum class TextType(val value: Int) {
|
||||||
companion object {
|
companion object {
|
||||||
fun fromInt(value: Int): TextType
|
fun fromInt(value: Int): TextType
|
||||||
{
|
{
|
||||||
val result = TextType.entries.firstOrNull { it.value == value };
|
val result = TextType.values().firstOrNull { it.value == value };
|
||||||
if(result == null)
|
if(result == null)
|
||||||
throw IllegalArgumentException("Unknown Texttype: $value");
|
throw IllegalArgumentException("Unknown Texttype: $value");
|
||||||
return result;
|
return result;
|
||||||
|
|
|
@ -8,7 +8,7 @@ enum class RatingType(val value : Int) {
|
||||||
|
|
||||||
companion object{
|
companion object{
|
||||||
fun fromInt(value : Int) : RatingType{
|
fun fromInt(value : Int) : RatingType{
|
||||||
return RatingType.entries.first { it.value == value };
|
return RatingType.values().first { it.value == value };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -3,7 +3,7 @@ package com.futo.platformplayer.api.media.models.streams
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||||
import com.futo.platformplayer.downloads.VideoLocal
|
import com.futo.platformplayer.downloads.VideoLocal
|
||||||
|
|
||||||
class DownloadedVideoMuxedSourceDescriptor(
|
class LocalVideoMuxedSourceDescriptor(
|
||||||
private val video: VideoLocal
|
private val video: VideoLocal
|
||||||
) : VideoMuxedSourceDescriptor() {
|
) : VideoMuxedSourceDescriptor() {
|
||||||
override val videoSources: Array<IVideoSource> get() = video.videoSource.toTypedArray();
|
override val videoSources: Array<IVideoSource> get() = video.videoSource.toTypedArray();
|
|
@ -13,8 +13,7 @@ class AudioUrlSource(
|
||||||
override val codec: String = "",
|
override val codec: String = "",
|
||||||
override val language: String = Language.UNKNOWN,
|
override val language: String = Language.UNKNOWN,
|
||||||
override val duration: Long? = null,
|
override val duration: Long? = null,
|
||||||
override var priority: Boolean = false,
|
override var priority: Boolean = false
|
||||||
override var original: Boolean = false
|
|
||||||
) : IAudioUrlSource, IStreamMetaDataSource{
|
) : IAudioUrlSource, IStreamMetaDataSource{
|
||||||
override var streamMetaData: StreamMetaData? = null;
|
override var streamMetaData: StreamMetaData? = null;
|
||||||
|
|
||||||
|
@ -37,9 +36,7 @@ class AudioUrlSource(
|
||||||
source.container,
|
source.container,
|
||||||
source.codec,
|
source.codec,
|
||||||
source.language,
|
source.language,
|
||||||
source.duration,
|
source.duration
|
||||||
source.priority,
|
|
||||||
source.original
|
|
||||||
);
|
);
|
||||||
ret.streamMetaData = streamData;
|
ret.streamMetaData = streamData;
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,6 @@ class HLSVariantAudioUrlSource(
|
||||||
override val language: String,
|
override val language: String,
|
||||||
override val duration: Long?,
|
override val duration: Long?,
|
||||||
override val priority: Boolean,
|
override val priority: Boolean,
|
||||||
override val original: Boolean,
|
|
||||||
val url: String
|
val url: String
|
||||||
) : IAudioUrlSource {
|
) : IAudioUrlSource {
|
||||||
override fun getAudioUrl(): String {
|
override fun getAudioUrl(): String {
|
||||||
|
|
|
@ -8,5 +8,4 @@ interface IAudioSource {
|
||||||
val language : String;
|
val language : String;
|
||||||
val duration : Long?;
|
val duration : Long?;
|
||||||
val priority: Boolean;
|
val priority: Boolean;
|
||||||
val original: Boolean;
|
|
||||||
}
|
}
|
|
@ -1,3 +0,0 @@
|
||||||
package com.futo.platformplayer.api.media.models.streams.sources
|
|
||||||
|
|
||||||
interface IAudioUrlWidevineSource : IAudioUrlSource, IWidevineSource
|
|
|
@ -1,5 +0,0 @@
|
||||||
package com.futo.platformplayer.api.media.models.streams.sources
|
|
||||||
|
|
||||||
interface IDashManifestWidevineSource : IWidevineSource {
|
|
||||||
val url: String
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
package com.futo.platformplayer.api.media.models.streams.sources
|
|
||||||
|
|
||||||
interface IVideoUrlWidevineSource : IVideoUrlSource, IWidevineSource
|
|
|
@ -1,9 +0,0 @@
|
||||||
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?
|
|
||||||
}
|
|
|
@ -15,7 +15,6 @@ class LocalAudioSource : IAudioSource, IStreamMetaDataSource {
|
||||||
override val duration: Long? = null;
|
override val duration: Long? = null;
|
||||||
|
|
||||||
override var priority: Boolean = false;
|
override var priority: Boolean = false;
|
||||||
override val original: Boolean = false;
|
|
||||||
|
|
||||||
val filePath : String;
|
val filePath : String;
|
||||||
val fileSize: Long;
|
val fileSize: Long;
|
||||||
|
@ -34,13 +33,13 @@ class LocalAudioSource : IAudioSource, IStreamMetaDataSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromSource(source: IAudioSource, path: String, fileSize: Long, overrideContainer: String? = null): LocalAudioSource {
|
fun fromSource(source: IAudioSource, path: String, fileSize: Long): LocalAudioSource {
|
||||||
return LocalAudioSource(
|
return LocalAudioSource(
|
||||||
source.name,
|
source.name,
|
||||||
path,
|
path,
|
||||||
fileSize,
|
fileSize,
|
||||||
source.bitrate,
|
source.bitrate,
|
||||||
overrideContainer ?: source.container,
|
source.container,
|
||||||
source.codec,
|
source.codec,
|
||||||
source.language
|
source.language
|
||||||
);
|
);
|
||||||
|
|
|
@ -35,7 +35,7 @@ class LocalVideoSource : IVideoSource, IStreamMetaDataSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromSource(source: IVideoSource, path: String, fileSize: Long, overrideContainer: String? = null): LocalVideoSource {
|
fun fromSource(source: IVideoSource, path: String, fileSize: Long): 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,
|
||||||
overrideContainer ?: source.container,
|
source.container,
|
||||||
source.codec,
|
source.codec,
|
||||||
source.bitrate?:0
|
source.bitrate?:0
|
||||||
);
|
);
|
||||||
|
|
|
@ -13,6 +13,4 @@ interface IPlatformVideo : IPlatformContent {
|
||||||
val viewCount: Long;
|
val viewCount: Long;
|
||||||
|
|
||||||
val isLive : Boolean;
|
val isLive : Boolean;
|
||||||
|
|
||||||
val isShort: Boolean;
|
|
||||||
}
|
}
|
|
@ -10,26 +10,23 @@ import com.futo.polycentric.core.combineHashCodes
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonNames
|
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
open class SerializedPlatformVideo(
|
open class SerializedPlatformVideo(
|
||||||
override val contentType: ContentType = ContentType.MEDIA,
|
|
||||||
override val id: PlatformID,
|
override val id: PlatformID,
|
||||||
override val name: String,
|
override val name: String,
|
||||||
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)
|
||||||
@JsonNames("datetime", "dateTime")
|
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 isLive: Boolean = false;
|
override val isLive: Boolean = false;
|
||||||
|
|
||||||
|
@ -46,7 +43,6 @@ open class SerializedPlatformVideo(
|
||||||
companion object {
|
companion object {
|
||||||
fun fromVideo(video: IPlatformVideo) : SerializedPlatformVideo {
|
fun fromVideo(video: IPlatformVideo) : SerializedPlatformVideo {
|
||||||
return SerializedPlatformVideo(
|
return SerializedPlatformVideo(
|
||||||
ContentType.MEDIA,
|
|
||||||
video.id,
|
video.id,
|
||||||
video.name,
|
video.name,
|
||||||
video.thumbnails,
|
video.thumbnails,
|
||||||
|
|
|
@ -7,7 +7,6 @@ import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
import com.futo.platformplayer.api.media.models.Thumbnails
|
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
|
||||||
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
||||||
import com.futo.platformplayer.api.media.models.ratings.IRating
|
import com.futo.platformplayer.api.media.models.ratings.IRating
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.*
|
import com.futo.platformplayer.api.media.models.streams.sources.*
|
||||||
|
@ -38,8 +37,7 @@ 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;
|
||||||
|
|
||||||
|
@ -58,7 +56,6 @@ open class SerializedPlatformVideoDetails(
|
||||||
|
|
||||||
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? = null;
|
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? = null;
|
||||||
override fun getPlaybackTracker(): IPlaybackTracker? = null;
|
override fun getPlaybackTracker(): IPlaybackTracker? = null;
|
||||||
override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? = null;
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromVideo(video : IPlatformVideoDetails, subtitleSources: List<SubtitleRawSource>) : SerializedPlatformVideoDetails {
|
fun fromVideo(video : IPlatformVideoDetails, subtitleSources: List<SubtitleRawSource>) : SerializedPlatformVideoDetails {
|
||||||
|
|
|
@ -8,8 +8,6 @@ import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StateDeveloper
|
import com.futo.platformplayer.states.StateDeveloper
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
class DevJSClient : JSClient {
|
class DevJSClient : JSClient {
|
||||||
|
@ -54,8 +52,8 @@ class DevJSClient : JSClient {
|
||||||
return DevJSClient(context, config, _devScript, _auth, _captcha, devID, descriptor.settings);
|
return DevJSClient(context, config, _devScript, _auth, _captcha, devID, descriptor.settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getCopy(privateCopy: Boolean): JSClient {
|
override fun getCopy(): JSClient {
|
||||||
return DevJSClient(_context, descriptor, _script, if(!privateCopy) _auth else null, _captcha, saveState(), devID);
|
return DevJSClient(_context, descriptor, _script, _auth, _captcha, saveState(), devID);
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun initialize() {
|
override fun initialize() {
|
||||||
|
@ -117,7 +115,7 @@ class DevJSClient : JSClient {
|
||||||
|
|
||||||
//Video
|
//Video
|
||||||
override fun isContentDetailsUrl(url: String): Boolean {
|
override fun isContentDetailsUrl(url: String): Boolean {
|
||||||
return StateDeveloper.instance.handleDevCall(devID, "isVideoDetailsUrl(${Json.encodeToString(url)})"){
|
return StateDeveloper.instance.handleDevCall(devID, "isVideoDetailsUrl"){
|
||||||
super.isContentDetailsUrl(url);
|
super.isContentDetailsUrl(url);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,6 @@ import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.api.media.IPlatformClient
|
import com.futo.platformplayer.api.media.IPlatformClient
|
||||||
import com.futo.platformplayer.api.media.PlatformClientCapabilities
|
import com.futo.platformplayer.api.media.PlatformClientCapabilities
|
||||||
import com.futo.platformplayer.api.media.models.IPlatformChannelContent
|
|
||||||
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.ResultCapabilities
|
||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
|
@ -21,7 +20,6 @@ import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
|
||||||
import com.futo.platformplayer.api.media.models.live.ILiveChatWindowDescriptor
|
import com.futo.platformplayer.api.media.models.live.ILiveChatWindowDescriptor
|
||||||
import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
|
import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
|
||||||
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
||||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
|
||||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
||||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSCallDocs
|
import com.futo.platformplayer.api.media.platforms.js.internal.JSCallDocs
|
||||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSDocs
|
import com.futo.platformplayer.api.media.platforms.js.internal.JSDocs
|
||||||
|
@ -32,7 +30,6 @@ import com.futo.platformplayer.api.media.platforms.js.internal.JSParameterDocs
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.IJSContent
|
import com.futo.platformplayer.api.media.platforms.js.models.IJSContent
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.IJSContentDetails
|
import com.futo.platformplayer.api.media.platforms.js.models.IJSContentDetails
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSChannel
|
import com.futo.platformplayer.api.media.platforms.js.models.JSChannel
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSChannelContentPager
|
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSChannelPager
|
import com.futo.platformplayer.api.media.platforms.js.models.JSChannelPager
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSChapter
|
import com.futo.platformplayer.api.media.platforms.js.models.JSChapter
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSComment
|
import com.futo.platformplayer.api.media.platforms.js.models.JSComment
|
||||||
|
@ -42,7 +39,6 @@ import com.futo.platformplayer.api.media.platforms.js.models.JSLiveChatWindowDes
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSLiveEventPager
|
import com.futo.platformplayer.api.media.platforms.js.models.JSLiveEventPager
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSPlaybackTracker
|
import com.futo.platformplayer.api.media.platforms.js.models.JSPlaybackTracker
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSPlaylistDetails
|
import com.futo.platformplayer.api.media.platforms.js.models.JSPlaylistDetails
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSPlaylistPager
|
|
||||||
import com.futo.platformplayer.api.media.structures.EmptyPager
|
import com.futo.platformplayer.api.media.structures.EmptyPager
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
@ -50,7 +46,6 @@ import com.futo.platformplayer.constructs.Event2
|
||||||
import com.futo.platformplayer.engine.V8Plugin
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
import com.futo.platformplayer.engine.exceptions.PluginEngineException
|
import com.futo.platformplayer.engine.exceptions.PluginEngineException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptException
|
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptValidationException
|
import com.futo.platformplayer.engine.exceptions.ScriptValidationException
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
@ -62,7 +57,6 @@ import com.futo.platformplayer.states.StatePlugins
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import kotlin.Exception
|
|
||||||
import kotlin.reflect.full.findAnnotations
|
import kotlin.reflect.full.findAnnotations
|
||||||
import kotlin.reflect.jvm.kotlinFunction
|
import kotlin.reflect.jvm.kotlinFunction
|
||||||
import kotlin.streams.asSequence
|
import kotlin.streams.asSequence
|
||||||
|
@ -166,16 +160,13 @@ open class JSClient : IPlatformClient {
|
||||||
|
|
||||||
_plugin.changeAllowDevSubmit(descriptor.appSettings.allowDeveloperSubmit);
|
_plugin.changeAllowDevSubmit(descriptor.appSettings.allowDeveloperSubmit);
|
||||||
}
|
}
|
||||||
constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String?, script: String, withoutCredentials: Boolean = false) {
|
constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String?, script: String) {
|
||||||
this._context = context;
|
this._context = context;
|
||||||
this.config = descriptor.config;
|
this.config = descriptor.config;
|
||||||
icon = StatePlatform.instance.getPlatformIcon(config.id) ?: ImageVariable(config.absoluteIconUrl, null, null);
|
icon = StatePlatform.instance.getPlatformIcon(config.id) ?: ImageVariable(config.absoluteIconUrl, null, null);
|
||||||
this.descriptor = descriptor;
|
this.descriptor = descriptor;
|
||||||
_injectedSaveState = saveState;
|
_injectedSaveState = saveState;
|
||||||
if(!withoutCredentials)
|
_auth = descriptor.getAuth();
|
||||||
_auth = descriptor.getAuth();
|
|
||||||
else
|
|
||||||
_auth = null;
|
|
||||||
_captcha = descriptor.getCaptchaData();
|
_captcha = descriptor.getCaptchaData();
|
||||||
flags = descriptor.flags.toTypedArray();
|
flags = descriptor.flags.toTypedArray();
|
||||||
|
|
||||||
|
@ -195,8 +186,8 @@ open class JSClient : IPlatformClient {
|
||||||
_plugin.changeAllowDevSubmit(descriptor.appSettings.allowDeveloperSubmit);
|
_plugin.changeAllowDevSubmit(descriptor.appSettings.allowDeveloperSubmit);
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun getCopy(withoutCredentials: Boolean = false): JSClient {
|
open fun getCopy(): JSClient {
|
||||||
return JSClient(_context, descriptor, saveState(), _script, withoutCredentials);
|
return JSClient(_context, descriptor, saveState(), _script);
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getUnderlyingPlugin(): V8Plugin {
|
fun getUnderlyingPlugin(): V8Plugin {
|
||||||
|
@ -238,9 +229,7 @@ open class JSClient : IPlatformClient {
|
||||||
hasGetLiveEvents = plugin.executeBoolean("!!source.getLiveEvents") ?: false,
|
hasGetLiveEvents = plugin.executeBoolean("!!source.getLiveEvents") ?: false,
|
||||||
hasGetLiveChatWindow = plugin.executeBoolean("!!source.getLiveChatWindow") ?: false,
|
hasGetLiveChatWindow = plugin.executeBoolean("!!source.getLiveChatWindow") ?: false,
|
||||||
hasGetContentChapters = plugin.executeBoolean("!!source.getContentChapters") ?: false,
|
hasGetContentChapters = plugin.executeBoolean("!!source.getContentChapters") ?: false,
|
||||||
hasPeekChannelContents = plugin.executeBoolean("!!source.peekChannelContents") ?: false,
|
hasPeekChannelContents = plugin.executeBoolean("!!source.peekChannelContents") ?: false
|
||||||
hasGetChannelPlaylists = plugin.executeBoolean("!!source.getChannelPlaylists") ?: false,
|
|
||||||
hasGetContentRecommendations = plugin.executeBoolean("!!source.getContentRecommendations") ?: false
|
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -363,10 +352,6 @@ open class JSClient : IPlatformClient {
|
||||||
return@isBusyWith JSChannelPager(config, this,
|
return@isBusyWith JSChannelPager(config, this,
|
||||||
plugin.executeTyped("source.searchChannels(${Json.encodeToString(query)})"));
|
plugin.executeTyped("source.searchChannels(${Json.encodeToString(query)})"));
|
||||||
}
|
}
|
||||||
override fun searchChannelsAsContent(query: String): IPager<IPlatformContent> = isBusyWith("searchChannels") {
|
|
||||||
ensureEnabled();
|
|
||||||
return@isBusyWith JSChannelContentPager(config, this, plugin.executeTyped("source.searchChannels(${Json.encodeToString(query)})"), );
|
|
||||||
}
|
|
||||||
|
|
||||||
@JSDocs(6, "source.isChannelUrl(url)", "Validates if an channel url is for this platform")
|
@JSDocs(6, "source.isChannelUrl(url)", "Validates if an channel url is for this platform")
|
||||||
@JSDocsParameter("url", "A channel url (May not be your platform)")
|
@JSDocsParameter("url", "A channel url (May not be your platform)")
|
||||||
|
@ -415,16 +400,6 @@ open class JSClient : IPlatformClient {
|
||||||
plugin.executeTyped("source.getChannelContents(${Json.encodeToString(channelUrl)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})"));
|
plugin.executeTyped("source.getChannelContents(${Json.encodeToString(channelUrl)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JSDocs(10, "source.getChannelPlaylists(url)", "Gets playlists of a channel")
|
|
||||||
@JSDocsParameter("channelUrl", "A channel url (this platform)")
|
|
||||||
override fun getChannelPlaylists(channelUrl: String): IPager<IPlatformPlaylist> = isBusyWith("getChannelPlaylists") {
|
|
||||||
ensureEnabled();
|
|
||||||
if(!capabilities.hasGetChannelPlaylists)
|
|
||||||
return@isBusyWith EmptyPager();
|
|
||||||
return@isBusyWith JSPlaylistPager(config, this,
|
|
||||||
plugin.executeTyped("source.getChannelPlaylists(${Json.encodeToString(channelUrl)})"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@JSDocs(10, "source.getPeekChannelTypes()", "Gets types this plugin has for peek channel contents")
|
@JSDocs(10, "source.getPeekChannelTypes()", "Gets types this plugin has for peek channel contents")
|
||||||
override fun getPeekChannelTypes(): List<String> {
|
override fun getPeekChannelTypes(): List<String> {
|
||||||
if(!capabilities.hasPeekChannelContents)
|
if(!capabilities.hasPeekChannelContents)
|
||||||
|
@ -446,7 +421,6 @@ open class JSClient : IPlatformClient {
|
||||||
return listOf();
|
return listOf();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@JSDocs(10, "source.peekChannelContents(url, type)", "Peek contents of a channel (reverse chronological order)")
|
@JSDocs(10, "source.peekChannelContents(url, type)", "Peek contents of a channel (reverse chronological order)")
|
||||||
@JSDocsParameter("channelUrl", "A channel url (this platform)")
|
@JSDocsParameter("channelUrl", "A channel url (this platform)")
|
||||||
@JSDocsParameter("type", "(optional) Type of contents to get from channel")
|
@JSDocsParameter("type", "(optional) Type of contents to get from channel")
|
||||||
|
@ -570,7 +544,7 @@ open class JSClient : IPlatformClient {
|
||||||
plugin.executeTyped("source.getSubComments(${Json.encodeToString(comment as JSComment)})"));
|
plugin.executeTyped("source.getSubComments(${Json.encodeToString(comment as JSComment)})"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JSDocs(18, "source.getLiveChatWindow(url)", "Gets live events for a livestream")
|
@JSDocs(16, "source.getLiveChatWindow(url)", "Gets live events for a livestream")
|
||||||
@JSDocsParameter("url", "Url of live stream")
|
@JSDocsParameter("url", "Url of live stream")
|
||||||
override fun getLiveChatWindow(url: String): ILiveChatWindowDescriptor? = isBusyWith("getLiveChatWindow") {
|
override fun getLiveChatWindow(url: String): ILiveChatWindowDescriptor? = isBusyWith("getLiveChatWindow") {
|
||||||
if(!capabilities.hasGetLiveChatWindow)
|
if(!capabilities.hasGetLiveChatWindow)
|
||||||
|
@ -579,7 +553,7 @@ open class JSClient : IPlatformClient {
|
||||||
return@isBusyWith JSLiveChatWindowDescriptor(config,
|
return@isBusyWith JSLiveChatWindowDescriptor(config,
|
||||||
plugin.executeTyped("source.getLiveChatWindow(${Json.encodeToString(url)})"));
|
plugin.executeTyped("source.getLiveChatWindow(${Json.encodeToString(url)})"));
|
||||||
}
|
}
|
||||||
@JSDocs(19, "source.getLiveEvents(url)", "Gets live events for a livestream")
|
@JSDocs(16, "source.getLiveEvents(url)", "Gets live events for a livestream")
|
||||||
@JSDocsParameter("url", "Url of live stream")
|
@JSDocsParameter("url", "Url of live stream")
|
||||||
override fun getLiveEvents(url: String): IPager<IPlatformLiveEvent>? = isBusyWith("getLiveEvents") {
|
override fun getLiveEvents(url: String): IPager<IPlatformLiveEvent>? = isBusyWith("getLiveEvents") {
|
||||||
if(!capabilities.hasGetLiveEvents)
|
if(!capabilities.hasGetLiveEvents)
|
||||||
|
@ -588,20 +562,6 @@ open class JSClient : IPlatformClient {
|
||||||
return@isBusyWith JSLiveEventPager(config, this,
|
return@isBusyWith JSLiveEventPager(config, this,
|
||||||
plugin.executeTyped("source.getLiveEvents(${Json.encodeToString(url)})"));
|
plugin.executeTyped("source.getLiveEvents(${Json.encodeToString(url)})"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@JSDocs(19, "source.getContentRecommendations(url)", "Gets recommendations of a content page")
|
|
||||||
@JSDocsParameter("url", "Url of content")
|
|
||||||
override fun getContentRecommendations(url: String): IPager<IPlatformContent>? = isBusyWith("getContentRecommendations") {
|
|
||||||
if(!capabilities.hasGetContentRecommendations)
|
|
||||||
return@isBusyWith null;
|
|
||||||
ensureEnabled();
|
|
||||||
return@isBusyWith JSContentPager(config, this,
|
|
||||||
plugin.executeTyped("source.getContentRecommendations(${Json.encodeToString(url)})"));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@JSDocs(19, "source.searchPlaylists(query)", "Searches for playlists on the platform")
|
@JSDocs(19, "source.searchPlaylists(query)", "Searches for playlists on the platform")
|
||||||
@JSDocsParameter("query", "Query that search results should match")
|
@JSDocsParameter("query", "Query that search results should match")
|
||||||
@JSDocsParameter("type", "(optional) Type of contents to get from search ")
|
@JSDocsParameter("type", "(optional) Type of contents to get from search ")
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
package com.futo.platformplayer.api.media.platforms.js
|
|
||||||
|
|
||||||
class JSClientConstants {
|
|
||||||
companion object {
|
|
||||||
val PLUGIN_SPEC_VERSION = 2;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,9 +4,7 @@ import android.net.Uri
|
||||||
import com.futo.platformplayer.SignatureProvider
|
import com.futo.platformplayer.SignatureProvider
|
||||||
import com.futo.platformplayer.api.media.Serializer
|
import com.futo.platformplayer.api.media.Serializer
|
||||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
import com.futo.platformplayer.matchesDomain
|
|
||||||
import com.futo.platformplayer.states.StatePlugins
|
import com.futo.platformplayer.states.StatePlugins
|
||||||
import kotlinx.serialization.Contextual
|
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
|
@ -33,7 +31,6 @@ 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(),
|
||||||
|
|
||||||
|
@ -49,11 +46,7 @@ class SourcePluginConfig(
|
||||||
var enableInHome: Boolean = true,
|
var enableInHome: Boolean = true,
|
||||||
var supportedClaimTypes: List<Int> = listOf(),
|
var supportedClaimTypes: List<Int> = listOf(),
|
||||||
var primaryClaimFieldType: Int? = null,
|
var primaryClaimFieldType: Int? = null,
|
||||||
var developerSubmitUrl: String? = null,
|
var developerSubmitUrl: String? = null
|
||||||
var allowAllHttpHeaderAccess: Boolean = false,
|
|
||||||
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);
|
||||||
|
@ -83,59 +76,16 @@ class SourcePluginConfig(
|
||||||
private var _allowUrlsLowerVal: List<String>? = null;
|
private var _allowUrlsLowerVal: List<String>? = null;
|
||||||
private val _allowUrlsLower: List<String> get() {
|
private val _allowUrlsLower: List<String> get() {
|
||||||
if(_allowUrlsLowerVal == null)
|
if(_allowUrlsLowerVal == null)
|
||||||
_allowUrlsLowerVal = allowUrls.map { it.lowercase() }
|
_allowUrlsLowerVal = allowUrls.map { it.lowercase() };
|
||||||
.filter { it.length > 0 };
|
|
||||||
return _allowUrlsLowerVal!!;
|
return _allowUrlsLowerVal!!;
|
||||||
};
|
};
|
||||||
|
|
||||||
fun isLowRiskUpdate(oldScript: String, newConfig: SourcePluginConfig, newScript: String): Boolean{
|
|
||||||
//New allow header access
|
|
||||||
if(!allowAllHttpHeaderAccess && newConfig.allowAllHttpHeaderAccess)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
//All urls should already be allowed
|
|
||||||
for(url in newConfig.allowUrls) {
|
|
||||||
if(!allowUrls.contains(url))
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
//All packages should already be allowed
|
|
||||||
for(pack in newConfig.packages) {
|
|
||||||
if(!packages.contains(pack))
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
for(pack in newConfig.packagesOptional) {
|
|
||||||
if(!packagesOptional.contains(pack))
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
//Developer Submit Url should be same or empty
|
|
||||||
if(!newConfig.developerSubmitUrl.isNullOrEmpty() && developerSubmitUrl != newConfig.developerSubmitUrl)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
//Should have a public key
|
|
||||||
if(scriptPublicKey.isNullOrEmpty() || scriptSignature.isNullOrEmpty())
|
|
||||||
return false;
|
|
||||||
|
|
||||||
//Should be same public key
|
|
||||||
if(scriptPublicKey != newConfig.scriptPublicKey)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
//Old signature should be valid
|
|
||||||
if(!validate(oldScript))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
//New signature should be valid
|
|
||||||
if(!newConfig.validate(newScript))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getWarnings(scriptToCheck: String? = null) : List<Pair<String,String>> {
|
fun getWarnings(scriptToCheck: String? = null) : List<Pair<String,String>> {
|
||||||
val list = mutableListOf<Pair<String,String>>();
|
val list = mutableListOf<Pair<String,String>>();
|
||||||
|
|
||||||
val currentlyInstalledPlugin = StatePlugins.instance.getPlugin(id);
|
val currentlyInstalledPlugin = StatePlugins.instance.getPlugin(id);
|
||||||
if (currentlyInstalledPlugin != null) {
|
if (currentlyInstalledPlugin != null) {
|
||||||
if (currentlyInstalledPlugin.config.scriptPublicKey != scriptPublicKey && !currentlyInstalledPlugin.config.scriptPublicKey.isNullOrEmpty()) {
|
if (currentlyInstalledPlugin.config.scriptPublicKey != scriptPublicKey) {
|
||||||
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."));
|
||||||
|
@ -158,11 +108,6 @@ class SourcePluginConfig(
|
||||||
list.add(Pair(
|
list.add(Pair(
|
||||||
"Unrestricted Web Access",
|
"Unrestricted Web Access",
|
||||||
"This plugin requires access to all URLs, this may include malicious URLs."));
|
"This plugin requires access to all URLs, this may include malicious URLs."));
|
||||||
if(allowAllHttpHeaderAccess)
|
|
||||||
list.add(Pair(
|
|
||||||
"Unrestricted Http Header access",
|
|
||||||
"Allows this plugin to access all headers (including cookies and authorization headers) for unauthenticated requests."
|
|
||||||
))
|
|
||||||
|
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
@ -181,20 +126,7 @@ class SourcePluginConfig(
|
||||||
return true;
|
return true;
|
||||||
val uri = Uri.parse(url);
|
val uri = Uri.parse(url);
|
||||||
val host = uri.host?.lowercase() ?: "";
|
val host = uri.host?.lowercase() ?: "";
|
||||||
return _allowUrlsLower.any { it == host || (it.length > 0 && it[0] == '.' && host.matchesDomain(it)) };
|
return _allowUrlsLower.any { it == host };
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
||||||
|
|
|
@ -91,10 +91,8 @@ class SourcePluginDescriptor {
|
||||||
@Serializable
|
@Serializable
|
||||||
class AppPluginSettings {
|
class AppPluginSettings {
|
||||||
|
|
||||||
@FormField(R.string.check_for_updates_setting, FieldForm.TOGGLE, R.string.check_for_updates_setting_description, -1)
|
@FormField(R.string.check_for_updates_setting, FieldForm.TOGGLE, R.string.check_for_updates_setting_description, 0)
|
||||||
var checkForUpdates: Boolean = true;
|
var checkForUpdates: Boolean = true;
|
||||||
@FormField(R.string.automatic_update_setting, FieldForm.TOGGLE, R.string.automatic_update_setting_description, 0)
|
|
||||||
var automaticUpdate: Boolean = false;
|
|
||||||
|
|
||||||
@FormField(R.string.visibility, "group", R.string.enable_where_this_plugins_content_are_visible, 2)
|
@FormField(R.string.visibility, "group", R.string.enable_where_this_plugins_content_are_visible, 2)
|
||||||
var tabEnabled = TabEnabled();
|
var tabEnabled = TabEnabled();
|
||||||
|
|
|
@ -2,22 +2,14 @@ package com.futo.platformplayer.api.media.platforms.js.internal
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
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.api.media.platforms.js.SourceAuth
|
import com.futo.platformplayer.api.media.platforms.js.SourceAuth
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourceCaptchaData
|
import com.futo.platformplayer.api.media.platforms.js.SourceCaptchaData
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSRequest
|
import com.futo.platformplayer.api.media.platforms.js.models.JSRequest
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestModifier
|
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestModifier
|
||||||
import com.futo.platformplayer.developer.DeveloperEndpoints
|
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
||||||
import com.futo.platformplayer.matchesDomain
|
import com.futo.platformplayer.matchesDomain
|
||||||
import com.futo.platformplayer.states.StateDeveloper
|
|
||||||
import com.google.common.net.MediaType
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okio.GzipSource
|
|
||||||
import java.net.InetSocketAddress
|
|
||||||
import java.net.Proxy
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
class JSHttpClient : ManagedHttpClient {
|
class JSHttpClient : ManagedHttpClient {
|
||||||
|
@ -36,15 +28,7 @@ class JSHttpClient : ManagedHttpClient {
|
||||||
private var _currentCookieMap: HashMap<String, HashMap<String, String>>;
|
private var _currentCookieMap: HashMap<String, HashMap<String, String>>;
|
||||||
private var _otherCookieMap: HashMap<String, HashMap<String, String>>;
|
private var _otherCookieMap: HashMap<String, HashMap<String, String>>;
|
||||||
|
|
||||||
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
|
|
||||||
(if((jsClient?.config?.id == StateDeveloper.DEV_ID || jsClient == null) && StateDeveloper.instance.devProxy != null)
|
|
||||||
OkHttpClient.Builder().proxy(Proxy(Proxy.Type.HTTP,
|
|
||||||
InetSocketAddress(StateDeveloper.instance.devProxy!!.url, StateDeveloper.instance.devProxy!!.port)
|
|
||||||
))
|
|
||||||
else
|
|
||||||
OkHttpClient.Builder())
|
|
||||||
) {
|
|
||||||
_jsClient = jsClient;
|
_jsClient = jsClient;
|
||||||
_jsConfig = config;
|
_jsConfig = config;
|
||||||
_auth = auth;
|
_auth = auth;
|
||||||
|
@ -217,16 +201,6 @@ class JSHttpClient : ManagedHttpClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(_jsClient is DevJSClient) {
|
|
||||||
//val peekBody = resp.peekBody(1000 * 1000).string();
|
|
||||||
StateDeveloper.instance.addDevHttpExchange(
|
|
||||||
StateDeveloper.DevHttpExchange(
|
|
||||||
StateDeveloper.DevHttpRequest(resp.request.method, resp.request.url.toString(), mapOf(*resp.request.headers.map { Pair(it.first, it.second) }.toTypedArray()), ""),
|
|
||||||
StateDeveloper.DevHttpRequest("RESP", resp.request.url.toString(), mapOf(*resp.headers.map { Pair(it.first, it.second) }.toTypedArray()), "", resp.code)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package com.futo.platformplayer.api.media.platforms.js.models
|
package com.futo.platformplayer.api.media.platforms.js.models
|
||||||
|
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
import com.futo.platformplayer.api.media.models.JSChannelContent
|
|
||||||
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.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
|
@ -27,7 +26,6 @@ interface IJSContent: IPlatformContent {
|
||||||
ContentType.NESTED_VIDEO -> JSNestedMediaContent(config, obj);
|
ContentType.NESTED_VIDEO -> JSNestedMediaContent(config, obj);
|
||||||
ContentType.PLAYLIST -> JSPlaylist(config, obj);
|
ContentType.PLAYLIST -> JSPlaylist(config, obj);
|
||||||
ContentType.LOCKED -> JSLockedContent(config, obj);
|
ContentType.LOCKED -> JSLockedContent(config, obj);
|
||||||
ContentType.CHANNEL -> JSChannelContent(config, obj)
|
|
||||||
else -> throw NotImplementedError("Unknown content type ${type}");
|
else -> throw NotImplementedError("Unknown content type ${type}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,6 @@ 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,162 +0,0 @@
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -7,7 +7,6 @@ import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.getOrDefault
|
|
||||||
import com.futo.platformplayer.getOrDefaultList
|
import com.futo.platformplayer.getOrDefaultList
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
import com.futo.platformplayer.getOrThrowNullable
|
import com.futo.platformplayer.getOrThrowNullable
|
||||||
|
@ -38,7 +37,7 @@ class JSChannel : IPlatformChannel {
|
||||||
description = _channel.getOrThrowNullable(config, "description", contextName);
|
description = _channel.getOrThrowNullable(config, "description", contextName);
|
||||||
url = _channel.getOrThrow(config, "url", contextName);
|
url = _channel.getOrThrow(config, "url", contextName);
|
||||||
urlAlternatives = _channel.getOrDefaultList(config, "urlAlternatives", contextName, listOf()) ?: listOf();
|
urlAlternatives = _channel.getOrDefaultList(config, "urlAlternatives", contextName, listOf()) ?: listOf();
|
||||||
links = HashMap(_channel.getOrDefault<Map<String, String>>(config, "links", contextName, mapOf()) ?: mapOf());
|
links = HashMap();
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getContents(client: IPlatformClient): IPager<IPlatformContent> {
|
override fun getContents(client: IPlatformClient): IPager<IPlatformContent> {
|
||||||
|
|
|
@ -5,6 +5,7 @@ import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
|
|
||||||
class JSChannelPager : JSPager<PlatformAuthorLink>, IPager<PlatformAuthorLink> {
|
class JSChannelPager : JSPager<PlatformAuthorLink>, IPager<PlatformAuthorLink> {
|
||||||
|
|
||||||
|
|
|
@ -42,15 +42,10 @@ 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);
|
val datetimeInt = _content.getOrThrow<Int>(config, "datetime", contextName).toLong();
|
||||||
if(authorObj != null)
|
if(datetimeInt == 0.toLong())
|
||||||
author = PlatformAuthorLink.fromV8(_pluginConfig, authorObj);
|
|
||||||
else
|
|
||||||
author = PlatformAuthorLink.UNKNOWN;
|
|
||||||
|
|
||||||
val datetimeInt = _content.getOrDefault<Int>(config, "datetime", contextName, null)?.toLong();
|
|
||||||
if(datetimeInt == null || datetimeInt == 0.toLong())
|
|
||||||
datetime = null;
|
datetime = null;
|
||||||
else
|
else
|
||||||
datetime = OffsetDateTime.of(LocalDateTime.ofEpochSecond(datetimeInt, 0, ZoneOffset.UTC), ZoneOffset.UTC);
|
datetime = OffsetDateTime.of(LocalDateTime.ofEpochSecond(datetimeInt, 0, ZoneOffset.UTC), ZoneOffset.UTC);
|
||||||
|
@ -59,8 +54,4 @@ open class JSContent : IPlatformContent, IPluginSourced {
|
||||||
|
|
||||||
_hasGetDetails = _content.has("getDetails");
|
_hasGetDetails = _content.has("getDetails");
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getUnderlyingObject(): V8ValueObject? {
|
|
||||||
return _content;
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -2,7 +2,6 @@ package com.futo.platformplayer.api.media.platforms.js.models
|
||||||
|
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
import com.futo.platformplayer.api.media.IPluginSourced
|
import com.futo.platformplayer.api.media.IPluginSourced
|
||||||
import com.futo.platformplayer.api.media.models.JSChannelContent
|
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
|
@ -17,13 +16,3 @@ class JSContentPager : JSPager<IPlatformContent>, IPluginSourced {
|
||||||
return IJSContent.fromV8(plugin, obj);
|
return IJSContent.fromV8(plugin, obj);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class JSChannelContentPager : JSPager<IPlatformContent>, IPluginSourced {
|
|
||||||
override val sourceConfig: SourcePluginConfig get() = config;
|
|
||||||
|
|
||||||
constructor(config: SourcePluginConfig, plugin: JSClient, pager: V8ValueObject) : super(config, plugin, pager) {}
|
|
||||||
|
|
||||||
override fun convertResult(obj: V8ValueObject): IPlatformContent {
|
|
||||||
return JSChannelContent(config, obj);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -14,13 +14,12 @@ import java.time.ZoneOffset
|
||||||
class JSLiveChatWindowDescriptor: ILiveChatWindowDescriptor {
|
class JSLiveChatWindowDescriptor: ILiveChatWindowDescriptor {
|
||||||
override val url: String;
|
override val url: String;
|
||||||
override val removeElements: List<String>;
|
override val removeElements: List<String>;
|
||||||
override val removeElementsInterval: List<String>;
|
|
||||||
|
|
||||||
constructor(config: SourcePluginConfig, obj: V8ValueObject) {
|
constructor(config: SourcePluginConfig, obj: V8ValueObject) {
|
||||||
val contextName = "LiveChatWindowDescriptor";
|
val contextName = "LiveChatWindowDescriptor";
|
||||||
|
|
||||||
url = obj.getOrThrow(config, "url", contextName);
|
url = obj.getOrThrow(config, "url", contextName);
|
||||||
removeElements = obj.getOrDefault(config, "removeElements", contextName, listOf()) ?: listOf();
|
removeElements = obj.getOrDefault(config, "removeElements", contextName, listOf()) ?: listOf();
|
||||||
removeElementsInterval = obj.getOrDefault(config, "removeElementsInterval", contextName, listOf()) ?: listOf();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -71,8 +71,6 @@ 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();
|
||||||
|
|
|
@ -17,8 +17,6 @@ class JSPlaybackTracker: IPlaybackTracker {
|
||||||
|
|
||||||
private var _lastRequest: Long = Long.MIN_VALUE;
|
private var _lastRequest: Long = Long.MIN_VALUE;
|
||||||
|
|
||||||
private val _hasOnConcluded: Boolean;
|
|
||||||
|
|
||||||
override var nextRequest: Int = 1000
|
override var nextRequest: Int = 1000
|
||||||
private set;
|
private set;
|
||||||
|
|
||||||
|
@ -28,7 +26,6 @@ class JSPlaybackTracker: IPlaybackTracker {
|
||||||
throw ScriptImplementationException(config, "Missing onProgress on PlaybackTracker");
|
throw ScriptImplementationException(config, "Missing onProgress on PlaybackTracker");
|
||||||
if(!obj.has("nextRequest"))
|
if(!obj.has("nextRequest"))
|
||||||
throw ScriptImplementationException(config, "Missing nextRequest on PlaybackTracker");
|
throw ScriptImplementationException(config, "Missing nextRequest on PlaybackTracker");
|
||||||
_hasOnConcluded = obj.has("onConcluded");
|
|
||||||
|
|
||||||
this._config = config;
|
this._config = config;
|
||||||
this._obj = obj;
|
this._obj = obj;
|
||||||
|
@ -62,16 +59,6 @@ class JSPlaybackTracker: IPlaybackTracker {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
override fun onConcluded() {
|
|
||||||
warnIfMainThread("JSPlaybackTracker.onConcluded");
|
|
||||||
if(_hasOnConcluded) {
|
|
||||||
synchronized(_obj) {
|
|
||||||
Logger.i("JSPlaybackTracker", "onConcluded");
|
|
||||||
_obj.invokeVoid("onConcluded", -1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
override fun shouldUpdate(): Boolean = (_lastRequest < 0 || (System.currentTimeMillis() - _lastRequest) > nextRequest);
|
override fun shouldUpdate(): Boolean = (_lastRequest < 0 || (System.currentTimeMillis() - _lastRequest) > nextRequest);
|
||||||
}
|
}
|
|
@ -14,6 +14,6 @@ open class JSPlaylist : JSContent, IPlatformPlaylist {
|
||||||
constructor(config: SourcePluginConfig, obj: V8ValueObject) : super(config, obj) {
|
constructor(config: SourcePluginConfig, obj: V8ValueObject) : super(config, obj) {
|
||||||
val contextName = "Playlist";
|
val contextName = "Playlist";
|
||||||
thumbnail = obj.getOrDefault(config, "thumbnail", contextName, null);
|
thumbnail = obj.getOrDefault(config, "thumbnail", contextName, null);
|
||||||
videoCount = obj.getOrDefault(config, "videoCount", contextName, -1)!!;
|
videoCount = obj.getOrDefault(config, "videoCount", contextName, 0)!!;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -7,37 +7,32 @@ 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.JSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.api.media.structures.ReusablePager
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
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>;
|
||||||
|
|
||||||
constructor(plugin: JSClient, config: SourcePluginConfig, obj: V8ValueObject): super(config, obj) {
|
constructor(plugin: JSClient, config: SourcePluginConfig, obj: V8ValueObject): super(config, obj) {
|
||||||
contents = ReusablePager(JSVideoPager(config, plugin, obj.getOrThrow(config, "contents", "PlaylistDetails")));
|
contents = JSVideoPager(config, plugin, obj.getOrThrow(config, "contents", "PlaylistDetails"));
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toPlaylist(onProgress: ((progress: Int) -> Unit)?): Playlist {
|
override fun toPlaylist(): Playlist {
|
||||||
val playlist = if (contents is ReusablePager) contents.getWindow() else contents;
|
val videos = contents.getResults().toMutableList();
|
||||||
val videos = playlist.getResults().toMutableList();
|
|
||||||
onProgress?.invoke(videos.size);
|
|
||||||
|
|
||||||
//Download all pages
|
//Download all pages
|
||||||
var allowedEmptyCount = 2;
|
var allowedEmptyCount = 2;
|
||||||
while(playlist.hasMorePages()) {
|
while(contents.hasMorePages()) {
|
||||||
playlist.nextPage();
|
contents.nextPage();
|
||||||
if(!videos.addAll(playlist.getResults())) {
|
if(!videos.addAll(contents.getResults())) {
|
||||||
allowedEmptyCount--;
|
allowedEmptyCount--;
|
||||||
if(allowedEmptyCount <= 0)
|
if(allowedEmptyCount <= 0)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
else allowedEmptyCount = 2;
|
else allowedEmptyCount = 2;
|
||||||
|
|
||||||
onProgress?.invoke(videos.size);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Playlist(UUID.randomUUID().toString(), name, videos.map { SerializedPlatformVideo.fromVideo(it)});
|
return Playlist(id.toString(), name, videos.map { SerializedPlatformVideo.fromVideo(it)});
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -3,7 +3,6 @@ package com.futo.platformplayer.api.media.platforms.js.models
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
import com.futo.platformplayer.api.media.IPlatformClient
|
import com.futo.platformplayer.api.media.IPlatformClient
|
||||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
|
||||||
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
||||||
import com.futo.platformplayer.api.media.models.post.IPlatformPost
|
import com.futo.platformplayer.api.media.models.post.IPlatformPost
|
||||||
import com.futo.platformplayer.api.media.models.post.IPlatformPostDetails
|
import com.futo.platformplayer.api.media.models.post.IPlatformPostDetails
|
||||||
|
@ -19,7 +18,6 @@ import com.futo.platformplayer.states.StateDeveloper
|
||||||
|
|
||||||
class JSPostDetails : JSPost, IPlatformPost, IPlatformPostDetails {
|
class JSPostDetails : JSPost, IPlatformPost, IPlatformPostDetails {
|
||||||
private val _hasGetComments: Boolean;
|
private val _hasGetComments: Boolean;
|
||||||
private val _hasGetContentRecommendations: Boolean;
|
|
||||||
|
|
||||||
override val rating: IRating;
|
override val rating: IRating;
|
||||||
|
|
||||||
|
@ -36,7 +34,6 @@ class JSPostDetails : JSPost, IPlatformPost, IPlatformPostDetails {
|
||||||
content = obj.getOrDefault(config, "content", contextName, "") ?: "";
|
content = obj.getOrDefault(config, "content", contextName, "") ?: "";
|
||||||
|
|
||||||
_hasGetComments = _content.has("getComments");
|
_hasGetComments = _content.has("getComments");
|
||||||
_hasGetContentRecommendations = _content.has("getContentRecommendations");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? {
|
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? {
|
||||||
|
@ -54,27 +51,9 @@ class JSPostDetails : JSPost, IPlatformPost, IPlatformPostDetails {
|
||||||
}
|
}
|
||||||
override fun getPlaybackTracker(): IPlaybackTracker? = 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 {
|
private fun getCommentsJS(client: JSClient): JSCommentPager {
|
||||||
val commentPager = _content.invoke<V8ValueObject>("getComments", arrayOf<Any>());
|
val commentPager = _content.invoke<V8ValueObject>("getComments", arrayOf<Any>());
|
||||||
return JSCommentPager(_pluginConfig, client, commentPager);
|
return JSCommentPager(_pluginConfig, client, commentPager);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -1,134 +0,0 @@
|
||||||
package com.futo.platformplayer.api.media.platforms.js.models
|
|
||||||
|
|
||||||
import com.caoccao.javet.values.V8Value
|
|
||||||
import com.caoccao.javet.values.primitive.V8ValueString
|
|
||||||
import com.caoccao.javet.values.primitive.V8ValueUndefined
|
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
|
||||||
import com.caoccao.javet.values.reference.V8ValueTypedArray
|
|
||||||
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
|
||||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
|
||||||
import com.futo.platformplayer.engine.V8Plugin
|
|
||||||
import com.futo.platformplayer.engine.exceptions.PluginException
|
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptException
|
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
|
||||||
import com.futo.platformplayer.getOrDefault
|
|
||||||
import com.futo.platformplayer.getOrThrow
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
|
||||||
import com.futo.platformplayer.states.StateDeveloper
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import java.util.Base64
|
|
||||||
|
|
||||||
class JSRequestExecutor {
|
|
||||||
private val _plugin: JSClient;
|
|
||||||
private val _config: IV8PluginConfig;
|
|
||||||
private var _executor: V8ValueObject;
|
|
||||||
val urlPrefix: String?;
|
|
||||||
|
|
||||||
private val hasCleanup: Boolean;
|
|
||||||
|
|
||||||
constructor(plugin: JSClient, executor: V8ValueObject) {
|
|
||||||
this._plugin = plugin;
|
|
||||||
this._executor = executor;
|
|
||||||
this._config = plugin.config;
|
|
||||||
val config = plugin.config;
|
|
||||||
|
|
||||||
urlPrefix = executor.getOrDefault(config, "urlPrefix", "RequestExecutor", null);
|
|
||||||
|
|
||||||
if(!executor.has("executeRequest"))
|
|
||||||
throw ScriptImplementationException(config, "RequestExecutor is missing executeRequest", null);
|
|
||||||
hasCleanup = executor.has("cleanup");
|
|
||||||
}
|
|
||||||
|
|
||||||
//TODO: Executor properties?
|
|
||||||
@Throws(ScriptException::class)
|
|
||||||
open fun executeRequest(method: String, url: String, body: ByteArray?, headers: Map<String, String>): ByteArray {
|
|
||||||
if (_executor.isClosed)
|
|
||||||
throw IllegalStateException("Executor object is closed");
|
|
||||||
|
|
||||||
val result = if(_plugin is DevJSClient)
|
|
||||||
StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") {
|
|
||||||
V8Plugin.catchScriptErrors<Any>(
|
|
||||||
_config,
|
|
||||||
"[${_config.name}] JSRequestExecutor",
|
|
||||||
"builder.modifyRequest()"
|
|
||||||
) {
|
|
||||||
_executor.invoke("executeRequest", url, headers, method, body);
|
|
||||||
} as V8Value;
|
|
||||||
}
|
|
||||||
else V8Plugin.catchScriptErrors<Any>(
|
|
||||||
_config,
|
|
||||||
"[${_config.name}] JSRequestExecutor",
|
|
||||||
"builder.modifyRequest()"
|
|
||||||
) {
|
|
||||||
_executor.invoke("executeRequest", url, headers, method, body);
|
|
||||||
} as V8Value;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if(result is V8ValueString) {
|
|
||||||
val base64Result = Base64.getDecoder().decode(result.value);
|
|
||||||
return base64Result;
|
|
||||||
}
|
|
||||||
if(result is V8ValueTypedArray) {
|
|
||||||
val buffer = result.buffer;
|
|
||||||
val byteBuffer = buffer.byteBuffer;
|
|
||||||
val bytesResult = ByteArray(result.byteLength);
|
|
||||||
byteBuffer.get(bytesResult, 0, result.byteLength);
|
|
||||||
buffer.close();
|
|
||||||
return bytesResult;
|
|
||||||
}
|
|
||||||
if(result is V8ValueObject && result.has("type")) {
|
|
||||||
val type = result.getOrThrow<Int>(_config, "type", "JSRequestModifier");
|
|
||||||
when(type) {
|
|
||||||
//TODO: Buffer type?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(result is V8ValueUndefined) {
|
|
||||||
if(_plugin is DevJSClient)
|
|
||||||
StateDeveloper.instance.logDevException(_plugin.devID, "JSRequestExecutor.executeRequest returned illegal undefined");
|
|
||||||
throw ScriptImplementationException(_config, "JSRequestExecutor.executeRequest returned illegal undefined", null);
|
|
||||||
}
|
|
||||||
throw NotImplementedError("Executor result type not implemented? " + result.javaClass.name);
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
result.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
open fun cleanup() {
|
|
||||||
if (!hasCleanup || _executor.isClosed)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if(_plugin is DevJSClient)
|
|
||||||
StateDeveloper.instance.handleDevCall(_plugin.devID, "requestExecutor.executeRequest()") {
|
|
||||||
V8Plugin.catchScriptErrors<Any>(
|
|
||||||
_config,
|
|
||||||
"[${_config.name}] JSRequestExecutor",
|
|
||||||
"builder.modifyRequest()"
|
|
||||||
) {
|
|
||||||
_executor.invokeVoid("cleanup", null);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else V8Plugin.catchScriptErrors<Any>(
|
|
||||||
_config,
|
|
||||||
"[${_config.name}] JSRequestExecutor",
|
|
||||||
"builder.modifyRequest()"
|
|
||||||
) {
|
|
||||||
_executor.invokeVoid("cleanup", null);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun finalize() {
|
|
||||||
cleanup();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//TODO: are these available..?
|
|
||||||
@Serializable
|
|
||||||
class ExecutorParameters {
|
|
||||||
var rangeStart: Int = -1;
|
|
||||||
var rangeEnd: Int = -1;
|
|
||||||
|
|
||||||
var segment: Int = -1;
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue