Compare commits

..

No commits in common. "master" and "210" have entirely different histories.
master ... 210

724 changed files with 8412 additions and 45217 deletions

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -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.

39
.gitmodules vendored
View file

@ -1,6 +1,9 @@
[submodule "dep/polycentricandroid"] [submodule "dep/polycentricandroid"]
path = dep/polycentricandroid path = dep/polycentricandroid
url = ../polycentricandroid.git url = ../polycentricandroid.git
[submodule "app/src/playstore/assets/sources/peertube"]
path = app/src/playstore/assets/sources/peertube
url = ../plugins/peertube.git
[submodule "app/src/stable/assets/sources/kick"] [submodule "app/src/stable/assets/sources/kick"]
path = app/src/stable/assets/sources/kick path = app/src/stable/assets/sources/kick
url = ../plugins/kick.git url = ../plugins/kick.git
@ -58,39 +61,3 @@
[submodule "dep/futopay"] [submodule "dep/futopay"]
path = dep/futopay path = dep/futopay
url = ../futopayclientlibraries.git url = ../futopayclientlibraries.git
[submodule "app/src/unstable/assets/sources/bilibili"]
path = app/src/unstable/assets/sources/bilibili
url = ../plugins/bilibili.git
[submodule "app/src/stable/assets/sources/bilibili"]
path = app/src/stable/assets/sources/bilibili
url = ../plugins/bilibili.git
[submodule "app/src/stable/assets/sources/spotify"]
path = app/src/stable/assets/sources/spotify
url = ../plugins/spotify.git
[submodule "app/src/unstable/assets/sources/spotify"]
path = app/src/unstable/assets/sources/spotify
url = ../plugins/spotify.git
[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

View file

@ -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
View 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.

View file

@ -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 Licensors 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.

View file

@ -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).

View file

@ -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,24 +144,14 @@ 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'
implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.11.0' implementation 'com.google.android.material:material:1.10.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
//Images //Images
@ -179,25 +169,25 @@ dependencies {
implementation 'com.google.code.gson:gson:2.10.1' //Used for complex/anonymous cases like during development conversions (eg. V8RemoteObject) implementation 'com.google.code.gson:gson:2.10.1' //Used for complex/anonymous cases like during development conversions (eg. V8RemoteObject)
//JS //JS
implementation("com.caoccao.javet:javet-android:3.0.2") implementation("com.caoccao.javet:javet-android:2.2.1")
//Exoplayer //Exoplayer
implementation 'androidx.media3:media3-exoplayer:1.2.1' implementation 'com.google.android.exoplayer:exoplayer-core:2.19.1'
implementation 'androidx.media3:media3-exoplayer-dash:1.2.1' implementation 'com.google.android.exoplayer:exoplayer-dash:2.19.1'
implementation 'androidx.media3:media3-ui:1.2.1' implementation 'com.google.android.exoplayer:exoplayer-ui:2.19.1'
implementation 'androidx.media3:media3-exoplayer-hls:1.2.1' implementation 'com.google.android.exoplayer:exoplayer-hls:2.19.1'
implementation 'androidx.media3:media3-exoplayer-rtsp:1.2.1' implementation 'com.google.android.exoplayer:exoplayer-rtsp:2.19.1'
implementation 'androidx.media3:media3-exoplayer-smoothstreaming:1.2.1' implementation 'com.google.android.exoplayer:exoplayer-smoothstreaming:2.19.1'
implementation 'androidx.media3:media3-transformer:1.2.1' implementation 'com.google.android.exoplayer:exoplayer-transformer:2.19.1'
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.6' implementation 'androidx.navigation:navigation-fragment-ktx:2.7.5'
implementation 'androidx.navigation:navigation-ui-ktx:2.7.6' implementation 'androidx.navigation:navigation-ui-ktx:2.7.5'
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'

View file

@ -1,111 +0,0 @@
package com.futo.platformplayer
import android.util.Base64
import android.util.Log
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.futo.platformplayer.casting.FCastCastingDevice
import com.futo.platformplayer.casting.Opcode
import com.futo.platformplayer.casting.models.FCastDecryptedMessage
import com.futo.platformplayer.casting.models.FCastEncryptedMessage
import com.futo.platformplayer.casting.models.FCastKeyExchangeMessage
import com.futo.platformplayer.casting.models.FCastPlayMessage
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.junit.Assert.*
import org.junit.Test
import org.junit.runner.RunWith
import java.security.KeyFactory
import java.security.spec.PKCS8EncodedKeySpec
import javax.crypto.spec.SecretKeySpec
@RunWith(AndroidJUnit4::class)
class FCastEncryptionTests {
@Test
fun testDHEncryptionSelf() {
val keyPair1 = FCastCastingDevice.generateKeyPair()
val keyPair2 = FCastCastingDevice.generateKeyPair()
Log.i("testDHEncryptionSelf", "privates (1: ${Base64.encodeToString(keyPair1.private.encoded, Base64.NO_WRAP)}, 2: ${Base64.encodeToString(keyPair2.private.encoded, Base64.NO_WRAP)})")
val keyExchangeMessage1 = FCastCastingDevice.getKeyExchangeMessage(keyPair1)
val keyExchangeMessage2 = FCastCastingDevice.getKeyExchangeMessage(keyPair2)
Log.i("testDHEncryptionSelf", "publics (1: ${keyExchangeMessage1.publicKey}, 2: ${keyExchangeMessage2.publicKey})")
val aesKey1 = FCastCastingDevice.computeSharedSecret(keyPair1.private, keyExchangeMessage2)
val aesKey2 = FCastCastingDevice.computeSharedSecret(keyPair2.private, keyExchangeMessage1)
assertEquals(Base64.encodeToString(aesKey1.encoded, Base64.NO_WRAP), Base64.encodeToString(aesKey2.encoded, Base64.NO_WRAP))
Log.i("testDHEncryptionSelf", "aesKey ${Base64.encodeToString(aesKey1.encoded, Base64.NO_WRAP)}")
val message = FCastPlayMessage("text/html")
val serializedBody = Json.encodeToString(message)
val encryptedMessage = FCastCastingDevice.encryptMessage(aesKey1, FCastDecryptedMessage(Opcode.Play.value.toLong(), serializedBody))
Log.i("testDHEncryptionSelf", Json.encodeToString(encryptedMessage))
val decryptedMessage = FCastCastingDevice.decryptMessage(aesKey1, encryptedMessage)
assertEquals(Opcode.Play.value.toLong(), decryptedMessage.opcode)
assertEquals(serializedBody, decryptedMessage.message)
}
@Test
fun testAESKeyGeneration() {
val cases = listOf(
listOf(
//Public other
"MIIBHzCBlQYJKoZIhvcNAQMBMIGHAoGBAP//////////yQ/aoiFowjTExmKLgNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7OZTgf//////////AgECA4GEAAKBgEnOS0oHteVA+3kND3u4yXe7GGRohy1LkR9Q5tL4c4ylC5n4iSwWSoIhcSIvUMWth6KAhPhu05sMcPY74rFMSS2AGTNCdT/5KilediipuUMdFVvjGqfNMNH1edzW5mquIw3iXKdfQmfY/qxLTI2wccyDj4hHFhLCZL3Y+shsm3KF",
//Private self
"MIIBIQIBADCBlQYJKoZIhvcNAQMBMIGHAoGBAP//////////yQ/aoiFowjTExmKLgNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7OZTgf//////////AgECBIGDAoGAeo/ceIeH8Jt1ZRNKX5aTHkMi23GCV1LtcS2O6Tktn9k8DCv7gIoekysQUhMyWtR+MsZlq2mXjr1JFpAyxl89rqoEPU6QDsGe9q8R4O8eBZ2u+48mkUkGSh7xPGRQUBvmhH2yk4hIEA8aK4BcYi1OTsCZtmk7pQq+uaFkKovD/8M=",
//AES
"7dpl1/6KQTTooOrFf2VlUOSqgrFHi6IYxapX0IxFfwk="
),
listOf(
//Public other
"MIIBHzCBlQYJKoZIhvcNAQMBMIGHAoGBAP//////////yQ/aoiFowjTExmKLgNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7OZTgf//////////AgECA4GEAAKBgGvIlCP/S+xpAuNEHSn4cEDOL1esUf+uMuY2Kp5J10a7HGbwzNd+7eYsgEc4+adddgB7hJgTvjsGg7lXUhHQ7WbfbCGgt7dbkx8qkic6Rgq4f5eRYd1Cgidw4MhZt7mEIOKrHweqnV6B9rypbXjbqauc6nGgtwx+Gvl6iLpVATRK",
//Private self
"MIIBIQIBADCBlQYJKoZIhvcNAQMBMIGHAoGBAP//////////yQ/aoiFowjTExmKLgNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7OZTgf//////////AgECBIGDAoGAMXmiIgWyutbaO+f4UiMAb09iVVSCI6Lb6xzNyD2MpUZyk4/JOT04Daj4JeCKFkF1Fq79yKhrnFlXCrF4WFX00xUOXb8BpUUUH35XG5ApvolQQLL6N0om8/MYP4FK/3PUxuZAJz45TUsI/v3u6UqJelVTNL83ltcFbZDIfEVftRA=",
//AES
"a2tUSxnXifKohfNocAQHkAlPffDv6ReihJ7OojBGt0Q="
)
)
for (case in cases) {
val decodedPrivateKey1 = Base64.decode(case[1], Base64.NO_WRAP)
val keyExchangeMessage2 = FCastKeyExchangeMessage(1, case[0])
val keyFactory = KeyFactory.getInstance("DH")
val privateKeySpec = PKCS8EncodedKeySpec(decodedPrivateKey1)
val privateKey = keyFactory.generatePrivate(privateKeySpec)
val aesKey1 = FCastCastingDevice.computeSharedSecret(privateKey, keyExchangeMessage2)
assertEquals(case[2], Base64.encodeToString(aesKey1.encoded, Base64.NO_WRAP))
}
}
@Test
fun testDHEncryptionKnown() {
val decodedPrivateKey1 = Base64.decode("MIIDJwIBADCCAhgGCSqGSIb3DQEDATCCAgkCggEBAJVHXPXZPllsP80dkCrdAvQn9fPHIQMTu0X7TVuy5f4cvWeM1LvdhMmDa+HzHAd3clrrbC/Di4X0gHb6drzYFGzImm+y9wbdcZiYwgg9yNiW+EBi4snJTRN7BUqNgJatuNUZUjmO7KhSoK8S34Pkdapl1OwMOKlWDVZhGG/5i5/J62Du6LAwN2sja8c746zb10/WHB0kdfowd7jwgEZ4gf9+HKVv7gZteVBq3lHtu1RDpWOSfbxLpSAIZ0YXXIiFkl68ZMYUeQZ3NJaZDLcU7GZzBOJh+u4zs8vfAI4MP6kGUNl9OQnJJ1v0rIb/yz0D5t/IraWTQkLdbTvMoqQGywsCggEAQt67naWz2IzJVuCHh+w/Ogm7pfSLiJp0qvUxdKoPvn48W4/NelO+9WOw6YVgMolgqVF/QBTTMl/Hlivx4Ek3DXbRMUp2E355Lz8NuFnQleSluTICTweezy7wnHl0UrB3DhNQeC7Vfd95SXnc7yPLlvGDBhllxOvJPJxxxWuSWVWnX5TMzxRJrEPVhtC+7kMlGwsihzSdaN4NFEQD8T6AL0FG2ILgV68ZtvYnXGZ2yPoOPKJxOjJX/Rsn0GOfaV40fY0c+ayBmibKmwTLDrm3sDWYjRW7rGUhKlUjnPx+WPrjjXJQq5mR/7yXE0Al/ozgTEOZrZZWm+kaVG9JeGk8egSCAQQCggEAECNvEczf0y6IoX/IwhrPeWZ5IxrHcpwjcdVAuyZQLLlOq0iqnYMFcSD8QjMF8NKObfZZCDQUJlzGzRsG0oXsWiWtmoRvUZ9tQK0j28hDylpbyP00Bt9NlMgeHXkAy54P7Z2v/BPCd3o23kzjgXzYaSRuCFY7zQo1g1IQG8mfjYjdE4jjRVdVrlh8FS8x4OLPeglc+cp2/kuyxaVEfXAG84z/M8019mRSfdczi4z1iidPX6HgDEEWsN42Ud60mNKy5jsQpQYkRdOLmxR3+iQEtGFjdzbVhVCUr7S5EORU9B1MOl5gyPJpjfU3baOqrg6WXVyTvMDaA05YEnAHQNOOfA==", Base64.NO_WRAP)
val keyExchangeMessage2 = FCastKeyExchangeMessage(1, "MIIDJTCCAhgGCSqGSIb3DQEDATCCAgkCggEBAJVHXPXZPllsP80dkCrdAvQn9fPHIQMTu0X7TVuy5f4cvWeM1LvdhMmDa+HzHAd3clrrbC/Di4X0gHb6drzYFGzImm+y9wbdcZiYwgg9yNiW+EBi4snJTRN7BUqNgJatuNUZUjmO7KhSoK8S34Pkdapl1OwMOKlWDVZhGG/5i5/J62Du6LAwN2sja8c746zb10/WHB0kdfowd7jwgEZ4gf9+HKVv7gZteVBq3lHtu1RDpWOSfbxLpSAIZ0YXXIiFkl68ZMYUeQZ3NJaZDLcU7GZzBOJh+u4zs8vfAI4MP6kGUNl9OQnJJ1v0rIb/yz0D5t/IraWTQkLdbTvMoqQGywsCggEAQt67naWz2IzJVuCHh+w/Ogm7pfSLiJp0qvUxdKoPvn48W4/NelO+9WOw6YVgMolgqVF/QBTTMl/Hlivx4Ek3DXbRMUp2E355Lz8NuFnQleSluTICTweezy7wnHl0UrB3DhNQeC7Vfd95SXnc7yPLlvGDBhllxOvJPJxxxWuSWVWnX5TMzxRJrEPVhtC+7kMlGwsihzSdaN4NFEQD8T6AL0FG2ILgV68ZtvYnXGZ2yPoOPKJxOjJX/Rsn0GOfaV40fY0c+ayBmibKmwTLDrm3sDWYjRW7rGUhKlUjnPx+WPrjjXJQq5mR/7yXE0Al/ozgTEOZrZZWm+kaVG9JeGk8egOCAQUAAoIBAGlL9EYsrFz3I83NdlwhM241M+M7PA9P5WXgtdvS+pcalIaqN2IYdfzzCUfye7lchVkT9A2Y9eWQYX0OUhmjf8PPKkRkATLXrqO5HTsxV96aYNxMjz5ipQ6CaErTQaPLr3OPoauIMPVVI9zM+WT0KOGp49YMyx+B5rafT066vOVbF/0z1crq0ZXxyYBUv135rwFkIHxBMj5bhRLXKsZ2G5aLAZg0DsVam104mgN/v75f7Spg/n5hO7qxbNgbvSrvQ7Ag/rMk5T3sk7KoM23Qsjl08IZKs2jjx21MiOtyLqGuCW6GOTNK4yEEDF5gA0K13eXGwL5lPS0ilRw+Lrw7cJU=")
val keyFactory = KeyFactory.getInstance("DH")
val privateKeySpec = PKCS8EncodedKeySpec(decodedPrivateKey1)
val privateKey = keyFactory.generatePrivate(privateKeySpec)
val aesKey1 = FCastCastingDevice.computeSharedSecret(privateKey, keyExchangeMessage2)
assertEquals("vI5LGE625zGEG350ggkyBsIAXm2y4sNohiPcED1oAEE=", Base64.encodeToString(aesKey1.encoded, Base64.NO_WRAP))
val message = FCastPlayMessage("text/html")
val serializedBody = Json.encodeToString(message)
val encryptedMessage = FCastCastingDevice.encryptMessage(aesKey1, FCastDecryptedMessage(Opcode.Play.value.toLong(), serializedBody))
val decryptedMessage = FCastCastingDevice.decryptMessage(aesKey1, encryptedMessage)
assertEquals(Opcode.Play.value.toLong(), decryptedMessage.opcode)
assertEquals(serializedBody, decryptedMessage.message)
}
@Test
fun testDecryptMessageKnown() {
val encryptedMessage = Json.decodeFromString<FCastEncryptedMessage>("{\"version\":1,\"iv\":\"C4H70VC5FWrNtkty9/cLIA==\",\"blob\":\"K6/N7JMyi1PFwKhU0mFj7ZJmd/tPp3NCOMldmQUtDaQ7hSmPoIMI5QNMOj+NFEiP4qTgtYp5QmBPoQum6O88pA==\"}")
val aesKey = SecretKeySpec(Base64.decode("+hr9Jg8yre7S9WGUohv2AUSzHNQN514JPh6MoFAcFNU=", Base64.NO_WRAP), "AES")
val decryptedMessage = FCastCastingDevice.decryptMessage(aesKey, encryptedMessage)
assertEquals(Opcode.Play.value.toLong(), decryptedMessage.opcode)
assertEquals("{\"container\":\"text/html\"}", decryptedMessage.message)
}
}

View file

@ -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
}

View file

@ -7,14 +7,12 @@
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" tools:ignore="ScopedStorage" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<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"/>
<application <application
android:allowBackup="true" android:allowBackup="true"
@ -26,8 +24,7 @@
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.FutoVideo" android:theme="@style/Theme.FutoVideo"
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true"
tools:targetApi="31" tools:targetApi="31">
android:largeHeap="true">
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="@string/authority" android:authorities="@string/authority"
@ -36,18 +33,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 +51,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 +146,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 +187,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>

View file

@ -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

View file

@ -233,9 +233,6 @@ function pluginRemoteProp(objID, propName) {
function pluginRemoteCall(objID, methodName, args) { function pluginRemoteCall(objID, methodName, args) {
return JSON.parse(syncPOST("/plugin/remoteCall?id=" + objID + "&method=" + methodName, {}, JSON.stringify(args))); return JSON.parse(syncPOST("/plugin/remoteCall?id=" + objID + "&method=" + methodName, {}, JSON.stringify(args)));
} }
function pluginRemoteTest(methodName, args) {
return JSON.parse(syncPOST("/plugin/remoteTest?method=" + methodName, {}, JSON.stringify(args)));
}
function pluginIsLoggedIn(cb, err) { function pluginIsLoggedIn(cb, err) {
fetch("/plugin/isLoggedIn", { fetch("/plugin/isLoggedIn", {
@ -262,17 +259,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, {});
} }

View file

@ -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>
@ -500,8 +385,8 @@
</v-card-text> </v-card-text>
</v-card> </v-card>
<div style="width: 50%" v-if="Plugin.currentPlugin"> <div style="width: 50%" v-if="Plugin.currentPlugin">
<v-text-field v-model="searchTestMethods" label="Search for source methods.." style="margin-left: 35px; margin-right: 35px;"></v-text-field> <!--Get Home-->
<v-card class="requestCard" v-for="req in Testing.requests" v-show="req.title.indexOf(searchTestMethods) >= 0"> <v-card class="requestCard" v-for="req in Testing.requests">
<v-card-text> <v-card-text>
<div class="title"> <div class="title">
<span v-if="req.isOptional">(Optional)</span> <span v-if="req.isOptional">(Optional)</span>
@ -517,11 +402,6 @@
<div class="code"> <div class="code">
{{req.code}} {{req.code}}
</div> </div>
<div class="documentation" v-if="req.docUrl" style="position: absolute; right: 15px; top: 15px;">
<a :href="req.docUrl" target="_blank">
Documentation
</a>
</div>
<div> <div>
<div class="parameter" v-for="parameter in req.parameters"> <div class="parameter" v-for="parameter in req.parameters">
<div class="name"> <div class="name">
@ -536,9 +416,6 @@
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn @click="testSourceRemotely(req)">
Test Android
</v-btn>
<v-btn @click="testSource(req)"> <v-btn @click="testSource(req)">
Test Test
</v-btn> </v-btn>
@ -620,62 +497,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>
@ -713,7 +535,6 @@
<!--<script src="./dependencies/vue.js"></script>--> <!--<script src="./dependencies/vue.js"></script>-->
<!--<script src="./dependencies/vuetify.js"></script>--> <!--<script src="./dependencies/vuetify.js"></script>-->
<script src="./source_docs.js"></script> <script src="./source_docs.js"></script>
<script src="./source_doc_urls.js"></script>
<script src="./source.js"></script> <script src="./source.js"></script>
<script src="./dev_bridge.js"></script> <script src="./dev_bridge.js"></script>
<script> <script>
@ -724,7 +545,6 @@
new Vue({ new Vue({
el: '#app', el: '#app',
data: { data: {
searchTestMethods: "",
page: "Plugin", page: "Plugin",
pastPluginUrls: [], pastPluginUrls: [],
settings: {}, settings: {},
@ -732,9 +552,7 @@
lastLogIndex: -1, lastLogIndex: -1,
lastLogDevID: "", lastLogDevID: "",
logs: [], logs: [],
httpExchanges: [], lastInjectTime: ""
lastInjectTime: "",
showHttpRequests: false
}, },
Plugin: { Plugin: {
loadUsingTag: false, loadUsingTag: false,
@ -752,9 +570,6 @@
Testing: { Testing: {
requests: sourceDocs.map(x=>{ requests: sourceDocs.map(x=>{
x.parameters.forEach(y=>y.value = null); x.parameters.forEach(y=>y.value = null);
if(sourceDocUrls[x.title])
x.docUrl = sourceDocUrls[x.title];
return x; return x;
}), }),
lastResult: "", lastResult: "",
@ -818,16 +633,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 +674,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(()=>{
@ -1061,58 +860,8 @@
"Error: " + ex; "Error: " + ex;
} }
}, },
testSourceRemotely(req) {
const name = req.title;
const parameterVals = req.parameters.map(x=>{
if(x.value && x.value.startsWith && x.value.startsWith("json:"))
return JSON.parse(x.value.substring(5));
return x.value
});
if(name == "enable") {
if(parameterVals.length > 0)
parameterVals[0] = this.Plugin.currentPlugin;
else
parameterVals.push(this.Plugin.currentPlugin);
if(parameterVals.length > 1)
parameterVals[1] = __DEV_SETTINGS;
else
parameterVals.push(__DEV_SETTINGS);
}
const func = source[name];
if(!func)
alert("Test func not found");
try {
const remoteResult = pluginRemoteTest(name, parameterVals);
console.log("Result for " + req.title, remoteResult);
this.Testing.lastResult = "//Results [" + name + "]\n" +
JSON.stringify(remoteResult, null, 3);
this.Testing.lastResultError = "";
}
catch(ex) {
if(ex.plugin_type == "CaptchaRequiredException") {
let shouldCaptcha = confirm("Do you want to request captcha?");
if(shouldCaptcha) {
pluginCaptchaTestPlugin(ex.url, ex.body);
}
}
console.error("Failed to run test for " + req.title, ex);
this.Testing.lastResult = ""
if(ex.message)
this.Testing.lastResultError = "//Results [" + name + "]\n\n" +
"Error: " + ex.message + "\n\n" + ex.stack;
else
this.Testing.lastResultError = "//Results [" + name + "]\n\n" +
"Error: " + ex;
}
},
showTestResults(results) { showTestResults(results) {
},
toggleHttpExchange(exchange) {
exchange.response.show = !exchange.response.show;
}, },
copyClipboard(cpy) { copyClipboard(cpy) {
if(navigator.clipboard) if(navigator.clipboard)

View file

@ -1,37 +1,13 @@
declare class ScriptException extends Error { declare class ScriptException extends Error {
//If only one parameter is provided, acts as msg
constructor(type: string, msg: string); constructor(type: string, msg: string);
} }
declare class LoginRequiredException extends ScriptException {
constructor(msg: string);
}
//Alias
declare class ScriptLoginRequiredException extends ScriptException {
constructor(msg: string);
}
declare class CaptchaRequiredException extends ScriptException {
constructor(url: string, body: string);
}
declare class CriticalException extends ScriptException {
constructor(msg: string);
}
declare class UnavailableException extends ScriptException {
constructor(msg: string);
}
declare class AgeException extends ScriptException {
constructor(msg: string);
}
declare class TimeoutException extends ScriptException { declare class TimeoutException extends ScriptException {
constructor(msg: string); constructor(msg: string);
} }
declare class UnavailableException extends ScriptException {
constructor(msg: string);
}
declare class ScriptImplementationException extends ScriptException { declare class ScriptImplementationException extends ScriptException {
constructor(msg: string); constructor(msg: string);
} }
@ -62,23 +38,16 @@ declare class FilterCapability {
declare class PlatformAuthorLink { declare class PlatformAuthorLink {
constructor(id: PlatformID, name: string, url: string, thumbnail: string, subscribers: integer?, membershipUrl: string?); constructor(id: PlatformID, name: string, url: string, thumbnail: string, subscribers: integer?);
}
declare class PlatformAuthorMembershipLink {
constructor(id: PlatformID, name: string, url: string, thumbnail: string, subscribers: integer?, membershipUrl: string?);
} }
declare interface PlatformContentDef { declare interface PlatformContentDef {
id: PlatformID, id: PlatformID,
name: string, name: string,
thumbnails: Thumbnails,
author: PlatformAuthorLink, author: PlatformAuthorLink,
datetime: integer, datetime: integer,
url: string url: string
} }
declare interface PlatformContent {}
declare interface PlatformNestedMediaContentDef extends PlatformContentDef { declare interface PlatformNestedMediaContentDef extends PlatformContentDef {
contentUrl: string, contentUrl: string,
contentName: string?, contentName: string?,
@ -90,26 +59,16 @@ declare class PlatformNestedMediaContent {
constructor(obj: PlatformNestedMediaContentDef); constructor(obj: PlatformNestedMediaContentDef);
} }
declare interface PlatformLockedContentDef extends PlatformContentDef {
contentName: string?,
contentThumbnails: Thumbnails?,
unlockUrl: string,
lockDescription: string?,
}
declare class PlatformLockedContent {
constructor(obj: PlatformLockedContentDef);
}
declare interface PlatformVideoDef extends PlatformContentDef { declare interface PlatformVideoDef extends PlatformContentDef {
thumbnails: Thumbnails, thumbnails: Thumbnails,
author: PlatformAuthorLink, author: PlatformAuthorLink,
duration: int, duration: int,
viewCount: long, viewCount: long,
isLive: boolean, isLive: boolean
shareUrl: string?
} }
declare interface PlatformContent {}
declare class PlatformVideo implements PlatformContent { declare class PlatformVideo implements PlatformContent {
constructor(obj: PlatformVideoDef); constructor(obj: PlatformVideoDef);
} }
@ -118,16 +77,15 @@ declare class PlatformVideo implements PlatformContent {
declare interface PlatformVideoDetailsDef extends PlatformVideoDef { declare interface PlatformVideoDetailsDef extends PlatformVideoDef {
description: string, description: string,
video: VideoSourceDescriptor, video: VideoSourceDescriptor,
live: IVideoSource, live: SubtitleSource[],
rating: IRating, rating: IRating
subtitles: SubtitleSource[]
} }
declare class PlatformVideoDetails extends PlatformVideo { declare class PlatformVideoDetails extends PlatformVideo {
constructor(obj: PlatformVideoDetailsDef); constructor(obj: PlatformVideoDetailsDef);
} }
declare interface PlatformPostDef extends PlatformContentDef { declare class PlatformPostDef extends PlatformContentDef {
thumbnails: Thumbnails[], thumbnails: string[],
images: string[], images: string[],
description: string description: string
} }
@ -135,7 +93,7 @@ declare class PlatformPost extends PlatformContent {
constructor(obj: PlatformPostDef) constructor(obj: PlatformPostDef)
} }
declare interface PlatformPostDetailsDef extends PlatformPostDef { declare class PlatformPostDetailsDef extends PlatformPostDef {
rating: IRating, rating: IRating,
textType: int, textType: int,
content: String content: String
@ -152,8 +110,8 @@ declare interface MuxVideoSourceDescriptorDef {
isUnMuxed: boolean, isUnMuxed: boolean,
videoSources: VideoSource[] videoSources: VideoSource[]
} }
declare class VideoSourceDescriptor implements IVideoSourceDescriptor { declare class MuxVideoSourceDescriptor implements IVideoSourceDescriptor {
constructor(videoSourcesOrObj: VideoSource[]); constructor(obj: VideoSourceDescriptorDef);
} }
declare interface UnMuxVideoSourceDescriptorDef { declare interface UnMuxVideoSourceDescriptorDef {
@ -171,7 +129,7 @@ declare interface IVideoSource {
declare interface IAudioSource { declare interface IAudioSource {
} }
declare interface VideoUrlSourceDef implements IVideoSource { interface VideoUrlSourceDef implements IVideoSource {
width: integer, width: integer,
height: integer, height: integer,
container: string, container: string,
@ -181,22 +139,22 @@ declare interface VideoUrlSourceDef implements IVideoSource {
duration: integer, duration: integer,
url: string url: string
} }
declare class VideoUrlSource { class VideoUrlSource {
constructor(obj: VideoUrlSourceDef); constructor(obj: VideoUrlSourceDef);
getRequestModifier(): RequestModifier?; getRequestModifier(): RequestModifier?;
} }
declare interface VideoUrlRangeSourceDef extends VideoUrlSource { interface VideoUrlRangeSourceDef extends VideoUrlSource {
itagId: integer, itagId: integer,
initStart: integer, initStart: integer,
initEnd: integer, initEnd: integer,
indexStart: integer, indexStart: integer,
indexEnd: integer, indexEnd: integer,
} }
declare class VideoUrlRangeSource extends VideoUrlSource { class VideoUrlRangeSource extends VideoUrlSource {
constructor(obj: YTVideoSourceDef); constructor(obj: YTVideoSourceDef);
} }
declare interface AudioUrlSourceDef { interface AudioUrlSourceDef {
name: string, name: string,
bitrate: integer, bitrate: integer,
container: string, container: string,
@ -205,12 +163,24 @@ declare interface AudioUrlSourceDef {
url: string, url: string,
language: string language: string
} }
declare class AudioUrlSource implements IAudioSource { class AudioUrlSource implements IAudioSource {
constructor(obj: AudioUrlSourceDef); constructor(obj: AudioUrlSourceDef);
getRequestModifier(): RequestModifier?; getRequestModifier(): RequestModifier?;
} }
declare interface AudioUrlRangeSourceDef extends AudioUrlSource { interface IRequest {
url: string,
headers: Map<string, string>
}
interface IRequestModifierDef {
allowByteSkip: boolean
}
class RequestModifier {
constructor(obj: IRequestModifierDef) { }
modifyRequest(url: string, headers: Map<string, string>): IRequest;
}
interface AudioUrlRangeSourceDef extends AudioUrlSource {
itagId: integer, itagId: integer,
initStart: integer, initStart: integer,
initEnd: integer, initEnd: integer,
@ -218,44 +188,28 @@ declare interface AudioUrlRangeSourceDef extends AudioUrlSource {
indexEnd: integer, indexEnd: integer,
audioChannels: integer audioChannels: integer
} }
declare class AudioUrlRangeSource extends AudioUrlSource { class AudioUrlRangeSource extends AudioUrlSource {
constructor(obj: AudioUrlRangeSourceDef); constructor(obj: AudioUrlRangeSourceDef);
} }
declare interface HLSSourceDef { interface HLSSourceDef {
name: string, name: string,
duration: integer, duration: integer,
url: string, url: string
priority: boolean?,
language: string?
} }
declare class HLSSource implements IVideoSource { class HLSSource implements IVideoSource {
constructor(obj: HLSSourceDef); constructor(obj: HLSSourceDef);
} }
declare interface DashSourceDef { interface DashSourceDef {
name: string, name: string,
duration: integer, duration: integer,
url: string, url: string
language: string?
} }
declare class DashSource implements IVideoSource { class DashSource implements IVideoSource {
constructor(obj: DashSourceDef) constructor(obj: DashSourceDef)
} }
declare interface IRequest {
url: string,
headers: Map<string, string>
}
declare interface IRequestModifierDef {
allowByteSkip: boolean
}
declare class RequestModifier {
constructor(obj: IRequestModifierDef) { }
modifyRequest(url: string, headers: Map<string, string>): IRequest;
}
//Channel //Channel
declare interface PlatformChannelDef { interface PlatformChannelDef {
id: PlatformID, id: PlatformID,
name: string, name: string,
thumbnail: string, thumbnail: string,
@ -263,29 +217,12 @@ declare interface PlatformChannelDef {
subscribers: integer, subscribers: integer,
description: string, description: string,
url: string, url: string,
urlAlternatives: string[],
links: Map<string>? links: Map<string>?
} }
declare class PlatformChannel { class PlatformChannel {
constructor(obj: PlatformChannelDef); constructor(obj: PlatformChannelDef);
} }
//Playlist
declare interface PlatformPlaylistDef implements PlatformContent {
videoCount: integer,
thumbnail: string
}
declare class PlatformPlaylist extends PlatformContent {
constructor(obj: PlatformPlaylistDef);
}
declare interface PlatformPlaylistDetailsDef implements PlatformPlaylistDef {
contents: ContentPager
}
declare class PlatformPlaylistDetails extends PlatformContent {
constructor(obj: PlatformPlaylistDetailsDef);
}
//Ratings //Ratings
interface IRating { interface IRating {
type: integer type: integer
@ -313,11 +250,7 @@ declare class PlatformComment {
constructor(obj: CommentDef); constructor(obj: CommentDef);
} }
declare class PlaybackTracker {
constructor(interval: integer);
setProgress(seconds: integer);
}
declare class LiveEventPager { declare class LiveEventPager {
nextRequest = 4000; nextRequest = 4000;
@ -328,8 +261,8 @@ declare class LiveEventPager {
nextPage(): LiveEventPager; //Could be self nextPage(): LiveEventPager; //Could be self
} }
declare class LiveEvent { class LiveEvent {
constructor(type: integer); type: String
} }
declare class LiveEventComment extends LiveEvent { declare class LiveEventComment extends LiveEvent {
constructor(name: string, message: string, thumbnail: string?, colorName: string?, badges: string[]); constructor(name: string, message: string, thumbnail: string?, colorName: string?, badges: string[]);
@ -354,31 +287,25 @@ declare class ContentPager {
constructor(results: PlatformContent[], hasMore: boolean); constructor(results: PlatformContent[], hasMore: boolean);
hasMorePagers(): boolean hasMorePagers(): boolean
nextPage(): ContentPager?; //Could be self nextPage(): VideoPager; //Could be self
} }
declare class VideoPager { declare class VideoPager {
constructor(results: PlatformVideo[], hasMore: boolean); constructor(results: PlatformVideo[], hasMore: boolean);
hasMorePagers(): boolean hasMorePagers(): boolean
nextPage(): VideoPager?; //Could be self nextPage(): VideoPager; //Could be self
} }
declare class ChannelPager { declare class ChannelPager {
constructor(results: PlatformChannel[], hasMore: boolean); constructor(results: PlatformChannel[], hasMore: boolean);
hasMorePagers(): boolean; hasMorePagers(): boolean;
nextPage(): ChannelPager?; //Could be self nextPage(): ChannelPager; //Could be self
}
declare class PlaylistPager {
constructor(results: PlatformPlaylist[], hasMore: boolean);
hasMorePagers(): boolean;
nextPage(): PlaylistPager?;
} }
declare class CommentPager { declare class CommentPager {
constructor(results: PlatformComment[], hasMore: boolean); constructor(results: PlatformComment[], hasMore: boolean);
hasMorePagers(): boolean hasMorePagers(): boolean
nextPage(): CommentPager?; //Could be self nextPage(): CommentPager; //Could be self
} }
interface Map<T> { interface Map<T> {
@ -414,9 +341,8 @@ interface Source {
getChannelCapabilities(): ResultCapabilities; getChannelCapabilities(): ResultCapabilities;
isContentDetailsUrl(url: string): boolean; isContentDetailsUrl(url: string): boolean;
getContentDetails(url: string): PlatformContentDetails; getContentDetails(url: string): PlatformVideoDetails;
//Optional
getLiveEvents(url: string): LiveEventPager; getLiveEvents(url: string): LiveEventPager;
//Optional //Optional

File diff suppressed because one or more lines are too long

View file

@ -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"
@ -38,26 +37,24 @@ let Type = {
NORMAL: 0, NORMAL: 0,
SKIPPABLE: 5, SKIPPABLE: 5,
SKIP: 6, SKIP: 6
SKIPONCE: 7
} }
}; };
let Language = { let Language = {
UNKNOWN: "Unknown", UNKNOWN: "Unknown",
ARABIC: "ar", ARABIC: "Arabic",
SPANISH: "es", SPANISH: "Spanish",
FRENCH: "fr", FRENCH: "French",
HINDI: "hi", HINDI: "Hindi",
INDONESIAN: "id", INDONESIAN: "Indonesian",
KOREAN: "ko", KOREAN: "Korean",
PORTUGUESE: "pt", PORTBRAZIL: "Portuguese Brazilian",
PORTBRAZIL: "pt", RUSSIAN: "Russian",
RUSSIAN: "ru", THAI: "Thai",
THAI: "th", TURKISH: "Turkish",
TURKISH: "tr", VIETNAMESE: "Vietnamese",
VIETNAMESE: "vi", ENGLISH: "English"
ENGLISH: "en"
} }
class ScriptException extends Error { class ScriptException extends Error {
@ -79,11 +76,6 @@ class ScriptLoginRequiredException extends ScriptException {
super("ScriptLoginRequiredException", msg); super("ScriptLoginRequiredException", msg);
} }
} }
class LoginRequiredException extends ScriptException {
constructor(msg) {
super("ScriptLoginRequiredException", msg);
}
}
class CaptchaRequiredException extends Error { class CaptchaRequiredException extends Error {
constructor(url, body) { constructor(url, body) {
super(JSON.stringify({ 'plugin_type': 'CaptchaRequiredException', url, body })); super(JSON.stringify({ 'plugin_type': 'CaptchaRequiredException', url, body }));
@ -202,7 +194,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 +237,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 {
@ -256,17 +247,12 @@ class PlatformVideoDetails extends PlatformVideo {
this.description = obj.description ?? "";//String this.description = obj.description ?? "";//String
this.video = obj.video ?? {}; //VideoSourceDescriptor this.video = obj.video ?? {}; //VideoSourceDescriptor
this.dash = obj.dash ?? null; //DashSource, deprecated this.dash = obj.dash ?? null; //DashSource
this.hls = obj.hls ?? null; //HLSSource, deprecated this.hls = obj.hls ?? null; //HLSSource
this.live = obj.live ?? null; //VideoSource this.live = obj.live ?? null; //VideoSource
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 +271,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) {
@ -370,18 +319,6 @@ class VideoUrlSource {
this.bitrate = obj.bitrate ?? 0; this.bitrate = obj.bitrate ?? 0;
this.duration = obj.duration ?? 0; this.duration = obj.duration ?? 0;
this.url = obj.url; this.url = obj.url;
if(obj.requestModifier)
this.requestModifier = obj.requestModifier;
}
}
class 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 {
@ -407,35 +344,6 @@ class AudioUrlSource {
this.duration = obj.duration ?? 0; this.duration = obj.duration ?? 0;
this.url = obj.url; this.url = obj.url;
this.language = obj.language ?? Language.UNKNOWN; this.language = obj.language ?? Language.UNKNOWN;
if(obj.requestModifier)
this.requestModifier = obj.requestModifier;
}
}
class AudioUrlWidevineSource extends AudioUrlSource {
constructor(obj) {
super(obj);
this.plugin_type = "AudioUrlWidevineSource";
this.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 {
@ -461,8 +369,6 @@ class HLSSource {
this.priority = obj.priority ?? false; this.priority = obj.priority ?? false;
if(obj.language) if(obj.language)
this.language = obj.language; this.language = obj.language;
if(obj.requestModifier)
this.requestModifier = obj.requestModifier;
} }
} }
class DashSource { class DashSource {
@ -474,58 +380,13 @@ class DashSource {
this.url = obj.url; this.url = obj.url;
if(obj.language) if(obj.language)
this.language = obj.language; this.language = obj.language;
if(obj.requestModifier)
this.requestModifier = obj.requestModifier;
} }
} }
class 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) {
obj = obj ?? {}; obj = obj ?? {};
this.allowByteSkip = obj.allowByteSkip; //Kinda deprecated.. wip this.allowByteSkip = obj.allowByteSkip;
} }
} }
@ -551,7 +412,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 +738,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;
};

View file

@ -1,31 +1,15 @@
import androidx.annotation.OptIn package com.futo.platformplayer
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.DefaultHttpDataSource
import androidx.media3.datasource.HttpDataSource
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
import com.futo.platformplayer.helpers.VideoHelper import com.futo.platformplayer.helpers.VideoHelper
import com.futo.platformplayer.views.video.datasources.JSHttpDataSource
fun IPlatformVideoDetails.isDownloadable(): Boolean = VideoHelper.isDownloadable(this); fun IPlatformVideoDetails.isDownloadable(): Boolean = VideoHelper.isDownloadable(this);
fun IVideoSource.isDownloadable(): Boolean = VideoHelper.isDownloadable(this); fun IVideoSource.isDownloadable(): Boolean = VideoHelper.isDownloadable(this);
fun IAudioSource.isDownloadable(): Boolean = VideoHelper.isDownloadable(this); fun IAudioSource.isDownloadable(): Boolean = VideoHelper.isDownloadable(this);
@UnstableApi
fun JSSource.getHttpDataSourceFactory(): HttpDataSource.Factory {
val requestModifier = getRequestModifier();
val requestExecutor = getRequestExecutor();
return if (requestExecutor != null) {
JSHttpDataSource.Factory().setRequestExecutor(requestExecutor);
} else if (requestModifier != null) {
JSHttpDataSource.Factory().setRequestModifier(requestModifier);
} else {
DefaultHttpDataSource.Factory();
}
}
fun IVideoSourceDescriptor.hasAnySource(): Boolean = this.videoSources.any() || (this is VideoUnMuxedSourceDescriptor && this.audioSources.any()); fun IVideoSourceDescriptor.hasAnySource(): Boolean = this.videoSources.any() || (this is VideoUnMuxedSourceDescriptor && this.audioSources.any());

File diff suppressed because one or more lines are too long

View file

@ -1,16 +1,15 @@
package com.futo.platformplayer package com.futo.platformplayer
import android.util.Log
import com.google.common.base.CharMatcher import com.google.common.base.CharMatcher
import java.io.ByteArrayOutputStream import java.io.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
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.charset.Charset
private const val IPV4_PART_COUNT = 4; private const val IPV4_PART_COUNT = 4;
@ -170,7 +169,7 @@ private fun parseHextet(ipString: String, start: Int, end: Int): Short {
var hextet = 0 var hextet = 0
for (i in start until end) { for (i in start until end) {
hextet = hextet shl 4 hextet = hextet shl 4
hextet = hextet or ipString[i].digitToIntOrNull(16)!! hextet = hextet or ipString[i].digitToIntOrNull(16)!! ?: -1
} }
return hextet.toShort() return hextet.toShort()
} }
@ -216,26 +215,16 @@ 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 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;
} }
if (addresses.size == 1) { if (addresses.size == 1) {
val socket = Socket()
try { try {
return socket.apply { this.connect(InetSocketAddress(addresses[0], port), timeout) } return Socket(addresses[0], port);
} catch (e: Throwable) { } catch (e: Throwable) {
Log.i("getConnectedSocket", "Failed to connect to: ${addresses[0]}", e) //Ignored.
socket.close()
} }
return null; return null;
@ -260,7 +249,7 @@ fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int): Socket?
} }
} }
socket.connect(InetSocketAddress(address, port), timeout); socket.connect(InetSocketAddress(address, port));
synchronized(syncObject) { synchronized(syncObject) {
if (connectedSocket == null) { if (connectedSocket == null) {
@ -274,7 +263,7 @@ fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int): Socket?
} }
} }
} catch (e: Throwable) { } catch (e: Throwable) {
Log.i("getConnectedSocket", "Failed to connect to: $address", e) //Ignore
} }
}; };

View file

@ -4,10 +4,8 @@ import com.futo.platformplayer.logging.Logger
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.platformplayer.views.adapters.CommentViewHolder
import com.futo.polycentric.core.ProcessHandle import com.futo.polycentric.core.ProcessHandle
import com.futo.polycentric.core.Store
import com.futo.polycentric.core.SystemState
import com.futo.polycentric.core.base64UrlToByteArray
import userpackage.Protocol import userpackage.Protocol
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.min import kotlin.math.min
@ -40,25 +38,27 @@ 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) })
} }
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 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)
}
} }

View file

@ -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
@ -28,18 +25,4 @@ 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")
}
}
} }

View file

@ -27,7 +27,7 @@ fun <R> V8Value?.orDefault(default: R, handler: (V8Value)->R): R {
inline fun <reified T> V8Value.expectOrThrow(config: IV8PluginConfig, contextName: String): T { inline fun <reified T> V8Value.expectOrThrow(config: IV8PluginConfig, contextName: String): T {
if(this !is T) if(this !is T)
throw ScriptImplementationException(config, "Expected ${contextName} to be of type ${T::class.simpleName}, but found ${this::class.simpleName}"); throw ScriptImplementationException(config, "Expected ${contextName} to be of type ${T::class.simpleName}, but found ${this::class.simpleName}");
return this; return this as T;
} }
//Singles //Singles

View file

@ -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()
}
}

View file

@ -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()
}
}

View file

@ -1,20 +0,0 @@
package com.futo.platformplayer
class PresetImages {
companion object {
val images = mapOf<String, Int>(
Pair("xp_book", R.drawable.xp_book),
Pair("xp_forest", R.drawable.xp_forest),
Pair("xp_code", R.drawable.xp_code),
Pair("xp_controller", R.drawable.xp_controller),
Pair("xp_laptop", R.drawable.xp_laptop)
);
fun getPresetResIdByName(name: String): Int {
return images[name] ?: -1;
}
fun getPresetNameByResId(id: Int): String? {
return images.entries.find { it.value == id }?.key;
}
}
}

View file

@ -6,45 +6,31 @@ import android.content.Intent
import android.net.Uri import android.net.Uri
import android.webkit.CookieManager import android.webkit.CookieManager
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.activities.MainActivity import com.futo.platformplayer.activities.*
import com.futo.platformplayer.activities.ManageTabsActivity
import com.futo.platformplayer.activities.PolycentricHomeActivity
import com.futo.platformplayer.activities.PolycentricProfileActivity
import com.futo.platformplayer.activities.SettingsActivity
import com.futo.platformplayer.activities.SyncHomeActivity
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.serializers.FlexibleBooleanSerializer import com.futo.platformplayer.serializers.FlexibleBooleanSerializer
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
import com.futo.platformplayer.states.StateAnnouncement import com.futo.platformplayer.states.*
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateBackup
import com.futo.platformplayer.states.StateCache
import com.futo.platformplayer.states.StateMeta
import com.futo.platformplayer.states.StatePayment
import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.states.StateUpdate
import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.FragmentedStorageFileJson import com.futo.platformplayer.stores.FragmentedStorageFileJson
import com.futo.platformplayer.views.FeedStyle import com.futo.platformplayer.views.FeedStyle
import com.futo.platformplayer.views.fields.DropdownFieldOptionsId import com.futo.platformplayer.views.fields.DropdownFieldOptionsId
import com.futo.platformplayer.views.fields.FieldForm
import com.futo.platformplayer.views.fields.FormField import com.futo.platformplayer.views.fields.FormField
import com.futo.platformplayer.views.fields.FieldForm
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
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable import kotlinx.serialization.*
import kotlinx.serialization.Transient import kotlinx.serialization.json.*
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.io.File import java.io.File
import java.time.OffsetDateTime import java.time.OffsetDateTime
@Serializable @Serializable
data class MenuBottomBarSetting(val id: Int, var enabled: Boolean); data class MenuBottomBarSetting(val id: Int, var enabled: Boolean);
@ -58,16 +44,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 +60,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 +70,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 +102,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 +116,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 +135,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 +165,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 +176,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 +214,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)
@ -296,26 +248,20 @@ class Settings : FragmentedStorageFileJson() {
return FeedStyle.THUMBNAIL; return FeedStyle.THUMBNAIL;
} }
@FormField(R.string.show_subscription_group, FieldForm.TOGGLE, R.string.show_subscription_group_description, 5) @FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 5)
var showSubscriptionGroups: Boolean = true;
@FormField(R.string.use_subscription_exchange, FieldForm.TOGGLE, R.string.use_subscription_exchange_description, 6)
var useSubscriptionExchange: Boolean = false;
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
var previewFeedItems: Boolean = true; var previewFeedItems: Boolean = true;
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 7) @FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
var progressBar: Boolean = true; var progressBar: Boolean = true;
@FormField(R.string.fetch_on_app_boot, FieldForm.TOGGLE, R.string.shortly_after_opening_the_app_start_fetching_subscriptions, 8) @FormField(R.string.fetch_on_app_boot, FieldForm.TOGGLE, R.string.shortly_after_opening_the_app_start_fetching_subscriptions, 7)
@Serializable(with = FlexibleBooleanSerializer::class) @Serializable(with = FlexibleBooleanSerializer::class)
var fetchOnAppBoot: Boolean = true; var fetchOnAppBoot: Boolean = true;
@FormField(R.string.fetch_on_tab_opened, FieldForm.TOGGLE, R.string.fetch_on_tab_opened_description, 9) @FormField(R.string.fetch_on_tab_opened, FieldForm.TOGGLE, R.string.fetch_on_tab_opened_description, 8)
var fetchOnTabOpen: Boolean = true; var fetchOnTabOpen: Boolean = true;
@FormField(R.string.background_update, FieldForm.DROPDOWN, R.string.experimental_background_update_for_subscriptions_cache, 10, "background_update") @FormField(R.string.background_update, FieldForm.DROPDOWN, R.string.experimental_background_update_for_subscriptions_cache, 9)
@DropdownFieldOptionsId(R.array.background_interval) @DropdownFieldOptionsId(R.array.background_interval)
var subscriptionsBackgroundUpdateInterval: Int = 0; var subscriptionsBackgroundUpdateInterval: Int = 0;
@ -331,7 +277,7 @@ class Settings : FragmentedStorageFileJson() {
}; };
@FormField(R.string.subscription_concurrency, FieldForm.DROPDOWN, R.string.specify_how_many_threads_are_used_to_fetch_channels, 11) @FormField(R.string.subscription_concurrency, FieldForm.DROPDOWN, R.string.specify_how_many_threads_are_used_to_fetch_channels, 10)
@DropdownFieldOptionsId(R.array.thread_count) @DropdownFieldOptionsId(R.array.thread_count)
var subscriptionConcurrency: Int = 3; var subscriptionConcurrency: Int = 3;
@ -339,20 +285,17 @@ class Settings : FragmentedStorageFileJson() {
return threadIndexToCount(subscriptionConcurrency); return threadIndexToCount(subscriptionConcurrency);
} }
@FormField(R.string.show_watch_metrics, FieldForm.TOGGLE, R.string.show_watch_metrics_description, 12) @FormField(R.string.show_watch_metrics, FieldForm.TOGGLE, R.string.show_watch_metrics_description, 11)
var showWatchMetrics: Boolean = false; var showWatchMetrics: Boolean = false;
@FormField(R.string.track_playtime_locally, FieldForm.TOGGLE, R.string.track_playtime_locally_description, 13) @FormField(R.string.track_playtime_locally, FieldForm.TOGGLE, R.string.track_playtime_locally_description, 12)
var allowPlaytimeTracking: Boolean = true; var allowPlaytimeTracking: Boolean = true;
@FormField(R.string.always_reload_from_cache, FieldForm.TOGGLE, R.string.always_reload_from_cache_description, 14) @FormField(R.string.always_reload_from_cache, FieldForm.TOGGLE, R.string.always_reload_from_cache_description, 13)
var alwaysReloadFromCache: Boolean = false; var alwaysReloadFromCache: Boolean = false;
@FormField(R.string.peek_channel_contents, FieldForm.TOGGLE, R.string.peek_channel_contents_description, 15) @FormField(R.string.clear_channel_cache, FieldForm.BUTTON, R.string.clear_channel_cache_description, 14)
var peekChannelContents: Boolean = false;
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON, R.string.clear_channel_cache_description, 16)
fun clearChannelCache() { fun clearChannelCache() {
UIDialogs.toast(SettingsActivity.getActivity()!!, "Started clearing.."); UIDialogs.toast(SettingsActivity.getActivity()!!, "Started clearing..");
StateCache.instance.clear(); StateCache.instance.clear();
@ -364,36 +307,13 @@ 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;
fun getPrimaryLanguage(context: Context): String? { fun getPrimaryLanguage(context: Context) = context.resources.getStringArray(R.array.audio_languages)[primaryLanguage];
return when(primaryLanguage) {
0 -> "en";
1 -> "es";
2 -> "de";
3 -> "fr";
4 -> "ja";
5 -> "ko";
6 -> "th";
7 -> "vi";
8 -> "id";
9 -> "hi";
10 -> "ar";
11 -> "tu";
12 -> "ru";
13 -> "pt";
14 -> "zh";
else -> null
}
}
@FormField(R.string.prefer_original_audio, FieldForm.TOGGLE, R.string.prefer_original_audio_description, -1)
var preferOriginalAudio: Boolean = true;
//= context.resources.getStringArray(R.array.audio_languages)[primaryLanguage]; @FormField(R.string.default_playback_speed, FieldForm.DROPDOWN, -1, 1)
@FormField(R.string.default_playback_speed, FieldForm.DROPDOWN, -1, 0)
@DropdownFieldOptionsId(R.array.playback_speeds) @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 +329,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;
@ -479,47 +407,18 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.restart_after_connectivity_loss, FieldForm.DROPDOWN, R.string.restart_playback_when_gaining_connectivity_after_a_loss, 12) @FormField(R.string.restart_after_connectivity_loss, FieldForm.DROPDOWN, R.string.restart_playback_when_gaining_connectivity_after_a_loss, 12)
@DropdownFieldOptionsId(R.array.restart_playback_after_loss) @DropdownFieldOptionsId(R.array.restart_playback_after_loss)
var restartPlaybackAfterConnectivityLoss: Int = 1; var restartPlaybackAfterConnectivityLoss: Int = 1;
@FormField(R.string.full_screen_portrait, FieldForm.TOGGLE, R.string.allow_full_screen_portrait, 13)
var fullscreenPortrait: Boolean = false;
@FormField(R.string.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 +467,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 +482,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)
@ -616,8 +506,6 @@ class Settings : FragmentedStorageFileJson() {
@DropdownFieldOptionsId(R.array.log_levels) @DropdownFieldOptionsId(R.array.log_levels)
var logLevel: Int = 0; var logLevel: Int = 0;
fun isVerbose() = logLevel >= 4;
@FormField(R.string.submit_logs, FieldForm.BUTTON, R.string.submit_logs_to_help_us_narrow_down_issues, 1) @FormField(R.string.submit_logs, FieldForm.BUTTON, R.string.submit_logs_to_help_us_narrow_down_issues, 1)
fun submitLogs() { fun submitLogs() {
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
@ -659,9 +547,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;
@ -760,9 +645,7 @@ class Settings : FragmentedStorageFileJson() {
fun manualCheck() { fun manualCheck() {
if (!BuildConfig.IS_PLAYSTORE_BUILD) { if (!BuildConfig.IS_PLAYSTORE_BUILD) {
SettingsActivity.getActivity()?.let { SettingsActivity.getActivity()?.let {
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { StateUpdate.instance.checkForUpdates(it, true);
StateUpdate.instance.checkForUpdates(it, true)
}
} }
} else { } else {
SettingsActivity.getActivity()?.let { SettingsActivity.getActivity()?.let {
@ -845,10 +728,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 +747,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,74 +759,15 @@ 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.info, FieldForm.GROUP, -1, 19)
var gestureControls = GestureControls();
@Serializable
class GestureControls {
@FormField(R.string.volume_slider, FieldForm.TOGGLE, R.string.volume_slider_descr, 1)
var volumeSlider: Boolean = true;
@FormField(R.string.brightness_slider, FieldForm.TOGGLE, R.string.brightness_slider_descr, 2)
var brightnessSlider: Boolean = true;
@FormField(R.string.toggle_full_screen, FieldForm.TOGGLE, R.string.toggle_full_screen_descr, 3)
var toggleFullscreen: Boolean = true;
@FormField(R.string.system_brightness, FieldForm.TOGGLE, R.string.system_brightness_descr, 4)
var useSystemBrightness: Boolean = false;
@FormField(R.string.system_volume, FieldForm.TOGGLE, R.string.system_volume_descr, 5)
var useSystemVolume: Boolean = true;
@FormField(R.string.restore_system_brightness, FieldForm.TOGGLE, R.string.restore_system_brightness_descr, 6)
var restoreSystemBrightness: Boolean = true;
@FormField(R.string.zoom_option, FieldForm.TOGGLE, R.string.zoom_option_descr, 7)
var zoom: Boolean = true;
@FormField(R.string.pan_option, FieldForm.TOGGLE, R.string.pan_option_descr, 8)
var pan: Boolean = true;
}
@FormField(R.string.synchronization, FieldForm.GROUP, -1, 20)
var synchronization = Synchronization();
@Serializable
class Synchronization {
@FormField(R.string.enabled, FieldForm.TOGGLE, R.string.enabled_description, 1)
var enabled: Boolean = 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 {

View file

@ -1,15 +1,19 @@
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.lifecycle.lifecycleScope
import androidx.work.Constraints
import androidx.work.Data import androidx.work.Data
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.PeriodicWorkRequest
import androidx.work.WorkManager import androidx.work.WorkManager
import androidx.work.WorkerParameters
import com.caoccao.javet.values.primitive.V8ValueInteger import com.caoccao.javet.values.primitive.V8ValueInteger
import com.caoccao.javet.values.primitive.V8ValueString import com.caoccao.javet.values.primitive.V8ValueString
import com.futo.platformplayer.activities.DeveloperActivity import com.futo.platformplayer.activities.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
@ -32,20 +36,19 @@ import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StateSubscriptions import com.futo.platformplayer.states.StateSubscriptions
import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.FragmentedStorageFileJson import com.futo.platformplayer.stores.FragmentedStorageFileJson
import com.futo.platformplayer.stores.db.types.DBHistory
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
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.Contextual import kotlinx.serialization.*
import kotlinx.serialization.Serializable import kotlinx.serialization.json.*
import kotlinx.serialization.Transient
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.time.OffsetDateTime import java.time.OffsetDateTime
import java.util.UUID
import java.util.concurrent.TimeUnit
import java.util.stream.IntStream.range import java.util.stream.IntStream.range
import kotlin.system.measureTimeMillis import kotlin.system.measureTimeMillis
@ -106,14 +109,14 @@ class SettingsDev : FragmentedStorageFileJson() {
StateApp.instance.scope.launch(Dispatchers.IO) { StateApp.instance.scope.launch(Dispatchers.IO) {
try { try {
val subsCache = val subsCache =
StateSubscriptions.instance.getSubscriptionsFeedWithExceptions(cacheScope = this).first; StateSubscriptions.instance.getSubscriptionsFeedWithExceptions(cacheScope = this)?.first;
var total = 0; var total = 0;
var page = 0; var page = 0;
var lastToast = System.currentTimeMillis(); var lastToast = System.currentTimeMillis();
while(subsCache.hasMorePages() && total < 5000) { while(subsCache!!.hasMorePages() && total < 5000) {
subsCache.nextPage(); subsCache!!.nextPage();
total += subsCache.getResults().size; total += subsCache!!.getResults().size;
page++; page++;
if(page % 10 == 0) if(page % 10 == 0)
@ -171,9 +174,9 @@ class SettingsDev : FragmentedStorageFileJson() {
var total = 0; var total = 0;
var page = 0; var page = 0;
var lastToast = System.currentTimeMillis(); var lastToast = System.currentTimeMillis();
while(subsCache.hasMorePages() && total < 5000) { while(subsCache!!.hasMorePages() && total < 5000) {
subsCache.nextPage(); subsCache!!.nextPage();
total += subsCache.getResults().size; total += subsCache!!.getResults().size;
page++; page++;
for(item in subsCache.getResults().filterIsInstance<IPlatformVideo>()) { for(item in subsCache.getResults().filterIsInstance<IPlatformVideo>()) {
@ -236,17 +239,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)
@ -376,9 +375,9 @@ class SettingsDev : FragmentedStorageFileJson() {
@FormField(R.string.getHome, FieldForm.BUTTON, R.string.attempts_to_fetch_2_pages_from_getHome, 2) @FormField(R.string.getHome, FieldForm.BUTTON, R.string.attempts_to_fetch_2_pages_from_getHome, 2)
fun testV8Home() { fun testV8Home() {
runTestPlugin(_currentPlugin) { runTestPlugin(_currentPlugin) {
var home: IPager<IPlatformContent>?; var home: IPager<IPlatformContent>? = null;
val resultPage1: String; var resultPage1: String = "";
val resultPage2: String; var resultPage2: String = "";
val page1Time = measureTimeMillis { val page1Time = measureTimeMillis {
home = it.getHome(); home = it.getHome();
val results = home!!.getResults(); val results = home!!.getResults();
@ -497,24 +496,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 +509,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);

View file

@ -5,49 +5,23 @@ 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
import android.view.View import android.view.View
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.ImageView import android.widget.*
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.futo.platformplayer.activities.MainActivity import com.futo.platformplayer.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.*
import com.futo.platformplayer.dialogs.AutomaticBackupDialog
import com.futo.platformplayer.dialogs.AutomaticRestoreDialog
import com.futo.platformplayer.dialogs.CastingAddDialog
import com.futo.platformplayer.dialogs.CastingHelpDialog
import com.futo.platformplayer.dialogs.ChangelogDialog
import com.futo.platformplayer.dialogs.CommentDialog
import com.futo.platformplayer.dialogs.ConnectCastingDialog
import com.futo.platformplayer.dialogs.ConnectedCastingDialog
import com.futo.platformplayer.dialogs.ImportDialog
import com.futo.platformplayer.dialogs.ImportOptionsDialog
import com.futo.platformplayer.dialogs.MigrateDialog
import com.futo.platformplayer.dialogs.PluginUpdateDialog
import com.futo.platformplayer.dialogs.ProgressDialog
import com.futo.platformplayer.engine.exceptions.PluginException import com.futo.platformplayer.engine.exceptions.PluginException
import com.futo.platformplayer.fragment.mainactivity.main.MainFragment
import com.futo.platformplayer.fragment.mainactivity.main.SourceDetailFragment
import com.futo.platformplayer.fragment.mainactivity.main.VideoDetailFragment
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.ImportCache
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateBackup import com.futo.platformplayer.states.StateBackup
import com.futo.platformplayer.states.StatePlugins
import com.futo.platformplayer.stores.v2.ManagedStore import com.futo.platformplayer.stores.v2.ManagedStore
import com.futo.platformplayer.views.ToastView
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -193,58 +167,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 +225,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 +240,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 +252,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,27 +288,16 @@ class UIDialogs {
showDialog(context, R.drawable.ic_error, text, null, null, 0, cancelButtonAction, confirmButtonAction) showDialog(context, R.drawable.ic_error, text, null, null, 0, cancelButtonAction, confirmButtonAction)
} }
fun showConfirmationDialog(context: Context, text: String, action: () -> Unit, cancelAction: (() -> Unit)? = null, doNotAskAgainAction: (() -> Unit)? = null) { fun showUpdateAvailableDialog(context: Context, lastVersion: Int) {
val confirmButtonAction = Action(context.getString(R.string.confirm), action, ActionStyle.PRIMARY)
val cancelButtonAction = Action(context.getString(R.string.cancel), cancelAction ?: {}, ActionStyle.ACCENT)
val doNotAskAgain = Action(context.getString(R.string.do_not_ask_again), doNotAskAgainAction ?: {}, ActionStyle.NONE)
showDialog(context, R.drawable.ic_error, text, null, null, 0, doNotAskAgain, cancelButtonAction, confirmButtonAction)
}
fun showUpdateAvailableDialog(context: Context, lastVersion: Int, hideExceptionButtons: Boolean = false) {
val dialog = AutoUpdateDialog(context); val dialog = AutoUpdateDialog(context);
registerDialogOpened(dialog); registerDialogOpened(dialog);
dialog.setOnDismissListener { registerDialogClosed(dialog) }; dialog.setOnDismissListener { registerDialogClosed(dialog) };
dialog.show(); dialog.show();
dialog.setMaxVersion(lastVersion); dialog.setMaxVersion(lastVersion);
if (hideExceptionButtons) {
dialog.hideExceptionButtons()
}
} }
fun showChangelogDialog(context: Context, lastVersion: Int, 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();
@ -402,8 +323,8 @@ class UIDialogs {
} }
} }
fun showImportDialog(context: Context, store: ManagedStore<*>, name: String, reconstructions: List<String>, cache: ImportCache?, onConcluded: () -> Unit) { fun showImportDialog(context: Context, store: ManagedStore<*>, name: String, reconstructions: List<String>, onConcluded: () -> Unit) {
val dialog = ImportDialog(context, store, name, reconstructions, cache, onConcluded); val dialog = ImportDialog(context, store, name, reconstructions, onConcluded);
registerDialogOpened(dialog); registerDialogOpened(dialog);
dialog.setOnDismissListener { registerDialogClosed(dialog) }; dialog.setOnDismissListener { registerDialogClosed(dialog) };
dialog.show(); dialog.show();
@ -420,17 +341,11 @@ class UIDialogs {
val d = StateCasting.instance.activeDevice; val d = StateCasting.instance.activeDevice;
if (d != null) { if (d != null) {
val dialog = ConnectedCastingDialog(context); val dialog = ConnectedCastingDialog(context);
if (context is Activity) {
dialog.setOwnerActivity(context)
}
registerDialogOpened(dialog); registerDialogOpened(dialog);
dialog.setOnDismissListener { registerDialogClosed(dialog) }; dialog.setOnDismissListener { registerDialogClosed(dialog) };
dialog.show(); dialog.show();
} else { } else {
val dialog = ConnectCastingDialog(context); val dialog = ConnectCastingDialog(context);
if (context is Activity) {
dialog.setOwnerActivity(context)
}
registerDialogOpened(dialog); registerDialogOpened(dialog);
val c = context val c = context
if (c is Activity) { if (c is Activity) {
@ -462,28 +377,13 @@ class UIDialogs {
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) { StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
try { try {
StateApp.withContext { StateApp.withContext {
toast(it, text, long); Toast.makeText(it, text, if (long) Toast.LENGTH_LONG else Toast.LENGTH_SHORT).show();
} }
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.e(TAG, "Failed to show toast.", e); Logger.e(TAG, "Failed to show toast.", e);
} }
} }
} }
fun appToast(text: String, long: Boolean = false) {
appToast(ToastView.Toast(text, long))
}
fun appToastError(text: String, long: Boolean) {
StateApp.withContext {
appToast(ToastView.Toast(text, long, it.getColor(R.color.pastel_red)));
};
}
fun appToast(toast: ToastView.Toast) {
StateApp.withContext {
if(it is MainActivity) {
it.showAppToast(toast);
}
}
}
fun showClickableToast(context: Context, text: String, onClick: () -> Unit, isLongDuration: Boolean = false) { fun showClickableToast(context: Context, text: String, onClick: () -> Unit, isLongDuration: Boolean = false) {
//TODO: Is not actually clickable... //TODO: Is not actually clickable...
@ -525,13 +425,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 {

File diff suppressed because it is too large Load diff

View file

@ -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 {
@ -150,7 +143,6 @@ fun InputStream.copyToOutputStream(inputStreamLength: Long, outputStream: Output
} }
} }
@Suppress("DEPRECATION")
fun Activity.setNavigationBarColorAndIcons() { fun Activity.setNavigationBarColorAndIcons() {
window.navigationBarColor = ContextCompat.getColor(this, android.R.color.black); window.navigationBarColor = ContextCompat.getColor(this, android.R.color.black);
@ -236,92 +228,4 @@ fun String.decodeUnicode(): String {
i++ i++
} }
return sb.toString() return sb.toString()
}
fun <T> smartMerge(targetArr: List<T>, toMerge: List<T>) : List<T>{
val missingToMerge = toMerge.filter { !targetArr.contains(it) }.toList();
val newArrResult = targetArr.toMutableList();
for(missing in missingToMerge) {
val newIndex = findNewIndex(toMerge, newArrResult, missing);
newArrResult.add(newIndex, missing);
}
return newArrResult;
}
fun <T> findNewIndex(originalArr: List<T>, newArr: List<T>, item: T): Int{
var originalIndex = originalArr.indexOf(item);
var newIndex = -1;
for(i in originalIndex-1 downTo 0) {
val previousItem = originalArr[i];
val indexInNewArr = newArr.indexOfFirst { it == previousItem };
if(indexInNewArr >= 0) {
newIndex = indexInNewArr + 1;
break;
}
}
if(newIndex < 0) {
for(i in originalIndex+1 until originalArr.size) {
val previousItem = originalArr[i];
val indexInNewArr = newArr.indexOfFirst { it == previousItem };
if(indexInNewArr >= 0) {
newIndex = indexInNewArr - 1;
break;
}
}
}
if(newIndex < 0)
return originalArr.size;
else
return newIndex;
}
fun ByteBuffer.toUtf8String(): String {
val remainingBytes = ByteArray(remaining())
get(remainingBytes)
return String(remainingBytes, Charsets.UTF_8)
}
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()
} }

View file

@ -5,21 +5,13 @@ import android.content.Intent
import android.graphics.drawable.Animatable import android.graphics.drawable.Animatable
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.widget.ImageButton import android.widget.*
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.ScrollView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R import com.futo.platformplayer.*
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StatePlugins import com.futo.platformplayer.states.StatePlugins
@ -38,10 +30,8 @@ class AddSourceActivity : AppCompatActivity() {
private lateinit var _sourceHeader: SourceHeaderView; private lateinit var _sourceHeader: SourceHeaderView;
private lateinit var _sourcePermissions: LinearLayout; private lateinit var _sourcePermissions: LinearLayout;
private lateinit var _sourceWarnings: LinearLayout; private lateinit var _sourceWarnings: LinearLayout;
private lateinit var _sourceWarningsContainer: LinearLayout;
private lateinit var _container: ScrollView; private lateinit var _container: ScrollView;
private lateinit var _loader: ImageView; private lateinit var _loader: ImageView;
@ -82,7 +72,6 @@ class AddSourceActivity : AppCompatActivity() {
_sourcePermissions = findViewById(R.id.source_permissions); _sourcePermissions = findViewById(R.id.source_permissions);
_sourceWarnings = findViewById(R.id.source_warnings); _sourceWarnings = findViewById(R.id.source_warnings);
_sourceWarningsContainer = findViewById(R.id.container_source_warnings);
_container = findViewById(R.id.configContainer); _container = findViewById(R.id.configContainer);
_loader = findViewById(R.id.loader); _loader = findViewById(R.id.loader);
@ -205,32 +194,23 @@ class AddSourceActivity : AppCompatActivity() {
config.allowUrls, true) config.allowUrls, true)
) )
val pastelRed = ContextCompat.getColor(this, R.color.pastel_red); val pastelRed = resources.getColor(R.color.pastel_red);
val warnings = config.getWarnings(script); for(warning in config.getWarnings(script))
for(warning in warnings)
_sourceWarnings.addView( _sourceWarnings.addView(
SourceInfoView(this, SourceInfoView(this,
R.drawable.ic_security_pred, R.drawable.ic_security_pred,
warning.first, warning.first,
warning.second) warning.second)
.withDescriptionColor(pastelRed)); .withDescriptionColor(pastelRed));
_sourceWarningsContainer.isVisible = warnings.isNotEmpty();
setLoading(false); setLoading(false);
} }
fun install(config: SourcePluginConfig, script: String) { fun install(config: SourcePluginConfig, script: String) {
val isNew = !StatePlatform.instance.getAvailableClients().any { it.id == config.id };
StatePlugins.instance.installPlugin(this, lifecycleScope, config, script) { StatePlugins.instance.installPlugin(this, lifecycleScope, config, script) {
if(it) { if(it)
StatePlugins.instance.clearUpdateAvailable(config)
if(isNew)
lifecycleScope.launch {
StatePlatform.instance.enableClient(listOf(config.id));
}
backToSources(); backToSources();
}
} }
} }

View file

@ -10,15 +10,13 @@ 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
import com.journeyapps.barcodescanner.CaptureActivity
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 _buttonURL: BigButton; lateinit var _buttonURL: BigButton;
lateinit var _buttonPlugins: BigButton; lateinit var _buttonPlugins: BigButton;
@ -56,11 +54,9 @@ 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);
_buttonBrowse = findViewById(R.id.option_browse);
_buttonURL = findViewById(R.id.option_url); _buttonURL = findViewById(R.id.option_url);
_buttonPlugins = findViewById(R.id.option_plugins); _buttonPlugins = findViewById(R.id.option_plugins);
@ -79,30 +75,9 @@ class AddSourceOptionsActivity : AppCompatActivity() {
integrator.setCaptureActivity(QRCaptureActivity::class.java); integrator.setCaptureActivity(QRCaptureActivity::class.java);
_qrCodeResultLauncher.launch(integrator.createScanIntent()) _qrCodeResultLauncher.launch(integrator.createScanIntent())
} }
_buttonBrowse.onClick.subscribe {
startActivity(MainActivity.getTabIntent(this, "BROWSE_PLUGINS"));
}
_buttonURL.onClick.subscribe { _buttonURL.onClick.subscribe {
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)
} }
} }
} }

View file

@ -26,7 +26,7 @@ class DeveloperActivity : AppCompatActivity() {
_form = findViewById(R.id.settings_form); _form = findViewById(R.id.settings_form);
_form.fromObject(SettingsDev.instance); _form.fromObject(SettingsDev.instance);
_form.onChanged.subscribe { _, _ -> _form.onChanged.subscribe { field, value ->
_form.setObjectValues(); _form.setObjectValues();
SettingsDev.instance.save(); SettingsDev.instance.save();
}; };

View file

@ -4,19 +4,15 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.View
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.BuildConfig import com.futo.platformplayer.*
import com.futo.platformplayer.R
import com.futo.platformplayer.logging.LogLevel import com.futo.platformplayer.logging.LogLevel
import com.futo.platformplayer.logging.Logging import com.futo.platformplayer.logging.Logging
import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateUpdate
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -30,7 +26,6 @@ class ExceptionActivity : AppCompatActivity() {
private lateinit var _buttonSubmit: LinearLayout; private lateinit var _buttonSubmit: LinearLayout;
private lateinit var _buttonRestart: LinearLayout; private lateinit var _buttonRestart: LinearLayout;
private lateinit var _buttonClose: LinearLayout; private lateinit var _buttonClose: LinearLayout;
private lateinit var _buttonCheckForUpdates: LinearLayout;
private var _file: File? = null; private var _file: File? = null;
private var _submitted = false; private var _submitted = false;
@ -48,7 +43,6 @@ class ExceptionActivity : AppCompatActivity() {
_buttonSubmit = findViewById(R.id.button_submit); _buttonSubmit = findViewById(R.id.button_submit);
_buttonRestart = findViewById(R.id.button_restart); _buttonRestart = findViewById(R.id.button_restart);
_buttonClose = findViewById(R.id.button_close); _buttonClose = findViewById(R.id.button_close);
_buttonCheckForUpdates = findViewById(R.id.button_check_for_updates);
val context = intent.getStringExtra(EXTRA_CONTEXT) ?: getString(R.string.unknown_context); val context = intent.getStringExtra(EXTRA_CONTEXT) ?: getString(R.string.unknown_context);
val stack = intent.getStringExtra(EXTRA_STACK) ?: getString(R.string.something_went_wrong_missing_stack_trace); val stack = intent.getStringExtra(EXTRA_STACK) ?: getString(R.string.something_went_wrong_missing_stack_trace);
@ -87,17 +81,6 @@ class ExceptionActivity : AppCompatActivity() {
_buttonClose.setOnClickListener { _buttonClose.setOnClickListener {
finish(); finish();
}; };
if (!BuildConfig.IS_PLAYSTORE_BUILD) {
_buttonCheckForUpdates.visibility = View.VISIBLE
_buttonCheckForUpdates.setOnClickListener {
lifecycleScope.launch(Dispatchers.IO) {
StateUpdate.instance.checkForUpdates(this@ExceptionActivity, true, true)
}
}
} else {
_buttonCheckForUpdates.visibility = View.GONE
}
} }
private fun submitFile() { private fun submitFile() {

View file

@ -2,6 +2,7 @@ package com.futo.platformplayer.activities
import android.content.Intent import android.content.Intent
import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
interface IWithResultLauncher { interface IWithResultLauncher {
fun launchForResult(intent: Intent, code: Int, handler: (ActivityResult)->Unit); fun launchForResult(intent: Intent, code: Int, handler: (ActivityResult)->Unit);

View file

@ -3,23 +3,24 @@ package com.futo.platformplayer.activities
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.webkit.ConsoleMessage
import android.webkit.CookieManager import android.webkit.CookieManager
import android.webkit.WebChromeClient
import android.webkit.WebView import android.webkit.WebView
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R import com.futo.platformplayer.*
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.media.platforms.js.SourceAuth import com.futo.platformplayer.api.media.platforms.js.SourceAuth
import com.futo.platformplayer.api.media.platforms.js.SourcePluginAuthConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginAuthConfig
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.others.LoginWebViewClient import com.futo.platformplayer.others.LoginWebViewClient
import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@ -40,7 +41,6 @@ class LoginActivity : AppCompatActivity() {
_textUrl = findViewById(R.id.text_url); _textUrl = findViewById(R.id.text_url);
_buttonClose = findViewById(R.id.button_close); _buttonClose = findViewById(R.id.button_close);
_buttonClose.setOnClickListener { _buttonClose.setOnClickListener {
UIDialogs.toast("Login cancelled", false);
finish(); finish();
} }
@ -102,7 +102,7 @@ class LoginActivity : AppCompatActivity() {
override fun finish() { override fun finish() {
lifecycleScope.launch(Dispatchers.Main) { lifecycleScope.launch(Dispatchers.Main) {
_webView.loadUrl("about:blank"); _webView?.loadUrl("about:blank");
} }
_callback?.let { _callback?.let {
_callback = null; _callback = null;
@ -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;

View file

@ -55,10 +55,10 @@ class ManageTabsActivity : AppCompatActivity() {
Settings.instance.save() Settings.instance.save()
} }
val items = ArrayList(Settings.instance.tabs.mapNotNull { val items = Settings.instance.tabs.mapNotNull {
val buttonDefinition = MenuBottomBarFragment.buttonDefinitions.find { d -> it.id == d.id } ?: return@mapNotNull null val buttonDefinition = MenuBottomBarFragment.buttonDefinitions.find { d -> it.id == d.id } ?: return@mapNotNull null
TabViewHolderData(buttonDefinition, it.enabled) TabViewHolderData(buttonDefinition, it.enabled)
}); };
_listTabs = _recyclerTabs.asAny(items) { _listTabs = _recyclerTabs.asAny(items) {
it.onDragDrop.subscribe { vh -> it.onDragDrop.subscribe { vh ->

View file

@ -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 {

View file

@ -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,17 +10,16 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.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 com.futo.polycentric.core.Synchronization
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -29,7 +27,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 +43,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 +65,28 @@ 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); processHandle.addServer("https://srv1-stg.polycentric.io");
processHandle.setUsername(username);
try { StatePolycentric.instance.setProcessHandle(processHandle);
PolycentricStorage.instance.addProcessSecret(processHandle.processSecret) } catch (e: Throwable) {
} catch (e: Throwable) { Logger.e(TAG, getString(R.string.failed_to_create_profile), e);
Logger.e(TAG, "Failed to save process secret to secret storage.", e) return@launch;
} } finally {
_creating = false;
processHandle.addServer(ApiMethods.SERVER);
processHandle.setUsername(username);
StatePolycentric.instance.setProcessHandle(processHandle);
} catch (e: Throwable) {
Logger.e(TAG, getString(R.string.failed_to_create_profile), e);
return@launch;
} finally {
_creating = false;
}
try {
Logger.i(TAG, "Started backfill");
processHandle.fullyBackfillServersAnnounceExceptions();
Logger.i(TAG, "Finished backfill");
} catch (e: Throwable) {
Logger.e(TAG, getString(R.string.failed_to_fully_backfill_servers), e);
}
} }
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) {

View file

@ -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));

View file

@ -12,20 +12,14 @@ 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.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.polycentric.core.*
import com.futo.polycentric.core.ApiMethods
import com.futo.polycentric.core.KeyPair
import com.futo.polycentric.core.Process
import com.futo.polycentric.core.ProcessSecret
import com.futo.polycentric.core.SignedEvent
import com.futo.polycentric.core.Store
import com.futo.polycentric.core.base64UrlToByteArray
import com.google.zxing.integration.android.IntentIntegrator import com.google.zxing.integration.android.IntentIntegrator
import com.journeyapps.barcodescanner.CaptureActivity
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import userpackage.Protocol import userpackage.Protocol
@ -36,7 +30,6 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
private lateinit var _buttonScanProfile: LinearLayout; private lateinit var _buttonScanProfile: LinearLayout;
private lateinit var _buttonImportProfile: LinearLayout; private lateinit var _buttonImportProfile: LinearLayout;
private lateinit var _editProfile: EditText; private lateinit var _editProfile: EditText;
private lateinit var _loaderOverlay: LoaderOverlay;
private val _qrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> private val _qrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data) val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
@ -60,7 +53,6 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
_buttonHelp = findViewById(R.id.button_help); _buttonHelp = findViewById(R.id.button_help);
_buttonScanProfile = findViewById(R.id.button_scan_profile); _buttonScanProfile = findViewById(R.id.button_scan_profile);
_buttonImportProfile = findViewById(R.id.button_import_profile); _buttonImportProfile = findViewById(R.id.button_import_profile);
_loaderOverlay = findViewById(R.id.loader_overlay);
_editProfile = findViewById(R.id.edit_profile); _editProfile = findViewById(R.id.edit_profile);
findViewById<ImageButton>(R.id.button_back).setOnClickListener { findViewById<ImageButton>(R.id.button_back).setOnClickListener {
finish(); finish();
@ -103,63 +95,42 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
return; return;
} }
_loaderOverlay.show() try {
val data = url.substring("polycentric://".length).base64UrlToByteArray();
val urlInfo = Protocol.URLInfo.parseFrom(data);
if (urlInfo.urlType != 3L) {
throw Exception("Expected urlInfo struct of type ExportBundle")
}
lifecycleScope.launch(Dispatchers.IO) { val exportBundle = ExportBundle.parseFrom(urlInfo.body);
try { val keyPair = KeyPair.fromProto(exportBundle.keyPair);
val data = url.substring("polycentric://".length).base64UrlToByteArray();
val urlInfo = Protocol.URLInfo.parseFrom(data);
if (urlInfo.urlType != 3L) {
throw Exception("Expected urlInfo struct of type ExportBundle")
}
val exportBundle = ExportBundle.parseFrom(urlInfo.body); val existingProcessSecret = Store.instance.getProcessSecret(keyPair.publicKey);
val keyPair = KeyPair.fromProto(exportBundle.keyPair); if (existingProcessSecret != null) {
UIDialogs.toast(this, getString(R.string.this_profile_is_already_imported));
return;
}
val existingProcessSecret = Store.instance.getProcessSecret(keyPair.publicKey); val processSecret = ProcessSecret(keyPair, Process.random());
if (existingProcessSecret != null) { Store.instance.addProcessSecret(processSecret);
withContext(Dispatchers.Main) {
UIDialogs.toast(this@PolycentricImportProfileActivity, getString(R.string.this_profile_is_already_imported));
}
return@launch;
}
val processSecret = ProcessSecret(keyPair, Process.random()); val processHandle = processSecret.toProcessHandle();
Store.instance.addProcessSecret(processSecret);
for (e in exportBundle.events.eventsList) {
try { try {
PolycentricStorage.instance.addProcessSecret(processSecret) val se = SignedEvent.fromProto(e);
Store.instance.putSignedEvent(se);
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.e(TAG, "Failed to save process secret to secret storage.", e) Logger.w(TAG, "Ignored invalid event", e);
}
val processHandle = processSecret.toProcessHandle();
for (e in exportBundle.events.eventsList) {
try {
val se = SignedEvent.fromProto(e);
Store.instance.putSignedEvent(se);
} catch (e: Throwable) {
Logger.w(TAG, "Ignored invalid event", e);
}
}
StatePolycentric.instance.setProcessHandle(processHandle);
processHandle.fullyBackfillClient(ApiMethods.SERVER);
withContext(Dispatchers.Main) {
startActivity(Intent(this@PolycentricImportProfileActivity, PolycentricProfileActivity::class.java));
finish();
}
} catch (e: Throwable) {
Logger.w(TAG, "Failed to import profile", e);
withContext(Dispatchers.Main) {
UIDialogs.toast(this@PolycentricImportProfileActivity, getString(R.string.failed_to_import_profile) + " '${e.message}'");
}
} finally {
withContext(Dispatchers.Main) {
_loaderOverlay.hide();
} }
} }
StatePolycentric.instance.setProcessHandle(processHandle);
startActivity(Intent(this@PolycentricImportProfileActivity, PolycentricProfileActivity::class.java));
finish();
} catch (e: Throwable) {
Logger.w(TAG, "Failed to import profile", e);
UIDialogs.toast(this, getString(R.string.failed_to_import_profile) + " '${e.message}'");
} }
} }

View file

@ -1,8 +1,6 @@
package com.futo.platformplayer.activities package com.futo.platformplayer.activities
import android.app.Activity import android.app.Activity
import android.content.ClipData
import android.content.ClipboardManager
import android.content.ContentResolver import android.content.ContentResolver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@ -14,28 +12,25 @@ import android.webkit.MimeTypeMap
import android.widget.EditText import android.widget.EditText
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.dialogs.CommentDialog
import com.futo.platformplayer.dp import com.futo.platformplayer.dp
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricStorage
import com.futo.platformplayer.selectBestImage import com.futo.platformplayer.selectBestImage
import com.futo.platformplayer.setNavigationBarColorAndIcons import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePolycentric import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.views.buttons.BigButton import com.futo.platformplayer.views.buttons.BigButton
import com.futo.platformplayer.views.overlays.LoaderOverlay
import com.futo.polycentric.core.ApiMethods
import com.futo.polycentric.core.Store import com.futo.polycentric.core.Store
import com.futo.polycentric.core.Synchronization
import com.futo.polycentric.core.SystemState import com.futo.polycentric.core.SystemState
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions import com.futo.polycentric.core.toURLInfoDataLink
import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl
import com.futo.polycentric.core.toBase64Url
import com.futo.polycentric.core.toURLInfoSystemLinkUrl import com.futo.polycentric.core.toURLInfoSystemLinkUrl
import com.github.dhaval2404.imagepicker.ImagePicker import com.github.dhaval2404.imagepicker.ImagePicker
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -49,13 +44,10 @@ 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;
private lateinit var _imagePolycentric: ImageView; private lateinit var _imagePolycentric: ImageView;
private lateinit var _loaderOverlay: LoaderOverlay;
private lateinit var _textSystem: TextView;
private var _avatarUri: Uri? = null; private var _avatarUri: Uri? = null;
override fun attachBaseContext(newBase: Context?) { override fun attachBaseContext(newBase: Context?) {
@ -71,19 +63,30 @@ 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);
_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();
}; };
lifecycleScope.launch(Dispatchers.IO) {
try {
val processHandle = StatePolycentric.instance.processHandle!!;
Synchronization.fullyBackFillClient(processHandle, processHandle.system, "https://srv1-stg.polycentric.io");
withContext(Dispatchers.Main) {
updateUI();
}
} catch (e: Throwable) {
withContext(Dispatchers.Main) {
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.failed_to_backfill_client));
}
}
}
updateUI();
_imagePolycentric.setOnClickListener { _imagePolycentric.setOnClickListener {
ImagePicker.with(this) ImagePicker.with(this)
.cropSquare() .cropSquare()
@ -99,16 +102,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,42 +118,10 @@ 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();
}); });
} }
_textSystem.setOnLongClickListener {
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip: ClipData = ClipData.newPlainText("system", _textSystem.text)
clipboard.setPrimaryClip(clip)
return@setOnLongClickListener true
}
updateUI()
StatePolycentric.instance.processHandle?.let { processHandle ->
_loaderOverlay.show()
lifecycleScope.launch(Dispatchers.IO) {
try {
processHandle.fullyBackfillClient(ApiMethods.SERVER)
withContext(Dispatchers.Main) {
updateUI();
}
} catch (e: Throwable) {
withContext(Dispatchers.Main) {
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.failed_to_backfill_client));
}
} finally {
withContext(Dispatchers.Main) {
_loaderOverlay.hide()
}
}
}
}
} }
private fun saveIfRequired() { private fun saveIfRequired() {
@ -169,17 +130,13 @@ class PolycentricProfileActivity : AppCompatActivity() {
var hasChanges = false; var hasChanges = false;
val username = _editName.text.toString(); val username = _editName.text.toString();
if (username.length < 3) { if (username.length < 3) {
withContext(Dispatchers.Main) { UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.name_must_be_at_least_3_characters_long));
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.name_must_be_at_least_3_characters_long));
}
return@launch; return@launch;
} }
val processHandle = StatePolycentric.instance.processHandle; val processHandle = StatePolycentric.instance.processHandle;
if (processHandle == null) { if (processHandle == null) {
withContext(Dispatchers.Main) { UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.process_handle_unset));
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.process_handle_unset));
}
return@launch; return@launch;
} }
@ -264,7 +221,6 @@ class PolycentricProfileActivity : AppCompatActivity() {
private fun updateUI() { private fun updateUI() {
val processHandle = StatePolycentric.instance.processHandle!!; val processHandle = StatePolycentric.instance.processHandle!!;
val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(processHandle.system)) val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(processHandle.system))
_textSystem.text = processHandle.system.key.toBase64Url()
_username = systemState.username; _username = systemState.username;
_editName.text.clear(); _editName.text.clear();
_editName.text.append(_username); _editName.text.append(_username);
@ -294,7 +250,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
} }
private fun getMimeType(contentResolver: ContentResolver, uri: Uri): String? { private fun getMimeType(contentResolver: ContentResolver, uri: Uri): String? {
var mimeType: String?; var mimeType: String? = null;
// Try to get MIME type from the content URI // Try to get MIME type from the content URI
mimeType = contentResolver.getType(uri); mimeType = contentResolver.getType(uri);

View file

@ -1,10 +1,8 @@
package com.futo.platformplayer.activities package com.futo.platformplayer.activities
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.NotificationManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.widget.FrameLayout import android.widget.FrameLayout
@ -14,11 +12,8 @@ import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.* import com.futo.platformplayer.*
import com.futo.platformplayer.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
@ -38,14 +33,6 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
lateinit var overlay: FrameLayout; lateinit var overlay: FrameLayout;
val notifPermission = "android.permission.POST_NOTIFICATIONS";
val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
if (isGranted)
UIDialogs.toast(this, "Notification permission granted");
else
UIDialogs.toast(this, "Notification permission denied");
}
override fun attachBaseContext(newBase: Context?) { override fun attachBaseContext(newBase: Context?) {
Logger.i("SettingsActivity", "SettingsActivity.attachBaseContext") Logger.i("SettingsActivity", "SettingsActivity.attachBaseContext")
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase)) super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
@ -62,7 +49,7 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
_loaderView = findViewById(R.id.loader); _loaderView = findViewById(R.id.loader);
overlay = findViewById(R.id.overlay_container); overlay = findViewById(R.id.overlay_container);
_form.onChanged.subscribe { field, _ -> _form.onChanged.subscribe { field, value ->
Logger.i("SettingsActivity", "Setting [${field.field?.name}] changed, saving"); Logger.i("SettingsActivity", "Setting [${field.field?.name}] changed, saving");
_form.setObjectValues(); _form.setObjectValues();
Settings.instance.save(); Settings.instance.save();
@ -71,33 +58,6 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
Logger.i("SettingsActivity", "App language change detected, propogating to shared preferences"); Logger.i("SettingsActivity", "App language change detected, propogating to shared preferences");
StateApp.instance.setLocaleSetting(this, Settings.instance.language.getAppLanguageLocaleString()); StateApp.instance.setLocaleSetting(this, Settings.instance.language.getAppLanguageLocaleString());
} }
if(field.descriptor?.id == "background_update") {
Logger.i("SettingsActivity", "Detected change in background work ${field.value}");
if(Settings.instance.subscriptions.subscriptionsBackgroundUpdateInterval > 0) {
val notifManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
if(!notifManager.areNotificationsEnabled()) {
UIDialogs.toast(this, "Notifications aren't enabled");
when {
ContextCompat.checkSelfPermission(this, notifPermission) == PackageManager.PERMISSION_GRANTED -> {
}
ActivityCompat.shouldShowRequestPermissionRationale(this, notifPermission) -> {
UIDialogs.showDialog(this, R.drawable.ic_notifications, "Notifications Required",
"Notifications need to be enabled for background updating to function", null, 0,
UIDialogs.Action("Cancel", {}),
UIDialogs.Action("Enable", {
requestPermissionLauncher.launch(notifPermission);
}, UIDialogs.ActionStyle.PRIMARY));
}
else -> {
requestPermissionLauncher.launch(notifPermission);
}
}
}
}
}
}; };
_buttonBack.setOnClickListener { _buttonBack.setOnClickListener {
finish(); finish();
@ -112,10 +72,7 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
reloadSettings(); reloadSettings();
} }
var isFirstLoad = true;
fun reloadSettings() { fun reloadSettings() {
val firstLoad = isFirstLoad;
isFirstLoad = false;
_form.setSearchVisible(false); _form.setSearchVisible(false);
_loaderView.start(); _loaderView.start();
_form.fromObject(lifecycleScope, Settings.instance) { _form.fromObject(lifecycleScope, Settings.instance) {
@ -133,13 +90,6 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
UIDialogs.toast(this, getString(R.string.you_are_now_in_developer_mode)); UIDialogs.toast(this, getString(R.string.you_are_now_in_developer_mode));
} }
}; };
if(firstLoad) {
val query = intent.getStringExtra("query");
if(!query.isNullOrEmpty()) {
_form.setSearchQuery(query);
}
}
}; };
} }
@ -185,19 +135,11 @@ 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)

View file

@ -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"
}
}

View file

@ -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"
}
}

View file

@ -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
}
}

View file

@ -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,12 @@ 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.util.Dictionary
import java.security.cert.X509Certificate import java.util.concurrent.TimeUnit
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 +27,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 +36,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;
@ -107,7 +60,7 @@ open class ManagedHttpClient {
val requestBuilder: okhttp3.Request.Builder = okhttp3.Request.Builder() val requestBuilder: okhttp3.Request.Builder = okhttp3.Request.Builder()
.url(url); .url(url);
if(user_agent.isNotEmpty() && !headers.any { it.key.lowercase() == "user-agent" }) if(user_agent != null && !user_agent.isEmpty() && !headers.any { it.key.lowercase() == "user-agent" })
requestBuilder.addHeader("User-Agent", user_agent) requestBuilder.addHeader("User-Agent", user_agent)
for (pair in headers.entries) for (pair in headers.entries)
@ -184,7 +137,7 @@ open class ManagedHttpClient {
val requestBuilder: okhttp3.Request.Builder = okhttp3.Request.Builder() val requestBuilder: okhttp3.Request.Builder = okhttp3.Request.Builder()
.method(request.method, requestBody) .method(request.method, requestBody)
.url(request.url); .url(request.url);
if(user_agent.isNotEmpty() && !request.headers.any { it.key.lowercase() == "user-agent" }) if(user_agent != null && !user_agent.isEmpty() && !request.headers.any { it.key.lowercase() == "user-agent" })
requestBuilder.addHeader("User-Agent", user_agent) requestBuilder.addHeader("User-Agent", user_agent)
for (pair in request.headers.entries) for (pair in request.headers.entries)
@ -195,7 +148,7 @@ open class ManagedHttpClient {
val time = measureTimeMillis { val time = measureTimeMillis {
val call = client.newCall(requestBuilder.build()); val call = client.newCall(requestBuilder.build());
request.onCallCreated.emit(call); request.onCallCreated?.emit(call);
response = call.execute() response = call.execute()
resp = Response( resp = Response(
response.code, response.code,

View file

@ -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");

View file

@ -1,11 +1,11 @@
package com.futo.platformplayer.api.http.server package com.futo.platformplayer.api.http.server
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.http.server.exceptions.EmptyRequestException import com.futo.platformplayer.api.http.server.exceptions.EmptyRequestException
import com.futo.platformplayer.api.http.server.handlers.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 java.io.BufferedInputStream import java.io.BufferedInputStream
import java.io.OutputStream import java.io.OutputStream
import java.lang.reflect.Field import java.lang.reflect.Field
@ -14,10 +14,11 @@ import java.net.InetAddress
import java.net.NetworkInterface import java.net.NetworkInterface
import java.net.ServerSocket import java.net.ServerSocket
import java.net.Socket import java.net.Socket
import java.util.UUID import java.util.*
import java.util.concurrent.ExecutorService import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.stream.IntStream.range import java.util.stream.IntStream.range
import kotlin.collections.HashMap
class ManagedHttpServer(private val _requestedPort: Int = 0) { class ManagedHttpServer(private val _requestedPort: Int = 0) {
private val _client : ManagedHttpClient = ManagedHttpClient(); private val _client : ManagedHttpClient = ManagedHttpClient();
@ -191,7 +192,7 @@ class ManagedHttpServer(private val _requestedPort: Int = 0) {
} }
} }
fun addBridgeHandlers(obj: Any, tag: String? = null) { fun addBridgeHandlers(obj: Any, tag: String? = null) {
//val tagToUse = tag ?: obj.javaClass.name; val tagToUse = tag ?: obj.javaClass.name;
val getMethods = obj::class.java.declaredMethods val getMethods = obj::class.java.declaredMethods
.filter { it.getAnnotation(HttpGET::class.java) != null } .filter { it.getAnnotation(HttpGET::class.java) != null }
.map { Pair<Method, HttpGET>(it, it.getAnnotation(HttpGET::class.java)!!) } .map { Pair<Method, HttpGET>(it, it.getAnnotation(HttpGET::class.java)!!) }
@ -208,20 +209,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(
@ -231,13 +232,13 @@ class ManagedHttpServer(private val _requestedPort: Int = 0) {
} }
else else
it.respondCode(204); it.respondCode(204);
}).withContentType(getField.second.contentType); }).withContentType(getField.second.contentType ?: "");
} }
} }
private fun keepAliveLoop(requestReader: BufferedInputStream, responseStream: OutputStream, requestId: String, handler: (HttpContext)->Unit) { private fun keepAliveLoop(requestReader: BufferedInputStream, responseStream: OutputStream, requestId: String, handler: (HttpContext)->Unit) {
val stopCount = _stopCount; val stopCount = _stopCount;
var keepAlive: Boolean; var keepAlive = false;
var requestsMax = 0; var requestsMax = 0;
var requestsTotal = 0; var requestsTotal = 0;
do { do {
@ -287,13 +288,11 @@ class ManagedHttpServer(private val _requestedPort: Int = 0) {
for (intf in NetworkInterface.getNetworkInterfaces()) { for (intf in NetworkInterface.getNetworkInterfaces()) {
for (addr in intf.inetAddresses) { for (addr in intf.inetAddresses) {
if (!addr.isLoopbackAddress) { if (!addr.isLoopbackAddress) {
val ipString: String = addr.hostAddress ?: continue val ipString: String = addr.hostAddress;
val isIPv4 = ipString.indexOf(':') < 0 val isIPv4 = ipString.indexOf(':') < 0;
if (!isIPv4) { if (!isIPv4)
continue continue;
} addresses.add(addr);
addresses.add(addr)
} }
} }
} }

View file

@ -1,3 +1,6 @@
package com.futo.platformplayer.api.http.server.exceptions package com.futo.platformplayer.api.http.server.exceptions
import java.net.SocketTimeoutException
import java.util.concurrent.TimeoutException
class EmptyRequestException(msg: String) : Exception(msg) {} class EmptyRequestException(msg: String) : Exception(msg) {}

View file

@ -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
} }

View file

@ -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);

View file

@ -0,0 +1,107 @@
package com.futo.platformplayer.api.media
import androidx.collection.LruCache
import com.futo.platformplayer.api.media.models.ResultCapabilities
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
import com.futo.platformplayer.api.media.models.chapters.IChapter
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
import com.futo.platformplayer.api.media.models.live.ILiveChatWindowDescriptor
import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.models.ImageVariable
import com.futo.platformplayer.models.Playlist
/**
* A temporary class that caches video results
* In future this should be part of a bigger system
*/
class CachedPlatformClient : IPlatformClient {
private val _client : IPlatformClient;
override val id: String get() = _client.id;
override val name: String get() = _client.name;
override val icon: ImageVariable? get() = _client.icon;
private val _cache: LruCache<String, IPlatformContentDetails>;
override val capabilities: PlatformClientCapabilities
get() = _client.capabilities;
constructor(client : IPlatformClient, cacheSize : Int = 10 * 1024 * 1024) {
this._client = client;
this._cache = LruCache<String, IPlatformContentDetails>(cacheSize);
}
override fun initialize() { _client.initialize() }
override fun disable() { _client.disable() }
override fun isContentDetailsUrl(url: String): Boolean = _client.isContentDetailsUrl(url);
override fun getContentDetails(url: String): IPlatformContentDetails {
var result = _cache.get(url);
if(result == null) {
result = _client.getContentDetails(url);
if (result != null)
_cache.put(url, result);
}
return result;
}
override fun getContentChapters(url: String): List<IChapter> = _client.getContentChapters(url);
override fun getPlaybackTracker(url: String): IPlaybackTracker? = _client.getPlaybackTracker(url);
override fun isChannelUrl(url: String): Boolean = _client.isChannelUrl(url);
override fun getChannel(channelUrl: String): IPlatformChannel = _client.getChannel(channelUrl);
override fun getChannelCapabilities(): ResultCapabilities = _client.getChannelCapabilities();
override fun getChannelContents(
channelUrl: String,
type: String?,
order: String?,
filters: Map<String, List<String>>?
): IPager<IPlatformContent> = _client.getChannelContents(channelUrl);
override fun getChannelUrlByClaim(claimType: Int, claimValues: Map<Int, String>): String? = _client.getChannelUrlByClaim(claimType, claimValues)
override fun searchSuggestions(query: String): Array<String> = _client.searchSuggestions(query);
override fun getSearchCapabilities(): ResultCapabilities = _client.getSearchCapabilities();
override fun search(
query: String,
type: String?,
order: String?,
filters: Map<String, List<String>>?
): IPager<IPlatformContent> = _client.search(query, type, order, filters);
override fun getSearchChannelContentsCapabilities(): ResultCapabilities = _client.getSearchChannelContentsCapabilities();
override fun searchChannelContents(
channelUrl: String,
query: String,
type: String?,
order: String?,
filters: Map<String, List<String>>?
): IPager<IPlatformContent> = _client.searchChannelContents(channelUrl, query, type, order, filters);
override fun searchChannels(query: String) = _client.searchChannels(query);
override fun getComments(url: String): IPager<IPlatformComment> = _client.getComments(url);
override fun getSubComments(comment: IPlatformComment): IPager<IPlatformComment> = _client.getSubComments(comment);
override fun getLiveChatWindow(url: String): ILiveChatWindowDescriptor? = _client.getLiveChatWindow(url);
override fun getLiveEvents(url: String): IPager<IPlatformLiveEvent>? = _client.getLiveEvents(url);
override fun getHome(): IPager<IPlatformContent> = _client.getHome();
override fun getUserSubscriptions(): Array<String> { return arrayOf(); };
override fun searchPlaylists(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = _client.searchPlaylists(query, type, order, filters);
override fun isPlaylistUrl(url: String): Boolean = _client.isPlaylistUrl(url);
override fun getPlaylist(url: String): IPlatformPlaylistDetails = _client.getPlaylist(url);
override fun getUserPlaylists(): Array<String> { return arrayOf(); };
override fun isClaimTypeSupported(claimType: Int): Boolean {
return _client.isClaimTypeSupported(claimType);
}
}

View file

@ -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
@ -15,6 +14,7 @@ import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.models.ImageVariable import com.futo.platformplayer.models.ImageVariable
import com.futo.platformplayer.models.Playlist
/** /**
* A client for a specific platform * A client for a specific platform
@ -67,11 +67,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
/** /**
@ -91,20 +86,6 @@ interface IPlatformClient {
*/ */
fun getChannelContents(channelUrl: String, type: String? = null, order: String? = null, filters: Map<String, List<String>>? = null): IPager<IPlatformContent>; fun getChannelContents(channelUrl: String, type: String? = null, order: String? = null, filters: Map<String, List<String>>? = null): IPager<IPlatformContent>;
/**
* Describes what the plugin is capable on peek channel results
*/
fun getPeekChannelTypes(): List<String>;
/**
* Peeks contents of a channel, upload time descending
*/
fun peekChannelContents(channelUrl: String, type: String? = null): List<IPlatformContent>
/**
* Gets all playlists of a channel
*/
fun getChannelPlaylists(channelUrl: String): IPager<IPlatformPlaylist>
/** /**
* Gets the channel url associated with a claimType * Gets the channel url associated with a claimType
*/ */
@ -127,11 +108,6 @@ interface IPlatformClient {
*/ */
fun getPlaybackTracker(url: String): IPlaybackTracker?; fun getPlaybackTracker(url: String): IPlaybackTracker?;
/**
* Get content recommendations
*/
fun getContentRecommendations(url: String): IPager<IPlatformContent>?;
//Comments //Comments
/** /**

View file

@ -9,6 +9,7 @@ import com.caverock.androidsvg.SVG
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
import com.futo.platformplayer.api.media.models.live.LiveEventComment import com.futo.platformplayer.api.media.models.live.LiveEventComment
import com.futo.platformplayer.api.media.models.live.LiveEventDonation
import com.futo.platformplayer.api.media.models.live.LiveEventEmojis import com.futo.platformplayer.api.media.models.live.LiveEventEmojis
import com.futo.platformplayer.api.media.platforms.js.models.JSLiveEventPager import com.futo.platformplayer.api.media.platforms.js.models.JSLiveEventPager
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
@ -194,7 +195,7 @@ class LiveChatManager {
fun getEmojiDrawable(emoji: String, cb: (drawable: Drawable?)->Unit) { fun getEmojiDrawable(emoji: String, cb: (drawable: Drawable?)->Unit) {
var drawable: Drawable? = null; var drawable: Drawable? = null;
var url: String?; var url: String? = null;
synchronized(_cache_lock) { synchronized(_cache_lock) {
url = _cache_urls[emoji]; url = _cache_urls[emoji];
if(url != null) if(url != null)

View file

@ -13,14 +13,10 @@ data class PlatformClientCapabilities(
val hasGetChannelUrlByClaim: Boolean = false, val hasGetChannelUrlByClaim: Boolean = false,
val hasGetChannelTemplateByClaimMap: Boolean = false, val hasGetChannelTemplateByClaimMap: Boolean = false,
val hasGetSearchCapabilities: Boolean = false, val hasGetSearchCapabilities: Boolean = false,
val hasGetSearchChannelContentsCapabilities: Boolean = false,
val hasGetChannelCapabilities: Boolean = false, val hasGetChannelCapabilities: Boolean = false,
val hasGetLiveEvents: Boolean = false, val hasGetLiveEvents: Boolean = false,
val hasGetLiveChatWindow: Boolean = false, val hasGetLiveChatWindow: Boolean = false,
val hasGetContentChapters: Boolean = false, val hasGetContentChapters: Boolean = false
val hasPeekChannelContents: Boolean = false,
val hasGetChannelPlaylists: Boolean = false,
val hasGetContentRecommendations: Boolean = false
) { ) {
} }

View file

@ -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);

View file

@ -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,8 +19,8 @@ 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 { client, pool ->
synchronized(_clientPools) { synchronized(_clientPools) {
if(_clientPools[parentClient] == pool) if(_clientPools[parentClient] == pool)
_clientPools.remove(parentClient); _clientPools.remove(parentClient);

View file

@ -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; };
} }
} }

View file

@ -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
@ -17,7 +14,7 @@ open class PlatformAuthorLink {
val id: PlatformID; val id: PlatformID;
val name: String; val name: String;
val url: String; val url: String;
var thumbnail: String?; val thumbnail: String?;
var subscribers: Long? = null; //Optional var subscribers: Long? = null; //Optional
constructor(id: PlatformID, name: String, url: String, thumbnail: String? = null, subscribers: Long? = null) constructor(id: PlatformID, name: String, url: String, thumbnail: String? = null, subscribers: Long? = null)
@ -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);
@ -45,21 +40,4 @@ 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
}
} }

View file

@ -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";
@ -65,6 +64,7 @@ class FilterGroup(
val isMultiSelect: Boolean, val isMultiSelect: Boolean,
val id: String? = null val id: String? = null
) { ) {
@kotlinx.serialization.Transient
val idOrName: String get() = id ?: name; val idOrName: String get() = id ?: name;
companion object { companion object {

View file

@ -3,8 +3,6 @@ package com.futo.platformplayer.api.media.models
import com.caoccao.javet.values.reference.V8ValueArray import com.caoccao.javet.values.reference.V8ValueArray
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8PluginConfig
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
@kotlinx.serialization.Serializable @kotlinx.serialization.Serializable
@ -33,7 +31,7 @@ class Thumbnails {
fun fromV8(config: IV8PluginConfig, value: V8ValueObject): Thumbnails { fun fromV8(config: IV8PluginConfig, value: V8ValueObject): Thumbnails {
return Thumbnails((value.getOrThrow<V8ValueArray>(config, "sources", "Thumbnails")) return Thumbnails((value.getOrThrow<V8ValueArray>(config, "sources", "Thumbnails"))
.toArray() .toArray()
.map { Thumbnail.fromV8(config, it as V8ValueObject) } .map { Thumbnail.fromV8(it as V8ValueObject) }
.toTypedArray()); .toTypedArray());
} }
} }
@ -42,10 +40,10 @@ class Thumbnails {
data class Thumbnail(val url : String?, val quality : Int = 0) { data class Thumbnail(val url : String?, val quality : Int = 0) {
companion object { companion object {
fun fromV8(config: IV8PluginConfig, value: V8ValueObject): Thumbnail { fun fromV8(value: V8ValueObject): Thumbnail {
return Thumbnail( return Thumbnail(
value.getOrDefault<String>(config,"url", "Thumbnail", null), value.getString("url"),
value.getOrDefault(config, "quality", "Thumbnail", 0) ?: 0); value.getInteger("quality"));
} }
} }
}; };

View file

@ -37,10 +37,6 @@ class SerializedChannel(
TODO("Not yet implemented") TODO("Not yet implemented")
} }
fun isSameUrl(url: String): Boolean {
return this.url == url || urlAlternatives.contains(url);
}
companion object { companion object {
fun fromChannel(channel: IPlatformChannel): SerializedChannel { fun fromChannel(channel: IPlatformChannel): SerializedChannel {
return SerializedChannel( return SerializedChannel(

View file

@ -14,8 +14,7 @@ enum class ChapterType(val value: Int) {
NORMAL(0), NORMAL(0),
SKIPPABLE(5), SKIPPABLE(5),
SKIP(6), SKIP(6);
SKIPONCE(7);
@ -23,7 +22,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;

View file

@ -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);
}
}

View file

@ -19,9 +19,8 @@ class PolycentricPlatformComment : IPlatformComment {
val eventPointer: Pointer; val eventPointer: Pointer;
val reference: Reference; val reference: Reference;
val parentReference: Reference?;
constructor(contextUrl: String, author: PlatformAuthorLink, msg: String, rating: IRating, date: OffsetDateTime, eventPointer: Pointer, parentReference: Reference?, replyCount: Int? = null) { constructor(contextUrl: String, author: PlatformAuthorLink, msg: String, rating: IRating, date: OffsetDateTime, eventPointer: Pointer, replyCount: Int? = null) {
this.contextUrl = contextUrl; this.contextUrl = contextUrl;
this.author = author; this.author = author;
this.message = msg; this.message = msg;
@ -30,7 +29,6 @@ class PolycentricPlatformComment : IPlatformComment {
this.replyCount = replyCount; this.replyCount = replyCount;
this.eventPointer = eventPointer; this.eventPointer = eventPointer;
this.reference = eventPointer.toReference(); this.reference = eventPointer.toReference();
this.parentReference = parentReference;
} }
override fun getReplies(client: IPlatformClient): IPager<IPlatformComment> { override fun getReplies(client: IPlatformClient): IPager<IPlatformComment> {
@ -38,11 +36,10 @@ class PolycentricPlatformComment : IPlatformComment {
} }
fun cloneWithUpdatedReplyCount(replyCount: Int?): PolycentricPlatformComment { fun cloneWithUpdatedReplyCount(replyCount: Int?): PolycentricPlatformComment {
return PolycentricPlatformComment(contextUrl, author, message, rating, date, eventPointer, parentReference, replyCount); return PolycentricPlatformComment(contextUrl, author, message, rating, date, eventPointer, replyCount);
} }
companion object { companion object {
private const val TAG = "PolycentricPlatformComment"
val MAX_COMMENT_SIZE = 2000 val MAX_COMMENT_SIZE = 2000
} }
} }

View file

@ -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;

View file

@ -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 {

View file

@ -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>?;
} }

View file

@ -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>;
} }

View file

@ -1,23 +1,31 @@
package com.futo.platformplayer.api.media.models.live package com.futo.platformplayer.api.media.models.live
import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
import com.futo.platformplayer.api.media.models.ratings.RatingScaler
import com.futo.platformplayer.api.media.models.ratings.RatingType
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.orDefault
interface IPlatformLiveEvent { interface IPlatformLiveEvent {
val type : LiveEventType; val type : LiveEventType;
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject, contextName: String = "LiveEvent") : IPlatformLiveEvent { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject, contextName: String = "Unknown") : IPlatformLiveEvent {
val t = LiveEventType.fromInt(obj.getOrThrow<Int>(config, "type", contextName)); val contextName = "LiveEvent";
return when(t) { val type = LiveEventType.fromInt(obj.getOrThrow<Int>(config, "type", contextName));
return when(type) {
LiveEventType.COMMENT -> LiveEventComment.fromV8(config, obj); LiveEventType.COMMENT -> LiveEventComment.fromV8(config, obj);
LiveEventType.EMOJIS -> LiveEventEmojis.fromV8(config, obj); LiveEventType.EMOJIS -> LiveEventEmojis.fromV8(config, obj);
LiveEventType.DONATION -> LiveEventDonation.fromV8(config, obj); LiveEventType.DONATION -> LiveEventDonation.fromV8(config, obj);
LiveEventType.VIEWCOUNT -> LiveEventViewCount.fromV8(config, obj); LiveEventType.VIEWCOUNT -> LiveEventViewCount.fromV8(config, obj);
LiveEventType.RAID -> LiveEventRaid.fromV8(config, obj); LiveEventType.RAID -> LiveEventRaid.fromV8(config, obj);
else -> throw NotImplementedError("Unknown type $t"); else -> throw NotImplementedError("Unknown type ${type}");
} }
} }
} }

View file

@ -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 };
} }
} }
} }

View file

@ -1,14 +0,0 @@
package com.futo.platformplayer.api.media.models.modifier
class AdhocRequestModifier: IRequestModifier {
val _handler: (String, Map<String,String>)->IRequest;
override var allowByteSkip: Boolean = false;
constructor(modifyReq: (String, Map<String,String>)->IRequest) {
_handler = modifyReq;
}
override fun modifyRequest(url: String, headers: Map<String, String>): IRequest {
return _handler(url, headers);
}
}

View file

@ -1,7 +0,0 @@
package com.futo.platformplayer.api.media.models.modifier
interface IModifierOptions {
val applyAuthClient: String?;
val applyCookieClient: String?;
val applyOtherHeaders: Boolean;
}

View file

@ -1,6 +0,0 @@
package com.futo.platformplayer.api.media.models.modifier
interface IRequest {
val url: String?;
val headers: Map<String, String>;
}

View file

@ -1,7 +0,0 @@
package com.futo.platformplayer.api.media.models.modifier
interface IRequestModifier {
var allowByteSkip: Boolean;
fun modifyRequest(url: String, headers: Map<String, String>): IRequest
}

View file

@ -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();
} }

View file

@ -1,6 +1,9 @@
package com.futo.platformplayer.api.media.models.playlists package com.futo.platformplayer.api.media.models.playlists
import com.futo.platformplayer.api.media.models.Thumbnails
import com.futo.platformplayer.api.media.models.contents.IPlatformContent import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.structures.IPager
interface IPlatformPlaylist : IPlatformContent { interface IPlatformPlaylist : IPlatformContent {
val thumbnail: String?; val thumbnail: String?;

View file

@ -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;
} }

View file

@ -2,6 +2,10 @@ package com.futo.platformplayer.api.media.models.post
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
import com.futo.platformplayer.api.media.models.ratings.IRating import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.sources.*
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
/** /**
* A detailed video model with data including video/audio sources * A detailed video model with data including video/audio sources

View file

@ -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;

View file

@ -14,13 +14,14 @@ interface IRating {
companion object { companion object {
fun fromV8OrDefault(config: IV8PluginConfig, obj: V8Value?, default: IRating) = obj.orDefault(default) { fromV8(config, it as V8ValueObject) }; fun fromV8OrDefault(config: IV8PluginConfig, obj: V8Value?, default: IRating) = obj.orDefault(default) { fromV8(config, it as V8ValueObject) };
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject, contextName: String = "Rating") : IRating { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject, contextName: String = "Unknown") : IRating {
val t = RatingType.fromInt(obj.getOrThrow<Int>(config, "type", contextName)); val contextName = "Rating";
return when(t) { val type = RatingType.fromInt(obj.getOrThrow<Int>(config, "type", contextName));
return when(type) {
RatingType.LIKES -> RatingLikes.fromV8(config, obj); RatingType.LIKES -> RatingLikes.fromV8(config, obj);
RatingType.LIKEDISLIKES -> RatingLikeDislikes.fromV8(config, obj); RatingType.LIKEDISLIKES -> RatingLikeDislikes.fromV8(config, obj);
RatingType.SCALE -> RatingScaler.fromV8(config, obj); RatingType.SCALE -> RatingScaler.fromV8(config, obj);
else -> throw NotImplementedError("Unknown type $t"); else -> throw NotImplementedError("Unknown type ${type}");
} }
} }
} }

View file

@ -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 };
} }
} }
} }

View file

@ -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();

View file

@ -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;

View file

@ -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 {

View file

@ -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;
} }

View file

@ -1,3 +0,0 @@
package com.futo.platformplayer.api.media.models.streams.sources
interface IAudioUrlWidevineSource : IAudioUrlSource, IWidevineSource

View file

@ -1,5 +0,0 @@
package com.futo.platformplayer.api.media.models.streams.sources
interface IDashManifestWidevineSource : IWidevineSource {
val url: String
}

View file

@ -1,3 +0,0 @@
package com.futo.platformplayer.api.media.models.streams.sources
interface IVideoUrlWidevineSource : IVideoUrlSource, IWidevineSource

View file

@ -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?
}

View file

@ -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
); );

View file

@ -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
); );

View file

@ -1,6 +1,8 @@
package com.futo.platformplayer.api.media.models.subtitles package com.futo.platformplayer.api.media.models.subtitles
import android.net.Uri import android.net.Uri
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
interface ISubtitleSource { interface ISubtitleSource {
val name: String; val name: String;

View file

@ -13,6 +13,4 @@ interface IPlatformVideo : IPlatformContent {
val viewCount: Long; val viewCount: Long;
val isLive : Boolean; val isLive : Boolean;
val isShort: Boolean;
} }

View file

@ -1,12 +1,13 @@
package com.futo.platformplayer.api.media.models.video package com.futo.platformplayer.api.media.models.video
import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
import com.futo.platformplayer.api.media.models.ratings.IRating import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource import com.futo.platformplayer.api.media.models.streams.sources.*
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
import com.futo.platformplayer.api.media.structures.IPager
/** /**
* A detailed video model with data including video/audio sources * A detailed video model with data including video/audio sources

Some files were not shown because too many files have changed in this diff Show more