mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-27 18:25:21 +02:00
Compare commits
8 Commits
353
..
remote-sync
| Author | SHA1 | Date | |
|---|---|---|---|
| b025e8a30f | |||
| 5b2f8b8617 | |||
| 955ba23b0d | |||
| 1ae9f0ea26 | |||
| 97381739dd | |||
| 79a932b4ca | |||
| 436846ce1f | |||
| ccb1bed4a8 |
@@ -1,6 +0,0 @@
|
|||||||
aar/* filter=lfs diff=lfs merge=lfs -text
|
|
||||||
app/aar/* filter=lfs diff=lfs merge=lfs -text
|
|
||||||
app/src/main/jniLibs/arm64-v8a filter=lfs diff=lfs merge=lfs -text
|
|
||||||
app/src/main/jniLibs/armeabi-v7a filter=lfs diff=lfs merge=lfs -text
|
|
||||||
app/src/main/jniLibs/x86 filter=lfs diff=lfs merge=lfs -text
|
|
||||||
app/src/main/jniLibs/x86_64 filter=lfs diff=lfs merge=lfs -text
|
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
name: Bug Report
|
name: Bug Report
|
||||||
description: Let us know about an unexpected error, a crash, or an incorrect behavior.
|
description: Let us know about an unexpected error, a crash, or an incorrect behavior.
|
||||||
labels: ["Bug", "Android"]
|
labels: ["Bug"]
|
||||||
title: "Bug: "
|
|
||||||
type: bug
|
|
||||||
projects: ["futo-org/19"]
|
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
@@ -21,33 +18,11 @@ body:
|
|||||||
* if you've found out a particular series of UI interactions can introduce buggy behavior, please label those steps 1-n with markdown
|
* if you've found out a particular series of UI interactions can introduce buggy behavior, please label those steps 1-n with markdown
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: reproduction-steps
|
id: what-happened
|
||||||
attributes:
|
attributes:
|
||||||
label: Reproduction steps
|
label: What happened?
|
||||||
description: Please provide us with the steps to reproduce the issue if possible. This step makes a big difference if we are going to be able to fix it so be as precise as possible.
|
description: What did you expect to happen?
|
||||||
placeholder: |
|
placeholder: Tell us what you see!
|
||||||
0. Play a YouTube video
|
|
||||||
1. Press on Download button
|
|
||||||
2. Select quality 1440p
|
|
||||||
3. Grayjay crashes when attempting to download
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: actual-result
|
|
||||||
attributes:
|
|
||||||
label: Actual result
|
|
||||||
description: What happend?
|
|
||||||
placeholder: Tell us what you saw!
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: expected-result
|
|
||||||
attributes:
|
|
||||||
label: Expected result
|
|
||||||
description: What was suppose to happen?
|
|
||||||
placeholder: Tell us what you expected to happen!
|
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
@@ -56,7 +31,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Grayjay Version
|
label: Grayjay Version
|
||||||
description: In the application, select More > Settings, scroll to the bottom and locate the value next to "Version Name".
|
description: In the application, select More > Settings, scroll to the bottom and locate the value next to "Version Name".
|
||||||
placeholder: "311"
|
placeholder: "242"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
@@ -67,23 +42,19 @@ body:
|
|||||||
multiple: true
|
multiple: true
|
||||||
options:
|
options:
|
||||||
- "All"
|
- "All"
|
||||||
- "Apple Podcasts"
|
- "Youtube"
|
||||||
|
- "Odysee"
|
||||||
|
- "Rumble"
|
||||||
|
- "Kick"
|
||||||
|
- "Twitch"
|
||||||
|
- "PeerTube"
|
||||||
|
- "Patreon"
|
||||||
|
- "Nebula"
|
||||||
- "BiliBili (CN)"
|
- "BiliBili (CN)"
|
||||||
- "Bitchute"
|
- "Bitchute"
|
||||||
- "Crunchyroll"
|
|
||||||
- "CuriosityStream"
|
|
||||||
- "Dailymotion"
|
|
||||||
- "Kick"
|
|
||||||
- "Nebula"
|
|
||||||
- "Odysee"
|
|
||||||
- "Patreon"
|
|
||||||
- "PeerTube"
|
|
||||||
- "Rumble"
|
|
||||||
- "SoundCloud"
|
- "SoundCloud"
|
||||||
- "Spotify"
|
- "Dailymotion"
|
||||||
- "TedTalks"
|
- "Apple Podcasts"
|
||||||
- "Twitch"
|
|
||||||
- "YouTube"
|
|
||||||
- "Other"
|
- "Other"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
@@ -95,30 +66,6 @@ body:
|
|||||||
description: In the application, select Sources > [the broken plugin], write down the value under "Version".
|
description: In the application, select Sources > [the broken plugin], write down the value under "Version".
|
||||||
placeholder: "12"
|
placeholder: "12"
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: android-version
|
|
||||||
attributes:
|
|
||||||
label: Which android version are you using?
|
|
||||||
placeholder: "Android 15"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: phone-model
|
|
||||||
attributes:
|
|
||||||
label: Which device are you using?
|
|
||||||
placeholder: "Google Pixel 9"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: os-version
|
|
||||||
attributes:
|
|
||||||
label: Which operating system are you using?
|
|
||||||
placeholder: "GrapheneOS/CalyxOS/Tizen/HyperOS 2/..."
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
id: login
|
id: login
|
||||||
attributes:
|
attributes:
|
||||||
@@ -139,28 +86,9 @@ body:
|
|||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- 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? If so, please create a list below that mentions each of them. For example:
|
|
||||||
```
|
|
||||||
- #10
|
|
||||||
```
|
|
||||||
placeholder:
|
|
||||||
value:
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: logs
|
id: logs
|
||||||
attributes:
|
attributes:
|
||||||
label: Relevant log output
|
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.
|
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
||||||
render: shell
|
render: shell
|
||||||
|
|
||||||
- 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.
|
|
||||||
+1
-4
@@ -1,16 +1,13 @@
|
|||||||
name: Documentation Issue
|
name: Documentation Issue
|
||||||
description: Report an issue or suggest a change in the documentation.
|
description: Report an issue or suggest a change in the documentation.
|
||||||
labels: ["Documentation"]
|
labels: ["Documentation"]
|
||||||
title: "Documentation: "
|
|
||||||
type: task
|
|
||||||
projects: ["futo-org/19"]
|
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
# Thank you for opening a documentation change request.
|
# Thank you for opening a documentation change request.
|
||||||
|
|
||||||
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay android application. Use the `Documentation` issue type to report problems with the documentation in our code repositories, inside the application, or on [https://grayjay.app](https://grayjay.app)
|
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay Android Application. Use the `Documentation` issue type to report problems with the documentation in our code repositories, inside the application, or on [https://grayjay.app](https://grayjay.app)
|
||||||
Technical writers monitor this issue type, so report Grayjay bugs or feature requests with the `Bug report` or `Feature Request` issue types instead to get engineering attention.
|
Technical writers monitor this issue type, so report Grayjay bugs or feature requests with the `Bug report` or `Feature Request` issue types instead to get engineering attention.
|
||||||
|
|
||||||
For general usage questions, please see: [The Official FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)
|
For general usage questions, please see: [The Official FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)
|
||||||
+2
-5
@@ -1,16 +1,13 @@
|
|||||||
name: Feature Request
|
name: Feature Request
|
||||||
description: Suggest a new feature or other enhancement.
|
description: Suggest a new feature or other enhancement.
|
||||||
labels: ["Enhancement", "Android"]
|
labels: ["Enhancement"]
|
||||||
title: "Feature request: "
|
|
||||||
type: feature
|
|
||||||
projects: ["futo-org/19"]
|
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
# Thank you for opening a feature request.
|
# Thank you for opening a feature request.
|
||||||
|
|
||||||
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues and feature requests relating to the Grayjay android application
|
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay Android Application
|
||||||
|
|
||||||
For discussion related to enhancements, please see: [The FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)
|
For discussion related to enhancements, please see: [The FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)
|
||||||
|
|
||||||
+6
-18
@@ -64,6 +64,12 @@
|
|||||||
[submodule "app/src/stable/assets/sources/bilibili"]
|
[submodule "app/src/stable/assets/sources/bilibili"]
|
||||||
path = app/src/stable/assets/sources/bilibili
|
path = app/src/stable/assets/sources/bilibili
|
||||||
url = ../plugins/bilibili.git
|
url = ../plugins/bilibili.git
|
||||||
|
[submodule "app/src/stable/assets/sources/spotify"]
|
||||||
|
path = app/src/stable/assets/sources/spotify
|
||||||
|
url = ../plugins/spotify.git
|
||||||
|
[submodule "app/src/unstable/assets/sources/spotify"]
|
||||||
|
path = app/src/unstable/assets/sources/spotify
|
||||||
|
url = ../plugins/spotify.git
|
||||||
[submodule "app/src/stable/assets/sources/bitchute"]
|
[submodule "app/src/stable/assets/sources/bitchute"]
|
||||||
path = app/src/stable/assets/sources/bitchute
|
path = app/src/stable/assets/sources/bitchute
|
||||||
url = ../plugins/bitchute.git
|
url = ../plugins/bitchute.git
|
||||||
@@ -88,21 +94,3 @@
|
|||||||
[submodule "app/src/unstable/assets/sources/tedtalks"]
|
[submodule "app/src/unstable/assets/sources/tedtalks"]
|
||||||
path = app/src/unstable/assets/sources/tedtalks
|
path = app/src/unstable/assets/sources/tedtalks
|
||||||
url = ../plugins/tedtalks.git
|
url = ../plugins/tedtalks.git
|
||||||
[submodule "app/src/stable/assets/sources/curiositystream"]
|
|
||||||
path = app/src/stable/assets/sources/curiositystream
|
|
||||||
url = ../plugins/curiositystream.git
|
|
||||||
[submodule "app/src/unstable/assets/sources/curiositystream"]
|
|
||||||
path = app/src/unstable/assets/sources/curiositystream
|
|
||||||
url = ../plugins/curiositystream.git
|
|
||||||
[submodule "app/src/unstable/assets/sources/crunchyroll"]
|
|
||||||
path = app/src/unstable/assets/sources/crunchyroll
|
|
||||||
url = ../plugins/crunchyroll.git
|
|
||||||
[submodule "app/src/stable/assets/sources/crunchyroll"]
|
|
||||||
path = app/src/stable/assets/sources/crunchyroll
|
|
||||||
url = ../plugins/crunchyroll.git
|
|
||||||
[submodule "app/src/stable/assets/sources/mixcloud"]
|
|
||||||
path = app/src/stable/assets/sources/mixcloud
|
|
||||||
url = ../plugins/mixcloud.git
|
|
||||||
[submodule "app/src/unstable/assets/sources/mixcloud"]
|
|
||||||
path = app/src/unstable/assets/sources/mixcloud
|
|
||||||
url = ../plugins/mixcloud.git
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:22c06ca0d1a5808b2fc0a12227d5915b3126bc0b9b1305cf6bab855f2ec6fcbb
|
|
||||||
size 36133152
|
|
||||||
+46
-54
@@ -1,8 +1,8 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id 'com.android.application'
|
id 'com.android.application'
|
||||||
id 'org.jetbrains.kotlin.android'
|
id 'org.jetbrains.kotlin.android'
|
||||||
id 'org.jetbrains.kotlin.plugin.serialization' version '2.2.21'
|
id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.21'
|
||||||
id 'org.ajoberstar.grgit' version '5.3.3'
|
id 'org.ajoberstar.grgit' version '5.2.2'
|
||||||
id 'com.google.protobuf'
|
id 'com.google.protobuf'
|
||||||
id 'kotlin-parcelize'
|
id 'kotlin-parcelize'
|
||||||
id 'com.google.devtools.ksp'
|
id 'com.google.devtools.ksp'
|
||||||
@@ -39,7 +39,7 @@ protobuf {
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
namespace 'com.futo.platformplayer'
|
namespace 'com.futo.platformplayer'
|
||||||
compileSdk 36
|
compileSdk 34
|
||||||
flavorDimensions "buildType"
|
flavorDimensions "buildType"
|
||||||
productFlavors {
|
productFlavors {
|
||||||
stable {
|
stable {
|
||||||
@@ -97,7 +97,7 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdk 28
|
minSdk 28
|
||||||
targetSdk 36
|
targetSdk 34
|
||||||
versionCode gitVersionCode
|
versionCode gitVersionCode
|
||||||
versionName gitVersionName
|
versionName gitVersionName
|
||||||
|
|
||||||
@@ -146,7 +146,6 @@ android {
|
|||||||
}
|
}
|
||||||
sourceSets {
|
sourceSets {
|
||||||
main {
|
main {
|
||||||
jniLibs.srcDirs = ['src/main/jniLibs']
|
|
||||||
assets {
|
assets {
|
||||||
srcDirs 'src/main/assets', 'src/tests/assets', 'src/test/assets'
|
srcDirs 'src/main/assets', 'src/tests/assets', 'src/test/assets'
|
||||||
}
|
}
|
||||||
@@ -155,85 +154,78 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
//implementation 'com.google.dagger:dagger:2.48'
|
implementation 'com.google.dagger:dagger:2.48'
|
||||||
implementation 'androidx.test:monitor:1.8.0'
|
implementation 'androidx.test:monitor:1.7.2'
|
||||||
implementation 'com.google.android.material:material:1.13.0'
|
annotationProcessor 'com.google.dagger:dagger-compiler:2.48'
|
||||||
//annotationProcessor 'com.google.dagger:dagger-compiler:2.48'
|
|
||||||
|
|
||||||
//Core
|
//Core
|
||||||
implementation 'androidx.core:core-ktx:1.17.0'
|
implementation 'androidx.core:core-ktx:1.12.0'
|
||||||
implementation 'androidx.appcompat:appcompat:1.7.1'
|
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
|
implementation 'com.google.android.material:material:1.11.0'
|
||||||
implementation 'androidx.documentfile:documentfile:1.1.0'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||||
|
|
||||||
//Images
|
//Images
|
||||||
annotationProcessor 'com.github.bumptech.glide:compiler:5.0.5'
|
annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'
|
||||||
implementation 'com.github.bumptech.glide:glide:5.0.5'
|
implementation 'com.github.bumptech.glide:glide:4.16.0'
|
||||||
|
|
||||||
//Async
|
//Async
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
|
||||||
|
|
||||||
//HTTP
|
//HTTP
|
||||||
implementation "com.squareup.okhttp3:okhttp:5.3.0"
|
implementation "com.squareup.okhttp3:okhttp:4.11.0"
|
||||||
|
|
||||||
//JSON
|
//JSON
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0" //Used for structured json
|
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2" //Used for structured json
|
||||||
implementation 'com.google.code.gson:gson:2.13.2' //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-v8-android:4.1.5'
|
implementation("com.caoccao.javet:javet-android:3.0.2")
|
||||||
|
|
||||||
//Exoplayer
|
//Exoplayer
|
||||||
implementation 'androidx.media3:media3-exoplayer:1.8.0'
|
implementation 'androidx.media3:media3-exoplayer:1.2.1'
|
||||||
implementation 'androidx.media3:media3-exoplayer-dash:1.8.0'
|
implementation 'androidx.media3:media3-exoplayer-dash:1.2.1'
|
||||||
implementation 'androidx.media3:media3-ui:1.8.0'
|
implementation 'androidx.media3:media3-ui:1.2.1'
|
||||||
implementation 'androidx.media3:media3-exoplayer-hls:1.8.0'
|
implementation 'androidx.media3:media3-exoplayer-hls:1.2.1'
|
||||||
implementation 'androidx.media3:media3-exoplayer-rtsp:1.8.0'
|
implementation 'androidx.media3:media3-exoplayer-rtsp:1.2.1'
|
||||||
implementation 'androidx.media3:media3-exoplayer-smoothstreaming:1.8.0'
|
implementation 'androidx.media3:media3-exoplayer-smoothstreaming:1.2.1'
|
||||||
implementation 'androidx.media3:media3-transformer:1.8.0'
|
implementation 'androidx.media3:media3-transformer:1.2.1'
|
||||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.9.6'
|
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.6'
|
||||||
implementation 'androidx.navigation:navigation-ui-ktx:2.9.6'
|
implementation 'androidx.navigation:navigation-ui-ktx:2.7.6'
|
||||||
implementation 'androidx.media:media:1.7.1'
|
implementation 'androidx.media:media:1.7.0'
|
||||||
|
|
||||||
//Other
|
//Other
|
||||||
implementation 'org.jsoup:jsoup:1.21.2'
|
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 fileTree(dir: 'aar', include: ['*.aar'])
|
implementation 'com.arthenica:ffmpeg-kit-full:6.0-2.LTS'
|
||||||
implementation 'com.arthenica:smart-exception-java:0.2.1'
|
implementation 'org.jetbrains.kotlin:kotlin-reflect:1.9.0'
|
||||||
implementation 'org.jetbrains.kotlin:kotlin-reflect:2.2.0'
|
|
||||||
implementation 'com.github.dhaval2404:imagepicker:2.1'
|
implementation 'com.github.dhaval2404:imagepicker:2.1'
|
||||||
implementation 'com.google.zxing:core:3.5.3'
|
implementation 'com.google.zxing:core:3.4.1'
|
||||||
implementation 'com.journeyapps:zxing-android-embedded:4.3.0'
|
implementation 'com.journeyapps:zxing-android-embedded:4.3.0'
|
||||||
implementation 'com.caverock:androidsvg-aar:1.4'
|
implementation 'com.caverock:androidsvg-aar:1.4'
|
||||||
|
|
||||||
//Protobuf
|
//Protobuf
|
||||||
implementation 'com.google.protobuf:protobuf-javalite:4.33.0'
|
implementation 'com.google.protobuf:protobuf-javalite:3.25.1'
|
||||||
|
|
||||||
implementation 'com.polycentric.core:app:1.0'
|
implementation 'com.polycentric.core:app:1.0'
|
||||||
implementation 'com.futo.futopay:app:1.0'
|
implementation 'com.futo.futopay:app:1.0'
|
||||||
implementation 'androidx.work:work-runtime-ktx:2.11.0'
|
implementation 'androidx.work:work-runtime-ktx:2.9.0'
|
||||||
implementation 'androidx.concurrent:concurrent-futures-ktx:1.3.0'
|
implementation 'androidx.concurrent:concurrent-futures-ktx:1.1.0'
|
||||||
|
|
||||||
//Database
|
//Database
|
||||||
implementation("androidx.room:room-runtime:2.8.3")
|
implementation("androidx.room:room-runtime:2.6.1")
|
||||||
ksp("androidx.room:room-compiler:2.8.3")
|
annotationProcessor("androidx.room:room-compiler:2.6.1")
|
||||||
implementation("androidx.room:room-ktx:2.8.3")
|
ksp("androidx.room:room-compiler:2.6.1")
|
||||||
|
implementation("androidx.room:room-ktx:2.6.1")
|
||||||
|
|
||||||
//Payment
|
//Payment
|
||||||
implementation 'com.stripe:stripe-android:22.0.0'
|
implementation 'com.stripe:stripe-android:20.35.1'
|
||||||
|
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2'
|
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
|
||||||
testImplementation "org.jetbrains.kotlin:kotlin-test:2.0.21"
|
testImplementation "org.jetbrains.kotlin:kotlin-test:1.8.22"
|
||||||
testImplementation "org.xmlunit:xmlunit-core:2.11.0"
|
testImplementation "org.xmlunit:xmlunit-core:2.9.1"
|
||||||
testImplementation "org.mockito:mockito-core:5.20.0"
|
testImplementation "org.mockito:mockito-core:5.4.0"
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.3.0'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||||
|
|
||||||
//Rust casting SDK
|
|
||||||
implementation('org.futo.gitlab.videostreaming.fcast-sdk-jitpack:sender-sdk-minimal:0.3.1') {
|
|
||||||
// Polycentricandroid includes this
|
|
||||||
exclude group: 'net.java.dev.jna'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
package com.futo.platformplayer
|
|
||||||
|
|
||||||
import android.graphics.Color
|
|
||||||
import org.junit.Assert.assertEquals
|
|
||||||
import org.junit.Test
|
|
||||||
import toAndroidColor
|
|
||||||
|
|
||||||
class CSSColorTests {
|
|
||||||
@Test
|
|
||||||
fun test1() {
|
|
||||||
val androidHex = "#80336699"
|
|
||||||
val androidColorInt = Color.parseColor(androidHex)
|
|
||||||
|
|
||||||
val cssHex = "#33669980"
|
|
||||||
val cssColor = CSSColor.parseColor(cssHex)
|
|
||||||
|
|
||||||
assertEquals(
|
|
||||||
"CSSColor($cssHex).toAndroidColor() should equal Color.parseColor($androidHex)",
|
|
||||||
androidColorInt,
|
|
||||||
cssColor.toAndroidColor(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun test2() {
|
|
||||||
val androidHex = "#123ABC"
|
|
||||||
val androidColorInt = Color.parseColor(androidHex)
|
|
||||||
|
|
||||||
val cssHex = "#123ABCFF"
|
|
||||||
val cssColor = CSSColor.parseColor(cssHex)
|
|
||||||
|
|
||||||
assertEquals(
|
|
||||||
"CSSColor($cssHex).toAndroidColor() should equal Color.parseColor($androidHex)",
|
|
||||||
androidColorInt,
|
|
||||||
cssColor.toAndroidColor()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,21 +3,19 @@ package com.futo.platformplayer
|
|||||||
import com.futo.platformplayer.noise.protocol.Noise
|
import com.futo.platformplayer.noise.protocol.Noise
|
||||||
import com.futo.platformplayer.sync.internal.*
|
import com.futo.platformplayer.sync.internal.*
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.selects.select
|
|
||||||
import org.junit.Assert.*
|
import org.junit.Assert.*
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.net.Socket
|
import java.net.Socket
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
import kotlin.time.Duration.Companion.seconds
|
|
||||||
/*
|
|
||||||
class SyncServerTests {
|
class SyncServerTests {
|
||||||
|
|
||||||
//private val relayHost = "relay.grayjay.app"
|
//private val relayHost = "relay.grayjay.app"
|
||||||
//private val relayKey = "xGbHRzDOvE6plRbQaFgSen82eijF+gxS0yeUaeEErkw="
|
//private val relayKey = "xGbHRzDOvE6plRbQaFgSen82eijF+gxS0yeUaeEErkw="
|
||||||
private val relayKey = "XlUaSpIlRaCg0TGzZ7JYmPupgUHDqTZXUUBco2K7ejw="
|
private val relayKey = "XlUaSpIlRaCg0TGzZ7JYmPupgUHDqTZXUUBco2K7ejw="
|
||||||
private val relayHost = "192.168.1.138"
|
private val relayHost = "192.168.1.175"
|
||||||
private val relayPort = 9000
|
private val relayPort = 9000
|
||||||
|
|
||||||
/** Creates a client connected to the live relay server. */
|
/** Creates a client connected to the live relay server. */
|
||||||
@@ -25,8 +23,7 @@ class SyncServerTests {
|
|||||||
onHandshakeComplete: ((SyncSocketSession) -> Unit)? = null,
|
onHandshakeComplete: ((SyncSocketSession) -> Unit)? = null,
|
||||||
onData: ((SyncSocketSession, UByte, UByte, ByteBuffer) -> Unit)? = null,
|
onData: ((SyncSocketSession, UByte, UByte, ByteBuffer) -> Unit)? = null,
|
||||||
onNewChannel: ((SyncSocketSession, ChannelRelayed) -> Unit)? = null,
|
onNewChannel: ((SyncSocketSession, ChannelRelayed) -> Unit)? = null,
|
||||||
isHandshakeAllowed: ((LinkType, SyncSocketSession, String, String?, UInt) -> Boolean)? = null,
|
isHandshakeAllowed: ((SyncSocketSession, String, String?) -> Boolean)? = null
|
||||||
onException: ((Throwable) -> Unit)? = null
|
|
||||||
): SyncSocketSession = withContext(Dispatchers.IO) {
|
): SyncSocketSession = withContext(Dispatchers.IO) {
|
||||||
val p = Noise.createDH("25519")
|
val p = Noise.createDH("25519")
|
||||||
p.generateKeyPair()
|
p.generateKeyPair()
|
||||||
@@ -46,14 +43,10 @@ class SyncServerTests {
|
|||||||
},
|
},
|
||||||
onData = onData ?: { _, _, _, _ -> },
|
onData = onData ?: { _, _, _, _ -> },
|
||||||
onNewChannel = onNewChannel ?: { _, _ -> },
|
onNewChannel = onNewChannel ?: { _, _ -> },
|
||||||
isHandshakeAllowed = isHandshakeAllowed ?: { _, _, _, _, _ -> true }
|
isHandshakeAllowed = isHandshakeAllowed ?: { _, _, _ -> true }
|
||||||
)
|
)
|
||||||
socketSession.authorizable = AlwaysAuthorized()
|
socketSession.authorizable = AlwaysAuthorized()
|
||||||
try {
|
socketSession.startAsInitiator(relayKey)
|
||||||
socketSession.startAsInitiator(relayKey)
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
onException?.invoke(e)
|
|
||||||
}
|
|
||||||
withTimeout(5000.milliseconds) { tcs.await() }
|
withTimeout(5000.milliseconds) { tcs.await() }
|
||||||
return@withContext socketSession
|
return@withContext socketSession
|
||||||
}
|
}
|
||||||
@@ -266,73 +259,8 @@ class SyncServerTests {
|
|||||||
clientA.stop()
|
clientA.stop()
|
||||||
clientB.stop()
|
clientB.stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun relayedTransport_WithValidAppId_Success() = runBlocking {
|
|
||||||
// Arrange: Set up clients
|
|
||||||
val allowedAppId = 1234u
|
|
||||||
val tcsB = CompletableDeferred<ChannelRelayed>()
|
|
||||||
|
|
||||||
// Client B requires appId 1234
|
|
||||||
val clientB = createClient(
|
|
||||||
onNewChannel = { _, c -> tcsB.complete(c) },
|
|
||||||
isHandshakeAllowed = { linkType, _, _, _, appId -> linkType == LinkType.Relayed && appId == allowedAppId }
|
|
||||||
)
|
|
||||||
|
|
||||||
val clientA = createClient()
|
|
||||||
|
|
||||||
// Act: Start relayed channel with valid appId
|
|
||||||
val channelTask = async { clientA.startRelayedChannel(clientB.localPublicKey, appId = allowedAppId) }
|
|
||||||
val channelB = withTimeout(5.seconds) { tcsB.await() }
|
|
||||||
withTimeout(5.seconds) { channelTask.await() }
|
|
||||||
|
|
||||||
// Assert: Channel is established
|
|
||||||
assertNotNull("Channel should be created on target with valid appId", channelB)
|
|
||||||
|
|
||||||
// Clean up
|
|
||||||
clientA.stop()
|
|
||||||
clientB.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun relayedTransport_WithInvalidAppId_Fails() = runBlocking {
|
|
||||||
// Arrange: Set up clients
|
|
||||||
val allowedAppId = 1234u
|
|
||||||
val invalidAppId = 5678u
|
|
||||||
val tcsB = CompletableDeferred<ChannelRelayed>()
|
|
||||||
|
|
||||||
// Client B requires appId 1234
|
|
||||||
val clientB = createClient(
|
|
||||||
onNewChannel = { _, c -> tcsB.complete(c) },
|
|
||||||
isHandshakeAllowed = { linkType, _, _, _, appId -> linkType == LinkType.Relayed && appId == allowedAppId },
|
|
||||||
onException = { }
|
|
||||||
)
|
|
||||||
|
|
||||||
val clientA = createClient()
|
|
||||||
|
|
||||||
// Act & Assert: Attempt with invalid appId should fail
|
|
||||||
try {
|
|
||||||
withTimeout(5.seconds) {
|
|
||||||
clientA.startRelayedChannel(clientB.localPublicKey, appId = invalidAppId)
|
|
||||||
}
|
|
||||||
fail("Starting relayed channel with invalid appId should fail")
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
// Expected: The channel creation should time out or fail
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure no channel was created on client B
|
|
||||||
val completedTask = select {
|
|
||||||
tcsB.onAwait { "channel" }
|
|
||||||
async { delay(1.seconds); "timeout" }.onAwait { "timeout" }
|
|
||||||
}
|
|
||||||
assertEquals("No channel should be created with invalid appId", "timeout", completedTask)
|
|
||||||
|
|
||||||
// Clean up
|
|
||||||
clientA.stop()
|
|
||||||
clientB.stop()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class AlwaysAuthorized : IAuthorizable {
|
class AlwaysAuthorized : IAuthorizable {
|
||||||
override val isAuthorized: Boolean get() = true
|
override val isAuthorized: Boolean get() = true
|
||||||
}*/
|
}
|
||||||
@@ -1,512 +0,0 @@
|
|||||||
package com.futo.platformplayer
|
|
||||||
|
|
||||||
import com.futo.platformplayer.noise.protocol.DHState
|
|
||||||
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.io.PipedInputStream
|
|
||||||
import java.io.PipedOutputStream
|
|
||||||
import java.nio.ByteBuffer
|
|
||||||
import kotlin.random.Random
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.io.OutputStream
|
|
||||||
import kotlin.time.Duration.Companion.seconds
|
|
||||||
/*
|
|
||||||
data class PipeStreams(
|
|
||||||
val initiatorInput: LittleEndianDataInputStream,
|
|
||||||
val initiatorOutput: LittleEndianDataOutputStream,
|
|
||||||
val responderInput: LittleEndianDataInputStream,
|
|
||||||
val responderOutput: LittleEndianDataOutputStream
|
|
||||||
)
|
|
||||||
|
|
||||||
typealias OnHandshakeComplete = (SyncSocketSession) -> Unit
|
|
||||||
typealias IsHandshakeAllowed = (LinkType, SyncSocketSession, String, String?, UInt) -> Boolean
|
|
||||||
typealias OnClose = (SyncSocketSession) -> Unit
|
|
||||||
typealias OnData = (SyncSocketSession, UByte, UByte, ByteBuffer) -> Unit
|
|
||||||
|
|
||||||
class SyncSocketTests {
|
|
||||||
private fun createPipeStreams(): PipeStreams {
|
|
||||||
val initiatorOutput = PipedOutputStream()
|
|
||||||
val responderOutput = PipedOutputStream()
|
|
||||||
val responderInput = PipedInputStream(initiatorOutput)
|
|
||||||
val initiatorInput = PipedInputStream(responderOutput)
|
|
||||||
return PipeStreams(
|
|
||||||
LittleEndianDataInputStream(initiatorInput), LittleEndianDataOutputStream(initiatorOutput),
|
|
||||||
LittleEndianDataInputStream(responderInput), LittleEndianDataOutputStream(responderOutput)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun generateKeyPair(): DHState {
|
|
||||||
val p = Noise.createDH("25519")
|
|
||||||
p.generateKeyPair()
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createSessions(
|
|
||||||
initiatorInput: LittleEndianDataInputStream,
|
|
||||||
initiatorOutput: LittleEndianDataOutputStream,
|
|
||||||
responderInput: LittleEndianDataInputStream,
|
|
||||||
responderOutput: LittleEndianDataOutputStream,
|
|
||||||
initiatorKeyPair: DHState,
|
|
||||||
responderKeyPair: DHState,
|
|
||||||
onInitiatorHandshakeComplete: OnHandshakeComplete,
|
|
||||||
onResponderHandshakeComplete: OnHandshakeComplete,
|
|
||||||
onInitiatorClose: OnClose? = null,
|
|
||||||
onResponderClose: OnClose? = null,
|
|
||||||
onClose: OnClose? = null,
|
|
||||||
isHandshakeAllowed: IsHandshakeAllowed? = null,
|
|
||||||
onDataA: OnData? = null,
|
|
||||||
onDataB: OnData? = null
|
|
||||||
): Pair<SyncSocketSession, SyncSocketSession> {
|
|
||||||
val initiatorSession = SyncSocketSession(
|
|
||||||
"", initiatorKeyPair, initiatorInput, initiatorOutput,
|
|
||||||
onClose = {
|
|
||||||
onClose?.invoke(it)
|
|
||||||
onInitiatorClose?.invoke(it)
|
|
||||||
},
|
|
||||||
onHandshakeComplete = onInitiatorHandshakeComplete,
|
|
||||||
onData = onDataA,
|
|
||||||
isHandshakeAllowed = isHandshakeAllowed
|
|
||||||
)
|
|
||||||
|
|
||||||
val responderSession = SyncSocketSession(
|
|
||||||
"", responderKeyPair, responderInput, responderOutput,
|
|
||||||
onClose = {
|
|
||||||
onClose?.invoke(it)
|
|
||||||
onResponderClose?.invoke(it)
|
|
||||||
},
|
|
||||||
onHandshakeComplete = onResponderHandshakeComplete,
|
|
||||||
onData = onDataB,
|
|
||||||
isHandshakeAllowed = isHandshakeAllowed
|
|
||||||
)
|
|
||||||
|
|
||||||
return Pair(initiatorSession, responderSession)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun handshake_WithValidPairingCode_Succeeds(): Unit = runBlocking {
|
|
||||||
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
|
|
||||||
val initiatorKeyPair = generateKeyPair()
|
|
||||||
val responderKeyPair = generateKeyPair()
|
|
||||||
val validPairingCode = "secret"
|
|
||||||
|
|
||||||
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
|
|
||||||
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
|
|
||||||
|
|
||||||
val (initiatorSession, responderSession) = createSessions(
|
|
||||||
initiatorInput, initiatorOutput, responderInput, responderOutput,
|
|
||||||
initiatorKeyPair, responderKeyPair,
|
|
||||||
{ handshakeInitiatorCompleted.complete(true) },
|
|
||||||
{ handshakeResponderCompleted.complete(true) },
|
|
||||||
isHandshakeAllowed = { _, _, _, pairingCode, _ -> pairingCode == validPairingCode }
|
|
||||||
)
|
|
||||||
|
|
||||||
initiatorSession.startAsInitiator(responderSession.localPublicKey, pairingCode = validPairingCode)
|
|
||||||
responderSession.startAsResponder()
|
|
||||||
|
|
||||||
withTimeout(5.seconds) {
|
|
||||||
handshakeInitiatorCompleted.await()
|
|
||||||
handshakeResponderCompleted.await()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun handshake_WithInvalidPairingCode_Fails() = runBlocking {
|
|
||||||
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
|
|
||||||
val initiatorKeyPair = generateKeyPair()
|
|
||||||
val responderKeyPair = generateKeyPair()
|
|
||||||
val validPairingCode = "secret"
|
|
||||||
val invalidPairingCode = "wrong"
|
|
||||||
|
|
||||||
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
|
|
||||||
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
|
|
||||||
val initiatorClosed = CompletableDeferred<Boolean>()
|
|
||||||
val responderClosed = CompletableDeferred<Boolean>()
|
|
||||||
|
|
||||||
val (initiatorSession, responderSession) = createSessions(
|
|
||||||
initiatorInput, initiatorOutput, responderInput, responderOutput,
|
|
||||||
initiatorKeyPair, responderKeyPair,
|
|
||||||
{ handshakeInitiatorCompleted.complete(true) },
|
|
||||||
{ handshakeResponderCompleted.complete(true) },
|
|
||||||
onInitiatorClose = {
|
|
||||||
initiatorClosed.complete(true)
|
|
||||||
},
|
|
||||||
onResponderClose = {
|
|
||||||
responderClosed.complete(true)
|
|
||||||
},
|
|
||||||
isHandshakeAllowed = { _, _, _, pairingCode, _ -> pairingCode == validPairingCode }
|
|
||||||
)
|
|
||||||
|
|
||||||
initiatorSession.startAsInitiator(responderSession.localPublicKey, pairingCode = invalidPairingCode)
|
|
||||||
responderSession.startAsResponder()
|
|
||||||
|
|
||||||
withTimeout(100.seconds) {
|
|
||||||
initiatorClosed.await()
|
|
||||||
responderClosed.await()
|
|
||||||
}
|
|
||||||
|
|
||||||
assertFalse(handshakeInitiatorCompleted.isCompleted)
|
|
||||||
assertFalse(handshakeResponderCompleted.isCompleted)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun handshake_WithoutPairingCodeWhenRequired_Fails() = runBlocking {
|
|
||||||
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
|
|
||||||
val initiatorKeyPair = generateKeyPair()
|
|
||||||
val responderKeyPair = generateKeyPair()
|
|
||||||
val validPairingCode = "secret"
|
|
||||||
|
|
||||||
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
|
|
||||||
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
|
|
||||||
val initiatorClosed = CompletableDeferred<Boolean>()
|
|
||||||
val responderClosed = CompletableDeferred<Boolean>()
|
|
||||||
|
|
||||||
val (initiatorSession, responderSession) = createSessions(
|
|
||||||
initiatorInput, initiatorOutput, responderInput, responderOutput,
|
|
||||||
initiatorKeyPair, responderKeyPair,
|
|
||||||
{ handshakeInitiatorCompleted.complete(true) },
|
|
||||||
{ handshakeResponderCompleted.complete(true) },
|
|
||||||
onInitiatorClose = {
|
|
||||||
initiatorClosed.complete(true)
|
|
||||||
},
|
|
||||||
onResponderClose = {
|
|
||||||
responderClosed.complete(true)
|
|
||||||
},
|
|
||||||
isHandshakeAllowed = { _, _, _, pairingCode, _ -> pairingCode == validPairingCode }
|
|
||||||
)
|
|
||||||
|
|
||||||
initiatorSession.startAsInitiator(responderSession.localPublicKey) // No pairing code
|
|
||||||
responderSession.startAsResponder()
|
|
||||||
|
|
||||||
withTimeout(5.seconds) {
|
|
||||||
initiatorClosed.await()
|
|
||||||
responderClosed.await()
|
|
||||||
}
|
|
||||||
|
|
||||||
assertFalse(handshakeInitiatorCompleted.isCompleted)
|
|
||||||
assertFalse(handshakeResponderCompleted.isCompleted)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun handshake_WithPairingCodeWhenNotRequired_Succeeds(): Unit = runBlocking {
|
|
||||||
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
|
|
||||||
val initiatorKeyPair = generateKeyPair()
|
|
||||||
val responderKeyPair = generateKeyPair()
|
|
||||||
val pairingCode = "unnecessary"
|
|
||||||
|
|
||||||
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
|
|
||||||
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
|
|
||||||
|
|
||||||
val (initiatorSession, responderSession) = createSessions(
|
|
||||||
initiatorInput, initiatorOutput, responderInput, responderOutput,
|
|
||||||
initiatorKeyPair, responderKeyPair,
|
|
||||||
{ handshakeInitiatorCompleted.complete(true) },
|
|
||||||
{ handshakeResponderCompleted.complete(true) },
|
|
||||||
isHandshakeAllowed = { _, _, _, _, _ -> true } // Always allow
|
|
||||||
)
|
|
||||||
|
|
||||||
initiatorSession.startAsInitiator(responderSession.localPublicKey, pairingCode = pairingCode)
|
|
||||||
responderSession.startAsResponder()
|
|
||||||
|
|
||||||
withTimeout(10.seconds) {
|
|
||||||
handshakeInitiatorCompleted.await()
|
|
||||||
handshakeResponderCompleted.await()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun sendAndReceive_SmallDataPacket_Succeeds() = runBlocking {
|
|
||||||
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
|
|
||||||
val initiatorKeyPair = generateKeyPair()
|
|
||||||
val responderKeyPair = generateKeyPair()
|
|
||||||
|
|
||||||
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
|
|
||||||
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
|
|
||||||
val tcsDataReceived = CompletableDeferred<ByteArray>()
|
|
||||||
|
|
||||||
val (initiatorSession, responderSession) = createSessions(
|
|
||||||
initiatorInput, initiatorOutput, responderInput, responderOutput,
|
|
||||||
initiatorKeyPair, responderKeyPair,
|
|
||||||
{ handshakeInitiatorCompleted.complete(true) },
|
|
||||||
{ handshakeResponderCompleted.complete(true) },
|
|
||||||
onDataB = { _, opcode, subOpcode, data ->
|
|
||||||
if (opcode == Opcode.DATA.value && subOpcode == 0u.toUByte()) {
|
|
||||||
val b = ByteArray(data.remaining())
|
|
||||||
data.get(b)
|
|
||||||
tcsDataReceived.complete(b)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
initiatorSession.startAsInitiator(responderSession.localPublicKey)
|
|
||||||
responderSession.startAsResponder()
|
|
||||||
|
|
||||||
withTimeout(10.seconds) {
|
|
||||||
handshakeInitiatorCompleted.await()
|
|
||||||
handshakeResponderCompleted.await()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure both sessions are authorized
|
|
||||||
initiatorSession.authorizable = Authorized()
|
|
||||||
responderSession.authorizable = Authorized()
|
|
||||||
|
|
||||||
val smallData = byteArrayOf(1, 2, 3)
|
|
||||||
initiatorSession.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(smallData))
|
|
||||||
|
|
||||||
val receivedData = withTimeout(10.seconds) { tcsDataReceived.await() }
|
|
||||||
assertArrayEquals(smallData, receivedData)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun sendAndReceive_ExactlyMaximumPacketSize_Succeeds() = runBlocking {
|
|
||||||
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
|
|
||||||
val initiatorKeyPair = generateKeyPair()
|
|
||||||
val responderKeyPair = generateKeyPair()
|
|
||||||
|
|
||||||
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
|
|
||||||
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
|
|
||||||
val tcsDataReceived = CompletableDeferred<ByteArray>()
|
|
||||||
|
|
||||||
val (initiatorSession, responderSession) = createSessions(
|
|
||||||
initiatorInput, initiatorOutput, responderInput, responderOutput,
|
|
||||||
initiatorKeyPair, responderKeyPair,
|
|
||||||
{ handshakeInitiatorCompleted.complete(true) },
|
|
||||||
{ handshakeResponderCompleted.complete(true) },
|
|
||||||
onDataB = { _, opcode, subOpcode, data ->
|
|
||||||
if (opcode == Opcode.DATA.value && subOpcode == 0u.toUByte()) {
|
|
||||||
val b = ByteArray(data.remaining())
|
|
||||||
data.get(b)
|
|
||||||
tcsDataReceived.complete(b)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
initiatorSession.startAsInitiator(responderSession.localPublicKey)
|
|
||||||
responderSession.startAsResponder()
|
|
||||||
|
|
||||||
withTimeout(10.seconds) {
|
|
||||||
handshakeInitiatorCompleted.await()
|
|
||||||
handshakeResponderCompleted.await()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure both sessions are authorized
|
|
||||||
initiatorSession.authorizable = Authorized()
|
|
||||||
responderSession.authorizable = Authorized()
|
|
||||||
|
|
||||||
val maxData = ByteArray(SyncSocketSession.MAXIMUM_PACKET_SIZE - SyncSocketSession.HEADER_SIZE).apply { Random.nextBytes(this) }
|
|
||||||
initiatorSession.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(maxData))
|
|
||||||
|
|
||||||
val receivedData = withTimeout(10.seconds) { tcsDataReceived.await() }
|
|
||||||
assertArrayEquals(maxData, receivedData)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun stream_LargeData_Succeeds() = runBlocking {
|
|
||||||
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
|
|
||||||
val initiatorKeyPair = generateKeyPair()
|
|
||||||
val responderKeyPair = generateKeyPair()
|
|
||||||
|
|
||||||
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
|
|
||||||
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
|
|
||||||
val tcsDataReceived = CompletableDeferred<ByteArray>()
|
|
||||||
|
|
||||||
val (initiatorSession, responderSession) = createSessions(
|
|
||||||
initiatorInput, initiatorOutput, responderInput, responderOutput,
|
|
||||||
initiatorKeyPair, responderKeyPair,
|
|
||||||
{ handshakeInitiatorCompleted.complete(true) },
|
|
||||||
{ handshakeResponderCompleted.complete(true) },
|
|
||||||
onDataB = { _, opcode, subOpcode, data ->
|
|
||||||
if (opcode == Opcode.DATA.value && subOpcode == 0u.toUByte()) {
|
|
||||||
val b = ByteArray(data.remaining())
|
|
||||||
data.get(b)
|
|
||||||
tcsDataReceived.complete(b)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
initiatorSession.startAsInitiator(responderSession.localPublicKey)
|
|
||||||
responderSession.startAsResponder()
|
|
||||||
|
|
||||||
withTimeout(10.seconds) {
|
|
||||||
handshakeInitiatorCompleted.await()
|
|
||||||
handshakeResponderCompleted.await()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure both sessions are authorized
|
|
||||||
initiatorSession.authorizable = Authorized()
|
|
||||||
responderSession.authorizable = Authorized()
|
|
||||||
|
|
||||||
val largeData = ByteArray(2 * (SyncSocketSession.MAXIMUM_PACKET_SIZE - SyncSocketSession.HEADER_SIZE)).apply { Random.nextBytes(this) }
|
|
||||||
initiatorSession.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(largeData))
|
|
||||||
|
|
||||||
val receivedData = withTimeout(10.seconds) { tcsDataReceived.await() }
|
|
||||||
assertArrayEquals(largeData, receivedData)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun authorizedSession_CanSendData() = runBlocking {
|
|
||||||
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
|
|
||||||
val initiatorKeyPair = generateKeyPair()
|
|
||||||
val responderKeyPair = generateKeyPair()
|
|
||||||
|
|
||||||
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
|
|
||||||
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
|
|
||||||
val tcsDataReceived = CompletableDeferred<ByteArray>()
|
|
||||||
|
|
||||||
val (initiatorSession, responderSession) = createSessions(
|
|
||||||
initiatorInput, initiatorOutput, responderInput, responderOutput,
|
|
||||||
initiatorKeyPair, responderKeyPair,
|
|
||||||
{ handshakeInitiatorCompleted.complete(true) },
|
|
||||||
{ handshakeResponderCompleted.complete(true) },
|
|
||||||
onDataB = { _, opcode, subOpcode, data ->
|
|
||||||
if (opcode == Opcode.DATA.value && subOpcode == 0u.toUByte()) {
|
|
||||||
val b = ByteArray(data.remaining())
|
|
||||||
data.get(b)
|
|
||||||
tcsDataReceived.complete(b)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
initiatorSession.startAsInitiator(responderSession.localPublicKey)
|
|
||||||
responderSession.startAsResponder()
|
|
||||||
|
|
||||||
withTimeout(10.seconds) {
|
|
||||||
handshakeInitiatorCompleted.await()
|
|
||||||
handshakeResponderCompleted.await()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Authorize both sessions
|
|
||||||
initiatorSession.authorizable = Authorized()
|
|
||||||
responderSession.authorizable = Authorized()
|
|
||||||
|
|
||||||
val data = byteArrayOf(1, 2, 3)
|
|
||||||
initiatorSession.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(data))
|
|
||||||
|
|
||||||
val receivedData = withTimeout(10.seconds) { tcsDataReceived.await() }
|
|
||||||
assertArrayEquals(data, receivedData)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun unauthorizedSession_CannotSendData() = runBlocking {
|
|
||||||
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
|
|
||||||
val initiatorKeyPair = generateKeyPair()
|
|
||||||
val responderKeyPair = generateKeyPair()
|
|
||||||
|
|
||||||
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
|
|
||||||
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
|
|
||||||
val tcsDataReceived = CompletableDeferred<ByteArray>()
|
|
||||||
|
|
||||||
val (initiatorSession, responderSession) = createSessions(
|
|
||||||
initiatorInput, initiatorOutput, responderInput, responderOutput,
|
|
||||||
initiatorKeyPair, responderKeyPair,
|
|
||||||
{ handshakeInitiatorCompleted.complete(true) },
|
|
||||||
{ handshakeResponderCompleted.complete(true) },
|
|
||||||
onDataB = { _, _, _, _ -> }
|
|
||||||
)
|
|
||||||
|
|
||||||
initiatorSession.startAsInitiator(responderSession.localPublicKey)
|
|
||||||
responderSession.startAsResponder()
|
|
||||||
|
|
||||||
withTimeout(10.seconds) {
|
|
||||||
handshakeInitiatorCompleted.await()
|
|
||||||
handshakeResponderCompleted.await()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Authorize initiator but not responder
|
|
||||||
initiatorSession.authorizable = Authorized()
|
|
||||||
responderSession.authorizable = Unauthorized()
|
|
||||||
|
|
||||||
val data = byteArrayOf(1, 2, 3)
|
|
||||||
initiatorSession.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(data))
|
|
||||||
|
|
||||||
delay(1.seconds)
|
|
||||||
assertFalse(tcsDataReceived.isCompleted)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun directHandshake_WithValidAppId_Succeeds() = runBlocking {
|
|
||||||
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
|
|
||||||
val initiatorKeyPair = generateKeyPair()
|
|
||||||
val responderKeyPair = generateKeyPair()
|
|
||||||
val allowedAppId = 1234u
|
|
||||||
|
|
||||||
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
|
|
||||||
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
|
|
||||||
|
|
||||||
val responderIsHandshakeAllowed = { linkType: LinkType, _: SyncSocketSession, _: String, _: String?, appId: UInt ->
|
|
||||||
linkType == LinkType.Direct && appId == allowedAppId
|
|
||||||
}
|
|
||||||
|
|
||||||
val (initiatorSession, responderSession) = createSessions(
|
|
||||||
initiatorInput, initiatorOutput, responderInput, responderOutput,
|
|
||||||
initiatorKeyPair, responderKeyPair,
|
|
||||||
{ handshakeInitiatorCompleted.complete(true) },
|
|
||||||
{ handshakeResponderCompleted.complete(true) },
|
|
||||||
isHandshakeAllowed = responderIsHandshakeAllowed
|
|
||||||
)
|
|
||||||
|
|
||||||
initiatorSession.startAsInitiator(responderSession.localPublicKey, appId = allowedAppId)
|
|
||||||
responderSession.startAsResponder()
|
|
||||||
|
|
||||||
withTimeout(5.seconds) {
|
|
||||||
handshakeInitiatorCompleted.await()
|
|
||||||
handshakeResponderCompleted.await()
|
|
||||||
}
|
|
||||||
|
|
||||||
assertNotNull(initiatorSession.remotePublicKey)
|
|
||||||
assertNotNull(responderSession.remotePublicKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun directHandshake_WithInvalidAppId_Fails() = runBlocking {
|
|
||||||
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
|
|
||||||
val initiatorKeyPair = generateKeyPair()
|
|
||||||
val responderKeyPair = generateKeyPair()
|
|
||||||
val allowedAppId = 1234u
|
|
||||||
val invalidAppId = 5678u
|
|
||||||
|
|
||||||
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
|
|
||||||
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
|
|
||||||
val initiatorClosed = CompletableDeferred<Boolean>()
|
|
||||||
val responderClosed = CompletableDeferred<Boolean>()
|
|
||||||
|
|
||||||
val responderIsHandshakeAllowed = { linkType: LinkType, _: SyncSocketSession, _: String, _: String?, appId: UInt ->
|
|
||||||
linkType == LinkType.Direct && appId == allowedAppId
|
|
||||||
}
|
|
||||||
|
|
||||||
val (initiatorSession, responderSession) = createSessions(
|
|
||||||
initiatorInput, initiatorOutput, responderInput, responderOutput,
|
|
||||||
initiatorKeyPair, responderKeyPair,
|
|
||||||
{ handshakeInitiatorCompleted.complete(true) },
|
|
||||||
{ handshakeResponderCompleted.complete(true) },
|
|
||||||
onInitiatorClose = {
|
|
||||||
initiatorClosed.complete(true)
|
|
||||||
},
|
|
||||||
onResponderClose = {
|
|
||||||
responderClosed.complete(true)
|
|
||||||
},
|
|
||||||
isHandshakeAllowed = responderIsHandshakeAllowed
|
|
||||||
)
|
|
||||||
|
|
||||||
initiatorSession.startAsInitiator(responderSession.localPublicKey, appId = invalidAppId)
|
|
||||||
responderSession.startAsResponder()
|
|
||||||
|
|
||||||
withTimeout(5.seconds) {
|
|
||||||
initiatorClosed.await()
|
|
||||||
responderClosed.await()
|
|
||||||
}
|
|
||||||
|
|
||||||
assertFalse(handshakeInitiatorCompleted.isCompleted)
|
|
||||||
assertFalse(handshakeResponderCompleted.isCompleted)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Authorized : IAuthorizable {
|
|
||||||
override val isAuthorized: Boolean = true
|
|
||||||
}
|
|
||||||
|
|
||||||
class Unauthorized : IAuthorizable {
|
|
||||||
override val isAuthorized: Boolean = false
|
|
||||||
}*/
|
|
||||||
@@ -16,9 +16,6 @@
|
|||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
|
||||||
<uses-permission android:name="android.permission.WRITE_SETTINGS" tools:ignore="ProtectedPermissions"/>
|
<uses-permission android:name="android.permission.WRITE_SETTINGS" tools:ignore="ProtectedPermissions"/>
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
@@ -29,8 +26,6 @@
|
|||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.FutoVideo"
|
android:theme="@style/Theme.FutoVideo"
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
tools:replace="android:enableOnBackInvokedCallback"
|
|
||||||
android:enableOnBackInvokedCallback="false"
|
|
||||||
tools:targetApi="31"
|
tools:targetApi="31"
|
||||||
android:largeHeap="true">
|
android:largeHeap="true">
|
||||||
<provider
|
<provider
|
||||||
@@ -60,10 +55,9 @@
|
|||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.MainActivity"
|
android:name=".activities.MainActivity"
|
||||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout|uiMode"
|
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar"
|
android:theme="@style/Theme.FutoVideo.NoActionBar"
|
||||||
android:windowSoftInputMode="adjustPan"
|
|
||||||
android:launchMode="singleInstance"
|
android:launchMode="singleInstance"
|
||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
android:supportsPictureInPicture="true">
|
android:supportsPictureInPicture="true">
|
||||||
@@ -159,30 +153,30 @@
|
|||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.TestActivity"
|
android:name=".activities.TestActivity"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.SettingsActivity"
|
android:name=".activities.SettingsActivity"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.DeveloperActivity"
|
android:name=".activities.DeveloperActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.ExceptionActivity"
|
android:name=".activities.ExceptionActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.CaptchaActivity"
|
android:name=".activities.CaptchaActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.LoginActivity"
|
android:name=".activities.LoginActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.AddSourceActivity"
|
android:name=".activities.AddSourceActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem">
|
android:theme="@style/Theme.FutoVideo.NoActionBar">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
@@ -195,78 +189,54 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name=".activities.AddSourceOptionsActivity"
|
android:name=".activities.AddSourceOptionsActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.PolycentricHomeActivity"
|
android:name=".activities.PolycentricHomeActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.PolycentricBackupActivity"
|
android:name=".activities.PolycentricBackupActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.PolycentricCreateProfileActivity"
|
android:name=".activities.PolycentricCreateProfileActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.PolycentricProfileActivity"
|
android:name=".activities.PolycentricProfileActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.PolycentricWhyActivity"
|
android:name=".activities.PolycentricWhyActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.PolycentricImportProfileActivity"
|
android:name=".activities.PolycentricImportProfileActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.ManageTabsActivity"
|
android:name=".activities.ManageTabsActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.QRCaptureActivity"
|
android:name=".activities.QRCaptureActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.FCastGuideActivity"
|
android:name=".activities.FCastGuideActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.SyncHomeActivity"
|
android:name=".activities.SyncHomeActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.SyncPairActivity"
|
android:name=".activities.SyncPairActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.SyncShowPairingCodeActivity"
|
android:name=".activities.SyncShowPairingCodeActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:screenOrientation="sensorPortrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
|
||||||
<activity
|
|
||||||
android:name=".activities.PolycentricModerationActivity"
|
|
||||||
android:exported="false"
|
|
||||||
android:screenOrientation="portrait" />
|
|
||||||
<activity
|
|
||||||
android:name=".activities.QRCodeFullscreenActivity"
|
|
||||||
android:screenOrientation="sensorPortrait"
|
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<service
|
|
||||||
android:name=".UpdateDownloadService"
|
|
||||||
android:exported="false"
|
|
||||||
android:foregroundServiceType="dataSync" />
|
|
||||||
|
|
||||||
<receiver
|
|
||||||
android:name=".UpdateActionReceiver"
|
|
||||||
android:exported="false" />
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".activities.InstallUpdateActivity"
|
|
||||||
android:exported="false"
|
|
||||||
android:theme="@style/Theme.App.TransparentNoUi"
|
|
||||||
android:excludeFromRecents="true"
|
|
||||||
android:finishOnTaskLaunch="true" />
|
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -1022,38 +1022,15 @@
|
|||||||
return x.value
|
return x.value
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
let settingsToUse = __DEV_SETTINGS ?? {};
|
|
||||||
if (true) {
|
|
||||||
const settings = this.Plugin?.currentPlugin?.settings;
|
|
||||||
if (settings) {
|
|
||||||
for (let setting of settings) {
|
|
||||||
if (typeof settingsToUse[setting.variable] == "undefined") {
|
|
||||||
switch (setting?.type?.toLowerCase()) {
|
|
||||||
case "boolean":
|
|
||||||
settingsToUse[setting.variable] = setting.default === 'true';
|
|
||||||
break;
|
|
||||||
case "dropdown":
|
|
||||||
let dropDownIndex = parseInt(setting.default);
|
|
||||||
if (dropDownIndex) {
|
|
||||||
settingsToUse[setting.variable] = setting.options[dropDownIndex];
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(name == "enable") {
|
if(name == "enable") {
|
||||||
if(parameterVals.length > 0)
|
if(parameterVals.length > 0)
|
||||||
parameterVals[0] = this.Plugin.currentPlugin;
|
parameterVals[0] = this.Plugin.currentPlugin;
|
||||||
else
|
else
|
||||||
parameterVals.push(this.Plugin.currentPlugin);
|
parameterVals.push(this.Plugin.currentPlugin);
|
||||||
if(parameterVals.length > 1)
|
if(parameterVals.length > 1)
|
||||||
parameterVals[1] = settingsToUse;
|
parameterVals[1] = __DEV_SETTINGS;
|
||||||
else
|
else
|
||||||
parameterVals.push(settingsToUse);
|
parameterVals.push(__DEV_SETTINGS);
|
||||||
}
|
}
|
||||||
|
|
||||||
const func = source[name];
|
const func = source[name];
|
||||||
|
|||||||
@@ -32,8 +32,7 @@ let Type = {
|
|||||||
Text: {
|
Text: {
|
||||||
RAW: 0,
|
RAW: 0,
|
||||||
HTML: 1,
|
HTML: 1,
|
||||||
MARKUP: 2,
|
MARKUP: 2
|
||||||
CODE: 3
|
|
||||||
},
|
},
|
||||||
Chapter: {
|
Chapter: {
|
||||||
NORMAL: 0,
|
NORMAL: 0,
|
||||||
@@ -67,7 +66,6 @@ class ScriptException extends Error {
|
|||||||
super(arguments[0]);
|
super(arguments[0]);
|
||||||
this.plugin_type = "ScriptException";
|
this.plugin_type = "ScriptException";
|
||||||
this.message = arguments[0];
|
this.message = arguments[0];
|
||||||
this.msg = arguments[0];
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
super(msg);
|
super(msg);
|
||||||
@@ -104,12 +102,6 @@ class UnavailableException extends ScriptException {
|
|||||||
super("UnavailableException", msg);
|
super("UnavailableException", msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class ReloadRequiredException extends ScriptException {
|
|
||||||
constructor(msg, reloadData) {
|
|
||||||
super("ReloadRequiredException", msg);
|
|
||||||
this.reloadData = reloadData;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
class AgeException extends ScriptException {
|
class AgeException extends ScriptException {
|
||||||
constructor(msg) {
|
constructor(msg) {
|
||||||
super("AgeException", msg);
|
super("AgeException", msg);
|
||||||
@@ -252,9 +244,6 @@ class PlatformVideo extends PlatformContent {
|
|||||||
this.duration = obj.duration ?? -1; //Long
|
this.duration = obj.duration ?? -1; //Long
|
||||||
this.viewCount = obj.viewCount ?? -1; //Long
|
this.viewCount = obj.viewCount ?? -1; //Long
|
||||||
|
|
||||||
this.playbackTime = obj.playbackTime ?? -1;
|
|
||||||
this.playbackDate = obj.playbackDate ?? undefined;
|
|
||||||
|
|
||||||
this.isLive = obj.isLive ?? false; //Boolean
|
this.isLive = obj.isLive ?? false; //Boolean
|
||||||
this.isShort = !!obj.isShort ?? false;
|
this.isShort = !!obj.isShort ?? false;
|
||||||
}
|
}
|
||||||
@@ -302,39 +291,15 @@ class PlatformPostDetails extends PlatformPost {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PlatformWeb extends PlatformContent {
|
class PlatformArticleDetails extends PlatformContent {
|
||||||
constructor(obj) {
|
|
||||||
super(obj, 7);
|
|
||||||
obj = obj ?? {};
|
|
||||||
this.plugin_type = "PlatformWeb";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
class PlatformWebDetails extends PlatformWeb {
|
|
||||||
constructor(obj) {
|
|
||||||
super(obj, 7);
|
|
||||||
obj = obj ?? {};
|
|
||||||
this.plugin_type = "PlatformWebDetails";
|
|
||||||
this.html = obj.html;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class PlatformArticle extends PlatformContent {
|
|
||||||
constructor(obj) {
|
|
||||||
super(obj, 3);
|
|
||||||
obj = obj ?? {};
|
|
||||||
this.plugin_type = "PlatformArticle";
|
|
||||||
this.rating = obj.rating ?? new RatingLikes(-1);
|
|
||||||
this.summary = obj.summary ?? "";
|
|
||||||
this.thumbnails = obj.thumbnails ?? new Thumbnails([]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
class PlatformArticleDetails extends PlatformArticle {
|
|
||||||
constructor(obj) {
|
constructor(obj) {
|
||||||
super(obj, 3);
|
super(obj, 3);
|
||||||
obj = obj ?? {};
|
obj = obj ?? {};
|
||||||
this.plugin_type = "PlatformArticleDetails";
|
this.plugin_type = "PlatformArticleDetails";
|
||||||
this.rating = obj.rating ?? new RatingLikes(-1);
|
this.rating = obj.rating ?? new RatingLikes(-1);
|
||||||
|
this.summary = obj.summary ?? "";
|
||||||
this.segments = obj.segments ?? [];
|
this.segments = obj.segments ?? [];
|
||||||
|
this.thumbnails = obj.thumbnails ?? new Thumbnails([]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class ArticleSegment {
|
class ArticleSegment {
|
||||||
@@ -350,17 +315,9 @@ class ArticleTextSegment extends ArticleSegment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
class ArticleImagesSegment extends ArticleSegment {
|
class ArticleImagesSegment extends ArticleSegment {
|
||||||
constructor(images, caption) {
|
constructor(images) {
|
||||||
super(2);
|
super(2);
|
||||||
this.images = images;
|
this.images = images;
|
||||||
this.caption = caption;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
class ArticleHeaderSegment extends ArticleSegment {
|
|
||||||
constructor(content, level) {
|
|
||||||
super(3);
|
|
||||||
this.level = level;
|
|
||||||
this.content = content;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class ArticleNestedSegment extends ArticleSegment {
|
class ArticleNestedSegment extends ArticleSegment {
|
||||||
@@ -468,20 +425,14 @@ class AudioUrlWidevineSource extends AudioUrlSource {
|
|||||||
this.getLicenseRequestExecutor = () => {
|
this.getLicenseRequestExecutor = () => {
|
||||||
return {
|
return {
|
||||||
executeRequest: (url, _headers, _method, license_request_data) => {
|
executeRequest: (url, _headers, _method, license_request_data) => {
|
||||||
const response = http.POST(
|
return http.POST(
|
||||||
url,
|
url,
|
||||||
license_request_data,
|
license_request_data,
|
||||||
{ Authorization: `Bearer ${obj.bearerToken}` },
|
{ Authorization: `Bearer ${obj.bearerToken}` },
|
||||||
false,
|
false,
|
||||||
true
|
true
|
||||||
);
|
).body
|
||||||
|
}
|
||||||
if (!response.body) {
|
|
||||||
throw new ScriptException("Unable to acquire license key");
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.body;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -644,8 +595,6 @@ class PlatformComment {
|
|||||||
this.date = obj.date ?? 0;
|
this.date = obj.date ?? 0;
|
||||||
this.replyCount = obj.replyCount ?? 0;
|
this.replyCount = obj.replyCount ?? 0;
|
||||||
this.context = obj.context ?? {};
|
this.context = obj.context ?? {};
|
||||||
if(obj.getReplies)
|
|
||||||
this.getReplies = obj.getReplies;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -717,12 +666,11 @@ class LiveEventViewCount extends LiveEvent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
class LiveEventRaid extends LiveEvent {
|
class LiveEventRaid extends LiveEvent {
|
||||||
constructor(targetUrl, targetName, targetThumbnail, isOutgoing) {
|
constructor(targetUrl, targetName, targetThumbnail) {
|
||||||
super(100);
|
super(100);
|
||||||
this.targetUrl = targetUrl;
|
this.targetUrl = targetUrl;
|
||||||
this.targetName = targetName;
|
this.targetName = targetName;
|
||||||
this.targetThumbnail = targetThumbnail;
|
this.targetThumbnail = targetThumbnail;
|
||||||
this.isOutgoing = isOutgoing ?? true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -795,7 +743,6 @@ let plugin = {
|
|||||||
//To override by plugin
|
//To override by plugin
|
||||||
const source = {
|
const source = {
|
||||||
getHome() { return new ContentPager([], false, {}); },
|
getHome() { return new ContentPager([], false, {}); },
|
||||||
getShorts() { return new VideoPager([], false, {}); },
|
|
||||||
|
|
||||||
enable(config){ },
|
enable(config){ },
|
||||||
disable() {},
|
disable() {},
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
package com.futo.platformplayer
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import java.io.File
|
|
||||||
import java.net.HttpURLConnection
|
|
||||||
import java.net.URL
|
|
||||||
|
|
||||||
object AppCaUpdater {
|
|
||||||
private const val CA_URL = "https://curl.se/ca/cacert.pem"
|
|
||||||
private const val CACHE_FILENAME = "curl-ca-bundle.pem"
|
|
||||||
private const val MAX_AGE_DAYS = 30
|
|
||||||
|
|
||||||
suspend fun ensureCaBundle(context: Context): File = withContext(Dispatchers.IO) {
|
|
||||||
val file = File(context.noBackupFilesDir, CACHE_FILENAME)
|
|
||||||
val needsUpdate = !file.exists() || isOlderThanDays(file, MAX_AGE_DAYS)
|
|
||||||
if (needsUpdate) {
|
|
||||||
downloadToFile(CA_URL, file)
|
|
||||||
}
|
|
||||||
return@withContext file
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isOlderThanDays(file: File, days: Int): Boolean {
|
|
||||||
val ageMs = System.currentTimeMillis() - file.lastModified()
|
|
||||||
return ageMs > days * 24L * 60L * 60L * 1000L
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun downloadToFile(urlStr: String, dest: File) {
|
|
||||||
val conn = (URL(urlStr).openConnection() as HttpURLConnection).apply {
|
|
||||||
connectTimeout = 15000
|
|
||||||
readTimeout = 15000
|
|
||||||
instanceFollowRedirects = true
|
|
||||||
}
|
|
||||||
conn.inputStream.use { input ->
|
|
||||||
dest.parentFile?.mkdirs()
|
|
||||||
dest.outputStream().use { output ->
|
|
||||||
input.copyTo(output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
conn.disconnect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,319 +0,0 @@
|
|||||||
import kotlin.math.*
|
|
||||||
|
|
||||||
class CSSColor(r: Float, g: Float, b: Float, a: Float = 1f) {
|
|
||||||
init {
|
|
||||||
require(r in 0f..1f && g in 0f..1f && b in 0f..1f && a in 0f..1f) {
|
|
||||||
"RGBA channels must be in [0,1]"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- RGB(A) channels stored 0–1 --
|
|
||||||
var r: Float = r.coerceIn(0f, 1f)
|
|
||||||
set(v) { field = v.coerceIn(0f, 1f); _hslDirty = true }
|
|
||||||
var g: Float = g.coerceIn(0f, 1f)
|
|
||||||
set(v) { field = v.coerceIn(0f, 1f); _hslDirty = true }
|
|
||||||
var b: Float = b.coerceIn(0f, 1f)
|
|
||||||
set(v) { field = v.coerceIn(0f, 1f); _hslDirty = true }
|
|
||||||
var a: Float = a.coerceIn(0f, 1f)
|
|
||||||
set(v) { field = v.coerceIn(0f, 1f) }
|
|
||||||
|
|
||||||
// -- Int views of RGBA 0–255 --
|
|
||||||
var red: Int
|
|
||||||
get() = (r * 255).roundToInt()
|
|
||||||
set(v) { r = (v.coerceIn(0, 255) / 255f) }
|
|
||||||
var green: Int
|
|
||||||
get() = (g * 255).roundToInt()
|
|
||||||
set(v) { g = (v.coerceIn(0, 255) / 255f) }
|
|
||||||
var blue: Int
|
|
||||||
get() = (b * 255).roundToInt()
|
|
||||||
set(v) { b = (v.coerceIn(0, 255) / 255f) }
|
|
||||||
var alpha: Int
|
|
||||||
get() = (a * 255).roundToInt()
|
|
||||||
set(v) { a = (v.coerceIn(0, 255) / 255f) }
|
|
||||||
|
|
||||||
// -- HSLA storage & lazy recompute flags --
|
|
||||||
private var _h: Float = 0f
|
|
||||||
private var _s: Float = 0f
|
|
||||||
private var _l: Float = 0f
|
|
||||||
private var _hslDirty = true
|
|
||||||
|
|
||||||
/** Hue [0...360) */
|
|
||||||
var hue: Float
|
|
||||||
get() { computeHslIfNeeded(); return _h }
|
|
||||||
set(v) { setHsl(v, saturation, lightness) }
|
|
||||||
|
|
||||||
/** Saturation [0...1] */
|
|
||||||
var saturation: Float
|
|
||||||
get() { computeHslIfNeeded(); return _s }
|
|
||||||
set(v) { setHsl(hue, v, lightness) }
|
|
||||||
|
|
||||||
/** Lightness [0...1] */
|
|
||||||
var lightness: Float
|
|
||||||
get() { computeHslIfNeeded(); return _l }
|
|
||||||
set(v) { setHsl(hue, saturation, v) }
|
|
||||||
|
|
||||||
private fun computeHslIfNeeded() {
|
|
||||||
if (!_hslDirty) return
|
|
||||||
val max = max(max(r, g), b)
|
|
||||||
val min = min(min(r, g), b)
|
|
||||||
val d = max - min
|
|
||||||
_l = (max + min) / 2f
|
|
||||||
_s = if (d == 0f) 0f else d / (1f - abs(2f * _l - 1f))
|
|
||||||
_h = when {
|
|
||||||
d == 0f -> 0f
|
|
||||||
max == r -> ((g - b) / d % 6f) * 60f
|
|
||||||
max == g -> (((b - r) / d) + 2f) * 60f
|
|
||||||
else -> (((r - g) / d) + 4f) * 60f
|
|
||||||
}.let { if (it < 0f) it + 360f else it }
|
|
||||||
_hslDirty = false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set all three HSL channels at once.
|
|
||||||
* Hue in degrees [0...360), s/l [0...1].
|
|
||||||
*/
|
|
||||||
fun setHsl(h: Float, s: Float, l: Float) {
|
|
||||||
val hh = ((h % 360f) + 360f) % 360f
|
|
||||||
val cc = (1f - abs(2f * l - 1f)) * s
|
|
||||||
val x = cc * (1f - abs((hh / 60f) % 2f - 1f))
|
|
||||||
val m = l - cc / 2f
|
|
||||||
|
|
||||||
val (rp, gp, bp) = when {
|
|
||||||
hh < 60f -> Triple(cc, x, 0f)
|
|
||||||
hh < 120f -> Triple(x, cc, 0f)
|
|
||||||
hh < 180f -> Triple(0f, cc, x)
|
|
||||||
hh < 240f -> Triple(0f, x, cc)
|
|
||||||
hh < 300f -> Triple(x, 0f, cc)
|
|
||||||
else -> Triple(cc, 0f, x)
|
|
||||||
}
|
|
||||||
|
|
||||||
r = rp + m; g = gp + m; b = bp + m
|
|
||||||
_h = hh; _s = s; _l = l; _hslDirty = false
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Return 0xRRGGBBAA int */
|
|
||||||
fun toRgbaInt(): Int {
|
|
||||||
val ai = (a * 255).roundToInt() and 0xFF
|
|
||||||
val ri = (r * 255).roundToInt() and 0xFF
|
|
||||||
val gi = (g * 255).roundToInt() and 0xFF
|
|
||||||
val bi = (b * 255).roundToInt() and 0xFF
|
|
||||||
return (ri shl 24) or (gi shl 16) or (bi shl 8) or ai
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Return 0xAARRGGBB int */
|
|
||||||
fun toArgbInt(): Int {
|
|
||||||
val ai = (a * 255).roundToInt() and 0xFF
|
|
||||||
val ri = (r * 255).roundToInt() and 0xFF
|
|
||||||
val gi = (g * 255).roundToInt() and 0xFF
|
|
||||||
val bi = (b * 255).roundToInt() and 0xFF
|
|
||||||
return (ai shl 24) or (ri shl 16) or (gi shl 8) or bi
|
|
||||||
}
|
|
||||||
|
|
||||||
// — Convenience modifiers (chainable) —
|
|
||||||
|
|
||||||
/** Lighten by fraction [0...1] */
|
|
||||||
fun lighten(fraction: Float): CSSColor = apply {
|
|
||||||
lightness = (lightness + fraction).coerceIn(0f, 1f)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Darken by fraction [0...1] */
|
|
||||||
fun darken(fraction: Float): CSSColor = apply {
|
|
||||||
lightness = (lightness - fraction).coerceIn(0f, 1f)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Increase saturation by fraction [0...1] */
|
|
||||||
fun saturate(fraction: Float): CSSColor = apply {
|
|
||||||
saturation = (saturation + fraction).coerceIn(0f, 1f)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Decrease saturation by fraction [0...1] */
|
|
||||||
fun desaturate(fraction: Float): CSSColor = apply {
|
|
||||||
saturation = (saturation - fraction).coerceIn(0f, 1f)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Rotate hue by degrees (can be negative) */
|
|
||||||
fun rotateHue(degrees: Float): CSSColor = apply {
|
|
||||||
hue = (hue + degrees) % 360f
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
/** Create from Android 0xAARRGGBB */
|
|
||||||
@JvmStatic fun fromArgb(color: Int): CSSColor {
|
|
||||||
val a = ((color ushr 24) and 0xFF) / 255f
|
|
||||||
val r = ((color ushr 16) and 0xFF) / 255f
|
|
||||||
val g = ((color ushr 8) and 0xFF) / 255f
|
|
||||||
val b = ( color and 0xFF) / 255f
|
|
||||||
return CSSColor(r, g, b, a)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Create from Android 0xRRGGBBAA */
|
|
||||||
@JvmStatic fun fromRgba(color: Int): CSSColor {
|
|
||||||
val r = ((color ushr 24) and 0xFF) / 255f
|
|
||||||
val g = ((color ushr 16) and 0xFF) / 255f
|
|
||||||
val b = ((color ushr 8) and 0xFF) / 255f
|
|
||||||
val a = ( color and 0xFF) / 255f
|
|
||||||
return CSSColor(r, g, b, a)
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic fun fromAndroidColor(color: Int): CSSColor {
|
|
||||||
return fromArgb(color)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val NAMED_HEX = mapOf(
|
|
||||||
"aliceblue" to "F0F8FF", "antiquewhite" to "FAEBD7", "aqua" to "00FFFF",
|
|
||||||
"aquamarine" to "7FFFD4", "azure" to "F0FFFF", "beige" to "F5F5DC",
|
|
||||||
"bisque" to "FFE4C4", "black" to "000000", "blanchedalmond" to "FFEBCD",
|
|
||||||
"blue" to "0000FF", "blueviolet" to "8A2BE2", "brown" to "A52A2A",
|
|
||||||
"burlywood" to "DEB887", "cadetblue" to "5F9EA0", "chartreuse" to "7FFF00",
|
|
||||||
"chocolate" to "D2691E", "coral" to "FF7F50", "cornflowerblue" to "6495ED",
|
|
||||||
"cornsilk" to "FFF8DC", "crimson" to "DC143C", "cyan" to "00FFFF",
|
|
||||||
"darkblue" to "00008B", "darkcyan" to "008B8B", "darkgoldenrod" to "B8860B",
|
|
||||||
"darkgray" to "A9A9A9", "darkgreen" to "006400", "darkgrey" to "A9A9A9",
|
|
||||||
"darkkhaki" to "BDB76B", "darkmagenta" to "8B008B", "darkolivegreen" to "556B2F",
|
|
||||||
"darkorange" to "FF8C00", "darkorchid" to "9932CC", "darkred" to "8B0000",
|
|
||||||
"darksalmon" to "E9967A", "darkseagreen" to "8FBC8F", "darkslateblue" to "483D8B",
|
|
||||||
"darkslategray" to "2F4F4F", "darkslategrey" to "2F4F4F", "darkturquoise" to "00CED1",
|
|
||||||
"darkviolet" to "9400D3", "deeppink" to "FF1493", "deepskyblue" to "00BFFF",
|
|
||||||
"dimgray" to "696969", "dimgrey" to "696969", "dodgerblue" to "1E90FF",
|
|
||||||
"firebrick" to "B22222", "floralwhite" to "FFFAF0", "forestgreen" to "228B22",
|
|
||||||
"fuchsia" to "FF00FF", "gainsboro" to "DCDCDC", "ghostwhite" to "F8F8FF",
|
|
||||||
"gold" to "FFD700", "goldenrod" to "DAA520", "gray" to "808080",
|
|
||||||
"green" to "008000", "greenyellow" to "ADFF2F", "grey" to "808080",
|
|
||||||
"honeydew" to "F0FFF0", "hotpink" to "FF69B4", "indianred" to "CD5C5C",
|
|
||||||
"indigo" to "4B0082", "ivory" to "FFFFF0", "khaki" to "F0E68C",
|
|
||||||
"lavender" to "E6E6FA", "lavenderblush" to "FFF0F5", "lawngreen" to "7CFC00",
|
|
||||||
"lemonchiffon" to "FFFACD", "lightblue" to "ADD8E6", "lightcoral" to "F08080",
|
|
||||||
"lightcyan" to "E0FFFF", "lightgoldenrodyellow" to "FAFAD2", "lightgray" to "D3D3D3",
|
|
||||||
"lightgreen" to "90EE90", "lightgrey" to "D3D3D3", "lightpink" to "FFB6C1",
|
|
||||||
"lightsalmon" to "FFA07A", "lightseagreen" to "20B2AA", "lightskyblue" to "87CEFA",
|
|
||||||
"lightslategray" to "778899", "lightslategrey" to "778899", "lightsteelblue" to "B0C4DE",
|
|
||||||
"lightyellow" to "FFFFE0", "lime" to "00FF00", "limegreen" to "32CD32",
|
|
||||||
"linen" to "FAF0E6", "magenta" to "FF00FF", "maroon" to "800000",
|
|
||||||
"mediumaquamarine" to "66CDAA", "mediumblue" to "0000CD", "mediumorchid" to "BA55D3",
|
|
||||||
"mediumpurple" to "9370DB", "mediumseagreen" to "3CB371", "mediumslateblue" to "7B68EE",
|
|
||||||
"mediumspringgreen" to "00FA9A", "mediumturquoise" to "48D1CC", "mediumvioletred" to "C71585",
|
|
||||||
"midnightblue" to "191970", "mintcream" to "F5FFFA", "mistyrose" to "FFE4E1",
|
|
||||||
"moccasin" to "FFE4B5", "navajowhite" to "FFDEAD", "navy" to "000080",
|
|
||||||
"oldlace" to "FDF5E6", "olive" to "808000", "olivedrab" to "6B8E23",
|
|
||||||
"orange" to "FFA500", "orangered" to "FF4500", "orchid" to "DA70D6",
|
|
||||||
"palegoldenrod" to "EEE8AA", "palegreen" to "98FB98", "paleturquoise" to "AFEEEE",
|
|
||||||
"palevioletred" to "DB7093", "papayawhip" to "FFEFD5", "peachpuff" to "FFDAB9",
|
|
||||||
"peru" to "CD853F", "pink" to "FFC0CB", "plum" to "DDA0DD",
|
|
||||||
"powderblue" to "B0E0E6", "purple" to "800080", "rebeccapurple" to "663399",
|
|
||||||
"red" to "FF0000", "rosybrown" to "BC8F8F", "royalblue" to "4169E1",
|
|
||||||
"saddlebrown" to "8B4513", "salmon" to "FA8072", "sandybrown" to "F4A460",
|
|
||||||
"seagreen" to "2E8B57", "seashell" to "FFF5EE", "sienna" to "A0522D",
|
|
||||||
"silver" to "C0C0C0", "skyblue" to "87CEEB", "slateblue" to "6A5ACD",
|
|
||||||
"slategray" to "708090", "slategrey" to "708090", "snow" to "FFFAFA",
|
|
||||||
"springgreen" to "00FF7F", "steelblue" to "4682B4", "tan" to "D2B48C",
|
|
||||||
"teal" to "008080", "thistle" to "D8BFD8", "tomato" to "FF6347",
|
|
||||||
"turquoise" to "40E0D0", "violet" to "EE82EE", "wheat" to "F5DEB3",
|
|
||||||
"white" to "FFFFFF", "whitesmoke" to "F5F5F5", "yellow" to "FFFF00",
|
|
||||||
"yellowgreen" to "9ACD32"
|
|
||||||
)
|
|
||||||
private val NAMED: Map<String, Int> = NAMED_HEX
|
|
||||||
.mapValues { (_, hexRgb) ->
|
|
||||||
// parse hexRgb ("RRGGBB") to Int, then OR in 0xFF000000 for full opacity
|
|
||||||
val rgb = hexRgb.toInt(16)
|
|
||||||
(rgb shl 8) or 0xFF
|
|
||||||
} + ("transparent" to 0x00000000)
|
|
||||||
|
|
||||||
private val HEX_REGEX = Regex("^#([0-9a-fA-F]{3,8})$", RegexOption.IGNORE_CASE)
|
|
||||||
private val RGB_REGEX = Regex("^rgba?\\(([^)]+)\\)\$", RegexOption.IGNORE_CASE)
|
|
||||||
private val HSL_REGEX = Regex("^hsla?\\(([^)]+)\\)\$", RegexOption.IGNORE_CASE)
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun parseColor(s: String): CSSColor {
|
|
||||||
val str = s.trim()
|
|
||||||
// named
|
|
||||||
NAMED[str.lowercase()]?.let { return it.RGBAtoCSSColor() }
|
|
||||||
|
|
||||||
// hex
|
|
||||||
HEX_REGEX.matchEntire(str)?.groupValues?.get(1)?.let { part ->
|
|
||||||
return parseHexPart(part)
|
|
||||||
}
|
|
||||||
|
|
||||||
// rgb/rgba
|
|
||||||
RGB_REGEX.matchEntire(str)?.groupValues?.get(1)?.let {
|
|
||||||
return parseRgbParts(it.split(',').map(String::trim))
|
|
||||||
}
|
|
||||||
|
|
||||||
// hsl/hsla
|
|
||||||
HSL_REGEX.matchEntire(str)?.groupValues?.get(1)?.let {
|
|
||||||
return parseHslParts(it.split(',').map(String::trim))
|
|
||||||
}
|
|
||||||
|
|
||||||
error("Cannot parse color: \"$s\"")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseHexPart(p: String): CSSColor {
|
|
||||||
// expand shorthand like "RGB" or "RGBA" to full 8-chars "RRGGBBAA"
|
|
||||||
val hex = when (p.length) {
|
|
||||||
3 -> p.map { "$it$it" }.joinToString("") + "FF"
|
|
||||||
4 -> p.map { "$it$it" }.joinToString("")
|
|
||||||
6 -> p + "FF"
|
|
||||||
8 -> p
|
|
||||||
else -> error("Invalid hex color: #$p")
|
|
||||||
}
|
|
||||||
|
|
||||||
val parsed = hex.toLong(16).toInt()
|
|
||||||
val alpha = (parsed and 0xFF) shl 24
|
|
||||||
val rgbOnly = (parsed ushr 8) and 0x00FFFFFF
|
|
||||||
val argb = alpha or rgbOnly
|
|
||||||
return fromArgb(argb)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseRgbParts(parts: List<String>): CSSColor {
|
|
||||||
require(parts.size == 3 || parts.size == 4) { "rgb/rgba needs 3 or 4 parts" }
|
|
||||||
|
|
||||||
// r/g/b: "128" → 128/255, "50%" → 0.5
|
|
||||||
fun channel(ch: String): Float =
|
|
||||||
if (ch.endsWith("%")) ch.removeSuffix("%").toFloat() / 100f
|
|
||||||
else ch.toFloat().coerceIn(0f, 255f) / 255f
|
|
||||||
|
|
||||||
// alpha: "0.5" → 0.5, "50%" → 0.5
|
|
||||||
fun alpha(a: String): Float =
|
|
||||||
if (a.endsWith("%")) a.removeSuffix("%").toFloat() / 100f
|
|
||||||
else a.toFloat().coerceIn(0f, 1f)
|
|
||||||
|
|
||||||
val r = channel(parts[0])
|
|
||||||
val g = channel(parts[1])
|
|
||||||
val b = channel(parts[2])
|
|
||||||
val a = if (parts.size == 4) alpha(parts[3]) else 1f
|
|
||||||
|
|
||||||
return CSSColor(r, g, b, a)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseHslParts(parts: List<String>): CSSColor {
|
|
||||||
require(parts.size == 3 || parts.size == 4) { "hsl/hsla needs 3 or 4 parts" }
|
|
||||||
|
|
||||||
fun hueOf(h: String): Float = when {
|
|
||||||
h.endsWith("deg") -> h.removeSuffix("deg").toFloat()
|
|
||||||
h.endsWith("grad") -> h.removeSuffix("grad").toFloat() * 0.9f
|
|
||||||
h.endsWith("rad") -> h.removeSuffix("rad").toFloat() * (180f / PI.toFloat())
|
|
||||||
h.endsWith("turn") -> h.removeSuffix("turn").toFloat() * 360f
|
|
||||||
else -> h.toFloat()
|
|
||||||
}
|
|
||||||
|
|
||||||
// for s and l you only ever see percentages
|
|
||||||
fun pct(p: String): Float =
|
|
||||||
p.removeSuffix("%").toFloat().coerceIn(0f, 100f) / 100f
|
|
||||||
|
|
||||||
// alpha: "0.5" → 0.5, "50%" → 0.5
|
|
||||||
fun alpha(a: String): Float =
|
|
||||||
if (a.endsWith("%")) pct(a)
|
|
||||||
else a.toFloat().coerceIn(0f, 1f)
|
|
||||||
|
|
||||||
val h = hueOf(parts[0])
|
|
||||||
val s = pct(parts[1])
|
|
||||||
val l = pct(parts[2])
|
|
||||||
val a = if (parts.size == 4) alpha(parts[3]) else 1f
|
|
||||||
|
|
||||||
return CSSColor(0f, 0f, 0f, a).apply { setHsl(h, s, l) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Int.RGBAtoCSSColor(): CSSColor = CSSColor.fromRgba(this)
|
|
||||||
fun Int.ARGBtoCSSColor(): CSSColor = CSSColor.fromArgb(this)
|
|
||||||
fun CSSColor.toAndroidColor(): Int = toArgbInt()
|
|
||||||
@@ -14,6 +14,7 @@ import java.text.DecimalFormat
|
|||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.time.temporal.ChronoUnit
|
import java.time.temporal.ChronoUnit
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.roundToInt
|
||||||
import kotlin.math.roundToLong
|
import kotlin.math.roundToLong
|
||||||
|
|
||||||
|
|
||||||
@@ -375,19 +376,14 @@ private val slds = hashSetOf(".com.ac", ".net.ac", ".gov.ac", ".org.ac", ".mil.a
|
|||||||
fun String.matchesDomain(queryDomain: String): Boolean {
|
fun String.matchesDomain(queryDomain: String): Boolean {
|
||||||
|
|
||||||
if(queryDomain.startsWith(".")) {
|
if(queryDomain.startsWith(".")) {
|
||||||
val parts = this.lowercase().split(".");
|
|
||||||
val queryParts = queryDomain.lowercase().trimStart("."[0]).split(".");
|
val parts = queryDomain.lowercase().split(".");
|
||||||
if(queryParts.size < 2)
|
if(parts.size < 3)
|
||||||
throw IllegalStateException("Illegal use of wildcards on First-Level-Domain (" + queryDomain + ")");
|
throw IllegalStateException("Illegal use of wildcards on First-Level-Domain (" + queryDomain + ")");
|
||||||
else {
|
if(parts.size >= 3){
|
||||||
val possibleDomain = "." + queryParts.joinToString(".");
|
val isSLD = slds.contains("." + parts[parts.size - 2] + "." + parts[parts.size - 1]);
|
||||||
if(slds.contains(possibleDomain))
|
if(isSLD && parts.size <= 3)
|
||||||
throw IllegalStateException("Illegal use of wildcards on Second-Level-Domain (" + queryDomain + ")");
|
throw IllegalStateException("Illegal use of wildcards on Second-Level-Domain (" + queryDomain + ")");
|
||||||
/*
|
|
||||||
val isSLD = slds.contains("." + queryParts[queryParts.size - 2] + "." + queryParts[queryParts.size - 1]);
|
|
||||||
if(isSLD && queryParts.size <= 3)
|
|
||||||
throw IllegalStateException("Illegal use of wildcards on Second-Level-Domain (" + queryDomain + ")");
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: Should be safe, but double verify if can't be exploited
|
//TODO: Should be safe, but double verify if can't be exploited
|
||||||
@@ -399,11 +395,9 @@ fun String.matchesDomain(queryDomain: String): Boolean {
|
|||||||
|
|
||||||
fun String.getSubdomainWildcardQuery(): String {
|
fun String.getSubdomainWildcardQuery(): String {
|
||||||
val domainParts = this.split(".");
|
val domainParts = this.split(".");
|
||||||
var wildcardDomain = if(domainParts.size > 2)
|
val sldParts = "." + domainParts[domainParts.size - 2].lowercase() + "." + domainParts[domainParts.size - 1].lowercase();
|
||||||
"." + domainParts.drop(1).joinToString(".")
|
if(slds.contains(sldParts))
|
||||||
|
return "." + domainParts.drop(domainParts.size - 3).joinToString(".");
|
||||||
else
|
else
|
||||||
"." + domainParts.joinToString(".");
|
return "." + domainParts.drop(domainParts.size - 2).joinToString(".");
|
||||||
if(slds.contains(wildcardDomain.lowercase()))
|
|
||||||
"." + domainParts.joinToString(".");
|
|
||||||
return wildcardDomain;
|
|
||||||
}
|
}
|
||||||
@@ -216,8 +216,9 @@ private fun ByteArray.toInetAddress(): InetAddress {
|
|||||||
return InetAddress.getByAddress(this);
|
return InetAddress.getByAddress(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int, timeoutMs: Int = 10_000): Socket? {
|
fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int): Socket? {
|
||||||
ensureNotMainThread()
|
val timeout = 2000
|
||||||
|
|
||||||
|
|
||||||
val addresses = if(!Settings.instance.casting.allowIpv6) attemptAddresses.filterIsInstance<Inet4Address>() else attemptAddresses;
|
val addresses = if(!Settings.instance.casting.allowIpv6) attemptAddresses.filterIsInstance<Inet4Address>() else attemptAddresses;
|
||||||
if(addresses.isEmpty())
|
if(addresses.isEmpty())
|
||||||
@@ -231,7 +232,7 @@ fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int, timeoutMs
|
|||||||
val socket = Socket()
|
val socket = Socket()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return socket.apply { this.connect(InetSocketAddress(addresses[0], port), timeoutMs) }
|
return socket.apply { this.connect(InetSocketAddress(addresses[0], port), timeout) }
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Log.i("getConnectedSocket", "Failed to connect to: ${addresses[0]}", e)
|
Log.i("getConnectedSocket", "Failed to connect to: ${addresses[0]}", e)
|
||||||
socket.close()
|
socket.close()
|
||||||
@@ -240,11 +241,8 @@ fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int, timeoutMs
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
val sortedAddresses: List<InetAddress> = addresses
|
|
||||||
.sortedBy { addr -> addressScore(addr) }
|
|
||||||
|
|
||||||
val sockets: ArrayList<Socket> = arrayListOf();
|
val sockets: ArrayList<Socket> = arrayListOf();
|
||||||
for (i in sortedAddresses.indices) {
|
for (i in addresses.indices) {
|
||||||
sockets.add(Socket());
|
sockets.add(Socket());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,7 +250,7 @@ fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int, timeoutMs
|
|||||||
var connectedSocket: Socket? = null;
|
var connectedSocket: Socket? = null;
|
||||||
val threads: ArrayList<Thread> = arrayListOf();
|
val threads: ArrayList<Thread> = arrayListOf();
|
||||||
for (i in 0 until sockets.size) {
|
for (i in 0 until sockets.size) {
|
||||||
val address = sortedAddresses[i];
|
val address = addresses[i];
|
||||||
val socket = sockets[i];
|
val socket = sockets[i];
|
||||||
val thread = Thread {
|
val thread = Thread {
|
||||||
try {
|
try {
|
||||||
@@ -262,7 +260,7 @@ fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int, timeoutMs
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.connect(InetSocketAddress(address, port), timeoutMs);
|
socket.connect(InetSocketAddress(address, port), timeout);
|
||||||
|
|
||||||
synchronized(syncObject) {
|
synchronized(syncObject) {
|
||||||
if (connectedSocket == null) {
|
if (connectedSocket == null) {
|
||||||
|
|||||||
@@ -7,9 +7,6 @@ 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
|
||||||
import java.time.Instant
|
|
||||||
import java.time.OffsetDateTime
|
|
||||||
import java.time.ZoneOffset
|
|
||||||
|
|
||||||
//Syntax sugaring
|
//Syntax sugaring
|
||||||
inline fun <reified T> Any.assume(): T?{
|
inline fun <reified T> Any.assume(): T?{
|
||||||
@@ -36,37 +33,13 @@ fun Boolean?.toYesNo(): String {
|
|||||||
fun InetAddress?.toUrlAddress(): String {
|
fun InetAddress?.toUrlAddress(): String {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
is Inet6Address -> {
|
is Inet6Address -> {
|
||||||
val hostAddr = this.hostAddress ?: throw Exception("Invalid address: hostAddress is null")
|
"[${hostAddress}]"
|
||||||
val index = hostAddr.indexOf('%')
|
|
||||||
if (index != -1) {
|
|
||||||
val addrPart = hostAddr.substring(0, index)
|
|
||||||
val scopeId = hostAddr.substring(index + 1)
|
|
||||||
"[${addrPart}%25${scopeId}]" // %25 is URL-encoded '%'
|
|
||||||
} else {
|
|
||||||
"[$hostAddr]"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
is Inet4Address -> {
|
is Inet4Address -> {
|
||||||
this.hostAddress ?: throw Exception("Invalid address: hostAddress is null")
|
hostAddress
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
throw Exception("Invalid address type")
|
throw Exception("Invalid address type")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun Long?.sToOffsetDateTimeUTC(): OffsetDateTime {
|
|
||||||
if (this == null || this < 0)
|
|
||||||
return OffsetDateTime.MIN
|
|
||||||
if(this > 4070912400)
|
|
||||||
return OffsetDateTime.MAX;
|
|
||||||
return OffsetDateTime.ofInstant(Instant.ofEpochSecond(this), ZoneOffset.UTC)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Long?.msToOffsetDateTimeUTC(): OffsetDateTime {
|
|
||||||
if (this == null || this < 0)
|
|
||||||
return OffsetDateTime.MIN
|
|
||||||
if(this > 4070912400)
|
|
||||||
return OffsetDateTime.MAX;
|
|
||||||
return OffsetDateTime.ofInstant(Instant.ofEpochMilli(this), ZoneOffset.UTC)
|
|
||||||
}
|
}
|
||||||
@@ -2,32 +2,10 @@ package com.futo.platformplayer
|
|||||||
|
|
||||||
import com.caoccao.javet.values.V8Value
|
import com.caoccao.javet.values.V8Value
|
||||||
import com.caoccao.javet.values.primitive.*
|
import com.caoccao.javet.values.primitive.*
|
||||||
import com.caoccao.javet.values.reference.IV8ValuePromise
|
|
||||||
import com.caoccao.javet.values.reference.V8ValueArray
|
import com.caoccao.javet.values.reference.V8ValueArray
|
||||||
import com.caoccao.javet.values.reference.V8ValueError
|
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
import com.caoccao.javet.values.reference.V8ValuePromise
|
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
|
||||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
import com.futo.platformplayer.engine.V8Plugin
|
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptExecutionException
|
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
||||||
import com.futo.platformplayer.logging.Logger
|
|
||||||
import kotlinx.coroutines.CancellationException
|
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Deferred
|
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.cancel
|
|
||||||
import kotlinx.coroutines.selects.SelectClause0
|
|
||||||
import kotlinx.coroutines.selects.SelectClause1
|
|
||||||
import java.util.concurrent.CountDownLatch
|
|
||||||
import kotlin.coroutines.AbstractCoroutineContextElement
|
|
||||||
import kotlin.coroutines.CoroutineContext
|
|
||||||
import kotlin.reflect.jvm.internal.impl.load.kotlin.JvmType
|
|
||||||
|
|
||||||
|
|
||||||
//V8
|
//V8
|
||||||
@@ -46,10 +24,6 @@ fun <R> V8Value?.orDefault(default: R, handler: (V8Value)->R): R {
|
|||||||
return handler(this);
|
return handler(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun V8Value.getSourcePlugin(): V8Plugin? {
|
|
||||||
return V8Plugin.getPluginFromRuntime(this.v8Runtime);
|
|
||||||
}
|
|
||||||
|
|
||||||
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}");
|
||||||
@@ -115,29 +89,7 @@ inline fun <reified T> V8ValueArray.expectV8Variants(config: IV8PluginConfig, co
|
|||||||
.map { kv-> kv.second.orNull { it.expectV8Variant<T>(config, contextName + "[${kv.first}]", ) } as T };
|
.map { kv-> kv.second.orNull { it.expectV8Variant<T>(config, contextName + "[${kv.first}]", ) } as T };
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun V8Plugin.ensureIsBusy() {
|
|
||||||
this.let {
|
|
||||||
if (!it.isThreadAlreadyBusy()) {
|
|
||||||
//throw IllegalStateException("Tried to access V8Plugin without busy");
|
|
||||||
val stacktrace = Thread.currentThread().stackTrace;
|
|
||||||
Logger.w("Extensions_V8",
|
|
||||||
"V8 USE OUTSIDE BUSY: " + stacktrace.drop(3)?.firstOrNull().toString() +
|
|
||||||
", " + stacktrace.drop(4)?.firstOrNull().toString() +
|
|
||||||
", " + stacktrace.drop(5)?.firstOrNull()?.toString() +
|
|
||||||
", " + stacktrace.drop(6)?.firstOrNull()?.toString()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
inline fun V8Value.ensureIsBusy() {
|
|
||||||
this?.getSourcePlugin()?.let {
|
|
||||||
it.ensureIsBusy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun <reified T> V8Value.expectV8Variant(config: IV8PluginConfig, contextName: String): T {
|
inline fun <reified T> V8Value.expectV8Variant(config: IV8PluginConfig, contextName: String): T {
|
||||||
if(false)
|
|
||||||
ensureIsBusy();
|
|
||||||
return when(T::class) {
|
return when(T::class) {
|
||||||
String::class -> this.expectOrThrow<V8ValueString>(config, contextName).value as T;
|
String::class -> this.expectOrThrow<V8ValueString>(config, contextName).value as T;
|
||||||
Int::class -> {
|
Int::class -> {
|
||||||
@@ -194,209 +146,4 @@ fun V8ObjectToHashMap(obj: V8ValueObject?): HashMap<String, String> {
|
|||||||
for(prop in obj.ownPropertyNames.keys.map { obj.ownPropertyNames.get<V8Value>(it).toString() })
|
for(prop in obj.ownPropertyNames.keys.map { obj.ownPropertyNames.get<V8Value>(it).toString() })
|
||||||
map.put(prop, obj.getString(prop));
|
map.put(prop, obj.getString(prop));
|
||||||
return map;
|
return map;
|
||||||
}
|
|
||||||
|
|
||||||
fun <T: V8Value> V8ValuePromise.toV8ValueBlocking(plugin: V8Plugin): T {
|
|
||||||
val latch = CountDownLatch(1);
|
|
||||||
var promiseResult: T? = null;
|
|
||||||
var promiseException: Throwable? = null;
|
|
||||||
plugin.busy {
|
|
||||||
this.register(object: IV8ValuePromise.IListener {
|
|
||||||
override fun onFulfilled(p0: V8Value?) {
|
|
||||||
if(p0 is V8ValueError)
|
|
||||||
promiseException = ScriptExecutionException(plugin.config, p0.message);
|
|
||||||
else {
|
|
||||||
if(p0 is V8ValueObject)
|
|
||||||
p0.setWeak();
|
|
||||||
promiseResult = p0 as T;
|
|
||||||
}
|
|
||||||
latch.countDown();
|
|
||||||
}
|
|
||||||
override fun onRejected(p0: V8Value?) {
|
|
||||||
promiseException = p0?.toException(plugin.config);
|
|
||||||
latch.countDown();
|
|
||||||
}
|
|
||||||
override fun onCatch(p0: V8Value?) {
|
|
||||||
promiseException = p0?.toException(plugin.config);
|
|
||||||
latch.countDown();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
plugin.registerPromise(this) {
|
|
||||||
promiseException = CancellationException("Cancelled by system");
|
|
||||||
latch.countDown();
|
|
||||||
}
|
|
||||||
//Logger.i("V8", "V8ValueBlocking started (Busy) [" + blockCount + "]" + Thread.currentThread().stackTrace.drop(3)?.firstOrNull()?.toString() + ", " + Thread.currentThread().stackTrace.drop(4)?.firstOrNull()?.toString()+ ", " + Thread.currentThread().stackTrace.drop(5)?.firstOrNull()?.toString());
|
|
||||||
|
|
||||||
|
|
||||||
if(!promise.isPending) {
|
|
||||||
try {
|
|
||||||
Logger.i("V8", "V8Promise resolved synchronously");
|
|
||||||
if(promise.isFulfilled)
|
|
||||||
promiseResult = promise.getResult<T>();
|
|
||||||
else
|
|
||||||
promiseException = promise.getResult<V8Value>().toException(plugin.config);
|
|
||||||
}
|
|
||||||
catch(ex: Throwable) {
|
|
||||||
promiseException = ex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
plugin.unbusy {
|
|
||||||
latch.await();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(promiseException != null)
|
|
||||||
throw promiseException!!;
|
|
||||||
return promiseResult!!;
|
|
||||||
}
|
|
||||||
fun <T: V8Value> V8ValuePromise.toV8ValueAsync(plugin: V8Plugin): V8Deferred<T> {
|
|
||||||
val underlyingDef = CompletableDeferred<T>();
|
|
||||||
val def = if(this.has("estDuration"))
|
|
||||||
V8Deferred(underlyingDef,
|
|
||||||
this.getOrDefault(plugin.config, "estDuration", "toV8ValueAsync", -1) ?: -1);
|
|
||||||
else
|
|
||||||
V8Deferred<T>(underlyingDef);
|
|
||||||
|
|
||||||
if(def.estDuration > 0)
|
|
||||||
Logger.i("V8", "Promise with duration: [${def.estDuration}]");
|
|
||||||
|
|
||||||
val promise = this;
|
|
||||||
plugin.busy {
|
|
||||||
this.register(object: IV8ValuePromise.IListener {
|
|
||||||
override fun onFulfilled(p0: V8Value?) {
|
|
||||||
plugin.resolvePromise(promise);
|
|
||||||
underlyingDef.complete(p0 as T);
|
|
||||||
}
|
|
||||||
override fun onRejected(p0: V8Value?) {
|
|
||||||
try {
|
|
||||||
plugin.resolvePromise(promise);
|
|
||||||
val exceptionFound = p0?.toException(plugin.config) ?: NotImplementedError("onRejected promise not implemented..");
|
|
||||||
Logger.i("V8", "Promise rejected, setting exception");
|
|
||||||
underlyingDef.completeExceptionally(CancellationException(exceptionFound.message, exceptionFound));
|
|
||||||
}
|
|
||||||
catch(ex: Throwable) {
|
|
||||||
Logger.e("V8", "Rejection handling failed?" , ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
override fun onCatch(p0: V8Value?) {
|
|
||||||
try {
|
|
||||||
plugin.resolvePromise(promise);
|
|
||||||
val exceptionFound = p0?.toException(plugin.config) ?: NotImplementedError("onCatch promise not implemented..");
|
|
||||||
underlyingDef.completeExceptionally(CancellationException(exceptionFound.message, exceptionFound));
|
|
||||||
}
|
|
||||||
catch(ex: Throwable) {
|
|
||||||
Logger.e("V8", "Catching handling failed?" , ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
plugin.registerPromise(promise) {
|
|
||||||
if(def.isActive)
|
|
||||||
def.cancel("Cancelled by system");
|
|
||||||
}
|
|
||||||
return def;
|
|
||||||
}
|
|
||||||
|
|
||||||
fun V8Value.toException(config: IV8PluginConfig): Throwable {
|
|
||||||
val p0 = this;
|
|
||||||
if(p0 is V8ValueObject) {
|
|
||||||
return V8Plugin.getExceptionFromPlugin(config, p0, null, null, null, "P:");
|
|
||||||
/*
|
|
||||||
val pluginType = p0.getOrDefault(config, "plugin_type", "Promise Exception", "")?.let { if(!it.isNullOrBlank()) it + "" else "" }
|
|
||||||
val msg = p0.getOrDefault<String?>(config, "msg", "Promise Exception", null)
|
|
||||||
?: p0.getOrDefault(config, "message", "Promise Exception", "");
|
|
||||||
return Throwable("Promise Failed: " + pluginType + msg);
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
else if(p0 is V8ValueString)
|
|
||||||
return Throwable("Promise Failed:" + p0.value);
|
|
||||||
else
|
|
||||||
return NotImplementedError("onCatch promise not implemented..");
|
|
||||||
}
|
|
||||||
|
|
||||||
class V8Deferred<T>(val deferred: Deferred<T>, val estDuration: Int = -1): Deferred<T> by deferred {
|
|
||||||
|
|
||||||
fun <R> convert(conversion: (result: T)->R): V8Deferred<R>{
|
|
||||||
val newDef = CompletableDeferred<R>()
|
|
||||||
this.invokeOnCompletion {
|
|
||||||
if(it != null)
|
|
||||||
newDef.completeExceptionally(it);
|
|
||||||
else
|
|
||||||
newDef.complete(conversion(this@V8Deferred.getCompleted()));
|
|
||||||
}
|
|
||||||
|
|
||||||
return V8Deferred<R>(newDef, estDuration);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun <T, R> merge(scope: CoroutineScope, defs: List<V8Deferred<T>>, conversion: (result: List<T>)->R): V8Deferred<R> {
|
|
||||||
|
|
||||||
var amount = -1;
|
|
||||||
for(def in defs)
|
|
||||||
amount = Math.max(amount, def.estDuration);
|
|
||||||
|
|
||||||
val def = scope.async {
|
|
||||||
val results = defs.map { it.await() };
|
|
||||||
return@async conversion(results);
|
|
||||||
}
|
|
||||||
return V8Deferred(def, amount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun <T: V8Value> V8ValueObject.invokeV8(method: String, vararg obj: Any?): T {
|
|
||||||
var result = this.invoke<V8Value>(method, *obj);
|
|
||||||
if(result is V8ValuePromise) {
|
|
||||||
return result.toV8ValueBlocking(this.getSourcePlugin()!!);
|
|
||||||
}
|
|
||||||
return result as T;
|
|
||||||
}
|
|
||||||
fun <T: V8Value> V8ValueObject.invokeV8Async(method: String, vararg obj: Any?): V8Deferred<T> {
|
|
||||||
var result = this.invoke<V8Value>(method, *obj);
|
|
||||||
if(result is V8ValuePromise) {
|
|
||||||
return result.toV8ValueAsync(this.getSourcePlugin()!!);
|
|
||||||
}
|
|
||||||
return V8Deferred(CompletableDeferred(result as T));
|
|
||||||
}
|
|
||||||
fun V8ValueObject.invokeV8Void(method: String, vararg obj: Any?): V8Value {
|
|
||||||
var result = this.invoke<V8Value>(method, *obj);
|
|
||||||
if(result is V8ValuePromise) {
|
|
||||||
return result.toV8ValueBlocking(this.getSourcePlugin()!!);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
fun V8ValueObject.invokeV8VoidAsync(method: String, vararg obj: Any?): V8Deferred<V8Value> {
|
|
||||||
var result = this.invoke<V8Value>(method, *obj);
|
|
||||||
if(result is V8ValuePromise) {
|
|
||||||
val result = result.toV8ValueAsync<V8Value>(this.getSourcePlugin()!!);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
return V8Deferred(CompletableDeferred(result));
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun <T> Deferred<T>.awaitCancelConverted(): T {
|
|
||||||
try {
|
|
||||||
return this.await();
|
|
||||||
}
|
|
||||||
catch(ex: CancellationException) {
|
|
||||||
if(ex.cause != null) {
|
|
||||||
throw ex.cause!!;
|
|
||||||
}
|
|
||||||
throw ex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <T> IPager<T>.toList(): List<T> {
|
|
||||||
val list = this.getResults().toMutableList();
|
|
||||||
|
|
||||||
while(this.hasMorePages()) {
|
|
||||||
this.nextPage();
|
|
||||||
list.addAll(this.getResults());
|
|
||||||
}
|
|
||||||
|
|
||||||
return list.toList();
|
|
||||||
}
|
}
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
package com.futo.platformplayer
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.os.Build
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.view.Window
|
|
||||||
import android.view.WindowManager
|
|
||||||
import androidx.core.graphics.Insets
|
|
||||||
import androidx.core.view.ViewCompat
|
|
||||||
import androidx.core.view.WindowCompat
|
|
||||||
import androidx.core.view.WindowInsetsCompat
|
|
||||||
import androidx.core.view.WindowInsetsCompat.Type
|
|
||||||
import androidx.core.view.WindowInsetsControllerCompat
|
|
||||||
import androidx.core.view.doOnAttach
|
|
||||||
import androidx.core.view.updatePadding
|
|
||||||
import kotlin.math.max
|
|
||||||
|
|
||||||
class RootInsetsController private constructor(
|
|
||||||
private val activity: Activity,
|
|
||||||
private val window: Window,
|
|
||||||
private val root: ViewGroup
|
|
||||||
) {
|
|
||||||
private val controller by lazy { WindowInsetsControllerCompat(window, root) }
|
|
||||||
|
|
||||||
private val basePaddingLeft = root.paddingLeft
|
|
||||||
private val basePaddingTop = root.paddingTop
|
|
||||||
private val basePaddingRight = root.paddingRight
|
|
||||||
private val basePaddingBottom = root.paddingBottom
|
|
||||||
|
|
||||||
private var currentInsets: WindowInsetsCompat = WindowInsetsCompat.CONSUMED
|
|
||||||
private var fullscreen = false
|
|
||||||
|
|
||||||
init {
|
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
|
||||||
window.statusBarColor = Color.TRANSPARENT
|
|
||||||
window.navigationBarColor = Color.TRANSPARENT
|
|
||||||
controller.systemBarsBehavior =
|
|
||||||
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
|
||||||
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(root) { _, insets ->
|
|
||||||
currentInsets = insets
|
|
||||||
applyPadding()
|
|
||||||
insets
|
|
||||||
}
|
|
||||||
|
|
||||||
root.doOnAttach { ViewCompat.requestApplyInsets(root) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun effectiveInsets(): Insets {
|
|
||||||
if (fullscreen) return Insets.NONE
|
|
||||||
|
|
||||||
val sys = currentInsets.getInsets(Type.systemBars())
|
|
||||||
val cut = currentInsets.getInsetsIgnoringVisibility(Type.displayCutout())
|
|
||||||
val portrait = activity.resources.configuration.orientation == android.content.res.Configuration.ORIENTATION_PORTRAIT
|
|
||||||
|
|
||||||
val top = if (portrait) max(sys.top, cut.top) else sys.top
|
|
||||||
return Insets.of(sys.left, top, sys.right, sys.bottom)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun applyPadding() {
|
|
||||||
val e = effectiveInsets()
|
|
||||||
root.updatePadding(
|
|
||||||
left = basePaddingLeft + e.left,
|
|
||||||
top = basePaddingTop + e.top,
|
|
||||||
right = basePaddingRight + e.right,
|
|
||||||
bottom = basePaddingBottom + e.bottom
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun forceRelayoutAndInsets() {
|
|
||||||
root.post {
|
|
||||||
ViewCompat.requestApplyInsets(root)
|
|
||||||
applyPadding()
|
|
||||||
root.post {
|
|
||||||
ViewCompat.requestApplyInsets(root)
|
|
||||||
applyPadding()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun enterFullscreen(allowCutoutShortEdges: Boolean = true) {
|
|
||||||
fullscreen = true
|
|
||||||
if (allowCutoutShortEdges) {
|
|
||||||
window.attributes = window.attributes.apply {
|
|
||||||
layoutInDisplayCutoutMode =
|
|
||||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
|
||||||
}
|
|
||||||
}
|
|
||||||
controller.hide(Type.systemBars())
|
|
||||||
forceRelayoutAndInsets()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun exitFullscreen() {
|
|
||||||
fullscreen = false
|
|
||||||
window.attributes = window.attributes.apply {
|
|
||||||
layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
|
|
||||||
}
|
|
||||||
controller.show(Type.systemBars())
|
|
||||||
forceRelayoutAndInsets()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onConfigurationChanged() {
|
|
||||||
forceRelayoutAndInsets()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setLightSystemBarAppearance(lightStatus: Boolean, lightNav: Boolean) {
|
|
||||||
controller.isAppearanceLightStatusBars = lightStatus
|
|
||||||
controller.isAppearanceLightNavigationBars = lightNav
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun attach(activity: Activity, root: ViewGroup): RootInsetsController {
|
|
||||||
return RootInsetsController(activity, activity.window, root)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,11 +10,11 @@ import com.futo.platformplayer.activities.MainActivity
|
|||||||
import com.futo.platformplayer.activities.ManageTabsActivity
|
import com.futo.platformplayer.activities.ManageTabsActivity
|
||||||
import com.futo.platformplayer.activities.PolycentricHomeActivity
|
import com.futo.platformplayer.activities.PolycentricHomeActivity
|
||||||
import com.futo.platformplayer.activities.PolycentricProfileActivity
|
import com.futo.platformplayer.activities.PolycentricProfileActivity
|
||||||
|
import com.futo.platformplayer.activities.SettingsActivity
|
||||||
import com.futo.platformplayer.activities.SyncHomeActivity
|
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.fragment.mainactivity.main.SettingsFragment
|
|
||||||
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
|
||||||
@@ -25,23 +25,21 @@ import com.futo.platformplayer.states.StateCache
|
|||||||
import com.futo.platformplayer.states.StateMeta
|
import com.futo.platformplayer.states.StateMeta
|
||||||
import com.futo.platformplayer.states.StatePayment
|
import com.futo.platformplayer.states.StatePayment
|
||||||
import com.futo.platformplayer.states.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
import com.futo.platformplayer.states.StateSync
|
|
||||||
import com.futo.platformplayer.states.StateUpdate
|
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.AdvancedField
|
|
||||||
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.FieldForm
|
||||||
import com.futo.platformplayer.views.fields.FormField
|
import com.futo.platformplayer.views.fields.FormField
|
||||||
import com.futo.platformplayer.views.fields.FormFieldButton
|
import com.futo.platformplayer.views.fields.FormFieldButton
|
||||||
import com.futo.platformplayer.views.fields.FormFieldWarning
|
|
||||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.Transient
|
import kotlinx.serialization.Transient
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
@@ -63,7 +61,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@FormField(R.string.sync_grayjay, FieldForm.BUTTON, R.string.sync_grayjay_description, -8)
|
@FormField(R.string.sync_grayjay, FieldForm.BUTTON, R.string.sync_grayjay_description, -8)
|
||||||
@FormFieldButton(R.drawable.ic_update)
|
@FormFieldButton(R.drawable.ic_update)
|
||||||
fun syncGrayjay() {
|
fun syncGrayjay() {
|
||||||
StateApp?.instance?.activity?.let {
|
SettingsActivity.getActivity()?.let {
|
||||||
it.startActivity(Intent(it, SyncHomeActivity::class.java))
|
it.startActivity(Intent(it, SyncHomeActivity::class.java))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -72,7 +70,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@FormField(R.string.manage_polycentric_identity, FieldForm.BUTTON, R.string.manage_your_polycentric_identity, -7)
|
@FormField(R.string.manage_polycentric_identity, FieldForm.BUTTON, R.string.manage_your_polycentric_identity, -7)
|
||||||
@FormFieldButton(R.drawable.ic_person)
|
@FormFieldButton(R.drawable.ic_person)
|
||||||
fun managePolycentricIdentity() {
|
fun managePolycentricIdentity() {
|
||||||
StateApp?.instance?.activity?.let {
|
SettingsActivity.getActivity()?.let {
|
||||||
if (StatePolycentric.instance.enabled) {
|
if (StatePolycentric.instance.enabled) {
|
||||||
if (StatePolycentric.instance.processHandle != null) {
|
if (StatePolycentric.instance.processHandle != null) {
|
||||||
it.startActivity(Intent(it, PolycentricProfileActivity::class.java));
|
it.startActivity(Intent(it, PolycentricProfileActivity::class.java));
|
||||||
@@ -90,7 +88,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
fun openFAQ() {
|
fun openFAQ() {
|
||||||
try {
|
try {
|
||||||
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(Settings.URL_FAQ))
|
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(Settings.URL_FAQ))
|
||||||
StateApp?.instance?.activity?.startActivity(browserIntent);
|
SettingsActivity.getActivity()?.startActivity(browserIntent);
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
//Ignored
|
//Ignored
|
||||||
}
|
}
|
||||||
@@ -100,7 +98,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
fun openIssues() {
|
fun openIssues() {
|
||||||
try {
|
try {
|
||||||
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/futo-org/grayjay-android/issues"))
|
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/futo-org/grayjay-android/issues"))
|
||||||
StateApp?.instance?.activity?.startActivity(browserIntent);
|
SettingsActivity.getActivity()?.startActivity(browserIntent);
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
//Ignored
|
//Ignored
|
||||||
}
|
}
|
||||||
@@ -131,7 +129,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@FormFieldButton(R.drawable.ic_tabs)
|
@FormFieldButton(R.drawable.ic_tabs)
|
||||||
fun manageTabs() {
|
fun manageTabs() {
|
||||||
try {
|
try {
|
||||||
StateApp?.instance?.activity?.let {
|
SettingsActivity.getActivity()?.let {
|
||||||
it.startActivity(Intent(it, ManageTabsActivity::class.java));
|
it.startActivity(Intent(it, ManageTabsActivity::class.java));
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
@@ -144,7 +142,7 @@ 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, -3)
|
||||||
@FormFieldButton(R.drawable.ic_move_up)
|
@FormFieldButton(R.drawable.ic_move_up)
|
||||||
fun import() {
|
fun import() {
|
||||||
val act = StateApp.instance.activity ?: return;
|
val act = SettingsActivity.getActivity() ?: return;
|
||||||
val intent = MainActivity.getImportOptionsIntent(act);
|
val intent = MainActivity.getImportOptionsIntent(act);
|
||||||
act.startActivity(intent);
|
act.startActivity(intent);
|
||||||
}
|
}
|
||||||
@@ -153,7 +151,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@FormFieldButton(R.drawable.ic_link)
|
@FormFieldButton(R.drawable.ic_link)
|
||||||
fun manageLinks() {
|
fun manageLinks() {
|
||||||
try {
|
try {
|
||||||
StateApp.instance.activity?.let { UIDialogs.showUrlHandlingPrompt(it) }
|
SettingsActivity.getActivity()?.let { UIDialogs.showUrlHandlingPrompt(it) }
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to show url handling prompt", e)
|
Logger.e(TAG, "Failed to show url handling prompt", e)
|
||||||
}
|
}
|
||||||
@@ -162,7 +160,7 @@ 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)
|
/*@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)
|
@FormFieldButton(R.drawable.battery_full_24px)
|
||||||
fun ignoreBatteryOptimization() {
|
fun ignoreBatteryOptimization() {
|
||||||
StateApp.instance.activity?.let {
|
SettingsActivity.getActivity()?.let {
|
||||||
val intent = Intent()
|
val intent = Intent()
|
||||||
val packageName = it.packageName
|
val packageName = it.packageName
|
||||||
val pm = it.getSystemService(POWER_SERVICE) as PowerManager;
|
val pm = it.getSystemService(POWER_SERVICE) as PowerManager;
|
||||||
@@ -177,10 +175,6 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
}
|
}
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
|
|
||||||
@FormField(R.string.advanced_settings, FieldForm.TOGGLE, R.string.advanced_settings_description, -1, "advancedSettings")
|
|
||||||
var advancedSettings: Boolean = false;
|
|
||||||
|
|
||||||
@FormField(R.string.language, "group", -1, 0)
|
@FormField(R.string.language, "group", -1, 0)
|
||||||
var language = LanguageSettings();
|
var language = LanguageSettings();
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -202,8 +196,6 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
8 -> "zh";
|
8 -> "zh";
|
||||||
9 -> "ru";
|
9 -> "ru";
|
||||||
10 -> "ar";
|
10 -> "ar";
|
||||||
11 -> "it";
|
|
||||||
12 -> "tr";
|
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -229,11 +221,10 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@FormField(R.string.show_home_filters_plugin_names, FieldForm.TOGGLE, R.string.show_home_filters_plugin_names_description, 5)
|
@FormField(R.string.show_home_filters_plugin_names, FieldForm.TOGGLE, R.string.show_home_filters_plugin_names_description, 5)
|
||||||
var showHomeFiltersPluginNames: Boolean = false;
|
var showHomeFiltersPluginNames: Boolean = false;
|
||||||
|
|
||||||
@AdvancedField
|
|
||||||
@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;
|
||||||
|
|
||||||
@AdvancedField
|
|
||||||
@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;
|
||||||
|
|
||||||
@@ -243,7 +234,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
fun clearHidden() {
|
fun clearHidden() {
|
||||||
StateMeta.instance.removeAllHiddenCreators();
|
StateMeta.instance.removeAllHiddenCreators();
|
||||||
StateMeta.instance.removeAllHiddenVideos();
|
StateMeta.instance.removeAllHiddenVideos();
|
||||||
StateApp.instance.activity?.let {
|
SettingsActivity.getActivity()?.let {
|
||||||
UIDialogs.toast(it, "Creators and videos should show up again");
|
UIDialogs.toast(it, "Creators and videos should show up again");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -262,11 +253,9 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@DropdownFieldOptionsId(R.array.feed_style)
|
@DropdownFieldOptionsId(R.array.feed_style)
|
||||||
var searchFeedStyle: Int = 1;
|
var searchFeedStyle: Int = 1;
|
||||||
|
|
||||||
@AdvancedField
|
|
||||||
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 5)
|
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 5)
|
||||||
var previewFeedItems: Boolean = true;
|
var previewFeedItems: Boolean = true;
|
||||||
|
|
||||||
@AdvancedField
|
|
||||||
@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;
|
||||||
|
|
||||||
@@ -288,7 +277,6 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@Serializable
|
@Serializable
|
||||||
class ChannelSettings {
|
class ChannelSettings {
|
||||||
|
|
||||||
@AdvancedField
|
|
||||||
@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;
|
||||||
}
|
}
|
||||||
@@ -314,20 +302,16 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@FormField(R.string.use_subscription_exchange, FieldForm.TOGGLE, R.string.use_subscription_exchange_description, 6)
|
@FormField(R.string.use_subscription_exchange, FieldForm.TOGGLE, R.string.use_subscription_exchange_description, 6)
|
||||||
var useSubscriptionExchange: Boolean = false;
|
var useSubscriptionExchange: Boolean = false;
|
||||||
|
|
||||||
@AdvancedField
|
|
||||||
@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;
|
||||||
|
|
||||||
@AdvancedField
|
|
||||||
@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, 7)
|
||||||
var progressBar: Boolean = true;
|
var progressBar: Boolean = true;
|
||||||
|
|
||||||
@AdvancedField
|
|
||||||
@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, 8)
|
||||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||||
var fetchOnAppBoot: Boolean = true;
|
var fetchOnAppBoot: Boolean = true;
|
||||||
|
|
||||||
@AdvancedField
|
|
||||||
@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, 9)
|
||||||
var fetchOnTabOpen: Boolean = true;
|
var fetchOnTabOpen: Boolean = true;
|
||||||
|
|
||||||
@@ -358,24 +342,21 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@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, 12)
|
||||||
var showWatchMetrics: Boolean = false;
|
var showWatchMetrics: Boolean = false;
|
||||||
|
|
||||||
@AdvancedField
|
|
||||||
@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, 13)
|
||||||
var allowPlaytimeTracking: Boolean = true;
|
var allowPlaytimeTracking: Boolean = true;
|
||||||
|
|
||||||
|
|
||||||
@AdvancedField
|
|
||||||
@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, 14)
|
||||||
var alwaysReloadFromCache: Boolean = false;
|
var alwaysReloadFromCache: Boolean = false;
|
||||||
|
|
||||||
@AdvancedField
|
|
||||||
@FormField(R.string.peek_channel_contents, FieldForm.TOGGLE, R.string.peek_channel_contents_description, 15)
|
@FormField(R.string.peek_channel_contents, FieldForm.TOGGLE, R.string.peek_channel_contents_description, 15)
|
||||||
var peekChannelContents: Boolean = false;
|
var peekChannelContents: Boolean = false;
|
||||||
|
|
||||||
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON, R.string.clear_channel_cache_description, 16)
|
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON, R.string.clear_channel_cache_description, 16)
|
||||||
fun clearChannelCache() {
|
fun clearChannelCache() {
|
||||||
UIDialogs.toast(StateApp.instance.activity!!, "Started clearing..");
|
UIDialogs.toast(SettingsActivity.getActivity()!!, "Started clearing..");
|
||||||
StateCache.instance.clear();
|
StateCache.instance.clear();
|
||||||
UIDialogs.toast(StateApp.instance.activity!!, "Finished clearing");
|
UIDialogs.toast(SettingsActivity.getActivity()!!, "Finished clearing");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -407,10 +388,6 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.sticky_subtitles, FieldForm.TOGGLE, R.string.sticky_subtitles_description, -1)
|
|
||||||
var stickySubtitles: Boolean = true;
|
|
||||||
|
|
||||||
@FormField(R.string.prefer_original_audio, FieldForm.TOGGLE, R.string.prefer_original_audio_description, -1)
|
@FormField(R.string.prefer_original_audio, FieldForm.TOGGLE, R.string.prefer_original_audio_description, -1)
|
||||||
var preferOriginalAudio: Boolean = true;
|
var preferOriginalAudio: Boolean = true;
|
||||||
|
|
||||||
@@ -429,9 +406,6 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
6 -> 1.75f;
|
6 -> 1.75f;
|
||||||
7 -> 2.0f;
|
7 -> 2.0f;
|
||||||
8 -> 2.25f;
|
8 -> 2.25f;
|
||||||
9 -> 2.5f;
|
|
||||||
10 -> 2.75f;
|
|
||||||
11 -> 3.0f;
|
|
||||||
else -> 1.0f;
|
else -> 1.0f;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -451,11 +425,9 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
var preferredPreviewQuality: Int = 5;
|
var preferredPreviewQuality: Int = 5;
|
||||||
fun getPreferredPreviewQualityPixelCount(): Int = preferedQualityToPixels(preferredPreviewQuality);
|
fun getPreferredPreviewQualityPixelCount(): Int = preferedQualityToPixels(preferredPreviewQuality);
|
||||||
|
|
||||||
@AdvancedField
|
|
||||||
@FormField(R.string.simplify_sources, FieldForm.TOGGLE, R.string.simplify_sources_description, 4)
|
@FormField(R.string.simplify_sources, FieldForm.TOGGLE, R.string.simplify_sources_description, 4)
|
||||||
var simplifySources: Boolean = true;
|
var simplifySources: Boolean = true;
|
||||||
|
|
||||||
@AdvancedField
|
|
||||||
@FormField(R.string.always_allow_reverse_landscape_auto_rotate, FieldForm.TOGGLE, R.string.always_allow_reverse_landscape_auto_rotate_description, 5)
|
@FormField(R.string.always_allow_reverse_landscape_auto_rotate, FieldForm.TOGGLE, R.string.always_allow_reverse_landscape_auto_rotate_description, 5)
|
||||||
var alwaysAllowReverseLandscapeAutoRotate: Boolean = true
|
var alwaysAllowReverseLandscapeAutoRotate: Boolean = true
|
||||||
|
|
||||||
@@ -466,7 +438,6 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
fun isBackgroundContinue() = backgroundPlay == 1;
|
fun isBackgroundContinue() = backgroundPlay == 1;
|
||||||
fun isBackgroundPictureInPicture() = backgroundPlay == 2;
|
fun isBackgroundPictureInPicture() = backgroundPlay == 2;
|
||||||
|
|
||||||
@AdvancedField
|
|
||||||
@FormField(R.string.resume_after_preview, FieldForm.DROPDOWN, R.string.when_watching_a_video_in_preview_mode_resume_at_the_position_when_opening_the_video_code, 7)
|
@FormField(R.string.resume_after_preview, FieldForm.DROPDOWN, R.string.when_watching_a_video_in_preview_mode_resume_at_the_position_when_opening_the_video_code, 7)
|
||||||
@DropdownFieldOptionsId(R.array.resume_after_preview)
|
@DropdownFieldOptionsId(R.array.resume_after_preview)
|
||||||
var resumeAfterPreview: Int = 1;
|
var resumeAfterPreview: Int = 1;
|
||||||
@@ -493,10 +464,14 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@AdvancedField
|
|
||||||
@FormField(R.string.live_chat_webview, FieldForm.TOGGLE, R.string.use_the_live_chat_web_window_when_available_over_native_implementation, 9)
|
@FormField(R.string.live_chat_webview, FieldForm.TOGGLE, R.string.use_the_live_chat_web_window_when_available_over_native_implementation, 9)
|
||||||
var useLiveChatWindow: Boolean = true;
|
var useLiveChatWindow: Boolean = true;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@FormField(R.string.background_switch_audio, FieldForm.TOGGLE, R.string.background_switch_audio_description, 10)
|
||||||
|
var backgroundSwitchToAudio: Boolean = true;
|
||||||
|
|
||||||
@FormField(R.string.restart_after_audio_focus_loss, FieldForm.DROPDOWN, R.string.restart_playback_when_gaining_audio_focus_after_a_loss, 11)
|
@FormField(R.string.restart_after_audio_focus_loss, FieldForm.DROPDOWN, R.string.restart_playback_when_gaining_audio_focus_after_a_loss, 11)
|
||||||
@DropdownFieldOptionsId(R.array.restart_playback_after_loss)
|
@DropdownFieldOptionsId(R.array.restart_playback_after_loss)
|
||||||
var restartPlaybackAfterLoss: Int = 1;
|
var restartPlaybackAfterLoss: Int = 1;
|
||||||
@@ -522,107 +497,8 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@FormField(R.string.autoplay, FieldForm.TOGGLE, R.string.autoplay, 21)
|
@FormField(R.string.autoplay, FieldForm.TOGGLE, R.string.autoplay, 21)
|
||||||
var autoplay: Boolean = false;
|
var autoplay: Boolean = false;
|
||||||
|
|
||||||
@AdvancedField
|
|
||||||
@FormField(R.string.delete_watchlist_on_finish, FieldForm.TOGGLE, R.string.delete_watchlist_on_finish_description, 22)
|
@FormField(R.string.delete_watchlist_on_finish, FieldForm.TOGGLE, R.string.delete_watchlist_on_finish_description, 22)
|
||||||
var deleteFromWatchLaterAuto: Boolean = true;
|
var deleteFromWatchLaterAuto: Boolean = true;
|
||||||
|
|
||||||
@FormField(R.string.seek_offset, FieldForm.DROPDOWN, R.string.seek_offset_description, 23)
|
|
||||||
@DropdownFieldOptionsId(R.array.seek_offset_duration)
|
|
||||||
var seekOffset: Int = 2;
|
|
||||||
|
|
||||||
fun getSeekOffset(): Long {
|
|
||||||
return when(seekOffset) {
|
|
||||||
0 -> 3_000L;
|
|
||||||
1 -> 5_000L;
|
|
||||||
2 -> 10_000L;
|
|
||||||
3 -> 20_000L;
|
|
||||||
4 -> 30_000L;
|
|
||||||
5 -> 60_000L;
|
|
||||||
else -> 10_000L;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@FormField(R.string.min_playback_speed, FieldForm.DROPDOWN, R.string.min_playback_speed_description, 25)
|
|
||||||
@DropdownFieldOptionsId(R.array.min_playback_speed)
|
|
||||||
var minimumPlaybackSpeed: Int = 0;
|
|
||||||
@FormField(R.string.max_playback_speed, FieldForm.DROPDOWN, R.string.max_playback_speed_description, 26)
|
|
||||||
@DropdownFieldOptionsId(R.array.max_playback_speed)
|
|
||||||
var maximumPlaybackSpeed: Int = 2;
|
|
||||||
@FormField(R.string.step_playback_speed, FieldForm.DROPDOWN, R.string.step_playback_speed_description, 26)
|
|
||||||
@DropdownFieldOptionsId(R.array.step_playback_speed)
|
|
||||||
var stepPlaybackSpeed: Int = 1;
|
|
||||||
|
|
||||||
fun getPlaybackSpeedStep(): Double {
|
|
||||||
return when(stepPlaybackSpeed) {
|
|
||||||
0 -> 0.05
|
|
||||||
1 -> 0.1
|
|
||||||
2 -> 0.25
|
|
||||||
else -> 0.1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fun getPlaybackSpeeds(): List<Double> {
|
|
||||||
val playbackSpeeds = mutableListOf<Double>();
|
|
||||||
playbackSpeeds.add(1.0);
|
|
||||||
val minSpeed = when(minimumPlaybackSpeed) {
|
|
||||||
0 -> 0.25
|
|
||||||
1 -> 0.5
|
|
||||||
2 -> 1.0
|
|
||||||
else -> 0.25
|
|
||||||
}
|
|
||||||
val maxSpeed = when(maximumPlaybackSpeed) {
|
|
||||||
0 -> 2.0
|
|
||||||
1 -> 2.25
|
|
||||||
2 -> 3.0
|
|
||||||
3 -> 4.0
|
|
||||||
4 -> 5.0
|
|
||||||
else -> 2.25;
|
|
||||||
}
|
|
||||||
var testSpeed = 1.0;
|
|
||||||
|
|
||||||
while(testSpeed > minSpeed) {
|
|
||||||
val nextSpeed = (testSpeed - 0.25) as Double;
|
|
||||||
testSpeed = Math.max(nextSpeed, minSpeed);
|
|
||||||
playbackSpeeds.add(testSpeed);
|
|
||||||
}
|
|
||||||
testSpeed = 1.0;
|
|
||||||
while(testSpeed < maxSpeed) {
|
|
||||||
val nextSpeed = (testSpeed + if(testSpeed < 2) 0.25 else 1.0) as Double;
|
|
||||||
testSpeed = Math.min(nextSpeed, maxSpeed);
|
|
||||||
playbackSpeeds.add(testSpeed);
|
|
||||||
}
|
|
||||||
playbackSpeeds.sort();
|
|
||||||
return playbackSpeeds;
|
|
||||||
}
|
|
||||||
|
|
||||||
@FormField(R.string.hold_playback_speed, FieldForm.DROPDOWN, R.string.hold_playback_speed_description, 27)
|
|
||||||
@DropdownFieldOptionsId(R.array.hold_playback_speeds)
|
|
||||||
var holdPlaybackSpeed: Int = 4;
|
|
||||||
|
|
||||||
fun getHoldPlaybackSpeed(): Double {
|
|
||||||
return when(holdPlaybackSpeed) {
|
|
||||||
0 -> 1.0
|
|
||||||
1 -> 1.25
|
|
||||||
2 -> 1.5
|
|
||||||
3 -> 1.75
|
|
||||||
4 -> 2.0
|
|
||||||
5 -> 2.25
|
|
||||||
6 -> 2.5
|
|
||||||
7 -> 2.75
|
|
||||||
8 -> 3.0
|
|
||||||
else -> 2.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@AdvancedField
|
|
||||||
@FormField(R.string.shorts_pregenerate, FieldForm.TOGGLE, R.string.shorts_pregenerate_description, 28)
|
|
||||||
var shortsPregenerate: Boolean = false;
|
|
||||||
|
|
||||||
@AdvancedField
|
|
||||||
@FormField(R.string.shorts_fit_video, FieldForm.TOGGLE, R.string.shorts_fit_video_description, 29)
|
|
||||||
@FormFieldWarning(R.string.shorts_fit_video_warning)
|
|
||||||
var shortsFitVideo: Boolean = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.comments, "group", R.string.comments_description, 6)
|
@FormField(R.string.comments, "group", R.string.comments_description, 6)
|
||||||
@@ -638,7 +514,6 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@FormField(R.string.default_recommendations, FieldForm.TOGGLE, R.string.default_recommendations_description, 0)
|
@FormField(R.string.default_recommendations, FieldForm.TOGGLE, R.string.default_recommendations_description, 0)
|
||||||
var recommendationsDefault: Boolean = false;
|
var recommendationsDefault: Boolean = false;
|
||||||
|
|
||||||
@AdvancedField
|
|
||||||
@FormField(R.string.hide_recommendations, FieldForm.TOGGLE, R.string.hide_recommendations_description, 0)
|
@FormField(R.string.hide_recommendations, FieldForm.TOGGLE, R.string.hide_recommendations_description, 0)
|
||||||
var hideRecommendations: Boolean = false;
|
var hideRecommendations: Boolean = false;
|
||||||
|
|
||||||
@@ -675,12 +550,10 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
var preferredAudioQuality: Int = 1;
|
var preferredAudioQuality: Int = 1;
|
||||||
fun isHighBitrateDefault(): Boolean = preferredAudioQuality > 0;
|
fun isHighBitrateDefault(): Boolean = preferredAudioQuality > 0;
|
||||||
|
|
||||||
@AdvancedField
|
|
||||||
@FormField(R.string.byte_range_download, FieldForm.TOGGLE, R.string.attempt_to_utilize_byte_ranges, 4)
|
@FormField(R.string.byte_range_download, FieldForm.TOGGLE, R.string.attempt_to_utilize_byte_ranges, 4)
|
||||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||||
var byteRangeDownload: Boolean = true;
|
var byteRangeDownload: Boolean = true;
|
||||||
|
|
||||||
@AdvancedField
|
|
||||||
@FormField(R.string.byte_range_concurrency, FieldForm.DROPDOWN, R.string.number_of_concurrent_threads_to_multiply_download_speeds_from_throttled_sources, 5)
|
@FormField(R.string.byte_range_concurrency, FieldForm.DROPDOWN, R.string.number_of_concurrent_threads_to_multiply_download_speeds_from_throttled_sources, 5)
|
||||||
@DropdownFieldOptionsId(R.array.thread_count)
|
@DropdownFieldOptionsId(R.array.thread_count)
|
||||||
var byteRangeConcurrency: Int = 3;
|
var byteRangeConcurrency: Int = 3;
|
||||||
@@ -710,25 +583,14 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||||
var keepScreenOn: Boolean = true;
|
var keepScreenOn: Boolean = true;
|
||||||
|
|
||||||
@AdvancedField
|
|
||||||
@FormField(R.string.always_proxy_requests, FieldForm.TOGGLE, R.string.always_proxy_requests_description, 3)
|
@FormField(R.string.always_proxy_requests, FieldForm.TOGGLE, R.string.always_proxy_requests_description, 3)
|
||||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||||
var alwaysProxyRequests: Boolean = false;
|
var alwaysProxyRequests: Boolean = false;
|
||||||
|
|
||||||
@AdvancedField
|
|
||||||
@FormField(R.string.allow_ipv6, FieldForm.TOGGLE, R.string.allow_ipv6_description, 4)
|
@FormField(R.string.allow_ipv6, FieldForm.TOGGLE, R.string.allow_ipv6_description, 4)
|
||||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||||
var allowIpv6: Boolean = true;
|
var allowIpv6: Boolean = false;
|
||||||
|
|
||||||
@AdvancedField
|
|
||||||
@FormField(R.string.allow_ipv4, FieldForm.TOGGLE, R.string.allow_ipv4_description, 5)
|
|
||||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
|
||||||
var allowLinkLocalIpv4: Boolean = false;
|
|
||||||
|
|
||||||
@AdvancedField
|
|
||||||
@FormField(R.string.experimental_cast, FieldForm.TOGGLE, R.string.experimental_cast_description, 6)
|
|
||||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
|
||||||
var experimentalCasting: Boolean = true
|
|
||||||
|
|
||||||
/*TODO: Should we have a different casting quality?
|
/*TODO: Should we have a different casting quality?
|
||||||
@FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3)
|
@FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3)
|
||||||
@@ -762,7 +624,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
try {
|
try {
|
||||||
if (!Logger.submitLogs()) {
|
if (!Logger.submitLogs()) {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
StateApp.instance.activity?.let { UIDialogs.toast(it, it.getString(R.string.please_enable_logging_to_submit_logs)) }
|
SettingsActivity.getActivity()?.let { UIDialogs.toast(it, it.getString(R.string.please_enable_logging_to_submit_logs)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
@@ -779,7 +641,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@FormField(R.string.reset_announcements, FieldForm.BUTTON, R.string.reset_hidden_announcements, 1)
|
@FormField(R.string.reset_announcements, FieldForm.BUTTON, R.string.reset_hidden_announcements, 1)
|
||||||
fun resetAnnouncements() {
|
fun resetAnnouncements() {
|
||||||
StateAnnouncement.instance.resetAnnouncements();
|
StateAnnouncement.instance.resetAnnouncements();
|
||||||
StateApp.instance.activity?.let { UIDialogs.toast(it, it.getString(R.string.announcements_reset)); };
|
SettingsActivity.getActivity()?.let { UIDialogs.toast(it, it.getString(R.string.announcements_reset)); };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -797,11 +659,9 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@Serializable
|
@Serializable
|
||||||
class Plugins {
|
class Plugins {
|
||||||
|
|
||||||
@AdvancedField
|
|
||||||
@FormField(R.string.check_disabled_plugin_updates, FieldForm.TOGGLE, R.string.check_disabled_plugin_updates_description, -1)
|
@FormField(R.string.check_disabled_plugin_updates, FieldForm.TOGGLE, R.string.check_disabled_plugin_updates_description, -1)
|
||||||
var checkDisabledPluginsForUpdates: Boolean = false;
|
var checkDisabledPluginsForUpdates: Boolean = false;
|
||||||
|
|
||||||
@AdvancedField
|
|
||||||
@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;
|
||||||
|
|
||||||
@@ -847,13 +707,13 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
@FormField(R.string.change_external_general_directory, FieldForm.BUTTON, R.string.change_the_external_directory_for_general_files, 3)
|
@FormField(R.string.change_external_general_directory, FieldForm.BUTTON, R.string.change_the_external_directory_for_general_files, 3)
|
||||||
fun changeStorageGeneral() {
|
fun changeStorageGeneral() {
|
||||||
StateApp.instance.activity?.let {
|
SettingsActivity.getActivity()?.let {
|
||||||
StateApp.instance.changeExternalGeneralDirectory(it);
|
StateApp.instance.changeExternalGeneralDirectory(it);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@FormField(R.string.change_external_downloads_directory, FieldForm.BUTTON, R.string.change_the_external_storage_for_download_files, 4)
|
@FormField(R.string.change_external_downloads_directory, FieldForm.BUTTON, R.string.change_the_external_storage_for_download_files, 4)
|
||||||
fun changeStorageDownload() {
|
fun changeStorageDownload() {
|
||||||
StateApp.instance.activity?.let {
|
SettingsActivity.getActivity()?.let {
|
||||||
StateApp.instance.changeExternalDownloadDirectory(it);
|
StateApp.instance.changeExternalDownloadDirectory(it);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -862,7 +722,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
fun clearStorageDownload() {
|
fun clearStorageDownload() {
|
||||||
Settings.instance.storage.storage_download = null;
|
Settings.instance.storage.storage_download = null;
|
||||||
Settings.instance.save();
|
Settings.instance.save();
|
||||||
StateApp.instance.activity?.let { UIDialogs.toast(it, "Cleared download storage directory") };
|
SettingsActivity.getActivity()?.let { UIDialogs.toast(it, "Cleared download storage directory") };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -899,13 +759,13 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@FormField(R.string.manual_check, FieldForm.BUTTON, R.string.manually_check_for_updates, 3)
|
@FormField(R.string.manual_check, FieldForm.BUTTON, R.string.manually_check_for_updates, 3)
|
||||||
fun manualCheck() {
|
fun manualCheck() {
|
||||||
if (!BuildConfig.IS_PLAYSTORE_BUILD) {
|
if (!BuildConfig.IS_PLAYSTORE_BUILD) {
|
||||||
StateApp.instance.activity?.let {
|
SettingsActivity.getActivity()?.let {
|
||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
StateUpdate.instance.checkForUpdates(it, true)
|
StateUpdate.instance.checkForUpdates(it, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
StateApp.instance.activity?.let {
|
SettingsActivity.getActivity()?.let {
|
||||||
try {
|
try {
|
||||||
it.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=${it.packageName}")))
|
it.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=${it.packageName}")))
|
||||||
} catch (e: ActivityNotFoundException) {
|
} catch (e: ActivityNotFoundException) {
|
||||||
@@ -917,7 +777,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
@FormField(R.string.view_changelog, FieldForm.BUTTON, R.string.review_the_current_and_past_changelogs, 4)
|
@FormField(R.string.view_changelog, FieldForm.BUTTON, R.string.review_the_current_and_past_changelogs, 4)
|
||||||
fun viewChangelog() {
|
fun viewChangelog() {
|
||||||
StateApp.instance.activity?.let {
|
SettingsActivity.getActivity()?.let {
|
||||||
UIDialogs.toast(it.getString(R.string.retrieving_changelog));
|
UIDialogs.toast(it.getString(R.string.retrieving_changelog));
|
||||||
|
|
||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
@@ -957,7 +817,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
class Backup {
|
class Backup {
|
||||||
@Serializable(with = OffsetDateTimeSerializer::class)
|
@Serializable(with = OffsetDateTimeSerializer::class)
|
||||||
var lastAutoBackupTime: OffsetDateTime = OffsetDateTime.MIN;
|
var lastAutoBackupTime: OffsetDateTime = OffsetDateTime.MIN;
|
||||||
var didAskAutoBackup: Boolean = true;
|
var didAskAutoBackup: Boolean = false;
|
||||||
var autoBackupPassword: String? = null;
|
var autoBackupPassword: String? = null;
|
||||||
fun shouldAutomaticBackup() = autoBackupPassword != null;
|
fun shouldAutomaticBackup() = autoBackupPassword != null;
|
||||||
|
|
||||||
@@ -966,13 +826,13 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
@FormField(R.string.set_automatic_backup, FieldForm.BUTTON, R.string.configure_daily_backup_in_case_of_catastrophic_failure, 1)
|
@FormField(R.string.set_automatic_backup, FieldForm.BUTTON, R.string.configure_daily_backup_in_case_of_catastrophic_failure, 1)
|
||||||
fun configureAutomaticBackup() {
|
fun configureAutomaticBackup() {
|
||||||
UIDialogs.showAutomaticBackupDialog(StateApp.instance.activity!!, autoBackupPassword != null) {
|
UIDialogs.showAutomaticBackupDialog(SettingsActivity.getActivity()!!, autoBackupPassword != null) {
|
||||||
SettingsFragment.currentView?.reloadSettings();
|
SettingsActivity.getActivity()?.reloadSettings();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@FormField(R.string.restore_automatic_backup, FieldForm.BUTTON, R.string.restore_a_previous_automatic_backup, 2)
|
@FormField(R.string.restore_automatic_backup, FieldForm.BUTTON, R.string.restore_a_previous_automatic_backup, 2)
|
||||||
fun restoreAutomaticBackup() {
|
fun restoreAutomaticBackup() {
|
||||||
val activity = StateApp.instance.activity!!
|
val activity = SettingsActivity.getActivity()!!
|
||||||
|
|
||||||
if(!StateBackup.hasAutomaticBackup())
|
if(!StateBackup.hasAutomaticBackup())
|
||||||
UIDialogs.toast(activity, activity.getString(R.string.you_don_t_have_any_automatic_backups), false);
|
UIDialogs.toast(activity, activity.getString(R.string.you_don_t_have_any_automatic_backups), false);
|
||||||
@@ -983,9 +843,8 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
@FormField(R.string.export_data, FieldForm.BUTTON, R.string.creates_a_zip_file_with_your_data_which_can_be_imported_by_opening_it_with_grayjay, 3)
|
@FormField(R.string.export_data, FieldForm.BUTTON, R.string.creates_a_zip_file_with_your_data_which_can_be_imported_by_opening_it_with_grayjay, 3)
|
||||||
fun export() {
|
fun export() {
|
||||||
val activity = StateApp.instance.activity ?: return;
|
val activity = SettingsActivity.getActivity() ?: return;
|
||||||
val fragView = SettingsFragment.currentView ?: return;
|
UISlideOverlays.showOverlay(activity.overlay, "Select export type", null, {},
|
||||||
UISlideOverlays.showOverlay(fragView.overlay, "Select export type", null, {},
|
|
||||||
SlideUpMenuItem(activity, R.drawable.ic_share, "Share", "", tag = null, call = {
|
SlideUpMenuItem(activity, R.drawable.ic_share, "Share", "", tag = null, call = {
|
||||||
StateBackup.shareExternalBackup();
|
StateBackup.shareExternalBackup();
|
||||||
}),
|
}),
|
||||||
@@ -1001,32 +860,16 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@Serializable
|
@Serializable
|
||||||
class Payment {
|
class Payment {
|
||||||
@FormField(R.string.payment_status, FieldForm.READONLYTEXT, -1, 1)
|
@FormField(R.string.payment_status, FieldForm.READONLYTEXT, -1, 1)
|
||||||
val paymentStatus: String get() = StateApp.instance.activity?.let { if (StatePayment.instance.hasPaid) it.getString(R.string.paid) else it.getString(R.string.not_paid); } ?: "Unknown";
|
val paymentStatus: String get() = SettingsActivity.getActivity()?.let { if (StatePayment.instance.hasPaid) it.getString(R.string.paid) else it.getString(R.string.not_paid); } ?: "Unknown";
|
||||||
|
|
||||||
@FormField(R.string.license_status, FieldForm.BUTTON, R.string.view_license_status, 2)
|
@FormField(R.string.clear_payment, FieldForm.BUTTON, R.string.deletes_license_keys_from_app, 2)
|
||||||
fun viewLicenseStatus() {
|
|
||||||
StateApp.instance.activity?.let {
|
|
||||||
try {
|
|
||||||
if (StatePayment.instance.hasPaid) {
|
|
||||||
val paymentKey = StatePayment.instance.getPaymentKey()
|
|
||||||
UIDialogs.showDialogOk(it, R.drawable.ic_paid, "License activated\n" + paymentKey.first)
|
|
||||||
} else {
|
|
||||||
UIDialogs.showDialogOk(it, R.drawable.ic_paid, "No license activated")
|
|
||||||
}
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.e(TAG, "Failed to show license status dialog", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@FormField(R.string.clear_payment, FieldForm.BUTTON, R.string.deletes_license_keys_from_app, 3)
|
|
||||||
fun clearPayment() {
|
fun clearPayment() {
|
||||||
StateApp.instance.activity?.let { context ->
|
SettingsActivity.getActivity()?.let { context ->
|
||||||
UIDialogs.showConfirmationDialog(context, "Are you sure you want to delete your license?", {
|
UIDialogs.showConfirmationDialog(context, "Are you sure you want to delete your license?", {
|
||||||
StatePayment.instance.clearLicenses();
|
StatePayment.instance.clearLicenses();
|
||||||
StateApp.instance.activity?.let {
|
SettingsActivity.getActivity()?.let {
|
||||||
UIDialogs.toast(it, it.getString(R.string.licenses_cleared_might_require_app_restart));
|
UIDialogs.toast(it, it.getString(R.string.licenses_cleared_might_require_app_restart));
|
||||||
SettingsFragment.currentView?.reloadSettings();
|
it.reloadSettings();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -1037,23 +880,16 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
var other = Other();
|
var other = Other();
|
||||||
@Serializable
|
@Serializable
|
||||||
class Other {
|
class Other {
|
||||||
@AdvancedField
|
|
||||||
@FormField(R.string.playlist_delete_confirmation, FieldForm.TOGGLE, R.string.playlist_delete_confirmation_description, 2)
|
@FormField(R.string.playlist_delete_confirmation, FieldForm.TOGGLE, R.string.playlist_delete_confirmation_description, 2)
|
||||||
var playlistDeleteConfirmation: Boolean = true;
|
var playlistDeleteConfirmation: Boolean = true;
|
||||||
@AdvancedField
|
|
||||||
@FormField(R.string.playlist_allow_dups, FieldForm.TOGGLE, R.string.playlist_allow_dups_description, 3)
|
@FormField(R.string.playlist_allow_dups, FieldForm.TOGGLE, R.string.playlist_allow_dups_description, 3)
|
||||||
var playlistAllowDups: Boolean = true;
|
var playlistAllowDups: Boolean = true;
|
||||||
|
|
||||||
@FormField(R.string.watch_later_add_start, FieldForm.TOGGLE, R.string.watch_later_add_start_description, 4)
|
@FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 4)
|
||||||
var watchLaterAddStart: Boolean = true;
|
|
||||||
|
|
||||||
@FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 5)
|
|
||||||
var polycentricEnabled: Boolean = true;
|
var polycentricEnabled: Boolean = true;
|
||||||
|
|
||||||
@FormField(R.string.polycentric_local_cache, FieldForm.TOGGLE, R.string.polycentric_local_cache_description, 7)
|
@FormField(R.string.polycentric_local_cache, FieldForm.TOGGLE, R.string.polycentric_local_cache_description, 5)
|
||||||
var polycentricLocalCache: Boolean = true;
|
var polycentricLocalCache: Boolean = true;
|
||||||
|
|
||||||
var showPrivacyModeDialog: Boolean = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.gesture_controls, FieldForm.GROUP, -1, 19)
|
@FormField(R.string.gesture_controls, FieldForm.GROUP, -1, 19)
|
||||||
@@ -1090,7 +926,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@Serializable
|
@Serializable
|
||||||
class Synchronization {
|
class Synchronization {
|
||||||
@FormField(R.string.enabled, FieldForm.TOGGLE, R.string.enabled_description, 1)
|
@FormField(R.string.enabled, FieldForm.TOGGLE, R.string.enabled_description, 1)
|
||||||
var enabled: Boolean = false;
|
var enabled: Boolean = true;
|
||||||
|
|
||||||
@FormField(R.string.broadcast, FieldForm.TOGGLE, R.string.broadcast_description, 1)
|
@FormField(R.string.broadcast, FieldForm.TOGGLE, R.string.broadcast_description, 1)
|
||||||
var broadcast: Boolean = false;
|
var broadcast: Boolean = false;
|
||||||
@@ -1100,54 +936,6 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
@FormField(R.string.connect_last, FieldForm.TOGGLE, R.string.connect_last_description, 3)
|
@FormField(R.string.connect_last, FieldForm.TOGGLE, R.string.connect_last_description, 3)
|
||||||
var connectLast: Boolean = true;
|
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.connect_local_direct_through_relay, FieldForm.TOGGLE, R.string.connect_local_direct_through_relay_description, 3)
|
|
||||||
var connectLocalDirectThroughRelay: Boolean = true;
|
|
||||||
|
|
||||||
@FormField(R.string.local_connections, FieldForm.TOGGLE, R.string.local_connections_description, 3)
|
|
||||||
var localConnections: Boolean = true;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
var syncServerUrl: String? = null;
|
|
||||||
@FormField(R.string.relay_server, FieldForm.READONLYTEXT, -1, 6)
|
|
||||||
val syncServer: String get() = if(syncServerUrl?.isBlank() == true) StateSync.RELAY_SERVER else syncServerUrl ?: StateSync.RELAY_SERVER;
|
|
||||||
|
|
||||||
@AdvancedField
|
|
||||||
@FormField(R.string.configure_sync_server, FieldForm.BUTTON, R.string.configure_sync_server_description, 7)
|
|
||||||
fun configureSyncServer() {
|
|
||||||
StateApp.instance.activity?.let { context ->
|
|
||||||
UIDialogs.showDialog(context, R.drawable.device_sync, false,
|
|
||||||
"Enter the url to your relay server",
|
|
||||||
"Using your own relay server requires a proper setup with portforwarding.\nUse at your own risk.",
|
|
||||||
null,
|
|
||||||
syncServerUrl ?: "",
|
|
||||||
"YourRelayServerDomain.com", 0,
|
|
||||||
UIDialogs.Action("Cancel", {}),
|
|
||||||
UIDialogs.Action("Reset", {
|
|
||||||
syncServerUrl = null;
|
|
||||||
instance.save();
|
|
||||||
SettingsFragment.currentView?.reloadSettings();
|
|
||||||
UIDialogs.toast("Sync server changes require a restart");
|
|
||||||
}, UIDialogs.ActionStyle.ACCENT),
|
|
||||||
UIDialogs.Action.withInput("Configure", {
|
|
||||||
syncServerUrl = it?.text
|
|
||||||
instance.save();
|
|
||||||
SettingsFragment.currentView?.reloadSettings();
|
|
||||||
UIDialogs.toast("Sync server changes require a restart");
|
|
||||||
}, UIDialogs.ActionStyle.PRIMARY),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.info, FieldForm.GROUP, -1, 21)
|
@FormField(R.string.info, FieldForm.GROUP, -1, 21)
|
||||||
@@ -1216,4 +1004,4 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
//endregion
|
//endregion
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,9 @@ import androidx.work.OneTimeWorkRequestBuilder
|
|||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import com.caoccao.javet.values.primitive.V8ValueInteger
|
import com.caoccao.javet.values.primitive.V8ValueInteger
|
||||||
import com.caoccao.javet.values.primitive.V8ValueString
|
import com.caoccao.javet.values.primitive.V8ValueString
|
||||||
|
import com.futo.platformplayer.activities.DeveloperActivity
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
|
import com.futo.platformplayer.activities.SettingsActivity
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
@@ -18,8 +20,6 @@ import com.futo.platformplayer.api.media.platforms.js.SourcePluginDescriptor
|
|||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.background.BackgroundWorker
|
import com.futo.platformplayer.background.BackgroundWorker
|
||||||
import com.futo.platformplayer.engine.V8Plugin
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.DeveloperFragment
|
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.SettingsFragment
|
|
||||||
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.states.StateAnnouncement
|
import com.futo.platformplayer.states.StateAnnouncement
|
||||||
@@ -97,10 +97,10 @@ class SettingsDev : FragmentedStorageFileJson() {
|
|||||||
fun subscriptionsCache5000() {
|
fun subscriptionsCache5000() {
|
||||||
Logger.i("SettingsDev", "Started caching 5000 sub items");
|
Logger.i("SettingsDev", "Started caching 5000 sub items");
|
||||||
UIDialogs.toast(
|
UIDialogs.toast(
|
||||||
StateApp.instance.activity!!,
|
SettingsActivity.getActivity()!!,
|
||||||
"Started caching 5000 sub items"
|
"Started caching 5000 sub items"
|
||||||
);
|
);
|
||||||
val button = DeveloperFragment.currentView?.getField("subscription_cache_button");
|
val button = DeveloperActivity.getActivity()?.getField("subscription_cache_button");
|
||||||
if(button is ButtonField)
|
if(button is ButtonField)
|
||||||
button.setButtonEnabled(false);
|
button.setButtonEnabled(false);
|
||||||
StateApp.instance.scope.launch(Dispatchers.IO) {
|
StateApp.instance.scope.launch(Dispatchers.IO) {
|
||||||
@@ -121,7 +121,7 @@ class SettingsDev : FragmentedStorageFileJson() {
|
|||||||
val diff = System.currentTimeMillis() - lastToast;
|
val diff = System.currentTimeMillis() - lastToast;
|
||||||
lastToast = System.currentTimeMillis();
|
lastToast = System.currentTimeMillis();
|
||||||
UIDialogs.toast(
|
UIDialogs.toast(
|
||||||
StateApp.instance.activity!!,
|
SettingsActivity.getActivity()!!,
|
||||||
"Page: ${page}, Total: ${total}, Speed: ${diff}ms"
|
"Page: ${page}, Total: ${total}, Speed: ${diff}ms"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -130,7 +130,7 @@ class SettingsDev : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
UIDialogs.toast(
|
UIDialogs.toast(
|
||||||
StateApp.instance.activity!!,
|
SettingsActivity.getActivity()!!,
|
||||||
"FINISHED Page: ${page}, Total: ${total}"
|
"FINISHED Page: ${page}, Total: ${total}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -152,10 +152,10 @@ class SettingsDev : FragmentedStorageFileJson() {
|
|||||||
fun historyCache100() {
|
fun historyCache100() {
|
||||||
Logger.i("SettingsDev", "Started caching 100 history items (from home)");
|
Logger.i("SettingsDev", "Started caching 100 history items (from home)");
|
||||||
UIDialogs.toast(
|
UIDialogs.toast(
|
||||||
StateApp.instance.activity!!,
|
SettingsActivity.getActivity()!!,
|
||||||
"Started caching 100 history items (from home)"
|
"Started caching 100 history items (from home)"
|
||||||
);
|
);
|
||||||
val button = DeveloperFragment.currentView?.getField("history_cache_button");
|
val button = DeveloperActivity.getActivity()?.getField("history_cache_button");
|
||||||
if(button is ButtonField)
|
if(button is ButtonField)
|
||||||
button.setButtonEnabled(false);
|
button.setButtonEnabled(false);
|
||||||
StateApp.instance.scope.launch(Dispatchers.IO) {
|
StateApp.instance.scope.launch(Dispatchers.IO) {
|
||||||
@@ -186,7 +186,7 @@ class SettingsDev : FragmentedStorageFileJson() {
|
|||||||
val diff = System.currentTimeMillis() - lastToast;
|
val diff = System.currentTimeMillis() - lastToast;
|
||||||
lastToast = System.currentTimeMillis();
|
lastToast = System.currentTimeMillis();
|
||||||
UIDialogs.toast(
|
UIDialogs.toast(
|
||||||
StateApp.instance.activity!!,
|
SettingsActivity.getActivity()!!,
|
||||||
"Page: ${page}, Total: ${total}, Speed: ${diff}ms"
|
"Page: ${page}, Total: ${total}, Speed: ${diff}ms"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -195,7 +195,7 @@ class SettingsDev : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
UIDialogs.toast(
|
UIDialogs.toast(
|
||||||
StateApp.instance.activity!!,
|
SettingsActivity.getActivity()!!,
|
||||||
"FINISHED Page: ${page}, Total: ${total}"
|
"FINISHED Page: ${page}, Total: ${total}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -235,9 +235,9 @@ class SettingsDev : FragmentedStorageFileJson() {
|
|||||||
@FormField(R.string.test_background_worker, FieldForm.BUTTON,
|
@FormField(R.string.test_background_worker, FieldForm.BUTTON,
|
||||||
R.string.test_background_worker_description, 4)
|
R.string.test_background_worker_description, 4)
|
||||||
fun triggerBackgroundUpdate() {
|
fun triggerBackgroundUpdate() {
|
||||||
val act = StateApp.instance.activity!!;
|
val act = SettingsActivity.getActivity()!!;
|
||||||
try {
|
try {
|
||||||
UIDialogs.toast(StateApp.instance.activity!!, "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>()
|
||||||
@@ -251,9 +251,9 @@ class SettingsDev : FragmentedStorageFileJson() {
|
|||||||
@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)
|
||||||
fun clearChannelContentCache() {
|
fun clearChannelContentCache() {
|
||||||
UIDialogs.toast(StateApp.instance.activity!!, "Clearing cache");
|
UIDialogs.toast(SettingsActivity.getActivity()!!, "Clearing cache");
|
||||||
StateCache.instance.clearToday();
|
StateCache.instance.clearToday();
|
||||||
UIDialogs.toast(StateApp.instance.activity!!, "Cleared");
|
UIDialogs.toast(SettingsActivity.getActivity()!!, "Cleared");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -113,8 +113,8 @@ class UIDialogs {
|
|||||||
currentDialog.code,
|
currentDialog.code,
|
||||||
currentDialog.defaultCloseAction,
|
currentDialog.defaultCloseAction,
|
||||||
*currentDialog.actions.map {
|
*currentDialog.actions.map {
|
||||||
return@map Action.withInput(it.text, { str ->
|
return@map Action(it.text, {
|
||||||
it.invokeAction(str);
|
it.action();
|
||||||
multiShowDialog(context, dialogDescriptor.drop(1), finally);
|
multiShowDialog(context, dialogDescriptor.drop(1), finally);
|
||||||
}, it.style);
|
}, it.style);
|
||||||
}.toTypedArray());
|
}.toTypedArray());
|
||||||
@@ -203,9 +203,7 @@ class UIDialogs {
|
|||||||
fun showDialog(context: Context, icon: Int, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action): AlertDialog {
|
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);
|
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
|
fun showDialog(context: Context, icon: Int, animated: Boolean, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action): AlertDialog {
|
||||||
= showDialog(context, icon, animated, text, textDetails, code, null, null, defaultCloseAction, *actions);
|
|
||||||
fun showDialog(context: Context, icon: Int, animated: Boolean, text: String, textDetails: String? = null, code: String? = null, input: String?, placeholder: String?, 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);
|
||||||
@@ -228,16 +226,6 @@ class UIDialogs {
|
|||||||
this.text = textDetails;
|
this.text = textDetails;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
var inputView = view.findViewById<TextView>(R.id.dialog_text_input);
|
|
||||||
inputView.apply {
|
|
||||||
if (input == null && placeholder == null) this.visibility = View.GONE;
|
|
||||||
else {
|
|
||||||
this.text = input ?: "";
|
|
||||||
this.hint = placeholder ?: "";
|
|
||||||
this.visibility = View.VISIBLE;
|
|
||||||
this.textAlignment = View.TEXT_ALIGNMENT_VIEW_START
|
|
||||||
}
|
|
||||||
};
|
|
||||||
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 {
|
||||||
@@ -262,7 +250,7 @@ class UIDialogs {
|
|||||||
buttonView.textSize = 14f;
|
buttonView.textSize = 14f;
|
||||||
buttonView.typeface = resources.getFont(R.font.inter_regular);
|
buttonView.typeface = resources.getFont(R.font.inter_regular);
|
||||||
buttonView.text = act.text;
|
buttonView.text = act.text;
|
||||||
buttonView.setOnClickListener { act.invokeAction(DialogResult(inputView?.text?.toString())); dialog.dismiss(); };
|
buttonView.setOnClickListener { act.action(); dialog.dismiss(); };
|
||||||
when(act.style) {
|
when(act.style) {
|
||||||
ActionStyle.PRIMARY -> buttonView.setBackgroundResource(R.drawable.background_button_primary);
|
ActionStyle.PRIMARY -> buttonView.setBackgroundResource(R.drawable.background_button_primary);
|
||||||
ActionStyle.ACCENT -> buttonView.setBackgroundResource(R.drawable.background_button_accent);
|
ActionStyle.ACCENT -> buttonView.setBackgroundResource(R.drawable.background_button_accent);
|
||||||
@@ -287,7 +275,7 @@ class UIDialogs {
|
|||||||
};
|
};
|
||||||
dialog.setOnCancelListener {
|
dialog.setOnCancelListener {
|
||||||
if(defaultCloseAction >= 0 && defaultCloseAction < actions.size)
|
if(defaultCloseAction >= 0 && defaultCloseAction < actions.size)
|
||||||
actions[defaultCloseAction].invokeAction(DialogResult(inputView?.text?.toString()));
|
actions[defaultCloseAction].action();
|
||||||
}
|
}
|
||||||
dialog.setOnDismissListener {
|
dialog.setOnDismissListener {
|
||||||
registerDialogClosed(dialog);
|
registerDialogClosed(dialog);
|
||||||
@@ -331,11 +319,7 @@ class UIDialogs {
|
|||||||
closeAction?.invoke()
|
closeAction?.invoke()
|
||||||
}, UIDialogs.ActionStyle.NONE),
|
}, UIDialogs.ActionStyle.NONE),
|
||||||
UIDialogs.Action(context.getString(R.string.retry), {
|
UIDialogs.Action(context.getString(R.string.retry), {
|
||||||
try {
|
retryAction?.invoke();
|
||||||
retryAction?.invoke();
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.e(TAG, "Unhandled exception retrying", e)
|
|
||||||
}
|
|
||||||
}, UIDialogs.ActionStyle.PRIMARY)
|
}, UIDialogs.ActionStyle.PRIMARY)
|
||||||
);
|
);
|
||||||
else
|
else
|
||||||
@@ -349,11 +333,7 @@ class UIDialogs {
|
|||||||
closeAction?.invoke()
|
closeAction?.invoke()
|
||||||
}, UIDialogs.ActionStyle.NONE),
|
}, UIDialogs.ActionStyle.NONE),
|
||||||
UIDialogs.Action(context.getString(R.string.retry), {
|
UIDialogs.Action(context.getString(R.string.retry), {
|
||||||
try {
|
retryAction?.invoke();
|
||||||
retryAction?.invoke();
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.e(TAG, "Unhandled exception retrying", e)
|
|
||||||
}
|
|
||||||
}, UIDialogs.ActionStyle.PRIMARY)
|
}, UIDialogs.ActionStyle.PRIMARY)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -370,19 +350,17 @@ class UIDialogs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun showConfirmationDialog(context: Context, text: String, action: () -> Unit, cancelAction: (() -> Unit)? = null, dismissAction: (() -> Unit)? = null): AlertDialog {
|
fun showConfirmationDialog(context: Context, text: String, action: () -> Unit, cancelAction: (() -> Unit)? = null) {
|
||||||
val confirmButtonAction = Action(context.getString(R.string.confirm), action, ActionStyle.PRIMARY)
|
val confirmButtonAction = Action(context.getString(R.string.confirm), action, ActionStyle.PRIMARY)
|
||||||
val cancelButtonAction = Action(context.getString(R.string.cancel), cancelAction ?: {}, ActionStyle.ACCENT)
|
val cancelButtonAction = Action(context.getString(R.string.cancel), cancelAction ?: {}, ActionStyle.ACCENT)
|
||||||
return showDialog(context, R.drawable.ic_error, text, null, null, 0, cancelButtonAction, confirmButtonAction).apply {
|
showDialog(context, R.drawable.ic_error, text, null, null, 0, cancelButtonAction, confirmButtonAction)
|
||||||
setOnDismissListener { dismissAction?.invoke() }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showConfirmationDialog(context: Context, text: String, action: () -> Unit, cancelAction: (() -> Unit)? = null, dismissAction: (() -> Unit)? = null, doNotAskAgainAction: (() -> Unit)? = null): AlertDialog {
|
fun showConfirmationDialog(context: Context, text: String, action: () -> Unit, cancelAction: (() -> Unit)? = null, doNotAskAgainAction: (() -> Unit)? = null) {
|
||||||
val confirmButtonAction = Action(context.getString(R.string.confirm), action, ActionStyle.PRIMARY)
|
val confirmButtonAction = Action(context.getString(R.string.confirm), action, ActionStyle.PRIMARY)
|
||||||
val cancelButtonAction = Action(context.getString(R.string.cancel), cancelAction ?: {}, ActionStyle.ACCENT)
|
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)
|
val doNotAskAgain = Action(context.getString(R.string.do_not_ask_again), doNotAskAgainAction ?: {}, ActionStyle.NONE)
|
||||||
return showDialog(context, R.drawable.ic_error, text, null, null, 0, doNotAskAgain, cancelButtonAction, confirmButtonAction)
|
showDialog(context, R.drawable.ic_error, text, null, null, 0, doNotAskAgain, cancelButtonAction, confirmButtonAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showUpdateAvailableDialog(context: Context, lastVersion: Int, hideExceptionButtons: Boolean = false) {
|
fun showUpdateAvailableDialog(context: Context, lastVersion: Int, hideExceptionButtons: Boolean = false) {
|
||||||
@@ -405,6 +383,13 @@ class UIDialogs {
|
|||||||
dialog.setMaxVersion(lastVersion);
|
dialog.setMaxVersion(lastVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun showInstallDownloadedUpdateDialog(context: Context, apkFile: File) {
|
||||||
|
val dialog = AutoUpdateDialog(context);
|
||||||
|
registerDialogOpened(dialog);
|
||||||
|
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||||
|
dialog.showPredownloaded(apkFile);
|
||||||
|
}
|
||||||
|
|
||||||
fun showMigrateDialog(context: Context, store: ManagedStore<*>, onConcluded: ()->Unit) {
|
fun showMigrateDialog(context: Context, store: ManagedStore<*>, onConcluded: ()->Unit) {
|
||||||
if(!store.hasMissingReconstructions())
|
if(!store.hasMissingReconstructions())
|
||||||
onConcluded();
|
onConcluded();
|
||||||
@@ -431,7 +416,7 @@ class UIDialogs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun showCastingDialog(context: Context, ownerActivity: Activity? = null) {
|
fun showCastingDialog(context: Context) {
|
||||||
val d = StateCasting.instance.activeDevice;
|
val d = StateCasting.instance.activeDevice;
|
||||||
if (d != null) {
|
if (d != null) {
|
||||||
val dialog = ConnectedCastingDialog(context);
|
val dialog = ConnectedCastingDialog(context);
|
||||||
@@ -439,7 +424,6 @@ class UIDialogs {
|
|||||||
dialog.setOwnerActivity(context)
|
dialog.setOwnerActivity(context)
|
||||||
}
|
}
|
||||||
registerDialogOpened(dialog);
|
registerDialogOpened(dialog);
|
||||||
ownerActivity?.let { dialog.setOwnerActivity(it) }
|
|
||||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||||
dialog.show();
|
dialog.show();
|
||||||
} else {
|
} else {
|
||||||
@@ -452,24 +436,21 @@ class UIDialogs {
|
|||||||
if (c is Activity) {
|
if (c is Activity) {
|
||||||
dialog.setOwnerActivity(c);
|
dialog.setOwnerActivity(c);
|
||||||
}
|
}
|
||||||
ownerActivity?.let { dialog.setOwnerActivity(it) }
|
|
||||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||||
dialog.show();
|
dialog.show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showCastingTutorialDialog(context: Context, ownerActivity: Activity? = null) {
|
fun showCastingTutorialDialog(context: Context) {
|
||||||
val dialog = CastingHelpDialog(context);
|
val dialog = CastingHelpDialog(context);
|
||||||
registerDialogOpened(dialog);
|
registerDialogOpened(dialog);
|
||||||
ownerActivity?.let { dialog.setOwnerActivity(it) }
|
|
||||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||||
dialog.show();
|
dialog.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showCastingAddDialog(context: Context, ownerActivity: Activity? = null) {
|
fun showCastingAddDialog(context: Context) {
|
||||||
val dialog = CastingAddDialog(context);
|
val dialog = CastingAddDialog(context);
|
||||||
registerDialogOpened(dialog);
|
registerDialogOpened(dialog);
|
||||||
ownerActivity?.let { dialog.setOwnerActivity(it) }
|
|
||||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||||
dialog.show();
|
dialog.show();
|
||||||
}
|
}
|
||||||
@@ -542,36 +523,17 @@ class UIDialogs {
|
|||||||
}
|
}
|
||||||
class Action {
|
class Action {
|
||||||
val text: String;
|
val text: String;
|
||||||
val action: ((DialogResult?)->Unit);
|
val action: ()->Unit;
|
||||||
val style: ActionStyle;
|
val style: ActionStyle;
|
||||||
var center: Boolean;
|
var center: Boolean;
|
||||||
|
|
||||||
constructor(text: String, action: ()->Unit, style: ActionStyle = ActionStyle.NONE, center: Boolean = false) {
|
constructor(text: String, action: ()->Unit, style: ActionStyle = ActionStyle.NONE, center: Boolean = false) {
|
||||||
this.text = text;
|
|
||||||
this.action = { action() };
|
|
||||||
this.style = style;
|
|
||||||
this.center = center;
|
|
||||||
}
|
|
||||||
protected constructor(text: String, action: (DialogResult?)->Unit, style: ActionStyle = ActionStyle.NONE, center: Boolean = false) {
|
|
||||||
this.text = text;
|
this.text = text;
|
||||||
this.action = action;
|
this.action = action;
|
||||||
this.style = style;
|
this.style = style;
|
||||||
this.center = center;
|
this.center = center;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun invokeAction(input: DialogResult? = null) {
|
|
||||||
this.action(input);
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun withInput(text: String, action: (DialogResult?)->Unit, style: ActionStyle = ActionStyle.NONE, center: Boolean = false): Action {
|
|
||||||
return Action(text, action, style, center);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
class DialogResult(
|
|
||||||
val text: String?
|
|
||||||
);
|
|
||||||
enum class ActionStyle {
|
enum class ActionStyle {
|
||||||
NONE,
|
NONE,
|
||||||
PRIMARY,
|
PRIMARY,
|
||||||
|
|||||||
@@ -4,16 +4,11 @@ import android.app.NotificationManager
|
|||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.annotation.OptIn
|
|
||||||
import androidx.media3.common.util.UnstableApi
|
|
||||||
import androidx.media3.exoplayer.hls.playlist.DefaultHlsPlaylistParserFactory
|
|
||||||
import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist
|
|
||||||
import androidx.media3.exoplayer.hls.playlist.HlsMultivariantPlaylist
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
|
import com.futo.platformplayer.activities.SettingsActivity
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
@@ -42,9 +37,6 @@ import com.futo.platformplayer.models.Playlist
|
|||||||
import com.futo.platformplayer.models.Subscription
|
import com.futo.platformplayer.models.Subscription
|
||||||
import com.futo.platformplayer.models.SubscriptionGroup
|
import com.futo.platformplayer.models.SubscriptionGroup
|
||||||
import com.futo.platformplayer.parsers.HLS
|
import com.futo.platformplayer.parsers.HLS
|
||||||
import com.futo.platformplayer.parsers.HLS.MediaRendition
|
|
||||||
import com.futo.platformplayer.parsers.HLS.StreamInfo
|
|
||||||
import com.futo.platformplayer.parsers.HLS.VariantPlaylistReference
|
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StateDownloads
|
import com.futo.platformplayer.states.StateDownloads
|
||||||
import com.futo.platformplayer.states.StateHistory
|
import com.futo.platformplayer.states.StateHistory
|
||||||
@@ -71,9 +63,6 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.ByteArrayInputStream
|
|
||||||
import androidx.core.net.toUri
|
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.SettingsFragment
|
|
||||||
|
|
||||||
class UISlideOverlays {
|
class UISlideOverlays {
|
||||||
companion object {
|
companion object {
|
||||||
@@ -129,163 +118,115 @@ class UISlideOverlays {
|
|||||||
val originalVideo = subscription.doFetchVideos;
|
val originalVideo = subscription.doFetchVideos;
|
||||||
val originalPosts = subscription.doFetchPosts;
|
val originalPosts = subscription.doFetchPosts;
|
||||||
|
|
||||||
val menu = SlideUpMenuOverlay(
|
val menu = SlideUpMenuOverlay(container.context, container, "Subscription Settings", null, true, listOf());
|
||||||
container.context,
|
|
||||||
container,
|
|
||||||
"Subscription Settings",
|
|
||||||
null,
|
|
||||||
true,
|
|
||||||
listOf()
|
|
||||||
);
|
|
||||||
|
|
||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO){
|
||||||
try {
|
val plugin = StatePlatform.instance.getChannelClient(subscription.channel.url);
|
||||||
val plugin = StatePlatform.instance.getChannelClient(subscription.channel.url);
|
val capabilities = plugin.getChannelCapabilities();
|
||||||
val capabilities = plugin.getChannelCapabilities();
|
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
items.addAll(
|
items.addAll(listOf(
|
||||||
listOf(
|
SlideUpMenuItem(
|
||||||
SlideUpMenuItem(
|
container.context,
|
||||||
container.context,
|
R.drawable.ic_notifications,
|
||||||
R.drawable.ic_notifications,
|
"Notifications",
|
||||||
"Notifications",
|
"",
|
||||||
"",
|
tag = "notifications",
|
||||||
tag = "notifications",
|
call = {
|
||||||
call = {
|
subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications;
|
||||||
subscription.doNotifications =
|
},
|
||||||
menu?.selectOption(null, "notifications", true, true)
|
invokeParent = false
|
||||||
?: subscription.doNotifications;
|
),
|
||||||
},
|
if(StateSubscriptionGroups.instance.getSubscriptionGroups().isNotEmpty())
|
||||||
invokeParent = false
|
SlideUpMenuGroup(container.context, "Subscription Groups",
|
||||||
),
|
"You can select which groups this subscription is part of.",
|
||||||
if (StateSubscriptionGroups.instance.getSubscriptionGroups()
|
-1, listOf()) else null,
|
||||||
.isNotEmpty()
|
if(StateSubscriptionGroups.instance.getSubscriptionGroups().isNotEmpty())
|
||||||
)
|
SlideUpMenuRecycler(container.context, "as") {
|
||||||
SlideUpMenuGroup(
|
val groups = ArrayList<SubscriptionGroup>(StateSubscriptionGroups.instance.getSubscriptionGroups()
|
||||||
container.context, "Subscription Groups",
|
.map { SubscriptionGroup.Selectable(it, it.urls.contains(subscription.channel.url)) }
|
||||||
"You can select which groups this subscription is part of.",
|
.sortedBy { !it.selected });
|
||||||
-1, listOf()
|
var adapter: AnyAdapterView<SubscriptionGroup, SubscriptionGroupBarViewHolder>? = null;
|
||||||
) else null,
|
adapter = it.asAny(groups, RecyclerView.HORIZONTAL) {
|
||||||
if (StateSubscriptionGroups.instance.getSubscriptionGroups()
|
it.onClick.subscribe {
|
||||||
.isNotEmpty()
|
if(it is SubscriptionGroup.Selectable) {
|
||||||
)
|
val actualGroup = StateSubscriptionGroups.instance.getSubscriptionGroup(it.id)
|
||||||
SlideUpMenuRecycler(container.context, "as") {
|
?: return@subscribe;
|
||||||
val groups =
|
groups.clear();
|
||||||
ArrayList<SubscriptionGroup>(
|
if(it.selected)
|
||||||
StateSubscriptionGroups.instance.getSubscriptionGroups()
|
actualGroup.urls.remove(subscription.channel.url);
|
||||||
.map {
|
else
|
||||||
SubscriptionGroup.Selectable(
|
actualGroup.urls.add(subscription.channel.url);
|
||||||
it,
|
|
||||||
it.urls.contains(subscription.channel.url)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.sortedBy { !it.selected });
|
|
||||||
var adapter: AnyAdapterView<SubscriptionGroup, SubscriptionGroupBarViewHolder>? =
|
|
||||||
null;
|
|
||||||
adapter = it.asAny(groups, RecyclerView.HORIZONTAL) {
|
|
||||||
it.onClick.subscribe {
|
|
||||||
if (it is SubscriptionGroup.Selectable) {
|
|
||||||
val actualGroup =
|
|
||||||
StateSubscriptionGroups.instance.getSubscriptionGroup(
|
|
||||||
it.id
|
|
||||||
)
|
|
||||||
?: return@subscribe;
|
|
||||||
groups.clear();
|
|
||||||
if (it.selected)
|
|
||||||
actualGroup.urls.remove(subscription.channel.url);
|
|
||||||
else
|
|
||||||
actualGroup.urls.add(subscription.channel.url);
|
|
||||||
|
|
||||||
StateSubscriptionGroups.instance.updateSubscriptionGroup(
|
StateSubscriptionGroups.instance.updateSubscriptionGroup(actualGroup);
|
||||||
actualGroup
|
groups.addAll(StateSubscriptionGroups.instance.getSubscriptionGroups()
|
||||||
);
|
.map { SubscriptionGroup.Selectable(it, it.urls.contains(subscription.channel.url)) }
|
||||||
groups.addAll(
|
.sortedBy { !it.selected });
|
||||||
StateSubscriptionGroups.instance.getSubscriptionGroups()
|
adapter?.notifyContentChanged();
|
||||||
.map {
|
}
|
||||||
SubscriptionGroup.Selectable(
|
}
|
||||||
it,
|
};
|
||||||
it.urls.contains(subscription.channel.url)
|
return@SlideUpMenuRecycler adapter;
|
||||||
)
|
} else null,
|
||||||
}
|
SlideUpMenuGroup(container.context, "Fetch Settings",
|
||||||
.sortedBy { !it.selected });
|
"Depending on the platform you might not need to enable a type for it to be available.",
|
||||||
adapter?.notifyContentChanged();
|
-1, listOf()),
|
||||||
}
|
if(capabilities.hasType(ResultCapabilities.TYPE_LIVE)) SlideUpMenuItem(
|
||||||
}
|
container.context,
|
||||||
};
|
R.drawable.ic_live_tv,
|
||||||
return@SlideUpMenuRecycler adapter;
|
"Livestreams",
|
||||||
} else null,
|
"Check for livestreams",
|
||||||
SlideUpMenuGroup(
|
tag = "fetchLive",
|
||||||
container.context, "Fetch Settings",
|
call = {
|
||||||
"Depending on the platform you might not need to enable a type for it to be available.",
|
subscription.doFetchLive = menu?.selectOption(null, "fetchLive", true, true) ?: subscription.doFetchLive;
|
||||||
-1, listOf()
|
},
|
||||||
),
|
invokeParent = false
|
||||||
if (capabilities.hasType(ResultCapabilities.TYPE_LIVE)) SlideUpMenuItem(
|
) else null,
|
||||||
container.context,
|
if(capabilities.hasType(ResultCapabilities.TYPE_STREAMS)) SlideUpMenuItem(
|
||||||
R.drawable.ic_live_tv,
|
container.context,
|
||||||
"Livestreams",
|
R.drawable.ic_play,
|
||||||
"Check for livestreams",
|
"Streams",
|
||||||
tag = "fetchLive",
|
"Check for streams",
|
||||||
call = {
|
tag = "fetchStreams",
|
||||||
subscription.doFetchLive =
|
call = {
|
||||||
menu?.selectOption(null, "fetchLive", true, true)
|
subscription.doFetchStreams = menu?.selectOption(null, "fetchStreams", true, true) ?: subscription.doFetchStreams;
|
||||||
?: subscription.doFetchLive;
|
},
|
||||||
},
|
invokeParent = false
|
||||||
invokeParent = false
|
) else null,
|
||||||
) else null,
|
if(capabilities.hasType(ResultCapabilities.TYPE_VIDEOS))
|
||||||
if (capabilities.hasType(ResultCapabilities.TYPE_STREAMS)) SlideUpMenuItem(
|
SlideUpMenuItem(
|
||||||
container.context,
|
container.context,
|
||||||
R.drawable.ic_play,
|
R.drawable.ic_play,
|
||||||
"Streams",
|
"Videos",
|
||||||
"Check for streams",
|
"Check for videos",
|
||||||
tag = "fetchStreams",
|
tag = "fetchVideos",
|
||||||
call = {
|
call = {
|
||||||
subscription.doFetchStreams =
|
subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos;
|
||||||
menu?.selectOption(null, "fetchStreams", true, true)
|
},
|
||||||
?: subscription.doFetchStreams;
|
invokeParent = false
|
||||||
},
|
) else if(capabilities.hasType(ResultCapabilities.TYPE_MIXED) || capabilities.types.isEmpty())
|
||||||
invokeParent = false
|
SlideUpMenuItem(
|
||||||
) else null,
|
container.context,
|
||||||
if (capabilities.hasType(ResultCapabilities.TYPE_VIDEOS))
|
R.drawable.ic_play,
|
||||||
SlideUpMenuItem(
|
"Content",
|
||||||
container.context,
|
"Check for content",
|
||||||
R.drawable.ic_play,
|
tag = "fetchVideos",
|
||||||
"Videos",
|
call = {
|
||||||
"Check for videos",
|
subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos;
|
||||||
tag = "fetchVideos",
|
},
|
||||||
call = {
|
invokeParent = false
|
||||||
subscription.doFetchVideos =
|
) else null,
|
||||||
menu?.selectOption(null, "fetchVideos", true, true)
|
if(capabilities.hasType(ResultCapabilities.TYPE_POSTS)) SlideUpMenuItem(
|
||||||
?: subscription.doFetchVideos;
|
container.context,
|
||||||
},
|
R.drawable.ic_chat,
|
||||||
invokeParent = false
|
"Posts",
|
||||||
) else if (capabilities.hasType(ResultCapabilities.TYPE_MIXED) || capabilities.types.isEmpty())
|
"Check for posts",
|
||||||
SlideUpMenuItem(
|
tag = "fetchPosts",
|
||||||
container.context,
|
call = {
|
||||||
R.drawable.ic_play,
|
subscription.doFetchPosts = menu?.selectOption(null, "fetchPosts", true, true) ?: subscription.doFetchPosts;
|
||||||
"Content",
|
},
|
||||||
"Check for content",
|
invokeParent = false
|
||||||
tag = "fetchVideos",
|
) else null/*,,
|
||||||
call = {
|
|
||||||
subscription.doFetchVideos =
|
|
||||||
menu?.selectOption(null, "fetchVideos", true, true)
|
|
||||||
?: subscription.doFetchVideos;
|
|
||||||
},
|
|
||||||
invokeParent = false
|
|
||||||
) else null,
|
|
||||||
if (capabilities.hasType(ResultCapabilities.TYPE_POSTS)) SlideUpMenuItem(
|
|
||||||
container.context,
|
|
||||||
R.drawable.ic_chat,
|
|
||||||
"Posts",
|
|
||||||
"Check for posts",
|
|
||||||
tag = "fetchPosts",
|
|
||||||
call = {
|
|
||||||
subscription.doFetchPosts =
|
|
||||||
menu?.selectOption(null, "fetchPosts", true, true)
|
|
||||||
?: subscription.doFetchPosts;
|
|
||||||
},
|
|
||||||
invokeParent = false
|
|
||||||
) else null/*,,
|
|
||||||
|
|
||||||
SlideUpMenuGroup(container.context, "Actions",
|
SlideUpMenuGroup(container.context, "Actions",
|
||||||
"Various things you can do with this subscription",
|
"Various things you can do with this subscription",
|
||||||
@@ -293,76 +234,61 @@ class UISlideOverlays {
|
|||||||
SlideUpMenuItem(container.context, R.drawable.ic_list, "Add to Group", "", "btnAddToGroup", {
|
SlideUpMenuItem(container.context, R.drawable.ic_list, "Add to Group", "", "btnAddToGroup", {
|
||||||
showCreateSubscriptionGroup(container, subscription.channel);
|
showCreateSubscriptionGroup(container, subscription.channel);
|
||||||
}, false)*/
|
}, false)*/
|
||||||
).filterNotNull()
|
).filterNotNull());
|
||||||
);
|
|
||||||
|
|
||||||
menu.setItems(items);
|
menu.setItems(items);
|
||||||
|
|
||||||
if (subscription.doNotifications)
|
if(subscription.doNotifications)
|
||||||
menu.selectOption(null, "notifications", true, true);
|
menu.selectOption(null, "notifications", true, true);
|
||||||
if (subscription.doFetchLive)
|
if(subscription.doFetchLive)
|
||||||
menu.selectOption(null, "fetchLive", true, true);
|
menu.selectOption(null, "fetchLive", true, true);
|
||||||
if (subscription.doFetchStreams)
|
if(subscription.doFetchStreams)
|
||||||
menu.selectOption(null, "fetchStreams", true, true);
|
menu.selectOption(null, "fetchStreams", true, true);
|
||||||
if (subscription.doFetchVideos)
|
if(subscription.doFetchVideos)
|
||||||
menu.selectOption(null, "fetchVideos", true, true);
|
menu.selectOption(null, "fetchVideos", true, true);
|
||||||
if (subscription.doFetchPosts)
|
if(subscription.doFetchPosts)
|
||||||
menu.selectOption(null, "fetchPosts", true, true);
|
menu.selectOption(null, "fetchPosts", true, true);
|
||||||
|
|
||||||
menu.onOK.subscribe {
|
menu.onOK.subscribe {
|
||||||
subscription.save();
|
subscription.save();
|
||||||
menu.hide(true);
|
menu.hide(true);
|
||||||
|
|
||||||
if (subscription.doNotifications && !originalNotif) {
|
if(subscription.doNotifications && !originalNotif) {
|
||||||
val mainContext = StateApp.instance.contextOrNull;
|
val mainContext = StateApp.instance.contextOrNull;
|
||||||
if (Settings.instance.subscriptions.subscriptionsBackgroundUpdateInterval == 0) {
|
if(Settings.instance.subscriptions.subscriptionsBackgroundUpdateInterval == 0) {
|
||||||
UIDialogs.toast(
|
UIDialogs.toast(container.context, "Enable 'Background Update' in settings for notifications to work");
|
||||||
container.context,
|
|
||||||
"Enable 'Background Update' in settings for notifications to work"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (mainContext is MainActivity) {
|
if(mainContext is MainActivity) {
|
||||||
UIDialogs.showDialog(
|
UIDialogs.showDialog(mainContext, R.drawable.ic_settings, "Background Updating Required",
|
||||||
mainContext,
|
"You need to set a Background Updating interval for notifications", null, 0,
|
||||||
R.drawable.ic_settings,
|
UIDialogs.Action("Cancel", {}),
|
||||||
"Background Updating Required",
|
UIDialogs.Action("Configure", {
|
||||||
"You need to set a Background Updating interval for notifications",
|
val intent = Intent(mainContext, SettingsActivity::class.java);
|
||||||
null,
|
intent.putExtra("query", mainContext.getString(R.string.background_update));
|
||||||
0,
|
mainContext.startActivity(intent);
|
||||||
UIDialogs.Action("Cancel", {}),
|
}, UIDialogs.ActionStyle.PRIMARY));
|
||||||
UIDialogs.Action("Configure", {
|
}
|
||||||
StateApp.instance.activity?.let {
|
return@subscribe;
|
||||||
it.navigate(it.getFragment<SettingsFragment>(), mainContext.getString(R.string.background_update))
|
}
|
||||||
}
|
else if(!(mainContext?.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).areNotificationsEnabled()) {
|
||||||
}, UIDialogs.ActionStyle.PRIMARY)
|
UIDialogs.toast(container.context, "Android notifications are disabled");
|
||||||
);
|
if(mainContext is MainActivity) {
|
||||||
}
|
mainContext.requestNotificationPermissions("Notifications are required for subscription updating and notifications to work");
|
||||||
return@subscribe;
|
|
||||||
} else if (!(mainContext?.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).areNotificationsEnabled()) {
|
|
||||||
UIDialogs.toast(
|
|
||||||
container.context,
|
|
||||||
"Android notifications are disabled"
|
|
||||||
);
|
|
||||||
if (mainContext is MainActivity) {
|
|
||||||
mainContext.requestNotificationPermissions("Notifications are required for subscription updating and notifications to work");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
menu.onCancel.subscribe {
|
};
|
||||||
subscription.doNotifications = originalNotif;
|
menu.onCancel.subscribe {
|
||||||
subscription.doFetchLive = originalLive;
|
subscription.doNotifications = originalNotif;
|
||||||
subscription.doFetchStreams = originalStream;
|
subscription.doFetchLive = originalLive;
|
||||||
subscription.doFetchVideos = originalVideo;
|
subscription.doFetchStreams = originalStream;
|
||||||
subscription.doFetchPosts = originalPosts;
|
subscription.doFetchVideos = originalVideo;
|
||||||
};
|
subscription.doFetchPosts = originalPosts;
|
||||||
|
};
|
||||||
|
|
||||||
menu.setOk("Save");
|
menu.setOk("Save");
|
||||||
|
|
||||||
menu.show();
|
menu.show();
|
||||||
}
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.e(TAG, "Failed to show subscription overlay.", e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,7 +299,6 @@ class UISlideOverlays {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(UnstableApi::class)
|
|
||||||
fun showHlsPicker(video: IPlatformVideoDetails, source: Any, sourceUrl: String, container: ViewGroup): SlideUpMenuOverlay {
|
fun showHlsPicker(video: IPlatformVideoDetails, source: Any, sourceUrl: String, container: ViewGroup): SlideUpMenuOverlay {
|
||||||
val items = arrayListOf<View>(LoaderView(container.context))
|
val items = arrayListOf<View>(LoaderView(container.context))
|
||||||
val slideUpMenuOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.download_video), null, true, items)
|
val slideUpMenuOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.download_video), null, true, items)
|
||||||
@@ -385,8 +310,6 @@ class UISlideOverlays {
|
|||||||
val masterPlaylistContent = masterPlaylistResponse.body?.string()
|
val masterPlaylistContent = masterPlaylistResponse.body?.string()
|
||||||
?: throw Exception("Master playlist content is empty")
|
?: throw Exception("Master playlist content is empty")
|
||||||
|
|
||||||
val resolvedPlaylistUrl = masterPlaylistResponse.url
|
|
||||||
|
|
||||||
val videoButtons = arrayListOf<SlideUpMenuItem>()
|
val videoButtons = arrayListOf<SlideUpMenuItem>()
|
||||||
val audioButtons = arrayListOf<SlideUpMenuItem>()
|
val audioButtons = arrayListOf<SlideUpMenuItem>()
|
||||||
//TODO: Implement subtitles
|
//TODO: Implement subtitles
|
||||||
@@ -399,103 +322,55 @@ class UISlideOverlays {
|
|||||||
|
|
||||||
val masterPlaylist: HLS.MasterPlaylist
|
val masterPlaylist: HLS.MasterPlaylist
|
||||||
try {
|
try {
|
||||||
val inputStream = ByteArrayInputStream(masterPlaylistContent.toByteArray())
|
masterPlaylist = HLS.parseMasterPlaylist(masterPlaylistContent, sourceUrl)
|
||||||
val playlist = DefaultHlsPlaylistParserFactory().createPlaylistParser()
|
|
||||||
.parse(sourceUrl.toUri(), inputStream)
|
|
||||||
|
|
||||||
if (playlist is HlsMediaPlaylist) {
|
masterPlaylist.getAudioSources().forEach { it ->
|
||||||
if (source is IHLSManifestAudioSource) {
|
|
||||||
val variant = HLS.mediaRenditionToVariant(MediaRendition("AUDIO", playlist.baseUri, "Single Playlist", null, null, null, null, null))!!
|
|
||||||
|
|
||||||
val estSize = VideoHelper.estimateSourceSize(variant);
|
val estSize = VideoHelper.estimateSourceSize(it);
|
||||||
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
||||||
audioButtons.add(SlideUpMenuItem(
|
audioButtons.add(SlideUpMenuItem(
|
||||||
container.context,
|
container.context,
|
||||||
R.drawable.ic_music,
|
R.drawable.ic_music,
|
||||||
variant.name,
|
it.name,
|
||||||
listOf(variant.language, variant.codec).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "),
|
listOf(it.language, it.codec).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "),
|
||||||
(prefix + variant.codec).trim(),
|
(prefix + it.codec).trim(),
|
||||||
tag = variant,
|
tag = it,
|
||||||
call = {
|
call = {
|
||||||
selectedAudioVariant = variant
|
selectedAudioVariant = it
|
||||||
slideUpMenuOverlay.selectOption(audioButtons, variant)
|
slideUpMenuOverlay.selectOption(audioButtons, it)
|
||||||
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
|
||||||
},
|
|
||||||
invokeParent = false
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
val variant = HLS.variantReferenceToVariant(VariantPlaylistReference(playlist.baseUri, StreamInfo(null, null, null, null, null, null, null, null, null)))
|
|
||||||
|
|
||||||
val estSize = VideoHelper.estimateSourceSize(variant);
|
|
||||||
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
|
||||||
videoButtons.add(SlideUpMenuItem(
|
|
||||||
container.context,
|
|
||||||
R.drawable.ic_movie,
|
|
||||||
variant.name,
|
|
||||||
"${variant.width}x${variant.height}",
|
|
||||||
(prefix + variant.codec).trim(),
|
|
||||||
tag = variant,
|
|
||||||
call = {
|
|
||||||
selectedVideoVariant = variant
|
|
||||||
slideUpMenuOverlay.selectOption(videoButtons, variant)
|
|
||||||
if (audioButtons.isEmpty()){
|
|
||||||
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
invokeParent = false
|
|
||||||
))
|
|
||||||
}
|
|
||||||
} else if (playlist is HlsMultivariantPlaylist) {
|
|
||||||
masterPlaylist = HLS.parseMasterPlaylist(masterPlaylistContent, resolvedPlaylistUrl)
|
|
||||||
|
|
||||||
masterPlaylist.getAudioSources().forEach { it ->
|
|
||||||
|
|
||||||
val estSize = VideoHelper.estimateSourceSize(it);
|
|
||||||
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
|
||||||
audioButtons.add(SlideUpMenuItem(
|
|
||||||
container.context,
|
|
||||||
R.drawable.ic_music,
|
|
||||||
it.name,
|
|
||||||
listOf(it.language, it.codec).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "),
|
|
||||||
(prefix + it.codec).trim(),
|
|
||||||
tag = it,
|
|
||||||
call = {
|
|
||||||
selectedAudioVariant = it
|
|
||||||
slideUpMenuOverlay.selectOption(audioButtons, it)
|
|
||||||
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
|
||||||
},
|
|
||||||
invokeParent = false
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
/*masterPlaylist.getSubtitleSources().forEach { it ->
|
|
||||||
subtitleButtons.add(SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, listOf(it.format).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "), it, {
|
|
||||||
selectedSubtitleVariant = it
|
|
||||||
slideUpMenuOverlay.selectOption(subtitleButtons, it)
|
|
||||||
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
||||||
}, false))
|
},
|
||||||
}*/
|
invokeParent = false
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
masterPlaylist.getVideoSources().forEach {
|
/*masterPlaylist.getSubtitleSources().forEach { it ->
|
||||||
val estSize = VideoHelper.estimateSourceSize(it);
|
subtitleButtons.add(SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, listOf(it.format).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "), it, {
|
||||||
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
selectedSubtitleVariant = it
|
||||||
videoButtons.add(SlideUpMenuItem(
|
slideUpMenuOverlay.selectOption(subtitleButtons, it)
|
||||||
container.context,
|
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
||||||
R.drawable.ic_movie,
|
}, false))
|
||||||
it.name,
|
}*/
|
||||||
"${it.width}x${it.height}",
|
|
||||||
(prefix + it.codec).trim(),
|
masterPlaylist.getVideoSources().forEach {
|
||||||
tag = it,
|
val estSize = VideoHelper.estimateSourceSize(it);
|
||||||
call = {
|
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
||||||
selectedVideoVariant = it
|
videoButtons.add(SlideUpMenuItem(
|
||||||
slideUpMenuOverlay.selectOption(videoButtons, it)
|
container.context,
|
||||||
if (audioButtons.isEmpty()){
|
R.drawable.ic_movie,
|
||||||
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
it.name,
|
||||||
}
|
"${it.width}x${it.height}",
|
||||||
},
|
(prefix + it.codec).trim(),
|
||||||
invokeParent = false
|
tag = it,
|
||||||
))
|
call = {
|
||||||
}
|
selectedVideoVariant = it
|
||||||
|
slideUpMenuOverlay.selectOption(videoButtons, it)
|
||||||
|
if (audioButtons.isEmpty()){
|
||||||
|
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
invokeParent = false
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
val newItems = arrayListOf<View>()
|
val newItems = arrayListOf<View>()
|
||||||
@@ -523,11 +398,11 @@ class UISlideOverlays {
|
|||||||
if (masterPlaylistContent.lines().any { it.startsWith("#EXTINF:") }) {
|
if (masterPlaylistContent.lines().any { it.startsWith("#EXTINF:") }) {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
if (source is IHLSManifestSource) {
|
if (source is IHLSManifestSource) {
|
||||||
StateDownloads.instance.download(video, HLSVariantVideoUrlSource("variant", 0, 0, "application/vnd.apple.mpegurl", "", null, 0, false, resolvedPlaylistUrl), null, null)
|
StateDownloads.instance.download(video, HLSVariantVideoUrlSource("variant", 0, 0, "application/vnd.apple.mpegurl", "", null, 0, false, sourceUrl), null, null)
|
||||||
UIDialogs.toast(container.context, "Variant video HLS playlist download started")
|
UIDialogs.toast(container.context, "Variant video HLS playlist download started")
|
||||||
slideUpMenuOverlay.hide()
|
slideUpMenuOverlay.hide()
|
||||||
} else if (source is IHLSManifestAudioSource) {
|
} else if (source is IHLSManifestAudioSource) {
|
||||||
StateDownloads.instance.download(video, null, HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, false, resolvedPlaylistUrl), null)
|
StateDownloads.instance.download(video, null, HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, false, sourceUrl), null)
|
||||||
UIDialogs.toast(container.context, "Variant audio HLS playlist download started")
|
UIDialogs.toast(container.context, "Variant audio HLS playlist download started")
|
||||||
slideUpMenuOverlay.hide()
|
slideUpMenuOverlay.hide()
|
||||||
} else {
|
} else {
|
||||||
@@ -809,10 +684,6 @@ class UISlideOverlays {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(!Settings.instance.downloads.shouldDownload()) {
|
|
||||||
UIDialogs.appToast("Download will start when you're back on wifi.\n" +
|
|
||||||
"(You can change this in settings)", true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return menu.apply { show() };
|
return menu.apply { show() };
|
||||||
@@ -1109,30 +980,26 @@ class UISlideOverlays {
|
|||||||
+ actions).filterNotNull()
|
+ actions).filterNotNull()
|
||||||
));
|
));
|
||||||
items.add(
|
items.add(
|
||||||
SlideUpMenuGroup(
|
SlideUpMenuGroup(container.context, container.context.getString(R.string.add_to), "addto",
|
||||||
container.context, container.context.getString(R.string.add_to), "addto",
|
SlideUpMenuItem(container.context,
|
||||||
SlideUpMenuItem(
|
|
||||||
container.context,
|
|
||||||
R.drawable.ic_queue_add,
|
R.drawable.ic_queue_add,
|
||||||
container.context.getString(R.string.add_to_queue),
|
container.context.getString(R.string.add_to_queue),
|
||||||
"${queue.size} " + container.context.getString(R.string.videos),
|
"${queue.size} " + container.context.getString(R.string.videos),
|
||||||
tag = "queue",
|
tag = "queue",
|
||||||
call = { StatePlayer.instance.addToQueue(video); }),
|
call = { StatePlayer.instance.addToQueue(video); }),
|
||||||
SlideUpMenuItem(
|
SlideUpMenuItem(container.context,
|
||||||
container.context,
|
|
||||||
R.drawable.ic_watchlist_add,
|
R.drawable.ic_watchlist_add,
|
||||||
"${container.context.getString(R.string.add_to)} " + StatePlayer.TYPE_WATCHLATER + "",
|
"${container.context.getString(R.string.add_to)} " + StatePlayer.TYPE_WATCHLATER + "",
|
||||||
"${watchLater.size} " + container.context.getString(R.string.videos),
|
"${watchLater.size} " + container.context.getString(R.string.videos),
|
||||||
tag = "watch later",
|
tag = "watch later",
|
||||||
call = { StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true); }),
|
call = { StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true); }),
|
||||||
SlideUpMenuItem(
|
SlideUpMenuItem(container.context,
|
||||||
container.context,
|
|
||||||
R.drawable.ic_history,
|
R.drawable.ic_history,
|
||||||
container.context.getString(R.string.add_to_history),
|
container.context.getString(R.string.add_to_history),
|
||||||
"Mark as watched",
|
"Mark as watched",
|
||||||
tag = "history",
|
tag = "history",
|
||||||
call = { StateHistory.instance.markAsWatched(video); }),
|
call = { StateHistory.instance.markAsWatched(video); }),
|
||||||
));
|
));
|
||||||
|
|
||||||
val playlistItems = arrayListOf<SlideUpMenuItem>();
|
val playlistItems = arrayListOf<SlideUpMenuItem>();
|
||||||
playlistItems.add(SlideUpMenuItem(
|
playlistItems.add(SlideUpMenuItem(
|
||||||
@@ -1196,17 +1063,14 @@ class UISlideOverlays {
|
|||||||
val queue = StatePlayer.instance.getQueue();
|
val queue = StatePlayer.instance.getQueue();
|
||||||
val watchLater = StatePlaylists.instance.getWatchLater();
|
val watchLater = StatePlaylists.instance.getWatchLater();
|
||||||
items.add(
|
items.add(
|
||||||
SlideUpMenuGroup(
|
SlideUpMenuGroup(container.context, container.context.getString(R.string.other), "other",
|
||||||
container.context, container.context.getString(R.string.other), "other",
|
SlideUpMenuItem(container.context,
|
||||||
SlideUpMenuItem(
|
|
||||||
container.context,
|
|
||||||
R.drawable.ic_queue_add,
|
R.drawable.ic_queue_add,
|
||||||
container.context.getString(R.string.queue),
|
container.context.getString(R.string.queue),
|
||||||
"${queue.size} " + container.context.getString(R.string.videos),
|
"${queue.size} " + container.context.getString(R.string.videos),
|
||||||
tag = "queue",
|
tag = "queue",
|
||||||
call = { StatePlayer.instance.addToQueue(video); }),
|
call = { StatePlayer.instance.addToQueue(video); }),
|
||||||
SlideUpMenuItem(
|
SlideUpMenuItem(container.context,
|
||||||
container.context,
|
|
||||||
R.drawable.ic_watchlist_add,
|
R.drawable.ic_watchlist_add,
|
||||||
StatePlayer.TYPE_WATCHLATER,
|
StatePlayer.TYPE_WATCHLATER,
|
||||||
"${watchLater.size} " + container.context.getString(R.string.videos),
|
"${watchLater.size} " + container.context.getString(R.string.videos),
|
||||||
@@ -1214,10 +1078,8 @@ class UISlideOverlays {
|
|||||||
call = {
|
call = {
|
||||||
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true))
|
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true))
|
||||||
UIDialogs.appToast("Added to watch later", false);
|
UIDialogs.appToast("Added to watch later", false);
|
||||||
else
|
|
||||||
UIDialogs.toast(container.context.getString(R.string.already_in_watch_later))
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
val playlistItems = arrayListOf<SlideUpMenuItem>();
|
val playlistItems = arrayListOf<SlideUpMenuItem>();
|
||||||
@@ -1255,8 +1117,8 @@ class UISlideOverlays {
|
|||||||
return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.add_to), null, true, items).apply { show() };
|
return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.add_to), null, true, items).apply { show() };
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showFiltersOverlay(lifecycleScope: CoroutineScope, container: ViewGroup, enabledClientsIds: List<String>, filterValues: HashMap<String, List<String>>): SlideUpMenuFilters {
|
fun showFiltersOverlay(lifecycleScope: CoroutineScope, container: ViewGroup, enabledClientsIds: List<String>, filterValues: HashMap<String, List<String>>, isChannelSearch: Boolean = false): SlideUpMenuFilters {
|
||||||
val overlay = SlideUpMenuFilters(lifecycleScope, container, enabledClientsIds, filterValues);
|
val overlay = SlideUpMenuFilters(lifecycleScope, container, enabledClientsIds, filterValues, isChannelSearch);
|
||||||
overlay.show();
|
overlay.show();
|
||||||
return overlay;
|
return overlay;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
package com.futo.platformplayer
|
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import androidx.core.app.NotificationManagerCompat
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
|
||||||
import com.futo.platformplayer.dialogs.AutoUpdateDialog
|
|
||||||
import com.futo.platformplayer.states.StateApp
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
class UpdateActionReceiver : BroadcastReceiver() {
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
|
||||||
when (intent.action) {
|
|
||||||
UpdateNotificationManager.ACTION_UPDATE_YES -> handleUpdateYes(context, intent)
|
|
||||||
UpdateNotificationManager.ACTION_UPDATE_NO -> handleUpdateNo(context)
|
|
||||||
UpdateNotificationManager.ACTION_UPDATE_NEVER -> handleUpdateNever(context)
|
|
||||||
UpdateNotificationManager.ACTION_DOWNLOAD_CANCEL -> handleDownloadCancel(context, intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleUpdateYes(context: Context, intent: Intent) {
|
|
||||||
AutoUpdateDialog.currentDialog?.dismiss()
|
|
||||||
|
|
||||||
val version = intent.getIntExtra(UpdateNotificationManager.EXTRA_VERSION, 0)
|
|
||||||
if (version == 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
NotificationManagerCompat.from(context).cancel(UpdateNotificationManager.NOTIF_ID_AVAILABLE)
|
|
||||||
|
|
||||||
val serviceIntent = Intent(context, UpdateDownloadService::class.java).apply {
|
|
||||||
putExtra(UpdateDownloadService.EXTRA_VERSION, version)
|
|
||||||
}
|
|
||||||
ContextCompat.startForegroundService(context, serviceIntent)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleUpdateNo(context: Context) {
|
|
||||||
AutoUpdateDialog.currentDialog?.dismiss()
|
|
||||||
NotificationManagerCompat.from(context).cancel(UpdateNotificationManager.NOTIF_ID_AVAILABLE)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleUpdateNever(context: Context) {
|
|
||||||
AutoUpdateDialog.currentDialog?.dismiss()
|
|
||||||
Settings.instance.autoUpdate.check = 1
|
|
||||||
Settings.instance.save()
|
|
||||||
|
|
||||||
UpdateNotificationManager.cancelAll(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleDownloadCancel(context: Context, intent: Intent) {
|
|
||||||
val version = intent.getIntExtra(UpdateNotificationManager.EXTRA_VERSION, 0)
|
|
||||||
|
|
||||||
val cancelIntent = Intent(context, UpdateDownloadService::class.java).apply {
|
|
||||||
putExtra(UpdateDownloadService.EXTRA_CANCEL, true)
|
|
||||||
putExtra(UpdateDownloadService.EXTRA_VERSION, version)
|
|
||||||
}
|
|
||||||
ContextCompat.startForegroundService(context, cancelIntent)
|
|
||||||
|
|
||||||
NotificationManagerCompat.from(context).cancel(UpdateNotificationManager.NOTIF_ID_DOWNLOADING)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
package com.futo.platformplayer
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.work.CoroutineWorker
|
|
||||||
import androidx.work.WorkerParameters
|
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
|
||||||
import com.futo.platformplayer.states.StateApp
|
|
||||||
import com.futo.platformplayer.states.StateUpdate
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
class UpdateCheckWorker(appContext: Context, workerParams: WorkerParameters) : CoroutineWorker(appContext, workerParams) {
|
|
||||||
|
|
||||||
override suspend fun doWork(): Result {
|
|
||||||
if (!Settings.instance.autoUpdate.isAutoUpdateEnabled()) {
|
|
||||||
Logger.i(TAG, "Auto-update disabled, skipping worker run")
|
|
||||||
return Result.success()
|
|
||||||
}
|
|
||||||
|
|
||||||
return withContext(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
val client = ManagedHttpClient()
|
|
||||||
val latestVersion = StateUpdate.Companion.instance.downloadVersionCode(client)
|
|
||||||
|
|
||||||
if (latestVersion == null) {
|
|
||||||
Logger.w(TAG, "Failed to fetch latest version in worker")
|
|
||||||
return@withContext Result.retry()
|
|
||||||
}
|
|
||||||
|
|
||||||
val currentVersion = BuildConfig.VERSION_CODE
|
|
||||||
Logger.i(TAG, "Worker check: current=$currentVersion, latest=$latestVersion")
|
|
||||||
|
|
||||||
if (latestVersion <= currentVersion) {
|
|
||||||
return@withContext Result.success()
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdateNotificationManager.showUpdateAvailableNotification(applicationContext, latestVersion)
|
|
||||||
|
|
||||||
if (StateApp.instance.isMainActive) {
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
StateApp.withContext { ctx ->
|
|
||||||
try {
|
|
||||||
UIDialogs.showUpdateAvailableDialog(ctx, latestVersion, false)
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
Logger.w(TAG, "Failed to show in-app update dialog from worker", t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Result.success()
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
Logger.w(TAG, "Exception in UpdateCheckWorker", t)
|
|
||||||
Result.retry()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val TAG = "UpdateCheckWorker"
|
|
||||||
const val UNIQUE_WORK_NAME = "updateCheck"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,241 +0,0 @@
|
|||||||
package com.futo.platformplayer
|
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.app.Service
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.IBinder
|
|
||||||
import com.futo.platformplayer.UIDialogs.ActionStyle
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
|
||||||
import com.futo.platformplayer.states.StateApp
|
|
||||||
import com.futo.platformplayer.states.StateUpdate
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.net.HttpURLConnection
|
|
||||||
import java.net.URL
|
|
||||||
|
|
||||||
class UpdateDownloadService : Service() {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val TAG = "UpdateDownloadService"
|
|
||||||
const val EXTRA_VERSION = "version"
|
|
||||||
const val EXTRA_CANCEL = "cancel"
|
|
||||||
private const val MAX_RETRIES = 5
|
|
||||||
private const val INITIAL_BACKOFF_MS = 5_000L
|
|
||||||
private const val BUFFER_SIZE = 8 * 1024
|
|
||||||
|
|
||||||
var updateDownloadedDialog: Dialog? = null
|
|
||||||
}
|
|
||||||
|
|
||||||
private val job = SupervisorJob()
|
|
||||||
private val scope = CoroutineScope(Dispatchers.IO + job)
|
|
||||||
|
|
||||||
@Volatile
|
|
||||||
private var isDownloading: Boolean = false
|
|
||||||
|
|
||||||
@Volatile
|
|
||||||
private var cancelRequested: Boolean = false
|
|
||||||
|
|
||||||
override fun onBind(intent: Intent?): IBinder? = null
|
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
||||||
if (intent == null) {
|
|
||||||
stopSelf()
|
|
||||||
return START_NOT_STICKY
|
|
||||||
}
|
|
||||||
|
|
||||||
if (intent.getBooleanExtra(EXTRA_CANCEL, false)) {
|
|
||||||
cancelRequested = true
|
|
||||||
Logger.i(TAG, "Download cancel requested")
|
|
||||||
stopForeground(Service.STOP_FOREGROUND_REMOVE)
|
|
||||||
stopSelf()
|
|
||||||
return START_NOT_STICKY
|
|
||||||
}
|
|
||||||
|
|
||||||
val version = intent.getIntExtra(EXTRA_VERSION, 0)
|
|
||||||
if (version == 0) {
|
|
||||||
stopSelf()
|
|
||||||
return START_NOT_STICKY
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDownloading) {
|
|
||||||
Logger.i(TAG, "Download already in progress, ignoring new start")
|
|
||||||
return START_STICKY
|
|
||||||
}
|
|
||||||
|
|
||||||
isDownloading = true
|
|
||||||
cancelRequested = false
|
|
||||||
|
|
||||||
val notification = UpdateNotificationManager.buildDownloadProgressNotification(this, version, 0, true)
|
|
||||||
startForeground(UpdateNotificationManager.NOTIF_ID_DOWNLOADING, notification)
|
|
||||||
|
|
||||||
scope.launch {
|
|
||||||
downloadApk(version)
|
|
||||||
}
|
|
||||||
|
|
||||||
return START_STICKY
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
job.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun downloadApk(version: Int) {
|
|
||||||
val apkFile = StateUpdate.getApkFile(this, version)
|
|
||||||
val partialFile = StateUpdate.getPartialApkFile(this, version)
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (apkFile.exists() && apkFile.length() > 0L) {
|
|
||||||
Logger.i(TAG, "APK already downloaded at ${apkFile.absolutePath}")
|
|
||||||
onDownloadComplete(version, apkFile)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var backoffMs = INITIAL_BACKOFF_MS
|
|
||||||
|
|
||||||
for (attempt in 0 until MAX_RETRIES) {
|
|
||||||
if (cancelRequested) {
|
|
||||||
Logger.i(TAG, "Download cancelled before attempt ${attempt + 1}")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
performDownload(StateUpdate.APK_URL, partialFile, version)
|
|
||||||
|
|
||||||
if (!cancelRequested) {
|
|
||||||
if (apkFile.exists()) {
|
|
||||||
apkFile.delete()
|
|
||||||
}
|
|
||||||
if (!partialFile.renameTo(apkFile)) {
|
|
||||||
throw IllegalStateException("Failed to rename partial APK file")
|
|
||||||
}
|
|
||||||
onDownloadComplete(version, apkFile)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
if (cancelRequested) {
|
|
||||||
Logger.i(TAG, "Download cancelled by user", t)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if (attempt == MAX_RETRIES - 1) {
|
|
||||||
Logger.e(TAG, "Download failed after ${attempt + 1} attempts", t)
|
|
||||||
UpdateNotificationManager.showDownloadFailedNotification(this, version, t)
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
Logger.w(TAG, "Download attempt ${attempt + 1} failed, retrying in ${backoffMs / 1000}s", t)
|
|
||||||
delay(backoffMs)
|
|
||||||
backoffMs *= 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
isDownloading = false
|
|
||||||
cancelRequested = false
|
|
||||||
stopForeground(Service.STOP_FOREGROUND_REMOVE)
|
|
||||||
stopSelf()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun performDownload(url: String, partialFile: File, version: Int) {
|
|
||||||
var startOffset = if (partialFile.exists()) partialFile.length() else 0L
|
|
||||||
Logger.i(TAG, "Starting download. url=$url, existingBytes=$startOffset")
|
|
||||||
|
|
||||||
var connection: HttpURLConnection? = null
|
|
||||||
try {
|
|
||||||
connection = (URL(url).openConnection() as HttpURLConnection).apply {
|
|
||||||
connectTimeout = 15_000
|
|
||||||
readTimeout = 30_000
|
|
||||||
if (startOffset > 0L) {
|
|
||||||
setRequestProperty("Range", "bytes=$startOffset-")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
connection.connect()
|
|
||||||
val responseCode = connection.responseCode
|
|
||||||
|
|
||||||
if (responseCode == HttpURLConnection.HTTP_OK && startOffset > 0L) {
|
|
||||||
Logger.w(TAG, "Server ignored Range header, restarting download from scratch")
|
|
||||||
partialFile.delete()
|
|
||||||
startOffset = 0L
|
|
||||||
} else if (responseCode != HttpURLConnection.HTTP_OK &&
|
|
||||||
responseCode != HttpURLConnection.HTTP_PARTIAL) {
|
|
||||||
throw IllegalStateException("Unexpected HTTP response code $responseCode")
|
|
||||||
}
|
|
||||||
|
|
||||||
val contentLength = connection.contentLengthLong
|
|
||||||
val totalBytes = if (contentLength > 0L) startOffset + contentLength else -1L
|
|
||||||
|
|
||||||
val buffer = ByteArray(BUFFER_SIZE)
|
|
||||||
var downloaded = 0L
|
|
||||||
var lastProgress = -1
|
|
||||||
|
|
||||||
connection.inputStream.use { input ->
|
|
||||||
FileOutputStream(partialFile, startOffset > 0L).use { output ->
|
|
||||||
while (!cancelRequested) {
|
|
||||||
val read = input.read(buffer)
|
|
||||||
if (read == -1) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
output.write(buffer, 0, read)
|
|
||||||
downloaded += read
|
|
||||||
|
|
||||||
if (totalBytes > 0L) {
|
|
||||||
val progress = (((startOffset + downloaded) * 100L) / totalBytes).toInt()
|
|
||||||
if (progress != lastProgress) {
|
|
||||||
lastProgress = progress
|
|
||||||
val safeProgress = when {
|
|
||||||
progress < 0 -> 0
|
|
||||||
progress > 100 -> 100
|
|
||||||
else -> progress
|
|
||||||
}
|
|
||||||
UpdateNotificationManager.updateDownloadProgress(this, version, safeProgress, false)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
UpdateNotificationManager.updateDownloadProgress(this, version, 0, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
output.flush()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cancelRequested) {
|
|
||||||
throw CancellationException("Download cancelled")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (totalBytes > 0L && startOffset + downloaded < totalBytes) {
|
|
||||||
throw IllegalStateException("Download incomplete: expected=$totalBytes, got=${startOffset + downloaded}")
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
connection?.disconnect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onDownloadComplete(version: Int, apkFile: File) {
|
|
||||||
Logger.i(TAG, "Download complete for version=$version, file=${apkFile.absolutePath}")
|
|
||||||
UpdateNotificationManager.showDownloadCompleteNotification(this, version, apkFile)
|
|
||||||
|
|
||||||
if (StateApp.instance.isMainActive) {
|
|
||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
|
||||||
StateApp.withContext { ctx ->
|
|
||||||
try {
|
|
||||||
updateDownloadedDialog = UIDialogs.showDialog(ctx, R.drawable.foreground,
|
|
||||||
"Update downloaded",
|
|
||||||
"Would you like to install it now?", null, 0,
|
|
||||||
UIDialogs.Action("Cancel", {
|
|
||||||
updateDownloadedDialog = null
|
|
||||||
}, ActionStyle.NONE, true),
|
|
||||||
UIDialogs.Action("Install", {
|
|
||||||
UpdateNotificationManager.cancelAll(ctx)
|
|
||||||
UpdateInstaller.startInstall(ctx, apkFile)
|
|
||||||
}, ActionStyle.PRIMARY, true));
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
Logger.w(TAG, "Failed to show in-app update downloaded dialog", t)
|
|
||||||
updateDownloadedDialog = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
package com.futo.platformplayer
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.PendingIntent.FLAG_MUTABLE
|
|
||||||
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
|
||||||
import android.app.PendingIntent.getBroadcast
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.PackageInstaller
|
|
||||||
import android.graphics.drawable.Animatable
|
|
||||||
import android.provider.Settings
|
|
||||||
import android.view.View
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
|
||||||
import com.futo.platformplayer.receivers.InstallReceiver
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import java.io.File
|
|
||||||
import java.io.InputStream
|
|
||||||
import androidx.core.net.toUri
|
|
||||||
import com.futo.platformplayer.dialogs.AutoUpdateDialog
|
|
||||||
import com.futo.platformplayer.states.StateApp
|
|
||||||
|
|
||||||
object UpdateInstaller {
|
|
||||||
private const val TAG = "UpdateInstaller"
|
|
||||||
|
|
||||||
@SuppressLint("RequestInstallPackagesPolicy")
|
|
||||||
fun startInstall(context: Context, apkFile: File) {
|
|
||||||
if (!apkFile.exists()) {
|
|
||||||
Logger.w(TAG, "APK file does not exist: ${apkFile.absolutePath}")
|
|
||||||
UIDialogs.toast(context, "Update file missing")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (BuildConfig.IS_PLAYSTORE_BUILD) {
|
|
||||||
UIDialogs.toast(context, "Updates are managed by the Play Store")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
val pm = context.packageManager
|
|
||||||
if (!pm.canRequestPackageInstalls()) {
|
|
||||||
UIDialogs.toast(context, "Allow this app to install updates, then try again")
|
|
||||||
|
|
||||||
val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES).apply {
|
|
||||||
data = "package:${context.packageName}".toUri()
|
|
||||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
||||||
}
|
|
||||||
context.startActivity(intent)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
Logger.e(TAG, "Failed to check unknown sources permission", t)
|
|
||||||
}
|
|
||||||
|
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
|
||||||
var inputStream: InputStream? = null
|
|
||||||
var session: PackageInstaller.Session? = null
|
|
||||||
try {
|
|
||||||
|
|
||||||
val packageInstaller: PackageInstaller = context.packageManager.packageInstaller
|
|
||||||
val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
|
|
||||||
val sessionId = packageInstaller.createSession(params)
|
|
||||||
session = packageInstaller.openSession(sessionId)
|
|
||||||
|
|
||||||
inputStream = apkFile.inputStream()
|
|
||||||
val dataLength = apkFile.length()
|
|
||||||
|
|
||||||
session.openWrite("package", 0, dataLength).use { sessionStream ->
|
|
||||||
inputStream.copyToOutputStream(dataLength, sessionStream) { _ -> }
|
|
||||||
session.fsync(sessionStream)
|
|
||||||
}
|
|
||||||
|
|
||||||
val intent = Intent(context, InstallReceiver::class.java)
|
|
||||||
val pendingIntent = getBroadcast(context, 0, intent, FLAG_MUTABLE or FLAG_UPDATE_CURRENT)
|
|
||||||
val statusReceiver = pendingIntent.intentSender
|
|
||||||
|
|
||||||
InstallReceiver.onReceiveResult.subscribe(this) { message ->
|
|
||||||
InstallReceiver.onReceiveResult.clear();
|
|
||||||
onReceiveResult(context, message);
|
|
||||||
};
|
|
||||||
Logger.i(TAG, "Committing install session for ${apkFile.absolutePath}")
|
|
||||||
session.commit(statusReceiver)
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.w(TAG, "Exception while installing update", e)
|
|
||||||
session?.abandon()
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
UIDialogs.toast(context, "Failed to install update: ${e.message}")
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
session?.close()
|
|
||||||
inputStream?.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private fun onReceiveResult(context: Context, result: String?) {
|
|
||||||
InstallReceiver.onReceiveResult.remove(this);
|
|
||||||
UIDialogs.showGeneralErrorDialog(context, "Install failed due to:\n" + result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
package com.futo.platformplayer
|
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.app.Notification
|
|
||||||
import android.app.NotificationChannel
|
|
||||||
import android.app.NotificationManager
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.app.PendingIntent.FLAG_MUTABLE
|
|
||||||
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
|
||||||
import android.app.PendingIntent.getBroadcast
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import androidx.core.app.NotificationManagerCompat
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import com.futo.platformplayer.activities.InstallUpdateActivity
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
object UpdateNotificationManager {
|
|
||||||
private const val CHANNEL_ID = "app_updates"
|
|
||||||
private const val CHANNEL_NAME = "App updates"
|
|
||||||
private const val CHANNEL_DESCRIPTION = "Notifications about new app versions"
|
|
||||||
|
|
||||||
const val ACTION_UPDATE_YES = "com.futo.platformplayer.UPDATE_YES"
|
|
||||||
const val ACTION_UPDATE_NO = "com.futo.platformplayer.UPDATE_NO"
|
|
||||||
const val ACTION_UPDATE_NEVER = "com.futo.platformplayer.UPDATE_NEVER"
|
|
||||||
const val ACTION_DOWNLOAD_CANCEL = "com.futo.platformplayer.UPDATE_CANCEL"
|
|
||||||
const val ACTION_INSTALL_NOW = "com.futo.platformplayer.UPDATE_INSTALL"
|
|
||||||
private const val REQUEST_CODE_INSTALL = 1001
|
|
||||||
|
|
||||||
const val EXTRA_VERSION = "version"
|
|
||||||
const val EXTRA_APK_PATH = "apk_path"
|
|
||||||
|
|
||||||
const val NOTIF_ID_AVAILABLE = 2001
|
|
||||||
const val NOTIF_ID_DOWNLOADING = 2002
|
|
||||||
const val NOTIF_ID_READY = 2003
|
|
||||||
|
|
||||||
fun ensureChannel(context: Context) {
|
|
||||||
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
||||||
if (manager.getNotificationChannel(CHANNEL_ID) == null) {
|
|
||||||
val channel = NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT).apply {
|
|
||||||
description = CHANNEL_DESCRIPTION
|
|
||||||
enableVibration(false)
|
|
||||||
enableLights(false)
|
|
||||||
setSound(null, null)
|
|
||||||
}
|
|
||||||
manager.createNotificationChannel(channel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun showUpdateAvailableNotification(context: Context, version: Int) {
|
|
||||||
if (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ensureChannel(context)
|
|
||||||
|
|
||||||
val yesIntent = Intent(context, UpdateActionReceiver::class.java).apply {
|
|
||||||
action = ACTION_UPDATE_YES
|
|
||||||
putExtra(EXTRA_VERSION, version)
|
|
||||||
}
|
|
||||||
val yesPendingIntent = getBroadcast(context, 0, yesIntent, FLAG_MUTABLE or FLAG_UPDATE_CURRENT)
|
|
||||||
val noIntent = Intent(context, UpdateActionReceiver::class.java).apply {
|
|
||||||
action = ACTION_UPDATE_NO
|
|
||||||
putExtra(EXTRA_VERSION, version)
|
|
||||||
}
|
|
||||||
val noPendingIntent = getBroadcast(context, 1, noIntent, FLAG_MUTABLE or FLAG_UPDATE_CURRENT)
|
|
||||||
val neverIntent = Intent(context, UpdateActionReceiver::class.java).apply {
|
|
||||||
action = ACTION_UPDATE_NEVER
|
|
||||||
putExtra(EXTRA_VERSION, version)
|
|
||||||
}
|
|
||||||
val neverPendingIntent = getBroadcast(context, 2, neverIntent, FLAG_MUTABLE or FLAG_UPDATE_CURRENT)
|
|
||||||
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
|
|
||||||
.setSmallIcon(R.drawable.foreground)
|
|
||||||
.setContentTitle("Update available")
|
|
||||||
.setContentText("A new version ($version) is available.")
|
|
||||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
|
||||||
.setAutoCancel(true)
|
|
||||||
.setSilent(true)
|
|
||||||
.addAction(0, "Never", neverPendingIntent)
|
|
||||||
.addAction(0, "Not now", noPendingIntent)
|
|
||||||
.addAction(0, "Download", yesPendingIntent)
|
|
||||||
|
|
||||||
NotificationManagerCompat.from(context).notify(NOTIF_ID_AVAILABLE, builder.build())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun buildDownloadProgressNotification(context: Context, version: Int, progress: Int, indeterminate: Boolean): Notification {
|
|
||||||
ensureChannel(context)
|
|
||||||
|
|
||||||
val cancelIntent = Intent(context, UpdateActionReceiver::class.java).apply {
|
|
||||||
action = ACTION_DOWNLOAD_CANCEL
|
|
||||||
putExtra(EXTRA_VERSION, version)
|
|
||||||
}
|
|
||||||
val cancelPendingIntent = getBroadcast(
|
|
||||||
context,
|
|
||||||
3,
|
|
||||||
cancelIntent,
|
|
||||||
FLAG_MUTABLE or FLAG_UPDATE_CURRENT
|
|
||||||
)
|
|
||||||
|
|
||||||
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
|
|
||||||
.setSmallIcon(R.drawable.foreground)
|
|
||||||
.setContentTitle("Downloading update")
|
|
||||||
.setContentText("Downloading version $version")
|
|
||||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
|
||||||
.setOngoing(true)
|
|
||||||
.setSilent(true)
|
|
||||||
.addAction(0, "Cancel", cancelPendingIntent)
|
|
||||||
|
|
||||||
if (indeterminate) {
|
|
||||||
builder.setProgress(0, 0, true)
|
|
||||||
} else {
|
|
||||||
builder.setProgress(100, progress, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
return builder.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateDownloadProgress(context: Context, version: Int, progress: Int, indeterminate: Boolean) {
|
|
||||||
if (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val notification = buildDownloadProgressNotification(context, version, progress, indeterminate)
|
|
||||||
NotificationManagerCompat.from(context).notify(NOTIF_ID_DOWNLOADING, notification)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun showDownloadCompleteNotification(context: Context, version: Int, apkFile: File) {
|
|
||||||
if (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ensureChannel(context)
|
|
||||||
|
|
||||||
val installIntent = InstallUpdateActivity.createIntent(context, version, apkFile.absolutePath)
|
|
||||||
val installPendingIntent = PendingIntent.getActivity(context, REQUEST_CODE_INSTALL, installIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
|
||||||
|
|
||||||
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
|
|
||||||
.setSmallIcon(R.drawable.foreground)
|
|
||||||
.setContentTitle("Update downloaded")
|
|
||||||
.setContentText("Tap to install version $version.")
|
|
||||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
|
||||||
.setAutoCancel(true)
|
|
||||||
.setSilent(true)
|
|
||||||
.addAction(0, "Install", installPendingIntent)
|
|
||||||
|
|
||||||
NotificationManagerCompat.from(context).notify(NOTIF_ID_READY, builder.build())
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun showDownloadFailedNotification(context: Context, version: Int, error: Throwable?) {
|
|
||||||
if (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ensureChannel(context)
|
|
||||||
|
|
||||||
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
|
|
||||||
.setSmallIcon(R.drawable.foreground)
|
|
||||||
.setContentTitle("Failed to download update")
|
|
||||||
.setContentText(error?.message ?: "Unknown error")
|
|
||||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
|
||||||
.setAutoCancel(true)
|
|
||||||
.setSilent(true)
|
|
||||||
|
|
||||||
NotificationManagerCompat.from(context).notify(NOTIF_ID_READY, builder.build())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun cancelAll(context: Context) {
|
|
||||||
NotificationManagerCompat.from(context).cancel(NOTIF_ID_AVAILABLE)
|
|
||||||
NotificationManagerCompat.from(context).cancel(NOTIF_ID_DOWNLOADING)
|
|
||||||
NotificationManagerCompat.from(context).cancel(NOTIF_ID_READY)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,6 +5,8 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.icu.util.Output
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.os.OperationCanceledException
|
import android.os.OperationCanceledException
|
||||||
@@ -29,12 +31,6 @@ 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.net.Inet4Address
|
|
||||||
import java.net.Inet6Address
|
|
||||||
import java.net.InetAddress
|
|
||||||
import java.net.InterfaceAddress
|
|
||||||
import java.net.NetworkInterface
|
|
||||||
import java.net.SocketException
|
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
@@ -42,9 +38,6 @@ import java.util.*
|
|||||||
import java.util.concurrent.ThreadLocalRandom
|
import java.util.concurrent.ThreadLocalRandom
|
||||||
import java.util.zip.GZIPInputStream
|
import java.util.zip.GZIPInputStream
|
||||||
import java.util.zip.GZIPOutputStream
|
import java.util.zip.GZIPOutputStream
|
||||||
import androidx.core.graphics.scale
|
|
||||||
import com.bumptech.glide.RequestBuilder
|
|
||||||
import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
|
|
||||||
|
|
||||||
private val _allowedCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz ";
|
private val _allowedCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz ";
|
||||||
fun getRandomString(sizeOfRandomString: Int): String {
|
fun getRandomString(sizeOfRandomString: Int): String {
|
||||||
@@ -76,14 +69,7 @@ fun warnIfMainThread(context: String) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun ensureNotMainThread() {
|
fun ensureNotMainThread() {
|
||||||
val isMainLooper = try {
|
if (Looper.myLooper() == Looper.getMainLooper()) {
|
||||||
Looper.myLooper() == Looper.getMainLooper()
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
//Ignore, for unit tests where its not mocked
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isMainLooper) {
|
|
||||||
Logger.e("Utility", "Throwing exception because a function that should not be called on main thread, is called on main thread")
|
Logger.e("Utility", "Throwing exception because a function that should not be called on main thread, is called on main thread")
|
||||||
throw IllegalStateException("Cannot run on main thread")
|
throw IllegalStateException("Cannot run on main thread")
|
||||||
}
|
}
|
||||||
@@ -102,7 +88,7 @@ fun String.isHexColor(): Boolean {
|
|||||||
|
|
||||||
fun IPlatformClient.fromPool(pool: PlatformMultiClientPool) = pool.getClientPooled(this);
|
fun IPlatformClient.fromPool(pool: PlatformMultiClientPool) = pool.getClientPooled(this);
|
||||||
|
|
||||||
fun IPlatformVideo.withTimestamp(sec: Long) = PlatformVideoWithTime(this, sec);
|
fun IPlatformVideo.withTimestamp(sec: Long) = PlatformVideoWithTime(this, sec);
|
||||||
|
|
||||||
fun DocumentFile.getInputStream(context: Context) = context.contentResolver.openInputStream(this.uri);
|
fun DocumentFile.getInputStream(context: Context) = context.contentResolver.openInputStream(this.uri);
|
||||||
fun DocumentFile.getOutputStream(context: Context) = context.contentResolver.openOutputStream(this.uri);
|
fun DocumentFile.getOutputStream(context: Context) = context.contentResolver.openOutputStream(this.uri);
|
||||||
@@ -115,6 +101,23 @@ fun DocumentFile.writeBytes(context: Context, byteArray: ByteArray) = context.co
|
|||||||
it.flush();
|
it.flush();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fun loadBitmap(url: String): Bitmap {
|
||||||
|
try {
|
||||||
|
val client = ManagedHttpClient();
|
||||||
|
val response = client.get(url);
|
||||||
|
if (response.isOk && response.body != null) {
|
||||||
|
val bitmapStream = response.body.byteStream();
|
||||||
|
val bitmap = BitmapFactory.decodeStream(bitmapStream);
|
||||||
|
return bitmap;
|
||||||
|
} else {
|
||||||
|
throw Exception("Failed to find data at URL.");
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.w("Utility", "Exception thrown while downloading bitmap.", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun TextView.setPlatformPlayerLinkMovementMethod(context: Context) {
|
fun TextView.setPlatformPlayerLinkMovementMethod(context: Context) {
|
||||||
this.movementMethod = PlatformLinkMovementMethod(context);
|
this.movementMethod = PlatformLinkMovementMethod(context);
|
||||||
}
|
}
|
||||||
@@ -269,7 +272,7 @@ fun <T> findNewIndex(originalArr: List<T>, newArr: List<T>, item: T): Int{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(newIndex < 0)
|
if(newIndex < 0)
|
||||||
return newArr.size;
|
return originalArr.size;
|
||||||
else
|
else
|
||||||
return newIndex;
|
return newIndex;
|
||||||
}
|
}
|
||||||
@@ -321,132 +324,4 @@ fun ByteArray.fromGzip(): ByteArray {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return outputStream.toByteArray()
|
return outputStream.toByteArray()
|
||||||
}
|
|
||||||
|
|
||||||
fun findCandidateAddresses(): List<InetAddress> {
|
|
||||||
val candidates = NetworkInterface.getNetworkInterfaces()
|
|
||||||
.toList()
|
|
||||||
.asSequence()
|
|
||||||
.filter(::isUsableInterface)
|
|
||||||
.flatMap { nif ->
|
|
||||||
nif.interfaceAddresses
|
|
||||||
.asSequence()
|
|
||||||
.mapNotNull { ia ->
|
|
||||||
ia.address.takeIf(::isUsableAddress)?.let { addr ->
|
|
||||||
nif to ia
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.toList()
|
|
||||||
|
|
||||||
return candidates
|
|
||||||
.sortedWith(
|
|
||||||
compareBy<Pair<NetworkInterface, InterfaceAddress>>(
|
|
||||||
{ addressScore(it.second.address) },
|
|
||||||
{ interfaceScore(it.first) },
|
|
||||||
{ -it.second.networkPrefixLength.toInt() },
|
|
||||||
{ -it.first.mtu }
|
|
||||||
)
|
|
||||||
).map { it.second.address }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun findPreferredAddress(): InetAddress? {
|
|
||||||
val candidates = NetworkInterface.getNetworkInterfaces()
|
|
||||||
.toList()
|
|
||||||
.asSequence()
|
|
||||||
.filter(::isUsableInterface)
|
|
||||||
.flatMap { nif ->
|
|
||||||
nif.interfaceAddresses
|
|
||||||
.asSequence()
|
|
||||||
.mapNotNull { ia ->
|
|
||||||
ia.address.takeIf(::isUsableAddress)?.let { addr ->
|
|
||||||
nif to ia
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.toList()
|
|
||||||
|
|
||||||
return candidates
|
|
||||||
.minWithOrNull(
|
|
||||||
compareBy<Pair<NetworkInterface, InterfaceAddress>>(
|
|
||||||
{ addressScore(it.second.address) },
|
|
||||||
{ interfaceScore(it.first) },
|
|
||||||
{ -it.second.networkPrefixLength.toInt() },
|
|
||||||
{ -it.first.mtu }
|
|
||||||
)
|
|
||||||
)?.second?.address
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isUsableInterface(nif: NetworkInterface): Boolean {
|
|
||||||
val name = nif.name.lowercase()
|
|
||||||
return try {
|
|
||||||
// must be up, not loopback/virtual/PtP, have a MAC, not Docker/tun/etc.
|
|
||||||
nif.isUp
|
|
||||||
&& !nif.isLoopback
|
|
||||||
&& !nif.isPointToPoint
|
|
||||||
&& !nif.isVirtual
|
|
||||||
&& !name.startsWith("docker")
|
|
||||||
&& !name.startsWith("veth")
|
|
||||||
&& !name.startsWith("br-")
|
|
||||||
&& !name.startsWith("virbr")
|
|
||||||
&& !name.startsWith("vmnet")
|
|
||||||
&& !name.startsWith("tun")
|
|
||||||
&& !name.startsWith("tap")
|
|
||||||
} catch (e: SocketException) {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isUsableAddress(addr: InetAddress): Boolean {
|
|
||||||
return when {
|
|
||||||
addr.isAnyLocalAddress -> false // 0.0.0.0 / ::
|
|
||||||
addr.isLoopbackAddress -> false
|
|
||||||
addr.isLinkLocalAddress -> false // 169.254.x.x or fe80::/10
|
|
||||||
addr.isMulticastAddress -> false
|
|
||||||
else -> true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun interfaceScore(nif: NetworkInterface): Int {
|
|
||||||
val name = nif.name.lowercase()
|
|
||||||
return when {
|
|
||||||
name.matches(Regex("^(eth|enp|eno|ens|em)\\d+")) -> 0
|
|
||||||
name.startsWith("eth") || name.contains("ethernet") -> 0
|
|
||||||
name.matches(Regex("^(wlan|wlp)\\d+")) -> 1
|
|
||||||
name.contains("wi-fi") || name.contains("wifi") -> 1
|
|
||||||
else -> 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addressScore(addr: InetAddress): Int {
|
|
||||||
return when (addr) {
|
|
||||||
is Inet4Address -> {
|
|
||||||
val octets = addr.address.map { it.toInt() and 0xFF }
|
|
||||||
when {
|
|
||||||
octets[0] == 10 -> 0 // 10/8
|
|
||||||
octets[0] == 192 && octets[1] == 168 -> 0 // 192.168/16
|
|
||||||
octets[0] == 172 && octets[1] in 16..31 -> 0 // 172.16–31/12
|
|
||||||
else -> 1 // public IPv4
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is Inet6Address -> {
|
|
||||||
// ULA (fc00::/7) vs global vs others
|
|
||||||
val b0 = addr.address[0].toInt() and 0xFF
|
|
||||||
when {
|
|
||||||
(b0 and 0xFE) == 0xFC -> 2 // ULA
|
|
||||||
(b0 and 0xE0) == 0x20 -> 3 // global
|
|
||||||
else -> 4
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> Int.MAX_VALUE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <T> Enumeration<T>.toList(): List<T> = Collections.list(this)
|
|
||||||
|
|
||||||
fun <T> RequestBuilder<T>.withMaxSizePx(maxSizePx: Int = 1920): RequestBuilder<T> {
|
|
||||||
return this;
|
|
||||||
//.downsample(DownsampleStrategy.AT_MOST)
|
|
||||||
//.override(maxSizePx, maxSizePx)
|
|
||||||
//.centerInside()
|
|
||||||
}
|
}
|
||||||
@@ -107,9 +107,10 @@ class AddSourceActivity : AppCompatActivity() {
|
|||||||
onNewIntent(intent);
|
onNewIntent(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent) {
|
override fun onNewIntent(intent: Intent?) {
|
||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
var url = intent.dataString;
|
var url = intent?.dataString;
|
||||||
|
|
||||||
if(url == null)
|
if(url == null)
|
||||||
UIDialogs.showDialog(this, R.drawable.ic_error, getString(R.string.no_valid_url_provided), null, null,
|
UIDialogs.showDialog(this, R.drawable.ic_error, getString(R.string.no_valid_url_provided), null, null,
|
||||||
0, UIDialogs.Action(getString(R.string.ok), { finish() }, UIDialogs.ActionStyle.PRIMARY));
|
0, UIDialogs.Action(getString(R.string.ok), { finish() }, UIDialogs.ActionStyle.PRIMARY));
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import com.futo.platformplayer.*
|
||||||
|
import com.futo.platformplayer.views.fields.FieldForm
|
||||||
|
import com.futo.platformplayer.views.fields.IField
|
||||||
|
|
||||||
|
class DeveloperActivity : AppCompatActivity() {
|
||||||
|
private lateinit var _form: FieldForm;
|
||||||
|
private lateinit var _buttonBack: ImageButton;
|
||||||
|
|
||||||
|
fun getField(id: String): IField? {
|
||||||
|
return _form.findField(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
DeveloperActivity._lastActivity = this;
|
||||||
|
setContentView(R.layout.activity_dev);
|
||||||
|
setNavigationBarColorAndIcons();
|
||||||
|
|
||||||
|
_buttonBack = findViewById(R.id.button_back);
|
||||||
|
_form = findViewById(R.id.settings_form);
|
||||||
|
|
||||||
|
_form.fromObject(SettingsDev.instance);
|
||||||
|
_form.onChanged.subscribe { _, _ ->
|
||||||
|
_form.setObjectValues();
|
||||||
|
SettingsDev.instance.save();
|
||||||
|
};
|
||||||
|
|
||||||
|
_buttonBack.setOnClickListener {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun finish() {
|
||||||
|
super.finish()
|
||||||
|
overridePendingTransition(R.anim.slide_lighten, R.anim.slide_out_up)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
//TODO: Temporary for solving Settings issues
|
||||||
|
@SuppressLint("StaticFieldLeak")
|
||||||
|
private var _lastActivity: DeveloperActivity? = null;
|
||||||
|
|
||||||
|
fun getActivity(): DeveloperActivity? {
|
||||||
|
val act = _lastActivity;
|
||||||
|
if(act != null)
|
||||||
|
return act;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
package com.futo.platformplayer.activities
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import com.futo.platformplayer.UIDialogs
|
|
||||||
import com.futo.platformplayer.UpdateInstaller
|
|
||||||
import com.futo.platformplayer.UpdateNotificationManager
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
class InstallUpdateActivity : AppCompatActivity() {
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
val version = intent.getIntExtra(UpdateNotificationManager.EXTRA_VERSION, 0)
|
|
||||||
val apkPath = intent.getStringExtra(UpdateNotificationManager.EXTRA_APK_PATH)
|
|
||||||
|
|
||||||
if (version == 0 || apkPath.isNullOrEmpty()) {
|
|
||||||
Logger.w("InstallUpdateActivity", "Missing version or apkPath")
|
|
||||||
finish()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val apkFile = File(apkPath)
|
|
||||||
if (!apkFile.exists()) {
|
|
||||||
Logger.w("InstallUpdateActivity", "APK file does not exist: $apkPath")
|
|
||||||
UIDialogs.Companion.toast(this, "Update file missing")
|
|
||||||
finish()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdateInstaller.startInstall(this, apkFile)
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun createIntent(context: Context, version: Int, apkPath: String): Intent =
|
|
||||||
Intent(context, InstallUpdateActivity::class.java).apply {
|
|
||||||
putExtra(UpdateNotificationManager.EXTRA_VERSION, version)
|
|
||||||
putExtra(UpdateNotificationManager.EXTRA_APK_PATH, apkPath)
|
|
||||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -15,7 +15,6 @@ 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.matchesDomain
|
|
||||||
import com.futo.platformplayer.others.LoginWebViewClient
|
import com.futo.platformplayer.others.LoginWebViewClient
|
||||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
@@ -75,26 +74,9 @@ class LoginActivity : AppCompatActivity() {
|
|||||||
finish();
|
finish();
|
||||||
};
|
};
|
||||||
var isFirstLoad = true;
|
var isFirstLoad = true;
|
||||||
val loginWarnings = authConfig.loginWarnings?.toMutableList() ?: mutableListOf<SourcePluginAuthConfig.Warning>();
|
|
||||||
val uiMods = authConfig.uiMods?.toMutableList() ?: mutableListOf<SourcePluginAuthConfig.UIMod>();
|
|
||||||
var currentScale = 100;
|
|
||||||
var currentDesktop = false;
|
|
||||||
webViewClient.onPageLoaded.subscribe { view, url ->
|
webViewClient.onPageLoaded.subscribe { view, url ->
|
||||||
_textUrl.setText(url ?: "");
|
_textUrl.setText(url ?: "");
|
||||||
|
|
||||||
if(loginWarnings.size > 0 && url != null) {
|
|
||||||
synchronized(loginWarnings) {
|
|
||||||
val warning = loginWarnings.find { url.matches(it.getRegex()) };
|
|
||||||
if(warning != null) {
|
|
||||||
if(warning.once == true)
|
|
||||||
loginWarnings.remove(warning);
|
|
||||||
UIDialogs.showDialog(this@LoginActivity, R.drawable.ic_warning_yellow, warning.text ?: "", warning.details ?: "", null, 0,
|
|
||||||
UIDialogs.Action("Understood", {
|
|
||||||
}, UIDialogs.ActionStyle.PRIMARY));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!isFirstLoad)
|
if(!isFirstLoad)
|
||||||
return@subscribe;
|
return@subscribe;
|
||||||
isFirstLoad = false;
|
isFirstLoad = false;
|
||||||
@@ -104,35 +86,6 @@ class LoginActivity : AppCompatActivity() {
|
|||||||
//TODO: Find most reliable way to wait for page js to finish
|
//TODO: Find most reliable way to wait for page js to finish
|
||||||
view?.evaluateJavascript("setTimeout(()=> document.querySelector(\"${authConfig.loginButton}\")?.click(), 1000)", {});
|
view?.evaluateJavascript("setTimeout(()=> document.querySelector(\"${authConfig.loginButton}\")?.click(), 1000)", {});
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
var specifiedScale = false;
|
|
||||||
var specifiedDesktop = false;
|
|
||||||
if(uiMods.size > 0 && url != null) {
|
|
||||||
synchronized(uiMods) {
|
|
||||||
val uimod = uiMods.find { url.matches(it.getRegex()) };
|
|
||||||
if(uimod != null) {
|
|
||||||
if(uimod.scale != null) {
|
|
||||||
currentScale =(uimod.scale * 100).toInt();
|
|
||||||
_webView.setInitialScale(currentScale);
|
|
||||||
specifiedScale = true;
|
|
||||||
}
|
|
||||||
if(uimod.desktop != null && uimod.desktop) {
|
|
||||||
_webView.settings.useWideViewPort = true;
|
|
||||||
specifiedDesktop = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(!specifiedScale && currentScale != 100) {
|
|
||||||
currentScale = (100).toInt();
|
|
||||||
_webView.setInitialScale(currentScale);
|
|
||||||
}
|
|
||||||
if(!specifiedDesktop && currentDesktop) {
|
|
||||||
_webView.settings.useWideViewPort = false;
|
|
||||||
currentDesktop = false;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
_webView.settings.domStorageEnabled = true;
|
_webView.settings.domStorageEnabled = true;
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +1,26 @@
|
|||||||
package com.futo.platformplayer.activities
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.AlertDialog
|
import android.content.ComponentName
|
||||||
import android.app.UiModeManager
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.graphics.Color
|
import android.media.AudioManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.StrictMode
|
import android.os.StrictMode
|
||||||
import android.os.StrictMode.VmPolicy
|
import android.os.StrictMode.VmPolicy
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.WindowManager
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import androidx.activity.result.ActivityResult
|
import androidx.activity.result.ActivityResult
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.annotation.OptIn
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.constraintlayout.motion.widget.MotionLayout
|
import androidx.constraintlayout.motion.widget.MotionLayout
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
@@ -32,20 +30,15 @@ import androidx.fragment.app.Fragment
|
|||||||
import androidx.fragment.app.FragmentContainerView
|
import androidx.fragment.app.FragmentContainerView
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.withStateAtLeast
|
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import com.futo.platformplayer.BuildConfig
|
import com.futo.platformplayer.BuildConfig
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.RootInsetsController
|
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.api.media.models.video.LocalVideoDetails
|
|
||||||
import com.futo.platformplayer.casting.StateCasting
|
import com.futo.platformplayer.casting.StateCasting
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.dp
|
|
||||||
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.ArticleDetailFragment
|
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.BrowserFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.BrowserFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.BuyFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.BuyFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.ChannelFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.ChannelFragment
|
||||||
@@ -53,29 +46,17 @@ import com.futo.platformplayer.fragment.mainactivity.main.CommentsFragment
|
|||||||
import com.futo.platformplayer.fragment.mainactivity.main.ContentSearchResultsFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.ContentSearchResultsFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.CreatorSearchResultsFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.CreatorSearchResultsFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.CreatorsFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.CreatorsFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.DeveloperFragment
|
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.DownloadsFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.DownloadsFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.HistoryFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.HistoryFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.HomeFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.HomeFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.ImportPlaylistsFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.ImportPlaylistsFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.ImportSubscriptionsFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.ImportSubscriptionsFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.LibraryAlbumFragment
|
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.LibraryAlbumsFragment
|
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.LibraryArtistFragment
|
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.LibraryArtistsFragment
|
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.LibraryFilesFragment
|
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.LibraryFragment
|
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.LibrarySearchFragment
|
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.LibraryVideosFragment
|
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.LoginFragment
|
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.MainFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.MainFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PlaylistFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.PlaylistFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PlaylistSearchResultsFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.PlaylistSearchResultsFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PlaylistsFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.PlaylistsFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PostDetailFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.PostDetailFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.RemotePlaylistFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.RemotePlaylistFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.SettingsFragment
|
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.ShortsFragment
|
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.SourceDetailFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.SourceDetailFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.SourcesFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.SourcesFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionGroupFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionGroupFragment
|
||||||
@@ -84,11 +65,8 @@ import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionsFeedFragm
|
|||||||
import com.futo.platformplayer.fragment.mainactivity.main.SuggestionsFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.SuggestionsFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.TutorialFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.TutorialFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.VideoDetailFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.VideoDetailFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.VideoDetailFragment.State
|
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.WatchLaterFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.WatchLaterFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.WebDetailFragment
|
|
||||||
import com.futo.platformplayer.fragment.mainactivity.topbar.AddTopBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.topbar.AddTopBarFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.topbar.FilesTopBarFragment
|
|
||||||
import com.futo.platformplayer.fragment.mainactivity.topbar.GeneralTopBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.topbar.GeneralTopBarFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.topbar.ImportTopBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.topbar.ImportTopBarFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFragment
|
||||||
@@ -96,6 +74,7 @@ import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
|
|||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.ImportCache
|
import com.futo.platformplayer.models.ImportCache
|
||||||
import com.futo.platformplayer.models.UrlVideoWithTime
|
import com.futo.platformplayer.models.UrlVideoWithTime
|
||||||
|
import com.futo.platformplayer.receivers.MediaButtonReceiver
|
||||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StateBackup
|
import com.futo.platformplayer.states.StateBackup
|
||||||
@@ -128,7 +107,7 @@ import java.io.PrintWriter
|
|||||||
import java.io.StringWriter
|
import java.io.StringWriter
|
||||||
import java.lang.reflect.InvocationTargetException
|
import java.lang.reflect.InvocationTargetException
|
||||||
import java.util.LinkedList
|
import java.util.LinkedList
|
||||||
import java.util.UUID
|
import java.util.Queue
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue
|
import java.util.concurrent.ConcurrentLinkedQueue
|
||||||
|
|
||||||
|
|
||||||
@@ -160,7 +139,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
lateinit var _fragTopBarNavigation: NavigationTopBarFragment;
|
lateinit var _fragTopBarNavigation: NavigationTopBarFragment;
|
||||||
lateinit var _fragTopBarImport: ImportTopBarFragment;
|
lateinit var _fragTopBarImport: ImportTopBarFragment;
|
||||||
lateinit var _fragTopBarAdd: AddTopBarFragment;
|
lateinit var _fragTopBarAdd: AddTopBarFragment;
|
||||||
lateinit var _fragTopBarFiles: FilesTopBarFragment;
|
|
||||||
|
|
||||||
//Frags BotBar
|
//Frags BotBar
|
||||||
lateinit var _fragBotBarMenu: MenuBottomBarFragment;
|
lateinit var _fragBotBarMenu: MenuBottomBarFragment;
|
||||||
@@ -168,8 +146,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
//Frags Main
|
//Frags Main
|
||||||
lateinit var _fragMainHome: HomeFragment;
|
lateinit var _fragMainHome: HomeFragment;
|
||||||
lateinit var _fragPostDetail: PostDetailFragment;
|
lateinit var _fragPostDetail: PostDetailFragment;
|
||||||
lateinit var _fragArticleDetail: ArticleDetailFragment;
|
|
||||||
lateinit var _fragWebDetail: WebDetailFragment;
|
|
||||||
lateinit var _fragMainVideoSearchResults: ContentSearchResultsFragment;
|
lateinit var _fragMainVideoSearchResults: ContentSearchResultsFragment;
|
||||||
lateinit var _fragMainCreatorSearchResults: CreatorSearchResultsFragment;
|
lateinit var _fragMainCreatorSearchResults: CreatorSearchResultsFragment;
|
||||||
lateinit var _fragMainPlaylistSearchResults: PlaylistSearchResultsFragment;
|
lateinit var _fragMainPlaylistSearchResults: PlaylistSearchResultsFragment;
|
||||||
@@ -185,7 +161,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
lateinit var _fragMainRemotePlaylist: RemotePlaylistFragment;
|
lateinit var _fragMainRemotePlaylist: RemotePlaylistFragment;
|
||||||
lateinit var _fragWatchlist: WatchLaterFragment;
|
lateinit var _fragWatchlist: WatchLaterFragment;
|
||||||
lateinit var _fragHistory: HistoryFragment;
|
lateinit var _fragHistory: HistoryFragment;
|
||||||
lateinit var _fragShorts: ShortsFragment;
|
|
||||||
lateinit var _fragSourceDetail: SourceDetailFragment;
|
lateinit var _fragSourceDetail: SourceDetailFragment;
|
||||||
lateinit var _fragDownloads: DownloadsFragment;
|
lateinit var _fragDownloads: DownloadsFragment;
|
||||||
lateinit var _fragImportSubscriptions: ImportSubscriptionsFragment;
|
lateinit var _fragImportSubscriptions: ImportSubscriptionsFragment;
|
||||||
@@ -193,17 +168,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
lateinit var _fragBuy: BuyFragment;
|
lateinit var _fragBuy: BuyFragment;
|
||||||
lateinit var _fragSubGroup: SubscriptionGroupFragment;
|
lateinit var _fragSubGroup: SubscriptionGroupFragment;
|
||||||
lateinit var _fragSubGroupList: SubscriptionGroupListFragment;
|
lateinit var _fragSubGroupList: SubscriptionGroupListFragment;
|
||||||
lateinit var _fragLibrary: LibraryFragment;
|
|
||||||
lateinit var _fragLibraryAlbums: LibraryAlbumsFragment;
|
|
||||||
lateinit var _fragLibraryAlbum: LibraryAlbumFragment;
|
|
||||||
lateinit var _fragLibraryArtists: LibraryArtistsFragment;
|
|
||||||
lateinit var _fragLibraryArtist: LibraryArtistFragment;
|
|
||||||
lateinit var _fragLibraryVideos: LibraryVideosFragment;
|
|
||||||
lateinit var _fragLibrarySearch: LibrarySearchFragment;
|
|
||||||
lateinit var _fragLibraryFiles: LibraryFilesFragment;
|
|
||||||
lateinit var _fragSettings: SettingsFragment;
|
|
||||||
lateinit var _fragDeveloper: DeveloperFragment;
|
|
||||||
lateinit var _fragLogin: LoginFragment;
|
|
||||||
|
|
||||||
lateinit var _fragBrowser: BrowserFragment;
|
lateinit var _fragBrowser: BrowserFragment;
|
||||||
|
|
||||||
@@ -211,8 +175,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
lateinit var _fragVideoDetail: VideoDetailFragment;
|
lateinit var _fragVideoDetail: VideoDetailFragment;
|
||||||
|
|
||||||
//State
|
//State
|
||||||
private val _queue: LinkedList<Pair<MainFragment, Any?>> = LinkedList();
|
private val _queue: Queue<Pair<MainFragment, Any?>> = LinkedList();
|
||||||
var fragCurrent: MainFragment? = null; private set;
|
lateinit var fragCurrent: MainFragment private set;
|
||||||
private var _parameterCurrent: Any? = null;
|
private var _parameterCurrent: Any? = null;
|
||||||
|
|
||||||
var fragBeforeOverlay: MainFragment? = null; private set;
|
var fragBeforeOverlay: MainFragment? = null; private set;
|
||||||
@@ -221,10 +185,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
|
|
||||||
private var _isVisible = true;
|
private var _isVisible = true;
|
||||||
private var _wasStopped = false;
|
private var _wasStopped = false;
|
||||||
private var _privateModeEnabled = false
|
|
||||||
private var _pictureInPictureEnabled = false
|
|
||||||
private var _isFullscreen = false
|
|
||||||
private lateinit var _rootInsetsController: RootInsetsController
|
|
||||||
|
|
||||||
private val _urlQrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
private val _urlQrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
|
val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
|
||||||
@@ -236,7 +196,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
lifecycleScope.launch {
|
runBlocking {
|
||||||
handleUrlAll(content)
|
handleUrlAll(content)
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
@@ -246,8 +206,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val mainId = UUID.randomUUID().toString().substring(0, 5)
|
|
||||||
|
|
||||||
constructor() : super() {
|
constructor() : super() {
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
StrictMode.setVmPolicy(
|
StrictMode.setVmPolicy(
|
||||||
@@ -299,29 +257,19 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
Logger.w(TAG, "MainActivity Starting [$mainId]");
|
Logger.i(TAG, "MainActivity Starting");
|
||||||
|
StateApp.instance.setGlobalContext(this, lifecycleScope);
|
||||||
StateApp.instance.setGlobalContext(this, lifecycleScope, mainId);
|
|
||||||
StateApp.instance.mainAppStarting(this);
|
StateApp.instance.mainAppStarting(this);
|
||||||
|
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
||||||
val uiMode = getSystemService(UiModeManager::class.java)
|
|
||||||
uiMode.setApplicationNightMode(UiModeManager.MODE_NIGHT_YES)
|
|
||||||
}
|
|
||||||
setContentView(R.layout.activity_main);
|
setContentView(R.layout.activity_main);
|
||||||
setNavigationBarColorAndIcons();
|
setNavigationBarColorAndIcons();
|
||||||
|
if (Settings.instance.playback.allowVideoToGoUnderCutout)
|
||||||
|
window.attributes.layoutInDisplayCutoutMode =
|
||||||
|
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
||||||
|
|
||||||
runBlocking {
|
runBlocking {
|
||||||
try {
|
StatePlatform.instance.updateAvailableClients(this@MainActivity);
|
||||||
StatePlatform.instance.updateAvailableClients(this@MainActivity);
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.e(TAG, "Unhandled exception in updateAvailableClients", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
||||||
window.isNavigationBarContrastEnforced = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//Preload common files to memory
|
//Preload common files to memory
|
||||||
@@ -329,9 +277,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
FragmentedStorage.get<Settings>();
|
FragmentedStorage.get<Settings>();
|
||||||
|
|
||||||
rootView = findViewById(R.id.rootView);
|
rootView = findViewById(R.id.rootView);
|
||||||
_rootInsetsController = RootInsetsController.attach(this, rootView)
|
|
||||||
_rootInsetsController.setLightSystemBarAppearance(lightStatus = false, lightNav = false)
|
|
||||||
|
|
||||||
_fragContainerTopBar = findViewById(R.id.fragment_top_bar);
|
_fragContainerTopBar = findViewById(R.id.fragment_top_bar);
|
||||||
_fragContainerMain = findViewById(R.id.fragment_main);
|
_fragContainerMain = findViewById(R.id.fragment_main);
|
||||||
_fragContainerBotBar = findViewById(R.id.fragment_bottom_bar);
|
_fragContainerBotBar = findViewById(R.id.fragment_bottom_bar);
|
||||||
@@ -348,7 +293,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
_fragTopBarNavigation = NavigationTopBarFragment.newInstance();
|
_fragTopBarNavigation = NavigationTopBarFragment.newInstance();
|
||||||
_fragTopBarImport = ImportTopBarFragment.newInstance();
|
_fragTopBarImport = ImportTopBarFragment.newInstance();
|
||||||
_fragTopBarAdd = AddTopBarFragment.newInstance();
|
_fragTopBarAdd = AddTopBarFragment.newInstance();
|
||||||
_fragTopBarFiles = FilesTopBarFragment.newInstance();
|
|
||||||
|
|
||||||
//BotBars
|
//BotBars
|
||||||
_fragBotBarMenu = MenuBottomBarFragment.newInstance();
|
_fragBotBarMenu = MenuBottomBarFragment.newInstance();
|
||||||
@@ -369,11 +313,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
_fragMainPlaylist = PlaylistFragment.newInstance();
|
_fragMainPlaylist = PlaylistFragment.newInstance();
|
||||||
_fragMainRemotePlaylist = RemotePlaylistFragment.newInstance();
|
_fragMainRemotePlaylist = RemotePlaylistFragment.newInstance();
|
||||||
_fragPostDetail = PostDetailFragment.newInstance();
|
_fragPostDetail = PostDetailFragment.newInstance();
|
||||||
_fragArticleDetail = ArticleDetailFragment.newInstance();
|
|
||||||
_fragWebDetail = WebDetailFragment.newInstance();
|
|
||||||
_fragWatchlist = WatchLaterFragment.newInstance();
|
_fragWatchlist = WatchLaterFragment.newInstance();
|
||||||
_fragHistory = HistoryFragment.newInstance();
|
_fragHistory = HistoryFragment.newInstance();
|
||||||
_fragShorts = ShortsFragment.newInstance();
|
|
||||||
_fragSourceDetail = SourceDetailFragment.newInstance();
|
_fragSourceDetail = SourceDetailFragment.newInstance();
|
||||||
_fragDownloads = DownloadsFragment();
|
_fragDownloads = DownloadsFragment();
|
||||||
_fragImportSubscriptions = ImportSubscriptionsFragment.newInstance();
|
_fragImportSubscriptions = ImportSubscriptionsFragment.newInstance();
|
||||||
@@ -381,17 +322,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
_fragBuy = BuyFragment.newInstance();
|
_fragBuy = BuyFragment.newInstance();
|
||||||
_fragSubGroup = SubscriptionGroupFragment.newInstance();
|
_fragSubGroup = SubscriptionGroupFragment.newInstance();
|
||||||
_fragSubGroupList = SubscriptionGroupListFragment.newInstance();
|
_fragSubGroupList = SubscriptionGroupListFragment.newInstance();
|
||||||
_fragLibrary = LibraryFragment.newInstance();
|
|
||||||
_fragLibraryAlbums = LibraryAlbumsFragment.newInstance();
|
|
||||||
_fragLibraryAlbum = LibraryAlbumFragment.newInstance();
|
|
||||||
_fragLibraryArtists = LibraryArtistsFragment.newInstance();
|
|
||||||
_fragLibraryArtist = LibraryArtistFragment.newInstance();
|
|
||||||
_fragLibraryVideos = LibraryVideosFragment.newInstance();
|
|
||||||
_fragLibraryFiles = LibraryFilesFragment.newInstance();
|
|
||||||
_fragLibrarySearch = LibrarySearchFragment.newInstance();
|
|
||||||
_fragSettings = SettingsFragment.newInstance();
|
|
||||||
_fragDeveloper = DeveloperFragment.newInstance();
|
|
||||||
_fragLogin = LoginFragment.newInstance();
|
|
||||||
|
|
||||||
_fragBrowser = BrowserFragment.newInstance();
|
_fragBrowser = BrowserFragment.newInstance();
|
||||||
|
|
||||||
@@ -410,17 +340,12 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
updateSegmentPaddings();
|
updateSegmentPaddings();
|
||||||
};
|
};
|
||||||
_fragVideoDetail.onTransitioning.subscribe {
|
_fragVideoDetail.onTransitioning.subscribe {
|
||||||
if (it || _fragVideoDetail.state != VideoDetailFragment.State.MINIMIZED) {
|
if (it || _fragVideoDetail.state != VideoDetailFragment.State.MINIMIZED)
|
||||||
Logger.i(TAG, "onTransition Setting elevation higher");
|
|
||||||
_fragContainerOverlay.elevation =
|
_fragContainerOverlay.elevation =
|
||||||
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 15f, resources.displayMetrics);
|
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 15f, resources.displayMetrics);
|
||||||
}
|
else
|
||||||
else {
|
|
||||||
Logger.i(TAG, "onTransition Setting elevation lower");
|
|
||||||
_fragContainerOverlay.elevation =
|
_fragContainerOverlay.elevation =
|
||||||
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics);
|
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics);
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_fragVideoDetail.onCloseEvent.subscribe {
|
_fragVideoDetail.onCloseEvent.subscribe {
|
||||||
@@ -429,18 +354,22 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
_fragMainSubscriptionsFeed.setPreviewsEnabled(true);
|
_fragMainSubscriptionsFeed.setPreviewsEnabled(true);
|
||||||
_fragContainerVideoDetail.visibility = View.INVISIBLE;
|
_fragContainerVideoDetail.visibility = View.INVISIBLE;
|
||||||
updateSegmentPaddings();
|
updateSegmentPaddings();
|
||||||
updatePrivateModeVisibility()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
_buttonIncognito = findViewById(R.id.incognito_button);
|
_buttonIncognito = findViewById(R.id.incognito_button);
|
||||||
updatePrivateModeVisibility()
|
_buttonIncognito.elevation = -99f;
|
||||||
|
_buttonIncognito.alpha = 0f;
|
||||||
StateApp.instance.privateModeChanged.subscribe {
|
StateApp.instance.privateModeChanged.subscribe {
|
||||||
//Messing with visibility causes some issues with layout ordering?
|
//Messing with visibility causes some issues with layout ordering?
|
||||||
_privateModeEnabled = it
|
if (it) {
|
||||||
updatePrivateModeVisibility()
|
_buttonIncognito.elevation = 99f;
|
||||||
|
_buttonIncognito.alpha = 1f;
|
||||||
|
} else {
|
||||||
|
_buttonIncognito.elevation = -99f;
|
||||||
|
_buttonIncognito.alpha = 0f;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_buttonIncognito.setOnClickListener {
|
_buttonIncognito.setOnClickListener {
|
||||||
if (!StateApp.instance.privateMode)
|
if (!StateApp.instance.privateMode)
|
||||||
return@setOnClickListener;
|
return@setOnClickListener;
|
||||||
@@ -457,23 +386,21 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
};
|
};
|
||||||
_fragVideoDetail.onFullscreenChanged.subscribe {
|
_fragVideoDetail.onFullscreenChanged.subscribe {
|
||||||
Logger.i(TAG, "onFullscreenChanged ${it}");
|
Logger.i(TAG, "onFullscreenChanged ${it}");
|
||||||
_isFullscreen = it
|
|
||||||
updatePrivateModeVisibility()
|
|
||||||
if (it) {
|
if (it) {
|
||||||
_rootInsetsController.enterFullscreen(allowCutoutShortEdges = Settings.instance.playback.allowVideoToGoUnderCutout)
|
_buttonIncognito.elevation = -99f;
|
||||||
|
_buttonIncognito.alpha = 0f;
|
||||||
} else {
|
} else {
|
||||||
_rootInsetsController.exitFullscreen()
|
if (StateApp.instance.privateMode) {
|
||||||
|
_buttonIncognito.elevation = 99f;
|
||||||
|
_buttonIncognito.alpha = 1f;
|
||||||
|
} else {
|
||||||
|
_buttonIncognito.elevation = -99f;
|
||||||
|
_buttonIncognito.alpha = 0f;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_fragVideoDetail.onMinimize.subscribe {
|
|
||||||
updatePrivateModeVisibility()
|
|
||||||
}
|
|
||||||
|
|
||||||
_fragVideoDetail.onMaximized.subscribe {
|
|
||||||
updatePrivateModeVisibility()
|
|
||||||
}
|
|
||||||
|
|
||||||
StatePlayer.instance.also {
|
StatePlayer.instance.also {
|
||||||
it.onQueueChanged.subscribe { shouldSwapCurrentItem ->
|
it.onQueueChanged.subscribe { shouldSwapCurrentItem ->
|
||||||
if (!shouldSwapCurrentItem) {
|
if (!shouldSwapCurrentItem) {
|
||||||
@@ -519,8 +446,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
_fragMainPlaylist.topBar = _fragTopBarNavigation;
|
_fragMainPlaylist.topBar = _fragTopBarNavigation;
|
||||||
_fragMainRemotePlaylist.topBar = _fragTopBarNavigation;
|
_fragMainRemotePlaylist.topBar = _fragTopBarNavigation;
|
||||||
_fragPostDetail.topBar = _fragTopBarNavigation;
|
_fragPostDetail.topBar = _fragTopBarNavigation;
|
||||||
_fragArticleDetail.topBar = _fragTopBarNavigation;
|
|
||||||
_fragWebDetail.topBar = _fragTopBarNavigation;
|
|
||||||
_fragWatchlist.topBar = _fragTopBarNavigation;
|
_fragWatchlist.topBar = _fragTopBarNavigation;
|
||||||
_fragHistory.topBar = _fragTopBarNavigation;
|
_fragHistory.topBar = _fragTopBarNavigation;
|
||||||
_fragSourceDetail.topBar = _fragTopBarNavigation;
|
_fragSourceDetail.topBar = _fragTopBarNavigation;
|
||||||
@@ -528,16 +453,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
_fragImportSubscriptions.topBar = _fragTopBarImport;
|
_fragImportSubscriptions.topBar = _fragTopBarImport;
|
||||||
_fragImportPlaylists.topBar = _fragTopBarImport;
|
_fragImportPlaylists.topBar = _fragTopBarImport;
|
||||||
_fragSubGroupList.topBar = _fragTopBarAdd;
|
_fragSubGroupList.topBar = _fragTopBarAdd;
|
||||||
_fragLibrary.topBar = _fragTopBarGeneral;
|
|
||||||
_fragLibraryAlbums.topBar = _fragTopBarNavigation;
|
|
||||||
_fragLibraryAlbum.topBar = _fragTopBarNavigation;
|
|
||||||
_fragLibraryArtists.topBar = _fragTopBarNavigation;
|
|
||||||
_fragLibraryArtist.topBar = _fragTopBarNavigation;
|
|
||||||
_fragLibraryVideos.topBar = _fragTopBarNavigation;
|
|
||||||
_fragLibraryFiles.topBar = _fragTopBarFiles;
|
|
||||||
_fragLibrarySearch.topBar = _fragTopBarSearch;
|
|
||||||
_fragSettings.topBar = _fragTopBarNavigation;
|
|
||||||
_fragDeveloper.topBar = _fragTopBarNavigation;
|
|
||||||
|
|
||||||
_fragBrowser.topBar = _fragTopBarNavigation;
|
_fragBrowser.topBar = _fragTopBarNavigation;
|
||||||
|
|
||||||
@@ -563,7 +478,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
defaultTab.action(_fragBotBarMenu);
|
defaultTab.action(_fragBotBarMenu);
|
||||||
StateSubscriptions.instance;
|
StateSubscriptions.instance;
|
||||||
|
|
||||||
fragCurrent?.onShown(null, false);
|
fragCurrent.onShown(null, false);
|
||||||
|
|
||||||
//Other stuff
|
//Other stuff
|
||||||
rootView.progress = 0f;
|
rootView.progress = 0f;
|
||||||
@@ -618,10 +533,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
sharedPreferences.edit().putBoolean("IsFirstBoot", false).apply()
|
sharedPreferences.edit().putBoolean("IsFirstBoot", false).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && Settings.instance.autoUpdate.isAutoUpdateEnabled()) {
|
|
||||||
requestNotificationPermissions("Grayjay uses notifications to inform you when a new app update is available.");
|
|
||||||
}
|
|
||||||
|
|
||||||
val submissionStatus = FragmentedStorage.get<StringStorage>("subscriptionSubmissionStatus")
|
val submissionStatus = FragmentedStorage.get<StringStorage>("subscriptionSubmissionStatus")
|
||||||
|
|
||||||
val numSubscriptions = StateSubscriptions.instance.getSubscriptionCount()
|
val numSubscriptions = StateSubscriptions.instance.getSubscriptionCount()
|
||||||
@@ -679,8 +590,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}, UIDialogs.ActionStyle.PRIMARY)
|
}, UIDialogs.ActionStyle.PRIMARY)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
//startActivity(Intent(this, TestActivity::class.java))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -704,23 +613,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
UIDialogs.toast(this, "No external file permissions\nExporting and auto backups will not work");
|
UIDialogs.toast(this, "No external file permissions\nExporting and auto backups will not work");
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
private var _qrCodeLoadingDialog: AlertDialog? = null
|
|
||||||
|
|
||||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
|
||||||
super.onConfigurationChanged(newConfig)
|
|
||||||
_rootInsetsController.onConfigurationChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun showUrlQrCodeScanner() {
|
fun showUrlQrCodeScanner() {
|
||||||
try {
|
try {
|
||||||
_qrCodeLoadingDialog = UIDialogs.showDialog(this, R.drawable.ic_loader_animated, true,
|
|
||||||
"Launching QR scanner",
|
|
||||||
"Make sure your camera is enabled", null, -2,
|
|
||||||
UIDialogs.Action("Close", {
|
|
||||||
_qrCodeLoadingDialog?.dismiss()
|
|
||||||
_qrCodeLoadingDialog = null
|
|
||||||
}));
|
|
||||||
|
|
||||||
val integrator = IntentIntegrator(this)
|
val integrator = IntentIntegrator(this)
|
||||||
integrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
|
integrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
|
||||||
integrator.setPrompt(getString(R.string.scan_a_qr_code))
|
integrator.setPrompt(getString(R.string.scan_a_qr_code))
|
||||||
@@ -736,46 +630,35 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(UnstableApi::class)
|
|
||||||
private fun updatePrivateModeVisibility() {
|
|
||||||
if (_privateModeEnabled && (_fragVideoDetail.state == State.CLOSED || !_pictureInPictureEnabled && !_isFullscreen)) {
|
|
||||||
_buttonIncognito.elevation = 99f;
|
|
||||||
_buttonIncognito.alpha = 1f;
|
|
||||||
_buttonIncognito.translationY = if (_fragVideoDetail.state == State.MINIMIZED) -60.dp(resources).toFloat() else 0f
|
|
||||||
} else {
|
|
||||||
_buttonIncognito.elevation = -99f;
|
|
||||||
_buttonIncognito.alpha = 0f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
Logger.w(TAG, "onResume [$mainId]")
|
Logger.v(TAG, "onResume")
|
||||||
_isVisible = true;
|
_isVisible = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
super.onPause();
|
super.onPause();
|
||||||
Logger.w(TAG, "onPause [$mainId]")
|
Logger.v(TAG, "onPause")
|
||||||
_isVisible = false;
|
_isVisible = false;
|
||||||
|
|
||||||
_qrCodeLoadingDialog?.dismiss()
|
|
||||||
_qrCodeLoadingDialog = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
super.onStop()
|
super.onStop()
|
||||||
Logger.w(TAG, "onStop [$mainId]");
|
Logger.v(TAG, "_wasStopped = true");
|
||||||
_wasStopped = true;
|
_wasStopped = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent) {
|
override fun onNewIntent(intent: Intent?) {
|
||||||
super.onNewIntent(intent);
|
super.onNewIntent(intent);
|
||||||
handleIntent(intent);
|
handleIntent(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleIntent(intent: Intent) {
|
private fun handleIntent(intent: Intent?) {
|
||||||
|
if (intent == null)
|
||||||
|
return;
|
||||||
Logger.i(TAG, "handleIntent started by " + intent.action);
|
Logger.i(TAG, "handleIntent started by " + intent.action);
|
||||||
|
|
||||||
|
|
||||||
var targetData: String? = null;
|
var targetData: String? = null;
|
||||||
|
|
||||||
when (intent.action) {
|
when (intent.action) {
|
||||||
@@ -795,7 +678,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
|
|
||||||
"VIDEO" -> {
|
"VIDEO" -> {
|
||||||
val url = intent.getStringExtra("VIDEO");
|
val url = intent.getStringExtra("VIDEO");
|
||||||
navigateWhenReady(_fragVideoDetail, url);
|
navigate(_fragVideoDetail, url);
|
||||||
}
|
}
|
||||||
|
|
||||||
"IMPORT_OPTIONS" -> {
|
"IMPORT_OPTIONS" -> {
|
||||||
@@ -813,11 +696,11 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
"Sources" -> {
|
"Sources" -> {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
StatePlatform.instance.updateAvailableClients(this@MainActivity, true) //Ideally this is not needed..
|
StatePlatform.instance.updateAvailableClients(this@MainActivity, true) //Ideally this is not needed..
|
||||||
navigateWhenReady(_fragMainSources);
|
navigate(_fragMainSources);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
"BROWSE_PLUGINS" -> {
|
"BROWSE_PLUGINS" -> {
|
||||||
navigateWhenReady(_fragBrowser, BrowserFragment.NavigateOptions("https://plugins.grayjay.app/phone.html", mapOf(
|
navigate(_fragBrowser, BrowserFragment.NavigateOptions("https://plugins.grayjay.app/phone.html", mapOf(
|
||||||
Pair("grayjay") { req ->
|
Pair("grayjay") { req ->
|
||||||
StateApp.instance.contextOrNull?.let {
|
StateApp.instance.contextOrNull?.let {
|
||||||
if (it is MainActivity) {
|
if (it is MainActivity) {
|
||||||
@@ -835,12 +718,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (targetData != null) {
|
if (targetData != null) {
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
runBlocking {
|
||||||
try {
|
handleUrlAll(targetData)
|
||||||
handleUrlAll(targetData, intent)
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.e(TAG, "Unhandled exception in handleUrlAll", e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (ex: Throwable) {
|
} catch (ex: Throwable) {
|
||||||
@@ -848,9 +727,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun handleUrlAll(url: String, openIntent: Intent? = null) {
|
suspend fun handleUrlAll(url: String) {
|
||||||
val uri = Uri.parse(url)
|
val uri = Uri.parse(url)
|
||||||
val intent = openIntent ?: this.intent;
|
|
||||||
when (uri.scheme) {
|
when (uri.scheme) {
|
||||||
"grayjay" -> {
|
"grayjay" -> {
|
||||||
if (url.startsWith("grayjay://license/")) {
|
if (url.startsWith("grayjay://license/")) {
|
||||||
@@ -869,19 +747,19 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
} else if (url.startsWith("grayjay://video/")) {
|
} else if (url.startsWith("grayjay://video/")) {
|
||||||
val videoUrl = url.substring("grayjay://video/".length);
|
val videoUrl = url.substring("grayjay://video/".length);
|
||||||
navigateWhenReady(_fragVideoDetail, videoUrl);
|
navigate(_fragVideoDetail, videoUrl);
|
||||||
} else if (url.startsWith("grayjay://channel/")) {
|
} else if (url.startsWith("grayjay://channel/")) {
|
||||||
val channelUrl = url.substring("grayjay://channel/".length);
|
val channelUrl = url.substring("grayjay://channel/".length);
|
||||||
navigateWhenReady(_fragMainChannel, channelUrl);
|
navigate(_fragMainChannel, channelUrl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"content" -> {
|
"content" -> {
|
||||||
if (!handleContent(url, intent?.type)) {
|
if (!handleContent(url, intent.type)) {
|
||||||
UIDialogs.showSingleButtonDialog(
|
UIDialogs.showSingleButtonDialog(
|
||||||
this,
|
this,
|
||||||
R.drawable.ic_play,
|
R.drawable.ic_play,
|
||||||
getString(R.string.unknown_content_format) + " [${url}]\n[${intent?.type}]",
|
getString(R.string.unknown_content_format) + " [${url}]\n[${intent.type}]",
|
||||||
"Ok",
|
"Ok",
|
||||||
{ });
|
{ });
|
||||||
}
|
}
|
||||||
@@ -938,29 +816,29 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
|
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
Logger.i(TAG, "handleUrl(url=$url) on IO");
|
Logger.i(TAG, "handleUrl(url=$url) on IO");
|
||||||
if (StatePlatform.instance.hasEnabledContentClient(url)) {
|
if (StatePlatform.instance.hasEnabledVideoClient(url)) {
|
||||||
Logger.i(TAG, "handleUrl(url=$url) found video client");
|
Logger.i(TAG, "handleUrl(url=$url) found video client");
|
||||||
withContext(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
if (position > 0)
|
if (position > 0)
|
||||||
navigateWhenReady(_fragVideoDetail, UrlVideoWithTime(url, position.toLong(), true));
|
navigate(_fragVideoDetail, UrlVideoWithTime(url, position.toLong(), true));
|
||||||
else
|
else
|
||||||
navigateWhenReady(_fragVideoDetail, url);
|
navigate(_fragVideoDetail, url);
|
||||||
|
|
||||||
_fragVideoDetail.maximizeVideoDetail(true);
|
_fragVideoDetail.maximizeVideoDetail(true);
|
||||||
}
|
}
|
||||||
return@withContext true;
|
return@withContext true;
|
||||||
} else if (StatePlatform.instance.hasEnabledChannelClient(url)) {
|
} else if (StatePlatform.instance.hasEnabledChannelClient(url)) {
|
||||||
Logger.i(TAG, "handleUrl(url=$url) found channel client");
|
Logger.i(TAG, "handleUrl(url=$url) found channel client");
|
||||||
withContext(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
navigateWhenReady(_fragMainChannel, url);
|
navigate(_fragMainChannel, url);
|
||||||
delay(100);
|
delay(100);
|
||||||
_fragVideoDetail.minimizeVideoDetail();
|
_fragVideoDetail.minimizeVideoDetail();
|
||||||
};
|
};
|
||||||
return@withContext true;
|
return@withContext true;
|
||||||
} else if (StatePlatform.instance.hasEnabledPlaylistClient(url)) {
|
} else if (StatePlatform.instance.hasEnabledPlaylistClient(url)) {
|
||||||
Logger.i(TAG, "handleUrl(url=$url) found playlist client");
|
Logger.i(TAG, "handleUrl(url=$url) found playlist client");
|
||||||
withContext(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
navigateWhenReady(_fragMainRemotePlaylist, url);
|
navigate(_fragMainRemotePlaylist, url);
|
||||||
delay(100);
|
delay(100);
|
||||||
_fragVideoDetail.minimizeVideoDetail();
|
_fragVideoDetail.minimizeVideoDetail();
|
||||||
};
|
};
|
||||||
@@ -1002,12 +880,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
} else if (file.lowercase().endsWith(".txt") || mime == "text/plain") {
|
} else if (file.lowercase().endsWith(".txt") || mime == "text/plain") {
|
||||||
return handleUnknownText(String(data));
|
return handleUnknownText(String(data));
|
||||||
}
|
}
|
||||||
else if (mime?.let { it.startsWith("video/") || it.startsWith("audio/") } ?: false) {
|
|
||||||
val mediaItem = LocalVideoDetails.fromContent(file, mime);
|
|
||||||
navigateWhenReady(_fragVideoDetail, mediaItem);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1122,7 +994,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
Logger.i(TAG, "handleFCast");
|
Logger.i(TAG, "handleFCast");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
StateCasting.instance.handleUrl(url)
|
StateCasting.instance.handleUrl(this, url)
|
||||||
return true;
|
return true;
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Log.e(TAG, "Failed to parse FCast URL '${url}'.", e)
|
Log.e(TAG, "Failed to parse FCast URL '${url}'.", e)
|
||||||
@@ -1154,7 +1026,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
if (_fragVideoDetail.state == VideoDetailFragment.State.MAXIMIZED && _fragVideoDetail.onBackPressed())
|
if (_fragVideoDetail.state == VideoDetailFragment.State.MAXIMIZED && _fragVideoDetail.onBackPressed())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!(fragCurrent?.onBackPressed() ?: true))
|
if (!fragCurrent.onBackPressed())
|
||||||
closeSegment();
|
closeSegment();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1178,38 +1050,18 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
Logger.v(TAG, "onPictureInPictureModeChanged isInPictureInPictureMode=$isInPictureInPictureMode isStop=$isStop")
|
Logger.v(TAG, "onPictureInPictureModeChanged isInPictureInPictureMode=$isInPictureInPictureMode isStop=$isStop")
|
||||||
_fragVideoDetail.onPictureInPictureModeChanged(isInPictureInPictureMode, isStop, newConfig);
|
_fragVideoDetail.onPictureInPictureModeChanged(isInPictureInPictureMode, isStop, newConfig);
|
||||||
Logger.v(TAG, "onPictureInPictureModeChanged Ready");
|
Logger.v(TAG, "onPictureInPictureModeChanged Ready");
|
||||||
|
|
||||||
_pictureInPictureEnabled = isInPictureInPictureMode
|
|
||||||
updatePrivateModeVisibility()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
Logger.w(TAG, "onDestroy [$mainId]")
|
Logger.v(TAG, "onDestroy")
|
||||||
StateApp.instance.mainAppDestroyed(this, mainId);
|
StateApp.instance.mainAppDestroyed(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun <reified T> isFragmentActive(): Boolean {
|
inline fun <reified T> isFragmentActive(): Boolean {
|
||||||
return fragCurrent is T;
|
return fragCurrent is T;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun navigateWhenReady(segment: MainFragment, parameter: Any? = null, withHistory: Boolean = true, isBack: Boolean = false) {
|
|
||||||
if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
|
|
||||||
navigate(segment, parameter, withHistory, isBack)
|
|
||||||
} else {
|
|
||||||
lifecycleScope.launch {
|
|
||||||
lifecycle.withStateAtLeast(Lifecycle.State.RESUMED) {
|
|
||||||
navigate(segment, parameter, withHistory, isBack)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun <reified T : Fragment> navigate(parameter: Any? = null, withHistory: Boolean = true, isBack: Boolean = false) {
|
|
||||||
val segment = getFragment<T>();
|
|
||||||
navigate(segment as MainFragment, parameter, withHistory, isBack);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigate takes a MainFragment, and makes them the current main visible view
|
* Navigate takes a MainFragment, and makes them the current main visible view
|
||||||
* A parameter can be provided which becomes available in the onShow of said fragment
|
* A parameter can be provided which becomes available in the onShow of said fragment
|
||||||
@@ -1232,27 +1084,27 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
fragCurrent?.onHide();
|
fragCurrent.onHide();
|
||||||
|
|
||||||
if (segment.isMainView) {
|
if (segment.isMainView) {
|
||||||
var transaction = supportFragmentManager.beginTransaction();
|
var transaction = supportFragmentManager.beginTransaction();
|
||||||
if (segment.topBar != null) {
|
if (segment.topBar != null) {
|
||||||
if (segment.topBar != fragCurrent?.topBar) {
|
if (segment.topBar != fragCurrent.topBar) {
|
||||||
transaction = transaction
|
transaction = transaction
|
||||||
.show(segment.topBar as Fragment)
|
.show(segment.topBar as Fragment)
|
||||||
.replace(R.id.fragment_top_bar, segment.topBar as Fragment);
|
.replace(R.id.fragment_top_bar, segment.topBar as Fragment);
|
||||||
fragCurrent?.topBar?.onHide();
|
fragCurrent.topBar?.onHide();
|
||||||
}
|
}
|
||||||
} else if (fragCurrent?.topBar != null)
|
} else if (fragCurrent.topBar != null)
|
||||||
transaction.hide(fragCurrent?.topBar as Fragment);
|
transaction.hide(fragCurrent.topBar as Fragment);
|
||||||
|
|
||||||
transaction = transaction.replace(R.id.fragment_main, segment);
|
transaction = transaction.replace(R.id.fragment_main, segment);
|
||||||
|
|
||||||
if (segment.hasBottomBar) {
|
if (segment.hasBottomBar) {
|
||||||
if (!(fragCurrent?.hasBottomBar ?: false))
|
if (!fragCurrent.hasBottomBar)
|
||||||
transaction = transaction.show(_fragBotBarMenu);
|
transaction = transaction.show(_fragBotBarMenu);
|
||||||
} else {
|
} else {
|
||||||
if (fragCurrent?.hasBottomBar ?: false)
|
if (fragCurrent.hasBottomBar)
|
||||||
transaction = transaction.hide(_fragBotBarMenu);
|
transaction = transaction.hide(_fragBotBarMenu);
|
||||||
}
|
}
|
||||||
transaction.commitNow();
|
transaction.commitNow();
|
||||||
@@ -1265,12 +1117,13 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fragCurrent?.isHistory ?: false && withHistory && _queue.lastOrNull() != fragCurrent)
|
if (fragCurrent.isHistory && withHistory && _queue.lastOrNull() != fragCurrent)
|
||||||
_queue.add(Pair(fragCurrent!!, _parameterCurrent));
|
_queue.add(Pair(fragCurrent, _parameterCurrent));
|
||||||
|
|
||||||
if (segment.isOverlay && !(fragCurrent?.isOverlay ?: false) && withHistory)// && fragCurrent.isHistory)
|
if (segment.isOverlay && !fragCurrent.isOverlay && withHistory)// && fragCurrent.isHistory)
|
||||||
fragBeforeOverlay = fragCurrent;
|
fragBeforeOverlay = fragCurrent;
|
||||||
|
|
||||||
|
|
||||||
fragCurrent = segment;
|
fragCurrent = segment;
|
||||||
_parameterCurrent = parameter;
|
_parameterCurrent = parameter;
|
||||||
}
|
}
|
||||||
@@ -1301,21 +1154,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
if (_fragVideoDetail.state == VideoDetailFragment.State.CLOSED) {
|
if (_fragVideoDetail.state == VideoDetailFragment.State.CLOSED) {
|
||||||
finish();
|
finish();
|
||||||
} else {
|
} else {
|
||||||
//UIDialogs.toast("Grayjay continues in background because of an open video.")
|
|
||||||
if(Settings.instance.playback.isBackgroundPictureInPicture()) {
|
|
||||||
try {
|
|
||||||
_fragVideoDetail._viewDetail?.startPictureInPicture();
|
|
||||||
_fragVideoDetail?.forcePictureInPicture();
|
|
||||||
} catch (ex: Throwable) {
|
|
||||||
} //Fail silently
|
|
||||||
}
|
|
||||||
else
|
|
||||||
moveTaskToBack(false);
|
|
||||||
/*
|
|
||||||
UIDialogs.showConfirmationDialog(this, "There is a video playing, are you sure you want to exit the app?", {
|
UIDialogs.showConfirmationDialog(this, "There is a video playing, are you sure you want to exit the app?", {
|
||||||
finish();
|
finish();
|
||||||
})
|
})
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1334,7 +1175,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
VideoDetailFragment::class -> _fragVideoDetail as T;
|
VideoDetailFragment::class -> _fragVideoDetail as T;
|
||||||
MenuBottomBarFragment::class -> _fragBotBarMenu as T;
|
MenuBottomBarFragment::class -> _fragBotBarMenu as T;
|
||||||
GeneralTopBarFragment::class -> _fragTopBarGeneral as T;
|
GeneralTopBarFragment::class -> _fragTopBarGeneral as T;
|
||||||
FilesTopBarFragment::class -> _fragTopBarFiles as T;
|
|
||||||
SearchTopBarFragment::class -> _fragTopBarSearch as T;
|
SearchTopBarFragment::class -> _fragTopBarSearch as T;
|
||||||
CreatorsFragment::class -> _fragMainSubscriptions as T;
|
CreatorsFragment::class -> _fragMainSubscriptions as T;
|
||||||
CommentsFragment::class -> _fragMainComments as T;
|
CommentsFragment::class -> _fragMainComments as T;
|
||||||
@@ -1346,11 +1186,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
PlaylistFragment::class -> _fragMainPlaylist as T;
|
PlaylistFragment::class -> _fragMainPlaylist as T;
|
||||||
RemotePlaylistFragment::class -> _fragMainRemotePlaylist as T;
|
RemotePlaylistFragment::class -> _fragMainRemotePlaylist as T;
|
||||||
PostDetailFragment::class -> _fragPostDetail as T;
|
PostDetailFragment::class -> _fragPostDetail as T;
|
||||||
ArticleDetailFragment::class -> _fragArticleDetail as T;
|
|
||||||
WebDetailFragment::class -> _fragWebDetail as T;
|
|
||||||
WatchLaterFragment::class -> _fragWatchlist as T;
|
WatchLaterFragment::class -> _fragWatchlist as T;
|
||||||
HistoryFragment::class -> _fragHistory as T;
|
HistoryFragment::class -> _fragHistory as T;
|
||||||
ShortsFragment::class -> _fragShorts as T;
|
|
||||||
SourceDetailFragment::class -> _fragSourceDetail as T;
|
SourceDetailFragment::class -> _fragSourceDetail as T;
|
||||||
DownloadsFragment::class -> _fragDownloads as T;
|
DownloadsFragment::class -> _fragDownloads as T;
|
||||||
ImportSubscriptionsFragment::class -> _fragImportSubscriptions as T;
|
ImportSubscriptionsFragment::class -> _fragImportSubscriptions as T;
|
||||||
@@ -1359,17 +1196,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
BuyFragment::class -> _fragBuy as T;
|
BuyFragment::class -> _fragBuy as T;
|
||||||
SubscriptionGroupFragment::class -> _fragSubGroup as T;
|
SubscriptionGroupFragment::class -> _fragSubGroup as T;
|
||||||
SubscriptionGroupListFragment::class -> _fragSubGroupList as T;
|
SubscriptionGroupListFragment::class -> _fragSubGroupList as T;
|
||||||
LibraryFragment::class -> _fragLibrary as T;
|
|
||||||
LibraryAlbumsFragment::class -> _fragLibraryAlbums as T;
|
|
||||||
LibraryAlbumFragment::class -> _fragLibraryAlbum as T;
|
|
||||||
LibraryArtistsFragment::class -> _fragLibraryArtists as T;
|
|
||||||
LibraryArtistFragment::class -> _fragLibraryArtist as T;
|
|
||||||
LibraryVideosFragment::class -> _fragLibraryVideos as T;
|
|
||||||
LibraryFilesFragment::class -> _fragLibraryFiles as T;
|
|
||||||
LibrarySearchFragment::class -> _fragLibrarySearch as T;
|
|
||||||
SettingsFragment:: class -> _fragSettings as T;
|
|
||||||
DeveloperFragment::class -> _fragDeveloper as T;
|
|
||||||
LoginFragment::class -> _fragLogin as T;
|
|
||||||
else -> throw IllegalArgumentException("Fragment type ${T::class.java.name} is not available in MainActivity");
|
else -> throw IllegalArgumentException("Fragment type ${T::class.java.name} is not available in MainActivity");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1377,7 +1203,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
|
|
||||||
private fun updateSegmentPaddings() {
|
private fun updateSegmentPaddings() {
|
||||||
var paddingBottom = 0f;
|
var paddingBottom = 0f;
|
||||||
if (fragCurrent?.hasBottomBar ?: false)
|
if (fragCurrent.hasBottomBar)
|
||||||
paddingBottom += HEIGHT_MENU_DP;
|
paddingBottom += HEIGHT_MENU_DP;
|
||||||
|
|
||||||
_fragContainerOverlay.setPadding(
|
_fragContainerOverlay.setPadding(
|
||||||
@@ -1394,23 +1220,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
var _callbackPermissionAudio: ((Boolean)->Unit)? = null;
|
|
||||||
var _callbackPermissionVideo: ((Boolean)->Unit)? = null;
|
|
||||||
val permissionReqAudio = registerForActivityResult(ActivityResultContracts.RequestPermission(), { isGranted ->
|
|
||||||
_callbackPermissionAudio?.invoke(isGranted);
|
|
||||||
});
|
|
||||||
val permissionReqVideo = registerForActivityResult(ActivityResultContracts.RequestPermission(), { isGranted ->
|
|
||||||
_callbackPermissionVideo?.invoke(isGranted);
|
|
||||||
});
|
|
||||||
fun requestPermissionAudio(cb: ((Boolean)->Unit)? = null) {
|
|
||||||
_callbackPermissionAudio = cb;
|
|
||||||
permissionReqAudio.launch(android.Manifest.permission.READ_MEDIA_AUDIO);
|
|
||||||
}
|
|
||||||
fun requestPermissionVideo(cb: ((Boolean)->Unit)? = null) {
|
|
||||||
_callbackPermissionVideo = cb;
|
|
||||||
permissionReqVideo.launch(android.Manifest.permission.READ_MEDIA_VIDEO);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
val notifPermission = "android.permission.POST_NOTIFICATIONS";
|
val notifPermission = "android.permission.POST_NOTIFICATIONS";
|
||||||
val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
|
val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
|
||||||
|
|||||||
+16
-113
@@ -13,18 +13,13 @@ import android.view.View
|
|||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.UIDialogs
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StateApp.Companion.withContext
|
|
||||||
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.activities.QRCodeFullscreenActivity
|
|
||||||
import com.futo.polycentric.core.ContentType
|
import com.futo.polycentric.core.ContentType
|
||||||
import com.futo.polycentric.core.SignedEvent
|
import com.futo.polycentric.core.SignedEvent
|
||||||
import com.futo.polycentric.core.StorageTypeCRDTItem
|
import com.futo.polycentric.core.StorageTypeCRDTItem
|
||||||
@@ -32,13 +27,8 @@ import com.futo.polycentric.core.StorageTypeCRDTSetItem
|
|||||||
import com.futo.polycentric.core.Store
|
import com.futo.polycentric.core.Store
|
||||||
import com.futo.polycentric.core.toBase64Url
|
import com.futo.polycentric.core.toBase64Url
|
||||||
import com.google.zxing.BarcodeFormat
|
import com.google.zxing.BarcodeFormat
|
||||||
import com.google.zxing.EncodeHintType
|
|
||||||
import com.google.zxing.MultiFormatWriter
|
import com.google.zxing.MultiFormatWriter
|
||||||
import com.google.zxing.common.BitMatrix
|
import com.google.zxing.common.BitMatrix
|
||||||
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import userpackage.Protocol
|
import userpackage.Protocol
|
||||||
import userpackage.Protocol.ExportBundle
|
import userpackage.Protocol.ExportBundle
|
||||||
import userpackage.Protocol.URLInfo
|
import userpackage.Protocol.URLInfo
|
||||||
@@ -46,26 +36,9 @@ import userpackage.Protocol.URLInfo
|
|||||||
class PolycentricBackupActivity : AppCompatActivity() {
|
class PolycentricBackupActivity : AppCompatActivity() {
|
||||||
private lateinit var _buttonShare: BigButton;
|
private lateinit var _buttonShare: BigButton;
|
||||||
private lateinit var _buttonCopy: BigButton;
|
private lateinit var _buttonCopy: BigButton;
|
||||||
private lateinit var _buttonExportFile: BigButton;
|
|
||||||
private lateinit var _imageQR: ImageView;
|
private lateinit var _imageQR: ImageView;
|
||||||
private lateinit var _exportBundle: String;
|
private lateinit var _exportBundle: String;
|
||||||
private lateinit var _textQR: TextView;
|
private lateinit var _textQR: TextView;
|
||||||
private lateinit var _textQRHint: TextView;
|
|
||||||
private lateinit var _loader: View
|
|
||||||
|
|
||||||
private val _createDocumentLauncher = registerForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { uri ->
|
|
||||||
uri?.let { fileUri ->
|
|
||||||
try {
|
|
||||||
contentResolver.openOutputStream(fileUri)?.use { outputStream ->
|
|
||||||
outputStream.write(_exportBundle.toByteArray())
|
|
||||||
}
|
|
||||||
UIDialogs.toast(this, getString(R.string.profile_saved_successfully))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Logger.e(TAG, "Failed to write to document", e)
|
|
||||||
UIDialogs.toast(this, "Failed to save profile: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun attachBaseContext(newBase: Context?) {
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
||||||
@@ -76,75 +49,24 @@ class PolycentricBackupActivity : AppCompatActivity() {
|
|||||||
setContentView(R.layout.activity_polycentric_backup);
|
setContentView(R.layout.activity_polycentric_backup);
|
||||||
setNavigationBarColorAndIcons();
|
setNavigationBarColorAndIcons();
|
||||||
|
|
||||||
_buttonShare = findViewById(R.id.button_share)
|
_buttonShare = findViewById(R.id.button_share);
|
||||||
_buttonCopy = findViewById(R.id.button_copy)
|
_buttonCopy = findViewById(R.id.button_copy);
|
||||||
_buttonExportFile = findViewById(R.id.button_export_file)
|
_imageQR = findViewById(R.id.image_qr);
|
||||||
_imageQR = findViewById(R.id.image_qr)
|
_textQR = findViewById(R.id.text_qr);
|
||||||
_textQR = findViewById(R.id.text_qr)
|
|
||||||
_textQRHint = findViewById(R.id.text_qr_hint)
|
|
||||||
_loader = findViewById(R.id.progress_loader)
|
|
||||||
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
|
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
|
||||||
finish();
|
finish();
|
||||||
};
|
};
|
||||||
|
|
||||||
_imageQR.visibility = View.INVISIBLE
|
_exportBundle = createExportBundle();
|
||||||
_textQR.visibility = View.INVISIBLE
|
|
||||||
_textQRHint.visibility = View.INVISIBLE
|
|
||||||
_loader.visibility = View.VISIBLE
|
|
||||||
_buttonShare.visibility = View.INVISIBLE
|
|
||||||
_buttonCopy.visibility = View.INVISIBLE
|
|
||||||
_buttonExportFile.visibility = View.INVISIBLE
|
|
||||||
|
|
||||||
lifecycleScope.launch {
|
try {
|
||||||
val bundle = withContext(Dispatchers.IO) { createExportBundle() }
|
val dimension = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 200f, resources.displayMetrics).toInt();
|
||||||
_exportBundle = bundle
|
val qrCodeBitmap = generateQRCode(_exportBundle, dimension, dimension);
|
||||||
Logger.i(TAG, "Export bundle created, length: ${bundle.length}")
|
_imageQR.setImageBitmap(qrCodeBitmap);
|
||||||
|
} catch (e: Exception) {
|
||||||
try {
|
Logger.e(TAG, getString(R.string.failed_to_generate_qr_code), e);
|
||||||
val pair = withContext(Dispatchers.IO) {
|
_imageQR.visibility = View.INVISIBLE;
|
||||||
if (!isContentSuitableForQRCode(bundle)) {
|
_textQR.visibility = View.INVISIBLE;
|
||||||
throw Exception("Data too big for QR code generation")
|
|
||||||
}
|
|
||||||
|
|
||||||
val dimension = TypedValue.applyDimension(
|
|
||||||
TypedValue.COMPLEX_UNIT_DIP, 200f, resources.displayMetrics
|
|
||||||
).toInt()
|
|
||||||
val qr = generateQRCode(bundle, dimension, dimension)
|
|
||||||
Pair(bundle, qr)
|
|
||||||
}
|
|
||||||
|
|
||||||
_imageQR.setImageBitmap(pair.second)
|
|
||||||
_imageQR.visibility = View.VISIBLE
|
|
||||||
_textQR.visibility = View.VISIBLE
|
|
||||||
_textQRHint.visibility = View.VISIBLE
|
|
||||||
_buttonShare.visibility = View.VISIBLE
|
|
||||||
_buttonCopy.visibility = View.VISIBLE
|
|
||||||
|
|
||||||
_imageQR.setOnClickListener {
|
|
||||||
val intent = QRCodeFullscreenActivity.createIntent(this@PolycentricBackupActivity, _exportBundle)
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
val byteSize = bundle.toByteArray(Charsets.UTF_8).size
|
|
||||||
Logger.e(TAG, "QR code generation failed. Bundle length: ${bundle.length} chars, ${byteSize} bytes, Error: ${e.message}", e)
|
|
||||||
|
|
||||||
if (e.message?.contains("Data too big") == true) {
|
|
||||||
_textQR.text = getString(R.string.qr_code_too_large_use_file_export)
|
|
||||||
_buttonExportFile.visibility = View.VISIBLE
|
|
||||||
} else {
|
|
||||||
_textQR.text = getString(R.string.failed_to_generate_qr_code)
|
|
||||||
}
|
|
||||||
|
|
||||||
_textQR.visibility = View.VISIBLE
|
|
||||||
_textQRHint.visibility = View.INVISIBLE
|
|
||||||
_buttonShare.visibility = View.VISIBLE
|
|
||||||
_buttonCopy.visibility = View.VISIBLE
|
|
||||||
|
|
||||||
// Hide QR image since generation failed
|
|
||||||
_imageQR.visibility = View.INVISIBLE
|
|
||||||
} finally {
|
|
||||||
_loader.visibility = View.GONE
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_buttonShare.onClick.subscribe {
|
_buttonShare.onClick.subscribe {
|
||||||
@@ -157,29 +79,11 @@ class PolycentricBackupActivity : AppCompatActivity() {
|
|||||||
val clip = ClipData.newPlainText(getString(R.string.copied_text), _exportBundle);
|
val clip = ClipData.newPlainText(getString(R.string.copied_text), _exportBundle);
|
||||||
clipboard.setPrimaryClip(clip);
|
clipboard.setPrimaryClip(clip);
|
||||||
};
|
};
|
||||||
|
|
||||||
_buttonExportFile.onClick.subscribe {
|
|
||||||
val fileName = "polycentric_profile_${System.currentTimeMillis()}.txt"
|
|
||||||
_createDocumentLauncher.launch(fileName)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isContentSuitableForQRCode(content: String): Boolean {
|
|
||||||
val bytes = content.toByteArray(Charsets.UTF_8)
|
|
||||||
return bytes.size <= 2300 // QR Code Version 40 with Error Correction Level M can hold ~2331 bytes
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun generateQRCode(content: String, width: Int, height: Int): Bitmap {
|
private fun generateQRCode(content: String, width: Int, height: Int): Bitmap {
|
||||||
if (!isContentSuitableForQRCode(content)) {
|
val bitMatrix = MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height);
|
||||||
throw Exception("Data too big for QR code generation")
|
return bitMatrixToBitmap(bitMatrix);
|
||||||
}
|
|
||||||
|
|
||||||
val hints = java.util.EnumMap<EncodeHintType, Any>(EncodeHintType::class.java)
|
|
||||||
hints[EncodeHintType.ERROR_CORRECTION] = ErrorCorrectionLevel.M
|
|
||||||
hints[EncodeHintType.MARGIN] = 1
|
|
||||||
|
|
||||||
val bitMatrix = MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height, hints)
|
|
||||||
return bitMatrixToBitmap(bitMatrix)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bitMatrixToBitmap(matrix: BitMatrix): Bitmap {
|
private fun bitMatrixToBitmap(matrix: BitMatrix): Bitmap {
|
||||||
@@ -270,8 +174,7 @@ class PolycentricBackupActivity : AppCompatActivity() {
|
|||||||
.setBody(exportBundle.toByteString())
|
.setBody(exportBundle.toByteString())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
val data = urlInfo.toByteArray()
|
return "polycentric://" + urlInfo.toByteArray().toBase64Url()
|
||||||
return "polycentric://" + data.toBase64Url()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
+61
-133
@@ -32,166 +32,100 @@ import userpackage.Protocol
|
|||||||
import userpackage.Protocol.ExportBundle
|
import userpackage.Protocol.ExportBundle
|
||||||
|
|
||||||
class PolycentricImportProfileActivity : AppCompatActivity() {
|
class PolycentricImportProfileActivity : AppCompatActivity() {
|
||||||
private lateinit var _buttonHelp: ImageButton
|
private lateinit var _buttonHelp: ImageButton;
|
||||||
private lateinit var _buttonScanProfile: LinearLayout
|
private lateinit var _buttonScanProfile: LinearLayout;
|
||||||
private lateinit var _buttonImportFile: 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 lateinit var _loaderOverlay: LoaderOverlay
|
|
||||||
|
|
||||||
private val _qrCodeResultLauncher =
|
private val _qrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
|
||||||
val scanResult =
|
scanResult?.let {
|
||||||
IntentIntegrator.parseActivityResult(result.resultCode, result.data)
|
if (it.contents != null) {
|
||||||
scanResult?.let {
|
val scannedUrl = it.contents
|
||||||
if (it.contents != null) {
|
import(scannedUrl)
|
||||||
val scannedUrl = it.contents
|
|
||||||
import(scannedUrl)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val _filePickerLauncher =
|
|
||||||
registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
|
|
||||||
uri?.let { fileUri ->
|
|
||||||
try {
|
|
||||||
// Check file size before reading
|
|
||||||
val fileSize =
|
|
||||||
contentResolver.openFileDescriptor(fileUri, "r")?.statSize ?: 0
|
|
||||||
val maxFileSize = 10 * 1024 * 1024 // 10MB limit
|
|
||||||
|
|
||||||
if (fileSize > maxFileSize) {
|
|
||||||
UIDialogs.toast(this, "File too large. Maximum size is 10MB.")
|
|
||||||
return@let
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fileSize == 0L) {
|
|
||||||
UIDialogs.toast(this, "Selected file is empty.")
|
|
||||||
return@let
|
|
||||||
}
|
|
||||||
|
|
||||||
val content =
|
|
||||||
contentResolver
|
|
||||||
.openInputStream(fileUri)
|
|
||||||
?.bufferedReader()
|
|
||||||
?.readText()
|
|
||||||
content?.let { fileContent ->
|
|
||||||
val trimmedContent = fileContent.trim()
|
|
||||||
|
|
||||||
// Check if content is empty after trimming
|
|
||||||
if (trimmedContent.isEmpty()) {
|
|
||||||
UIDialogs.toast(this, "Selected file contains no data.")
|
|
||||||
return@let
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if content looks like a valid polycentric URL
|
|
||||||
if (!trimmedContent.startsWith("polycentric://")) {
|
|
||||||
UIDialogs.toast(
|
|
||||||
this,
|
|
||||||
"Selected file does not contain a valid polycentric profile URL."
|
|
||||||
)
|
|
||||||
return@let
|
|
||||||
}
|
|
||||||
|
|
||||||
import(trimmedContent)
|
|
||||||
}
|
|
||||||
?: run { UIDialogs.toast(this, "Could not read file content.") }
|
|
||||||
} catch (e: SecurityException) {
|
|
||||||
Logger.e(TAG, "Security exception reading file", e)
|
|
||||||
UIDialogs.toast(this, "Permission denied to read file.")
|
|
||||||
} catch (e: OutOfMemoryError) {
|
|
||||||
Logger.e(TAG, "Out of memory reading file", e)
|
|
||||||
UIDialogs.toast(this, "File too large to process.")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Logger.e(TAG, "Failed to read file", e)
|
|
||||||
UIDialogs.toast(this, "Failed to read file: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun attachBaseContext(newBase: Context?) {
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.activity_polycentric_import_profile)
|
setContentView(R.layout.activity_polycentric_import_profile);
|
||||||
setNavigationBarColorAndIcons()
|
setNavigationBarColorAndIcons();
|
||||||
|
|
||||||
_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);
|
||||||
_buttonImportFile = findViewById(R.id.button_import_file)
|
_buttonImportProfile = findViewById(R.id.button_import_profile);
|
||||||
_buttonImportProfile = findViewById(R.id.button_import_profile)
|
_loaderOverlay = findViewById(R.id.loader_overlay);
|
||||||
_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();
|
||||||
|
};
|
||||||
|
|
||||||
_buttonHelp.setOnClickListener {
|
_buttonHelp.setOnClickListener {
|
||||||
startActivity(Intent(this, PolycentricWhyActivity::class.java))
|
startActivity(Intent(this, PolycentricWhyActivity::class.java));
|
||||||
}
|
};
|
||||||
|
|
||||||
_buttonScanProfile.setOnClickListener {
|
_buttonScanProfile.setOnClickListener {
|
||||||
val integrator = IntentIntegrator(this)
|
val integrator = IntentIntegrator(this)
|
||||||
integrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
|
integrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
|
||||||
integrator.setPrompt(getString(R.string.scan_a_qr_code))
|
integrator.setPrompt(getString(R.string.scan_a_qr_code))
|
||||||
integrator.setOrientationLocked(true)
|
integrator.setOrientationLocked(true);
|
||||||
integrator.setCameraId(0)
|
integrator.setCameraId(0)
|
||||||
integrator.setBeepEnabled(false)
|
integrator.setBeepEnabled(false)
|
||||||
integrator.setBarcodeImageEnabled(true)
|
integrator.setBarcodeImageEnabled(true)
|
||||||
integrator.setCaptureActivity(QRCaptureActivity::class.java)
|
integrator.setCaptureActivity(QRCaptureActivity::class.java);
|
||||||
_qrCodeResultLauncher.launch(integrator.createScanIntent())
|
_qrCodeResultLauncher.launch(integrator.createScanIntent())
|
||||||
}
|
};
|
||||||
|
|
||||||
_buttonImportFile.setOnClickListener { _filePickerLauncher.launch("text/plain") }
|
|
||||||
|
|
||||||
_buttonImportProfile.setOnClickListener {
|
_buttonImportProfile.setOnClickListener {
|
||||||
if (_editProfile.text.isEmpty()) {
|
if (_editProfile.text.isEmpty()) {
|
||||||
UIDialogs.toast(this, getString(R.string.text_field_does_not_contain_any_data))
|
UIDialogs.toast(this, getString(R.string.text_field_does_not_contain_any_data));
|
||||||
return@setOnClickListener
|
return@setOnClickListener;
|
||||||
}
|
}
|
||||||
|
|
||||||
import(_editProfile.text.toString())
|
import(_editProfile.text.toString());
|
||||||
}
|
};
|
||||||
|
|
||||||
val url = intent.getStringExtra("url")
|
val url = intent.getStringExtra("url");
|
||||||
if (url != null) {
|
if (url != null) {
|
||||||
import(url)
|
import(url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun import(url: String) {
|
private fun import(url: String) {
|
||||||
if (!url.startsWith("polycentric://")) {
|
if (!url.startsWith("polycentric://")) {
|
||||||
UIDialogs.toast(this, getString(R.string.not_a_valid_url))
|
UIDialogs.toast(this, getString(R.string.not_a_valid_url));
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_loaderOverlay.show()
|
_loaderOverlay.show()
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val data = url.substring("polycentric://".length).base64UrlToByteArray()
|
val data = url.substring("polycentric://".length).base64UrlToByteArray();
|
||||||
val urlInfo = Protocol.URLInfo.parseFrom(data)
|
val urlInfo = Protocol.URLInfo.parseFrom(data);
|
||||||
|
|
||||||
if (urlInfo.urlType != 3L) {
|
if (urlInfo.urlType != 3L) {
|
||||||
throw Exception("Expected urlInfo struct of type ExportBundle")
|
throw Exception("Expected urlInfo struct of type ExportBundle")
|
||||||
}
|
}
|
||||||
|
|
||||||
val exportBundle = ExportBundle.parseFrom(urlInfo.body)
|
val exportBundle = ExportBundle.parseFrom(urlInfo.body);
|
||||||
val keyPair = KeyPair.fromProto(exportBundle.keyPair)
|
val keyPair = KeyPair.fromProto(exportBundle.keyPair);
|
||||||
|
|
||||||
val existingProcessSecret = Store.instance.getProcessSecret(keyPair.publicKey)
|
val existingProcessSecret = Store.instance.getProcessSecret(keyPair.publicKey);
|
||||||
if (existingProcessSecret != null) {
|
if (existingProcessSecret != null) {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
UIDialogs.toast(
|
UIDialogs.toast(this@PolycentricImportProfileActivity, getString(R.string.this_profile_is_already_imported));
|
||||||
this@PolycentricImportProfileActivity,
|
|
||||||
getString(R.string.this_profile_is_already_imported)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return@launch
|
return@launch;
|
||||||
}
|
}
|
||||||
|
|
||||||
val processSecret = ProcessSecret(keyPair, Process.random())
|
val processSecret = ProcessSecret(keyPair, Process.random());
|
||||||
Store.instance.addProcessSecret(processSecret)
|
Store.instance.addProcessSecret(processSecret);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
PolycentricStorage.instance.addProcessSecret(processSecret)
|
PolycentricStorage.instance.addProcessSecret(processSecret)
|
||||||
@@ -199,43 +133,37 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
|||||||
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
|
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
val processHandle = processSecret.toProcessHandle()
|
val processHandle = processSecret.toProcessHandle();
|
||||||
|
|
||||||
for (e in exportBundle.events.eventsList) {
|
for (e in exportBundle.events.eventsList) {
|
||||||
try {
|
try {
|
||||||
val se = SignedEvent.fromProto(e)
|
val se = SignedEvent.fromProto(e);
|
||||||
Store.instance.putSignedEvent(se)
|
Store.instance.putSignedEvent(se);
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.w(TAG, "Ignored invalid event", e)
|
Logger.w(TAG, "Ignored invalid event", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StatePolycentric.instance.setProcessHandle(processHandle)
|
StatePolycentric.instance.setProcessHandle(processHandle);
|
||||||
processHandle.fullyBackfillClient(ApiMethods.SERVER)
|
processHandle.fullyBackfillClient(ApiMethods.SERVER);
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
startActivity(
|
startActivity(Intent(this@PolycentricImportProfileActivity, PolycentricProfileActivity::class.java));
|
||||||
Intent(
|
finish();
|
||||||
this@PolycentricImportProfileActivity,
|
|
||||||
PolycentricProfileActivity::class.java
|
|
||||||
)
|
|
||||||
)
|
|
||||||
finish()
|
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.w(TAG, "Failed to import profile", e)
|
Logger.w(TAG, "Failed to import profile", e);
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
UIDialogs.toast(
|
UIDialogs.toast(this@PolycentricImportProfileActivity, getString(R.string.failed_to_import_profile) + " '${e.message}'");
|
||||||
this@PolycentricImportProfileActivity,
|
|
||||||
getString(R.string.failed_to_import_profile) + " '${e.message}'"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
withContext(Dispatchers.Main) { _loaderOverlay.hide() }
|
withContext(Dispatchers.Main) {
|
||||||
|
_loaderOverlay.hide();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "PolycentricImportProfileActivity"
|
private const val TAG = "PolycentricImportProfileActivity";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
-147
@@ -1,147 +0,0 @@
|
|||||||
package com.futo.platformplayer.activities
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.widget.ImageButton
|
|
||||||
import android.widget.SeekBar
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import com.futo.platformplayer.polycentric.ModerationsManager
|
|
||||||
import com.futo.platformplayer.R
|
|
||||||
import com.futo.platformplayer.states.StateApp
|
|
||||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
|
||||||
|
|
||||||
class PolycentricModerationActivity : AppCompatActivity() {
|
|
||||||
private lateinit var _seekbarOffensive: SeekBar
|
|
||||||
private lateinit var _seekbarExplicit: SeekBar
|
|
||||||
private lateinit var _seekbarViolence: SeekBar
|
|
||||||
private lateinit var _textOffensiveDesc: TextView
|
|
||||||
private lateinit var _textExplicitDesc: TextView
|
|
||||||
private lateinit var _textViolenceDesc: TextView
|
|
||||||
private lateinit var _textOffensiveValue: TextView
|
|
||||||
private lateinit var _textExplicitValue: TextView
|
|
||||||
private lateinit var _textViolenceValue: TextView
|
|
||||||
private lateinit var _moderationsManager: ModerationsManager
|
|
||||||
|
|
||||||
override fun attachBaseContext(newBase: Context?) {
|
|
||||||
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setContentView(R.layout.activity_polycentric_moderation)
|
|
||||||
setNavigationBarColorAndIcons()
|
|
||||||
|
|
||||||
_moderationsManager = ModerationsManager.getInstance()
|
|
||||||
try {
|
|
||||||
_moderationsManager = ModerationsManager.getInstance()
|
|
||||||
} catch (e: IllegalStateException) {
|
|
||||||
finish()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_seekbarOffensive = findViewById(R.id.seekbar_offensive)
|
|
||||||
_seekbarExplicit = findViewById(R.id.seekbar_explicit)
|
|
||||||
_seekbarViolence = findViewById(R.id.seekbar_violence)
|
|
||||||
_textOffensiveDesc = findViewById(R.id.text_offensive_desc)
|
|
||||||
_textExplicitDesc = findViewById(R.id.text_explicit_desc)
|
|
||||||
_textViolenceDesc = findViewById(R.id.text_violence_desc)
|
|
||||||
_textOffensiveValue = findViewById(R.id.text_offensive_value)
|
|
||||||
_textExplicitValue = findViewById(R.id.text_explicit_value)
|
|
||||||
_textViolenceValue = findViewById(R.id.text_violence_value)
|
|
||||||
|
|
||||||
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
loadSettings()
|
|
||||||
setupListeners()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadSettings() {
|
|
||||||
val levels = _moderationsManager.moderationLevels.value ?: mapOf()
|
|
||||||
|
|
||||||
val offensiveLevel = levels["hate"] ?: 2
|
|
||||||
val explicitLevel = levels["sexual"] ?: 1
|
|
||||||
val violenceLevel = levels["violence"] ?: 1
|
|
||||||
|
|
||||||
_seekbarOffensive.progress = offensiveLevel
|
|
||||||
_seekbarExplicit.progress = explicitLevel
|
|
||||||
_seekbarViolence.progress = violenceLevel
|
|
||||||
|
|
||||||
updateDescriptionText(_seekbarOffensive, _textOffensiveDesc, _textOffensiveValue, getOffensiveDescriptions())
|
|
||||||
updateDescriptionText(_seekbarExplicit, _textExplicitDesc, _textExplicitValue, getExplicitDescriptions())
|
|
||||||
updateDescriptionText(_seekbarViolence, _textViolenceDesc, _textViolenceValue, getViolenceDescriptions())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupListeners() {
|
|
||||||
_seekbarOffensive.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
|
||||||
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
|
|
||||||
updateDescriptionText(seekBar, _textOffensiveDesc, _textOffensiveValue, getOffensiveDescriptions())
|
|
||||||
if (fromUser) {
|
|
||||||
_moderationsManager.setModerationLevel("hate", progress)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStartTrackingTouch(seekBar: SeekBar?) {}
|
|
||||||
override fun onStopTrackingTouch(seekBar: SeekBar?) {}
|
|
||||||
})
|
|
||||||
|
|
||||||
_seekbarExplicit.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
|
||||||
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
|
|
||||||
updateDescriptionText(seekBar, _textExplicitDesc, _textExplicitValue, getExplicitDescriptions())
|
|
||||||
if (fromUser) {
|
|
||||||
_moderationsManager.setModerationLevel("sexual", progress)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStartTrackingTouch(seekBar: SeekBar?) {}
|
|
||||||
override fun onStopTrackingTouch(seekBar: SeekBar?) {}
|
|
||||||
})
|
|
||||||
|
|
||||||
_seekbarViolence.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
|
||||||
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
|
|
||||||
updateDescriptionText(seekBar, _textViolenceDesc, _textViolenceValue, getViolenceDescriptions())
|
|
||||||
if (fromUser) {
|
|
||||||
_moderationsManager.setModerationLevel("violence", progress)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStartTrackingTouch(seekBar: SeekBar?) {}
|
|
||||||
override fun onStopTrackingTouch(seekBar: SeekBar?) {}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateDescriptionText(seekBar: SeekBar?, textDesc: TextView, textValue: TextView, descriptions: Array<String>) {
|
|
||||||
val progress = seekBar?.progress ?: 0
|
|
||||||
textDesc.text = descriptions[progress]
|
|
||||||
textValue.text = progress.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getOffensiveDescriptions(): Array<String> {
|
|
||||||
return arrayOf(
|
|
||||||
"Neutral, general terms, no bias or hate.",
|
|
||||||
"Mildly sensitive, factual.",
|
|
||||||
"Potentially offensive content",
|
|
||||||
"Offensive content"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getExplicitDescriptions(): Array<String> {
|
|
||||||
return arrayOf(
|
|
||||||
"No explicit content",
|
|
||||||
"Mildly suggestive, factual or educational",
|
|
||||||
"Moderate sexual content, non-graphic",
|
|
||||||
"Explicit sexual content"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getViolenceDescriptions(): Array<String> {
|
|
||||||
return arrayOf(
|
|
||||||
"Non-violent",
|
|
||||||
"Mild violence, factual or contextual",
|
|
||||||
"Moderate violence, some graphic content.",
|
|
||||||
"Graphic violence"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -49,7 +49,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
|||||||
private lateinit var _buttonHelp: ImageButton;
|
private lateinit var _buttonHelp: ImageButton;
|
||||||
private lateinit var _editName: EditText;
|
private lateinit var _editName: EditText;
|
||||||
private lateinit var _buttonExport: BigButton;
|
private lateinit var _buttonExport: BigButton;
|
||||||
private lateinit var _buttonModeration: BigButton;
|
private lateinit var _buttonOpenHarborProfile: BigButton;
|
||||||
private lateinit var _buttonLogout: BigButton;
|
private lateinit var _buttonLogout: BigButton;
|
||||||
private lateinit var _buttonDelete: BigButton;
|
private lateinit var _buttonDelete: BigButton;
|
||||||
private lateinit var _username: String;
|
private lateinit var _username: String;
|
||||||
@@ -71,7 +71,7 @@ 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);
|
||||||
_buttonModeration = findViewById(R.id.button_moderation);
|
_buttonOpenHarborProfile = findViewById(R.id.button_open_harbor_profile);
|
||||||
_buttonLogout = findViewById(R.id.button_logout);
|
_buttonLogout = findViewById(R.id.button_logout);
|
||||||
_buttonDelete = findViewById(R.id.button_delete);
|
_buttonDelete = findViewById(R.id.button_delete);
|
||||||
_loaderOverlay = findViewById(R.id.loader_overlay);
|
_loaderOverlay = findViewById(R.id.loader_overlay);
|
||||||
@@ -99,9 +99,15 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
|||||||
startActivity(Intent(this, PolycentricBackupActivity::class.java));
|
startActivity(Intent(this, PolycentricBackupActivity::class.java));
|
||||||
};
|
};
|
||||||
|
|
||||||
_buttonModeration.onClick.subscribe {
|
_buttonOpenHarborProfile.onClick.subscribe {
|
||||||
startActivity(Intent(this, PolycentricModerationActivity::class.java));
|
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);
|
||||||
|
|||||||
@@ -1,109 +0,0 @@
|
|||||||
package com.futo.platformplayer.activities
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.TypedValue
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.ImageButton
|
|
||||||
import android.widget.ImageView
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import com.futo.platformplayer.R
|
|
||||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
|
||||||
import com.futo.platformplayer.states.StateApp
|
|
||||||
import com.google.zxing.BarcodeFormat
|
|
||||||
import com.google.zxing.EncodeHintType
|
|
||||||
import com.google.zxing.MultiFormatWriter
|
|
||||||
import com.google.zxing.common.BitMatrix
|
|
||||||
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
|
|
||||||
|
|
||||||
class QRCodeFullscreenActivity : AppCompatActivity() {
|
|
||||||
companion object {
|
|
||||||
private const val EXTRA_QR_TEXT = "qr_text"
|
|
||||||
|
|
||||||
fun createIntent(context: Context, qrText: String): android.content.Intent {
|
|
||||||
return android.content.Intent(context, QRCodeFullscreenActivity::class.java).apply {
|
|
||||||
putExtra(EXTRA_QR_TEXT, qrText)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun attachBaseContext(newBase: Context?) {
|
|
||||||
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setContentView(R.layout.activity_qr_code_fullscreen)
|
|
||||||
setNavigationBarColorAndIcons()
|
|
||||||
|
|
||||||
val qrText = intent.getStringExtra(EXTRA_QR_TEXT)
|
|
||||||
|
|
||||||
val imageQR = findViewById<ImageView>(R.id.image_qr_fullscreen)
|
|
||||||
val buttonBack = findViewById<ImageButton>(R.id.button_back_fullscreen)
|
|
||||||
val buttonClose = findViewById<ImageButton>(R.id.button_close_fullscreen)
|
|
||||||
|
|
||||||
// Generate QR code bitmap from text
|
|
||||||
qrText?.let { text ->
|
|
||||||
try {
|
|
||||||
if (!isContentSuitableForQRCode(text)) {
|
|
||||||
throw Exception("Data too big for QR code generation")
|
|
||||||
}
|
|
||||||
|
|
||||||
val dimension = TypedValue.applyDimension(
|
|
||||||
TypedValue.COMPLEX_UNIT_DIP, 300f, resources.displayMetrics
|
|
||||||
).toInt()
|
|
||||||
val qrBitmap = generateQRCode(text, dimension, dimension)
|
|
||||||
imageQR.setImageBitmap(qrBitmap)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// If QR generation fails, show error or fallback
|
|
||||||
imageQR.setImageResource(R.drawable.ic_qr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buttonBack.setOnClickListener {
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
buttonClose.setOnClickListener {
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
imageQR.setOnClickListener {
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isContentSuitableForQRCode(content: String): Boolean {
|
|
||||||
val bytes = content.toByteArray(Charsets.UTF_8)
|
|
||||||
return bytes.size <= 2300 // QR Code Version 40 with Error Correction Level M can hold ~2331 bytes
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun generateQRCode(content: String, width: Int, height: Int): Bitmap {
|
|
||||||
if (!isContentSuitableForQRCode(content)) {
|
|
||||||
throw Exception("Data too big for QR code generation")
|
|
||||||
}
|
|
||||||
|
|
||||||
val hints = java.util.EnumMap<EncodeHintType, Any>(EncodeHintType::class.java)
|
|
||||||
hints[EncodeHintType.ERROR_CORRECTION] = ErrorCorrectionLevel.M
|
|
||||||
hints[EncodeHintType.MARGIN] = 1
|
|
||||||
|
|
||||||
val bitMatrix = MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height, hints)
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import androidx.activity.result.ActivityResult
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.futo.platformplayer.*
|
||||||
|
import com.futo.platformplayer.constructs.Event0
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.futo.platformplayer.views.LoaderView
|
||||||
|
import com.futo.platformplayer.views.fields.FieldForm
|
||||||
|
import com.futo.platformplayer.views.fields.ReadOnlyTextField
|
||||||
|
import com.google.android.material.button.MaterialButton
|
||||||
|
|
||||||
|
class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
|
||||||
|
private lateinit var _form: FieldForm;
|
||||||
|
private lateinit var _buttonBack: ImageButton;
|
||||||
|
private lateinit var _loaderView: LoaderView;
|
||||||
|
|
||||||
|
private lateinit var _devSets: LinearLayout;
|
||||||
|
private lateinit var _buttonDev: MaterialButton;
|
||||||
|
|
||||||
|
private var _isFinished = false;
|
||||||
|
|
||||||
|
lateinit var overlay: FrameLayout;
|
||||||
|
|
||||||
|
val notifPermission = "android.permission.POST_NOTIFICATIONS";
|
||||||
|
val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
|
||||||
|
if (isGranted)
|
||||||
|
UIDialogs.toast(this, "Notification permission granted");
|
||||||
|
else
|
||||||
|
UIDialogs.toast(this, "Notification permission denied");
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
|
Logger.i("SettingsActivity", "SettingsActivity.attachBaseContext")
|
||||||
|
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
||||||
|
}
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_settings);
|
||||||
|
setNavigationBarColorAndIcons();
|
||||||
|
|
||||||
|
_form = findViewById(R.id.settings_form);
|
||||||
|
_buttonBack = findViewById(R.id.button_back);
|
||||||
|
_buttonDev = findViewById(R.id.button_dev);
|
||||||
|
_devSets = findViewById(R.id.dev_settings);
|
||||||
|
_loaderView = findViewById(R.id.loader);
|
||||||
|
overlay = findViewById(R.id.overlay_container);
|
||||||
|
|
||||||
|
_form.onChanged.subscribe { field, _ ->
|
||||||
|
Logger.i("SettingsActivity", "Setting [${field.field?.name}] changed, saving");
|
||||||
|
_form.setObjectValues();
|
||||||
|
Settings.instance.save();
|
||||||
|
|
||||||
|
if(field.descriptor?.id == "app_language") {
|
||||||
|
Logger.i("SettingsActivity", "App language change detected, propogating to shared preferences");
|
||||||
|
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 {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
_buttonDev.setOnClickListener {
|
||||||
|
startActivity(Intent(this, DeveloperActivity::class.java));
|
||||||
|
}
|
||||||
|
|
||||||
|
_lastActivity = this;
|
||||||
|
|
||||||
|
reloadSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
var isFirstLoad = true;
|
||||||
|
fun reloadSettings() {
|
||||||
|
val firstLoad = isFirstLoad;
|
||||||
|
isFirstLoad = false;
|
||||||
|
_form.setSearchVisible(false);
|
||||||
|
_loaderView.start();
|
||||||
|
_form.fromObject(lifecycleScope, Settings.instance) {
|
||||||
|
_loaderView.stop();
|
||||||
|
_form.setSearchVisible(true);
|
||||||
|
|
||||||
|
var devCounter = 0;
|
||||||
|
_form.findField("code")?.assume<ReadOnlyTextField>()?.setOnClickListener {
|
||||||
|
devCounter++;
|
||||||
|
if(devCounter > 5) {
|
||||||
|
devCounter = 0;
|
||||||
|
SettingsDev.instance.developerMode = true;
|
||||||
|
SettingsDev.instance.save();
|
||||||
|
updateDevMode();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
updateDevMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateDevMode() {
|
||||||
|
if(SettingsDev.instance.developerMode)
|
||||||
|
_devSets.visibility = View.VISIBLE;
|
||||||
|
else
|
||||||
|
_devSets.visibility = View.GONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun finish() {
|
||||||
|
super.finish()
|
||||||
|
_isFinished = true;
|
||||||
|
if(_lastActivity == this)
|
||||||
|
_lastActivity = null;
|
||||||
|
overridePendingTransition(R.anim.slide_lighten, R.anim.slide_out_up)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private var resultLauncherMap = mutableMapOf<Int, (ActivityResult)->Unit>();
|
||||||
|
private var requestCode: Int? = -1;
|
||||||
|
private val resultLauncher: ActivityResultLauncher<Intent> = registerForActivityResult(
|
||||||
|
ActivityResultContracts.StartActivityForResult()) {
|
||||||
|
result: ActivityResult ->
|
||||||
|
val handler = synchronized(resultLauncherMap) {
|
||||||
|
resultLauncherMap.remove(requestCode);
|
||||||
|
}
|
||||||
|
if(handler != null)
|
||||||
|
handler(result);
|
||||||
|
};
|
||||||
|
override fun launchForResult(intent: Intent, code: Int, handler: (ActivityResult)->Unit) {
|
||||||
|
synchronized(resultLauncherMap) {
|
||||||
|
resultLauncherMap[code] = handler;
|
||||||
|
}
|
||||||
|
requestCode = code;
|
||||||
|
resultLauncher.launch(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
settingsActivityClosed.emit()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
//TODO: Temporary for solving Settings issues
|
||||||
|
@SuppressLint("StaticFieldLeak")
|
||||||
|
private var _lastActivity: SettingsActivity? = null;
|
||||||
|
|
||||||
|
val settingsActivityClosed = Event0()
|
||||||
|
|
||||||
|
fun getActivity(): SettingsActivity? {
|
||||||
|
val act = _lastActivity;
|
||||||
|
if(act != null && !act._isFinished)
|
||||||
|
return act;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,8 +9,6 @@ import android.widget.LinearLayout
|
|||||||
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.R
|
||||||
import com.futo.platformplayer.UIDialogs
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
|
||||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StateSync
|
import com.futo.platformplayer.states.StateSync
|
||||||
@@ -31,16 +29,6 @@ class SyncHomeActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
if (StateApp.instance.contextOrNull == null) {
|
|
||||||
Logger.w(TAG, "No main activity, restarting main.")
|
|
||||||
val intent = Intent(this, MainActivity::class.java)
|
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
|
||||||
startActivity(intent)
|
|
||||||
finish()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setContentView(R.layout.activity_sync_home)
|
setContentView(R.layout.activity_sync_home)
|
||||||
setNavigationBarColorAndIcons()
|
setNavigationBarColorAndIcons()
|
||||||
|
|
||||||
@@ -66,6 +54,7 @@ class SyncHomeActivity : AppCompatActivity() {
|
|||||||
val view = _viewMap[publicKey]
|
val view = _viewMap[publicKey]
|
||||||
if (!session.isAuthorized) {
|
if (!session.isAuthorized) {
|
||||||
if (view != null) {
|
if (view != null) {
|
||||||
|
_layoutDevices.removeView(view)
|
||||||
_viewMap.remove(publicKey)
|
_viewMap.remove(publicKey)
|
||||||
}
|
}
|
||||||
return@launch
|
return@launch
|
||||||
@@ -100,20 +89,6 @@ class SyncHomeActivity : AppCompatActivity() {
|
|||||||
updateEmptyVisibility()
|
updateEmptyVisibility()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StateSync.instance.confirmStarted(this, onStarted = {
|
|
||||||
if (StateSync.instance.syncService?.serverSocketFailedToStart == true) {
|
|
||||||
UIDialogs.toast(this, "Server socket failed to start, is the port in use?", true)
|
|
||||||
}
|
|
||||||
if (StateSync.instance.syncService?.relayConnected == false) {
|
|
||||||
UIDialogs.toast(this, "Not connected to relay, remote connections will work.", false)
|
|
||||||
}
|
|
||||||
if (StateSync.instance.syncService?.serverSocketStarted == false) {
|
|
||||||
UIDialogs.toast(this, "Listener not started, local connections will not work.", false)
|
|
||||||
}
|
|
||||||
}, onNotStarted = {
|
|
||||||
finish()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
@@ -125,12 +100,11 @@ class SyncHomeActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private fun updateDeviceView(syncDeviceView: SyncDeviceView, publicKey: String, session: SyncSession?): SyncDeviceView {
|
private fun updateDeviceView(syncDeviceView: SyncDeviceView, publicKey: String, session: SyncSession?): SyncDeviceView {
|
||||||
val connected = session?.connected ?: false
|
val connected = session?.connected ?: false
|
||||||
val authorized = session?.isAuthorized ?: false
|
|
||||||
|
|
||||||
syncDeviceView.setLinkType(session?.linkType ?: LinkType.None)
|
syncDeviceView.setLinkType(session?.linkType ?: LinkType.None)
|
||||||
.setName(session?.displayName ?: StateSync.instance.getCachedName(publicKey) ?: publicKey)
|
.setName(session?.displayName ?: StateSync.instance.getCachedName(publicKey) ?: publicKey)
|
||||||
//TODO: also display public key?
|
//TODO: also display public key?
|
||||||
.setStatus(if (connected && authorized) "Connected" else "Disconnected or unauthorized")
|
.setStatus(if (connected) "Connected" else "Disconnected")
|
||||||
return syncDeviceView
|
return syncDeviceView
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -83,7 +83,6 @@ class SyncPairActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
_layoutPairingSuccess.setOnClickListener {
|
_layoutPairingSuccess.setOnClickListener {
|
||||||
_layoutPairingSuccess.visibility = View.GONE
|
_layoutPairingSuccess.visibility = View.GONE
|
||||||
finish()
|
|
||||||
}
|
}
|
||||||
_layoutPairingError.setOnClickListener {
|
_layoutPairingError.setOnClickListener {
|
||||||
_layoutPairingError.visibility = View.GONE
|
_layoutPairingError.visibility = View.GONE
|
||||||
@@ -110,29 +109,11 @@ class SyncPairActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
var wasCompleted = false
|
StateSync.instance.connect(deviceInfo) { complete, message ->
|
||||||
|
|
||||||
StateSync.instance.syncService?.connect(deviceInfo, true) { complete, message ->
|
|
||||||
if (wasCompleted) {
|
|
||||||
Logger.i(TAG, "onStatusUpdate(complete = ${complete}, message = '${message} ignored because wasCompleted')")
|
|
||||||
return@connect
|
|
||||||
}
|
|
||||||
|
|
||||||
if (complete == true) {
|
|
||||||
wasCompleted = true
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.i(TAG, "onStatusUpdate(complete = ${complete}, message = '${message}')")
|
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
if (complete != null) {
|
if (complete != null && complete) {
|
||||||
if (complete) {
|
_layoutPairingSuccess.visibility = View.VISIBLE
|
||||||
_layoutPairingSuccess.visibility = View.VISIBLE
|
_layoutPairing.visibility = View.GONE
|
||||||
_layoutPairing.visibility = View.GONE
|
|
||||||
} else {
|
|
||||||
_textError.text = message
|
|
||||||
_layoutPairingError.visibility = View.VISIBLE
|
|
||||||
_layoutPairing.visibility = View.GONE
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
_textPairingStatus.text = message
|
_textPairingStatus.text = message
|
||||||
}
|
}
|
||||||
@@ -156,6 +137,8 @@ class SyncPairActivity : AppCompatActivity() {
|
|||||||
_textError.text = e.message
|
_textError.text = e.message
|
||||||
_layoutPairing.visibility = View.GONE
|
_layoutPairing.visibility = View.GONE
|
||||||
Logger.e(TAG, "Failed to pair", e)
|
Logger.e(TAG, "Failed to pair", e)
|
||||||
|
} finally {
|
||||||
|
_layoutPairing.visibility = View.GONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+5
-12
@@ -67,18 +67,11 @@ class SyncShowPairingCodeActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val ips = getIPs()
|
val ips = getIPs()
|
||||||
val publicKey = StateSync.instance.syncService?.publicKey
|
val selfDeviceInfo = SyncDeviceInfo(StateSync.instance.publicKey!!, ips.toTypedArray(), StateSync.PORT, StateSync.instance.pairingCode)
|
||||||
val pairingCode = StateSync.instance.syncService?.pairingCode
|
val json = Json.encodeToString(selfDeviceInfo)
|
||||||
if (publicKey == null || pairingCode == null) {
|
val base64 = Base64.encodeToString(json.toByteArray(), Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP)
|
||||||
setCode("Public key or pairing code was not known, is sync enabled?")
|
val url = "grayjay://sync/${base64}"
|
||||||
} else {
|
setCode(url)
|
||||||
val selfDeviceInfo = SyncDeviceInfo(publicKey, ips.toTypedArray(), StateSync.PORT, 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?) {
|
fun setCode(code: String?) {
|
||||||
|
|||||||
@@ -2,24 +2,12 @@ package com.futo.platformplayer.activities
|
|||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.views.TargetTapLoaderView
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class TestActivity : AppCompatActivity() {
|
class TestActivity : AppCompatActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.activity_test);
|
setContentView(R.layout.activity_test);
|
||||||
|
|
||||||
val view = findViewById<TargetTapLoaderView>(R.id.test_view)
|
|
||||||
view.startLoader(10000)
|
|
||||||
|
|
||||||
lifecycleScope.launch {
|
|
||||||
delay(5000)
|
|
||||||
view.startLoader()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -90,7 +90,6 @@ open class ManagedHttpClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun tryHead(url: String): Map<String, String>? {
|
fun tryHead(url: String): Map<String, String>? {
|
||||||
ensureNotMainThread()
|
|
||||||
try {
|
try {
|
||||||
val result = head(url);
|
val result = head(url);
|
||||||
if(result.isOk)
|
if(result.isOk)
|
||||||
@@ -105,7 +104,7 @@ open class ManagedHttpClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun socket(url: String, headers: MutableMap<String, String> = HashMap(), listener: SocketListener): Socket {
|
fun socket(url: String, headers: MutableMap<String, String> = HashMap(), listener: SocketListener): Socket {
|
||||||
ensureNotMainThread()
|
|
||||||
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.isNotEmpty() && !headers.any { it.key.lowercase() == "user-agent" })
|
||||||
@@ -301,7 +300,6 @@ open class ManagedHttpClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun send(msg: String) {
|
fun send(msg: String) {
|
||||||
ensureNotMainThread()
|
|
||||||
socket.send(msg);
|
socket.send(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
-318
@@ -1,318 +0,0 @@
|
|||||||
package com.futo.platformplayer.api.http.server.handlers
|
|
||||||
|
|
||||||
import android.content.ContentResolver
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
|
||||||
import android.provider.MediaStore
|
|
||||||
import android.provider.OpenableColumns
|
|
||||||
import com.futo.platformplayer.api.http.server.HttpContext
|
|
||||||
import com.futo.platformplayer.api.http.server.HttpHeaders
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
|
||||||
import java.io.FileNotFoundException
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.io.OutputStream
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class HttpContentUriHandler(
|
|
||||||
method: String,
|
|
||||||
path: String,
|
|
||||||
private val contentResolver: ContentResolver,
|
|
||||||
private val uri: Uri,
|
|
||||||
private val explicitContentType: String? = null
|
|
||||||
) : HttpHandler(method, path) {
|
|
||||||
|
|
||||||
override fun handle(httpContext: HttpContext) {
|
|
||||||
val resolver = contentResolver
|
|
||||||
val requestHeaders = httpContext.headers
|
|
||||||
val responseHeaders = this.headers.clone()
|
|
||||||
|
|
||||||
val meta = try {
|
|
||||||
queryMetadata(resolver, uri)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Logger.e(TAG, "Failed to query metadata for $uri", e)
|
|
||||||
httpContext.respondCode(404, responseHeaders)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val contentType = explicitContentType
|
|
||||||
?: resolver.getType(uri)
|
|
||||||
?: "application/octet-stream"
|
|
||||||
responseHeaders["Content-Type"] = contentType
|
|
||||||
|
|
||||||
meta.lastModifiedMillis?.let { lastModified ->
|
|
||||||
responseHeaders["Last-Modified"] = httpDateFormat.format(Date(lastModified))
|
|
||||||
|
|
||||||
val ifModifiedSinceHeader = requestHeaders["If-Modified-Since"]
|
|
||||||
if (ifModifiedSinceHeader != null) {
|
|
||||||
val ifModifiedSince = try {
|
|
||||||
httpDateFormat.parse(ifModifiedSinceHeader)
|
|
||||||
} catch (_: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ifModifiedSince != null && lastModified <= ifModifiedSince.time) {
|
|
||||||
httpContext.respondCode(304, responseHeaders)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val safeName = (meta.displayName ?: "content.bin").replace("\"", "\\\"")
|
|
||||||
responseHeaders["Content-Disposition"] = "attachment; filename=\"$safeName\""
|
|
||||||
|
|
||||||
val length = meta.size
|
|
||||||
if (length == null) {
|
|
||||||
Logger.i(TAG, "Streaming $uri with unknown length; Range not supported")
|
|
||||||
responseHeaders.remove("Content-Length")
|
|
||||||
responseHeaders.remove("Content-Range")
|
|
||||||
responseHeaders.remove("Accept-Ranges")
|
|
||||||
|
|
||||||
stream(
|
|
||||||
httpContext = httpContext,
|
|
||||||
resolver = resolver,
|
|
||||||
uri = uri,
|
|
||||||
statusCode = 200,
|
|
||||||
headers = responseHeaders,
|
|
||||||
start = null,
|
|
||||||
length = null
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
responseHeaders["Accept-Ranges"] = "bytes"
|
|
||||||
|
|
||||||
val rangeHeader = requestHeaders["Range"]
|
|
||||||
if (rangeHeader.isNullOrBlank()) {
|
|
||||||
responseHeaders["Content-Length"] = length.toString()
|
|
||||||
Logger.i(TAG, "Sending full content for $uri, length=$length")
|
|
||||||
|
|
||||||
stream(
|
|
||||||
httpContext = httpContext,
|
|
||||||
resolver = resolver,
|
|
||||||
uri = uri,
|
|
||||||
statusCode = 200,
|
|
||||||
headers = responseHeaders,
|
|
||||||
start = 0L,
|
|
||||||
length = length
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val range = parseRange(rangeHeader, length)
|
|
||||||
if (range == null) {
|
|
||||||
Logger.w(TAG, "Invalid Range '$rangeHeader' for $uri (length=$length)")
|
|
||||||
responseHeaders["Content-Range"] = "bytes */$length"
|
|
||||||
httpContext.respondCode(416, responseHeaders)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val start = range.first
|
|
||||||
val endInclusive = range.last
|
|
||||||
val bytesToSend = endInclusive - start + 1
|
|
||||||
|
|
||||||
responseHeaders["Content-Range"] = "bytes $start-$endInclusive/$length"
|
|
||||||
responseHeaders["Content-Length"] = bytesToSend.toString()
|
|
||||||
Logger.i(TAG, "Sending range $start-$endInclusive (length=$bytesToSend) of $length for $uri")
|
|
||||||
|
|
||||||
stream(
|
|
||||||
httpContext = httpContext,
|
|
||||||
resolver = resolver,
|
|
||||||
uri = uri,
|
|
||||||
statusCode = 206,
|
|
||||||
headers = responseHeaders,
|
|
||||||
start = start,
|
|
||||||
length = bytesToSend
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
data class ContentMeta(
|
|
||||||
val displayName: String?,
|
|
||||||
val size: Long?,
|
|
||||||
val lastModifiedMillis: Long?
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun queryMetadata(resolver: ContentResolver, uri: Uri): ContentMeta {
|
|
||||||
var displayName: String? = null
|
|
||||||
var size: Long? = null
|
|
||||||
var lastModifiedMillis: Long? = null
|
|
||||||
|
|
||||||
resolver.query(uri, null, null, null, null)?.use { cursor ->
|
|
||||||
if (cursor.moveToFirst()) {
|
|
||||||
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
|
||||||
if (nameIndex != -1 && !cursor.isNull(nameIndex)) {
|
|
||||||
displayName = cursor.getString(nameIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
|
|
||||||
if (sizeIndex != -1 && !cursor.isNull(sizeIndex)) {
|
|
||||||
val s = cursor.getLong(sizeIndex)
|
|
||||||
if (s >= 0) size = s // -1 means unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
val dateModifiedIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED)
|
|
||||||
if (dateModifiedIndex != -1 && !cursor.isNull(dateModifiedIndex)) {
|
|
||||||
val seconds = cursor.getLong(dateModifiedIndex)
|
|
||||||
if (seconds > 0) {
|
|
||||||
lastModifiedMillis = seconds * 1000L
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastModifiedMillis == null) {
|
|
||||||
val dateAddedIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DATE_ADDED)
|
|
||||||
if (dateAddedIndex != -1 && !cursor.isNull(dateAddedIndex)) {
|
|
||||||
val seconds = cursor.getLong(dateAddedIndex)
|
|
||||||
if (seconds > 0) {
|
|
||||||
lastModifiedMillis = seconds * 1000L
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (displayName == null) {
|
|
||||||
displayName = uri.lastPathSegment
|
|
||||||
}
|
|
||||||
|
|
||||||
if (size == null) {
|
|
||||||
try {
|
|
||||||
resolver.openAssetFileDescriptor(uri, "r")?.use { afd ->
|
|
||||||
val assetLen = afd.length
|
|
||||||
if (assetLen >= 0) {
|
|
||||||
size = assetLen
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (_: Exception) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
return ContentMeta(
|
|
||||||
displayName = displayName,
|
|
||||||
size = size,
|
|
||||||
lastModifiedMillis = lastModifiedMillis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseRange(header: String, totalLength: Long): LongRange? {
|
|
||||||
if (totalLength <= 0L) return null
|
|
||||||
|
|
||||||
val prefix = "bytes="
|
|
||||||
if (!header.startsWith(prefix, ignoreCase = true)) return null
|
|
||||||
|
|
||||||
val spec = header.substring(prefix.length).trim()
|
|
||||||
if (spec.isEmpty()) return null
|
|
||||||
|
|
||||||
if (spec.contains(",")) return null
|
|
||||||
|
|
||||||
val dashIndex = spec.indexOf('-')
|
|
||||||
if (dashIndex < 0) return null
|
|
||||||
|
|
||||||
val startPart = spec.substring(0, dashIndex).trim()
|
|
||||||
val endPart = spec.substring(dashIndex + 1).trim()
|
|
||||||
|
|
||||||
return when {
|
|
||||||
startPart.isNotEmpty() -> {
|
|
||||||
val start = startPart.toLongOrNull() ?: return null
|
|
||||||
if (start < 0 || start >= totalLength) return null
|
|
||||||
|
|
||||||
val end = if (endPart.isNotEmpty()) {
|
|
||||||
val rawEnd = endPart.toLongOrNull() ?: return null
|
|
||||||
if (rawEnd < start) return null
|
|
||||||
rawEnd.coerceAtMost(totalLength - 1)
|
|
||||||
} else {
|
|
||||||
totalLength - 1
|
|
||||||
}
|
|
||||||
|
|
||||||
start..end
|
|
||||||
}
|
|
||||||
|
|
||||||
endPart.isNotEmpty() -> {
|
|
||||||
val suffixLen = endPart.toLongOrNull() ?: return null
|
|
||||||
if (suffixLen <= 0L) return null
|
|
||||||
|
|
||||||
if (suffixLen >= totalLength) {
|
|
||||||
0L..(totalLength - 1)
|
|
||||||
} else {
|
|
||||||
val start = totalLength - suffixLen
|
|
||||||
val end = totalLength - 1
|
|
||||||
start..end
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun stream(httpContext: HttpContext, resolver: ContentResolver, uri: Uri, statusCode: Int, headers: HttpHeaders, start: Long?, length: Long?) {
|
|
||||||
try {
|
|
||||||
val input = resolver.openInputStream(uri)
|
|
||||||
if (input == null) {
|
|
||||||
Logger.w(TAG, "Content not found: $uri")
|
|
||||||
httpContext.respondCode(404, headers)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
input.use { inputStream ->
|
|
||||||
httpContext.respond(statusCode, headers) { outputStream ->
|
|
||||||
try {
|
|
||||||
val offset = start ?: 0L
|
|
||||||
if (offset > 0L) {
|
|
||||||
skipFully(inputStream, offset)
|
|
||||||
}
|
|
||||||
copyStream(inputStream, outputStream, length)
|
|
||||||
outputStream.flush()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Logger.e(TAG, "Error while streaming $uri (start=$start, length=$length)", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: FileNotFoundException) {
|
|
||||||
Logger.w(TAG, "Content not found: $uri", e)
|
|
||||||
httpContext.respondCode(404, headers)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Logger.e(TAG, "Failed to open stream for $uri", e)
|
|
||||||
httpContext.respondCode(500, headers)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun copyStream(input: InputStream, output: OutputStream, limit: Long?) {
|
|
||||||
val buffer = ByteArray(8192)
|
|
||||||
if (limit == null) {
|
|
||||||
while (true) {
|
|
||||||
val read = input.read(buffer)
|
|
||||||
if (read < 0) break
|
|
||||||
output.write(buffer, 0, read)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
var remaining = limit
|
|
||||||
while (remaining > 0L) {
|
|
||||||
val toRead = remaining.coerceAtMost(buffer.size.toLong()).toInt()
|
|
||||||
val read = input.read(buffer, 0, toRead)
|
|
||||||
if (read < 0) break
|
|
||||||
output.write(buffer, 0, read)
|
|
||||||
remaining -= read.toLong()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun skipFully(input: InputStream, bytesToSkip: Long) {
|
|
||||||
var remaining = bytesToSkip
|
|
||||||
while (remaining > 0L) {
|
|
||||||
val skipped = input.skip(remaining)
|
|
||||||
if (skipped <= 0L) {
|
|
||||||
val b = input.read()
|
|
||||||
if (b == -1) break
|
|
||||||
remaining -= 1L
|
|
||||||
} else {
|
|
||||||
remaining -= skipped
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val TAG = "HttpContentUriHandler"
|
|
||||||
|
|
||||||
private val httpDateFormat = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.US).apply {
|
|
||||||
timeZone = TimeZone.getTimeZone("GMT")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+9
-39
@@ -5,7 +5,6 @@ import android.util.Log
|
|||||||
import com.futo.platformplayer.api.http.server.HttpContext
|
import com.futo.platformplayer.api.http.server.HttpContext
|
||||||
import com.futo.platformplayer.api.http.server.HttpHeaders
|
import com.futo.platformplayer.api.http.server.HttpHeaders
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.api.media.models.modifier.IRequest
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.parsers.HttpResponseParser
|
import com.futo.platformplayer.parsers.HttpResponseParser
|
||||||
import com.futo.platformplayer.readLine
|
import com.futo.platformplayer.readLine
|
||||||
@@ -28,7 +27,6 @@ class HttpProxyHandler(method: String, path: String, val targetUrl: String, priv
|
|||||||
private var _injectReferer = false;
|
private var _injectReferer = false;
|
||||||
|
|
||||||
private val _client = ManagedHttpClient();
|
private val _client = ManagedHttpClient();
|
||||||
private var _requestModifier: ((String, Map<String, String>) -> IRequest)? = null;
|
|
||||||
|
|
||||||
override fun handle(context: HttpContext) {
|
override fun handle(context: HttpContext) {
|
||||||
if (useTcp) {
|
if (useTcp) {
|
||||||
@@ -45,33 +43,21 @@ class HttpProxyHandler(method: String, path: String, val targetUrl: String, priv
|
|||||||
for (injectHeader in _injectRequestHeader)
|
for (injectHeader in _injectRequestHeader)
|
||||||
proxyHeaders[injectHeader.first] = injectHeader.second;
|
proxyHeaders[injectHeader.first] = injectHeader.second;
|
||||||
|
|
||||||
val req = _requestModifier?.invoke(targetUrl, proxyHeaders)
|
val parsed = Uri.parse(targetUrl);
|
||||||
var url = targetUrl
|
|
||||||
if (req != null) {
|
|
||||||
req.url?.let {
|
|
||||||
url = it
|
|
||||||
}
|
|
||||||
req.headers.let {
|
|
||||||
proxyHeaders.clear()
|
|
||||||
proxyHeaders.putAll(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val parsed = Uri.parse(url);
|
|
||||||
if(_injectHost)
|
if(_injectHost)
|
||||||
proxyHeaders.put("Host", parsed.host!!);
|
proxyHeaders.put("Host", parsed.host!!);
|
||||||
if(_injectReferer)
|
if(_injectReferer)
|
||||||
proxyHeaders.put("Referer", url);
|
proxyHeaders.put("Referer", targetUrl);
|
||||||
|
|
||||||
val useMethod = if (method == "inherit") context.method else method;
|
val useMethod = if (method == "inherit") context.method else method;
|
||||||
Logger.i(TAG, "handleWithOkHttp Proxied Request ${useMethod}: ${url}");
|
Logger.i(TAG, "handleWithOkHttp Proxied Request ${useMethod}: ${targetUrl}");
|
||||||
Logger.i(TAG, "handleWithOkHttp Headers:" + proxyHeaders.map { "${it.key}: ${it.value}" }.joinToString("\n"));
|
Logger.i(TAG, "handleWithOkHttp Headers:" + proxyHeaders.map { "${it.key}: ${it.value}" }.joinToString("\n"));
|
||||||
|
|
||||||
val resp = when (useMethod) {
|
val resp = when (useMethod) {
|
||||||
"GET" -> _client.get(url, proxyHeaders);
|
"GET" -> _client.get(targetUrl, proxyHeaders);
|
||||||
"POST" -> _client.post(url, content ?: "", proxyHeaders);
|
"POST" -> _client.post(targetUrl, content ?: "", proxyHeaders);
|
||||||
"HEAD" -> _client.head(url, proxyHeaders)
|
"HEAD" -> _client.head(targetUrl, proxyHeaders)
|
||||||
else -> _client.requestMethod(useMethod, url, proxyHeaders);
|
else -> _client.requestMethod(useMethod, targetUrl, proxyHeaders);
|
||||||
};
|
};
|
||||||
|
|
||||||
Logger.i(TAG, "Proxied Response [${resp.code}]");
|
Logger.i(TAG, "Proxied Response [${resp.code}]");
|
||||||
@@ -105,23 +91,11 @@ class HttpProxyHandler(method: String, path: String, val targetUrl: String, priv
|
|||||||
for (injectHeader in _injectRequestHeader)
|
for (injectHeader in _injectRequestHeader)
|
||||||
proxyHeaders[injectHeader.first] = injectHeader.second;
|
proxyHeaders[injectHeader.first] = injectHeader.second;
|
||||||
|
|
||||||
val req = _requestModifier?.invoke(targetUrl, proxyHeaders)
|
val parsed = Uri.parse(targetUrl);
|
||||||
var url = targetUrl
|
|
||||||
if (req != null) {
|
|
||||||
req.url?.let {
|
|
||||||
url = it
|
|
||||||
}
|
|
||||||
req.headers.let {
|
|
||||||
proxyHeaders.clear()
|
|
||||||
proxyHeaders.putAll(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val parsed = Uri.parse(url);
|
|
||||||
if(_injectHost)
|
if(_injectHost)
|
||||||
proxyHeaders.put("Host", parsed.host!!);
|
proxyHeaders.put("Host", parsed.host!!);
|
||||||
if(_injectReferer)
|
if(_injectReferer)
|
||||||
proxyHeaders.put("Referer", url);
|
proxyHeaders.put("Referer", targetUrl);
|
||||||
|
|
||||||
val useMethod = if (method == "inherit") context.method else method;
|
val useMethod = if (method == "inherit") context.method else method;
|
||||||
Logger.i(TAG, "handleWithTcp Proxied Request ${useMethod}: ${parsed}");
|
Logger.i(TAG, "handleWithTcp Proxied Request ${useMethod}: ${parsed}");
|
||||||
@@ -268,10 +242,6 @@ class HttpProxyHandler(method: String, path: String, val targetUrl: String, priv
|
|||||||
_ignoreRequestHeaders.add("referer");
|
_ignoreRequestHeaders.add("referer");
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
fun withRequestModifier(modifier: (String, Map<String, String>) -> IRequest) : HttpProxyHandler {
|
|
||||||
_requestModifier = modifier;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "HttpProxyHandler"
|
private const val TAG = "HttpProxyHandler"
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -13,7 +12,6 @@ import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
|
|||||||
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
||||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
||||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.models.ImageVariable
|
import com.futo.platformplayer.models.ImageVariable
|
||||||
|
|
||||||
@@ -37,11 +35,6 @@ interface IPlatformClient {
|
|||||||
*/
|
*/
|
||||||
fun getHome(): IPager<IPlatformContent>
|
fun getHome(): IPager<IPlatformContent>
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the shorts feed
|
|
||||||
*/
|
|
||||||
fun getShorts(): IPager<IPlatformVideo>
|
|
||||||
|
|
||||||
//Search
|
//Search
|
||||||
/**
|
/**
|
||||||
* Gets search suggestion for the provided query string
|
* Gets search suggestion for the provided query string
|
||||||
@@ -73,11 +66,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
|
||||||
/**
|
/**
|
||||||
@@ -182,10 +170,6 @@ interface IPlatformClient {
|
|||||||
* Retrieves the subscriptions of the currently logged in user
|
* Retrieves the subscriptions of the currently logged in user
|
||||||
*/
|
*/
|
||||||
fun getUserSubscriptions(): Array<String>;
|
fun getUserSubscriptions(): Array<String>;
|
||||||
/**
|
|
||||||
* Retrieves the history of the currently logged in user
|
|
||||||
*/
|
|
||||||
fun getUserHistory(): IPager<IPlatformContent>;
|
|
||||||
|
|
||||||
|
|
||||||
fun isClaimTypeSupported(claimType: Int): Boolean;
|
fun isClaimTypeSupported(claimType: Int): Boolean;
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ 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.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.platforms.js.models.JSVODEventPager
|
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.constructs.BatchedTaskHandler
|
import com.futo.platformplayer.constructs.BatchedTaskHandler
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
@@ -27,17 +26,12 @@ class LiveChatManager {
|
|||||||
private val _emojiCache: EmojiCache = EmojiCache();
|
private val _emojiCache: EmojiCache = EmojiCache();
|
||||||
private val _pager: IPager<IPlatformLiveEvent>?;
|
private val _pager: IPager<IPlatformLiveEvent>?;
|
||||||
|
|
||||||
private var _position: Long = 0;
|
|
||||||
private var _eventsPosition: Long = 0;
|
|
||||||
|
|
||||||
private val _history: ArrayList<IPlatformLiveEvent> = arrayListOf();
|
private val _history: ArrayList<IPlatformLiveEvent> = arrayListOf();
|
||||||
|
|
||||||
private var _startCounter = 0;
|
private var _startCounter = 0;
|
||||||
|
|
||||||
private val _followers: HashMap<Any, (List<IPlatformLiveEvent>) -> Unit> = hashMapOf();
|
private val _followers: HashMap<Any, (List<IPlatformLiveEvent>) -> Unit> = hashMapOf();
|
||||||
|
|
||||||
val isVOD get() = _pager is JSVODEventPager;
|
|
||||||
|
|
||||||
var viewCount: Long = 0
|
var viewCount: Long = 0
|
||||||
private set;
|
private set;
|
||||||
|
|
||||||
@@ -45,24 +39,8 @@ class LiveChatManager {
|
|||||||
_scope = scope;
|
_scope = scope;
|
||||||
_pager = pager;
|
_pager = pager;
|
||||||
viewCount = initialViewCount;
|
viewCount = initialViewCount;
|
||||||
if(pager is JSVODEventPager)
|
handleEvents(listOf(LiveEventComment("SYSTEM", null, "Live chat is still under construction. While it is mostly functional, the experience still needs to be improved.\n")));
|
||||||
handleEvents(listOf(LiveEventComment("SYSTEM", null, "VOD chat is still under construction. While it is mostly functional, the experience still needs to be improved.\n")));
|
handleEvents(pager.getResults());
|
||||||
else
|
|
||||||
handleEvents(listOf(LiveEventComment("SYSTEM", null, "Live chat is still under construction. While it is mostly functional, the experience still needs to be improved.\n")));
|
|
||||||
|
|
||||||
if(pager is JSVODEventPager) {
|
|
||||||
var replayResults = pager.getResults().filter { it.time > _eventsPosition || it is LiveEventEmojis };
|
|
||||||
//TODO: Remove this once dripfeed is done properly
|
|
||||||
replayResults = replayResults.filter{ it.time < _eventsPosition + 1500 || it is LiveEventEmojis };
|
|
||||||
if(replayResults.size > 0) {
|
|
||||||
_eventsPosition = replayResults.maxOf { it.time };
|
|
||||||
Logger.i(TAG, "VOD Events last event: " + _eventsPosition);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
_eventsPosition = _eventsPosition + 1500;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
handleEvents(pager.getResults());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun start() {
|
fun start() {
|
||||||
@@ -74,10 +52,6 @@ class LiveChatManager {
|
|||||||
_startCounter++;
|
_startCounter++;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setVideoPosition(ms: Long) {
|
|
||||||
_position = ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getHistory(): List<IPlatformLiveEvent> {
|
fun getHistory(): List<IPlatformLiveEvent> {
|
||||||
synchronized(_history) {
|
synchronized(_history) {
|
||||||
return _history.toList();
|
return _history.toList();
|
||||||
@@ -111,34 +85,13 @@ class LiveChatManager {
|
|||||||
try {
|
try {
|
||||||
while(_startCounter == counter) {
|
while(_startCounter == counter) {
|
||||||
var nextInterval = 1000L;
|
var nextInterval = 1000L;
|
||||||
if(_pager is JSVODEventPager && _eventsPosition > _position) {
|
|
||||||
delay(500);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if(_pager == null || !_pager.hasMorePages())
|
if(_pager == null || !_pager.hasMorePages())
|
||||||
return@launch;
|
return@launch;
|
||||||
val newEvents = if(_pager is JSVODEventPager) {
|
_pager.nextPage();
|
||||||
val requestPosition = _position;
|
val newEvents = _pager.getResults();
|
||||||
_pager.nextPage(requestPosition.toInt());
|
|
||||||
var replayResults = _pager.getResults().filter { it.time > requestPosition || it is LiveEventEmojis };
|
|
||||||
if(replayResults.size > 0) {
|
|
||||||
_eventsPosition = replayResults.maxOf { it.time };
|
|
||||||
Logger.i(TAG, "VOD Events last event: " + _eventsPosition);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
_eventsPosition = requestPosition + _pager.nextRequest.coerceAtLeast(800).toLong();
|
|
||||||
replayResults;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
_pager.nextPage();
|
|
||||||
_pager.getResults();
|
|
||||||
}
|
|
||||||
if(_pager is JSLiveEventPager)
|
if(_pager is JSLiveEventPager)
|
||||||
nextInterval = _pager.nextRequest.coerceAtLeast(800).toLong();
|
nextInterval = _pager.nextRequest.coerceAtLeast(800).toLong();
|
||||||
else if(_pager is JSVODEventPager)
|
|
||||||
nextInterval = _pager.nextRequest.coerceAtLeast(800).toLong();
|
|
||||||
|
|
||||||
if(newEvents.size > 0)
|
if(newEvents.size > 0)
|
||||||
Logger.i(TAG, "New Live Events (${newEvents.size}) [${newEvents.map { it.type.name }.joinToString(", ")}]");
|
Logger.i(TAG, "New Live Events (${newEvents.size}) [${newEvents.map { it.type.name }.joinToString(", ")}]");
|
||||||
|
|||||||
@@ -20,8 +20,7 @@ data class PlatformClientCapabilities(
|
|||||||
val hasGetContentChapters: Boolean = false,
|
val hasGetContentChapters: Boolean = false,
|
||||||
val hasPeekChannelContents: Boolean = false,
|
val hasPeekChannelContents: Boolean = false,
|
||||||
val hasGetChannelPlaylists: Boolean = false,
|
val hasGetChannelPlaylists: Boolean = false,
|
||||||
val hasGetContentRecommendations: Boolean = false,
|
val hasGetContentRecommendations: Boolean = false
|
||||||
val hasGetUserHistory: Boolean = false
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -14,16 +14,14 @@ class PlatformClientPool {
|
|||||||
private var _poolCounter = 0;
|
private var _poolCounter = 0;
|
||||||
private val _poolName: String?;
|
private val _poolName: String?;
|
||||||
private val _privatePool: Boolean;
|
private val _privatePool: Boolean;
|
||||||
private val _isolatedInitialization: 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, isolatedInitialization: Boolean = false) {
|
constructor(parentClient: IPlatformClient, name: String? = null, privatePool: Boolean = false) {
|
||||||
_poolName = name;
|
_poolName = name;
|
||||||
_privatePool = privatePool;
|
_privatePool = privatePool;
|
||||||
_isolatedInitialization = isolatedInitialization
|
|
||||||
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");
|
||||||
@@ -34,10 +32,8 @@ class PlatformClientPool {
|
|||||||
isDead = true;
|
isDead = true;
|
||||||
onDead.emit(parentClient, this);
|
onDead.emit(parentClient, this);
|
||||||
|
|
||||||
synchronized(_pool) {
|
for(clientPair in _pool) {
|
||||||
for (clientPair in _pool) {
|
clientPair.key.disable();
|
||||||
clientPair.key.disable();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -57,7 +53,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, _isolatedInitialization);
|
reserved = _parent.getCopy(_privatePool);
|
||||||
|
|
||||||
reserved?.onCaptchaException?.subscribe { client, ex ->
|
reserved?.onCaptchaException?.subscribe { client, ex ->
|
||||||
StateApp.instance.handleCaptchaException(client, ex);
|
StateApp.instance.handleCaptchaException(client, ex);
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package com.futo.platformplayer.api.media
|
|||||||
|
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.ensureIsBusy
|
|
||||||
import com.futo.platformplayer.getOrDefault
|
import com.futo.platformplayer.getOrDefault
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
import com.futo.platformplayer.getOrThrowNullable
|
import com.futo.platformplayer.getOrThrowNullable
|
||||||
@@ -45,7 +44,6 @@ class PlatformID {
|
|||||||
val NONE = PlatformID("Unknown", null);
|
val NONE = PlatformID("Unknown", null);
|
||||||
|
|
||||||
fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformID {
|
fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformID {
|
||||||
value.ensureIsBusy();
|
|
||||||
val contextName = "PlatformID";
|
val contextName = "PlatformID";
|
||||||
return PlatformID(
|
return PlatformID(
|
||||||
value.getOrThrow(config, "platform", contextName),
|
value.getOrThrow(config, "platform", contextName),
|
||||||
|
|||||||
@@ -7,15 +7,13 @@ class PlatformMultiClientPool {
|
|||||||
|
|
||||||
private var _isFake = false;
|
private var _isFake = false;
|
||||||
private var _privatePool = false;
|
private var _privatePool = false;
|
||||||
private val _isolatedInitialization: Boolean
|
|
||||||
|
|
||||||
constructor(name: String, maxCap: Int = -1, isPrivatePool: Boolean = false, isolatedInitialization: Boolean = false) {
|
constructor(name: String, maxCap: Int = -1, isPrivatePool: Boolean = false) {
|
||||||
_name = name;
|
_name = name;
|
||||||
_maxCap = if(maxCap > 0)
|
_maxCap = if(maxCap > 0)
|
||||||
maxCap
|
maxCap
|
||||||
else 99;
|
else 99;
|
||||||
_privatePool = isPrivatePool;
|
_privatePool = isPrivatePool;
|
||||||
_isolatedInitialization = isolatedInitialization
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getClientPooled(parentClient: IPlatformClient, capacity: Int = _maxCap): IPlatformClient {
|
fun getClientPooled(parentClient: IPlatformClient, capacity: Int = _maxCap): IPlatformClient {
|
||||||
@@ -23,7 +21,7 @@ class PlatformMultiClientPool {
|
|||||||
return parentClient;
|
return parentClient;
|
||||||
val pool = synchronized(_clientPools) {
|
val pool = synchronized(_clientPools) {
|
||||||
if(!_clientPools.containsKey(parentClient))
|
if(!_clientPools.containsKey(parentClient))
|
||||||
_clientPools[parentClient] = PlatformClientPool(parentClient, _name, _privatePool, _isolatedInitialization).apply {
|
_clientPools[parentClient] = PlatformClientPool(parentClient, _name, _privatePool).apply {
|
||||||
this.onDead.subscribe { _, pool ->
|
this.onDead.subscribe { _, pool ->
|
||||||
synchronized(_clientPools) {
|
synchronized(_clientPools) {
|
||||||
if(_clientPools[parentClient] == pool)
|
if(_clientPools[parentClient] == pool)
|
||||||
|
|||||||
@@ -2,11 +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.ensureIsBusy
|
|
||||||
import com.futo.platformplayer.getOrDefault
|
import com.futo.platformplayer.getOrDefault
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
|
||||||
@@ -34,7 +30,6 @@ open class PlatformAuthorLink {
|
|||||||
val UNKNOWN = PlatformAuthorLink(PlatformID.NONE, "Unknown", "", null, null);
|
val UNKNOWN = PlatformAuthorLink(PlatformID.NONE, "Unknown", "", null, null);
|
||||||
|
|
||||||
fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformAuthorLink {
|
fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformAuthorLink {
|
||||||
value.ensureIsBusy();
|
|
||||||
if(value.has("membershipUrl"))
|
if(value.has("membershipUrl"))
|
||||||
return PlatformAuthorMembershipLink.fromV8(config, value);
|
return PlatformAuthorMembershipLink.fromV8(config, value);
|
||||||
|
|
||||||
@@ -47,23 +42,4 @@ open class PlatformAuthorLink {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IPlatformChannelContent : IPlatformContent {
|
|
||||||
val thumbnail: String?
|
|
||||||
val subscribers: Long?
|
|
||||||
}
|
|
||||||
|
|
||||||
open class JSChannelContent(
|
|
||||||
config: SourcePluginConfig,
|
|
||||||
obj: V8ValueObject
|
|
||||||
) : JSContent(config, obj), IPlatformChannelContent {
|
|
||||||
|
|
||||||
final override val contentType: ContentType = ContentType.CHANNEL
|
|
||||||
|
|
||||||
override val thumbnail: String? =
|
|
||||||
_content.getOrDefault<String>(_pluginConfig, "thumbnail", "Channel", null)
|
|
||||||
|
|
||||||
override val subscribers: Long? =
|
|
||||||
_content.getOrDefault<Long>(_pluginConfig, "subscribers", "Channel", null)?.toLong()
|
|
||||||
}
|
|
||||||
-2
@@ -3,7 +3,6 @@ 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.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.ensureIsBusy
|
|
||||||
import com.futo.platformplayer.getOrDefault
|
import com.futo.platformplayer.getOrDefault
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
|
||||||
@@ -21,7 +20,6 @@ class PlatformAuthorMembershipLink: PlatformAuthorLink {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformAuthorMembershipLink {
|
fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformAuthorMembershipLink {
|
||||||
value.ensureIsBusy();
|
|
||||||
val context = "AuthorMembershipLink"
|
val context = "AuthorMembershipLink"
|
||||||
return PlatformAuthorMembershipLink(PlatformID.fromV8(config, value.getOrThrow(config, "id", context, false)),
|
return PlatformAuthorMembershipLink(PlatformID.fromV8(config, value.getOrThrow(config, "id", context, false)),
|
||||||
value.getOrThrow(config ,"name", context),
|
value.getOrThrow(config ,"name", context),
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import com.caoccao.javet.values.primitive.V8ValueInteger
|
|||||||
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.ensureIsBusy
|
|
||||||
import com.futo.platformplayer.expectV8Variant
|
import com.futo.platformplayer.expectV8Variant
|
||||||
import com.futo.platformplayer.getOrDefault
|
import com.futo.platformplayer.getOrDefault
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
@@ -47,7 +46,6 @@ class ResultCapabilities(
|
|||||||
|
|
||||||
fun fromV8(config: IV8PluginConfig, value: V8ValueObject): ResultCapabilities {
|
fun fromV8(config: IV8PluginConfig, value: V8ValueObject): ResultCapabilities {
|
||||||
val contextName = "ResultCapabilities";
|
val contextName = "ResultCapabilities";
|
||||||
value.ensureIsBusy();
|
|
||||||
return ResultCapabilities(
|
return ResultCapabilities(
|
||||||
value.getOrThrow<V8ValueArray>(config, "types", contextName).toArray().map { it.expectV8Variant(config, "Capabilities.types") },
|
value.getOrThrow<V8ValueArray>(config, "types", contextName).toArray().map { it.expectV8Variant(config, "Capabilities.types") },
|
||||||
value.getOrThrow<V8ValueArray>(config, "sorts", contextName).toArray().map { it.expectV8Variant(config, "Capabilities.sorts"); },
|
value.getOrThrow<V8ValueArray>(config, "sorts", contextName).toArray().map { it.expectV8Variant(config, "Capabilities.sorts"); },
|
||||||
@@ -71,7 +69,6 @@ class FilterGroup(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromV8(config: IV8PluginConfig, value: V8ValueObject): FilterGroup {
|
fun fromV8(config: IV8PluginConfig, value: V8ValueObject): FilterGroup {
|
||||||
value.ensureIsBusy();
|
|
||||||
return FilterGroup(
|
return FilterGroup(
|
||||||
value.getString("name"),
|
value.getString("name"),
|
||||||
value.getOrDefault<V8ValueArray>(config, "filters", "FilterGroup", null)
|
value.getOrDefault<V8ValueArray>(config, "filters", "FilterGroup", null)
|
||||||
@@ -93,7 +90,6 @@ class FilterCapability(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromV8(obj: V8ValueObject): FilterCapability {
|
fun fromV8(obj: V8ValueObject): FilterCapability {
|
||||||
obj.ensureIsBusy();
|
|
||||||
val value = obj.get("value") as V8Value;
|
val value = obj.get("value") as V8Value;
|
||||||
return FilterCapability(
|
return FilterCapability(
|
||||||
obj.getString("name"),
|
obj.getString("name"),
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ 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.engine.V8PluginConfig
|
||||||
import com.futo.platformplayer.ensureIsBusy
|
|
||||||
import com.futo.platformplayer.getOrDefault
|
import com.futo.platformplayer.getOrDefault
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
|
||||||
@@ -32,7 +31,6 @@ class Thumbnails {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromV8(config: IV8PluginConfig, value: V8ValueObject): Thumbnails {
|
fun fromV8(config: IV8PluginConfig, value: V8ValueObject): Thumbnails {
|
||||||
value.ensureIsBusy();
|
|
||||||
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(config, it as V8ValueObject) }
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
package com.futo.platformplayer.api.media.models.article
|
|
||||||
|
|
||||||
import com.futo.platformplayer.api.media.models.Thumbnails
|
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
|
||||||
|
|
||||||
interface IPlatformArticle: IPlatformContent {
|
|
||||||
val summary: String?;
|
|
||||||
val thumbnails: Thumbnails?;
|
|
||||||
}
|
|
||||||
-12
@@ -1,12 +0,0 @@
|
|||||||
package com.futo.platformplayer.api.media.models.article
|
|
||||||
|
|
||||||
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.IPlatformContentDetails
|
|
||||||
import com.futo.platformplayer.api.media.models.ratings.IRating
|
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.IJSArticleSegment
|
|
||||||
|
|
||||||
interface IPlatformArticleDetails: IPlatformContent, IPlatformArticle, IPlatformContentDetails {
|
|
||||||
val segments: List<IJSArticleSegment>;
|
|
||||||
val rating : IRating;
|
|
||||||
}
|
|
||||||
+21
-11
@@ -6,15 +6,25 @@ import com.futo.platformplayer.api.media.models.ratings.IRating
|
|||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
open class PlatformComment(
|
open class PlatformComment : IPlatformComment {
|
||||||
override val contextUrl: String,
|
override val contextUrl: String;
|
||||||
override val author: PlatformAuthorLink,
|
override val author: PlatformAuthorLink;
|
||||||
override val message: String,
|
override val message: String;
|
||||||
override val rating: IRating,
|
override val rating: IRating;
|
||||||
override val date: OffsetDateTime,
|
override val date: OffsetDateTime;
|
||||||
override val replyCount: Int? = null
|
|
||||||
) : IPlatformComment {
|
|
||||||
|
|
||||||
override fun getReplies(client: IPlatformClient): IPager<IPlatformComment> =
|
override val replyCount: Int?;
|
||||||
NoCommentsPager()
|
|
||||||
}
|
constructor(contextUrl: String, author: PlatformAuthorLink, msg: String, rating: IRating, date: OffsetDateTime, replyCount: Int? = null) {
|
||||||
|
this.contextUrl = contextUrl;
|
||||||
|
this.author = author;
|
||||||
|
this.message = msg;
|
||||||
|
this.rating = rating;
|
||||||
|
this.date = date;
|
||||||
|
this.replyCount = replyCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getReplies(client: IPlatformClient): IPager<IPlatformComment> {
|
||||||
|
return NoCommentsPager();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,12 +8,10 @@ enum class ContentType(val value: Int) {
|
|||||||
POST(2),
|
POST(2),
|
||||||
ARTICLE(3),
|
ARTICLE(3),
|
||||||
PLAYLIST(4),
|
PLAYLIST(4),
|
||||||
WEB(7),
|
|
||||||
|
|
||||||
URL(9),
|
URL(9),
|
||||||
|
|
||||||
NESTED_VIDEO(11),
|
NESTED_VIDEO(11),
|
||||||
CHANNEL(60),
|
|
||||||
|
|
||||||
LOCKED(70),
|
LOCKED(70),
|
||||||
|
|
||||||
|
|||||||
@@ -2,17 +2,14 @@ package com.futo.platformplayer.api.media.models.live
|
|||||||
|
|
||||||
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.ensureIsBusy
|
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
|
||||||
interface IPlatformLiveEvent {
|
interface IPlatformLiveEvent {
|
||||||
val type : LiveEventType;
|
val type : LiveEventType;
|
||||||
var time: Long;
|
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject, contextName: String = "LiveEvent") : IPlatformLiveEvent {
|
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject, contextName: String = "LiveEvent") : IPlatformLiveEvent {
|
||||||
obj.ensureIsBusy();
|
|
||||||
val t = LiveEventType.fromInt(obj.getOrThrow<Int>(config, "type", contextName));
|
val t = LiveEventType.fromInt(obj.getOrThrow<Int>(config, "type", contextName));
|
||||||
return when(t) {
|
return when(t) {
|
||||||
LiveEventType.COMMENT -> LiveEventComment.fromV8(config, obj);
|
LiveEventType.COMMENT -> LiveEventComment.fromV8(config, obj);
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ 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.api.media.models.ratings.RatingLikes
|
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
import com.futo.platformplayer.ensureIsBusy
|
|
||||||
import com.futo.platformplayer.getOrDefault
|
import com.futo.platformplayer.getOrDefault
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
|
||||||
@@ -18,21 +17,16 @@ class LiveEventComment: IPlatformLiveEvent, ILiveEventChatMessage {
|
|||||||
val colorName: String?;
|
val colorName: String?;
|
||||||
val badges: List<String>;
|
val badges: List<String>;
|
||||||
|
|
||||||
override var time: Long = -1;
|
constructor(name: String, thumbnail: String?, message: String, colorName: String? = null, badges: List<String>? = null) {
|
||||||
|
|
||||||
constructor(name: String, thumbnail: String?, message: String, colorName: String? = null, badges: List<String>? = null, time: Long = -1) {
|
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.message = message;
|
this.message = message;
|
||||||
this.thumbnail = thumbnail;
|
this.thumbnail = thumbnail;
|
||||||
this.colorName = colorName;
|
this.colorName = colorName;
|
||||||
this.badges = badges ?: listOf();
|
this.badges = badges ?: listOf();
|
||||||
this.time = time;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventComment {
|
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventComment {
|
||||||
obj.ensureIsBusy();
|
|
||||||
|
|
||||||
val contextName = "LiveEventComment"
|
val contextName = "LiveEventComment"
|
||||||
|
|
||||||
val colorName = obj.getOrDefault<String>(config, "colorName", contextName, null);
|
val colorName = obj.getOrDefault<String>(config, "colorName", contextName, null);
|
||||||
@@ -42,8 +36,7 @@ class LiveEventComment: IPlatformLiveEvent, ILiveEventChatMessage {
|
|||||||
obj.getOrThrow(config, "name", contextName),
|
obj.getOrThrow(config, "name", contextName),
|
||||||
obj.getOrThrow(config, "thumbnail", contextName, true),
|
obj.getOrThrow(config, "thumbnail", contextName, true),
|
||||||
obj.getOrThrow(config, "message", contextName),
|
obj.getOrThrow(config, "message", contextName),
|
||||||
colorName, badges,
|
colorName, badges);
|
||||||
obj.getOrDefault(config, "time", contextName, -1) ?: -1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,6 @@ package com.futo.platformplayer.api.media.models.live
|
|||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
import com.futo.platformplayer.ensureIsBusy
|
|
||||||
import com.futo.platformplayer.getOrDefault
|
import com.futo.platformplayer.getOrDefault
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
|
||||||
@@ -21,8 +20,6 @@ class LiveEventDonation: IPlatformLiveEvent, ILiveEventChatMessage {
|
|||||||
|
|
||||||
var expire: Int = 6000;
|
var expire: Int = 6000;
|
||||||
|
|
||||||
override var time: Long = -1;
|
|
||||||
|
|
||||||
|
|
||||||
constructor(name: String, thumbnail: String?, message: String, amount: String, expire: Int = 6000, colorDonation: String? = null) {
|
constructor(name: String, thumbnail: String?, message: String, amount: String, expire: Int = 6000, colorDonation: String? = null) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
@@ -40,7 +37,6 @@ class LiveEventDonation: IPlatformLiveEvent, ILiveEventChatMessage {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventDonation {
|
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventDonation {
|
||||||
obj.ensureIsBusy();
|
|
||||||
val contextName = "LiveEventDonation"
|
val contextName = "LiveEventDonation"
|
||||||
return LiveEventDonation(
|
return LiveEventDonation(
|
||||||
obj.getOrThrow(config, "name", contextName),
|
obj.getOrThrow(config, "name", contextName),
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package com.futo.platformplayer.api.media.models.live
|
|||||||
|
|
||||||
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.ensureIsBusy
|
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
|
||||||
class LiveEventEmojis: IPlatformLiveEvent {
|
class LiveEventEmojis: IPlatformLiveEvent {
|
||||||
@@ -10,17 +9,15 @@ class LiveEventEmojis: IPlatformLiveEvent {
|
|||||||
|
|
||||||
val emojis: HashMap<String, String>;
|
val emojis: HashMap<String, String>;
|
||||||
|
|
||||||
override var time: Long = -1;
|
|
||||||
|
|
||||||
constructor(emojis: HashMap<String, String>) {
|
constructor(emojis: HashMap<String, String>) {
|
||||||
this.emojis = emojis;
|
this.emojis = emojis;
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventEmojis {
|
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventEmojis {
|
||||||
obj.ensureIsBusy();
|
|
||||||
val contextName = "LiveEventEmojis"
|
val contextName = "LiveEventEmojis"
|
||||||
return LiveEventEmojis(obj.getOrThrow(config, "emojis", contextName));
|
return LiveEventEmojis(
|
||||||
|
obj.getOrThrow(config, "emojis", contextName));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,8 +2,6 @@ package com.futo.platformplayer.api.media.models.live
|
|||||||
|
|
||||||
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.ensureIsBusy
|
|
||||||
import com.futo.platformplayer.getOrDefault
|
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
|
||||||
class LiveEventRaid: IPlatformLiveEvent {
|
class LiveEventRaid: IPlatformLiveEvent {
|
||||||
@@ -12,26 +10,20 @@ class LiveEventRaid: IPlatformLiveEvent {
|
|||||||
val targetName: String;
|
val targetName: String;
|
||||||
val targetThumbnail: String;
|
val targetThumbnail: String;
|
||||||
val targetUrl: String;
|
val targetUrl: String;
|
||||||
val isOutgoing: Boolean;
|
|
||||||
|
|
||||||
override var time: Long = -1;
|
constructor(name: String, url: String, thumbnail: String) {
|
||||||
|
|
||||||
constructor(name: String, url: String, thumbnail: String, isOutgoing: Boolean) {
|
|
||||||
this.targetName = name;
|
this.targetName = name;
|
||||||
this.targetUrl = url;
|
this.targetUrl = url;
|
||||||
this.targetThumbnail = thumbnail;
|
this.targetThumbnail = thumbnail;
|
||||||
this.isOutgoing = isOutgoing;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventRaid {
|
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventRaid {
|
||||||
obj.ensureIsBusy();
|
|
||||||
val contextName = "LiveEventRaid"
|
val contextName = "LiveEventRaid"
|
||||||
return LiveEventRaid(
|
return LiveEventRaid(
|
||||||
obj.getOrThrow(config, "targetName", contextName),
|
obj.getOrThrow(config, "targetName", contextName),
|
||||||
obj.getOrThrow(config, "targetUrl", contextName),
|
obj.getOrThrow(config, "targetUrl", contextName),
|
||||||
obj.getOrThrow(config, "targetThumbnail", contextName),
|
obj.getOrThrow(config, "targetThumbnail", contextName));
|
||||||
obj.getOrDefault<Boolean>(config, "isOutgoing", contextName, true) ?: true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,6 @@ package com.futo.platformplayer.api.media.models.live
|
|||||||
|
|
||||||
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.ensureIsBusy
|
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
|
||||||
class LiveEventViewCount: IPlatformLiveEvent {
|
class LiveEventViewCount: IPlatformLiveEvent {
|
||||||
@@ -10,15 +9,12 @@ class LiveEventViewCount: IPlatformLiveEvent {
|
|||||||
|
|
||||||
val viewCount: Int;
|
val viewCount: Int;
|
||||||
|
|
||||||
override var time: Long = -1;
|
|
||||||
|
|
||||||
constructor(viewCount: Int) {
|
constructor(viewCount: Int) {
|
||||||
this.viewCount = viewCount;
|
this.viewCount = viewCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventViewCount {
|
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventViewCount {
|
||||||
obj.ensureIsBusy();
|
|
||||||
val contextName = "LiveEventViewCount"
|
val contextName = "LiveEventViewCount"
|
||||||
return LiveEventViewCount(
|
return LiveEventViewCount(
|
||||||
obj.getOrThrow(config, "viewCount", contextName));
|
obj.getOrThrow(config, "viewCount", contextName));
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ import com.futo.platformplayer.api.media.exceptions.UnknownPlatformException
|
|||||||
enum class TextType(val value: Int) {
|
enum class TextType(val value: Int) {
|
||||||
RAW(0),
|
RAW(0),
|
||||||
HTML(1),
|
HTML(1),
|
||||||
MARKUP(2),
|
MARKUP(2);
|
||||||
CODE(3);
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromInt(value: Int): TextType
|
fun fromInt(value: Int): TextType
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package com.futo.platformplayer.api.media.models.ratings
|
|||||||
import com.caoccao.javet.values.V8Value
|
import com.caoccao.javet.values.V8Value
|
||||||
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.ensureIsBusy
|
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
import com.futo.platformplayer.orDefault
|
import com.futo.platformplayer.orDefault
|
||||||
import com.futo.platformplayer.serializers.IRatingSerializer
|
import com.futo.platformplayer.serializers.IRatingSerializer
|
||||||
@@ -14,12 +13,8 @@ interface IRating {
|
|||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromV8OrDefault(config: IV8PluginConfig, obj: V8Value?, default: IRating): IRating {
|
fun fromV8OrDefault(config: IV8PluginConfig, obj: V8Value?, default: IRating) = obj.orDefault(default) { fromV8(config, it as V8ValueObject) };
|
||||||
obj?.ensureIsBusy();
|
|
||||||
return 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 = "Rating") : IRating {
|
||||||
obj.ensureIsBusy();
|
|
||||||
val t = RatingType.fromInt(obj.getOrThrow<Int>(config, "type", contextName));
|
val t = RatingType.fromInt(obj.getOrThrow<Int>(config, "type", contextName));
|
||||||
return when(t) {
|
return when(t) {
|
||||||
RatingType.LIKES -> RatingLikes.fromV8(config, obj);
|
RatingType.LIKES -> RatingLikes.fromV8(config, obj);
|
||||||
|
|||||||
-2
@@ -2,7 +2,6 @@ package com.futo.platformplayer.api.media.models.ratings
|
|||||||
|
|
||||||
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.ensureIsBusy
|
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -15,7 +14,6 @@ class RatingLikeDislikes(val likes: Long, val dislikes: Long) : IRating {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : RatingLikeDislikes {
|
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : RatingLikeDislikes {
|
||||||
obj.ensureIsBusy();
|
|
||||||
return RatingLikeDislikes(obj.getOrThrow(config, "likes", "RatingLikeDislikes"), obj.getOrThrow(config, "dislikes", "RatingLikeDislikes"));
|
return RatingLikeDislikes(obj.getOrThrow(config, "likes", "RatingLikeDislikes"), obj.getOrThrow(config, "dislikes", "RatingLikeDislikes"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package com.futo.platformplayer.api.media.models.ratings
|
|||||||
|
|
||||||
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.ensureIsBusy
|
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -14,7 +13,6 @@ class RatingLikes(val likes: Long) : IRating {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : RatingLikes {
|
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : RatingLikes {
|
||||||
obj.ensureIsBusy();
|
|
||||||
return RatingLikes(obj.getOrThrow(config, "likes", "RatingLikes"));
|
return RatingLikes(obj.getOrThrow(config, "likes", "RatingLikes"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package com.futo.platformplayer.api.media.models.ratings
|
|||||||
|
|
||||||
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.ensureIsBusy
|
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -14,7 +13,6 @@ class RatingScaler(val value: Float) : IRating {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : RatingScaler {
|
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : RatingScaler {
|
||||||
obj.ensureIsBusy()
|
|
||||||
return RatingScaler(obj.getOrThrow(config, "value", "RatingScaler"));
|
return RatingScaler(obj.getOrThrow(config, "value", "RatingScaler"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-17
@@ -2,24 +2,10 @@ package com.futo.platformplayer.api.media.models.streams
|
|||||||
|
|
||||||
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.platforms.local.models.sources.LocalAudioContentSource
|
|
||||||
import com.futo.platformplayer.downloads.VideoLocal
|
import com.futo.platformplayer.downloads.VideoLocal
|
||||||
|
|
||||||
|
|
||||||
class LocalVideoUnMuxedSourceDescriptor : VideoUnMuxedSourceDescriptor {
|
class LocalVideoUnMuxedSourceDescriptor(private val video: VideoLocal) : VideoUnMuxedSourceDescriptor() {
|
||||||
override val videoSources: Array<IVideoSource>;
|
override val videoSources: Array<IVideoSource> get() = video.videoSource.toTypedArray();
|
||||||
override val audioSources: Array<IAudioSource>;
|
override val audioSources: Array<IAudioSource> get() = video.audioSource.toTypedArray();
|
||||||
|
|
||||||
constructor(video: VideoLocal) {
|
|
||||||
videoSources = video.videoSource.toTypedArray();
|
|
||||||
audioSources = video.audioSource.toTypedArray();
|
|
||||||
}
|
|
||||||
constructor(audio: LocalAudioContentSource) {
|
|
||||||
videoSources = arrayOf()
|
|
||||||
audioSources = arrayOf(audio);
|
|
||||||
}
|
|
||||||
constructor(videoSources: Array<IVideoSource>, audioSources: Array<IAudioSource>) {
|
|
||||||
this.videoSources = videoSources;
|
|
||||||
this.audioSources = audioSources;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+1
-2
@@ -14,8 +14,7 @@ class AudioUrlSource(
|
|||||||
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,
|
override var original: Boolean = false
|
||||||
var isLocal: Boolean = false
|
|
||||||
) : IAudioUrlSource, IStreamMetaDataSource{
|
) : IAudioUrlSource, IStreamMetaDataSource{
|
||||||
override var streamMetaData: StreamMetaData? = null;
|
override var streamMetaData: StreamMetaData? = null;
|
||||||
|
|
||||||
|
|||||||
-1
@@ -41,7 +41,6 @@ class HLSVariantSubtitleUrlSource(
|
|||||||
override val format: String,
|
override val format: String,
|
||||||
) : ISubtitleSource {
|
) : ISubtitleSource {
|
||||||
override val hasFetch: Boolean = false
|
override val hasFetch: Boolean = false
|
||||||
override val language: String? = null
|
|
||||||
|
|
||||||
override fun getSubtitles(): String? {
|
override fun getSubtitles(): String? {
|
||||||
return null
|
return null
|
||||||
|
|||||||
+1
-4
@@ -9,15 +9,13 @@ class LocalSubtitleSource : ISubtitleSource {
|
|||||||
override val name: String;
|
override val name: String;
|
||||||
override val url: String?;
|
override val url: String?;
|
||||||
override val format: String?;
|
override val format: String?;
|
||||||
override val language: String?
|
|
||||||
override val hasFetch: Boolean get() = false;
|
override val hasFetch: Boolean get() = false;
|
||||||
|
|
||||||
val filePath: String;
|
val filePath: String;
|
||||||
|
|
||||||
constructor(name: String, language: String?, format: String?, filePath: String) {
|
constructor(name: String, format: String?, filePath: String) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.format = format;
|
this.format = format;
|
||||||
this.language = language
|
|
||||||
this.filePath = filePath;
|
this.filePath = filePath;
|
||||||
this.url = Uri.fromFile(File(filePath)).toString();
|
this.url = Uri.fromFile(File(filePath)).toString();
|
||||||
}
|
}
|
||||||
@@ -34,7 +32,6 @@ class LocalSubtitleSource : ISubtitleSource {
|
|||||||
fun fromSource(source: SubtitleRawSource, path: String): LocalSubtitleSource {
|
fun fromSource(source: SubtitleRawSource, path: String): LocalSubtitleSource {
|
||||||
return LocalSubtitleSource(
|
return LocalSubtitleSource(
|
||||||
source.name,
|
source.name,
|
||||||
source.language,
|
|
||||||
source.format,
|
source.format,
|
||||||
path
|
path
|
||||||
);
|
);
|
||||||
|
|||||||
-1
@@ -6,7 +6,6 @@ import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
|||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
class SubtitleRawSource(
|
class SubtitleRawSource(
|
||||||
override val name: String,
|
override val name: String,
|
||||||
override val language: String?,
|
|
||||||
override val format: String?,
|
override val format: String?,
|
||||||
val _subtitles: String,
|
val _subtitles: String,
|
||||||
override val url: String? = null,
|
override val url: String? = null,
|
||||||
|
|||||||
+1
-2
@@ -14,8 +14,7 @@ open class VideoUrlSource(
|
|||||||
override val codec : String = "",
|
override val codec : String = "",
|
||||||
override val bitrate : Int? = 0,
|
override val bitrate : Int? = 0,
|
||||||
|
|
||||||
override var priority: Boolean = false,
|
override var priority: Boolean = false
|
||||||
var isLocal: Boolean = false
|
|
||||||
) : IVideoUrlSource, IStreamMetaDataSource {
|
) : IVideoUrlSource, IStreamMetaDataSource {
|
||||||
override var streamMetaData: StreamMetaData? = null;
|
override var streamMetaData: StreamMetaData? = null;
|
||||||
|
|
||||||
|
|||||||
-1
@@ -7,7 +7,6 @@ interface ISubtitleSource {
|
|||||||
val url: String?;
|
val url: String?;
|
||||||
val format: String?;
|
val format: String?;
|
||||||
val hasFetch: Boolean;
|
val hasFetch: Boolean;
|
||||||
val language: String?
|
|
||||||
|
|
||||||
fun getSubtitles(): String?;
|
fun getSubtitles(): String?;
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package com.futo.platformplayer.api.media.models.video
|
|||||||
|
|
||||||
import com.futo.platformplayer.api.media.models.Thumbnails
|
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 java.time.OffsetDateTime
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A search result representing a video (overview data)
|
* A search result representing a video (overview data)
|
||||||
@@ -13,9 +12,6 @@ interface IPlatformVideo : IPlatformContent {
|
|||||||
val duration: Long;
|
val duration: Long;
|
||||||
val viewCount: Long;
|
val viewCount: Long;
|
||||||
|
|
||||||
val playbackTime: Long;
|
|
||||||
val playbackDate: OffsetDateTime?;
|
|
||||||
|
|
||||||
val isLive : Boolean;
|
val isLive : Boolean;
|
||||||
|
|
||||||
val isShort: Boolean;
|
val isShort: Boolean;
|
||||||
|
|||||||
-122
@@ -1,122 +0,0 @@
|
|||||||
package com.futo.platformplayer.api.media.models.video
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.net.Uri
|
|
||||||
import android.provider.MediaStore
|
|
||||||
import android.provider.OpenableColumns
|
|
||||||
import androidx.core.net.toUri
|
|
||||||
import com.futo.platformplayer.api.media.IPlatformClient
|
|
||||||
import com.futo.platformplayer.api.media.PlatformID
|
|
||||||
import com.futo.platformplayer.api.media.Serializer
|
|
||||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
|
||||||
import com.futo.platformplayer.api.media.models.Thumbnails
|
|
||||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
|
||||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
|
||||||
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
|
||||||
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.streams.IVideoSourceDescriptor
|
|
||||||
import com.futo.platformplayer.api.media.models.streams.LocalVideoUnMuxedSourceDescriptor
|
|
||||||
import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor
|
|
||||||
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
|
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.AudioUrlSource
|
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
|
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.LocalAudioSource
|
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.LocalVideoSource
|
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.SubtitleRawSource
|
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.VideoUrlSource
|
|
||||||
import com.futo.platformplayer.api.media.platforms.local.models.LocalVideoMuxedSourceDescriptor
|
|
||||||
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalAudioContentSource
|
|
||||||
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalVideoContentSource
|
|
||||||
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalVideoFileSource
|
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
|
||||||
import com.futo.platformplayer.others.Language
|
|
||||||
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
|
||||||
import com.futo.platformplayer.states.StateApp
|
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import java.time.OffsetDateTime
|
|
||||||
|
|
||||||
@kotlinx.serialization.Serializable
|
|
||||||
open class LocalVideoDetails(
|
|
||||||
override val id: PlatformID,
|
|
||||||
override val name: String,
|
|
||||||
override val thumbnails: Thumbnails,
|
|
||||||
override val author: PlatformAuthorLink,
|
|
||||||
override val url: String,
|
|
||||||
override val duration: Long,
|
|
||||||
|
|
||||||
val mimeType: String? = null,
|
|
||||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
|
||||||
override val datetime: OffsetDateTime?
|
|
||||||
) : IPlatformVideo, IPlatformVideoDetails {
|
|
||||||
final override val contentType: ContentType get() = ContentType.MEDIA;
|
|
||||||
|
|
||||||
override var playbackTime: Long = -1;
|
|
||||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
|
||||||
override var playbackDate: OffsetDateTime? = null;
|
|
||||||
|
|
||||||
override val isLive: Boolean get() = false;
|
|
||||||
|
|
||||||
override val dash: IDashManifestSource? get() = null;
|
|
||||||
override val hls: IHLSManifestSource? get() = null;
|
|
||||||
override val live: IVideoSource? get() = null;
|
|
||||||
|
|
||||||
|
|
||||||
override val shareUrl: String = ""
|
|
||||||
override val viewCount: Long = -1
|
|
||||||
override val rating: IRating = RatingLikes(0)
|
|
||||||
override val description: String = "";
|
|
||||||
override val video: IVideoSourceDescriptor = (if(mimeType?.startsWith("audio/") ?: false)
|
|
||||||
(LocalVideoUnMuxedSourceDescriptor(
|
|
||||||
arrayOf(),
|
|
||||||
arrayOf(LocalAudioContentSource(url, mimeType ?: "", name, duration))
|
|
||||||
))
|
|
||||||
else (LocalVideoMuxedSourceDescriptor(
|
|
||||||
LocalVideoContentSource(url, mimeType ?: "", name, duration)
|
|
||||||
))
|
|
||||||
);
|
|
||||||
override val preview: ISerializedVideoSourceDescriptor? = null;
|
|
||||||
|
|
||||||
override val subtitles: List<SubtitleRawSource> = listOf()
|
|
||||||
override val isShort: Boolean = false
|
|
||||||
|
|
||||||
fun toJson() : String {
|
|
||||||
return Json.encodeToString(this);
|
|
||||||
}
|
|
||||||
fun fromJson(str : String) : SerializedPlatformVideoDetails {
|
|
||||||
return Serializer.json.decodeFromString<SerializedPlatformVideoDetails>(str);
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? = null;
|
|
||||||
override fun getPlaybackTracker(): IPlaybackTracker? = null;
|
|
||||||
override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? = null;
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun fromFile(name: String, filePath: String, mimeType: String? = null) : LocalVideoDetails {
|
|
||||||
if(filePath.startsWith("content://"))
|
|
||||||
return fromContent(filePath, mimeType);
|
|
||||||
|
|
||||||
return LocalVideoDetails(PlatformID("FILE", filePath, null, 0, -1),
|
|
||||||
name, Thumbnails(), PlatformAuthorLink.UNKNOWN, filePath, -1, mimeType, null);
|
|
||||||
}
|
|
||||||
fun fromContent(contentUrl: String, mimeType: String? = null) : LocalVideoDetails {
|
|
||||||
var nameToUse = getFileNameFromContentUrl(contentUrl) ?: "File";
|
|
||||||
|
|
||||||
return LocalVideoDetails(PlatformID("FILE", contentUrl, null, 0, -1),
|
|
||||||
nameToUse, Thumbnails(), PlatformAuthorLink.UNKNOWN, contentUrl, -1, mimeType, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("Range")
|
|
||||||
private fun getFileNameFromContentUrl(url: String): String? {
|
|
||||||
val cursor = StateApp.instance.context.contentResolver.query(url.toUri(), null, null, null, null);
|
|
||||||
cursor?.moveToFirst();
|
|
||||||
val fileName = cursor?.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
|
|
||||||
cursor?.close();
|
|
||||||
return fileName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+3
-6
@@ -3,10 +3,11 @@ package com.futo.platformplayer.api.media.models.video
|
|||||||
import com.futo.platformplayer.api.media.PlatformID
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
import com.futo.platformplayer.api.media.Serializer
|
import com.futo.platformplayer.api.media.Serializer
|
||||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
import com.futo.platformplayer.api.media.models.Thumbnail
|
|
||||||
import com.futo.platformplayer.api.media.models.Thumbnails
|
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||||
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||||
|
import com.futo.polycentric.core.combineHashCodes
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonNames
|
import kotlinx.serialization.json.JsonNames
|
||||||
@@ -17,7 +18,7 @@ open class SerializedPlatformVideo(
|
|||||||
override val contentType: ContentType = ContentType.MEDIA,
|
override val contentType: ContentType = ContentType.MEDIA,
|
||||||
override val id: PlatformID,
|
override val id: PlatformID,
|
||||||
override val name: String,
|
override val name: String,
|
||||||
override val thumbnails: Thumbnails = Thumbnails(),
|
override val thumbnails: Thumbnails,
|
||||||
override val author: PlatformAuthorLink,
|
override val author: PlatformAuthorLink,
|
||||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
||||||
@JsonNames("datetime", "dateTime")
|
@JsonNames("datetime", "dateTime")
|
||||||
@@ -32,10 +33,6 @@ open class SerializedPlatformVideo(
|
|||||||
|
|
||||||
override val isLive: Boolean = false;
|
override val isLive: Boolean = false;
|
||||||
|
|
||||||
override var playbackTime: Long = -1;
|
|
||||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
|
||||||
override var playbackDate: OffsetDateTime? = null;
|
|
||||||
|
|
||||||
override fun toJson() : String {
|
override fun toJson() : String {
|
||||||
return Json.encodeToString(this);
|
return Json.encodeToString(this);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-4
@@ -13,6 +13,7 @@ import com.futo.platformplayer.api.media.models.ratings.IRating
|
|||||||
import com.futo.platformplayer.api.media.models.streams.sources.*
|
import com.futo.platformplayer.api.media.models.streams.sources.*
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
@@ -42,10 +43,6 @@ open class SerializedPlatformVideoDetails(
|
|||||||
) : IPlatformVideo, IPlatformVideoDetails {
|
) : IPlatformVideo, IPlatformVideoDetails {
|
||||||
final override val contentType: ContentType get() = ContentType.MEDIA;
|
final override val contentType: ContentType get() = ContentType.MEDIA;
|
||||||
|
|
||||||
override var playbackTime: Long = -1;
|
|
||||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
|
||||||
override var playbackDate: OffsetDateTime? = null;
|
|
||||||
|
|
||||||
override val isLive: Boolean get() = false;
|
override val isLive: Boolean get() = false;
|
||||||
|
|
||||||
override val dash: IDashManifestSource? get() = null;
|
override val dash: IDashManifestSource? get() = null;
|
||||||
|
|||||||
@@ -54,12 +54,8 @@ class DevJSClient : JSClient {
|
|||||||
return DevJSClient(context, config, _devScript, _auth, _captcha, devID, descriptor.settings);
|
return DevJSClient(context, config, _devScript, _auth, _captcha, devID, descriptor.settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getCopy(privateCopy: Boolean, noSaveState: Boolean): JSClient {
|
override fun getCopy(privateCopy: Boolean): JSClient {
|
||||||
val client = DevJSClient(_context, descriptor, _script, if(!privateCopy) _auth else null, _captcha, if (noSaveState) null else saveState(), devID);
|
return DevJSClient(_context, descriptor, _script, if(!privateCopy) _auth else null, _captcha, saveState(), devID);
|
||||||
client.setReloadData(getReloadData(true));
|
|
||||||
if (noSaveState)
|
|
||||||
client.initialize()
|
|
||||||
return client
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun initialize() {
|
override fun initialize() {
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import com.caoccao.javet.values.reference.V8ValueObject
|
|||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.api.media.IPlatformClient
|
import com.futo.platformplayer.api.media.IPlatformClient
|
||||||
import com.futo.platformplayer.api.media.PlatformClientCapabilities
|
import com.futo.platformplayer.api.media.PlatformClientCapabilities
|
||||||
import com.futo.platformplayer.api.media.models.IPlatformChannelContent
|
|
||||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
@@ -23,7 +22,6 @@ import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
|
|||||||
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
||||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
||||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
|
||||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSCallDocs
|
import com.futo.platformplayer.api.media.platforms.js.internal.JSCallDocs
|
||||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSDocs
|
import com.futo.platformplayer.api.media.platforms.js.internal.JSDocs
|
||||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSDocsParameter
|
import com.futo.platformplayer.api.media.platforms.js.internal.JSDocsParameter
|
||||||
@@ -33,7 +31,6 @@ import com.futo.platformplayer.api.media.platforms.js.internal.JSParameterDocs
|
|||||||
import com.futo.platformplayer.api.media.platforms.js.models.IJSContent
|
import com.futo.platformplayer.api.media.platforms.js.models.IJSContent
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.IJSContentDetails
|
import com.futo.platformplayer.api.media.platforms.js.models.IJSContentDetails
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSChannel
|
import com.futo.platformplayer.api.media.platforms.js.models.JSChannel
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSChannelContentPager
|
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSChannelPager
|
import com.futo.platformplayer.api.media.platforms.js.models.JSChannelPager
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSChapter
|
import com.futo.platformplayer.api.media.platforms.js.models.JSChapter
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSComment
|
import com.futo.platformplayer.api.media.platforms.js.models.JSComment
|
||||||
@@ -44,7 +41,6 @@ import com.futo.platformplayer.api.media.platforms.js.models.JSLiveEventPager
|
|||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSPlaybackTracker
|
import com.futo.platformplayer.api.media.platforms.js.models.JSPlaybackTracker
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSPlaylistDetails
|
import com.futo.platformplayer.api.media.platforms.js.models.JSPlaylistDetails
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSPlaylistPager
|
import com.futo.platformplayer.api.media.platforms.js.models.JSPlaylistPager
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSVideoPager
|
|
||||||
import com.futo.platformplayer.api.media.structures.EmptyPager
|
import com.futo.platformplayer.api.media.structures.EmptyPager
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
@@ -61,13 +57,9 @@ 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.states.StatePlugins
|
import com.futo.platformplayer.states.StatePlugins
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
|
||||||
import kotlinx.coroutines.Deferred
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.util.Random
|
|
||||||
import kotlin.Exception
|
import kotlin.Exception
|
||||||
import kotlin.reflect.full.findAnnotations
|
import kotlin.reflect.full.findAnnotations
|
||||||
import kotlin.reflect.jvm.kotlinFunction
|
import kotlin.reflect.jvm.kotlinFunction
|
||||||
@@ -89,8 +81,6 @@ open class JSClient : IPlatformClient {
|
|||||||
private var _channelCapabilities: ResultCapabilities? = null;
|
private var _channelCapabilities: ResultCapabilities? = null;
|
||||||
private var _peekChannelTypes: List<String>? = null;
|
private var _peekChannelTypes: List<String>? = null;
|
||||||
|
|
||||||
private var _usedReloadData: String? = null;
|
|
||||||
|
|
||||||
protected val _script: String;
|
protected val _script: String;
|
||||||
|
|
||||||
private var _initialized: Boolean = false;
|
private var _initialized: Boolean = false;
|
||||||
@@ -103,17 +93,17 @@ open class JSClient : IPlatformClient {
|
|||||||
|
|
||||||
override val id: String get() = config.id;
|
override val id: String get() = config.id;
|
||||||
override val name: String get() = config.name;
|
override val name: String get() = config.name;
|
||||||
override val icon: ImageVariable get() = StatePlatform.instance.getPlatformIcon(config.id) ?: ImageVariable(config.absoluteIconUrl, null, null)
|
override val icon: ImageVariable;
|
||||||
override var capabilities: PlatformClientCapabilities = PlatformClientCapabilities();
|
override var capabilities: PlatformClientCapabilities = PlatformClientCapabilities();
|
||||||
|
|
||||||
|
private val _busyLock = Object();
|
||||||
|
private var _busyCounter = 0;
|
||||||
private var _busyAction = "";
|
private var _busyAction = "";
|
||||||
val isBusy: Boolean get() = _plugin.isBusy;
|
val isBusy: Boolean get() = _busyCounter > 0;
|
||||||
val isBusyAction: String get() {
|
val isBusyAction: String get() {
|
||||||
return _busyAction;
|
return _busyAction;
|
||||||
}
|
}
|
||||||
|
|
||||||
val declareOnEnable = HashMap<String, String>();
|
|
||||||
|
|
||||||
val settings: HashMap<String, String?> get() = descriptor.settings;
|
val settings: HashMap<String, String?> get() = descriptor.settings;
|
||||||
|
|
||||||
val flags: Array<String>;
|
val flags: Array<String>;
|
||||||
@@ -126,7 +116,6 @@ open class JSClient : IPlatformClient {
|
|||||||
|
|
||||||
val enableInSearch get() = descriptor.appSettings.tabEnabled.enableSearch ?: true
|
val enableInSearch get() = descriptor.appSettings.tabEnabled.enableSearch ?: true
|
||||||
val enableInHome get() = descriptor.appSettings.tabEnabled.enableHome ?: true
|
val enableInHome get() = descriptor.appSettings.tabEnabled.enableHome ?: true
|
||||||
val enableInShorts get() = descriptor.appSettings.tabEnabled.enableShorts ?: true
|
|
||||||
|
|
||||||
fun getSubscriptionRateLimit(): Int? {
|
fun getSubscriptionRateLimit(): Int? {
|
||||||
val pluginRateLimit = config.subscriptionRateLimit;
|
val pluginRateLimit = config.subscriptionRateLimit;
|
||||||
@@ -147,14 +136,15 @@ open class JSClient : IPlatformClient {
|
|||||||
constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String? = null) {
|
constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String? = null) {
|
||||||
this._context = context;
|
this._context = context;
|
||||||
this.config = descriptor.config;
|
this.config = descriptor.config;
|
||||||
|
icon = StatePlatform.instance.getPlatformIcon(config.id) ?: ImageVariable(config.absoluteIconUrl, null, null);
|
||||||
this.descriptor = descriptor;
|
this.descriptor = descriptor;
|
||||||
_injectedSaveState = saveState;
|
_injectedSaveState = saveState;
|
||||||
_auth = descriptor.getAuth();
|
_auth = descriptor.getAuth();
|
||||||
_captcha = descriptor.getCaptchaData();
|
_captcha = descriptor.getCaptchaData();
|
||||||
flags = descriptor.flags.toTypedArray();
|
flags = descriptor.flags.toTypedArray();
|
||||||
|
|
||||||
_httpClient = JSHttpClient(this, null, _captcha, config);
|
_httpClient = JSHttpClient(this, null, _captcha);
|
||||||
_httpClientAuth = JSHttpClient(this, _auth, _captcha, config);
|
_httpClientAuth = JSHttpClient(this, _auth, _captcha);
|
||||||
_plugin = V8Plugin(context, descriptor.config, null, _httpClient, _httpClientAuth);
|
_plugin = V8Plugin(context, descriptor.config, null, _httpClient, _httpClientAuth);
|
||||||
_plugin.withDependency(context, "scripts/polyfil.js");
|
_plugin.withDependency(context, "scripts/polyfil.js");
|
||||||
_plugin.withDependency(context, "scripts/source.js");
|
_plugin.withDependency(context, "scripts/source.js");
|
||||||
@@ -177,6 +167,7 @@ open class JSClient : IPlatformClient {
|
|||||||
constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String?, script: String, withoutCredentials: Boolean = false) {
|
constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String?, script: String, withoutCredentials: Boolean = false) {
|
||||||
this._context = context;
|
this._context = context;
|
||||||
this.config = descriptor.config;
|
this.config = descriptor.config;
|
||||||
|
icon = StatePlatform.instance.getPlatformIcon(config.id) ?: ImageVariable(config.absoluteIconUrl, null, null);
|
||||||
this.descriptor = descriptor;
|
this.descriptor = descriptor;
|
||||||
_injectedSaveState = saveState;
|
_injectedSaveState = saveState;
|
||||||
if(!withoutCredentials)
|
if(!withoutCredentials)
|
||||||
@@ -186,8 +177,8 @@ open class JSClient : IPlatformClient {
|
|||||||
_captcha = descriptor.getCaptchaData();
|
_captcha = descriptor.getCaptchaData();
|
||||||
flags = descriptor.flags.toTypedArray();
|
flags = descriptor.flags.toTypedArray();
|
||||||
|
|
||||||
_httpClient = JSHttpClient(this, null, _captcha, config);
|
_httpClient = JSHttpClient(this, null, _captcha);
|
||||||
_httpClientAuth = JSHttpClient(this, _auth, _captcha, config);
|
_httpClientAuth = JSHttpClient(this, _auth, _captcha);
|
||||||
_plugin = V8Plugin(context, descriptor.config, script, _httpClient, _httpClientAuth);
|
_plugin = V8Plugin(context, descriptor.config, script, _httpClient, _httpClientAuth);
|
||||||
_plugin.withDependency(context, "scripts/polyfil.js");
|
_plugin.withDependency(context, "scripts/polyfil.js");
|
||||||
_plugin.withDependency(context, "scripts/source.js");
|
_plugin.withDependency(context, "scripts/source.js");
|
||||||
@@ -202,12 +193,8 @@ open class JSClient : IPlatformClient {
|
|||||||
_plugin.changeAllowDevSubmit(descriptor.appSettings.allowDeveloperSubmit);
|
_plugin.changeAllowDevSubmit(descriptor.appSettings.allowDeveloperSubmit);
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun getCopy(withoutCredentials: Boolean = false, noSaveState: Boolean = false): JSClient {
|
open fun getCopy(withoutCredentials: Boolean = false): JSClient {
|
||||||
val client = JSClient(_context, descriptor, if (noSaveState) null else saveState(), _script, withoutCredentials);
|
return JSClient(_context, descriptor, saveState(), _script, withoutCredentials);
|
||||||
client.setReloadData(getReloadData(true));
|
|
||||||
if (noSaveState)
|
|
||||||
client.initialize()
|
|
||||||
return client
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getUnderlyingPlugin(): V8Plugin {
|
fun getUnderlyingPlugin(): V8Plugin {
|
||||||
@@ -221,31 +208,12 @@ open class JSClient : IPlatformClient {
|
|||||||
return plugin.httpClientOthers[id];
|
return plugin.httpClientOthers[id];
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setReloadData(data: String?) {
|
|
||||||
if(data == null) {
|
|
||||||
if(declareOnEnable.containsKey("__reloadData"))
|
|
||||||
declareOnEnable.remove("__reloadData");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
declareOnEnable.put("__reloadData", data ?: "");
|
|
||||||
}
|
|
||||||
fun getReloadData(orLast: Boolean): String? {
|
|
||||||
if(declareOnEnable.containsKey("__reloadData"))
|
|
||||||
return declareOnEnable["__reloadData"];
|
|
||||||
else if(orLast)
|
|
||||||
return _usedReloadData;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun initialize() {
|
override fun initialize() {
|
||||||
if (_initialized) return
|
Logger.i(TAG, "Plugin [${config.name}] initializing");
|
||||||
|
|
||||||
plugin.start();
|
plugin.start();
|
||||||
|
|
||||||
plugin.execute("plugin.config = ${Json.encodeToString(config)}");
|
plugin.execute("plugin.config = ${Json.encodeToString(config)}");
|
||||||
plugin.execute("plugin.settings = parseSettings(${Json.encodeToString(descriptor.getSettingsWithDefaults())})");
|
plugin.execute("plugin.settings = parseSettings(${Json.encodeToString(descriptor.getSettingsWithDefaults())})");
|
||||||
|
|
||||||
|
|
||||||
descriptor.appSettings.loadDefaults(descriptor.config);
|
descriptor.appSettings.loadDefaults(descriptor.config);
|
||||||
|
|
||||||
_initialized = true;
|
_initialized = true;
|
||||||
@@ -270,8 +238,7 @@ open class JSClient : IPlatformClient {
|
|||||||
hasGetContentChapters = plugin.executeBoolean("!!source.getContentChapters") ?: false,
|
hasGetContentChapters = plugin.executeBoolean("!!source.getContentChapters") ?: false,
|
||||||
hasPeekChannelContents = plugin.executeBoolean("!!source.peekChannelContents") ?: false,
|
hasPeekChannelContents = plugin.executeBoolean("!!source.peekChannelContents") ?: false,
|
||||||
hasGetChannelPlaylists = plugin.executeBoolean("!!source.getChannelPlaylists") ?: false,
|
hasGetChannelPlaylists = plugin.executeBoolean("!!source.getChannelPlaylists") ?: false,
|
||||||
hasGetContentRecommendations = plugin.executeBoolean("!!source.getContentRecommendations") ?: false,
|
hasGetContentRecommendations = plugin.executeBoolean("!!source.getContentRecommendations") ?: false
|
||||||
hasGetUserHistory = plugin.executeBoolean("!!source.getUserHistory") ?: false
|
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -286,28 +253,19 @@ open class JSClient : IPlatformClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@JSDocs(0, "source.enable()", "Called when the plugin is enabled/started")
|
@JSDocs(0, "source.enable()", "Called when the plugin is enabled/started")
|
||||||
fun enable() = isBusyWith("enable") {
|
fun enable() {
|
||||||
if(!_initialized)
|
if(!_initialized)
|
||||||
initialize();
|
initialize();
|
||||||
for(toDeclare in declareOnEnable) {
|
|
||||||
plugin.execute("var ${toDeclare.key} = " + Json.encodeToString(toDeclare.value));
|
|
||||||
}
|
|
||||||
plugin.execute("source.enable(${Json.encodeToString(config)}, parseSettings(${Json.encodeToString(descriptor.getSettingsWithDefaults())}), ${Json.encodeToString(_injectedSaveState)})");
|
plugin.execute("source.enable(${Json.encodeToString(config)}, parseSettings(${Json.encodeToString(descriptor.getSettingsWithDefaults())}), ${Json.encodeToString(_injectedSaveState)})");
|
||||||
|
|
||||||
if(declareOnEnable.containsKey("__reloadData")) {
|
|
||||||
Logger.i(TAG, "Plugin [${config.name}] enabled with reload data: ${declareOnEnable["__reloadData"]}");
|
|
||||||
_usedReloadData = declareOnEnable["__reloadData"];
|
|
||||||
declareOnEnable.remove("__reloadData");
|
|
||||||
}
|
|
||||||
_enabled = true;
|
_enabled = true;
|
||||||
}
|
}
|
||||||
@JSDocs(1, "source.saveState()", "Provide a string that is passed to enable for quicker startup of multiple instances")
|
@JSDocs(1, "source.saveState()", "Provide a string that is passed to enable for quicker startup of multiple instances")
|
||||||
fun saveState(): String? = isBusyWith("saveState") {
|
fun saveState(): String? {
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
if(!capabilities.hasSaveState)
|
if(!capabilities.hasSaveState)
|
||||||
return@isBusyWith null;
|
return null;
|
||||||
val resp = plugin.executeTyped<V8ValueString>("source.saveState()").value;
|
val resp = plugin.executeTyped<V8ValueString>("source.saveState()").value;
|
||||||
return@isBusyWith resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
@JSDocs(1, "source.disable()", "Called before the plugin is disabled/stopped")
|
@JSDocs(1, "source.disable()", "Called before the plugin is disabled/stopped")
|
||||||
@@ -330,13 +288,6 @@ open class JSClient : IPlatformClient {
|
|||||||
plugin.executeTyped("source.getHome()"));
|
plugin.executeTyped("source.getHome()"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JSDocs(2, "source.getShorts()", "Gets the Shorts feed of the platform")
|
|
||||||
override fun getShorts(): IPager<IPlatformVideo> = isBusyWith("getShorts") {
|
|
||||||
ensureEnabled()
|
|
||||||
return@isBusyWith JSVideoPager(config, this,
|
|
||||||
plugin.executeTyped("source.getShorts()"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@JSDocs(3, "source.searchSuggestions(query)", "Gets search suggestions for a given query")
|
@JSDocs(3, "source.searchSuggestions(query)", "Gets search suggestions for a given query")
|
||||||
@JSDocsParameter("query", "Query to complete suggestions for")
|
@JSDocsParameter("query", "Query to complete suggestions for")
|
||||||
override fun searchSuggestions(query: String): Array<String> = isBusyWith("searchSuggestions") {
|
override fun searchSuggestions(query: String): Array<String> = isBusyWith("searchSuggestions") {
|
||||||
@@ -355,10 +306,8 @@ open class JSClient : IPlatformClient {
|
|||||||
return _searchCapabilities!!;
|
return _searchCapabilities!!;
|
||||||
}
|
}
|
||||||
|
|
||||||
return busy {
|
_searchCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getSearchCapabilities()"));
|
||||||
_searchCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getSearchCapabilities()"));
|
return _searchCapabilities!!;
|
||||||
return@busy _searchCapabilities!!;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch(ex: Throwable) {
|
catch(ex: Throwable) {
|
||||||
announcePluginUnhandledException("getSearchCapabilities", ex);
|
announcePluginUnhandledException("getSearchCapabilities", ex);
|
||||||
@@ -386,10 +335,8 @@ open class JSClient : IPlatformClient {
|
|||||||
if (_searchChannelContentsCapabilities != null)
|
if (_searchChannelContentsCapabilities != null)
|
||||||
return _searchChannelContentsCapabilities!!;
|
return _searchChannelContentsCapabilities!!;
|
||||||
|
|
||||||
return busy {
|
_searchChannelContentsCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getSearchChannelContentsCapabilities()"));
|
||||||
_searchChannelContentsCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getSearchChannelContentsCapabilities()"));
|
return _searchChannelContentsCapabilities!!;
|
||||||
return@busy _searchChannelContentsCapabilities!!;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@JSDocs(5, "source.searchChannelContents(query)", "Searches for videos on the platform")
|
@JSDocs(5, "source.searchChannelContents(query)", "Searches for videos on the platform")
|
||||||
@JSDocsParameter("channelUrl", "Channel url to search")
|
@JSDocsParameter("channelUrl", "Channel url to search")
|
||||||
@@ -414,21 +361,17 @@ open class JSClient : IPlatformClient {
|
|||||||
return@isBusyWith JSChannelPager(config, this,
|
return@isBusyWith JSChannelPager(config, this,
|
||||||
plugin.executeTyped("source.searchChannels(${Json.encodeToString(query)})"));
|
plugin.executeTyped("source.searchChannels(${Json.encodeToString(query)})"));
|
||||||
}
|
}
|
||||||
override fun searchChannelsAsContent(query: String): IPager<IPlatformContent> = isBusyWith("searchChannels") {
|
|
||||||
ensureEnabled();
|
|
||||||
return@isBusyWith JSChannelContentPager(config, this, plugin.executeTyped("source.searchChannels(${Json.encodeToString(query)})"), );
|
|
||||||
}
|
|
||||||
|
|
||||||
@JSDocs(6, "source.isChannelUrl(url)", "Validates if an channel url is for this platform")
|
@JSDocs(6, "source.isChannelUrl(url)", "Validates if an channel url is for this platform")
|
||||||
@JSDocsParameter("url", "A channel url (May not be your platform)")
|
@JSDocsParameter("url", "A channel url (May not be your platform)")
|
||||||
override fun isChannelUrl(url: String): Boolean = isBusyWith("isChannelUrl") {
|
override fun isChannelUrl(url: String): Boolean {
|
||||||
try {
|
try {
|
||||||
return@isBusyWith plugin.executeTyped<V8ValueBoolean>("source.isChannelUrl(${Json.encodeToString(url)})")
|
return plugin.executeTyped<V8ValueBoolean>("source.isChannelUrl(${Json.encodeToString(url)})")
|
||||||
.value;
|
.value;
|
||||||
}
|
}
|
||||||
catch(ex: Throwable) {
|
catch(ex: Throwable) {
|
||||||
announcePluginUnhandledException("isChannelUrl", ex);
|
announcePluginUnhandledException("isChannelUrl", ex);
|
||||||
return@isBusyWith false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@JSDocs(7, "source.getChannel(channelUrl)", "Gets a channel by its url")
|
@JSDocs(7, "source.getChannel(channelUrl)", "Gets a channel by its url")
|
||||||
@@ -446,10 +389,9 @@ open class JSClient : IPlatformClient {
|
|||||||
if (_channelCapabilities != null) {
|
if (_channelCapabilities != null) {
|
||||||
return _channelCapabilities!!;
|
return _channelCapabilities!!;
|
||||||
}
|
}
|
||||||
return busy {
|
|
||||||
_channelCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getChannelCapabilities()"));
|
_channelCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getChannelCapabilities()"));
|
||||||
return@busy _channelCapabilities!!;
|
return _channelCapabilities!!;
|
||||||
};
|
|
||||||
}
|
}
|
||||||
catch(ex: Throwable) {
|
catch(ex: Throwable) {
|
||||||
announcePluginUnhandledException("getChannelCapabilities", ex);
|
announcePluginUnhandledException("getChannelCapabilities", ex);
|
||||||
@@ -560,14 +502,14 @@ open class JSClient : IPlatformClient {
|
|||||||
|
|
||||||
@JSDocs(13, "source.isContentDetailsUrl(url)", "Validates if an content url is for this platform")
|
@JSDocs(13, "source.isContentDetailsUrl(url)", "Validates if an content url is for this platform")
|
||||||
@JSDocsParameter("url", "A content url (May not be your platform)")
|
@JSDocsParameter("url", "A content url (May not be your platform)")
|
||||||
override fun isContentDetailsUrl(url: String): Boolean = isBusyWith("isContentDetailsUrl") {
|
override fun isContentDetailsUrl(url: String): Boolean {
|
||||||
try {
|
try {
|
||||||
return@isBusyWith plugin.executeTyped<V8ValueBoolean>("source.isContentDetailsUrl(${Json.encodeToString(url)})")
|
return plugin.executeTyped<V8ValueBoolean>("source.isContentDetailsUrl(${Json.encodeToString(url)})")
|
||||||
.value;
|
.value;
|
||||||
}
|
}
|
||||||
catch(ex: Throwable) {
|
catch(ex: Throwable) {
|
||||||
announcePluginUnhandledException("isContentDetailsUrl", ex);
|
announcePluginUnhandledException("isContentDetailsUrl", ex);
|
||||||
return@isBusyWith false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@JSDocs(14, "source.getContentDetails(url)", "Gets content details by its url")
|
@JSDocs(14, "source.getContentDetails(url)", "Gets content details by its url")
|
||||||
@@ -599,7 +541,7 @@ open class JSClient : IPlatformClient {
|
|||||||
Logger.i(TAG, "JSClient.getPlaybackTracker(${url})");
|
Logger.i(TAG, "JSClient.getPlaybackTracker(${url})");
|
||||||
val tracker = plugin.executeTyped<V8Value>("source.getPlaybackTracker(${Json.encodeToString(url)})");
|
val tracker = plugin.executeTyped<V8Value>("source.getPlaybackTracker(${Json.encodeToString(url)})");
|
||||||
if(tracker is V8ValueObject)
|
if(tracker is V8ValueObject)
|
||||||
return@isBusyWith JSPlaybackTracker(this, tracker);
|
return@isBusyWith JSPlaybackTracker(config, tracker);
|
||||||
else
|
else
|
||||||
return@isBusyWith null;
|
return@isBusyWith null;
|
||||||
}
|
}
|
||||||
@@ -641,6 +583,7 @@ open class JSClient : IPlatformClient {
|
|||||||
plugin.executeTyped("source.getLiveEvents(${Json.encodeToString(url)})"));
|
plugin.executeTyped("source.getLiveEvents(${Json.encodeToString(url)})"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@JSDocs(19, "source.getContentRecommendations(url)", "Gets recommendations of a content page")
|
@JSDocs(19, "source.getContentRecommendations(url)", "Gets recommendations of a content page")
|
||||||
@JSDocsParameter("url", "Url of content")
|
@JSDocsParameter("url", "Url of content")
|
||||||
override fun getContentRecommendations(url: String): IPager<IPlatformContent>? = isBusyWith("getContentRecommendations") {
|
override fun getContentRecommendations(url: String): IPager<IPlatformContent>? = isBusyWith("getContentRecommendations") {
|
||||||
@@ -668,19 +611,17 @@ open class JSClient : IPlatformClient {
|
|||||||
@JSOptional
|
@JSOptional
|
||||||
@JSDocs(20, "source.isPlaylistUrl(url)", "Validates if a playlist url is for this platform")
|
@JSDocs(20, "source.isPlaylistUrl(url)", "Validates if a playlist url is for this platform")
|
||||||
@JSDocsParameter("url", "Url of playlist")
|
@JSDocsParameter("url", "Url of playlist")
|
||||||
override fun isPlaylistUrl(url: String): Boolean = isBusyWith("isPlaylistUrl") {
|
override fun isPlaylistUrl(url: String): Boolean {
|
||||||
if (!capabilities.hasGetPlaylist)
|
if (!capabilities.hasGetPlaylist)
|
||||||
return@isBusyWith false;
|
return false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return@isBusyWith busy {
|
return plugin.executeTyped<V8ValueBoolean>("source.isPlaylistUrl(${Json.encodeToString(url)})")
|
||||||
return@busy plugin.executeTyped<V8ValueBoolean>("source.isPlaylistUrl(${Json.encodeToString(url)})")
|
.value;
|
||||||
.value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch(ex: Throwable) {
|
catch(ex: Throwable) {
|
||||||
announcePluginUnhandledException("isPlaylistUrl", ex);
|
announcePluginUnhandledException("isPlaylistUrl", ex);
|
||||||
return@isBusyWith false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@JSOptional
|
@JSOptional
|
||||||
@@ -711,13 +652,6 @@ open class JSClient : IPlatformClient {
|
|||||||
.toTypedArray();
|
.toTypedArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
@JSOptional
|
|
||||||
@JSDocs(23, "source.getUserHistory()", "Gets the history of the current user")
|
|
||||||
override fun getUserHistory(): IPager<IPlatformContent> {
|
|
||||||
ensureEnabled();
|
|
||||||
return JSContentPager(config, this, plugin.executeTyped("source.getUserHistory()"));
|
|
||||||
}
|
|
||||||
|
|
||||||
fun validate() {
|
fun validate() {
|
||||||
try {
|
try {
|
||||||
plugin.start();
|
plugin.start();
|
||||||
@@ -789,29 +723,19 @@ open class JSClient : IPlatformClient {
|
|||||||
return urls;
|
return urls;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T> busy(handle: ()->T): T {
|
|
||||||
return _plugin.busy {
|
|
||||||
return@busy handle();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fun <T> busyBlockingSuspended(handle: suspend ()->T): T {
|
|
||||||
return _plugin.busy {
|
|
||||||
return@busy runBlocking {
|
|
||||||
return@runBlocking handle();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <T> isBusyWith(actionName: String, handle: ()->T): T {
|
|
||||||
//val busyId = kotlin.random.Random.nextInt(9999);
|
|
||||||
return busy {
|
|
||||||
try {
|
|
||||||
_busyAction = actionName;
|
|
||||||
return@busy handle();
|
|
||||||
|
|
||||||
|
private fun <T> isBusyWith(actionName: String, handle: ()->T): T {
|
||||||
|
try {
|
||||||
|
synchronized(_busyLock) {
|
||||||
|
_busyCounter++;
|
||||||
}
|
}
|
||||||
finally {
|
_busyAction = actionName;
|
||||||
_busyAction = "";
|
return handle();
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
_busyAction = "";
|
||||||
|
synchronized(_busyLock) {
|
||||||
|
_busyCounter--;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-47
@@ -1,11 +1,6 @@
|
|||||||
package com.futo.platformplayer.api.media.platforms.js
|
package com.futo.platformplayer.api.media.platforms.js
|
||||||
|
|
||||||
import kotlinx.serialization.Contextual
|
@kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.Transient
|
|
||||||
import java.util.Dictionary
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class SourcePluginAuthConfig(
|
class SourcePluginAuthConfig(
|
||||||
val loginUrl: String,
|
val loginUrl: String,
|
||||||
val completionUrl: String? = null,
|
val completionUrl: String? = null,
|
||||||
@@ -16,44 +11,5 @@ class SourcePluginAuthConfig(
|
|||||||
val userAgent: String? = null,
|
val userAgent: String? = null,
|
||||||
val loginButton: String? = null,
|
val loginButton: String? = null,
|
||||||
val domainHeadersToFind: Map<String, List<String>>? = null,
|
val domainHeadersToFind: Map<String, List<String>>? = null,
|
||||||
val loginWarning: String? = null,
|
val loginWarning: String? = null
|
||||||
val loginWarnings: List<Warning>? = null,
|
) { }
|
||||||
val uiMods: List<UIMod>? = null
|
|
||||||
) {
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class Warning(
|
|
||||||
val url: String,
|
|
||||||
val text: String?,
|
|
||||||
val details: String? = null,
|
|
||||||
val once: Boolean? = true
|
|
||||||
) {
|
|
||||||
@Transient
|
|
||||||
private var _regex: Regex? = null;
|
|
||||||
|
|
||||||
fun getRegex(): Regex {
|
|
||||||
return _regex ?: url.let {
|
|
||||||
val reg = Regex(it);
|
|
||||||
_regex = reg;
|
|
||||||
return reg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@Serializable
|
|
||||||
class UIMod(
|
|
||||||
val url: String,
|
|
||||||
val scale: Float?,
|
|
||||||
val desktop: Boolean?
|
|
||||||
) {
|
|
||||||
@Contextual
|
|
||||||
private var _regex: Regex? = null;
|
|
||||||
|
|
||||||
fun getRegex(): Regex {
|
|
||||||
return _regex ?: url.let {
|
|
||||||
val reg = Regex(it);
|
|
||||||
_regex = reg;
|
|
||||||
return reg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+6
-15
@@ -4,7 +4,6 @@ import android.net.Uri
|
|||||||
import com.futo.platformplayer.SignatureProvider
|
import com.futo.platformplayer.SignatureProvider
|
||||||
import com.futo.platformplayer.api.media.Serializer
|
import com.futo.platformplayer.api.media.Serializer
|
||||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
import com.futo.platformplayer.logging.Logger
|
|
||||||
import com.futo.platformplayer.matchesDomain
|
import com.futo.platformplayer.matchesDomain
|
||||||
import com.futo.platformplayer.states.StatePlugins
|
import com.futo.platformplayer.states.StatePlugins
|
||||||
import kotlinx.serialization.Contextual
|
import kotlinx.serialization.Contextual
|
||||||
@@ -23,7 +22,7 @@ class SourcePluginConfig(
|
|||||||
//Script
|
//Script
|
||||||
val repositoryUrl: String? = null,
|
val repositoryUrl: String? = null,
|
||||||
val scriptUrl: String = "",
|
val scriptUrl: String = "",
|
||||||
var version: Int = -1,
|
val version: Int = -1,
|
||||||
|
|
||||||
val iconUrl: String? = null,
|
val iconUrl: String? = null,
|
||||||
var id: String = UUID.randomUUID().toString(),
|
var id: String = UUID.randomUUID().toString(),
|
||||||
@@ -48,7 +47,6 @@ class SourcePluginConfig(
|
|||||||
var subscriptionRateLimit: Int? = null,
|
var subscriptionRateLimit: Int? = null,
|
||||||
var enableInSearch: Boolean = true,
|
var enableInSearch: Boolean = true,
|
||||||
var enableInHome: Boolean = true,
|
var enableInHome: Boolean = true,
|
||||||
var enableInShorts: Boolean = true,
|
|
||||||
var supportedClaimTypes: List<Int> = listOf(),
|
var supportedClaimTypes: List<Int> = listOf(),
|
||||||
var primaryClaimFieldType: Int? = null,
|
var primaryClaimFieldType: Int? = null,
|
||||||
var developerSubmitUrl: String? = null,
|
var developerSubmitUrl: String? = null,
|
||||||
@@ -170,17 +168,12 @@ class SourcePluginConfig(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun validate(text: String): Boolean {
|
fun validate(text: String): Boolean {
|
||||||
try {
|
if(scriptPublicKey.isNullOrEmpty())
|
||||||
if (scriptPublicKey.isNullOrEmpty())
|
throw IllegalStateException("No public key present");
|
||||||
throw IllegalStateException("No public key present");
|
if(scriptSignature.isNullOrEmpty())
|
||||||
if (scriptSignature.isNullOrEmpty())
|
throw IllegalStateException("No signature present");
|
||||||
throw IllegalStateException("No signature present");
|
|
||||||
|
|
||||||
return SignatureProvider.verify(text, scriptSignature, scriptPublicKey);
|
return SignatureProvider.verify(text, scriptSignature, scriptPublicKey);
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.e(TAG, "Failed to verify due to an unhandled exception", e)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isUrlAllowed(url: String): Boolean {
|
fun isUrlAllowed(url: String): Boolean {
|
||||||
@@ -211,8 +204,6 @@ class SourcePluginConfig(
|
|||||||
obj.sourceUrl = sourceUrl;
|
obj.sourceUrl = sourceUrl;
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
private val TAG = "SourcePluginConfig"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
|
|||||||
+2
-20
@@ -5,16 +5,10 @@ import com.futo.platformplayer.constructs.Event0
|
|||||||
import com.futo.platformplayer.logging.Logger
|
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.StateApp
|
|
||||||
import com.futo.platformplayer.states.StateHistory
|
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
|
||||||
import com.futo.platformplayer.views.fields.DropdownFieldOptions
|
import com.futo.platformplayer.views.fields.DropdownFieldOptions
|
||||||
import com.futo.platformplayer.views.fields.FieldForm
|
import com.futo.platformplayer.views.fields.FieldForm
|
||||||
import com.futo.platformplayer.views.fields.FormField
|
import com.futo.platformplayer.views.fields.FormField
|
||||||
import com.futo.platformplayer.views.fields.FormFieldButton
|
|
||||||
import com.futo.platformplayer.views.fields.FormFieldWarning
|
import com.futo.platformplayer.views.fields.FormFieldWarning
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -109,22 +103,12 @@ class SourcePluginDescriptor {
|
|||||||
@FormField(R.string.home, FieldForm.TOGGLE, R.string.show_content_in_home_tab, 1)
|
@FormField(R.string.home, FieldForm.TOGGLE, R.string.show_content_in_home_tab, 1)
|
||||||
var enableHome: Boolean? = null;
|
var enableHome: Boolean? = null;
|
||||||
|
|
||||||
|
|
||||||
@FormField(R.string.search, FieldForm.TOGGLE, R.string.show_content_in_search_results, 2)
|
@FormField(R.string.search, FieldForm.TOGGLE, R.string.show_content_in_search_results, 2)
|
||||||
var enableSearch: Boolean? = null;
|
var enableSearch: Boolean? = null;
|
||||||
|
|
||||||
@FormField(R.string.shorts, FieldForm.TOGGLE, R.string.show_content_in_shorts_tab, 3)
|
|
||||||
var enableShorts: Boolean? = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.sync, "group", R.string.sync_desc, 3,"sync")
|
@FormField(R.string.ratelimit, "group", R.string.ratelimit_description, 3)
|
||||||
var sync = Sync();
|
|
||||||
@Serializable
|
|
||||||
class Sync {
|
|
||||||
@FormField(R.string.sync_history, FieldForm.TOGGLE, R.string.sync_history_desc, 1,"syncHistory")
|
|
||||||
var enableHistorySync: Boolean? = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@FormField(R.string.ratelimit, "group", R.string.ratelimit_description, 4)
|
|
||||||
var rateLimit = RateLimit();
|
var rateLimit = RateLimit();
|
||||||
@Serializable
|
@Serializable
|
||||||
class RateLimit {
|
class RateLimit {
|
||||||
@@ -159,8 +143,6 @@ class SourcePluginDescriptor {
|
|||||||
tabEnabled.enableHome = config.enableInHome
|
tabEnabled.enableHome = config.enableInHome
|
||||||
if(tabEnabled.enableSearch == null)
|
if(tabEnabled.enableSearch == null)
|
||||||
tabEnabled.enableSearch = config.enableInSearch
|
tabEnabled.enableSearch = config.enableInSearch
|
||||||
if(tabEnabled.enableShorts == null)
|
|
||||||
tabEnabled.enableShorts = config.enableInShorts
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-97
@@ -23,7 +23,6 @@ import java.util.UUID
|
|||||||
class JSHttpClient : ManagedHttpClient {
|
class JSHttpClient : ManagedHttpClient {
|
||||||
private val _jsClient: JSClient?;
|
private val _jsClient: JSClient?;
|
||||||
private val _jsConfig: SourcePluginConfig?;
|
private val _jsConfig: SourcePluginConfig?;
|
||||||
val config get() = _jsConfig
|
|
||||||
private val _auth: SourceAuth?;
|
private val _auth: SourceAuth?;
|
||||||
private val _captcha: SourceCaptchaData?;
|
private val _captcha: SourceCaptchaData?;
|
||||||
|
|
||||||
@@ -68,25 +67,6 @@ class JSHttpClient : ManagedHttpClient {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resetAuthCookies() {
|
|
||||||
_currentCookieMap.clear();
|
|
||||||
if(!_auth?.cookieMap.isNullOrEmpty()) {
|
|
||||||
for(domainCookies in _auth!!.cookieMap!!)
|
|
||||||
_currentCookieMap.put(domainCookies.key, HashMap(domainCookies.value));
|
|
||||||
}
|
|
||||||
if(!_captcha?.cookieMap.isNullOrEmpty()) {
|
|
||||||
for(domainCookies in _captcha!!.cookieMap!!) {
|
|
||||||
if(_currentCookieMap.containsKey(domainCookies.key))
|
|
||||||
_currentCookieMap[domainCookies.key]?.putAll(domainCookies.value);
|
|
||||||
else
|
|
||||||
_currentCookieMap.put(domainCookies.key, HashMap(domainCookies.value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fun clearOtherCookies() {
|
|
||||||
_otherCookieMap.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun clone(): ManagedHttpClient {
|
override fun clone(): ManagedHttpClient {
|
||||||
val newClient = JSHttpClient(_jsClient, _auth);
|
val newClient = JSHttpClient(_jsClient, _auth);
|
||||||
newClient._currentCookieMap = HashMap(_currentCookieMap.toList().associate { Pair(it.first, HashMap(it.second)) })
|
newClient._currentCookieMap = HashMap(_currentCookieMap.toList().associate { Pair(it.first, HashMap(it.second)) })
|
||||||
@@ -147,7 +127,7 @@ class JSHttpClient : ManagedHttpClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(doApplyCookies) {
|
if(doApplyCookies) {
|
||||||
if (_currentCookieMap.isNotEmpty() || _otherCookieMap.isNotEmpty()) {
|
if (_currentCookieMap.isNotEmpty()) {
|
||||||
val cookiesToApply = hashMapOf<String, String>();
|
val cookiesToApply = hashMapOf<String, String>();
|
||||||
synchronized(_currentCookieMap) {
|
synchronized(_currentCookieMap) {
|
||||||
for(cookie in _currentCookieMap
|
for(cookie in _currentCookieMap
|
||||||
@@ -155,12 +135,6 @@ class JSHttpClient : ManagedHttpClient {
|
|||||||
.flatMap { it.value.toList() })
|
.flatMap { it.value.toList() })
|
||||||
cookiesToApply[cookie.first] = cookie.second;
|
cookiesToApply[cookie.first] = cookie.second;
|
||||||
};
|
};
|
||||||
synchronized(_otherCookieMap) {
|
|
||||||
for(cookie in _otherCookieMap
|
|
||||||
.filter { domain.matchesDomain(it.key) }
|
|
||||||
.flatMap { it.value.toList() })
|
|
||||||
cookiesToApply[cookie.first] = cookie.second;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(cookiesToApply.size > 0) {
|
if(cookiesToApply.size > 0) {
|
||||||
val cookieString = cookiesToApply.map { it.key + "=" + it.value }.joinToString("; ");
|
val cookieString = cookiesToApply.map { it.key + "=" + it.value }.joinToString("; ");
|
||||||
@@ -255,76 +229,6 @@ class JSHttpClient : ManagedHttpClient {
|
|||||||
|
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
fun processRequest(method: String, responseCode: Int, url: Uri, headers: Map<String, List<String>>) {
|
|
||||||
if(doUpdateCookies) {
|
|
||||||
val domain = url.host?.lowercase() ?: return;
|
|
||||||
val domainParts = domain.split(".");
|
|
||||||
val defaultCookieDomain =
|
|
||||||
"." + domainParts.drop(domainParts.size - 2).joinToString(".");
|
|
||||||
for (header in headers) {
|
|
||||||
if(header.key.lowercase() == "set-cookie") {
|
|
||||||
var domainToUse = domain;
|
|
||||||
val cookie = cookieStringToPair(header.value.first());
|
|
||||||
var cookieValue = cookie.second;
|
|
||||||
|
|
||||||
if (cookie.first.isNotEmpty() && cookie.second.isNotEmpty()) {
|
|
||||||
val cookieParts = cookie.second.split(";");
|
|
||||||
if (cookieParts.size == 0)
|
|
||||||
continue;
|
|
||||||
cookieValue = cookieParts[0].trim();
|
|
||||||
|
|
||||||
val cookieVariables = cookieParts.drop(1).map {
|
|
||||||
val splitIndex = it.indexOf("=");
|
|
||||||
if (splitIndex < 0)
|
|
||||||
return@map Pair(it.trim().lowercase(), "");
|
|
||||||
return@map Pair<String, String>(
|
|
||||||
it.substring(0, splitIndex).lowercase().trim(),
|
|
||||||
it.substring(splitIndex + 1).trim()
|
|
||||||
);
|
|
||||||
}.toMap();
|
|
||||||
domainToUse = if (cookieVariables.containsKey("domain"))
|
|
||||||
cookieVariables["domain"]!!.lowercase();
|
|
||||||
else defaultCookieDomain;
|
|
||||||
//TODO: Make sure this has no negative effect besides apply cookies to root domain
|
|
||||||
if(!domainToUse.startsWith("."))
|
|
||||||
domainToUse = ".${domainToUse}";
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((_auth != null || _currentCookieMap.isNotEmpty())) {
|
|
||||||
val cookieMap = if (_currentCookieMap.containsKey(domainToUse))
|
|
||||||
_currentCookieMap[domainToUse]!!;
|
|
||||||
else {
|
|
||||||
val newMap = hashMapOf<String, String>();
|
|
||||||
_currentCookieMap[domainToUse] = newMap
|
|
||||||
newMap;
|
|
||||||
}
|
|
||||||
if (cookieMap.containsKey(cookie.first) || doAllowNewCookies)
|
|
||||||
cookieMap[cookie.first] = cookieValue;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
val cookieMap = if (_otherCookieMap.containsKey(domainToUse))
|
|
||||||
_otherCookieMap[domainToUse]!!;
|
|
||||||
else {
|
|
||||||
val newMap = hashMapOf<String, String>();
|
|
||||||
_otherCookieMap[domainToUse] = newMap
|
|
||||||
newMap;
|
|
||||||
}
|
|
||||||
if (cookieMap.containsKey(cookie.first) || doAllowNewCookies)
|
|
||||||
cookieMap[cookie.first] = cookieValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(_jsClient is DevJSClient) {
|
|
||||||
//val peekBody = resp.peekBody(1000 * 1000).string();
|
|
||||||
StateDeveloper.instance.addDevHttpExchange(
|
|
||||||
StateDeveloper.DevHttpExchange(
|
|
||||||
StateDeveloper.DevHttpRequest(method, url.toString(), mapOf(*headers.map { Pair(it.key, it.value.joinToString(";")) }.toTypedArray()), ""),
|
|
||||||
StateDeveloper.DevHttpRequest("RESP", url.toString(), mapOf(*headers.map { Pair(it.key, it.value.joinToString(";")) }.toTypedArray()), "", responseCode)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun cookieStringToPair(cookie: String): Pair<String, String> {
|
private fun cookieStringToPair(cookie: String): Pair<String, String> {
|
||||||
val cookieKey = cookie.substring(0, cookie.indexOf("="));
|
val cookieKey = cookie.substring(0, cookie.indexOf("="));
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
package com.futo.platformplayer.api.media.platforms.js.models
|
package com.futo.platformplayer.api.media.platforms.js.models
|
||||||
|
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
import com.futo.platformplayer.api.media.models.JSChannelContent
|
|
||||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.ensureIsBusy
|
|
||||||
import com.futo.platformplayer.getOrDefault
|
import com.futo.platformplayer.getOrDefault
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
|
||||||
@@ -14,7 +12,6 @@ interface IJSContent: IPlatformContent {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromV8(plugin: JSClient, obj: V8ValueObject): IPlatformContent {
|
fun fromV8(plugin: JSClient, obj: V8ValueObject): IPlatformContent {
|
||||||
obj.ensureIsBusy();
|
|
||||||
val config = plugin.config;
|
val config = plugin.config;
|
||||||
val type: Int = obj.getOrThrow(config, "contentType", "ContentItem");
|
val type: Int = obj.getOrThrow(config, "contentType", "ContentItem");
|
||||||
val pluginType: String? = obj.getOrDefault(config, "plugin_type", "ContentItem", null);
|
val pluginType: String? = obj.getOrDefault(config, "plugin_type", "ContentItem", null);
|
||||||
@@ -29,9 +26,6 @@ interface IJSContent: IPlatformContent {
|
|||||||
ContentType.NESTED_VIDEO -> JSNestedMediaContent(config, obj);
|
ContentType.NESTED_VIDEO -> JSNestedMediaContent(config, obj);
|
||||||
ContentType.PLAYLIST -> JSPlaylist(config, obj);
|
ContentType.PLAYLIST -> JSPlaylist(config, obj);
|
||||||
ContentType.LOCKED -> JSLockedContent(config, obj);
|
ContentType.LOCKED -> JSLockedContent(config, obj);
|
||||||
ContentType.CHANNEL -> JSChannelContent(config, obj);
|
|
||||||
ContentType.ARTICLE -> JSArticle(config, obj);
|
|
||||||
ContentType.WEB -> JSWeb(config, obj);
|
|
||||||
else -> throw NotImplementedError("Unknown content type ${type}");
|
else -> throw NotImplementedError("Unknown content type ${type}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-3
@@ -6,20 +6,17 @@ import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
|||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.ensureIsBusy
|
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
|
||||||
interface IJSContentDetails: IPlatformContent {
|
interface IJSContentDetails: IPlatformContent {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromV8(plugin: JSClient, obj: V8ValueObject): IPlatformContentDetails {
|
fun fromV8(plugin: JSClient, obj: V8ValueObject): IPlatformContentDetails {
|
||||||
obj.ensureIsBusy();
|
|
||||||
val type: Int = obj.getOrThrow(plugin.config, "contentType", "ContentDetails");
|
val type: Int = obj.getOrThrow(plugin.config, "contentType", "ContentDetails");
|
||||||
return when(ContentType.fromInt(type)) {
|
return when(ContentType.fromInt(type)) {
|
||||||
ContentType.MEDIA -> JSVideoDetails(plugin, obj);
|
ContentType.MEDIA -> JSVideoDetails(plugin, obj);
|
||||||
ContentType.POST -> JSPostDetails(plugin.config, obj);
|
ContentType.POST -> JSPostDetails(plugin.config, obj);
|
||||||
ContentType.ARTICLE -> JSArticleDetails(plugin, obj);
|
ContentType.ARTICLE -> JSArticleDetails(plugin, obj);
|
||||||
ContentType.WEB -> JSWebDetails(plugin, obj);
|
|
||||||
else -> throw NotImplementedError("Unknown content type ${type}");
|
else -> throw NotImplementedError("Unknown content type ${type}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
package com.futo.platformplayer.api.media.platforms.js.models
|
|
||||||
|
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
|
||||||
import com.futo.platformplayer.api.media.IPlatformClient
|
|
||||||
import com.futo.platformplayer.api.media.IPluginSourced
|
|
||||||
import com.futo.platformplayer.api.media.models.Thumbnails
|
|
||||||
import com.futo.platformplayer.api.media.models.article.IPlatformArticle
|
|
||||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
|
||||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
|
|
||||||
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
|
||||||
import com.futo.platformplayer.api.media.models.post.IPlatformPost
|
|
||||||
import com.futo.platformplayer.api.media.models.post.TextType
|
|
||||||
import com.futo.platformplayer.api.media.models.ratings.IRating
|
|
||||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
|
||||||
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
|
||||||
import com.futo.platformplayer.getOrDefault
|
|
||||||
import com.futo.platformplayer.getOrThrow
|
|
||||||
import com.futo.platformplayer.getOrThrowNullableList
|
|
||||||
import com.futo.platformplayer.states.StateDeveloper
|
|
||||||
|
|
||||||
open class JSArticle(
|
|
||||||
config: SourcePluginConfig,
|
|
||||||
obj: V8ValueObject
|
|
||||||
) : JSContent(config, obj), IPlatformArticle, IPluginSourced {
|
|
||||||
|
|
||||||
final override val contentType: ContentType = ContentType.ARTICLE
|
|
||||||
|
|
||||||
override val summary: String =
|
|
||||||
obj.getOrDefault<String>(config, "summary", "PlatformArticle", "") ?: ""
|
|
||||||
|
|
||||||
override val thumbnails: Thumbnails? =
|
|
||||||
if (obj.has("thumbnails"))
|
|
||||||
Thumbnails.fromV8(
|
|
||||||
config,
|
|
||||||
obj.getOrThrow<V8ValueObject>(config, "thumbnails", "PlatformArticle")
|
|
||||||
)
|
|
||||||
else
|
|
||||||
null
|
|
||||||
}
|
|
||||||
+25
-42
@@ -4,8 +4,6 @@ import com.caoccao.javet.values.reference.V8ValueObject
|
|||||||
import com.futo.platformplayer.api.media.IPlatformClient
|
import com.futo.platformplayer.api.media.IPlatformClient
|
||||||
import com.futo.platformplayer.api.media.IPluginSourced
|
import com.futo.platformplayer.api.media.IPluginSourced
|
||||||
import com.futo.platformplayer.api.media.models.Thumbnails
|
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||||
import com.futo.platformplayer.api.media.models.article.IPlatformArticle
|
|
||||||
import com.futo.platformplayer.api.media.models.article.IPlatformArticleDetails
|
|
||||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
@@ -21,40 +19,38 @@ import com.futo.platformplayer.api.media.structures.IPager
|
|||||||
import com.futo.platformplayer.getOrDefault
|
import com.futo.platformplayer.getOrDefault
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
import com.futo.platformplayer.getOrThrowNullableList
|
import com.futo.platformplayer.getOrThrowNullableList
|
||||||
import com.futo.platformplayer.invokeV8
|
|
||||||
import com.futo.platformplayer.states.StateDeveloper
|
import com.futo.platformplayer.states.StateDeveloper
|
||||||
|
|
||||||
open class JSArticleDetails(
|
open class JSArticleDetails : JSContent, IPluginSourced, IPlatformContentDetails {
|
||||||
private val client: JSClient,
|
final override val contentType: ContentType get() = ContentType.ARTICLE;
|
||||||
obj: V8ValueObject
|
|
||||||
) : JSContent(client.config, obj), IPlatformArticleDetails, IPluginSourced, IPlatformContentDetails {
|
|
||||||
|
|
||||||
final override val contentType: ContentType = ContentType.ARTICLE
|
private val _hasGetComments: Boolean;
|
||||||
|
private val _hasGetContentRecommendations: Boolean;
|
||||||
|
|
||||||
private val _hasGetComments: Boolean = _content.has("getComments")
|
val rating: IRating;
|
||||||
private val _hasGetContentRecommendations: Boolean = _content.has("getContentRecommendations")
|
|
||||||
|
|
||||||
override val rating: IRating =
|
val summary: String;
|
||||||
obj.getOrDefault<V8ValueObject>(client.config, "rating", "PlatformArticle", null)
|
val thumbnails: Thumbnails?;
|
||||||
?.let { IRating.fromV8(client.config, it, "PlatformArticle") }
|
val segments: List<IJSArticleSegment>;
|
||||||
?: RatingLikes(0)
|
|
||||||
|
|
||||||
override val summary: String =
|
constructor(client: JSClient, obj: V8ValueObject): super(client.config, obj) {
|
||||||
_content.getOrThrow(client.config, "summary", "PlatformArticle")
|
val contextName = "PlatformPost";
|
||||||
|
|
||||||
override val thumbnails: Thumbnails? =
|
rating = obj.getOrDefault<V8ValueObject>(client.config, "rating", contextName, null)?.let { IRating.fromV8(client.config, it, contextName) } ?: RatingLikes(0);
|
||||||
if (_content.has("thumbnails"))
|
summary = _content.getOrThrow(client.config, "summary", contextName);
|
||||||
Thumbnails.fromV8(
|
if(_content.has("thumbnails"))
|
||||||
client.config,
|
thumbnails = Thumbnails.fromV8(client.config, _content.getOrThrow(client.config, "thumbnails", contextName));
|
||||||
_content.getOrThrow(client.config, "thumbnails", "PlatformArticle")
|
|
||||||
)
|
|
||||||
else
|
else
|
||||||
null
|
thumbnails = null;
|
||||||
|
|
||||||
override val segments: List<IJSArticleSegment> =
|
|
||||||
obj.getOrThrowNullableList<V8ValueObject>(client.config, "segments", "PlatformArticle")
|
segments = (obj.getOrThrowNullableList<V8ValueObject>(client.config, "segments", contextName)
|
||||||
?.mapNotNull { fromV8Segment(client, it) }
|
?.map { fromV8Segment(client, it) }
|
||||||
?: emptyList()
|
?.filterNotNull() ?: listOf());
|
||||||
|
|
||||||
|
_hasGetComments = _content.has("getComments");
|
||||||
|
_hasGetContentRecommendations = _content.has("getContentRecommendations");
|
||||||
|
}
|
||||||
|
|
||||||
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? {
|
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? {
|
||||||
if(!_hasGetComments || _content.isClosed)
|
if(!_hasGetComments || _content.isClosed)
|
||||||
@@ -87,12 +83,12 @@ open class JSArticleDetails(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun getContentRecommendationsJS(client: JSClient): JSContentPager {
|
private fun getContentRecommendationsJS(client: JSClient): JSContentPager {
|
||||||
val contentPager = _content.invokeV8<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
|
val contentPager = _content.invoke<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
|
||||||
return JSContentPager(_pluginConfig, client, contentPager);
|
return JSContentPager(_pluginConfig, client, contentPager);
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getCommentsJS(client: JSClient): JSCommentPager {
|
private fun getCommentsJS(client: JSClient): JSCommentPager {
|
||||||
val commentPager = _content.invokeV8<V8ValueObject>("getComments", arrayOf<Any>());
|
val commentPager = _content.invoke<V8ValueObject>("getComments", arrayOf<Any>());
|
||||||
return JSCommentPager(_pluginConfig, client, commentPager);
|
return JSCommentPager(_pluginConfig, client, commentPager);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +99,6 @@ open class JSArticleDetails(
|
|||||||
return when(SegmentType.fromInt(obj.getOrThrow(client.config, "type", "JSArticle.Segment"))) {
|
return when(SegmentType.fromInt(obj.getOrThrow(client.config, "type", "JSArticle.Segment"))) {
|
||||||
SegmentType.TEXT -> JSTextSegment(client, obj);
|
SegmentType.TEXT -> JSTextSegment(client, obj);
|
||||||
SegmentType.IMAGES -> JSImagesSegment(client, obj);
|
SegmentType.IMAGES -> JSImagesSegment(client, obj);
|
||||||
SegmentType.HEADER -> JSHeaderSegment(client, obj);
|
|
||||||
SegmentType.NESTED -> JSNestedSegment(client, obj);
|
SegmentType.NESTED -> JSNestedSegment(client, obj);
|
||||||
else -> null;
|
else -> null;
|
||||||
}
|
}
|
||||||
@@ -115,7 +110,6 @@ enum class SegmentType(val value: Int) {
|
|||||||
UNKNOWN(0),
|
UNKNOWN(0),
|
||||||
TEXT(1),
|
TEXT(1),
|
||||||
IMAGES(2),
|
IMAGES(2),
|
||||||
HEADER(3),
|
|
||||||
|
|
||||||
NESTED(9);
|
NESTED(9);
|
||||||
|
|
||||||
@@ -156,17 +150,6 @@ class JSImagesSegment: IJSArticleSegment {
|
|||||||
caption = obj.getOrDefault(client.config, "caption", contextName, "") ?: "";
|
caption = obj.getOrDefault(client.config, "caption", contextName, "") ?: "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class JSHeaderSegment: IJSArticleSegment {
|
|
||||||
override val type = SegmentType.HEADER;
|
|
||||||
val content: String;
|
|
||||||
val level: Int;
|
|
||||||
|
|
||||||
constructor(client: JSClient, obj: V8ValueObject) {
|
|
||||||
val contextName = "JSHeaderSegment";
|
|
||||||
content = obj.getOrDefault(client.config, "content", contextName, "") ?: "";
|
|
||||||
level = obj.getOrDefault(client.config, "level", contextName, 1) ?: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
class JSNestedSegment: IJSArticleSegment {
|
class JSNestedSegment: IJSArticleSegment {
|
||||||
override val type = SegmentType.NESTED;
|
override val type = SegmentType.NESTED;
|
||||||
val nested: IPlatformContent;
|
val nested: IPlatformContent;
|
||||||
|
|||||||
+1
@@ -5,6 +5,7 @@ import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
|||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
|
|
||||||
class JSChannelPager : JSPager<PlatformAuthorLink>, IPager<PlatformAuthorLink> {
|
class JSChannelPager : JSPager<PlatformAuthorLink>, IPager<PlatformAuthorLink> {
|
||||||
|
|
||||||
|
|||||||
+1
-2
@@ -12,7 +12,6 @@ import com.futo.platformplayer.engine.V8Plugin
|
|||||||
import com.futo.platformplayer.getOrDefault
|
import com.futo.platformplayer.getOrDefault
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
import com.futo.platformplayer.getOrThrowNullable
|
import com.futo.platformplayer.getOrThrowNullable
|
||||||
import com.futo.platformplayer.invokeV8
|
|
||||||
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
@@ -61,7 +60,7 @@ class JSComment : IPlatformComment {
|
|||||||
if(!_hasGetReplies)
|
if(!_hasGetReplies)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
val obj = _comment!!.invokeV8<V8ValueObject>("getReplies", arrayOf<Any>());
|
val obj = _comment!!.invoke<V8ValueObject>("getReplies", arrayOf<Any>());
|
||||||
val plugin = if(client is JSClient) client else throw NotImplementedError("Only implemented for JSClient");
|
val plugin = if(client is JSClient) client else throw NotImplementedError("Only implemented for JSClient");
|
||||||
return JSCommentPager(_config!!, plugin, obj);
|
return JSCommentPager(_config!!, plugin, obj);
|
||||||
}
|
}
|
||||||
|
|||||||
+36
-34
@@ -16,49 +16,51 @@ import java.time.LocalDateTime
|
|||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.time.ZoneOffset
|
import java.time.ZoneOffset
|
||||||
|
|
||||||
open class JSContent(
|
open class JSContent : IPlatformContent, IPluginSourced {
|
||||||
protected val _pluginConfig: SourcePluginConfig,
|
protected val _pluginConfig: SourcePluginConfig;
|
||||||
protected val _content: V8ValueObject
|
protected val _content : V8ValueObject;
|
||||||
) : IPlatformContent, IPluginSourced {
|
|
||||||
|
|
||||||
override val contentType: ContentType = ContentType.UNKNOWN
|
protected val _hasGetDetails: Boolean;
|
||||||
|
|
||||||
protected val _hasGetDetails: Boolean = _content.has("getDetails")
|
override val contentType: ContentType get() = ContentType.UNKNOWN;
|
||||||
|
|
||||||
override val id: PlatformID =
|
override val id: PlatformID;
|
||||||
PlatformID.fromV8(_pluginConfig, _content.getOrThrow(_pluginConfig, "id", CTX))
|
override val name: String;
|
||||||
|
override val author: PlatformAuthorLink;
|
||||||
|
override val datetime: OffsetDateTime?;
|
||||||
|
|
||||||
override val name: String =
|
override val url: String;
|
||||||
HtmlCompat.fromHtml(
|
override val shareUrl: String;
|
||||||
_content.getOrThrow<String>(_pluginConfig, "name", CTX).decodeUnicode(),
|
|
||||||
HtmlCompat.FROM_HTML_MODE_LEGACY
|
|
||||||
).toString()
|
|
||||||
|
|
||||||
override val author: PlatformAuthorLink =
|
override val sourceConfig: SourcePluginConfig get() = _pluginConfig;
|
||||||
_content.getOrDefault<V8ValueObject>(_pluginConfig, "author", CTX, null)
|
|
||||||
?.let { PlatformAuthorLink.fromV8(_pluginConfig, it) }
|
|
||||||
?: PlatformAuthorLink.UNKNOWN
|
|
||||||
|
|
||||||
private val _epoch: Long? =
|
constructor(config: SourcePluginConfig, obj: V8ValueObject) {
|
||||||
_content.getOrDefault<Long>(_pluginConfig, "datetime", CTX, null)?.toLong()
|
_pluginConfig = config;
|
||||||
|
_content = obj;
|
||||||
|
|
||||||
override val datetime: OffsetDateTime? =
|
val contextName = "PlatformContent";
|
||||||
_epoch?.takeIf { it != 0L }?.let {
|
|
||||||
OffsetDateTime.of(LocalDateTime.ofEpochSecond(it, 0, ZoneOffset.UTC), ZoneOffset.UTC)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val url: String =
|
id = PlatformID.fromV8(_pluginConfig, _content.getOrThrow(config, "id", contextName));
|
||||||
_content.getOrThrow<String>(_pluginConfig, "url", CTX)
|
name = HtmlCompat.fromHtml(_content.getOrThrow<String>(config, "name", contextName).decodeUnicode(), HtmlCompat.FROM_HTML_MODE_LEGACY).toString();
|
||||||
|
|
||||||
override val shareUrl: String =
|
val authorObj = _content.getOrDefault<V8ValueObject>(config, "author", contextName, null);
|
||||||
_content.getOrDefault<String>(_pluginConfig, "shareUrl", CTX, null) ?: url
|
if(authorObj != null)
|
||||||
|
author = PlatformAuthorLink.fromV8(_pluginConfig, authorObj);
|
||||||
|
else
|
||||||
|
author = PlatformAuthorLink.UNKNOWN;
|
||||||
|
|
||||||
override val sourceConfig: SourcePluginConfig
|
val datetimeInt = _content.getOrThrow<Int>(config, "datetime", contextName).toLong();
|
||||||
get() = _pluginConfig
|
if(datetimeInt == 0.toLong())
|
||||||
|
datetime = null;
|
||||||
|
else
|
||||||
|
datetime = OffsetDateTime.of(LocalDateTime.ofEpochSecond(datetimeInt, 0, ZoneOffset.UTC), ZoneOffset.UTC);
|
||||||
|
url = _content.getOrThrow(config, "url", contextName);
|
||||||
|
shareUrl = _content.getOrDefault<String>(config, "shareUrl", contextName, null) ?: url;
|
||||||
|
|
||||||
fun getUnderlyingObject(): V8ValueObject? = _content
|
_hasGetDetails = _content.has("getDetails");
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val CTX = "PlatformContent"
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
fun getUnderlyingObject(): V8ValueObject? {
|
||||||
|
return _content;
|
||||||
|
}
|
||||||
|
}
|
||||||
-11
@@ -2,7 +2,6 @@ package com.futo.platformplayer.api.media.platforms.js.models
|
|||||||
|
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
import com.futo.platformplayer.api.media.IPluginSourced
|
import com.futo.platformplayer.api.media.IPluginSourced
|
||||||
import com.futo.platformplayer.api.media.models.JSChannelContent
|
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
@@ -16,14 +15,4 @@ class JSContentPager : JSPager<IPlatformContent>, IPluginSourced {
|
|||||||
override fun convertResult(obj: V8ValueObject): IPlatformContent {
|
override fun convertResult(obj: V8ValueObject): IPlatformContent {
|
||||||
return IJSContent.fromV8(plugin, obj);
|
return IJSContent.fromV8(plugin, obj);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
class JSChannelContentPager : JSPager<IPlatformContent>, IPluginSourced {
|
|
||||||
override val sourceConfig: SourcePluginConfig get() = config;
|
|
||||||
|
|
||||||
constructor(config: SourcePluginConfig, plugin: JSClient, pager: V8ValueObject) : super(config, plugin, pager) {}
|
|
||||||
|
|
||||||
override fun convertResult(obj: V8ValueObject): IPlatformContent {
|
|
||||||
return JSChannelContent(config, obj);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+1
-1
@@ -15,7 +15,7 @@ class JSLiveEventPager : JSPager<IPlatformLiveEvent>, IPlatformLiveEventPager {
|
|||||||
nextRequest = pager.getOrThrow(config, "nextRequest", "LiveEventPager");
|
nextRequest = pager.getOrThrow(config, "nextRequest", "LiveEventPager");
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun nextPage() = plugin.isBusyWith("JSLiveEventPager.nextPage") {
|
override fun nextPage() {
|
||||||
super.nextPage();
|
super.nextPage();
|
||||||
nextRequest = pager.getOrThrow(config, "nextRequest", "LiveEventPager");
|
nextRequest = pager.getOrThrow(config, "nextRequest", "LiveEventPager");
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-27
@@ -10,7 +10,6 @@ import com.futo.platformplayer.api.media.structures.IPager
|
|||||||
import com.futo.platformplayer.engine.V8Plugin
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
import com.futo.platformplayer.getOrDefault
|
import com.futo.platformplayer.getOrDefault
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
import com.futo.platformplayer.invokeV8
|
|
||||||
import com.futo.platformplayer.warnIfMainThread
|
import com.futo.platformplayer.warnIfMainThread
|
||||||
|
|
||||||
abstract class JSPager<T> : IPager<T> {
|
abstract class JSPager<T> : IPager<T> {
|
||||||
@@ -19,8 +18,8 @@ abstract class JSPager<T> : IPager<T> {
|
|||||||
protected var pager: V8ValueObject;
|
protected var pager: V8ValueObject;
|
||||||
|
|
||||||
private var _lastResults: List<T>? = null;
|
private var _lastResults: List<T>? = null;
|
||||||
protected var _resultChanged: Boolean = true;
|
private var _resultChanged: Boolean = true;
|
||||||
protected var _hasMorePages: Boolean = false;
|
private var _hasMorePages: Boolean = false;
|
||||||
//private var _morePagesWasFalse: Boolean = false;
|
//private var _morePagesWasFalse: Boolean = false;
|
||||||
|
|
||||||
val isAvailable get() = plugin.getUnderlyingPlugin()._runtime?.let { !it.isClosed && !it.isDead } ?: false;
|
val isAvailable get() = plugin.getUnderlyingPlugin()._runtime?.let { !it.isClosed && !it.isDead } ?: false;
|
||||||
@@ -30,9 +29,7 @@ abstract class JSPager<T> : IPager<T> {
|
|||||||
this.pager = pager;
|
this.pager = pager;
|
||||||
this.config = config;
|
this.config = config;
|
||||||
|
|
||||||
plugin.busy {
|
_hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;
|
||||||
_hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;
|
|
||||||
}
|
|
||||||
getResults();
|
getResults();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,20 +38,17 @@ abstract class JSPager<T> : IPager<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun hasMorePages(): Boolean {
|
override fun hasMorePages(): Boolean {
|
||||||
return _hasMorePages && !pager.isClosed;
|
return _hasMorePages;
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun nextPage() {
|
override fun nextPage() {
|
||||||
warnIfMainThread("JSPager.nextPage");
|
warnIfMainThread("JSPager.nextPage");
|
||||||
|
|
||||||
val pluginV8 = plugin.getUnderlyingPlugin();
|
pager = plugin.getUnderlyingPlugin().catchScriptErrors("[${plugin.config.name}] JSPager", "pager.nextPage()") {
|
||||||
pluginV8.busy {
|
pager.invoke("nextPage", arrayOf<Any>());
|
||||||
pager = pluginV8.catchScriptErrors("[${plugin.config.name}] JSPager", "pager.nextPage()") {
|
};
|
||||||
pager.invokeV8("nextPage", arrayOf<Any>());
|
_hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;
|
||||||
};
|
_resultChanged = true;
|
||||||
_hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;
|
|
||||||
_resultChanged = true;
|
|
||||||
}
|
|
||||||
/*
|
/*
|
||||||
try {
|
try {
|
||||||
}
|
}
|
||||||
@@ -76,18 +70,15 @@ abstract class JSPager<T> : IPager<T> {
|
|||||||
return previousResults;
|
return previousResults;
|
||||||
|
|
||||||
warnIfMainThread("JSPager.getResults");
|
warnIfMainThread("JSPager.getResults");
|
||||||
|
val items = pager.getOrThrow<V8ValueArray>(config, "results", "JSPager");
|
||||||
return plugin.getUnderlyingPlugin().busy {
|
if(items.v8Runtime.isDead || items.v8Runtime.isClosed)
|
||||||
val items = pager.getOrThrow<V8ValueArray>(config, "results", "JSPager");
|
throw IllegalStateException("Runtime closed");
|
||||||
if (items.v8Runtime.isDead || items.v8Runtime.isClosed)
|
val newResults = items.toArray()
|
||||||
throw IllegalStateException("Runtime closed");
|
.map { convertResult(it as V8ValueObject) }
|
||||||
val newResults = items.toArray()
|
.toList();
|
||||||
.map { convertResult(it as V8ValueObject) }
|
_lastResults = newResults;
|
||||||
.toList();
|
_resultChanged = false;
|
||||||
_lastResults = newResults;
|
return newResults;
|
||||||
_resultChanged = false;
|
|
||||||
return@busy newResults;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract fun convertResult(obj: V8ValueObject): T;
|
abstract fun convertResult(obj: V8ValueObject): T;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user