mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-16 04:52:39 +02:00
Compare commits
438 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 959c192762 | |||
| 8be7b1272b | |||
| 6b57878275 | |||
| 66c7741c38 | |||
| b370af9d91 | |||
| 40b86cb5de | |||
| 84622e22aa | |||
| 092b20041e | |||
| f6cc00f471 | |||
| be2067067b | |||
| 67a7dd9698 | |||
| 6ffc067b24 | |||
| 56e6314c11 | |||
| e590bb4a19 | |||
| 35fe7f0e7a | |||
| 45d818ac81 | |||
| 7729681829 | |||
| b12d04b27d | |||
| e6608b9a5c | |||
| 2d503dfaf6 | |||
| 08934ef8de | |||
| 62d927739a | |||
| c8db8f58e8 | |||
| 0fc966a77d | |||
| 9f6c6c8cf3 | |||
| 43a6ff138c | |||
| 269a3460e7 | |||
| 18150e9e15 | |||
| 362c7f5b2c | |||
| 2adb8ad7f9 | |||
| 6b5d4e7507 | |||
| 49c82726f0 | |||
| c8ddcda384 | |||
| b75217f789 | |||
| 8ba8e535bd | |||
| e4c574db6b | |||
| fae73293d7 | |||
| 3bd0aac4f8 | |||
| 26b822e04b | |||
| 96b9b8843c | |||
| 6d9c1e17b5 | |||
| 507ad105c0 | |||
| 40a283017e | |||
| be14597670 | |||
| 837609abb9 | |||
| d64cd98b43 | |||
| 0081ff1483 | |||
| f78ca6c7ed | |||
| cfc7cbcaa4 | |||
| e533eb7778 | |||
| 7c1d0a7f88 | |||
| 01ef471708 | |||
| 2fd0a9a41d | |||
| 635749dfe4 | |||
| c4bd5626f3 | |||
| 568a0f6329 | |||
| 7ee67b5cd0 | |||
| fc94c6903c | |||
| a0af8805e7 | |||
| 9b64cde17d | |||
| f6931bcf8c | |||
| a4ff47d863 | |||
| 982d251126 | |||
| 8820a0ecc0 | |||
| b99a713ffc | |||
| dfc8c4b740 | |||
| c3df9e5259 | |||
| b9c7e0a8ca | |||
| 2c7f02a24d | |||
| 5cc8488d94 | |||
| 6f7304f59c | |||
| ea4fea4401 | |||
| 9b48664de4 | |||
| 8964dc68f0 | |||
| 4711b8055b | |||
| 84e3373fa7 | |||
| fdd7e32dd8 | |||
| e57119ebbd | |||
| ed29dd8365 | |||
| 196cacb452 | |||
| c025913fc8 | |||
| 48b2c68e72 | |||
| 689766a6ac | |||
| 9306024d17 | |||
| 195163840b | |||
| 788c54bf8f | |||
| 031aabd523 | |||
| 85db4cc4e6 | |||
| 745aad385b | |||
| ba87261f9f | |||
| 7d091382c0 | |||
| 781d0797e7 | |||
| ec12a06b88 | |||
| bf3e8867c3 | |||
| 176814a715 | |||
| 898637a616 | |||
| f1860126a7 | |||
| f8402676d7 | |||
| cf86ce1ab3 | |||
| f4cb1719e0 | |||
| 4898cb53ae | |||
| 0f60d4737e | |||
| 0dc33e1f2b | |||
| d86a997a88 | |||
| 34d4d92289 | |||
| 4cb1bf268f | |||
| 8488706ff9 | |||
| a348bb2662 | |||
| 60a17b3c67 | |||
| 386c58d4ad | |||
| 356ba01dc1 | |||
| ed2aa848da | |||
| c5dd90048f | |||
| ab04f334dc | |||
| 0d44f8a416 | |||
| d01a1545e2 | |||
| e599729ba1 | |||
| 3ac043740e | |||
| 89603d0ff3 | |||
| 05b6cd7c97 | |||
| ea5aad0631 | |||
| 96e034b9bf | |||
| 6141c36855 | |||
| 4084ab3ed0 | |||
| 34e733823a | |||
| f1d01642cd | |||
| d5551d7118 | |||
| d079a1e8e4 | |||
| c06c00ee9b | |||
| 1d8eababc2 | |||
| 75cf1ffbdd | |||
| 5499706a9b | |||
| ba57e32920 | |||
| df96c5b51c | |||
| 75f81d20db | |||
| 3fc92e4065 | |||
| 8ffd5f411f | |||
| 918161a299 | |||
| 9f50f72eaa | |||
| 2f66f124aa | |||
| 9a11717cf4 | |||
| 0d80424799 | |||
| ed9a65b2f0 | |||
| 8a53297be2 | |||
| 20862a27c8 | |||
| 95785e6c78 | |||
| e88c649578 | |||
| 09f91e64fb | |||
| b8923e59a1 | |||
| e722c0ce9a | |||
| 56248bf4b0 | |||
| 5af4787c45 | |||
| 0990247322 | |||
| 0154525578 | |||
| 1dc6eee242 | |||
| c63a63cb33 | |||
| c1967556ac | |||
| 309a57f5a1 | |||
| ee0bc96e53 | |||
| a4422fdd56 | |||
| b7c4047f1d | |||
| 65174ffc97 | |||
| eac3e37af5 | |||
| 0d5ad90ff9 | |||
| f42b14e95a | |||
| b8acd0b5b2 | |||
| ef72561768 | |||
| d63627bd61 | |||
| 422cceb225 | |||
| 76f5962232 | |||
| 30df22d225 | |||
| cd4295be59 | |||
| 7d366110b1 | |||
| 35c5045b3f | |||
| 4930ea8183 | |||
| 02292fed04 | |||
| bf6e61ed90 | |||
| 2ac8e0e621 | |||
| 0432f06eb3 | |||
| 7bfab8409f | |||
| 52d833d726 | |||
| 14d579eb1b | |||
| d3ab8ecf3a | |||
| 627b8c2b5d | |||
| 7f1cb22c12 | |||
| 5551bd31fe | |||
| 189d855c3f | |||
| 0ab52e8f4d | |||
| 27eb5aa6e1 | |||
| 49b5b16641 | |||
| 73dd52af28 | |||
| 3b8d256bad | |||
| 5d7dc1fdcb | |||
| f31b6c50e9 | |||
| fa12f8277c | |||
| 150a7d5006 | |||
| a0a73a8e5c | |||
| 4723a0b29a | |||
| adbe0357ba | |||
| b0a35bcf3f | |||
| 0e7482321c | |||
| e50d195b85 | |||
| 33780f1046 | |||
| 8b20b4909f | |||
| 71a3828fe4 | |||
| d713f2bd55 | |||
| 069a615193 | |||
| f7d2cb4055 | |||
| f109d82537 | |||
| ab49d4749b | |||
| 507eed4f53 | |||
| 23ca4addf9 | |||
| 331ed09775 | |||
| 85303b54bc | |||
| f224cd1ca5 | |||
| d433d6e774 | |||
| 90de54ac5c | |||
| 5ff8f1ba6d | |||
| bc00b12b8c | |||
| 1c0cfa89a3 | |||
| efa1361fbe | |||
| 73918a8d76 | |||
| a3c8bbb21f | |||
| 53525cb365 | |||
| e4d39cbec4 | |||
| a15e4beafb | |||
| d47298102e | |||
| 280feea06e | |||
| f649d62e38 | |||
| 0ae05e7cd4 | |||
| b284176072 | |||
| 5fffaf2f4e | |||
| 58da91eae8 | |||
| 98d92d3fe2 | |||
| c5d35b27f0 | |||
| aee5b75c2f | |||
| fe02197bd8 | |||
| a1060a15be | |||
| dc7b2f420b | |||
| b35390a4bb | |||
| 3b253ad2b6 | |||
| 06c39ce973 | |||
| 11b8914615 | |||
| e45c8617df | |||
| 9075a2599c | |||
| dd8d50e0e2 | |||
| 55a11d82ac | |||
| 7ee4f411cb | |||
| c9d5508018 | |||
| bef8fc682c | |||
| c37d464403 | |||
| cbf2712654 | |||
| 08134b4427 | |||
| f90290c4ec | |||
| 7cde8ed538 | |||
| 585cf090d6 | |||
| 23d1085755 | |||
| fc5888d57e | |||
| c5541b1747 | |||
| 0fd8ba28bb | |||
| 6d9f4959e0 | |||
| 4be4bb631f | |||
| 948f5a2a6d | |||
| baad342aec | |||
| aeb29c54cd | |||
| a5dfa653ad | |||
| 3387c727d1 | |||
| c806ff2e33 | |||
| 1db4d427fc | |||
| 3bf73ed5e8 | |||
| db44aa2c4d | |||
| 0e6e381800 | |||
| 69e43dc533 | |||
| ee4442d553 | |||
| c49b9f7841 | |||
| 8a35cd0e82 | |||
| 0ae90ecf03 | |||
| 3d2840fe15 | |||
| b6ad3fd991 | |||
| 2ee3c30b0e | |||
| 662e94bcee | |||
| f3c9e0196e | |||
| f15eb9bf9e | |||
| 12b2552185 | |||
| d245e20b14 | |||
| e47349d010 | |||
| eb3dd854d4 | |||
| c529446219 | |||
| fa2f8c3447 | |||
| 840d1ae534 | |||
| 2530c6eb58 | |||
| 869789f0e2 | |||
| ee3761c780 | |||
| e4c89e9aa9 | |||
| 9d5888ddf7 | |||
| ecc94920d7 | |||
| 5cafbf243e | |||
| f3fa208680 | |||
| 502602e27a | |||
| 5054b093a4 | |||
| 0ffaec6bc2 | |||
| ef8ea9eecf | |||
| b09d22e479 | |||
| 01787b6229 | |||
| 4c022698d3 | |||
| bfdcab0e84 | |||
| aaea5cc963 | |||
| 23d9c33406 | |||
| fad1b216df | |||
| e221b508d3 | |||
| dfafac7d99 | |||
| 2246f8cee2 | |||
| b65fc594dc | |||
| f52b731615 | |||
| 8661ff88c0 | |||
| 99c06c516f | |||
| 0bba7fa373 | |||
| 0c1822b118 | |||
| 10e3d2122f | |||
| 6df8f84421 | |||
| 7fa80ec048 | |||
| b3f9b81984 | |||
| 1393c489c1 | |||
| 640c2cbed0 | |||
| e55509f549 | |||
| 27c7fb0c12 | |||
| 88f3815585 | |||
| 2e9405cfdb | |||
| 9c1b543ed6 | |||
| d34cb0f9c1 | |||
| 116dc90d21 | |||
| 17b9853bb6 | |||
| 8bfb8abd20 | |||
| 9ee3f1f26e | |||
| 5dcff29d8d | |||
| 6cfbd0c8bf | |||
| 01d96cce16 | |||
| 58c376f011 | |||
| 439d339330 | |||
| 44eacc2a47 | |||
| 8135d61398 | |||
| 66208f8265 | |||
| f52251e23a | |||
| dbea93efe5 | |||
| 3bf0740bd1 | |||
| fa7f1b11f3 | |||
| ff914bbdf4 | |||
| b822078d4b | |||
| 290d2ceb50 | |||
| 8ec9025990 | |||
| c4cf856dcd | |||
| 38bb4e25d3 | |||
| 0de996d91c | |||
| 1f38c9b27d | |||
| 234f31b02d | |||
| 00e40e8cd6 | |||
| 0bc6a43dc1 | |||
| e7e0157fbc | |||
| 4cae1a41a5 | |||
| 4fa61e7f52 | |||
| f02ac796f5 | |||
| 22146a6bdc | |||
| 5285eae01d | |||
| c47ca369e4 | |||
| f0b1f62bb1 | |||
| f7aa6d006e | |||
| 6b67cd549f | |||
| fc6bf85822 | |||
| fbd9345cf8 | |||
| 63137b4c4d | |||
| e28dc7a3a6 | |||
| 6e14acc685 | |||
| ba64153f1d | |||
| 72c04e7556 | |||
| 54f37ee5b2 | |||
| 4fbb325313 | |||
| e1d3b95f73 | |||
| 8f7b4b8257 | |||
| 9d906025ea | |||
| d7f4dd65e8 | |||
| 599b119e62 | |||
| 41176464db | |||
| dd0ad19fb9 | |||
| 430625d2fb | |||
| 796cd1a776 | |||
| baa26af0c0 | |||
| ea0c27936e | |||
| 4aade35d19 | |||
| 251a5701af | |||
| 2da3116111 | |||
| 4c82fa1a4a | |||
| 7eef6eece2 | |||
| 570f32e980 | |||
| 16a0351125 | |||
| 2fa9005806 | |||
| 25527997fa | |||
| 4655d8369d | |||
| aeaaace3a4 | |||
| e6997004ff | |||
| 5e1896b7f2 | |||
| 88ca90c13a | |||
| f8ee340499 | |||
| 93f5260e20 | |||
| 34ba44ffa4 | |||
| b3a3e459a4 | |||
| f234564952 | |||
| ffa5795cc9 | |||
| 4f50c51356 | |||
| 9e9c8a0bec | |||
| 1349358d7c | |||
| 9c50f15be7 | |||
| 31e771daca | |||
| 66ce156dea | |||
| db6756bc78 | |||
| cab2581476 | |||
| 4c0be35020 | |||
| 7114201c08 | |||
| d8aecd325b | |||
| 1d18c13817 | |||
| f65eb0cd53 | |||
| 206c3884e9 | |||
| 35f9173980 | |||
| 48ab77eadc | |||
| f486513105 | |||
| f338adf033 | |||
| 74be667114 | |||
| b5a1fc92dc | |||
| 9cec1a8c49 | |||
| d4afba929b | |||
| 70939cbac6 | |||
| a3aa61df6d | |||
| e13ab5cb40 | |||
| d059947925 | |||
| d6c4b730de | |||
| 8241863170 | |||
| 31a758e4f3 | |||
| ca971a0e77 | |||
| a45a0f9a8a |
@@ -1,9 +1,6 @@
|
|||||||
[submodule "dep/polycentricandroid"]
|
[submodule "dep/polycentricandroid"]
|
||||||
path = dep/polycentricandroid
|
path = dep/polycentricandroid
|
||||||
url = ../polycentricandroid.git
|
url = ../polycentricandroid.git
|
||||||
[submodule "app/src/playstore/assets/sources/peertube"]
|
|
||||||
path = app/src/playstore/assets/sources/peertube
|
|
||||||
url = ../plugins/peertube.git
|
|
||||||
[submodule "app/src/stable/assets/sources/kick"]
|
[submodule "app/src/stable/assets/sources/kick"]
|
||||||
path = app/src/stable/assets/sources/kick
|
path = app/src/stable/assets/sources/kick
|
||||||
url = ../plugins/kick.git
|
url = ../plugins/kick.git
|
||||||
|
|||||||
+1
-1
@@ -19,7 +19,7 @@ Thank you for your interest in contributing! This document outlines how you can
|
|||||||
|
|
||||||
### License
|
### License
|
||||||
|
|
||||||
The official plugins for this project are licensed under GPLv3. Any contributions you make will also fall under the GPLv3 license.
|
The official plugins for this project are licensed under AGPL. Any contributions you make will also fall under the AGPL license.
|
||||||
|
|
||||||
### How to Contribute
|
### How to Contribute
|
||||||
|
|
||||||
|
|||||||
+48
-33
@@ -1,10 +1,11 @@
|
|||||||
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 '1.6.10'
|
id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.21'
|
||||||
id 'org.ajoberstar.grgit' version '1.7.2'
|
id 'org.ajoberstar.grgit' version '1.7.2'
|
||||||
id 'com.google.protobuf'
|
id 'com.google.protobuf'
|
||||||
id 'kotlin-parcelize'
|
id 'kotlin-parcelize'
|
||||||
|
id 'com.google.devtools.ksp'
|
||||||
}
|
}
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
@@ -23,7 +24,7 @@ if (keystorePropertiesFile.exists()) {
|
|||||||
|
|
||||||
protobuf {
|
protobuf {
|
||||||
protoc {
|
protoc {
|
||||||
artifact = 'com.google.protobuf:protoc:3.22.3'
|
artifact = 'com.google.protobuf:protoc:3.25.1'
|
||||||
}
|
}
|
||||||
generateProtoTasks {
|
generateProtoTasks {
|
||||||
all().each { task ->
|
all().each { task ->
|
||||||
@@ -38,7 +39,7 @@ protobuf {
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
namespace 'com.futo.platformplayer'
|
namespace 'com.futo.platformplayer'
|
||||||
compileSdk 33
|
compileSdk 34
|
||||||
flavorDimensions "buildType"
|
flavorDimensions "buildType"
|
||||||
productFlavors {
|
productFlavors {
|
||||||
stable {
|
stable {
|
||||||
@@ -96,11 +97,15 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdk 28
|
minSdk 28
|
||||||
targetSdk 33
|
targetSdk 34
|
||||||
versionCode gitVersionCode
|
versionCode gitVersionCode
|
||||||
versionName gitVersionName
|
versionName gitVersionName
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
|
ksp {
|
||||||
|
arg("room.schemaLocation", "$projectDir/schemas")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
@@ -136,43 +141,47 @@ android {
|
|||||||
universalApk true
|
universalApk true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
buildFeatures {
|
||||||
|
buildConfig true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
//Core
|
//Core
|
||||||
implementation 'androidx.core:core-ktx:1.7.0'
|
implementation 'androidx.core:core-ktx:1.12.0'
|
||||||
implementation 'androidx.appcompat:appcompat:1.4.1'
|
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||||
implementation 'com.google.android.material:material:1.5.0'
|
implementation 'com.google.android.material:material:1.11.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||||
|
|
||||||
//Images
|
//Images
|
||||||
annotationProcessor 'com.github.bumptech.glide:compiler:4.15.1'
|
annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'
|
||||||
implementation 'com.github.bumptech.glide:glide:4.15.1'
|
implementation 'com.github.bumptech.glide:glide:4.16.0'
|
||||||
|
|
||||||
//Async
|
//Async
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.2"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
|
||||||
|
|
||||||
//HTTP
|
//HTTP
|
||||||
implementation "com.squareup.okhttp3:okhttp:4.10.0"
|
implementation "com.squareup.okhttp3:okhttp:4.11.0"
|
||||||
|
|
||||||
//JSON
|
//JSON
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1" //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.10.1' //Used for complex/anonymous cases like during development conversions (eg. V8RemoteObject)
|
implementation 'com.google.code.gson:gson:2.10.1' //Used for complex/anonymous cases like during development conversions (eg. V8RemoteObject)
|
||||||
|
|
||||||
//JS
|
//JS
|
||||||
implementation("com.caoccao.javet:javet-android:2.2.1")
|
implementation("com.caoccao.javet:javet-android:3.0.2")
|
||||||
|
|
||||||
//Exoplayer
|
//Exoplayer
|
||||||
implementation 'com.google.android.exoplayer:exoplayer-core:2.18.7'
|
implementation 'androidx.media3:media3-exoplayer:1.2.1'
|
||||||
implementation 'com.google.android.exoplayer:exoplayer-dash:2.18.7'
|
implementation 'androidx.media3:media3-exoplayer-dash:1.2.1'
|
||||||
implementation 'com.google.android.exoplayer:exoplayer-ui:2.18.7'
|
implementation 'androidx.media3:media3-ui:1.2.1'
|
||||||
implementation 'com.google.android.exoplayer:exoplayer-hls:2.18.7'
|
implementation 'androidx.media3:media3-exoplayer-hls:1.2.1'
|
||||||
implementation 'com.google.android.exoplayer:exoplayer-rtsp:2.18.7'
|
implementation 'androidx.media3:media3-exoplayer-rtsp:1.2.1'
|
||||||
implementation 'com.google.android.exoplayer:exoplayer-smoothstreaming:2.18.7'
|
implementation 'androidx.media3:media3-exoplayer-smoothstreaming:1.2.1'
|
||||||
implementation 'com.google.android.exoplayer:exoplayer-transformer:2.18.7'
|
implementation 'androidx.media3:media3-transformer:1.2.1'
|
||||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.3'
|
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.6'
|
||||||
implementation 'androidx.navigation:navigation-ui-ktx:2.5.3'
|
implementation 'androidx.navigation:navigation-ui-ktx:2.7.6'
|
||||||
|
implementation 'androidx.media:media:1.7.0'
|
||||||
|
|
||||||
//Other
|
//Other
|
||||||
implementation 'org.jmdns:jmdns:3.5.1'
|
implementation 'org.jmdns:jmdns:3.5.1'
|
||||||
@@ -180,28 +189,34 @@ dependencies {
|
|||||||
implementation 'com.google.android.flexbox:flexbox:3.0.0'
|
implementation 'com.google.android.flexbox:flexbox:3.0.0'
|
||||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||||
implementation 'com.arthenica:ffmpeg-kit-full:5.1'
|
implementation 'com.arthenica:ffmpeg-kit-full:5.1'
|
||||||
implementation 'org.jetbrains.kotlin:kotlin-reflect:1.7.20'
|
implementation 'org.jetbrains.kotlin:kotlin-reflect:1.9.0'
|
||||||
implementation 'com.github.dhaval2404:imagepicker:2.1'
|
implementation 'com.github.dhaval2404:imagepicker:2.1'
|
||||||
implementation 'com.google.zxing:core:3.4.1'
|
implementation 'com.google.zxing:core:3.4.1'
|
||||||
implementation 'com.journeyapps:zxing-android-embedded:4.2.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:3.22.3'
|
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.8.1'
|
implementation 'androidx.work:work-runtime-ktx:2.9.0'
|
||||||
implementation 'androidx.concurrent:concurrent-futures-ktx:1.1.0'
|
implementation 'androidx.concurrent:concurrent-futures-ktx:1.1.0'
|
||||||
|
|
||||||
|
//Database
|
||||||
|
implementation("androidx.room:room-runtime:2.6.1")
|
||||||
|
annotationProcessor("androidx.room:room-compiler:2.6.1")
|
||||||
|
ksp("androidx.room:room-compiler:2.6.1")
|
||||||
|
implementation("androidx.room:room-ktx:2.6.1")
|
||||||
|
|
||||||
//Payment
|
//Payment
|
||||||
implementation 'com.stripe:stripe-android:20.28.3'
|
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.7.2'
|
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
|
||||||
testImplementation "org.jetbrains.kotlin:kotlin-test:1.8.20"
|
testImplementation "org.jetbrains.kotlin:kotlin-test:1.8.22"
|
||||||
testImplementation "org.xmlunit:xmlunit-core:2.9.1"
|
testImplementation "org.xmlunit:xmlunit-core:2.9.1"
|
||||||
testImplementation "org.mockito:mockito-core:5.4.0"
|
testImplementation "org.mockito:mockito-core:5.4.0"
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 3,
|
||||||
|
"identityHash": "ffba56c2f572c25080ce8596e8bb8945",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `url` TEXT NOT NULL, `position` INTEGER NOT NULL, `datetime` INTEGER NOT NULL, `name` TEXT NOT NULL, `serialized` BLOB)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "position",
|
||||||
|
"columnName": "position",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "datetime",
|
||||||
|
"columnName": "datetime",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serialized",
|
||||||
|
"columnName": "serialized",
|
||||||
|
"affinity": "BLOB",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_history_url",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_history_url` ON `${TABLE_NAME}` (`url`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_history_name",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_history_name` ON `${TABLE_NAME}` (`name`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_history_datetime",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"datetime"
|
||||||
|
],
|
||||||
|
"orders": [
|
||||||
|
"DESC"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_history_datetime` ON `${TABLE_NAME}` (`datetime` DESC)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ffba56c2f572c25080ce8596e8bb8945')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 5,
|
||||||
|
"identityHash": "eb813d54b9c44d29f1d7bb198a16d4d1",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "subscription_cache",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `url` TEXT, `channelUrl` TEXT, `datetime` INTEGER, `serialized` BLOB)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "channelUrl",
|
||||||
|
"columnName": "channelUrl",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "datetime",
|
||||||
|
"columnName": "datetime",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serialized",
|
||||||
|
"columnName": "serialized",
|
||||||
|
"affinity": "BLOB",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_subscription_cache_url",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_subscription_cache_url` ON `${TABLE_NAME}` (`url`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_subscription_cache_channelUrl",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"channelUrl"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_subscription_cache_channelUrl` ON `${TABLE_NAME}` (`channelUrl`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_subscription_cache_datetime",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"datetime"
|
||||||
|
],
|
||||||
|
"orders": [
|
||||||
|
"DESC"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_subscription_cache_datetime` ON `${TABLE_NAME}` (`datetime` DESC)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'eb813d54b9c44d29f1d7bb198a16d4d1')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 3,
|
||||||
|
"identityHash": "6e3b2d286325c4ea8a7a4c94c290daec",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "testing",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`someString` TEXT NOT NULL, `someNum` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT, `serialized` BLOB)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "someString",
|
||||||
|
"columnName": "someString",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "someNum",
|
||||||
|
"columnName": "someNum",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serialized",
|
||||||
|
"columnName": "serialized",
|
||||||
|
"affinity": "BLOB",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6e3b2d286325c4ea8a7a4c94c290daec')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package com.futo.platformplayer
|
||||||
|
|
||||||
|
import android.util.Base64
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import com.futo.platformplayer.casting.FCastCastingDevice
|
||||||
|
import com.futo.platformplayer.casting.Opcode
|
||||||
|
import com.futo.platformplayer.casting.models.FCastDecryptedMessage
|
||||||
|
import com.futo.platformplayer.casting.models.FCastEncryptedMessage
|
||||||
|
import com.futo.platformplayer.casting.models.FCastKeyExchangeMessage
|
||||||
|
import com.futo.platformplayer.casting.models.FCastPlayMessage
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import org.junit.Assert.*
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import java.security.KeyFactory
|
||||||
|
import java.security.spec.PKCS8EncodedKeySpec
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class FCastEncryptionTests {
|
||||||
|
@Test
|
||||||
|
fun testDHEncryptionSelf() {
|
||||||
|
val keyPair1 = FCastCastingDevice.generateKeyPair()
|
||||||
|
val keyPair2 = FCastCastingDevice.generateKeyPair()
|
||||||
|
Log.i("testDHEncryptionSelf", "privates (1: ${Base64.encodeToString(keyPair1.private.encoded, Base64.NO_WRAP)}, 2: ${Base64.encodeToString(keyPair2.private.encoded, Base64.NO_WRAP)})")
|
||||||
|
|
||||||
|
val keyExchangeMessage1 = FCastCastingDevice.getKeyExchangeMessage(keyPair1)
|
||||||
|
val keyExchangeMessage2 = FCastCastingDevice.getKeyExchangeMessage(keyPair2)
|
||||||
|
Log.i("testDHEncryptionSelf", "publics (1: ${keyExchangeMessage1.publicKey}, 2: ${keyExchangeMessage2.publicKey})")
|
||||||
|
|
||||||
|
val aesKey1 = FCastCastingDevice.computeSharedSecret(keyPair1.private, keyExchangeMessage2)
|
||||||
|
val aesKey2 = FCastCastingDevice.computeSharedSecret(keyPair2.private, keyExchangeMessage1)
|
||||||
|
|
||||||
|
assertEquals(Base64.encodeToString(aesKey1.encoded, Base64.NO_WRAP), Base64.encodeToString(aesKey2.encoded, Base64.NO_WRAP))
|
||||||
|
Log.i("testDHEncryptionSelf", "aesKey ${Base64.encodeToString(aesKey1.encoded, Base64.NO_WRAP)}")
|
||||||
|
|
||||||
|
val message = FCastPlayMessage("text/html")
|
||||||
|
val serializedBody = Json.encodeToString(message)
|
||||||
|
val encryptedMessage = FCastCastingDevice.encryptMessage(aesKey1, FCastDecryptedMessage(Opcode.Play.value.toLong(), serializedBody))
|
||||||
|
Log.i("testDHEncryptionSelf", Json.encodeToString(encryptedMessage))
|
||||||
|
|
||||||
|
val decryptedMessage = FCastCastingDevice.decryptMessage(aesKey1, encryptedMessage)
|
||||||
|
|
||||||
|
assertEquals(Opcode.Play.value.toLong(), decryptedMessage.opcode)
|
||||||
|
assertEquals(serializedBody, decryptedMessage.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testAESKeyGeneration() {
|
||||||
|
val cases = listOf(
|
||||||
|
listOf(
|
||||||
|
//Public other
|
||||||
|
"MIIBHzCBlQYJKoZIhvcNAQMBMIGHAoGBAP//////////yQ/aoiFowjTExmKLgNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7OZTgf//////////AgECA4GEAAKBgEnOS0oHteVA+3kND3u4yXe7GGRohy1LkR9Q5tL4c4ylC5n4iSwWSoIhcSIvUMWth6KAhPhu05sMcPY74rFMSS2AGTNCdT/5KilediipuUMdFVvjGqfNMNH1edzW5mquIw3iXKdfQmfY/qxLTI2wccyDj4hHFhLCZL3Y+shsm3KF",
|
||||||
|
//Private self
|
||||||
|
"MIIBIQIBADCBlQYJKoZIhvcNAQMBMIGHAoGBAP//////////yQ/aoiFowjTExmKLgNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7OZTgf//////////AgECBIGDAoGAeo/ceIeH8Jt1ZRNKX5aTHkMi23GCV1LtcS2O6Tktn9k8DCv7gIoekysQUhMyWtR+MsZlq2mXjr1JFpAyxl89rqoEPU6QDsGe9q8R4O8eBZ2u+48mkUkGSh7xPGRQUBvmhH2yk4hIEA8aK4BcYi1OTsCZtmk7pQq+uaFkKovD/8M=",
|
||||||
|
//AES
|
||||||
|
"7dpl1/6KQTTooOrFf2VlUOSqgrFHi6IYxapX0IxFfwk="
|
||||||
|
),
|
||||||
|
listOf(
|
||||||
|
//Public other
|
||||||
|
"MIIBHzCBlQYJKoZIhvcNAQMBMIGHAoGBAP//////////yQ/aoiFowjTExmKLgNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7OZTgf//////////AgECA4GEAAKBgGvIlCP/S+xpAuNEHSn4cEDOL1esUf+uMuY2Kp5J10a7HGbwzNd+7eYsgEc4+adddgB7hJgTvjsGg7lXUhHQ7WbfbCGgt7dbkx8qkic6Rgq4f5eRYd1Cgidw4MhZt7mEIOKrHweqnV6B9rypbXjbqauc6nGgtwx+Gvl6iLpVATRK",
|
||||||
|
//Private self
|
||||||
|
"MIIBIQIBADCBlQYJKoZIhvcNAQMBMIGHAoGBAP//////////yQ/aoiFowjTExmKLgNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7OZTgf//////////AgECBIGDAoGAMXmiIgWyutbaO+f4UiMAb09iVVSCI6Lb6xzNyD2MpUZyk4/JOT04Daj4JeCKFkF1Fq79yKhrnFlXCrF4WFX00xUOXb8BpUUUH35XG5ApvolQQLL6N0om8/MYP4FK/3PUxuZAJz45TUsI/v3u6UqJelVTNL83ltcFbZDIfEVftRA=",
|
||||||
|
//AES
|
||||||
|
"a2tUSxnXifKohfNocAQHkAlPffDv6ReihJ7OojBGt0Q="
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
for (case in cases) {
|
||||||
|
val decodedPrivateKey1 = Base64.decode(case[1], Base64.NO_WRAP)
|
||||||
|
val keyExchangeMessage2 = FCastKeyExchangeMessage(1, case[0])
|
||||||
|
|
||||||
|
val keyFactory = KeyFactory.getInstance("DH")
|
||||||
|
val privateKeySpec = PKCS8EncodedKeySpec(decodedPrivateKey1)
|
||||||
|
val privateKey = keyFactory.generatePrivate(privateKeySpec)
|
||||||
|
val aesKey1 = FCastCastingDevice.computeSharedSecret(privateKey, keyExchangeMessage2)
|
||||||
|
assertEquals(case[2], Base64.encodeToString(aesKey1.encoded, Base64.NO_WRAP))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDHEncryptionKnown() {
|
||||||
|
val decodedPrivateKey1 = Base64.decode("MIIDJwIBADCCAhgGCSqGSIb3DQEDATCCAgkCggEBAJVHXPXZPllsP80dkCrdAvQn9fPHIQMTu0X7TVuy5f4cvWeM1LvdhMmDa+HzHAd3clrrbC/Di4X0gHb6drzYFGzImm+y9wbdcZiYwgg9yNiW+EBi4snJTRN7BUqNgJatuNUZUjmO7KhSoK8S34Pkdapl1OwMOKlWDVZhGG/5i5/J62Du6LAwN2sja8c746zb10/WHB0kdfowd7jwgEZ4gf9+HKVv7gZteVBq3lHtu1RDpWOSfbxLpSAIZ0YXXIiFkl68ZMYUeQZ3NJaZDLcU7GZzBOJh+u4zs8vfAI4MP6kGUNl9OQnJJ1v0rIb/yz0D5t/IraWTQkLdbTvMoqQGywsCggEAQt67naWz2IzJVuCHh+w/Ogm7pfSLiJp0qvUxdKoPvn48W4/NelO+9WOw6YVgMolgqVF/QBTTMl/Hlivx4Ek3DXbRMUp2E355Lz8NuFnQleSluTICTweezy7wnHl0UrB3DhNQeC7Vfd95SXnc7yPLlvGDBhllxOvJPJxxxWuSWVWnX5TMzxRJrEPVhtC+7kMlGwsihzSdaN4NFEQD8T6AL0FG2ILgV68ZtvYnXGZ2yPoOPKJxOjJX/Rsn0GOfaV40fY0c+ayBmibKmwTLDrm3sDWYjRW7rGUhKlUjnPx+WPrjjXJQq5mR/7yXE0Al/ozgTEOZrZZWm+kaVG9JeGk8egSCAQQCggEAECNvEczf0y6IoX/IwhrPeWZ5IxrHcpwjcdVAuyZQLLlOq0iqnYMFcSD8QjMF8NKObfZZCDQUJlzGzRsG0oXsWiWtmoRvUZ9tQK0j28hDylpbyP00Bt9NlMgeHXkAy54P7Z2v/BPCd3o23kzjgXzYaSRuCFY7zQo1g1IQG8mfjYjdE4jjRVdVrlh8FS8x4OLPeglc+cp2/kuyxaVEfXAG84z/M8019mRSfdczi4z1iidPX6HgDEEWsN42Ud60mNKy5jsQpQYkRdOLmxR3+iQEtGFjdzbVhVCUr7S5EORU9B1MOl5gyPJpjfU3baOqrg6WXVyTvMDaA05YEnAHQNOOfA==", Base64.NO_WRAP)
|
||||||
|
val keyExchangeMessage2 = FCastKeyExchangeMessage(1, "MIIDJTCCAhgGCSqGSIb3DQEDATCCAgkCggEBAJVHXPXZPllsP80dkCrdAvQn9fPHIQMTu0X7TVuy5f4cvWeM1LvdhMmDa+HzHAd3clrrbC/Di4X0gHb6drzYFGzImm+y9wbdcZiYwgg9yNiW+EBi4snJTRN7BUqNgJatuNUZUjmO7KhSoK8S34Pkdapl1OwMOKlWDVZhGG/5i5/J62Du6LAwN2sja8c746zb10/WHB0kdfowd7jwgEZ4gf9+HKVv7gZteVBq3lHtu1RDpWOSfbxLpSAIZ0YXXIiFkl68ZMYUeQZ3NJaZDLcU7GZzBOJh+u4zs8vfAI4MP6kGUNl9OQnJJ1v0rIb/yz0D5t/IraWTQkLdbTvMoqQGywsCggEAQt67naWz2IzJVuCHh+w/Ogm7pfSLiJp0qvUxdKoPvn48W4/NelO+9WOw6YVgMolgqVF/QBTTMl/Hlivx4Ek3DXbRMUp2E355Lz8NuFnQleSluTICTweezy7wnHl0UrB3DhNQeC7Vfd95SXnc7yPLlvGDBhllxOvJPJxxxWuSWVWnX5TMzxRJrEPVhtC+7kMlGwsihzSdaN4NFEQD8T6AL0FG2ILgV68ZtvYnXGZ2yPoOPKJxOjJX/Rsn0GOfaV40fY0c+ayBmibKmwTLDrm3sDWYjRW7rGUhKlUjnPx+WPrjjXJQq5mR/7yXE0Al/ozgTEOZrZZWm+kaVG9JeGk8egOCAQUAAoIBAGlL9EYsrFz3I83NdlwhM241M+M7PA9P5WXgtdvS+pcalIaqN2IYdfzzCUfye7lchVkT9A2Y9eWQYX0OUhmjf8PPKkRkATLXrqO5HTsxV96aYNxMjz5ipQ6CaErTQaPLr3OPoauIMPVVI9zM+WT0KOGp49YMyx+B5rafT066vOVbF/0z1crq0ZXxyYBUv135rwFkIHxBMj5bhRLXKsZ2G5aLAZg0DsVam104mgN/v75f7Spg/n5hO7qxbNgbvSrvQ7Ag/rMk5T3sk7KoM23Qsjl08IZKs2jjx21MiOtyLqGuCW6GOTNK4yEEDF5gA0K13eXGwL5lPS0ilRw+Lrw7cJU=")
|
||||||
|
|
||||||
|
val keyFactory = KeyFactory.getInstance("DH")
|
||||||
|
val privateKeySpec = PKCS8EncodedKeySpec(decodedPrivateKey1)
|
||||||
|
val privateKey = keyFactory.generatePrivate(privateKeySpec)
|
||||||
|
val aesKey1 = FCastCastingDevice.computeSharedSecret(privateKey, keyExchangeMessage2)
|
||||||
|
assertEquals("vI5LGE625zGEG350ggkyBsIAXm2y4sNohiPcED1oAEE=", Base64.encodeToString(aesKey1.encoded, Base64.NO_WRAP))
|
||||||
|
|
||||||
|
val message = FCastPlayMessage("text/html")
|
||||||
|
val serializedBody = Json.encodeToString(message)
|
||||||
|
val encryptedMessage = FCastCastingDevice.encryptMessage(aesKey1, FCastDecryptedMessage(Opcode.Play.value.toLong(), serializedBody))
|
||||||
|
val decryptedMessage = FCastCastingDevice.decryptMessage(aesKey1, encryptedMessage)
|
||||||
|
|
||||||
|
assertEquals(Opcode.Play.value.toLong(), decryptedMessage.opcode)
|
||||||
|
assertEquals(serializedBody, decryptedMessage.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDecryptMessageKnown() {
|
||||||
|
val encryptedMessage = Json.decodeFromString<FCastEncryptedMessage>("{\"version\":1,\"iv\":\"C4H70VC5FWrNtkty9/cLIA==\",\"blob\":\"K6/N7JMyi1PFwKhU0mFj7ZJmd/tPp3NCOMldmQUtDaQ7hSmPoIMI5QNMOj+NFEiP4qTgtYp5QmBPoQum6O88pA==\"}")
|
||||||
|
val aesKey = SecretKeySpec(Base64.decode("+hr9Jg8yre7S9WGUohv2AUSzHNQN514JPh6MoFAcFNU=", Base64.NO_WRAP), "AES")
|
||||||
|
val decryptedMessage = FCastCastingDevice.decryptMessage(aesKey, encryptedMessage)
|
||||||
|
assertEquals(Opcode.Play.value.toLong(), decryptedMessage.opcode)
|
||||||
|
assertEquals("{\"container\":\"text/html\"}", decryptedMessage.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
+29
-13
@@ -1,13 +1,14 @@
|
|||||||
package com.futo.platformplayer
|
package com.futo.platformplayer
|
||||||
|
|
||||||
import com.futo.platformplayer.encryption.EncryptionProvider
|
import com.futo.platformplayer.encryption.GEncryptionProviderV0
|
||||||
|
import com.futo.platformplayer.encryption.GEncryptionProviderV1
|
||||||
import junit.framework.TestCase.assertEquals
|
import junit.framework.TestCase.assertEquals
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class EncryptionProviderTests {
|
class GEncryptionProviderTests {
|
||||||
@Test
|
@Test
|
||||||
fun testEncryptDecrypt() {
|
fun testEncryptDecryptV1() {
|
||||||
val encryptionProvider = EncryptionProvider.instance
|
val encryptionProvider = GEncryptionProviderV1.instance
|
||||||
val plaintext = "This is a test string."
|
val plaintext = "This is a test string."
|
||||||
|
|
||||||
// Encrypt the plaintext
|
// Encrypt the plaintext
|
||||||
@@ -22,8 +23,8 @@ class EncryptionProviderTests {
|
|||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testEncryptDecryptBytes() {
|
fun testEncryptDecryptBytesV1() {
|
||||||
val encryptionProvider = EncryptionProvider.instance
|
val encryptionProvider = GEncryptionProviderV1.instance
|
||||||
val bytes = "This is a test string.".toByteArray();
|
val bytes = "This is a test string.".toByteArray();
|
||||||
|
|
||||||
// Encrypt the plaintext
|
// Encrypt the plaintext
|
||||||
@@ -36,21 +37,36 @@ class EncryptionProviderTests {
|
|||||||
assertArrayEquals(bytes, decrypted);
|
assertArrayEquals(bytes, decrypted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testEncryptDecryptBytesPassword() {
|
fun testEncryptDecryptV0() {
|
||||||
val encryptionProvider = EncryptionProvider.instance
|
val encryptionProvider = GEncryptionProviderV0.instance
|
||||||
val bytes = "This is a test string.".toByteArray();
|
val plaintext = "This is a test string."
|
||||||
val password = "1234".padStart(32, '9');
|
|
||||||
|
|
||||||
// Encrypt the plaintext
|
// Encrypt the plaintext
|
||||||
val ciphertext = encryptionProvider.encrypt(bytes, password)
|
val ciphertext = encryptionProvider.encrypt(plaintext)
|
||||||
|
|
||||||
// Decrypt the ciphertext
|
// Decrypt the ciphertext
|
||||||
val decrypted = encryptionProvider.decrypt(ciphertext, password)
|
val decrypted = encryptionProvider.decrypt(ciphertext)
|
||||||
|
|
||||||
|
// The decrypted string should be equal to the original plaintext
|
||||||
|
assertEquals(plaintext, decrypted)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testEncryptDecryptBytesV0() {
|
||||||
|
val encryptionProvider = GEncryptionProviderV0.instance
|
||||||
|
val bytes = "This is a test string.".toByteArray();
|
||||||
|
|
||||||
|
// Encrypt the plaintext
|
||||||
|
val ciphertext = encryptionProvider.encrypt(bytes)
|
||||||
|
|
||||||
|
// Decrypt the ciphertext
|
||||||
|
val decrypted = encryptionProvider.decrypt(ciphertext)
|
||||||
|
|
||||||
// The decrypted string should be equal to the original plaintext
|
// The decrypted string should be equal to the original plaintext
|
||||||
assertArrayEquals(bytes, decrypted);
|
assertArrayEquals(bytes, decrypted);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun assertArrayEquals(a: ByteArray, b: ByteArray) {
|
private fun assertArrayEquals(a: ByteArray, b: ByteArray) {
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package com.futo.platformplayer
|
||||||
|
|
||||||
|
import com.futo.platformplayer.encryption.GPasswordEncryptionProviderV0
|
||||||
|
import com.futo.platformplayer.encryption.GPasswordEncryptionProviderV1
|
||||||
|
import junit.framework.TestCase.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class GPasswordEncryptionProviderTests {
|
||||||
|
@Test
|
||||||
|
fun testEncryptDecryptBytesPasswordV1() {
|
||||||
|
val encryptionProvider = GPasswordEncryptionProviderV1();
|
||||||
|
val bytes = "This is a test string.".toByteArray();
|
||||||
|
|
||||||
|
// Encrypt the plaintext
|
||||||
|
val ciphertext = encryptionProvider.encrypt(bytes, "1234")
|
||||||
|
|
||||||
|
// Decrypt the ciphertext
|
||||||
|
val decrypted = encryptionProvider.decrypt(ciphertext, "1234")
|
||||||
|
|
||||||
|
// The decrypted string should be equal to the original plaintext
|
||||||
|
assertArrayEquals(bytes, decrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testEncryptDecryptBytesPasswordV0() {
|
||||||
|
val encryptionProvider = GPasswordEncryptionProviderV0("1234".padStart(32, '9'));
|
||||||
|
val bytes = "This is a test string.".toByteArray();
|
||||||
|
|
||||||
|
// Encrypt the plaintext
|
||||||
|
val ciphertext = encryptionProvider.encrypt(bytes)
|
||||||
|
|
||||||
|
// Decrypt the ciphertext
|
||||||
|
val decrypted = encryptionProvider.decrypt(ciphertext)
|
||||||
|
|
||||||
|
// The decrypted string should be equal to the original plaintext
|
||||||
|
assertArrayEquals(bytes, decrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assertArrayEquals(a: ByteArray, b: ByteArray) {
|
||||||
|
assertEquals(a.size, b.size);
|
||||||
|
for(i in 0 until a.size) {
|
||||||
|
assertEquals(a[i], b[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,368 @@
|
|||||||
|
package com.futo.platformplayer
|
||||||
|
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
|
import com.futo.platformplayer.stores.db.ManagedDBDescriptor
|
||||||
|
import com.futo.platformplayer.stores.db.ManagedDBStore
|
||||||
|
import com.futo.platformplayer.testing.DBTOs
|
||||||
|
import org.junit.Assert
|
||||||
|
import org.junit.Test
|
||||||
|
import java.util.UUID
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import java.util.concurrent.ConcurrentMap
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
class ManagedDBStoreTests {
|
||||||
|
val context = InstrumentationRegistry.getInstrumentation().targetContext;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun startup() {
|
||||||
|
val store = ManagedDBStore.create("test", Descriptor())
|
||||||
|
.load(context, true);
|
||||||
|
store.deleteAll();
|
||||||
|
|
||||||
|
store.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun insert() {
|
||||||
|
val store = ManagedDBStore.create("test", Descriptor())
|
||||||
|
.load(context, true);
|
||||||
|
store.deleteAll();
|
||||||
|
|
||||||
|
val testObj = DBTOs.TestObject();
|
||||||
|
createAndAssert(store, testObj);
|
||||||
|
|
||||||
|
store.shutdown();
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
fun update() {
|
||||||
|
val store = ManagedDBStore.create("test", Descriptor())
|
||||||
|
.load(context, true);
|
||||||
|
store.deleteAll();
|
||||||
|
|
||||||
|
val testObj = DBTOs.TestObject();
|
||||||
|
val obj = createAndAssert(store, testObj);
|
||||||
|
|
||||||
|
testObj.someStr = "Testing";
|
||||||
|
store.update(obj.id!!, testObj);
|
||||||
|
val obj2 = store.get(obj.id!!);
|
||||||
|
assertIndexEquals(obj2, testObj);
|
||||||
|
|
||||||
|
store.shutdown();
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
fun delete() {
|
||||||
|
val store = ManagedDBStore.create("test", Descriptor())
|
||||||
|
.load(context, true);
|
||||||
|
store.deleteAll();
|
||||||
|
|
||||||
|
val testObj = DBTOs.TestObject();
|
||||||
|
val obj = createAndAssert(store, testObj);
|
||||||
|
store.delete(obj.id!!);
|
||||||
|
|
||||||
|
Assert.assertEquals(store.count(), 0);
|
||||||
|
Assert.assertNull(store.getOrNull(obj.id!!));
|
||||||
|
|
||||||
|
store.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun withIndex() {
|
||||||
|
val index = ConcurrentHashMap<Any, DBTOs.TestIndex>();
|
||||||
|
val store = ManagedDBStore.create("test", Descriptor())
|
||||||
|
.withIndex({it.someString}, index, true)
|
||||||
|
.load(context, true);
|
||||||
|
store.deleteAll();
|
||||||
|
|
||||||
|
val testObj1 = DBTOs.TestObject();
|
||||||
|
val testObj2 = DBTOs.TestObject();
|
||||||
|
val testObj3 = DBTOs.TestObject();
|
||||||
|
val obj1 = createAndAssert(store, testObj1);
|
||||||
|
val obj2 = createAndAssert(store, testObj2);
|
||||||
|
val obj3 = createAndAssert(store, testObj3);
|
||||||
|
Assert.assertEquals(store.count(), 3);
|
||||||
|
|
||||||
|
Assert.assertTrue(index.containsKey(testObj1.someStr));
|
||||||
|
Assert.assertTrue(index.containsKey(testObj2.someStr));
|
||||||
|
Assert.assertTrue(index.containsKey(testObj3.someStr));
|
||||||
|
Assert.assertEquals(index.size, 3);
|
||||||
|
|
||||||
|
val oldStr = testObj1.someStr;
|
||||||
|
testObj1.someStr = UUID.randomUUID().toString();
|
||||||
|
store.update(obj1.id!!, testObj1);
|
||||||
|
|
||||||
|
Assert.assertEquals(index.size, 3);
|
||||||
|
Assert.assertFalse(index.containsKey(oldStr));
|
||||||
|
Assert.assertTrue(index.containsKey(testObj1.someStr));
|
||||||
|
Assert.assertTrue(index.containsKey(testObj2.someStr));
|
||||||
|
Assert.assertTrue(index.containsKey(testObj3.someStr));
|
||||||
|
|
||||||
|
store.delete(obj2.id!!);
|
||||||
|
Assert.assertEquals(index.size, 2);
|
||||||
|
|
||||||
|
Assert.assertFalse(index.containsKey(oldStr));
|
||||||
|
Assert.assertTrue(index.containsKey(testObj1.someStr));
|
||||||
|
Assert.assertFalse(index.containsKey(testObj2.someStr));
|
||||||
|
Assert.assertTrue(index.containsKey(testObj3.someStr));
|
||||||
|
store.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun withUnique() {
|
||||||
|
val index = ConcurrentHashMap<Any, DBTOs.TestIndex>();
|
||||||
|
val store = ManagedDBStore.create("test", Descriptor())
|
||||||
|
.withIndex({it.someString}, index, false, true)
|
||||||
|
.load(context, true);
|
||||||
|
store.deleteAll();
|
||||||
|
|
||||||
|
val testObj1 = DBTOs.TestObject();
|
||||||
|
val testObj2 = DBTOs.TestObject();
|
||||||
|
val testObj3 = DBTOs.TestObject();
|
||||||
|
val obj1 = createAndAssert(store, testObj1);
|
||||||
|
val obj2 = createAndAssert(store, testObj2);
|
||||||
|
|
||||||
|
testObj3.someStr = testObj2.someStr;
|
||||||
|
Assert.assertEquals(store.insert(testObj3), obj2.id!!);
|
||||||
|
Assert.assertEquals(store.count(), 2);
|
||||||
|
|
||||||
|
store.shutdown();
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
fun getPage() {
|
||||||
|
val store = ManagedDBStore.create("test", Descriptor())
|
||||||
|
.load(context, true);
|
||||||
|
store.deleteAll();
|
||||||
|
|
||||||
|
val testObjs = createSequence(store, 25);
|
||||||
|
|
||||||
|
val page1 = store.getPage(0, 10);
|
||||||
|
val page2 = store.getPage(1, 10);
|
||||||
|
val page3 = store.getPage(2, 10);
|
||||||
|
Assert.assertEquals(10, page1.size);
|
||||||
|
Assert.assertEquals(10, page2.size);
|
||||||
|
Assert.assertEquals(5, page3.size);
|
||||||
|
|
||||||
|
store.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun query() {
|
||||||
|
val store = ManagedDBStore.create("test", Descriptor())
|
||||||
|
.load(context, true);
|
||||||
|
store.deleteAll();
|
||||||
|
|
||||||
|
val testStr = UUID.randomUUID().toString();
|
||||||
|
|
||||||
|
val testObj1 = DBTOs.TestObject();
|
||||||
|
val testObj2 = DBTOs.TestObject();
|
||||||
|
val testObj3 = DBTOs.TestObject();
|
||||||
|
val testObj4 = DBTOs.TestObject();
|
||||||
|
testObj3.someStr = testStr;
|
||||||
|
testObj4.someStr = testStr;
|
||||||
|
val obj1 = createAndAssert(store, testObj1);
|
||||||
|
val obj2 = createAndAssert(store, testObj2);
|
||||||
|
val obj3 = createAndAssert(store, testObj3);
|
||||||
|
val obj4 = createAndAssert(store, testObj4);
|
||||||
|
|
||||||
|
val results = store.query(DBTOs.TestIndex::someString, testStr);
|
||||||
|
|
||||||
|
Assert.assertEquals(2, results.size);
|
||||||
|
for(result in results) {
|
||||||
|
if(result.someNum == obj3.someNum)
|
||||||
|
assertIndexEquals(obj3, result);
|
||||||
|
else
|
||||||
|
assertIndexEquals(obj4, result);
|
||||||
|
}
|
||||||
|
store.shutdown();
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
fun queryPage() {
|
||||||
|
val index = ConcurrentHashMap<Any, DBTOs.TestIndex>();
|
||||||
|
val store = ManagedDBStore.create("test", Descriptor())
|
||||||
|
.withIndex({ it.someNum }, index)
|
||||||
|
.load(context, true);
|
||||||
|
store.deleteAll();
|
||||||
|
|
||||||
|
val testStr = UUID.randomUUID().toString();
|
||||||
|
|
||||||
|
val testResults = createSequence(store, 40, { i, testObject ->
|
||||||
|
if(i % 2 == 0)
|
||||||
|
testObject.someStr = testStr;
|
||||||
|
});
|
||||||
|
val page1 = store.queryPage(DBTOs.TestIndex::someString, testStr, 0,10);
|
||||||
|
val page2 = store.queryPage(DBTOs.TestIndex::someString, testStr, 1,10);
|
||||||
|
val page3 = store.queryPage(DBTOs.TestIndex::someString, testStr, 2,10);
|
||||||
|
|
||||||
|
Assert.assertEquals(10, page1.size);
|
||||||
|
Assert.assertEquals(10, page2.size);
|
||||||
|
Assert.assertEquals(0, page3.size);
|
||||||
|
|
||||||
|
|
||||||
|
store.shutdown();
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
fun queryPager() {
|
||||||
|
val testStr = UUID.randomUUID().toString();
|
||||||
|
testQuery(100, { i, testObject ->
|
||||||
|
if(i % 2 == 0)
|
||||||
|
testObject.someStr = testStr;
|
||||||
|
}) {
|
||||||
|
val pager = it.queryPager(DBTOs.TestIndex::someString, testStr, 10);
|
||||||
|
|
||||||
|
val items = pager.getResults().toMutableList();
|
||||||
|
while(pager.hasMorePages()) {
|
||||||
|
pager.nextPage();
|
||||||
|
items.addAll(pager.getResults());
|
||||||
|
}
|
||||||
|
Assert.assertEquals(50, items.size);
|
||||||
|
for(i in 0 until 50) {
|
||||||
|
val k = i * 2;
|
||||||
|
Assert.assertEquals(k, items[i].someNum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun queryLike() {
|
||||||
|
val testStr = UUID.randomUUID().toString();
|
||||||
|
val testStrLike = testStr.substring(0, 8) + "Testing" + testStr.substring(8, testStr.length);
|
||||||
|
testQuery(100, { i, testObject ->
|
||||||
|
if(i % 2 == 0)
|
||||||
|
testObject.someStr = testStrLike;
|
||||||
|
}) {
|
||||||
|
val results = it.queryLike(DBTOs.TestIndex::someString, "%Testing%");
|
||||||
|
|
||||||
|
Assert.assertEquals(50, results.size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
fun queryLikePager() {
|
||||||
|
val testStr = UUID.randomUUID().toString();
|
||||||
|
val testStrLike = testStr.substring(0, 8) + "Testing" + testStr.substring(8, testStr.length);
|
||||||
|
testQuery(100, { i, testObject ->
|
||||||
|
if(i % 2 == 0)
|
||||||
|
testObject.someStr = testStrLike;
|
||||||
|
|
||||||
|
}) {
|
||||||
|
val pager = it.queryLikePager(DBTOs.TestIndex::someString, "%Testing%", 10);
|
||||||
|
val items = pager.getResults().toMutableList();
|
||||||
|
while(pager.hasMorePages()) {
|
||||||
|
pager.nextPage();
|
||||||
|
items.addAll(pager.getResults());
|
||||||
|
}
|
||||||
|
Assert.assertEquals(50, items.size);
|
||||||
|
for(i in 0 until 50) {
|
||||||
|
val k = i * 2;
|
||||||
|
Assert.assertEquals(k, items[i].someNum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun queryGreater() {
|
||||||
|
testQuery(100, { i, testObject ->
|
||||||
|
testObject.someNum = i;
|
||||||
|
}) {
|
||||||
|
val results = it.queryGreater(DBTOs.TestIndex::someNum, 51);
|
||||||
|
Assert.assertEquals(48, results.size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
fun querySmaller() {
|
||||||
|
testQuery(100, { i, testObject ->
|
||||||
|
testObject.someNum = i;
|
||||||
|
}) {
|
||||||
|
val results = it.querySmaller(DBTOs.TestIndex::someNum, 30);
|
||||||
|
Assert.assertEquals(30, results.size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
fun queryBetween() {
|
||||||
|
testQuery(100, { i, testObject ->
|
||||||
|
testObject.someNum = i;
|
||||||
|
}) {
|
||||||
|
val results = it.queryBetween(DBTOs.TestIndex::someNum, 30, 65);
|
||||||
|
Assert.assertEquals(34, results.size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
fun queryIn() {
|
||||||
|
val ids = mutableListOf<String>()
|
||||||
|
testQuery(1100, { i, testObject ->
|
||||||
|
testObject.someNum = i;
|
||||||
|
ids.add(testObject.someStr);
|
||||||
|
}) {
|
||||||
|
val pager = it.queryInPager(DBTOs.TestIndex::someString, ids.take(1000), 65);
|
||||||
|
val list = mutableListOf<Any>();
|
||||||
|
list.addAll(pager.getResults());
|
||||||
|
while(pager.hasMorePages())
|
||||||
|
{
|
||||||
|
pager.nextPage();
|
||||||
|
list.addAll(pager.getResults());
|
||||||
|
}
|
||||||
|
Assert.assertEquals(1000, list.size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun testQuery(items: Int, modifier: (Int, DBTOs.TestObject)->Unit, testing: (ManagedDBStore<DBTOs.TestIndex, DBTOs.TestObject, DBTOs.DB, DBTOs.DBDAO>)->Unit) {
|
||||||
|
val store = ManagedDBStore.create("test", Descriptor())
|
||||||
|
.load(context, true);
|
||||||
|
store.deleteAll();
|
||||||
|
createSequence(store, items, modifier);
|
||||||
|
try {
|
||||||
|
testing(store);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
store.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun createSequence(store: ManagedDBStore<DBTOs.TestIndex, DBTOs.TestObject, DBTOs.DB, DBTOs.DBDAO>, count: Int, modifier: ((Int, DBTOs.TestObject)->Unit)? = null): List<DBTOs.TestIndex> {
|
||||||
|
val list = mutableListOf<DBTOs.TestIndex>();
|
||||||
|
for(i in 0 until count) {
|
||||||
|
val obj = DBTOs.TestObject();
|
||||||
|
obj.someNum = i;
|
||||||
|
modifier?.invoke(i, obj);
|
||||||
|
list.add(createAndAssert(store, obj));
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createAndAssert(store: ManagedDBStore<DBTOs.TestIndex, DBTOs.TestObject, DBTOs.DB, DBTOs.DBDAO>, obj: DBTOs.TestObject): DBTOs.TestIndex {
|
||||||
|
val id = store.insert(obj);
|
||||||
|
Assert.assertTrue(id > 0);
|
||||||
|
|
||||||
|
val dbObj = store.get(id);
|
||||||
|
assertIndexEquals(dbObj, obj);
|
||||||
|
return dbObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assertObjectEquals(obj1: DBTOs.TestObject, obj2: DBTOs.TestObject) {
|
||||||
|
Assert.assertEquals(obj1.someStr, obj2.someStr);
|
||||||
|
Assert.assertEquals(obj1.someNum, obj2.someNum);
|
||||||
|
}
|
||||||
|
private fun assertIndexEquals(obj1: DBTOs.TestIndex, obj2: DBTOs.TestObject) {
|
||||||
|
Assert.assertEquals(obj1.someString, obj2.someStr);
|
||||||
|
Assert.assertEquals(obj1.someNum, obj2.someNum);
|
||||||
|
assertObjectEquals(obj1.obj, obj2);
|
||||||
|
}
|
||||||
|
private fun assertIndexEquals(obj1: DBTOs.TestIndex, obj2: DBTOs.TestIndex) {
|
||||||
|
Assert.assertEquals(obj1.someString, obj2.someString);
|
||||||
|
Assert.assertEquals(obj1.someNum, obj2.someNum);
|
||||||
|
assertIndexEquals(obj1, obj2.obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Descriptor: ManagedDBDescriptor<DBTOs.TestObject, DBTOs.TestIndex, DBTOs.DB, DBTOs.DBDAO>() {
|
||||||
|
override val table_name: String = "testing";
|
||||||
|
override fun indexClass(): KClass<DBTOs.TestIndex> = DBTOs.TestIndex::class;
|
||||||
|
override fun dbClass(): KClass<DBTOs.DB> = DBTOs.DB::class;
|
||||||
|
override fun create(obj: DBTOs.TestObject): DBTOs.TestIndex = DBTOs.TestIndex(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,8 +7,13 @@
|
|||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" tools:ignore="ScopedStorage" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
|
||||||
|
<uses-permission android:name="com.android.alarm.permission.SET_ALARM"/>
|
||||||
|
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
|
||||||
|
<uses-permission android:name="android.permission.WRITE_SETTINGS" tools:ignore="ProtectedPermissions"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
@@ -20,7 +25,8 @@
|
|||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.FutoVideo"
|
android:theme="@style/Theme.FutoVideo"
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
tools:targetApi="31">
|
tools:targetApi="31"
|
||||||
|
android:largeHeap="true">
|
||||||
<provider
|
<provider
|
||||||
android:name="androidx.core.content.FileProvider"
|
android:name="androidx.core.content.FileProvider"
|
||||||
android:authorities="@string/authority"
|
android:authorities="@string/authority"
|
||||||
@@ -33,11 +39,15 @@
|
|||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:foregroundServiceType="mediaPlayback" />
|
android:foregroundServiceType="mediaPlayback" />
|
||||||
<service android:name=".services.DownloadService"
|
<service android:name=".services.DownloadService"
|
||||||
android:enabled="true" />
|
android:enabled="true"
|
||||||
|
android:foregroundServiceType="dataSync" />
|
||||||
<service android:name=".services.ExportingService"
|
<service android:name=".services.ExportingService"
|
||||||
android:enabled="true" />
|
android:enabled="true"
|
||||||
|
android:foregroundServiceType="dataSync" />
|
||||||
|
|
||||||
<receiver android:name=".receivers.MediaControlReceiver" />
|
<receiver android:name=".receivers.MediaControlReceiver" />
|
||||||
|
<receiver android:name=".receivers.AudioNoisyReceiver" />
|
||||||
|
<receiver android:name=".receivers.PlannedNotificationReceiver" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.MainActivity"
|
android:name=".activities.MainActivity"
|
||||||
@@ -57,6 +67,14 @@
|
|||||||
|
|
||||||
<data android:scheme="grayjay" />
|
<data android:scheme="grayjay" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="fcast" />
|
||||||
|
</intent-filter>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
@@ -91,6 +109,26 @@
|
|||||||
<data android:host="*" />
|
<data android:host="*" />
|
||||||
<data android:scheme="file" />
|
<data android:scheme="file" />
|
||||||
|
|
||||||
|
<data android:mimeType="text/plain" />
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter android:autoVerify="true">
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
<action android:name="android.intent.action.SEND" />
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<data android:host="*" />
|
||||||
|
<data android:scheme="content" />
|
||||||
|
|
||||||
|
<data android:mimeType="text/plain" />
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter android:autoVerify="true">
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
<action android:name="android.intent.action.SEND" />
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<data android:host="*" />
|
||||||
|
<data android:scheme="file" />
|
||||||
|
|
||||||
<data android:mimeType="application/zip" />
|
<data android:mimeType="application/zip" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter android:autoVerify="true">
|
<intent-filter android:autoVerify="true">
|
||||||
@@ -182,9 +220,12 @@
|
|||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.AddSourceOptionsActivity$QRCaptureActivity"
|
android:name=".activities.QRCaptureActivity"
|
||||||
|
android:screenOrientation="portrait"
|
||||||
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
|
<activity
|
||||||
|
android:name=".activities.FCastGuideActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
</application>
|
</application>
|
||||||
|
|||||||
@@ -233,6 +233,9 @@ function pluginRemoteProp(objID, propName) {
|
|||||||
function pluginRemoteCall(objID, methodName, args) {
|
function pluginRemoteCall(objID, methodName, args) {
|
||||||
return JSON.parse(syncPOST("/plugin/remoteCall?id=" + objID + "&method=" + methodName, {}, JSON.stringify(args)));
|
return JSON.parse(syncPOST("/plugin/remoteCall?id=" + objID + "&method=" + methodName, {}, JSON.stringify(args)));
|
||||||
}
|
}
|
||||||
|
function pluginRemoteTest(methodName, args) {
|
||||||
|
return JSON.parse(syncPOST("/plugin/remoteTest?method=" + methodName, {}, JSON.stringify(args)));
|
||||||
|
}
|
||||||
|
|
||||||
function pluginIsLoggedIn(cb, err) {
|
function pluginIsLoggedIn(cb, err) {
|
||||||
fetch("/plugin/isLoggedIn", {
|
fetch("/plugin/isLoggedIn", {
|
||||||
|
|||||||
@@ -385,8 +385,8 @@
|
|||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
<div style="width: 50%" v-if="Plugin.currentPlugin">
|
<div style="width: 50%" v-if="Plugin.currentPlugin">
|
||||||
<!--Get Home-->
|
<v-text-field v-model="searchTestMethods" label="Search for source methods.." style="margin-left: 35px; margin-right: 35px;"></v-text-field>
|
||||||
<v-card class="requestCard" v-for="req in Testing.requests">
|
<v-card class="requestCard" v-for="req in Testing.requests" v-show="req.title.indexOf(searchTestMethods) >= 0">
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<span v-if="req.isOptional">(Optional)</span>
|
<span v-if="req.isOptional">(Optional)</span>
|
||||||
@@ -402,6 +402,11 @@
|
|||||||
<div class="code">
|
<div class="code">
|
||||||
{{req.code}}
|
{{req.code}}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="documentation" v-if="req.docUrl" style="position: absolute; right: 15px; top: 15px;">
|
||||||
|
<a :href="req.docUrl" target="_blank">
|
||||||
|
Documentation
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="parameter" v-for="parameter in req.parameters">
|
<div class="parameter" v-for="parameter in req.parameters">
|
||||||
<div class="name">
|
<div class="name">
|
||||||
@@ -416,6 +421,9 @@
|
|||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-card-actions>
|
<v-card-actions>
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn @click="testSourceRemotely(req)">
|
||||||
|
Test Android
|
||||||
|
</v-btn>
|
||||||
<v-btn @click="testSource(req)">
|
<v-btn @click="testSource(req)">
|
||||||
Test
|
Test
|
||||||
</v-btn>
|
</v-btn>
|
||||||
@@ -535,14 +543,18 @@
|
|||||||
<!--<script src="./dependencies/vue.js"></script>-->
|
<!--<script src="./dependencies/vue.js"></script>-->
|
||||||
<!--<script src="./dependencies/vuetify.js"></script>-->
|
<!--<script src="./dependencies/vuetify.js"></script>-->
|
||||||
<script src="./source_docs.js"></script>
|
<script src="./source_docs.js"></script>
|
||||||
|
<script src="./source_doc_urls.js"></script>
|
||||||
<script src="./source.js"></script>
|
<script src="./source.js"></script>
|
||||||
<script src="./dev_bridge.js"></script>
|
<script src="./dev_bridge.js"></script>
|
||||||
<script>
|
<script>
|
||||||
IS_TESTING = true;
|
IS_TESTING = true;
|
||||||
let lastScriptTag = null;
|
let lastScriptTag = null;
|
||||||
|
let shouldDevLog = true;
|
||||||
|
let shouldLoginCheck = true;
|
||||||
new Vue({
|
new Vue({
|
||||||
el: '#app',
|
el: '#app',
|
||||||
data: {
|
data: {
|
||||||
|
searchTestMethods: "",
|
||||||
page: "Plugin",
|
page: "Plugin",
|
||||||
pastPluginUrls: [],
|
pastPluginUrls: [],
|
||||||
settings: {},
|
settings: {},
|
||||||
@@ -568,6 +580,9 @@
|
|||||||
Testing: {
|
Testing: {
|
||||||
requests: sourceDocs.map(x=>{
|
requests: sourceDocs.map(x=>{
|
||||||
x.parameters.forEach(y=>y.value = null);
|
x.parameters.forEach(y=>y.value = null);
|
||||||
|
|
||||||
|
if(sourceDocUrls[x.title])
|
||||||
|
x.docUrl = sourceDocUrls[x.title];
|
||||||
return x;
|
return x;
|
||||||
}),
|
}),
|
||||||
lastResult: "",
|
lastResult: "",
|
||||||
@@ -603,7 +618,7 @@
|
|||||||
};
|
};
|
||||||
setInterval(()=>{
|
setInterval(()=>{
|
||||||
try{
|
try{
|
||||||
if(!this.Plugin.currentPlugin)
|
if(!this.Plugin.currentPlugin || !shouldDevLog)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
getDevLogs(this.Integration.lastLogIndex, (newLogs)=> {
|
getDevLogs(this.Integration.lastLogIndex, (newLogs)=> {
|
||||||
@@ -638,7 +653,8 @@
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
setInterval(()=>{
|
setInterval(()=>{
|
||||||
try{
|
try{
|
||||||
this.isTestLoggedIn();
|
if(shouldLoginCheck)
|
||||||
|
this.isTestLoggedIn();
|
||||||
}catch(ex){}
|
}catch(ex){}
|
||||||
}, 2500);
|
}, 2500);
|
||||||
},
|
},
|
||||||
@@ -857,6 +873,53 @@
|
|||||||
"Error: " + ex;
|
"Error: " + ex;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
testSourceRemotely(req) {
|
||||||
|
const name = req.title;
|
||||||
|
const parameterVals = req.parameters.map(x=>{
|
||||||
|
if(x.value && x.value.startsWith && x.value.startsWith("json:"))
|
||||||
|
return JSON.parse(x.value.substring(5));
|
||||||
|
return x.value
|
||||||
|
});
|
||||||
|
|
||||||
|
if(name == "enable") {
|
||||||
|
if(parameterVals.length > 0)
|
||||||
|
parameterVals[0] = this.Plugin.currentPlugin;
|
||||||
|
else
|
||||||
|
parameterVals.push(this.Plugin.currentPlugin);
|
||||||
|
if(parameterVals.length > 1)
|
||||||
|
parameterVals[1] = __DEV_SETTINGS;
|
||||||
|
else
|
||||||
|
parameterVals.push(__DEV_SETTINGS);
|
||||||
|
}
|
||||||
|
|
||||||
|
const func = source[name];
|
||||||
|
if(!func)
|
||||||
|
alert("Test func not found");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const remoteResult = pluginRemoteTest(name, parameterVals);
|
||||||
|
console.log("Result for " + req.title, remoteResult);
|
||||||
|
this.Testing.lastResult = "//Results [" + name + "]\n" +
|
||||||
|
JSON.stringify(remoteResult, null, 3);
|
||||||
|
this.Testing.lastResultError = "";
|
||||||
|
}
|
||||||
|
catch(ex) {
|
||||||
|
if(ex.plugin_type == "CaptchaRequiredException") {
|
||||||
|
let shouldCaptcha = confirm("Do you want to request captcha?");
|
||||||
|
if(shouldCaptcha) {
|
||||||
|
pluginCaptchaTestPlugin(ex.url, ex.body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.error("Failed to run test for " + req.title, ex);
|
||||||
|
this.Testing.lastResult = ""
|
||||||
|
if(ex.message)
|
||||||
|
this.Testing.lastResultError = "//Results [" + name + "]\n\n" +
|
||||||
|
"Error: " + ex.message + "\n\n" + ex.stack;
|
||||||
|
else
|
||||||
|
this.Testing.lastResultError = "//Results [" + name + "]\n\n" +
|
||||||
|
"Error: " + ex;
|
||||||
|
}
|
||||||
|
},
|
||||||
showTestResults(results) {
|
showTestResults(results) {
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|||||||
+120
-46
@@ -1,13 +1,37 @@
|
|||||||
|
|
||||||
declare class ScriptException extends Error {
|
declare class ScriptException extends Error {
|
||||||
|
//If only one parameter is provided, acts as msg
|
||||||
constructor(type: string, msg: string);
|
constructor(type: string, msg: string);
|
||||||
}
|
}
|
||||||
declare class TimeoutException extends ScriptException {
|
|
||||||
|
declare class LoginRequiredException extends ScriptException {
|
||||||
constructor(msg: string);
|
constructor(msg: string);
|
||||||
}
|
}
|
||||||
|
//Alias
|
||||||
|
declare class ScriptLoginRequiredException extends ScriptException {
|
||||||
|
constructor(msg: string);
|
||||||
|
}
|
||||||
|
|
||||||
|
declare class CaptchaRequiredException extends ScriptException {
|
||||||
|
constructor(url: string, body: string);
|
||||||
|
}
|
||||||
|
|
||||||
|
declare class CriticalException extends ScriptException {
|
||||||
|
constructor(msg: string);
|
||||||
|
}
|
||||||
|
|
||||||
declare class UnavailableException extends ScriptException {
|
declare class UnavailableException extends ScriptException {
|
||||||
constructor(msg: string);
|
constructor(msg: string);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare class AgeException extends ScriptException {
|
||||||
|
constructor(msg: string);
|
||||||
|
}
|
||||||
|
|
||||||
|
declare class TimeoutException extends ScriptException {
|
||||||
|
constructor(msg: string);
|
||||||
|
}
|
||||||
|
|
||||||
declare class ScriptImplementationException extends ScriptException {
|
declare class ScriptImplementationException extends ScriptException {
|
||||||
constructor(msg: string);
|
constructor(msg: string);
|
||||||
}
|
}
|
||||||
@@ -38,16 +62,23 @@ declare class FilterCapability {
|
|||||||
|
|
||||||
|
|
||||||
declare class PlatformAuthorLink {
|
declare class PlatformAuthorLink {
|
||||||
constructor(id: PlatformID, name: string, url: string, thumbnail: string, subscribers: integer?);
|
constructor(id: PlatformID, name: string, url: string, thumbnail: string, subscribers: integer?, membershipUrl: string?);
|
||||||
|
}
|
||||||
|
|
||||||
|
declare class PlatformAuthorMembershipLink {
|
||||||
|
constructor(id: PlatformID, name: string, url: string, thumbnail: string, subscribers: integer?, membershipUrl: string?);
|
||||||
}
|
}
|
||||||
|
|
||||||
declare interface PlatformContentDef {
|
declare interface PlatformContentDef {
|
||||||
id: PlatformID,
|
id: PlatformID,
|
||||||
name: string,
|
name: string,
|
||||||
|
thumbnails: Thumbnails,
|
||||||
author: PlatformAuthorLink,
|
author: PlatformAuthorLink,
|
||||||
datetime: integer,
|
datetime: integer,
|
||||||
url: string
|
url: string
|
||||||
}
|
}
|
||||||
|
declare interface PlatformContent {}
|
||||||
|
|
||||||
declare interface PlatformNestedMediaContentDef extends PlatformContentDef {
|
declare interface PlatformNestedMediaContentDef extends PlatformContentDef {
|
||||||
contentUrl: string,
|
contentUrl: string,
|
||||||
contentName: string?,
|
contentName: string?,
|
||||||
@@ -59,16 +90,26 @@ declare class PlatformNestedMediaContent {
|
|||||||
constructor(obj: PlatformNestedMediaContentDef);
|
constructor(obj: PlatformNestedMediaContentDef);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare interface PlatformLockedContentDef extends PlatformContentDef {
|
||||||
|
contentName: string?,
|
||||||
|
contentThumbnails: Thumbnails?,
|
||||||
|
unlockUrl: string,
|
||||||
|
lockDescription: string?,
|
||||||
|
}
|
||||||
|
declare class PlatformLockedContent {
|
||||||
|
constructor(obj: PlatformLockedContentDef);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
declare interface PlatformVideoDef extends PlatformContentDef {
|
declare interface PlatformVideoDef extends PlatformContentDef {
|
||||||
thumbnails: Thumbnails,
|
thumbnails: Thumbnails,
|
||||||
author: PlatformAuthorLink,
|
author: PlatformAuthorLink,
|
||||||
|
|
||||||
duration: int,
|
duration: int,
|
||||||
viewCount: long,
|
viewCount: long,
|
||||||
isLive: boolean
|
isLive: boolean,
|
||||||
|
shareUrl: string?
|
||||||
}
|
}
|
||||||
declare interface PlatformContent {}
|
|
||||||
|
|
||||||
declare class PlatformVideo implements PlatformContent {
|
declare class PlatformVideo implements PlatformContent {
|
||||||
constructor(obj: PlatformVideoDef);
|
constructor(obj: PlatformVideoDef);
|
||||||
}
|
}
|
||||||
@@ -77,14 +118,15 @@ declare class PlatformVideo implements PlatformContent {
|
|||||||
declare interface PlatformVideoDetailsDef extends PlatformVideoDef {
|
declare interface PlatformVideoDetailsDef extends PlatformVideoDef {
|
||||||
description: string,
|
description: string,
|
||||||
video: VideoSourceDescriptor,
|
video: VideoSourceDescriptor,
|
||||||
live: SubtitleSource[],
|
live: IVideoSource,
|
||||||
rating: IRating
|
rating: IRating,
|
||||||
|
subtitles: SubtitleSource[]
|
||||||
}
|
}
|
||||||
declare class PlatformVideoDetails extends PlatformVideo {
|
declare class PlatformVideoDetails extends PlatformVideo {
|
||||||
constructor(obj: PlatformVideoDetailsDef);
|
constructor(obj: PlatformVideoDetailsDef);
|
||||||
}
|
}
|
||||||
|
|
||||||
declare class PlatformPostDef extends PlatformContentDef {
|
declare interface PlatformPostDef extends PlatformContentDef {
|
||||||
thumbnails: string[],
|
thumbnails: string[],
|
||||||
images: string[],
|
images: string[],
|
||||||
description: string
|
description: string
|
||||||
@@ -93,7 +135,7 @@ declare class PlatformPost extends PlatformContent {
|
|||||||
constructor(obj: PlatformPostDef)
|
constructor(obj: PlatformPostDef)
|
||||||
}
|
}
|
||||||
|
|
||||||
declare class PlatformPostDetailsDef extends PlatformPostDef {
|
declare interface PlatformPostDetailsDef extends PlatformPostDef {
|
||||||
rating: IRating,
|
rating: IRating,
|
||||||
textType: int,
|
textType: int,
|
||||||
content: String
|
content: String
|
||||||
@@ -110,8 +152,8 @@ declare interface MuxVideoSourceDescriptorDef {
|
|||||||
isUnMuxed: boolean,
|
isUnMuxed: boolean,
|
||||||
videoSources: VideoSource[]
|
videoSources: VideoSource[]
|
||||||
}
|
}
|
||||||
declare class MuxVideoSourceDescriptor implements IVideoSourceDescriptor {
|
declare class VideoSourceDescriptor implements IVideoSourceDescriptor {
|
||||||
constructor(obj: VideoSourceDescriptorDef);
|
constructor(videoSourcesOrObj: VideoSource[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
declare interface UnMuxVideoSourceDescriptorDef {
|
declare interface UnMuxVideoSourceDescriptorDef {
|
||||||
@@ -129,7 +171,7 @@ declare interface IVideoSource {
|
|||||||
declare interface IAudioSource {
|
declare interface IAudioSource {
|
||||||
|
|
||||||
}
|
}
|
||||||
interface VideoUrlSourceDef implements IVideoSource {
|
declare interface VideoUrlSourceDef implements IVideoSource {
|
||||||
width: integer,
|
width: integer,
|
||||||
height: integer,
|
height: integer,
|
||||||
container: string,
|
container: string,
|
||||||
@@ -139,22 +181,22 @@ interface VideoUrlSourceDef implements IVideoSource {
|
|||||||
duration: integer,
|
duration: integer,
|
||||||
url: string
|
url: string
|
||||||
}
|
}
|
||||||
class VideoUrlSource {
|
declare class VideoUrlSource {
|
||||||
constructor(obj: VideoUrlSourceDef);
|
constructor(obj: VideoUrlSourceDef);
|
||||||
|
|
||||||
getRequestModifier(): RequestModifier?;
|
getRequestModifier(): RequestModifier?;
|
||||||
}
|
}
|
||||||
interface VideoUrlRangeSourceDef extends VideoUrlSource {
|
declare interface VideoUrlRangeSourceDef extends VideoUrlSource {
|
||||||
itagId: integer,
|
itagId: integer,
|
||||||
initStart: integer,
|
initStart: integer,
|
||||||
initEnd: integer,
|
initEnd: integer,
|
||||||
indexStart: integer,
|
indexStart: integer,
|
||||||
indexEnd: integer,
|
indexEnd: integer,
|
||||||
}
|
}
|
||||||
class VideoUrlRangeSource extends VideoUrlSource {
|
declare class VideoUrlRangeSource extends VideoUrlSource {
|
||||||
constructor(obj: YTVideoSourceDef);
|
constructor(obj: YTVideoSourceDef);
|
||||||
}
|
}
|
||||||
interface AudioUrlSourceDef {
|
declare interface AudioUrlSourceDef {
|
||||||
name: string,
|
name: string,
|
||||||
bitrate: integer,
|
bitrate: integer,
|
||||||
container: string,
|
container: string,
|
||||||
@@ -163,24 +205,12 @@ interface AudioUrlSourceDef {
|
|||||||
url: string,
|
url: string,
|
||||||
language: string
|
language: string
|
||||||
}
|
}
|
||||||
class AudioUrlSource implements IAudioSource {
|
declare class AudioUrlSource implements IAudioSource {
|
||||||
constructor(obj: AudioUrlSourceDef);
|
constructor(obj: AudioUrlSourceDef);
|
||||||
|
|
||||||
getRequestModifier(): RequestModifier?;
|
getRequestModifier(): RequestModifier?;
|
||||||
}
|
}
|
||||||
interface IRequest {
|
declare interface AudioUrlRangeSourceDef extends AudioUrlSource {
|
||||||
url: string,
|
|
||||||
headers: Map<string, string>
|
|
||||||
}
|
|
||||||
interface IRequestModifierDef {
|
|
||||||
allowByteSkip: boolean
|
|
||||||
}
|
|
||||||
class RequestModifier {
|
|
||||||
constructor(obj: IRequestModifierDef) { }
|
|
||||||
|
|
||||||
modifyRequest(url: string, headers: Map<string, string>): IRequest;
|
|
||||||
}
|
|
||||||
interface AudioUrlRangeSourceDef extends AudioUrlSource {
|
|
||||||
itagId: integer,
|
itagId: integer,
|
||||||
initStart: integer,
|
initStart: integer,
|
||||||
initEnd: integer,
|
initEnd: integer,
|
||||||
@@ -188,28 +218,44 @@ interface AudioUrlRangeSourceDef extends AudioUrlSource {
|
|||||||
indexEnd: integer,
|
indexEnd: integer,
|
||||||
audioChannels: integer
|
audioChannels: integer
|
||||||
}
|
}
|
||||||
class AudioUrlRangeSource extends AudioUrlSource {
|
declare class AudioUrlRangeSource extends AudioUrlSource {
|
||||||
constructor(obj: AudioUrlRangeSourceDef);
|
constructor(obj: AudioUrlRangeSourceDef);
|
||||||
}
|
}
|
||||||
interface HLSSourceDef {
|
declare interface HLSSourceDef {
|
||||||
name: string,
|
name: string,
|
||||||
duration: integer,
|
duration: integer,
|
||||||
url: string
|
url: string,
|
||||||
|
priority: boolean?,
|
||||||
|
language: string?
|
||||||
}
|
}
|
||||||
class HLSSource implements IVideoSource {
|
declare class HLSSource implements IVideoSource {
|
||||||
constructor(obj: HLSSourceDef);
|
constructor(obj: HLSSourceDef);
|
||||||
}
|
}
|
||||||
interface DashSourceDef {
|
declare interface DashSourceDef {
|
||||||
name: string,
|
name: string,
|
||||||
duration: integer,
|
duration: integer,
|
||||||
url: string
|
url: string,
|
||||||
|
language: string?
|
||||||
}
|
}
|
||||||
class DashSource implements IVideoSource {
|
declare class DashSource implements IVideoSource {
|
||||||
constructor(obj: DashSourceDef)
|
constructor(obj: DashSourceDef)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare interface IRequest {
|
||||||
|
url: string,
|
||||||
|
headers: Map<string, string>
|
||||||
|
}
|
||||||
|
declare interface IRequestModifierDef {
|
||||||
|
allowByteSkip: boolean
|
||||||
|
}
|
||||||
|
declare class RequestModifier {
|
||||||
|
constructor(obj: IRequestModifierDef) { }
|
||||||
|
|
||||||
|
modifyRequest(url: string, headers: Map<string, string>): IRequest;
|
||||||
|
}
|
||||||
|
|
||||||
//Channel
|
//Channel
|
||||||
interface PlatformChannelDef {
|
declare interface PlatformChannelDef {
|
||||||
id: PlatformID,
|
id: PlatformID,
|
||||||
name: string,
|
name: string,
|
||||||
thumbnail: string,
|
thumbnail: string,
|
||||||
@@ -217,12 +263,29 @@ interface PlatformChannelDef {
|
|||||||
subscribers: integer,
|
subscribers: integer,
|
||||||
description: string,
|
description: string,
|
||||||
url: string,
|
url: string,
|
||||||
|
urlAlternatives: string[],
|
||||||
links: Map<string>?
|
links: Map<string>?
|
||||||
}
|
}
|
||||||
class PlatformChannel {
|
declare class PlatformChannel {
|
||||||
constructor(obj: PlatformChannelDef);
|
constructor(obj: PlatformChannelDef);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Playlist
|
||||||
|
declare interface PlatformPlaylistDef implements PlatformContent {
|
||||||
|
videoCount: integer,
|
||||||
|
thumbnail: string
|
||||||
|
}
|
||||||
|
declare class PlatformPlaylist extends PlatformContent {
|
||||||
|
constructor(obj: PlatformPlaylistDef);
|
||||||
|
}
|
||||||
|
declare interface PlatformPlaylistDetailsDef implements PlatformPlaylistDef {
|
||||||
|
contents: ContentPager
|
||||||
|
}
|
||||||
|
declare class PlatformPlaylistDetails extends PlatformContent {
|
||||||
|
constructor(obj: PlatformPlaylistDetailsDef);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//Ratings
|
//Ratings
|
||||||
interface IRating {
|
interface IRating {
|
||||||
type: integer
|
type: integer
|
||||||
@@ -250,7 +313,11 @@ declare class PlatformComment {
|
|||||||
constructor(obj: CommentDef);
|
constructor(obj: CommentDef);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare class PlaybackTracker {
|
||||||
|
constructor(interval: integer);
|
||||||
|
|
||||||
|
setProgress(seconds: integer);
|
||||||
|
}
|
||||||
|
|
||||||
declare class LiveEventPager {
|
declare class LiveEventPager {
|
||||||
nextRequest = 4000;
|
nextRequest = 4000;
|
||||||
@@ -261,8 +328,8 @@ declare class LiveEventPager {
|
|||||||
nextPage(): LiveEventPager; //Could be self
|
nextPage(): LiveEventPager; //Could be self
|
||||||
}
|
}
|
||||||
|
|
||||||
class LiveEvent {
|
declare class LiveEvent {
|
||||||
type: String
|
constructor(type: integer);
|
||||||
}
|
}
|
||||||
declare class LiveEventComment extends LiveEvent {
|
declare class LiveEventComment extends LiveEvent {
|
||||||
constructor(name: string, message: string, thumbnail: string?, colorName: string?, badges: string[]);
|
constructor(name: string, message: string, thumbnail: string?, colorName: string?, badges: string[]);
|
||||||
@@ -287,25 +354,31 @@ declare class ContentPager {
|
|||||||
constructor(results: PlatformContent[], hasMore: boolean);
|
constructor(results: PlatformContent[], hasMore: boolean);
|
||||||
|
|
||||||
hasMorePagers(): boolean
|
hasMorePagers(): boolean
|
||||||
nextPage(): VideoPager; //Could be self
|
nextPage(): ContentPager?; //Could be self
|
||||||
}
|
}
|
||||||
declare class VideoPager {
|
declare class VideoPager {
|
||||||
constructor(results: PlatformVideo[], hasMore: boolean);
|
constructor(results: PlatformVideo[], hasMore: boolean);
|
||||||
|
|
||||||
hasMorePagers(): boolean
|
hasMorePagers(): boolean
|
||||||
nextPage(): VideoPager; //Could be self
|
nextPage(): VideoPager?; //Could be self
|
||||||
}
|
}
|
||||||
declare class ChannelPager {
|
declare class ChannelPager {
|
||||||
constructor(results: PlatformChannel[], hasMore: boolean);
|
constructor(results: PlatformChannel[], hasMore: boolean);
|
||||||
|
|
||||||
hasMorePagers(): boolean;
|
hasMorePagers(): boolean;
|
||||||
nextPage(): ChannelPager; //Could be self
|
nextPage(): ChannelPager?; //Could be self
|
||||||
|
}
|
||||||
|
declare class PlaylistPager {
|
||||||
|
constructor(results: PlatformPlaylist[], hasMore: boolean);
|
||||||
|
|
||||||
|
hasMorePagers(): boolean;
|
||||||
|
nextPage(): PlaylistPager?;
|
||||||
}
|
}
|
||||||
declare class CommentPager {
|
declare class CommentPager {
|
||||||
constructor(results: PlatformComment[], hasMore: boolean);
|
constructor(results: PlatformComment[], hasMore: boolean);
|
||||||
|
|
||||||
hasMorePagers(): boolean
|
hasMorePagers(): boolean
|
||||||
nextPage(): CommentPager; //Could be self
|
nextPage(): CommentPager?; //Could be self
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Map<T> {
|
interface Map<T> {
|
||||||
@@ -341,8 +414,9 @@ interface Source {
|
|||||||
getChannelCapabilities(): ResultCapabilities;
|
getChannelCapabilities(): ResultCapabilities;
|
||||||
|
|
||||||
isContentDetailsUrl(url: string): boolean;
|
isContentDetailsUrl(url: string): boolean;
|
||||||
getContentDetails(url: string): PlatformVideoDetails;
|
getContentDetails(url: string): PlatformContentDetails;
|
||||||
|
|
||||||
|
//Optional
|
||||||
getLiveEvents(url: string): LiveEventPager;
|
getLiveEvents(url: string): LiveEventPager;
|
||||||
|
|
||||||
//Optional
|
//Optional
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ let Type = {
|
|||||||
Videos: "VIDEOS",
|
Videos: "VIDEOS",
|
||||||
Streams: "STREAMS",
|
Streams: "STREAMS",
|
||||||
Mixed: "MIXED",
|
Mixed: "MIXED",
|
||||||
Live: "LIVE"
|
Live: "LIVE",
|
||||||
|
Subscriptions: "SUBSCRIPTIONS"
|
||||||
},
|
},
|
||||||
Order: {
|
Order: {
|
||||||
Chronological: "CHRONOLOGICAL"
|
Chronological: "CHRONOLOGICAL"
|
||||||
@@ -31,23 +32,31 @@ let Type = {
|
|||||||
RAW: 0,
|
RAW: 0,
|
||||||
HTML: 1,
|
HTML: 1,
|
||||||
MARKUP: 2
|
MARKUP: 2
|
||||||
|
},
|
||||||
|
Chapter: {
|
||||||
|
NORMAL: 0,
|
||||||
|
|
||||||
|
SKIPPABLE: 5,
|
||||||
|
SKIP: 6,
|
||||||
|
SKIPONCE: 7
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let Language = {
|
let Language = {
|
||||||
UNKNOWN: "Unknown",
|
UNKNOWN: "Unknown",
|
||||||
ARABIC: "Arabic",
|
ARABIC: "ar",
|
||||||
SPANISH: "Spanish",
|
SPANISH: "es",
|
||||||
FRENCH: "French",
|
FRENCH: "fr",
|
||||||
HINDI: "Hindi",
|
HINDI: "hi",
|
||||||
INDONESIAN: "Indonesian",
|
INDONESIAN: "id",
|
||||||
KOREAN: "Korean",
|
KOREAN: "ko",
|
||||||
PORTBRAZIL: "Portuguese Brazilian",
|
PORTUGUESE: "pt",
|
||||||
RUSSIAN: "Russian",
|
PORTBRAZIL: "pt",
|
||||||
THAI: "Thai",
|
RUSSIAN: "ru",
|
||||||
TURKISH: "Turkish",
|
THAI: "th",
|
||||||
VIETNAMESE: "Vietnamese",
|
TURKISH: "tr",
|
||||||
ENGLISH: "English"
|
VIETNAMESE: "vi",
|
||||||
|
ENGLISH: "en"
|
||||||
}
|
}
|
||||||
|
|
||||||
class ScriptException extends Error {
|
class ScriptException extends Error {
|
||||||
@@ -64,6 +73,16 @@ class ScriptException extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
class ScriptLoginRequiredException extends ScriptException {
|
||||||
|
constructor(msg) {
|
||||||
|
super("ScriptLoginRequiredException", msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class LoginRequiredException extends ScriptException {
|
||||||
|
constructor(msg) {
|
||||||
|
super("ScriptLoginRequiredException", msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
class CaptchaRequiredException extends Error {
|
class CaptchaRequiredException extends Error {
|
||||||
constructor(url, body) {
|
constructor(url, body) {
|
||||||
super(JSON.stringify({ 'plugin_type': 'CaptchaRequiredException', url, body }));
|
super(JSON.stringify({ 'plugin_type': 'CaptchaRequiredException', url, body }));
|
||||||
@@ -153,13 +172,27 @@ class FilterCapability {
|
|||||||
|
|
||||||
|
|
||||||
class PlatformAuthorLink {
|
class PlatformAuthorLink {
|
||||||
constructor(id, name, url, thumbnail, subscribers) {
|
constructor(id, name, url, thumbnail, subscribers, membershipUrl) {
|
||||||
this.id = id ?? PlatformID(); //PlatformID
|
this.id = id ?? PlatformID(); //PlatformID
|
||||||
this.name = name ?? ""; //string
|
this.name = name ?? ""; //string
|
||||||
this.url = url ?? ""; //string
|
this.url = url ?? ""; //string
|
||||||
this.thumbnail = thumbnail; //string
|
this.thumbnail = thumbnail; //string
|
||||||
if(subscribers)
|
if(subscribers)
|
||||||
this.subscribers = subscribers;
|
this.subscribers = subscribers;
|
||||||
|
if(membershipUrl)
|
||||||
|
this.membershipUrl = membershipUrl ?? null; //string (for backcompat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class PlatformAuthorMembershipLink {
|
||||||
|
constructor(id, name, url, thumbnail, subscribers, membershipUrl) {
|
||||||
|
this.id = id ?? PlatformID(); //PlatformID
|
||||||
|
this.name = name ?? ""; //string
|
||||||
|
this.url = url ?? ""; //string
|
||||||
|
this.thumbnail = thumbnail; //string
|
||||||
|
if(subscribers)
|
||||||
|
this.subscribers = subscribers;
|
||||||
|
if(membershipUrl)
|
||||||
|
this.membershipUrl = membershipUrl ?? null; //string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class PlatformContent {
|
class PlatformContent {
|
||||||
@@ -190,6 +223,16 @@ class PlatformNestedMediaContent extends PlatformContent {
|
|||||||
this.contentThumbnails = obj.contentThumbnails ?? new Thumbnails();
|
this.contentThumbnails = obj.contentThumbnails ?? new Thumbnails();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
class PlatformLockedContent extends PlatformContent {
|
||||||
|
constructor(obj) {
|
||||||
|
super(obj, 70);
|
||||||
|
obj = obj ?? {};
|
||||||
|
this.contentName = obj.contentName;
|
||||||
|
this.contentThumbnails = obj.contentThumbnails ?? new Thumbnails();
|
||||||
|
this.unlockUrl = obj.unlockUrl ?? "";
|
||||||
|
this.lockDescription = obj.lockDescription;
|
||||||
|
}
|
||||||
|
}
|
||||||
class PlatformVideo extends PlatformContent {
|
class PlatformVideo extends PlatformContent {
|
||||||
constructor(obj) {
|
constructor(obj) {
|
||||||
super(obj, 1);
|
super(obj, 1);
|
||||||
@@ -211,8 +254,8 @@ class PlatformVideoDetails extends PlatformVideo {
|
|||||||
|
|
||||||
this.description = obj.description ?? "";//String
|
this.description = obj.description ?? "";//String
|
||||||
this.video = obj.video ?? {}; //VideoSourceDescriptor
|
this.video = obj.video ?? {}; //VideoSourceDescriptor
|
||||||
this.dash = obj.dash ?? null; //DashSource
|
this.dash = obj.dash ?? null; //DashSource, deprecated
|
||||||
this.hls = obj.hls ?? null; //HLSSource
|
this.hls = obj.hls ?? null; //HLSSource, deprecated
|
||||||
this.live = obj.live ?? null; //VideoSource
|
this.live = obj.live ?? null; //VideoSource
|
||||||
|
|
||||||
this.rating = obj.rating ?? null; //IRating
|
this.rating = obj.rating ?? null; //IRating
|
||||||
@@ -283,6 +326,8 @@ class VideoUrlSource {
|
|||||||
this.bitrate = obj.bitrate ?? 0;
|
this.bitrate = obj.bitrate ?? 0;
|
||||||
this.duration = obj.duration ?? 0;
|
this.duration = obj.duration ?? 0;
|
||||||
this.url = obj.url;
|
this.url = obj.url;
|
||||||
|
if(obj.requestModifier)
|
||||||
|
this.requestModifier = obj.requestModifier;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class VideoUrlRangeSource extends VideoUrlSource {
|
class VideoUrlRangeSource extends VideoUrlSource {
|
||||||
@@ -308,6 +353,8 @@ class AudioUrlSource {
|
|||||||
this.duration = obj.duration ?? 0;
|
this.duration = obj.duration ?? 0;
|
||||||
this.url = obj.url;
|
this.url = obj.url;
|
||||||
this.language = obj.language ?? Language.UNKNOWN;
|
this.language = obj.language ?? Language.UNKNOWN;
|
||||||
|
if(obj.requestModifier)
|
||||||
|
this.requestModifier = obj.requestModifier;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class AudioUrlRangeSource extends AudioUrlSource {
|
class AudioUrlRangeSource extends AudioUrlSource {
|
||||||
@@ -333,6 +380,8 @@ class HLSSource {
|
|||||||
this.priority = obj.priority ?? false;
|
this.priority = obj.priority ?? false;
|
||||||
if(obj.language)
|
if(obj.language)
|
||||||
this.language = obj.language;
|
this.language = obj.language;
|
||||||
|
if(obj.requestModifier)
|
||||||
|
this.requestModifier = obj.requestModifier;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class DashSource {
|
class DashSource {
|
||||||
@@ -344,13 +393,15 @@ class DashSource {
|
|||||||
this.url = obj.url;
|
this.url = obj.url;
|
||||||
if(obj.language)
|
if(obj.language)
|
||||||
this.language = obj.language;
|
this.language = obj.language;
|
||||||
|
if(obj.requestModifier)
|
||||||
|
this.requestModifier = obj.requestModifier;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class RequestModifier {
|
class RequestModifier {
|
||||||
constructor(obj) {
|
constructor(obj) {
|
||||||
obj = obj ?? {};
|
obj = obj ?? {};
|
||||||
this.allowByteSkip = obj.allowByteSkip;
|
this.allowByteSkip = obj.allowByteSkip; //Kinda deprecated.. wip
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,28 @@
|
|||||||
package com.futo.platformplayer
|
import androidx.annotation.OptIn
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
import androidx.media3.datasource.DefaultHttpDataSource
|
||||||
|
import androidx.media3.datasource.HttpDataSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
|
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
|
||||||
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
|
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
|
||||||
import com.futo.platformplayer.helpers.VideoHelper
|
import com.futo.platformplayer.helpers.VideoHelper
|
||||||
|
import com.futo.platformplayer.views.video.datasources.JSHttpDataSource
|
||||||
|
|
||||||
fun IPlatformVideoDetails.isDownloadable(): Boolean = VideoHelper.isDownloadable(this);
|
fun IPlatformVideoDetails.isDownloadable(): Boolean = VideoHelper.isDownloadable(this);
|
||||||
fun IVideoSource.isDownloadable(): Boolean = VideoHelper.isDownloadable(this);
|
fun IVideoSource.isDownloadable(): Boolean = VideoHelper.isDownloadable(this);
|
||||||
fun IAudioSource.isDownloadable(): Boolean = VideoHelper.isDownloadable(this);
|
fun IAudioSource.isDownloadable(): Boolean = VideoHelper.isDownloadable(this);
|
||||||
|
|
||||||
|
@UnstableApi
|
||||||
|
fun JSSource.getHttpDataSourceFactory(): HttpDataSource.Factory {
|
||||||
|
val requestModifier = getRequestModifier();
|
||||||
|
return if (requestModifier != null) {
|
||||||
|
JSHttpDataSource.Factory().setRequestModifier(requestModifier);
|
||||||
|
} else {
|
||||||
|
DefaultHttpDataSource.Factory();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun IVideoSourceDescriptor.hasAnySource(): Boolean = this.videoSources.any() || (this is VideoUnMuxedSourceDescriptor && this.audioSources.any());
|
fun IVideoSourceDescriptor.hasAnySource(): Boolean = this.videoSources.any() || (this is VideoUnMuxedSourceDescriptor && this.audioSources.any());
|
||||||
@@ -13,7 +13,8 @@ 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.time.toDuration
|
import kotlin.math.roundToInt
|
||||||
|
import kotlin.math.roundToLong
|
||||||
|
|
||||||
|
|
||||||
//Long
|
//Long
|
||||||
@@ -120,7 +121,8 @@ fun OffsetDateTime.getNowDiffMonths(): Long {
|
|||||||
return ChronoUnit.MONTHS.between(this, OffsetDateTime.now());
|
return ChronoUnit.MONTHS.between(this, OffsetDateTime.now());
|
||||||
}
|
}
|
||||||
fun OffsetDateTime.getNowDiffYears(): Long {
|
fun OffsetDateTime.getNowDiffYears(): Long {
|
||||||
return ChronoUnit.YEARS.between(this, OffsetDateTime.now());
|
val diff = ChronoUnit.MONTHS.between(this, OffsetDateTime.now()) / 12.0;
|
||||||
|
return diff.roundToLong();
|
||||||
}
|
}
|
||||||
|
|
||||||
fun OffsetDateTime.getDiffDays(otherDate: OffsetDateTime): Long {
|
fun OffsetDateTime.getDiffDays(otherDate: OffsetDateTime): Long {
|
||||||
@@ -151,6 +153,7 @@ fun OffsetDateTime.toHumanNowDiffString(abs: Boolean = false) : String {
|
|||||||
if(value >= secondsInYear) {
|
if(value >= secondsInYear) {
|
||||||
value = getNowDiffYears();
|
value = getNowDiffYears();
|
||||||
if(abs) value = abs(value);
|
if(abs) value = abs(value);
|
||||||
|
value = Math.max(1, value);
|
||||||
unit = "year";
|
unit = "year";
|
||||||
}
|
}
|
||||||
else if(value >= secondsInMonth) {
|
else if(value >= secondsInMonth) {
|
||||||
@@ -185,6 +188,25 @@ fun OffsetDateTime.toHumanNowDiffString(abs: Boolean = false) : String {
|
|||||||
|
|
||||||
return "${value} ${unit}";
|
return "${value} ${unit}";
|
||||||
};
|
};
|
||||||
|
fun Int.toHumanTimeIndicator(abs: Boolean = false) : String {
|
||||||
|
var value = this;
|
||||||
|
|
||||||
|
var unit = "s";
|
||||||
|
|
||||||
|
if(abs) value = abs(value);
|
||||||
|
if(value >= secondsInHour) {
|
||||||
|
value = (this / secondsInHour).toInt();
|
||||||
|
if(abs) value = abs(value);
|
||||||
|
unit = "hr" + (if(value > 1) "s" else "");
|
||||||
|
}
|
||||||
|
else if(value >= secondsInMinute) {
|
||||||
|
value = (this / secondsInMinute).toInt();
|
||||||
|
if(abs) value = abs(value);
|
||||||
|
unit = "min";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "${value}${unit}";
|
||||||
|
}
|
||||||
|
|
||||||
fun Long.toHumanTime(isMs: Boolean): String {
|
fun Long.toHumanTime(isMs: Boolean): String {
|
||||||
var scaler = 1;
|
var scaler = 1;
|
||||||
@@ -209,6 +231,18 @@ fun String.fixHtmlWhitespace(): Spanned {
|
|||||||
return Html.fromHtml(replace("\n", "<br />"), HtmlCompat.FROM_HTML_MODE_LEGACY);
|
return Html.fromHtml(replace("\n", "<br />"), HtmlCompat.FROM_HTML_MODE_LEGACY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Long.formatDuration(): String {
|
||||||
|
val hours = this / 3600000
|
||||||
|
val minutes = (this % 3600000) / 60000
|
||||||
|
val seconds = (this % 60000) / 1000
|
||||||
|
|
||||||
|
return if (hours > 0) {
|
||||||
|
String.format("%02d:%02d:%02d", hours, minutes, seconds)
|
||||||
|
} else {
|
||||||
|
String.format("%02d:%02d", minutes, seconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun String.fixHtmlLinks(): Spanned {
|
fun String.fixHtmlLinks(): Spanned {
|
||||||
//TODO: Properly fix whitespace handling.
|
//TODO: Properly fix whitespace handling.
|
||||||
val doc = Jsoup.parse(replace("\n", "<br />"));
|
val doc = Jsoup.parse(replace("\n", "<br />"));
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
package com.futo.platformplayer
|
package com.futo.platformplayer
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import com.google.common.base.CharMatcher
|
import com.google.common.base.CharMatcher
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
import java.net.Inet4Address
|
import java.net.Inet4Address
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import java.net.InetSocketAddress
|
import java.net.InetSocketAddress
|
||||||
@@ -165,7 +169,7 @@ private fun parseHextet(ipString: String, start: Int, end: Int): Short {
|
|||||||
var hextet = 0
|
var hextet = 0
|
||||||
for (i in start until end) {
|
for (i in start until end) {
|
||||||
hextet = hextet shl 4
|
hextet = hextet shl 4
|
||||||
hextet = hextet or ipString[i].digitToIntOrNull(16)!! ?: -1
|
hextet = hextet or ipString[i].digitToIntOrNull(16)!!
|
||||||
}
|
}
|
||||||
return hextet.toShort()
|
return hextet.toShort()
|
||||||
}
|
}
|
||||||
@@ -212,15 +216,20 @@ private fun ByteArray.toInetAddress(): InetAddress {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? {
|
fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? {
|
||||||
|
val timeout = 2000
|
||||||
|
|
||||||
if (addresses.isEmpty()) {
|
if (addresses.isEmpty()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (addresses.size == 1) {
|
if (addresses.size == 1) {
|
||||||
|
val socket = Socket()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return Socket(addresses[0], port);
|
return socket.apply { this.connect(InetSocketAddress(addresses[0], port), timeout) }
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
//Ignored.
|
Log.i("getConnectedSocket", "Failed to connect to: ${addresses[0]}", e)
|
||||||
|
socket.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@@ -245,7 +254,7 @@ fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.connect(InetSocketAddress(address, port));
|
socket.connect(InetSocketAddress(address, port), timeout);
|
||||||
|
|
||||||
synchronized(syncObject) {
|
synchronized(syncObject) {
|
||||||
if (connectedSocket == null) {
|
if (connectedSocket == null) {
|
||||||
@@ -259,7 +268,7 @@ fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
//Ignore
|
Log.i("getConnectedSocket", "Failed to connect to: $address", e)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -273,3 +282,46 @@ fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? {
|
|||||||
|
|
||||||
return connectedSocket;
|
return connectedSocket;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun InputStream.readHttpHeaderBytes() : ByteArray {
|
||||||
|
val headerBytes = ByteArrayOutputStream()
|
||||||
|
var crlfCount = 0
|
||||||
|
|
||||||
|
while (crlfCount < 4) {
|
||||||
|
val b = read()
|
||||||
|
if (b == -1) {
|
||||||
|
throw IOException("Unexpected end of stream while reading headers")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (b == 0x0D || b == 0x0A) { // CR or LF
|
||||||
|
crlfCount++
|
||||||
|
} else {
|
||||||
|
crlfCount = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
headerBytes.write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
return headerBytes.toByteArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun InputStream.readLine() : String? {
|
||||||
|
val line = ByteArrayOutputStream()
|
||||||
|
var crlfCount = 0
|
||||||
|
|
||||||
|
while (crlfCount < 2) {
|
||||||
|
val b = read()
|
||||||
|
if (b == -1) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (b == 0x0D || b == 0x0A) { // CR or LF
|
||||||
|
crlfCount++
|
||||||
|
} else {
|
||||||
|
crlfCount = 0
|
||||||
|
line.write(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(line.toByteArray(), Charsets.UTF_8)
|
||||||
|
}
|
||||||
@@ -1,6 +1,13 @@
|
|||||||
package com.futo.platformplayer
|
package com.futo.platformplayer
|
||||||
|
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||||
|
import com.futo.platformplayer.states.AnnouncementType
|
||||||
|
import com.futo.platformplayer.states.StateAnnouncement
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
|
import com.futo.polycentric.core.ProcessHandle
|
||||||
|
import com.futo.polycentric.core.Store
|
||||||
|
import com.futo.polycentric.core.SystemState
|
||||||
import userpackage.Protocol
|
import userpackage.Protocol
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
@@ -35,4 +42,34 @@ fun Protocol.ImageBundle?.selectHighestResolutionImage(): Protocol.ImageManifest
|
|||||||
|
|
||||||
fun Protocol.Claim.resolveChannelUrl(): String? {
|
fun Protocol.Claim.resolveChannelUrl(): String? {
|
||||||
return StatePlatform.instance.resolveChannelUrlByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) })
|
return StatePlatform.instance.resolveChannelUrlByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) })
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Protocol.Claim.resolveChannelUrls(): List<String> {
|
||||||
|
return StatePlatform.instance.resolveChannelUrlsByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) })
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun ProcessHandle.fullyBackfillServersAnnounceExceptions() {
|
||||||
|
val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(system))
|
||||||
|
if (!systemState.servers.contains(PolycentricCache.STAGING_SERVER)) {
|
||||||
|
removeServer(PolycentricCache.STAGING_SERVER)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!systemState.servers.contains(PolycentricCache.SERVER)) {
|
||||||
|
removeServer(PolycentricCache.SERVER)
|
||||||
|
}
|
||||||
|
|
||||||
|
val exceptions = fullyBackfillServers()
|
||||||
|
for (pair in exceptions) {
|
||||||
|
val server = pair.key
|
||||||
|
val exception = pair.value
|
||||||
|
|
||||||
|
StateAnnouncement.instance.registerAnnouncement(
|
||||||
|
"backfill-failed",
|
||||||
|
"Backfill failed",
|
||||||
|
"Failed to backfill server $server. $exception",
|
||||||
|
AnnouncementType.SESSION_RECURRING
|
||||||
|
);
|
||||||
|
|
||||||
|
Logger.e("Backfill", "Failed to backfill server $server.", exception)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
package com.futo.platformplayer
|
package com.futo.platformplayer
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import java.net.URI
|
||||||
|
import java.net.URISyntaxException
|
||||||
|
import java.net.URLEncoder
|
||||||
|
|
||||||
//Syntax sugaring
|
//Syntax sugaring
|
||||||
inline fun <reified T> Any.assume(): T?{
|
inline fun <reified T> Any.assume(): T?{
|
||||||
if(this is T)
|
if(this is T)
|
||||||
@@ -12,4 +17,12 @@ inline fun <reified T, R> Any.assume(cb: (T) -> R): R? {
|
|||||||
if(result != null)
|
if(result != null)
|
||||||
return cb(result);
|
return cb(result);
|
||||||
return null;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fun String?.yesNoToBoolean(): Boolean {
|
||||||
|
return this?.uppercase() == "YES"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Boolean?.toYesNo(): String {
|
||||||
|
return if (this == true) "YES" else "NO"
|
||||||
}
|
}
|
||||||
@@ -27,7 +27,7 @@ fun <R> V8Value?.orDefault(default: R, handler: (V8Value)->R): R {
|
|||||||
inline fun <reified T> V8Value.expectOrThrow(config: IV8PluginConfig, contextName: String): T {
|
inline fun <reified T> V8Value.expectOrThrow(config: IV8PluginConfig, contextName: String): T {
|
||||||
if(this !is T)
|
if(this !is T)
|
||||||
throw ScriptImplementationException(config, "Expected ${contextName} to be of type ${T::class.simpleName}, but found ${this::class.simpleName}");
|
throw ScriptImplementationException(config, "Expected ${contextName} to be of type ${T::class.simpleName}, but found ${this::class.simpleName}");
|
||||||
return this as T;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
//Singles
|
//Singles
|
||||||
@@ -109,11 +109,29 @@ inline fun <reified T> V8Value.expectV8Variant(config: IV8PluginConfig, contextN
|
|||||||
else
|
else
|
||||||
return this.expectOrThrow<V8ValueLong>(config, contextName).value.toLong() as T
|
return this.expectOrThrow<V8ValueLong>(config, contextName).value.toLong() as T
|
||||||
};
|
};
|
||||||
|
Float::class -> {
|
||||||
|
if(this is V8ValueDouble)
|
||||||
|
return this.value.toFloat() as T;
|
||||||
|
else if(this is V8ValueInteger)
|
||||||
|
return this.value.toFloat() as T;
|
||||||
|
else if(this is V8ValueLong)
|
||||||
|
return this.value.toFloat() as T;
|
||||||
|
else
|
||||||
|
return this.expectOrThrow<V8ValueDouble>(config, contextName).value.toDouble() as T
|
||||||
|
};
|
||||||
|
Double::class -> {
|
||||||
|
if(this is V8ValueDouble)
|
||||||
|
return this.value.toDouble() as T;
|
||||||
|
else if(this is V8ValueInteger)
|
||||||
|
return this.value.toDouble() as T;
|
||||||
|
else if(this is V8ValueLong)
|
||||||
|
return this.value.toDouble() as T;
|
||||||
|
else
|
||||||
|
return this.expectOrThrow<V8ValueDouble>(config, contextName).value.toDouble() as T
|
||||||
|
};
|
||||||
V8ValueObject::class -> this.expectOrThrow<V8ValueObject>(config, contextName) as T
|
V8ValueObject::class -> this.expectOrThrow<V8ValueObject>(config, contextName) as T
|
||||||
V8ValueArray::class -> this.expectOrThrow<V8ValueArray>(config, contextName) as T;
|
V8ValueArray::class -> this.expectOrThrow<V8ValueArray>(config, contextName) as T;
|
||||||
Boolean::class -> this.expectOrThrow<V8ValueBoolean>(config, contextName).value as T;
|
Boolean::class -> this.expectOrThrow<V8ValueBoolean>(config, contextName).value as T;
|
||||||
Float::class -> this.expectOrThrow<V8ValueDouble>(config, contextName).value.toFloat() as T;
|
|
||||||
Double::class -> this.expectOrThrow<V8ValueDouble>(config, contextName).value as T;
|
|
||||||
HashMap::class -> this.expectOrThrow<V8ValueObject>(config, contextName).let { V8ObjectToHashMap(it) } as T;
|
HashMap::class -> this.expectOrThrow<V8ValueObject>(config, contextName).let { V8ObjectToHashMap(it) } as T;
|
||||||
Map::class -> this.expectOrThrow<V8ValueObject>(config, contextName).let { V8ObjectToHashMap(it) } as T;
|
Map::class -> this.expectOrThrow<V8ValueObject>(config, contextName).let { V8ObjectToHashMap(it) } as T;
|
||||||
List::class -> this.expectOrThrow<V8ValueArray>(config, contextName).let { V8ArrayToStringList(it) } as T;
|
List::class -> this.expectOrThrow<V8ValueArray>(config, contextName).let { V8ArrayToStringList(it) } as T;
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.futo.platformplayer
|
||||||
|
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.view.View
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
|
||||||
|
class HorizontalSpaceItemDecoration(private val startSpace: Int, private val betweenSpace: Int, private val endSpace: Int) : RecyclerView.ItemDecoration() {
|
||||||
|
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
|
||||||
|
outRect.left = betweenSpace
|
||||||
|
|
||||||
|
val position = parent.getChildAdapterPosition(view)
|
||||||
|
if (position == 0) {
|
||||||
|
outRect.left = startSpace
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (position == state.itemCount - 1) {
|
||||||
|
outRect.right = endSpace
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.futo.platformplayer
|
||||||
|
|
||||||
|
class PresetImages {
|
||||||
|
companion object {
|
||||||
|
val images = mapOf<String, Int>(
|
||||||
|
Pair("xp_book", R.drawable.xp_book),
|
||||||
|
Pair("xp_forest", R.drawable.xp_forest),
|
||||||
|
Pair("xp_code", R.drawable.xp_code),
|
||||||
|
Pair("xp_controller", R.drawable.xp_controller),
|
||||||
|
Pair("xp_laptop", R.drawable.xp_laptop)
|
||||||
|
);
|
||||||
|
|
||||||
|
fun getPresetResIdByName(name: String): Int {
|
||||||
|
return images[name] ?: -1;
|
||||||
|
}
|
||||||
|
fun getPresetNameByResId(id: Int): String? {
|
||||||
|
return images.entries.find { it.value == id }?.key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,29 +4,43 @@ import android.content.ActivityNotFoundException
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
|
||||||
import android.webkit.CookieManager
|
import android.webkit.CookieManager
|
||||||
import androidx.core.content.ContextCompat.startActivity
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.activities.*
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
|
import com.futo.platformplayer.activities.ManageTabsActivity
|
||||||
|
import com.futo.platformplayer.activities.PolycentricHomeActivity
|
||||||
|
import com.futo.platformplayer.activities.PolycentricProfileActivity
|
||||||
|
import com.futo.platformplayer.activities.SettingsActivity
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.constructs.Event0
|
import com.futo.platformplayer.constructs.Event0
|
||||||
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.serializers.FlexibleBooleanSerializer
|
import com.futo.platformplayer.serializers.FlexibleBooleanSerializer
|
||||||
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
|
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
|
||||||
import com.futo.platformplayer.states.*
|
import com.futo.platformplayer.states.StateAnnouncement
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.futo.platformplayer.states.StateBackup
|
||||||
|
import com.futo.platformplayer.states.StateCache
|
||||||
|
import com.futo.platformplayer.states.StateMeta
|
||||||
|
import com.futo.platformplayer.states.StatePayment
|
||||||
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
|
import com.futo.platformplayer.states.StateUpdate
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
import com.futo.platformplayer.stores.FragmentedStorageFileJson
|
import com.futo.platformplayer.stores.FragmentedStorageFileJson
|
||||||
import com.futo.platformplayer.views.FeedStyle
|
import com.futo.platformplayer.views.FeedStyle
|
||||||
import com.futo.platformplayer.views.fields.DropdownFieldOptionsId
|
import com.futo.platformplayer.views.fields.DropdownFieldOptionsId
|
||||||
import com.futo.platformplayer.views.fields.FormField
|
|
||||||
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.FormFieldButton
|
||||||
|
import com.futo.platformplayer.views.fields.FormFieldWarning
|
||||||
|
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.*
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.*
|
import kotlinx.serialization.Transient
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
@@ -43,37 +57,49 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@Transient
|
@Transient
|
||||||
val onTabsChanged = Event0();
|
val onTabsChanged = Event0();
|
||||||
|
|
||||||
@FormField(
|
@FormField(R.string.manage_polycentric_identity, FieldForm.BUTTON, R.string.manage_your_polycentric_identity, -6)
|
||||||
"Manage Polycentric identity", FieldForm.BUTTON,
|
@FormFieldButton(R.drawable.ic_person)
|
||||||
"Manage your Polycentric identity", -3
|
|
||||||
)
|
|
||||||
fun managePolycentricIdentity() {
|
fun managePolycentricIdentity() {
|
||||||
SettingsActivity.getActivity()?.let {
|
SettingsActivity.getActivity()?.let {
|
||||||
if (StatePolycentric.instance.processHandle != null) {
|
if (StatePolycentric.instance.enabled) {
|
||||||
it.startActivity(Intent(it, PolycentricProfileActivity::class.java));
|
if (StatePolycentric.instance.processHandle != null) {
|
||||||
|
it.startActivity(Intent(it, PolycentricProfileActivity::class.java));
|
||||||
|
} else {
|
||||||
|
it.startActivity(Intent(it, PolycentricHomeActivity::class.java));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
it.startActivity(Intent(it, PolycentricHomeActivity::class.java));
|
UIDialogs.toast(it, "Polycentric is disabled")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(
|
@FormField(R.string.show_faq, FieldForm.BUTTON, R.string.get_answers_to_common_questions, -5)
|
||||||
"Open FAQ", FieldForm.BUTTON,
|
@FormFieldButton(R.drawable.ic_quiz)
|
||||||
"Get answers to common questions", -2
|
|
||||||
)
|
|
||||||
fun openFAQ() {
|
fun openFAQ() {
|
||||||
try {
|
try {
|
||||||
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://grayjay.app/faq.html"))
|
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(Settings.URL_FAQ))
|
||||||
|
SettingsActivity.getActivity()?.startActivity(browserIntent);
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
//Ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@FormField(R.string.show_issues, FieldForm.BUTTON, R.string.a_list_of_user_reported_and_self_reported_issues, -4)
|
||||||
|
@FormFieldButton(R.drawable.ic_data_alert)
|
||||||
|
fun openIssues() {
|
||||||
|
try {
|
||||||
|
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/futo-org/grayjay-android/issues"))
|
||||||
SettingsActivity.getActivity()?.startActivity(browserIntent);
|
SettingsActivity.getActivity()?.startActivity(browserIntent);
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
//Ignored
|
//Ignored
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
@FormField(
|
@FormField(
|
||||||
"Submit feedback", FieldForm.BUTTON,
|
R.string.submit_feedback, FieldForm.BUTTON,
|
||||||
"Give feedback on the application", -1
|
R.string.give_feedback_on_the_application, -1
|
||||||
)
|
)
|
||||||
|
@FormFieldButton(R.drawable.ic_bug)
|
||||||
fun submitFeedback() {
|
fun submitFeedback() {
|
||||||
try {
|
try {
|
||||||
val i = Intent(Intent.ACTION_VIEW);
|
val i = Intent(Intent.ACTION_VIEW);
|
||||||
@@ -87,12 +113,10 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
//Ignored
|
//Ignored
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
|
|
||||||
@FormField(
|
@FormField(R.string.manage_tabs, FieldForm.BUTTON, R.string.change_tabs_visible_on_the_home_screen, -3)
|
||||||
"Manage Tabs", FieldForm.BUTTON,
|
@FormFieldButton(R.drawable.ic_tabs)
|
||||||
"Change tabs visible on the home screen", -1
|
|
||||||
)
|
|
||||||
fun manageTabs() {
|
fun manageTabs() {
|
||||||
try {
|
try {
|
||||||
SettingsActivity.getActivity()?.let {
|
SettingsActivity.getActivity()?.let {
|
||||||
@@ -103,11 +127,58 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField("Home", "group", "Configure how your Home tab works and feels", 1)
|
|
||||||
|
|
||||||
|
@FormField(R.string.import_data, FieldForm.BUTTON, R.string.import_data_description, -2)
|
||||||
|
@FormFieldButton(R.drawable.ic_move_up)
|
||||||
|
fun import() {
|
||||||
|
val act = SettingsActivity.getActivity() ?: return;
|
||||||
|
val intent = MainActivity.getImportOptionsIntent(act);
|
||||||
|
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK;
|
||||||
|
act.startActivity(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FormField(R.string.link_handling, FieldForm.BUTTON, R.string.allow_grayjay_to_handle_links, -1)
|
||||||
|
@FormFieldButton(R.drawable.ic_link)
|
||||||
|
fun manageLinks() {
|
||||||
|
try {
|
||||||
|
SettingsActivity.getActivity()?.let { UIDialogs.showUrlHandlingPrompt(it) }
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to show url handling prompt", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@FormField(R.string.language, "group", -1, 0)
|
||||||
|
var language = LanguageSettings();
|
||||||
|
@Serializable
|
||||||
|
class LanguageSettings {
|
||||||
|
@FormField(R.string.app_language, FieldForm.DROPDOWN, R.string.may_require_restart, 5, "app_language")
|
||||||
|
@DropdownFieldOptionsId(R.array.app_languages)
|
||||||
|
var appLanguage: Int = 0;
|
||||||
|
|
||||||
|
fun getAppLanguageLocaleString(): String? {
|
||||||
|
return when(appLanguage) {
|
||||||
|
0 -> null
|
||||||
|
1 -> "en";
|
||||||
|
2 -> "de";
|
||||||
|
3 -> "es";
|
||||||
|
4 -> "pt";
|
||||||
|
5 -> "fr"
|
||||||
|
6 -> "ja";
|
||||||
|
7 -> "ko";
|
||||||
|
8 -> "zh";
|
||||||
|
9 -> "ru";
|
||||||
|
10 -> "ar";
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@FormField(R.string.home, "group", R.string.configure_how_your_home_tab_works_and_feels, 1)
|
||||||
var home = HomeSettings();
|
var home = HomeSettings();
|
||||||
@Serializable
|
@Serializable
|
||||||
class HomeSettings {
|
class HomeSettings {
|
||||||
@FormField("Feed Style", FieldForm.DROPDOWN, "", 5)
|
@FormField(R.string.feed_style, FieldForm.DROPDOWN, R.string.may_require_restart, 5)
|
||||||
@DropdownFieldOptionsId(R.array.feed_style)
|
@DropdownFieldOptionsId(R.array.feed_style)
|
||||||
var homeFeedStyle: Int = 1;
|
var homeFeedStyle: Int = 1;
|
||||||
|
|
||||||
@@ -117,21 +188,45 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
else
|
else
|
||||||
return FeedStyle.THUMBNAIL;
|
return FeedStyle.THUMBNAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
|
||||||
|
var previewFeedItems: Boolean = true;
|
||||||
|
|
||||||
|
|
||||||
|
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
|
||||||
|
var progressBar: Boolean = true;
|
||||||
|
|
||||||
|
|
||||||
|
@FormField(R.string.clear_hidden, FieldForm.BUTTON, R.string.clear_hidden_description, 8)
|
||||||
|
@FormFieldButton(R.drawable.ic_visibility_off)
|
||||||
|
fun clearHidden() {
|
||||||
|
StateMeta.instance.removeAllHiddenCreators();
|
||||||
|
StateMeta.instance.removeAllHiddenVideos();
|
||||||
|
SettingsActivity.getActivity()?.let {
|
||||||
|
UIDialogs.toast(it, "Creators and videos should show up again");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField("Search", "group", "", 2)
|
@FormField(R.string.search, "group", -1, 2)
|
||||||
var search = SearchSettings();
|
var search = SearchSettings();
|
||||||
@Serializable
|
@Serializable
|
||||||
class SearchSettings {
|
class SearchSettings {
|
||||||
@FormField("Search History", FieldForm.TOGGLE, "", 4)
|
@FormField(R.string.search_history, FieldForm.TOGGLE, R.string.may_require_restart, 3)
|
||||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||||
var searchHistory: Boolean = true;
|
var searchHistory: Boolean = true;
|
||||||
|
|
||||||
|
|
||||||
@FormField("Feed Style", FieldForm.DROPDOWN, "", 5)
|
@FormField(R.string.feed_style, FieldForm.DROPDOWN, -1, 4)
|
||||||
@DropdownFieldOptionsId(R.array.feed_style)
|
@DropdownFieldOptionsId(R.array.feed_style)
|
||||||
var searchFeedStyle: Int = 1;
|
var searchFeedStyle: Int = 1;
|
||||||
|
|
||||||
|
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 5)
|
||||||
|
var previewFeedItems: Boolean = true;
|
||||||
|
|
||||||
|
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
|
||||||
|
var progressBar: Boolean = true;
|
||||||
|
|
||||||
|
|
||||||
fun getSearchFeedStyle(): FeedStyle {
|
fun getSearchFeedStyle(): FeedStyle {
|
||||||
if(searchFeedStyle == 0)
|
if(searchFeedStyle == 0)
|
||||||
@@ -141,11 +236,21 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField("Subscriptions", "group", "Configure how your Subscriptions works and feels", 3)
|
|
||||||
|
@FormField(R.string.channel, "group", -1, 3)
|
||||||
|
var channel = ChannelSettings();
|
||||||
|
@Serializable
|
||||||
|
class ChannelSettings {
|
||||||
|
|
||||||
|
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
|
||||||
|
var progressBar: Boolean = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@FormField(R.string.subscriptions, "group", R.string.configure_how_your_subscriptions_works_and_feels, 4)
|
||||||
var subscriptions = SubscriptionsSettings();
|
var subscriptions = SubscriptionsSettings();
|
||||||
@Serializable
|
@Serializable
|
||||||
class SubscriptionsSettings {
|
class SubscriptionsSettings {
|
||||||
@FormField("Feed Style", FieldForm.DROPDOWN, "", 5)
|
@FormField(R.string.feed_style, FieldForm.DROPDOWN, R.string.may_require_restart, 4)
|
||||||
@DropdownFieldOptionsId(R.array.feed_style)
|
@DropdownFieldOptionsId(R.array.feed_style)
|
||||||
var subscriptionsFeedStyle: Int = 1;
|
var subscriptionsFeedStyle: Int = 1;
|
||||||
|
|
||||||
@@ -156,11 +261,23 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
return FeedStyle.THUMBNAIL;
|
return FeedStyle.THUMBNAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField("Fetch on app boot", FieldForm.TOGGLE, "Shortly after opening the app, start fetching subscriptions.", 6)
|
@FormField(R.string.show_subscription_group, FieldForm.TOGGLE, R.string.show_subscription_group_description, 5)
|
||||||
|
var showSubscriptionGroups: Boolean = true;
|
||||||
|
|
||||||
|
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
|
||||||
|
var previewFeedItems: Boolean = true;
|
||||||
|
|
||||||
|
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 7)
|
||||||
|
var progressBar: Boolean = true;
|
||||||
|
|
||||||
|
@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;
|
||||||
|
|
||||||
@FormField("Background Update", FieldForm.DROPDOWN, "Experimental background update for subscriptions cache (requires restart)", 7)
|
@FormField(R.string.fetch_on_tab_opened, FieldForm.TOGGLE, R.string.fetch_on_tab_opened_description, 9)
|
||||||
|
var fetchOnTabOpen: Boolean = true;
|
||||||
|
|
||||||
|
@FormField(R.string.background_update, FieldForm.DROPDOWN, R.string.experimental_background_update_for_subscriptions_cache, 10, "background_update")
|
||||||
@DropdownFieldOptionsId(R.array.background_interval)
|
@DropdownFieldOptionsId(R.array.background_interval)
|
||||||
var subscriptionsBackgroundUpdateInterval: Int = 0;
|
var subscriptionsBackgroundUpdateInterval: Int = 0;
|
||||||
|
|
||||||
@@ -176,26 +293,67 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@FormField("Subscription Concurrency", FieldForm.DROPDOWN, "Specify how many threads are used to fetch channels (requires restart)", 8)
|
@FormField(R.string.subscription_concurrency, FieldForm.DROPDOWN, R.string.specify_how_many_threads_are_used_to_fetch_channels, 11)
|
||||||
@DropdownFieldOptionsId(R.array.thread_count)
|
@DropdownFieldOptionsId(R.array.thread_count)
|
||||||
var subscriptionConcurrency: Int = 3;
|
var subscriptionConcurrency: Int = 3;
|
||||||
|
|
||||||
fun getSubscriptionsConcurrency() : Int {
|
fun getSubscriptionsConcurrency() : Int {
|
||||||
return threadIndexToCount(subscriptionConcurrency);
|
return threadIndexToCount(subscriptionConcurrency);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@FormField(R.string.show_watch_metrics, FieldForm.TOGGLE, R.string.show_watch_metrics_description, 12)
|
||||||
|
var showWatchMetrics: Boolean = false;
|
||||||
|
|
||||||
|
@FormField(R.string.track_playtime_locally, FieldForm.TOGGLE, R.string.track_playtime_locally_description, 13)
|
||||||
|
var allowPlaytimeTracking: Boolean = true;
|
||||||
|
|
||||||
|
|
||||||
|
@FormField(R.string.always_reload_from_cache, FieldForm.TOGGLE, R.string.always_reload_from_cache_description, 14)
|
||||||
|
var alwaysReloadFromCache: Boolean = false;
|
||||||
|
|
||||||
|
@FormField(R.string.peek_channel_contents, FieldForm.TOGGLE, R.string.peek_channel_contents_description, 15)
|
||||||
|
var peekChannelContents: Boolean = false;
|
||||||
|
|
||||||
|
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON, R.string.clear_channel_cache_description, 16)
|
||||||
|
fun clearChannelCache() {
|
||||||
|
UIDialogs.toast(SettingsActivity.getActivity()!!, "Started clearing..");
|
||||||
|
StateCache.instance.clear();
|
||||||
|
UIDialogs.toast(SettingsActivity.getActivity()!!, "Finished clearing");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField("Player", "group", "Change behavior of the player", 4)
|
@FormField(R.string.player, "group", R.string.change_behavior_of_the_player, 5)
|
||||||
var playback = PlaybackSettings();
|
var playback = PlaybackSettings();
|
||||||
@Serializable
|
@Serializable
|
||||||
class PlaybackSettings {
|
class PlaybackSettings {
|
||||||
@FormField("Primary Language", FieldForm.DROPDOWN, "", 0)
|
@FormField(R.string.primary_language, FieldForm.DROPDOWN, -1, 0)
|
||||||
@DropdownFieldOptionsId(R.array.languages)
|
@DropdownFieldOptionsId(R.array.audio_languages)
|
||||||
var primaryLanguage: Int = 0;
|
var primaryLanguage: Int = 0;
|
||||||
|
|
||||||
fun getPrimaryLanguage(context: Context) = context.resources.getStringArray(R.array.languages)[primaryLanguage];
|
fun getPrimaryLanguage(context: Context): String? {
|
||||||
|
return when(primaryLanguage) {
|
||||||
|
0 -> "en";
|
||||||
|
1 -> "es";
|
||||||
|
2 -> "de";
|
||||||
|
3 -> "fr";
|
||||||
|
4 -> "ja";
|
||||||
|
5 -> "ko";
|
||||||
|
6 -> "th";
|
||||||
|
7 -> "vi";
|
||||||
|
8 -> "id";
|
||||||
|
9 -> "hi";
|
||||||
|
10 -> "ar";
|
||||||
|
11 -> "tu";
|
||||||
|
12 -> "ru";
|
||||||
|
13 -> "pt";
|
||||||
|
14 -> "zh";
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@FormField("Default Playback Speed", FieldForm.DROPDOWN, "", 1)
|
//= context.resources.getStringArray(R.array.audio_languages)[primaryLanguage];
|
||||||
|
|
||||||
|
@FormField(R.string.default_playback_speed, FieldForm.DROPDOWN, -1, 1)
|
||||||
@DropdownFieldOptionsId(R.array.playback_speeds)
|
@DropdownFieldOptionsId(R.array.playback_speeds)
|
||||||
var defaultPlaybackSpeed: Int = 3;
|
var defaultPlaybackSpeed: Int = 3;
|
||||||
fun getDefaultPlaybackSpeed(): Float = when(defaultPlaybackSpeed) {
|
fun getDefaultPlaybackSpeed(): Float = when(defaultPlaybackSpeed) {
|
||||||
@@ -211,29 +369,29 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
else -> 1.0f;
|
else -> 1.0f;
|
||||||
};
|
};
|
||||||
|
|
||||||
@FormField("Preferred Quality", FieldForm.DROPDOWN, "", 2)
|
@FormField(R.string.preferred_quality, FieldForm.DROPDOWN, R.string.preferred_quality_description, 2)
|
||||||
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
||||||
var preferredQuality: Int = 0;
|
var preferredQuality: Int = 0;
|
||||||
|
|
||||||
@FormField("Preferred Metered Quality", FieldForm.DROPDOWN, "", 2)
|
@FormField(R.string.preferred_metered_quality, FieldForm.DROPDOWN, R.string.preferred_metered_quality_description, 3)
|
||||||
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
||||||
var preferredMeteredQuality: Int = 0;
|
var preferredMeteredQuality: Int = 0;
|
||||||
fun getPreferredQualityPixelCount(): Int = preferedQualityToPixels(preferredQuality);
|
fun getPreferredQualityPixelCount(): Int = preferedQualityToPixels(preferredQuality);
|
||||||
fun getPreferredMeteredQualityPixelCount(): Int = preferedQualityToPixels(preferredMeteredQuality);
|
fun getPreferredMeteredQualityPixelCount(): Int = preferedQualityToPixels(preferredMeteredQuality);
|
||||||
fun getCurrentPreferredQualityPixelCount(): Int = if(!StateApp.instance.isCurrentMetered()) getPreferredQualityPixelCount() else getPreferredMeteredQualityPixelCount();
|
fun getCurrentPreferredQualityPixelCount(): Int = if(!StateApp.instance.isCurrentMetered()) getPreferredQualityPixelCount() else getPreferredMeteredQualityPixelCount();
|
||||||
|
|
||||||
@FormField("Preferred Preview Quality", FieldForm.DROPDOWN, "", 3)
|
@FormField(R.string.preferred_preview_quality, FieldForm.DROPDOWN, R.string.preferred_preview_quality_description, 4)
|
||||||
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
||||||
var preferredPreviewQuality: Int = 5;
|
var preferredPreviewQuality: Int = 5;
|
||||||
fun getPreferredPreviewQualityPixelCount(): Int = preferedQualityToPixels(preferredPreviewQuality);
|
fun getPreferredPreviewQualityPixelCount(): Int = preferedQualityToPixels(preferredPreviewQuality);
|
||||||
|
|
||||||
@FormField("Auto-Rotate", FieldForm.DROPDOWN, "", 4)
|
@FormField(R.string.auto_rotate, FieldForm.DROPDOWN, -1, 5)
|
||||||
@DropdownFieldOptionsId(R.array.system_enabled_disabled_array)
|
@DropdownFieldOptionsId(R.array.system_enabled_disabled_array)
|
||||||
var autoRotate: Int = 2;
|
var autoRotate: Int = 2;
|
||||||
|
|
||||||
fun isAutoRotate() = autoRotate == 1 || (autoRotate == 2 && StateApp.instance.getCurrentSystemAutoRotate());
|
fun isAutoRotate() = autoRotate == 1 || (autoRotate == 2 && StateApp.instance.getCurrentSystemAutoRotate());
|
||||||
|
|
||||||
@FormField("Auto-Rotate Dead Zone", FieldForm.DROPDOWN, "This prevents the device from rotating within the given amount of degrees.", 5)
|
@FormField(R.string.auto_rotate_dead_zone, FieldForm.DROPDOWN, R.string.this_prevents_the_device_from_rotating_within_the_given_amount_of_degrees, 6)
|
||||||
@DropdownFieldOptionsId(R.array.auto_rotate_dead_zone)
|
@DropdownFieldOptionsId(R.array.auto_rotate_dead_zone)
|
||||||
var autoRotateDeadZone: Int = 0;
|
var autoRotateDeadZone: Int = 0;
|
||||||
|
|
||||||
@@ -241,21 +399,17 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
return autoRotateDeadZone * 5;
|
return autoRotateDeadZone * 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField("Background Behavior", FieldForm.DROPDOWN, "", 6)
|
@FormField(R.string.background_behavior, FieldForm.DROPDOWN, -1, 7)
|
||||||
@DropdownFieldOptionsId(R.array.player_background_behavior)
|
@DropdownFieldOptionsId(R.array.player_background_behavior)
|
||||||
var backgroundPlay: Int = 2;
|
var backgroundPlay: Int = 2;
|
||||||
|
|
||||||
fun isBackgroundContinue() = backgroundPlay == 1;
|
fun isBackgroundContinue() = backgroundPlay == 1;
|
||||||
fun isBackgroundPictureInPicture() = backgroundPlay == 2;
|
fun isBackgroundPictureInPicture() = backgroundPlay == 2;
|
||||||
|
|
||||||
@FormField("Resume After Preview", FieldForm.DROPDOWN, "When watching a video in preview mode, resume at the position when opening the video", 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;
|
||||||
|
|
||||||
|
|
||||||
@FormField("Live Chat Webview", FieldForm.TOGGLE, "Use the live chat web window when available over native implementation.", 8)
|
|
||||||
var useLiveChatWindow: Boolean = true;
|
|
||||||
|
|
||||||
fun shouldResumePreview(previewedPosition: Long): Boolean{
|
fun shouldResumePreview(previewedPosition: Long): Boolean{
|
||||||
if(resumeAfterPreview == 2)
|
if(resumeAfterPreview == 2)
|
||||||
return true;
|
return true;
|
||||||
@@ -263,14 +417,59 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
return true;
|
return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@FormField(R.string.chapter_update_fps_title, FieldForm.DROPDOWN, R.string.chapter_update_fps_description, 8)
|
||||||
|
@DropdownFieldOptionsId(R.array.chapter_fps)
|
||||||
|
var chapterUpdateFPS: Int = 0;
|
||||||
|
|
||||||
|
fun getChapterUpdateFrames(): Int {
|
||||||
|
return when(chapterUpdateFPS) {
|
||||||
|
0 -> 24
|
||||||
|
1 -> 30
|
||||||
|
2 -> 60
|
||||||
|
3 -> 120
|
||||||
|
else -> 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@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;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@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)
|
||||||
|
@DropdownFieldOptionsId(R.array.restart_playback_after_loss)
|
||||||
|
var restartPlaybackAfterLoss: Int = 1;
|
||||||
|
|
||||||
|
@FormField(R.string.restart_after_connectivity_loss, FieldForm.DROPDOWN, R.string.restart_playback_when_gaining_connectivity_after_a_loss, 12)
|
||||||
|
@DropdownFieldOptionsId(R.array.restart_playback_after_loss)
|
||||||
|
var restartPlaybackAfterConnectivityLoss: Int = 1;
|
||||||
|
|
||||||
|
@FormField(R.string.full_screen_portrait, FieldForm.TOGGLE, R.string.allow_full_screen_portrait, 13)
|
||||||
|
var fullscreenPortrait: Boolean = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField("Downloads", "group", "Configure downloading of videos", 5)
|
@FormField(R.string.comments, "group", R.string.comments_description, 6)
|
||||||
|
var comments = CommentSettings();
|
||||||
|
@Serializable
|
||||||
|
class CommentSettings {
|
||||||
|
@FormField(R.string.default_comment_section, FieldForm.DROPDOWN, -1, 0)
|
||||||
|
@DropdownFieldOptionsId(R.array.comment_sections)
|
||||||
|
var defaultCommentSection: Int = 0;
|
||||||
|
|
||||||
|
@FormField(R.string.bad_reputation_comments_fading, FieldForm.TOGGLE, R.string.bad_reputation_comments_fading_description, 0)
|
||||||
|
var badReputationCommentsFading: Boolean = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@FormField(R.string.downloads, "group", R.string.configure_downloading_of_videos, 7)
|
||||||
var downloads = Downloads();
|
var downloads = Downloads();
|
||||||
@Serializable
|
@Serializable
|
||||||
class Downloads {
|
class Downloads {
|
||||||
|
|
||||||
@FormField("Download when", FieldForm.DROPDOWN, "Configure when videos should be downloaded", 0)
|
@FormField(R.string.download_when, FieldForm.DROPDOWN, R.string.configure_when_videos_should_be_downloaded, 0)
|
||||||
@DropdownFieldOptionsId(R.array.when_download)
|
@DropdownFieldOptionsId(R.array.when_download)
|
||||||
var whenDownload: Int = 0;
|
var whenDownload: Int = 0;
|
||||||
|
|
||||||
@@ -283,21 +482,21 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField("Default Video Quality", FieldForm.DROPDOWN, "", 2)
|
@FormField(R.string.default_video_quality, FieldForm.DROPDOWN, -1, 2)
|
||||||
@DropdownFieldOptionsId(R.array.preferred_video_download)
|
@DropdownFieldOptionsId(R.array.preferred_video_download)
|
||||||
var preferredVideoQuality: Int = 4;
|
var preferredVideoQuality: Int = 4;
|
||||||
fun getDefaultVideoQualityPixels(): Int = preferedQualityToPixels(preferredVideoQuality);
|
fun getDefaultVideoQualityPixels(): Int = preferedQualityToPixels(preferredVideoQuality);
|
||||||
|
|
||||||
@FormField("Default Audio Quality", FieldForm.DROPDOWN, "", 3)
|
@FormField(R.string.default_audio_quality, FieldForm.DROPDOWN, -1, 3)
|
||||||
@DropdownFieldOptionsId(R.array.preferred_audio_download)
|
@DropdownFieldOptionsId(R.array.preferred_audio_download)
|
||||||
var preferredAudioQuality: Int = 1;
|
var preferredAudioQuality: Int = 1;
|
||||||
fun isHighBitrateDefault(): Boolean = preferredAudioQuality > 0;
|
fun isHighBitrateDefault(): Boolean = preferredAudioQuality > 0;
|
||||||
|
|
||||||
@FormField("ByteRange Download", FieldForm.TOGGLE, "Attempt to utilize byte ranges, this can be combined with concurrency to bypass throttling", 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;
|
||||||
|
|
||||||
@FormField("ByteRange Concurrency", FieldForm.DROPDOWN, "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;
|
||||||
fun getByteRangeThreadCount(): Int {
|
fun getByteRangeThreadCount(): Int {
|
||||||
@@ -305,23 +504,26 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField("Browsing", "group", "Configure browsing behavior", 6)
|
@FormField(R.string.browsing, "group", R.string.configure_browsing_behavior, 8)
|
||||||
var browsing = Browsing();
|
var browsing = Browsing();
|
||||||
@Serializable
|
@Serializable
|
||||||
class Browsing {
|
class Browsing {
|
||||||
@FormField("Enable Video Cache", FieldForm.TOGGLE, "A cache to quickly load previously fetched videos", 0)
|
@FormField(R.string.enable_video_cache, FieldForm.TOGGLE, R.string.cache_to_quickly_load_previously_fetched_videos, 0)
|
||||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||||
var videoCache: Boolean = true;
|
var videoCache: Boolean = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField("Casting", "group", "Configure casting", 7)
|
@FormField(R.string.casting, "group", R.string.configure_casting, 9)
|
||||||
var casting = Casting();
|
var casting = Casting();
|
||||||
@Serializable
|
@Serializable
|
||||||
class Casting {
|
class Casting {
|
||||||
@FormField("Enabled", FieldForm.TOGGLE, "Enable casting", 0)
|
@FormField(R.string.enabled, FieldForm.TOGGLE, R.string.enable_casting, 0)
|
||||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||||
var enabled: Boolean = true;
|
var enabled: Boolean = true;
|
||||||
|
|
||||||
|
@FormField(R.string.keep_screen_on, FieldForm.TOGGLE, R.string.keep_screen_on_while_casting, 1)
|
||||||
|
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||||
|
var keepScreenOn: 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)
|
||||||
@@ -339,25 +541,21 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
}*/
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@FormField(R.string.logging, FieldForm.GROUP, -1, 10)
|
||||||
@FormField("Logging", FieldForm.GROUP, "", 8)
|
|
||||||
var logging = Logging();
|
var logging = Logging();
|
||||||
@Serializable
|
@Serializable
|
||||||
class Logging {
|
class Logging {
|
||||||
@FormField("Log Level", FieldForm.DROPDOWN, "", 0)
|
@FormField(R.string.log_level, FieldForm.DROPDOWN, -1, 0)
|
||||||
@DropdownFieldOptionsId(R.array.log_levels)
|
@DropdownFieldOptionsId(R.array.log_levels)
|
||||||
var logLevel: Int = 0;
|
var logLevel: Int = 0;
|
||||||
|
|
||||||
@FormField(
|
@FormField(R.string.submit_logs, FieldForm.BUTTON, R.string.submit_logs_to_help_us_narrow_down_issues, 1)
|
||||||
"Submit logs", FieldForm.BUTTON,
|
|
||||||
"Submit logs to help us narrow down issues", 1
|
|
||||||
)
|
|
||||||
fun submitLogs() {
|
fun submitLogs() {
|
||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
if (!Logger.submitLogs()) {
|
if (!Logger.submitLogs()) {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
SettingsActivity.getActivity()?.let { UIDialogs.toast(it, "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) {
|
||||||
@@ -367,43 +565,40 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@FormField(R.string.announcement, FieldForm.GROUP, -1, 11)
|
||||||
|
|
||||||
@FormField("Announcement", FieldForm.GROUP, "", 10)
|
|
||||||
var announcementSettings = AnnouncementSettings();
|
var announcementSettings = AnnouncementSettings();
|
||||||
@Serializable
|
@Serializable
|
||||||
class AnnouncementSettings {
|
class AnnouncementSettings {
|
||||||
@FormField(
|
@FormField(R.string.reset_announcements, FieldForm.BUTTON, R.string.reset_hidden_announcements, 1)
|
||||||
"Reset announcements", FieldForm.BUTTON,
|
|
||||||
"Reset hidden announcements", 1
|
|
||||||
)
|
|
||||||
fun resetAnnouncements() {
|
fun resetAnnouncements() {
|
||||||
StateAnnouncement.instance.resetAnnouncements();
|
StateAnnouncement.instance.resetAnnouncements();
|
||||||
UIDialogs.toast("Announcements reset.");
|
SettingsActivity.getActivity()?.let { UIDialogs.toast(it, it.getString(R.string.announcements_reset)); };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField("Plugins", FieldForm.GROUP, "", 11)
|
@FormField(R.string.notifications, FieldForm.GROUP, -1, 12)
|
||||||
|
var notifications = NotificationSettings();
|
||||||
|
@Serializable
|
||||||
|
class NotificationSettings {
|
||||||
|
@FormField(R.string.planned_content_notifications, FieldForm.TOGGLE, R.string.planned_content_notifications_description, 1)
|
||||||
|
var plannedContentNotification: Boolean = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@FormField(R.string.plugins, FieldForm.GROUP, -1, 13)
|
||||||
@Transient
|
@Transient
|
||||||
var plugins = Plugins();
|
var plugins = Plugins();
|
||||||
@Serializable
|
@Serializable
|
||||||
class Plugins {
|
class Plugins {
|
||||||
|
|
||||||
@FormField("Clear Cookies on Logout", FieldForm.TOGGLE, "Clears cookies when you log out, allowing you to change account.", 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;
|
||||||
|
|
||||||
@FormField(
|
@FormField(R.string.clear_cookies, FieldForm.BUTTON, R.string.clears_in_app_browser_cookies, 1)
|
||||||
"Clear Cookies", FieldForm.BUTTON,
|
|
||||||
"Clears in-app browser cookies, especially useful for fully logging out of plugins.", 1
|
|
||||||
)
|
|
||||||
fun clearCookies() {
|
fun clearCookies() {
|
||||||
val cookieManager: CookieManager = CookieManager.getInstance();
|
val cookieManager: CookieManager = CookieManager.getInstance();
|
||||||
cookieManager.removeAllCookies(null);
|
cookieManager.removeAllCookies(null);
|
||||||
}
|
}
|
||||||
@FormField(
|
/*@FormField(R.string.reinstall_embedded_plugins, FieldForm.BUTTON, R.string.also_removes_any_data_related_plugin_like_login_or_settings, 1)
|
||||||
"Reinstall Embedded Plugins", FieldForm.BUTTON,
|
|
||||||
"Also removes any data related plugin like login or settings (may not clear browser cache)", 1
|
|
||||||
)
|
|
||||||
fun reinstallEmbedded() {
|
fun reinstallEmbedded() {
|
||||||
StateApp.instance.scopeOrNull!!.launch(Dispatchers.IO) {
|
StateApp.instance.scopeOrNull!!.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
@@ -411,7 +606,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
StateApp.instance.contextOrNull?.let {
|
StateApp.instance.contextOrNull?.let {
|
||||||
UIDialogs.toast(it, "Embedded plugins reinstalled, a reboot is recommended");
|
UIDialogs.toast(it, it.getString(R.string.embedded_plugins_reinstalled_a_reboot_is_recommended));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
@@ -422,11 +617,11 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@FormField("External Storage", FieldForm.GROUP, "", 12)
|
@FormField(R.string.external_storage, FieldForm.GROUP, -1, 14)
|
||||||
var storage = Storage();
|
var storage = Storage();
|
||||||
@Serializable
|
@Serializable
|
||||||
class Storage {
|
class Storage {
|
||||||
@@ -438,34 +633,41 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
fun isStorageMainValid(context: Context): Boolean = StateApp.instance.isValidStorageUri(context, getStorageGeneralUri());
|
fun isStorageMainValid(context: Context): Boolean = StateApp.instance.isValidStorageUri(context, getStorageGeneralUri());
|
||||||
fun isStorageDownloadValid(context: Context): Boolean = StateApp.instance.isValidStorageUri(context, getStorageDownloadUri());
|
fun isStorageDownloadValid(context: Context): Boolean = StateApp.instance.isValidStorageUri(context, getStorageDownloadUri());
|
||||||
|
|
||||||
@FormField("Change external General directory", FieldForm.BUTTON, "Change the external directory for general files, used for persistent files like auto-backup", 3)
|
@FormField(R.string.change_external_general_directory, FieldForm.BUTTON, R.string.change_the_external_directory_for_general_files, 3)
|
||||||
fun changeStorageGeneral() {
|
fun changeStorageGeneral() {
|
||||||
SettingsActivity.getActivity()?.let {
|
SettingsActivity.getActivity()?.let {
|
||||||
StateApp.instance.changeExternalGeneralDirectory(it);
|
StateApp.instance.changeExternalGeneralDirectory(it);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@FormField("Change external Downloads directory", FieldForm.BUTTON, "Change the external storage for download files, used for exported 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() {
|
||||||
SettingsActivity.getActivity()?.let {
|
SettingsActivity.getActivity()?.let {
|
||||||
StateApp.instance.changeExternalDownloadDirectory(it);
|
StateApp.instance.changeExternalDownloadDirectory(it);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@FormField(R.string.clear_external_downloads_directory, FieldForm.BUTTON, R.string.clear_the_external_storage_for_download_files, 5)
|
||||||
|
fun clearStorageDownload() {
|
||||||
|
Settings.instance.storage.storage_download = null;
|
||||||
|
Settings.instance.save();
|
||||||
|
SettingsActivity.getActivity()?.let { UIDialogs.toast(it, "Cleared download storage directory") };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@FormField("Auto Update", "group", "Configure the auto updater", 12)
|
@FormField(R.string.auto_update, "group", R.string.configure_the_auto_updater, 15)
|
||||||
var autoUpdate = AutoUpdate();
|
var autoUpdate = AutoUpdate();
|
||||||
@Serializable
|
@Serializable
|
||||||
class AutoUpdate {
|
class AutoUpdate {
|
||||||
@FormField("Check", FieldForm.DROPDOWN, "", 0)
|
@FormField(R.string.check, FieldForm.DROPDOWN, -1, 0)
|
||||||
@DropdownFieldOptionsId(R.array.auto_update_when_array)
|
@DropdownFieldOptionsId(R.array.auto_update_when_array)
|
||||||
var check: Int = 0;
|
var check: Int = 0;
|
||||||
|
|
||||||
@FormField("Background download", FieldForm.DROPDOWN, "Configure if background download should be used", 1)
|
@FormField(R.string.background_download, FieldForm.DROPDOWN, R.string.configure_if_background_download_should_be_used, 1)
|
||||||
@DropdownFieldOptionsId(R.array.background_download)
|
@DropdownFieldOptionsId(R.array.background_download)
|
||||||
var backgroundDownload: Int = 0;
|
var backgroundDownload: Int = 0;
|
||||||
|
|
||||||
@FormField("Download when", FieldForm.DROPDOWN, "Configure when updates should be downloaded", 2)
|
@FormField(R.string.download_when, FieldForm.DROPDOWN, R.string.configure_when_updates_should_be_downloaded, 2)
|
||||||
@DropdownFieldOptionsId(R.array.when_download)
|
@DropdownFieldOptionsId(R.array.when_download)
|
||||||
var whenDownload: Int = 0;
|
var whenDownload: Int = 0;
|
||||||
|
|
||||||
@@ -482,33 +684,30 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
return check == 0 && !BuildConfig.IS_PLAYSTORE_BUILD;
|
return check == 0 && !BuildConfig.IS_PLAYSTORE_BUILD;
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(
|
@FormField(R.string.manual_check, FieldForm.BUTTON, R.string.manually_check_for_updates, 3)
|
||||||
"Manual check", FieldForm.BUTTON,
|
|
||||||
"Manually check for updates", 3
|
|
||||||
)
|
|
||||||
fun manualCheck() {
|
fun manualCheck() {
|
||||||
if (!BuildConfig.IS_PLAYSTORE_BUILD) {
|
if (!BuildConfig.IS_PLAYSTORE_BUILD) {
|
||||||
SettingsActivity.getActivity()?.let {
|
SettingsActivity.getActivity()?.let {
|
||||||
StateUpdate.instance.checkForUpdates(it, true);
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
|
StateUpdate.instance.checkForUpdates(it, true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
SettingsActivity.getActivity()?.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) {
|
||||||
UIDialogs.toast(it, "Failed to show store.");
|
UIDialogs.toast(it, it.getString(R.string.failed_to_show_store));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(
|
@FormField(R.string.view_changelog, FieldForm.BUTTON, R.string.review_the_current_and_past_changelogs, 4)
|
||||||
"View changelog", FieldForm.BUTTON,
|
|
||||||
"Review the current and past changelogs", 4
|
|
||||||
)
|
|
||||||
fun viewChangelog() {
|
fun viewChangelog() {
|
||||||
UIDialogs.toast("Retrieving changelog");
|
|
||||||
SettingsActivity.getActivity()?.let {
|
SettingsActivity.getActivity()?.let {
|
||||||
|
UIDialogs.toast(it.getString(R.string.retrieving_changelog));
|
||||||
|
|
||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val version = StateUpdate.instance.downloadVersionCode(ManagedHttpClient()) ?: return@launch;
|
val version = StateUpdate.instance.downloadVersionCode(ManagedHttpClient()) ?: return@launch;
|
||||||
@@ -524,10 +723,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(
|
@FormField(R.string.remove_cached_version, FieldForm.BUTTON, R.string.remove_the_last_downloaded_version, 5)
|
||||||
"Remove Cached Version", FieldForm.BUTTON,
|
|
||||||
"Remove the last downloaded version", 5
|
|
||||||
)
|
|
||||||
fun removeCachedVersion() {
|
fun removeCachedVersion() {
|
||||||
StateApp.withContext {
|
StateApp.withContext {
|
||||||
val outputDirectory = File(it.filesDir, "autoupdate");
|
val outputDirectory = File(it.filesDir, "autoupdate");
|
||||||
@@ -543,7 +739,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField("Backup", FieldForm.GROUP, "", 13)
|
@FormField(R.string.backup, FieldForm.GROUP, -1, 16)
|
||||||
var backup = Backup();
|
var backup = Backup();
|
||||||
@Serializable
|
@Serializable
|
||||||
class Backup {
|
class Backup {
|
||||||
@@ -553,58 +749,107 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
var autoBackupPassword: String? = null;
|
var autoBackupPassword: String? = null;
|
||||||
fun shouldAutomaticBackup() = autoBackupPassword != null;
|
fun shouldAutomaticBackup() = autoBackupPassword != null;
|
||||||
|
|
||||||
@FormField("Automatic Backup", FieldForm.READONLYTEXT, "", 0)
|
@FormField(R.string.automatic_backup, FieldForm.READONLYTEXT, -1, 0)
|
||||||
val automaticBackupText get() = if(!shouldAutomaticBackup()) "None" else "Every Day";
|
val automaticBackupText get() = if(!shouldAutomaticBackup()) "None" else "Every Day";
|
||||||
|
|
||||||
@FormField("Set Automatic Backup", FieldForm.BUTTON, "Configure daily backup in case of catastrophic failure. (Written to the external Grayjay directory)", 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(SettingsActivity.getActivity()!!, autoBackupPassword != null) {
|
UIDialogs.showAutomaticBackupDialog(SettingsActivity.getActivity()!!, autoBackupPassword != null) {
|
||||||
SettingsActivity.getActivity()?.reloadSettings();
|
SettingsActivity.getActivity()?.reloadSettings();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@FormField("Restore Automatic Backup", FieldForm.BUTTON, "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 = SettingsActivity.getActivity()!!
|
val activity = SettingsActivity.getActivity()!!
|
||||||
|
|
||||||
if(!StateBackup.hasAutomaticBackup())
|
if(!StateBackup.hasAutomaticBackup())
|
||||||
UIDialogs.toast(activity, "You don't have any automatic backups", false);
|
UIDialogs.toast(activity, activity.getString(R.string.you_don_t_have_any_automatic_backups), false);
|
||||||
else
|
else
|
||||||
UIDialogs.showAutomaticRestoreDialog(activity, activity.lifecycleScope);
|
UIDialogs.showAutomaticRestoreDialog(activity, activity.lifecycleScope);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@FormField("Export Data", FieldForm.BUTTON, "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() {
|
||||||
StateBackup.startExternalBackup();
|
val activity = SettingsActivity.getActivity() ?: return;
|
||||||
|
UISlideOverlays.showOverlay(activity.overlay, "Select export type", null, {},
|
||||||
|
SlideUpMenuItem(activity, R.drawable.ic_share, "Share", "", null, {
|
||||||
|
StateBackup.shareExternalBackup();
|
||||||
|
}),
|
||||||
|
SlideUpMenuItem(activity, R.drawable.ic_download, "File", "", null, {
|
||||||
|
StateBackup.saveExternalBackup(activity);
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField("Payment", FieldForm.GROUP, "", 14)
|
@FormField(R.string.payment, FieldForm.GROUP, -1, 17)
|
||||||
var payment = Payment();
|
var payment = Payment();
|
||||||
@Serializable
|
@Serializable
|
||||||
class Payment {
|
class Payment {
|
||||||
@FormField("Payment Status", FieldForm.READONLYTEXT, "", 1)
|
@FormField(R.string.payment_status, FieldForm.READONLYTEXT, -1, 1)
|
||||||
val paymentStatus: String get() = if (StatePayment.instance.hasPaid) "Paid" else "Not Paid";
|
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("Clear Payment", FieldForm.BUTTON, "Deletes license keys from app", 2)
|
@FormField(R.string.clear_payment, FieldForm.BUTTON, R.string.deletes_license_keys_from_app, 2)
|
||||||
fun clearPayment() {
|
fun clearPayment() {
|
||||||
StatePayment.instance.clearLicenses();
|
StatePayment.instance.clearLicenses();
|
||||||
SettingsActivity.getActivity()?.let {
|
SettingsActivity.getActivity()?.let {
|
||||||
UIDialogs.toast(it, "Licenses cleared, might require app restart");
|
UIDialogs.toast(it, it.getString(R.string.licenses_cleared_might_require_app_restart));
|
||||||
it.reloadSettings();
|
it.reloadSettings();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField("Info", FieldForm.GROUP, "", 15)
|
@FormField(R.string.other, FieldForm.GROUP, -1, 18)
|
||||||
|
var other = Other();
|
||||||
|
@Serializable
|
||||||
|
class Other {
|
||||||
|
@FormField(R.string.bypass_rotation_prevention, FieldForm.TOGGLE, R.string.bypass_rotation_prevention_description, 1)
|
||||||
|
@FormFieldWarning(R.string.bypass_rotation_prevention_warning)
|
||||||
|
var bypassRotationPrevention: Boolean = false;
|
||||||
|
|
||||||
|
@FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 1)
|
||||||
|
var polycentricEnabled: Boolean = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@FormField(R.string.gesture_controls, FieldForm.GROUP, -1, 19)
|
||||||
|
var gestureControls = GestureControls();
|
||||||
|
@Serializable
|
||||||
|
class GestureControls {
|
||||||
|
@FormField(R.string.volume_slider, FieldForm.TOGGLE, R.string.volume_slider_descr, 1)
|
||||||
|
var volumeSlider: Boolean = true;
|
||||||
|
|
||||||
|
@FormField(R.string.brightness_slider, FieldForm.TOGGLE, R.string.brightness_slider_descr, 2)
|
||||||
|
var brightnessSlider: Boolean = true;
|
||||||
|
|
||||||
|
@FormField(R.string.toggle_full_screen, FieldForm.TOGGLE, R.string.toggle_full_screen_descr, 3)
|
||||||
|
var toggleFullscreen: Boolean = true;
|
||||||
|
|
||||||
|
@FormField(R.string.system_brightness, FieldForm.TOGGLE, R.string.system_brightness_descr, 4)
|
||||||
|
var useSystemBrightness: Boolean = false;
|
||||||
|
|
||||||
|
@FormField(R.string.system_volume, FieldForm.TOGGLE, R.string.system_volume_descr, 5)
|
||||||
|
var useSystemVolume: Boolean = true;
|
||||||
|
|
||||||
|
@FormField(R.string.restore_system_brightness, FieldForm.TOGGLE, R.string.restore_system_brightness_descr, 6)
|
||||||
|
var restoreSystemBrightness: Boolean = true;
|
||||||
|
|
||||||
|
@FormField(R.string.zoom_option, FieldForm.TOGGLE, R.string.zoom_option_descr, 7)
|
||||||
|
var zoom: Boolean = true;
|
||||||
|
|
||||||
|
@FormField(R.string.pan_option, FieldForm.TOGGLE, R.string.pan_option_descr, 8)
|
||||||
|
var pan: Boolean = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@FormField(R.string.info, FieldForm.GROUP, -1, 20)
|
||||||
var info = Info();
|
var info = Info();
|
||||||
@Serializable
|
@Serializable
|
||||||
class Info {
|
class Info {
|
||||||
@FormField("Version Code", FieldForm.READONLYTEXT, "", 1, "code")
|
@FormField(R.string.version_code, FieldForm.READONLYTEXT, -1, 1, "code")
|
||||||
var versionCode = BuildConfig.VERSION_CODE;
|
var versionCode = BuildConfig.VERSION_CODE;
|
||||||
@FormField("Version Name", FieldForm.READONLYTEXT, "", 2)
|
@FormField(R.string.version_name, FieldForm.READONLYTEXT, -1, 2)
|
||||||
var versionName = BuildConfig.VERSION_NAME;
|
var versionName = BuildConfig.VERSION_NAME;
|
||||||
@FormField("Version Type", FieldForm.READONLYTEXT, "", 3)
|
@FormField(R.string.version_type, FieldForm.READONLYTEXT, -1, 3)
|
||||||
var versionType = BuildConfig.BUILD_TYPE;
|
var versionType = BuildConfig.BUILD_TYPE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -615,6 +860,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "Settings";
|
private const val TAG = "Settings";
|
||||||
|
const val URL_FAQ = "https://grayjay.app/faq.html";
|
||||||
|
|
||||||
private var _isFirst = true;
|
private var _isFirst = true;
|
||||||
|
|
||||||
|
|||||||
@@ -2,53 +2,69 @@ package com.futo.platformplayer
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.webkit.CookieManager
|
import android.webkit.CookieManager
|
||||||
|
import androidx.work.Data
|
||||||
|
import androidx.work.OneTimeWorkRequestBuilder
|
||||||
|
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.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.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.platforms.js.SourcePluginDescriptor
|
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.engine.V8Plugin
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
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
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.futo.platformplayer.states.StateCache
|
||||||
import com.futo.platformplayer.states.StateDeveloper
|
import com.futo.platformplayer.states.StateDeveloper
|
||||||
import com.futo.platformplayer.states.StateDownloads
|
import com.futo.platformplayer.states.StateDownloads
|
||||||
|
import com.futo.platformplayer.states.StateHistory
|
||||||
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
|
import com.futo.platformplayer.states.StateSubscriptions
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
import com.futo.platformplayer.stores.FragmentedStorageFileJson
|
import com.futo.platformplayer.stores.FragmentedStorageFileJson
|
||||||
|
import com.futo.platformplayer.views.fields.ButtonField
|
||||||
import com.futo.platformplayer.views.fields.FieldForm
|
import com.futo.platformplayer.views.fields.FieldForm
|
||||||
import com.futo.platformplayer.views.fields.FormField
|
import com.futo.platformplayer.views.fields.FormField
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.*
|
import kotlinx.serialization.Contextual
|
||||||
import kotlinx.serialization.json.*
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.Transient
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import java.time.OffsetDateTime
|
||||||
import java.util.stream.IntStream.range
|
import java.util.stream.IntStream.range
|
||||||
import kotlin.system.measureTimeMillis
|
import kotlin.system.measureTimeMillis
|
||||||
|
|
||||||
@Serializable()
|
@Serializable()
|
||||||
class SettingsDev : FragmentedStorageFileJson() {
|
class SettingsDev : FragmentedStorageFileJson() {
|
||||||
|
|
||||||
@FormField("Developer Mode", FieldForm.TOGGLE, "", 0)
|
@FormField(R.string.developer_mode, FieldForm.TOGGLE, -1, 0)
|
||||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||||
var developerMode: Boolean = false;
|
var developerMode: Boolean = false;
|
||||||
|
|
||||||
@FormField("Development Server", FieldForm.GROUP,
|
@FormField(R.string.development_server, FieldForm.GROUP,
|
||||||
"Settings related to development server, be careful as it may open your phone to security vulnerabilities", 1)
|
R.string.settings_related_to_development_server_be_careful_as_it_may_open_your_phone_to_security_vulnerabilities, 1)
|
||||||
val devServerSettings: DeveloperServerFields = DeveloperServerFields();
|
val devServerSettings: DeveloperServerFields = DeveloperServerFields();
|
||||||
@Serializable
|
@Serializable
|
||||||
class DeveloperServerFields {
|
class DeveloperServerFields {
|
||||||
|
|
||||||
@FormField("Start Server on boot", FieldForm.TOGGLE, "", 0)
|
@FormField(R.string.start_server_on_boot, FieldForm.TOGGLE, -1, 0)
|
||||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||||
var devServerOnBoot: Boolean = false;
|
var devServerOnBoot: Boolean = false;
|
||||||
|
|
||||||
@FormField("Start Server", FieldForm.BUTTON,
|
@FormField(R.string.start_server, FieldForm.BUTTON,
|
||||||
"Starts a DevServer on port 11337, may expose vulnerabilities.", 1)
|
R.string.starts_a_devServer_on_port_11337_may_expose_vulnerabilities, 1)
|
||||||
fun startServer() {
|
fun startServer() {
|
||||||
StateDeveloper.instance.runServer();
|
StateDeveloper.instance.runServer();
|
||||||
StateApp.instance.contextOrNull?.let {
|
StateApp.instance.contextOrNull?.let {
|
||||||
@@ -57,45 +73,192 @@ class SettingsDev : FragmentedStorageFileJson() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField("Experimental", FieldForm.GROUP,
|
@FormField(R.string.experimental, FieldForm.GROUP,
|
||||||
"Settings related to development server, be careful as it may open your phone to security vulnerabilities", 2)
|
R.string.settings_related_to_development_server_be_careful_as_it_may_open_your_phone_to_security_vulnerabilities, 2)
|
||||||
val experimentalSettings: ExperimentalFields = ExperimentalFields();
|
val experimentalSettings: ExperimentalFields = ExperimentalFields();
|
||||||
@Serializable
|
@Serializable
|
||||||
class ExperimentalFields {
|
class ExperimentalFields {
|
||||||
|
|
||||||
@FormField("Background Subscription Testing", FieldForm.TOGGLE, "", 0)
|
@FormField(R.string.background_subscription_testing, FieldForm.TOGGLE, -1, 0)
|
||||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||||
var backgroundSubscriptionFetching: Boolean = false;
|
var backgroundSubscriptionFetching: Boolean = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField("Crash Me", FieldForm.BUTTON,
|
|
||||||
"Crashes the application on purpose", 2)
|
@FormField(R.string.cache, FieldForm.GROUP, -1, 3)
|
||||||
|
val cache: Cache = Cache();
|
||||||
|
@Serializable
|
||||||
|
class Cache {
|
||||||
|
|
||||||
|
@FormField(R.string.subscriptions_cache_5000, FieldForm.BUTTON, -1, 1, "subscription_cache_button")
|
||||||
|
fun subscriptionsCache5000() {
|
||||||
|
Logger.i("SettingsDev", "Started caching 5000 sub items");
|
||||||
|
UIDialogs.toast(
|
||||||
|
SettingsActivity.getActivity()!!,
|
||||||
|
"Started caching 5000 sub items"
|
||||||
|
);
|
||||||
|
val button = DeveloperActivity.getActivity()?.getField("subscription_cache_button");
|
||||||
|
if(button is ButtonField)
|
||||||
|
button.setButtonEnabled(false);
|
||||||
|
StateApp.instance.scope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val subsCache =
|
||||||
|
StateSubscriptions.instance.getSubscriptionsFeedWithExceptions(cacheScope = this).first;
|
||||||
|
|
||||||
|
var total = 0;
|
||||||
|
var page = 0;
|
||||||
|
var lastToast = System.currentTimeMillis();
|
||||||
|
while(subsCache.hasMorePages() && total < 5000) {
|
||||||
|
subsCache.nextPage();
|
||||||
|
total += subsCache.getResults().size;
|
||||||
|
page++;
|
||||||
|
|
||||||
|
if(page % 10 == 0)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
val diff = System.currentTimeMillis() - lastToast;
|
||||||
|
lastToast = System.currentTimeMillis();
|
||||||
|
UIDialogs.toast(
|
||||||
|
SettingsActivity.getActivity()!!,
|
||||||
|
"Page: ${page}, Total: ${total}, Speed: ${diff}ms"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Thread.sleep(250);
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
UIDialogs.toast(
|
||||||
|
SettingsActivity.getActivity()!!,
|
||||||
|
"FINISHED Page: ${page}, Total: ${total}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e("SettingsDev", ex.message, ex);
|
||||||
|
Logger.i("SettingsDev", "Failed: ${ex.message}");
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
if(button is ButtonField)
|
||||||
|
button.setButtonEnabled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@FormField(R.string.history_cache_100, FieldForm.BUTTON, -1, 1, "history_cache_button")
|
||||||
|
fun historyCache100() {
|
||||||
|
Logger.i("SettingsDev", "Started caching 100 history items (from home)");
|
||||||
|
UIDialogs.toast(
|
||||||
|
SettingsActivity.getActivity()!!,
|
||||||
|
"Started caching 100 history items (from home)"
|
||||||
|
);
|
||||||
|
val button = DeveloperActivity.getActivity()?.getField("history_cache_button");
|
||||||
|
if(button is ButtonField)
|
||||||
|
button.setButtonEnabled(false);
|
||||||
|
StateApp.instance.scope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val subsCache = StatePlatform.instance.getHome();
|
||||||
|
|
||||||
|
var num = 0;
|
||||||
|
for(item in subsCache.getResults().filterIsInstance<IPlatformVideo>()) {
|
||||||
|
StateHistory.instance.getHistoryByVideo(item, true, OffsetDateTime.now().minusHours(num.toLong() * 4))
|
||||||
|
num++;
|
||||||
|
}
|
||||||
|
|
||||||
|
var total = 0;
|
||||||
|
var page = 0;
|
||||||
|
var lastToast = System.currentTimeMillis();
|
||||||
|
while(subsCache.hasMorePages() && total < 5000) {
|
||||||
|
subsCache.nextPage();
|
||||||
|
total += subsCache.getResults().size;
|
||||||
|
page++;
|
||||||
|
|
||||||
|
for(item in subsCache.getResults().filterIsInstance<IPlatformVideo>()) {
|
||||||
|
StateHistory.instance.getHistoryByVideo(item, true, OffsetDateTime.now().minusHours(num.toLong() * 4))
|
||||||
|
num++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(page % 4 == 0)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
val diff = System.currentTimeMillis() - lastToast;
|
||||||
|
lastToast = System.currentTimeMillis();
|
||||||
|
UIDialogs.toast(
|
||||||
|
SettingsActivity.getActivity()!!,
|
||||||
|
"Page: ${page}, Total: ${total}, Speed: ${diff}ms"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Thread.sleep(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
UIDialogs.toast(
|
||||||
|
SettingsActivity.getActivity()!!,
|
||||||
|
"FINISHED Page: ${page}, Total: ${total}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e("SettingsDev", ex.message, ex);
|
||||||
|
Logger.i("SettingsDev", "Failed: ${ex.message}");
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
if(button is ButtonField)
|
||||||
|
button.setButtonEnabled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@FormField(R.string.crash_me, FieldForm.BUTTON,
|
||||||
|
R.string.crashes_the_application_on_purpose, 3)
|
||||||
fun crashMe() {
|
fun crashMe() {
|
||||||
throw java.lang.IllegalStateException("This is an uncaught exception triggered on purpose!");
|
throw java.lang.IllegalStateException("This is an uncaught exception triggered on purpose!");
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField("Delete Announcements", FieldForm.BUTTON,
|
@FormField(R.string.delete_announcements, FieldForm.BUTTON,
|
||||||
"Delete all announcements", 2)
|
R.string.delete_all_announcements, 3)
|
||||||
fun deleteAnnouncements() {
|
fun deleteAnnouncements() {
|
||||||
StateAnnouncement.instance.deleteAllAnnouncements();
|
StateAnnouncement.instance.deleteAllAnnouncements();
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField("Clear Cookies", FieldForm.BUTTON,
|
@FormField(R.string.clear_cookies, FieldForm.BUTTON,
|
||||||
"Clear all cook from the CookieManager", 2)
|
R.string.clear_all_cookies_from_the_cookieManager, 3)
|
||||||
fun clearCookies() {
|
fun clearCookies() {
|
||||||
val cookieManager: CookieManager = CookieManager.getInstance()
|
val cookieManager: CookieManager = CookieManager.getInstance()
|
||||||
cookieManager.removeAllCookies(null);
|
cookieManager.removeAllCookies(null);
|
||||||
}
|
}
|
||||||
|
@FormField(R.string.test_background_worker, FieldForm.BUTTON,
|
||||||
|
R.string.test_background_worker_description, 4)
|
||||||
|
fun triggerBackgroundUpdate() {
|
||||||
|
val act = SettingsActivity.getActivity()!!;
|
||||||
|
UIDialogs.toast(SettingsActivity.getActivity()!!, "Starting test background worker");
|
||||||
|
|
||||||
|
val wm = WorkManager.getInstance(act);
|
||||||
|
val req = OneTimeWorkRequestBuilder<BackgroundWorker>()
|
||||||
|
.setInputData(Data.Builder().putBoolean("bypassMainCheck", true).build())
|
||||||
|
.build();
|
||||||
|
wm.enqueue(req);
|
||||||
|
}
|
||||||
|
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON,
|
||||||
|
R.string.test_background_worker_description, 4)
|
||||||
|
fun clearChannelContentCache() {
|
||||||
|
UIDialogs.toast(SettingsActivity.getActivity()!!, "Clearing cache");
|
||||||
|
StateCache.instance.clearToday();
|
||||||
|
UIDialogs.toast(SettingsActivity.getActivity()!!, "Cleared");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Contextual
|
@Contextual
|
||||||
@Transient
|
@Transient
|
||||||
@FormField("V8 Benchmarks", FieldForm.GROUP,
|
@FormField(R.string.v8_benchmarks, FieldForm.GROUP,
|
||||||
"Various benchmarks using the integrated V8 engine", 3)
|
R.string.various_benchmarks_using_the_integrated_v8_engine, 4)
|
||||||
val v8Benchmarks: V8Benchmarks = V8Benchmarks();
|
val v8Benchmarks: V8Benchmarks = V8Benchmarks();
|
||||||
class V8Benchmarks {
|
class V8Benchmarks {
|
||||||
@FormField(
|
@FormField(
|
||||||
"Test V8 Creation speed", FieldForm.BUTTON,
|
R.string.test_v8_creation_speed, FieldForm.BUTTON,
|
||||||
"Tests V8 creation times and running", 1
|
R.string.tests_v8_creation_times_and_running, 1
|
||||||
)
|
)
|
||||||
fun testV8Creation() {
|
fun testV8Creation() {
|
||||||
var plugin: V8Plugin? = null;
|
var plugin: V8Plugin? = null;
|
||||||
@@ -137,8 +300,8 @@ class SettingsDev : FragmentedStorageFileJson() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@FormField(
|
@FormField(
|
||||||
"Test V8 Communication speed", FieldForm.BUTTON,
|
R.string.test_v8_communication_speed, FieldForm.BUTTON,
|
||||||
"Tests V8 communication speeds", 2
|
R.string.tests_v8_communication_speeds, 4
|
||||||
)
|
)
|
||||||
fun testV8RunSpeeds() {
|
fun testV8RunSpeeds() {
|
||||||
var plugin: V8Plugin? = null;
|
var plugin: V8Plugin? = null;
|
||||||
@@ -182,12 +345,12 @@ class SettingsDev : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
@Contextual
|
@Contextual
|
||||||
@Transient
|
@Transient
|
||||||
@FormField("V8 Script Testing", FieldForm.GROUP, "Various tests against a custom source", 4)
|
@FormField(R.string.v8_script_testing, FieldForm.GROUP, R.string.various_tests_against_a_custom_source, 4)
|
||||||
val v8ScriptTests: V8ScriptTests = V8ScriptTests();
|
val v8ScriptTests: V8ScriptTests = V8ScriptTests();
|
||||||
class V8ScriptTests {
|
class V8ScriptTests {
|
||||||
@Contextual
|
@Contextual
|
||||||
private var _currentPlugin : JSClient? = null;
|
private var _currentPlugin : JSClient? = null;
|
||||||
@FormField("Inject", FieldForm.BUTTON, "Injects a test source config (local) into V8", 1)
|
@FormField(R.string.inject, FieldForm.BUTTON, R.string.injects_a_test_source_config_local_into_v8, 1)
|
||||||
fun testV8Init() {
|
fun testV8Init() {
|
||||||
StateApp.instance.scope.launch(Dispatchers.IO) {
|
StateApp.instance.scope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
@@ -203,12 +366,12 @@ class SettingsDev : FragmentedStorageFileJson() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@FormField("getHome", FieldForm.BUTTON, "Attempts to fetch 2 pages from getHome", 2)
|
@FormField(R.string.getHome, FieldForm.BUTTON, R.string.attempts_to_fetch_2_pages_from_getHome, 2)
|
||||||
fun testV8Home() {
|
fun testV8Home() {
|
||||||
runTestPlugin(_currentPlugin) {
|
runTestPlugin(_currentPlugin) {
|
||||||
var home: IPager<IPlatformContent>? = null;
|
var home: IPager<IPlatformContent>?;
|
||||||
var resultPage1: String = "";
|
val resultPage1: String;
|
||||||
var resultPage2: String = "";
|
val resultPage2: String;
|
||||||
val page1Time = measureTimeMillis {
|
val page1Time = measureTimeMillis {
|
||||||
home = it.getHome();
|
home = it.getHome();
|
||||||
val results = home!!.getResults();
|
val results = home!!.getResults();
|
||||||
@@ -269,27 +432,36 @@ class SettingsDev : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
@Contextual
|
@Contextual
|
||||||
@Transient
|
@Transient
|
||||||
@FormField("Other", FieldForm.GROUP, "Others...", 5)
|
@FormField(R.string.other, FieldForm.GROUP, R.string.others_ellipsis, 5)
|
||||||
val otherTests: OtherTests = OtherTests();
|
val otherTests: OtherTests = OtherTests();
|
||||||
class OtherTests {
|
class OtherTests {
|
||||||
@FormField("Clear Downloads", FieldForm.BUTTON, "Deletes all ongoing downloads", 1)
|
@FormField(R.string.unsubscribe_all, FieldForm.BUTTON, R.string.removes_all_subscriptions, -1)
|
||||||
|
fun unsubscribeAll() {
|
||||||
|
val toUnsub = StateSubscriptions.instance.getSubscriptions();
|
||||||
|
UIDialogs.toast("Started unsubbing.. (${toUnsub.size})")
|
||||||
|
toUnsub.forEach {
|
||||||
|
StateSubscriptions.instance.removeSubscription(it.channel.url);
|
||||||
|
};
|
||||||
|
UIDialogs.toast("Finished unsubbing.. (${toUnsub.size})")
|
||||||
|
}
|
||||||
|
@FormField(R.string.clear_downloads, FieldForm.BUTTON, R.string.deletes_all_ongoing_downloads, 1)
|
||||||
fun clearDownloads() {
|
fun clearDownloads() {
|
||||||
StateDownloads.instance.getDownloading().forEach {
|
StateDownloads.instance.getDownloading().forEach {
|
||||||
StateDownloads.instance.removeDownload(it);
|
StateDownloads.instance.removeDownload(it);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@FormField("Clear All Downloaded", FieldForm.BUTTON, "Deletes all downloaded videos and related files", 2)
|
@FormField(R.string.clear_all_downloaded, FieldForm.BUTTON, R.string.deletes_all_downloaded_videos_and_related_files, 2)
|
||||||
fun clearDownloaded() {
|
fun clearDownloaded() {
|
||||||
StateDownloads.instance.getDownloadedVideos().forEach {
|
StateDownloads.instance.getDownloadedVideos().forEach {
|
||||||
StateDownloads.instance.deleteCachedVideo(it.id);
|
StateDownloads.instance.deleteCachedVideo(it.id);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@FormField("Delete Unresolved", FieldForm.BUTTON, "Deletes all unresolved source files", 3)
|
@FormField(R.string.delete_unresolved, FieldForm.BUTTON, R.string.deletes_all_unresolved_source_files, 3)
|
||||||
fun cleanupDownloads() {
|
fun cleanupDownloads() {
|
||||||
StateDownloads.instance.cleanupDownloads();
|
StateDownloads.instance.cleanupDownloads();
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField("Fill storage till error", FieldForm.BUTTON, "Writes to disk till no space is left", 4)
|
@FormField(R.string.fill_storage_till_error, FieldForm.BUTTON, R.string.writes_to_disk_till_no_space_is_left, 4)
|
||||||
fun fillStorage(context: Context, scope: CoroutineScope?) {
|
fun fillStorage(context: Context, scope: CoroutineScope?) {
|
||||||
val gigabuffer = ByteArray(1024 * 1024 * 128);
|
val gigabuffer = ByteArray(1024 * 1024 * 128);
|
||||||
var count: Long = 0;
|
var count: Long = 0;
|
||||||
@@ -320,6 +492,17 @@ class SettingsDev : FragmentedStorageFileJson() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Contextual
|
||||||
|
@Transient
|
||||||
|
@FormField(R.string.info, FieldForm.GROUP, -1, 19)
|
||||||
|
var info = Info();
|
||||||
|
@Serializable
|
||||||
|
class Info {
|
||||||
|
@FormField(R.string.dev_info_channel_cache_size, FieldForm.READONLYTEXT, -1, 1, "channelCacheSize")
|
||||||
|
var channelCacheStartupCount = StateCache.instance.channelCacheStartupCount;
|
||||||
|
}
|
||||||
|
|
||||||
//region BOILERPLATE
|
//region BOILERPLATE
|
||||||
override fun encode(): String {
|
override fun encode(): String {
|
||||||
return Json.encodeToString(this);
|
return Json.encodeToString(this);
|
||||||
|
|||||||
@@ -1,24 +1,44 @@
|
|||||||
package com.futo.platformplayer
|
package com.futo.platformplayer
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
|
import android.net.Uri
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
import android.widget.*
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||||
import com.futo.platformplayer.casting.StateCasting
|
import com.futo.platformplayer.casting.StateCasting
|
||||||
import com.futo.platformplayer.dialogs.*
|
import com.futo.platformplayer.dialogs.AutoUpdateDialog
|
||||||
|
import com.futo.platformplayer.dialogs.AutomaticBackupDialog
|
||||||
|
import com.futo.platformplayer.dialogs.AutomaticRestoreDialog
|
||||||
|
import com.futo.platformplayer.dialogs.CastingAddDialog
|
||||||
|
import com.futo.platformplayer.dialogs.CastingHelpDialog
|
||||||
|
import com.futo.platformplayer.dialogs.ChangelogDialog
|
||||||
|
import com.futo.platformplayer.dialogs.CommentDialog
|
||||||
|
import com.futo.platformplayer.dialogs.ConnectCastingDialog
|
||||||
|
import com.futo.platformplayer.dialogs.ConnectedCastingDialog
|
||||||
|
import com.futo.platformplayer.dialogs.ImportDialog
|
||||||
|
import com.futo.platformplayer.dialogs.ImportOptionsDialog
|
||||||
|
import com.futo.platformplayer.dialogs.MigrateDialog
|
||||||
|
import com.futo.platformplayer.dialogs.ProgressDialog
|
||||||
import com.futo.platformplayer.engine.exceptions.PluginException
|
import com.futo.platformplayer.engine.exceptions.PluginException
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateAnnouncement
|
import com.futo.platformplayer.models.ImportCache
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StateBackup
|
import com.futo.platformplayer.states.StateBackup
|
||||||
import com.futo.platformplayer.stores.v2.ManagedStore
|
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||||
|
import com.futo.platformplayer.views.ToastView
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -91,6 +111,50 @@ class UIDialogs {
|
|||||||
}.toTypedArray());
|
}.toTypedArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun showUrlHandlingPrompt(context: Context, onYes: (() -> Unit)? = null) {
|
||||||
|
val builder = AlertDialog.Builder(context)
|
||||||
|
val view = LayoutInflater.from(context).inflate(R.layout.dialog_url_handling, null)
|
||||||
|
builder.setView(view)
|
||||||
|
|
||||||
|
val dialog = builder.create()
|
||||||
|
registerDialogOpened(dialog)
|
||||||
|
|
||||||
|
view.findViewById<TextView>(R.id.button_no).apply {
|
||||||
|
this.setOnClickListener {
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
view.findViewById<LinearLayout>(R.id.button_yes).apply {
|
||||||
|
this.setOnClickListener {
|
||||||
|
if (BuildConfig.IS_PLAYSTORE_BUILD) {
|
||||||
|
dialog.dismiss()
|
||||||
|
showDialogOk(context, R.drawable.ic_error_pred, context.getString(R.string.play_store_version_does_not_support_default_url_handling)) {
|
||||||
|
onYes?.invoke()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
val intent =
|
||||||
|
Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||||
|
val uri = Uri.fromParts("package", context.packageName, null)
|
||||||
|
intent.data = uri
|
||||||
|
context.startActivity(intent)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
toast(context, context.getString(R.string.failed_to_show_settings))
|
||||||
|
}
|
||||||
|
|
||||||
|
onYes?.invoke()
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.setOnDismissListener {
|
||||||
|
registerDialogClosed(dialog)
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.show()
|
||||||
|
}
|
||||||
|
|
||||||
fun showAutomaticBackupDialog(context: Context, skipRestoreCheck: Boolean = false, onClosed: (()->Unit)? = null) {
|
fun showAutomaticBackupDialog(context: Context, skipRestoreCheck: Boolean = false, onClosed: (()->Unit)? = null) {
|
||||||
val dialogAction: ()->Unit = {
|
val dialogAction: ()->Unit = {
|
||||||
@@ -100,14 +164,15 @@ class UIDialogs {
|
|||||||
dialog.show();
|
dialog.show();
|
||||||
};
|
};
|
||||||
if(StateBackup.hasAutomaticBackup() && !skipRestoreCheck)
|
if(StateBackup.hasAutomaticBackup() && !skipRestoreCheck)
|
||||||
UIDialogs.showDialog(context, R.drawable.ic_move_up, "An old backup is available", "Would you like to restore this backup?", null, 0,
|
UIDialogs.showDialog(context, R.drawable.ic_move_up, context.getString(R.string.an_old_backup_is_available), context.getString(R.string.would_you_like_to_restore_this_backup), null, 0,
|
||||||
UIDialogs.Action("Cancel", {}), //To nothing
|
UIDialogs.Action(context.getString(R.string.cancel), {}), //To nothing
|
||||||
UIDialogs.Action("Override", {
|
UIDialogs.Action(context.getString(R.string.override), {
|
||||||
dialogAction();
|
dialogAction();
|
||||||
}, UIDialogs.ActionStyle.DANGEROUS),
|
}, UIDialogs.ActionStyle.DANGEROUS),
|
||||||
UIDialogs.Action("Restore", {
|
UIDialogs.Action(context.getString(R.string.restore), {
|
||||||
UIDialogs.showAutomaticRestoreDialog(context, StateApp.instance.scope);
|
UIDialogs.showAutomaticRestoreDialog(context, StateApp.instance.scope);
|
||||||
}, UIDialogs.ActionStyle.PRIMARY));
|
}, UIDialogs.ActionStyle.PRIMARY)
|
||||||
|
);
|
||||||
else {
|
else {
|
||||||
dialogAction();
|
dialogAction();
|
||||||
}
|
}
|
||||||
@@ -142,8 +207,10 @@ class UIDialogs {
|
|||||||
view.findViewById<TextView>(R.id.dialog_text_code).apply {
|
view.findViewById<TextView>(R.id.dialog_text_code).apply {
|
||||||
if(code == null)
|
if(code == null)
|
||||||
this.visibility = View.GONE;
|
this.visibility = View.GONE;
|
||||||
else
|
else {
|
||||||
this.text = code;
|
this.text = code;
|
||||||
|
this.visibility = View.VISIBLE;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
view.findViewById<LinearLayout>(R.id.dialog_buttons).apply {
|
view.findViewById<LinearLayout>(R.id.dialog_buttons).apply {
|
||||||
val buttons = actions.map<Action, TextView> { act ->
|
val buttons = actions.map<Action, TextView> { act ->
|
||||||
@@ -211,10 +278,10 @@ class UIDialogs {
|
|||||||
(if(ex != null ) "${ex.message}" else ""),
|
(if(ex != null ) "${ex.message}" else ""),
|
||||||
if(ex is PluginException) ex.code else null,
|
if(ex is PluginException) ex.code else null,
|
||||||
0,
|
0,
|
||||||
UIDialogs.Action("Retry", {
|
UIDialogs.Action(context.getString(R.string.retry), {
|
||||||
retryAction?.invoke();
|
retryAction?.invoke();
|
||||||
}, UIDialogs.ActionStyle.PRIMARY),
|
}, UIDialogs.ActionStyle.PRIMARY),
|
||||||
UIDialogs.Action("Close", {
|
UIDialogs.Action(context.getString(R.string.close), {
|
||||||
closeAction?.invoke()
|
closeAction?.invoke()
|
||||||
}, UIDialogs.ActionStyle.NONE)
|
}, UIDialogs.ActionStyle.NONE)
|
||||||
);
|
);
|
||||||
@@ -226,24 +293,28 @@ class UIDialogs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun showDataRetryDialog(context: Context, reason: String? = null, retryAction: (() -> Unit)? = null, closeAction: (() -> Unit)? = null) {
|
fun showDataRetryDialog(context: Context, reason: String? = null, retryAction: (() -> Unit)? = null, closeAction: (() -> Unit)? = null) {
|
||||||
val retryButtonAction = Action("Retry", retryAction ?: {}, ActionStyle.PRIMARY)
|
val retryButtonAction = Action(context.getString(R.string.retry), retryAction ?: {}, ActionStyle.PRIMARY)
|
||||||
val closeButtonAction = Action("Close", closeAction ?: {}, ActionStyle.ACCENT)
|
val closeButtonAction = Action(context.getString(R.string.close), closeAction ?: {}, ActionStyle.ACCENT)
|
||||||
showDialog(context, R.drawable.ic_no_internet_86dp, "Data Retry", reason, null, 0, closeButtonAction, retryButtonAction)
|
showDialog(context, R.drawable.ic_no_internet_86dp, context.getString(R.string.data_retry), reason, null, 0, closeButtonAction, retryButtonAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun showConfirmationDialog(context: Context, text: String, action: () -> Unit, cancelAction: (() -> Unit)? = null) {
|
fun showConfirmationDialog(context: Context, text: String, action: () -> Unit, cancelAction: (() -> Unit)? = null) {
|
||||||
val confirmButtonAction = Action("Confirm", action, ActionStyle.PRIMARY)
|
val confirmButtonAction = Action(context.getString(R.string.confirm), action, ActionStyle.PRIMARY)
|
||||||
val cancelButtonAction = Action("Cancel", cancelAction ?: {}, ActionStyle.ACCENT)
|
val cancelButtonAction = Action(context.getString(R.string.cancel), cancelAction ?: {}, ActionStyle.ACCENT)
|
||||||
showDialog(context, R.drawable.ic_error, text, null, null, 0, cancelButtonAction, confirmButtonAction)
|
showDialog(context, R.drawable.ic_error, text, null, null, 0, cancelButtonAction, confirmButtonAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showUpdateAvailableDialog(context: Context, lastVersion: Int) {
|
fun showUpdateAvailableDialog(context: Context, lastVersion: Int, hideExceptionButtons: Boolean = false) {
|
||||||
val dialog = AutoUpdateDialog(context);
|
val dialog = AutoUpdateDialog(context);
|
||||||
registerDialogOpened(dialog);
|
registerDialogOpened(dialog);
|
||||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||||
dialog.show();
|
dialog.show();
|
||||||
dialog.setMaxVersion(lastVersion);
|
dialog.setMaxVersion(lastVersion);
|
||||||
|
|
||||||
|
if (hideExceptionButtons) {
|
||||||
|
dialog.hideExceptionButtons()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showChangelogDialog(context: Context, lastVersion: Int) {
|
fun showChangelogDialog(context: Context, lastVersion: Int) {
|
||||||
@@ -273,8 +344,14 @@ class UIDialogs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showImportDialog(context: Context, store: ManagedStore<*>, name: String, reconstructions: List<String>, onConcluded: () -> Unit) {
|
fun showImportDialog(context: Context, store: ManagedStore<*>, name: String, reconstructions: List<String>, cache: ImportCache?, onConcluded: () -> Unit) {
|
||||||
val dialog = ImportDialog(context, store, name, reconstructions, onConcluded);
|
val dialog = ImportDialog(context, store, name, reconstructions, cache, onConcluded);
|
||||||
|
registerDialogOpened(dialog);
|
||||||
|
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||||
|
dialog.show();
|
||||||
|
}
|
||||||
|
fun showImportOptionsDialog(context: MainActivity) {
|
||||||
|
val dialog = ImportOptionsDialog(context);
|
||||||
registerDialogOpened(dialog);
|
registerDialogOpened(dialog);
|
||||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||||
dialog.show();
|
dialog.show();
|
||||||
@@ -285,17 +362,34 @@ class UIDialogs {
|
|||||||
val d = StateCasting.instance.activeDevice;
|
val d = StateCasting.instance.activeDevice;
|
||||||
if (d != null) {
|
if (d != null) {
|
||||||
val dialog = ConnectedCastingDialog(context);
|
val dialog = ConnectedCastingDialog(context);
|
||||||
|
if (context is Activity) {
|
||||||
|
dialog.setOwnerActivity(context)
|
||||||
|
}
|
||||||
registerDialogOpened(dialog);
|
registerDialogOpened(dialog);
|
||||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||||
dialog.show();
|
dialog.show();
|
||||||
} else {
|
} else {
|
||||||
val dialog = ConnectCastingDialog(context);
|
val dialog = ConnectCastingDialog(context);
|
||||||
|
if (context is Activity) {
|
||||||
|
dialog.setOwnerActivity(context)
|
||||||
|
}
|
||||||
registerDialogOpened(dialog);
|
registerDialogOpened(dialog);
|
||||||
|
val c = context
|
||||||
|
if (c is Activity) {
|
||||||
|
dialog.setOwnerActivity(c);
|
||||||
|
}
|
||||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||||
dialog.show();
|
dialog.show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun showCastingTutorialDialog(context: Context) {
|
||||||
|
val dialog = CastingHelpDialog(context);
|
||||||
|
registerDialogOpened(dialog);
|
||||||
|
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||||
|
dialog.show();
|
||||||
|
}
|
||||||
|
|
||||||
fun showCastingAddDialog(context: Context) {
|
fun showCastingAddDialog(context: Context) {
|
||||||
val dialog = CastingAddDialog(context);
|
val dialog = CastingAddDialog(context);
|
||||||
registerDialogOpened(dialog);
|
registerDialogOpened(dialog);
|
||||||
@@ -310,13 +404,28 @@ class UIDialogs {
|
|||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
||||||
try {
|
try {
|
||||||
StateApp.withContext {
|
StateApp.withContext {
|
||||||
Toast.makeText(it, text, if (long) Toast.LENGTH_LONG else Toast.LENGTH_SHORT).show();
|
toast(it, text, long);
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to show toast.", e);
|
Logger.e(TAG, "Failed to show toast.", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fun appToast(text: String, long: Boolean = false) {
|
||||||
|
appToast(ToastView.Toast(text, long))
|
||||||
|
}
|
||||||
|
fun appToastError(text: String, long: Boolean) {
|
||||||
|
StateApp.withContext {
|
||||||
|
appToast(ToastView.Toast(text, long, it.getColor(R.color.pastel_red)));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
fun appToast(toast: ToastView.Toast) {
|
||||||
|
StateApp.withContext {
|
||||||
|
if(it is MainActivity) {
|
||||||
|
it.showAppToast(toast);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun showClickableToast(context: Context, text: String, onClick: () -> Unit, isLongDuration: Boolean = false) {
|
fun showClickableToast(context: Context, text: String, onClick: () -> Unit, isLongDuration: Boolean = false) {
|
||||||
//TODO: Is not actually clickable...
|
//TODO: Is not actually clickable...
|
||||||
|
|||||||
@@ -1,41 +1,69 @@
|
|||||||
package com.futo.platformplayer
|
package com.futo.platformplayer
|
||||||
|
|
||||||
|
import android.app.NotificationManager
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
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.channels.IPlatformChannel
|
||||||
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
|
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantAudioUrlSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantVideoUrlSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudioSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.SubtitleRawSource
|
|
||||||
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||||
import com.futo.platformplayer.downloads.VideoLocal
|
import com.futo.platformplayer.downloads.VideoLocal
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionGroupFragment
|
||||||
import com.futo.platformplayer.helpers.VideoHelper
|
import com.futo.platformplayer.helpers.VideoHelper
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.models.ImageVariable
|
||||||
import com.futo.platformplayer.models.Playlist
|
import com.futo.platformplayer.models.Playlist
|
||||||
import com.futo.platformplayer.states.*
|
import com.futo.platformplayer.models.Subscription
|
||||||
import com.futo.platformplayer.views.Loader
|
import com.futo.platformplayer.models.SubscriptionGroup
|
||||||
|
import com.futo.platformplayer.parsers.HLS
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.futo.platformplayer.states.StateDownloads
|
||||||
|
import com.futo.platformplayer.states.StateMeta
|
||||||
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
|
import com.futo.platformplayer.states.StatePlayer
|
||||||
|
import com.futo.platformplayer.states.StatePlaylists
|
||||||
|
import com.futo.platformplayer.states.StateSubscriptionGroups
|
||||||
|
import com.futo.platformplayer.states.StateSubscriptions
|
||||||
|
import com.futo.platformplayer.views.AnyAdapterView
|
||||||
|
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
|
||||||
|
import com.futo.platformplayer.views.LoaderView
|
||||||
|
import com.futo.platformplayer.views.adapters.viewholders.SubscriptionGroupBarViewHolder
|
||||||
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuFilters
|
||||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuGroup
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuGroup
|
||||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
|
||||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
||||||
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuRecycler
|
||||||
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTextInput
|
||||||
import com.futo.platformplayer.views.pills.RoundButton
|
import com.futo.platformplayer.views.pills.RoundButton
|
||||||
import com.futo.platformplayer.views.pills.RoundButtonGroup
|
import com.futo.platformplayer.views.pills.RoundButtonGroup
|
||||||
import com.futo.platformplayer.views.overlays.slideup.*
|
|
||||||
import com.futo.platformplayer.views.video.FutoVideoPlayerBase
|
import com.futo.platformplayer.views.video.FutoVideoPlayerBase
|
||||||
|
import isDownloadable
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.lang.IllegalStateException
|
|
||||||
|
|
||||||
class UISlideOverlays {
|
class UISlideOverlays {
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "UISlideOverlays";
|
private const val TAG = "UISlideOverlays";
|
||||||
|
|
||||||
fun showOverlay(container: ViewGroup, title: String, okButton: String?, onOk: ()->Unit, vararg views: View) {
|
fun showOverlay(container: ViewGroup, title: String, okButton: String?, onOk: ()->Unit, vararg views: View): SlideUpMenuOverlay {
|
||||||
var menu = SlideUpMenuOverlay(container.context, container, title, okButton, true, *views);
|
var menu = SlideUpMenuOverlay(container.context, container, title, okButton, true, *views);
|
||||||
|
|
||||||
menu.onOK.subscribe {
|
menu.onOK.subscribe {
|
||||||
@@ -43,6 +71,244 @@ class UISlideOverlays {
|
|||||||
onOk.invoke();
|
onOk.invoke();
|
||||||
};
|
};
|
||||||
menu.show();
|
menu.show();
|
||||||
|
return menu;
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup): SlideUpMenuOverlay {
|
||||||
|
val items = arrayListOf<View>();
|
||||||
|
|
||||||
|
val originalNotif = subscription.doNotifications;
|
||||||
|
val originalLive = subscription.doFetchLive;
|
||||||
|
val originalStream = subscription.doFetchStreams;
|
||||||
|
val originalVideo = subscription.doFetchVideos;
|
||||||
|
val originalPosts = subscription.doFetchPosts;
|
||||||
|
|
||||||
|
val menu = SlideUpMenuOverlay(container.context, container, "Subscription Settings", null, true, listOf());
|
||||||
|
|
||||||
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO){
|
||||||
|
val plugin = StatePlatform.instance.getChannelClient(subscription.channel.url);
|
||||||
|
val capabilities = plugin.getChannelCapabilities();
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
items.addAll(listOf(
|
||||||
|
SlideUpMenuItem(container.context, R.drawable.ic_notifications, "Notifications", "", "notifications", {
|
||||||
|
subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications;
|
||||||
|
}, false),
|
||||||
|
if(StateSubscriptionGroups.instance.getSubscriptionGroups().isNotEmpty())
|
||||||
|
SlideUpMenuGroup(container.context, "Subscription Groups",
|
||||||
|
"You can select which groups this subscription is part of.",
|
||||||
|
-1, listOf()) else null,
|
||||||
|
if(StateSubscriptionGroups.instance.getSubscriptionGroups().isNotEmpty())
|
||||||
|
SlideUpMenuRecycler(container.context, "as") {
|
||||||
|
val groups = ArrayList<SubscriptionGroup>(StateSubscriptionGroups.instance.getSubscriptionGroups()
|
||||||
|
.map { SubscriptionGroup.Selectable(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(actualGroup);
|
||||||
|
groups.addAll(StateSubscriptionGroups.instance.getSubscriptionGroups()
|
||||||
|
.map { SubscriptionGroup.Selectable(it, it.urls.contains(subscription.channel.url)) }
|
||||||
|
.sortedBy { !it.selected });
|
||||||
|
adapter?.notifyContentChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return@SlideUpMenuRecycler adapter;
|
||||||
|
} else null,
|
||||||
|
SlideUpMenuGroup(container.context, "Fetch Settings",
|
||||||
|
"Depending on the platform you might not need to enable a type for it to be available.",
|
||||||
|
-1, listOf()),
|
||||||
|
if(capabilities.hasType(ResultCapabilities.TYPE_LIVE)) SlideUpMenuItem(container.context, R.drawable.ic_live_tv, "Livestreams", "Check for livestreams", "fetchLive", {
|
||||||
|
subscription.doFetchLive = menu?.selectOption(null, "fetchLive", true, true) ?: subscription.doFetchLive;
|
||||||
|
}, false) else null,
|
||||||
|
if(capabilities.hasType(ResultCapabilities.TYPE_STREAMS)) SlideUpMenuItem(container.context, R.drawable.ic_play, "Streams", "Check for streams", "fetchStreams", {
|
||||||
|
subscription.doFetchStreams = menu?.selectOption(null, "fetchStreams", true, true) ?: subscription.doFetchStreams;
|
||||||
|
}, false) else null,
|
||||||
|
if(capabilities.hasType(ResultCapabilities.TYPE_VIDEOS))
|
||||||
|
SlideUpMenuItem(container.context, R.drawable.ic_play, "Videos", "Check for videos", "fetchVideos", {
|
||||||
|
subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos;
|
||||||
|
}, false) else if(capabilities.hasType(ResultCapabilities.TYPE_MIXED) || capabilities.types.isEmpty())
|
||||||
|
SlideUpMenuItem(container.context, R.drawable.ic_play, "Content", "Check for content", "fetchVideos", {
|
||||||
|
subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos;
|
||||||
|
}, false) else null,
|
||||||
|
if(capabilities.hasType(ResultCapabilities.TYPE_POSTS)) SlideUpMenuItem(container.context, R.drawable.ic_chat, "Posts", "Check for posts", "fetchPosts", {
|
||||||
|
subscription.doFetchPosts = menu?.selectOption(null, "fetchPosts", true, true) ?: subscription.doFetchPosts;
|
||||||
|
}, false) else null/*,,
|
||||||
|
|
||||||
|
SlideUpMenuGroup(container.context, "Actions",
|
||||||
|
"Various things you can do with this subscription",
|
||||||
|
-1, listOf())
|
||||||
|
SlideUpMenuItem(container.context, R.drawable.ic_list, "Add to Group", "", "btnAddToGroup", {
|
||||||
|
showCreateSubscriptionGroup(container, subscription.channel);
|
||||||
|
}, false)*/
|
||||||
|
).filterNotNull());
|
||||||
|
|
||||||
|
menu.setItems(items);
|
||||||
|
|
||||||
|
if(subscription.doNotifications)
|
||||||
|
menu.selectOption(null, "notifications", true, true);
|
||||||
|
if(subscription.doFetchLive)
|
||||||
|
menu.selectOption(null, "fetchLive", true, true);
|
||||||
|
if(subscription.doFetchStreams)
|
||||||
|
menu.selectOption(null, "fetchStreams", true, true);
|
||||||
|
if(subscription.doFetchVideos)
|
||||||
|
menu.selectOption(null, "fetchVideos", true, true);
|
||||||
|
if(subscription.doFetchPosts)
|
||||||
|
menu.selectOption(null, "fetchPosts", true, true);
|
||||||
|
|
||||||
|
menu.onOK.subscribe {
|
||||||
|
subscription.save();
|
||||||
|
menu.hide(true);
|
||||||
|
|
||||||
|
if(subscription.doNotifications && !originalNotif) {
|
||||||
|
val mainContext = StateApp.instance.contextOrNull;
|
||||||
|
if(Settings.instance.subscriptions.subscriptionsBackgroundUpdateInterval == 0) {
|
||||||
|
UIDialogs.toast(container.context, "Enable 'Background Update' in settings for notifications to work");
|
||||||
|
|
||||||
|
if(mainContext is MainActivity) {
|
||||||
|
UIDialogs.showDialog(mainContext, R.drawable.ic_settings, "Background Updating Required",
|
||||||
|
"You need to set a Background Updating interval for notifications", null, 0,
|
||||||
|
UIDialogs.Action("Cancel", {}),
|
||||||
|
UIDialogs.Action("Configure", {
|
||||||
|
val intent = Intent(mainContext, SettingsActivity::class.java);
|
||||||
|
intent.putExtra("query", mainContext.getString(R.string.background_update));
|
||||||
|
mainContext.startActivity(intent);
|
||||||
|
}, UIDialogs.ActionStyle.PRIMARY));
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
subscription.doFetchLive = originalLive;
|
||||||
|
subscription.doFetchStreams = originalStream;
|
||||||
|
subscription.doFetchVideos = originalVideo;
|
||||||
|
subscription.doFetchPosts = originalPosts;
|
||||||
|
};
|
||||||
|
|
||||||
|
menu.setOk("Save");
|
||||||
|
|
||||||
|
menu.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return menu;
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showAddToGroupOverlay(channel: IPlatformVideo, container: ViewGroup) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showHlsPicker(video: IPlatformVideoDetails, source: Any, sourceUrl: String, container: ViewGroup): SlideUpMenuOverlay {
|
||||||
|
val items = arrayListOf<View>(LoaderView(container.context))
|
||||||
|
val slideUpMenuOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.download_video), null, true, items)
|
||||||
|
|
||||||
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
|
val masterPlaylistResponse = ManagedHttpClient().get(sourceUrl)
|
||||||
|
check(masterPlaylistResponse.isOk) { "Failed to get master playlist: ${masterPlaylistResponse.code}" }
|
||||||
|
|
||||||
|
val masterPlaylistContent = masterPlaylistResponse.body?.string()
|
||||||
|
?: throw Exception("Master playlist content is empty")
|
||||||
|
|
||||||
|
val videoButtons = arrayListOf<SlideUpMenuItem>()
|
||||||
|
val audioButtons = arrayListOf<SlideUpMenuItem>()
|
||||||
|
//TODO: Implement subtitles
|
||||||
|
//val subtitleButtons = arrayListOf<SlideUpMenuItem>()
|
||||||
|
|
||||||
|
var selectedVideoVariant: HLSVariantVideoUrlSource? = null
|
||||||
|
var selectedAudioVariant: HLSVariantAudioUrlSource? = null
|
||||||
|
//TODO: Implement subtitles
|
||||||
|
//var selectedSubtitleVariant: HLSVariantSubtitleUrlSource? = null
|
||||||
|
|
||||||
|
val masterPlaylist: HLS.MasterPlaylist
|
||||||
|
try {
|
||||||
|
masterPlaylist = HLS.parseMasterPlaylist(masterPlaylistContent, sourceUrl)
|
||||||
|
|
||||||
|
masterPlaylist.getAudioSources().forEach { it ->
|
||||||
|
audioButtons.add(SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, listOf(it.language, it.codec).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "), it, {
|
||||||
|
selectedAudioVariant = it
|
||||||
|
slideUpMenuOverlay.selectOption(audioButtons, it)
|
||||||
|
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
||||||
|
}, 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))
|
||||||
|
}, false))
|
||||||
|
}*/
|
||||||
|
|
||||||
|
masterPlaylist.getVideoSources().forEach {
|
||||||
|
videoButtons.add(SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "${it.width}x${it.height}", it, {
|
||||||
|
selectedVideoVariant = it
|
||||||
|
slideUpMenuOverlay.selectOption(videoButtons, it)
|
||||||
|
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
||||||
|
}, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
val newItems = arrayListOf<View>()
|
||||||
|
if (videoButtons.isNotEmpty()) {
|
||||||
|
newItems.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.video), videoButtons, videoButtons))
|
||||||
|
}
|
||||||
|
if (audioButtons.isNotEmpty()) {
|
||||||
|
newItems.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.audio), audioButtons, audioButtons))
|
||||||
|
}
|
||||||
|
//TODO: Implement subtitles
|
||||||
|
/*if (subtitleButtons.isNotEmpty()) {
|
||||||
|
newItems.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.subtitles), subtitleButtons, subtitleButtons))
|
||||||
|
}*/
|
||||||
|
|
||||||
|
slideUpMenuOverlay.onOK.subscribe {
|
||||||
|
//TODO: Fix SubtitleRawSource issue
|
||||||
|
StateDownloads.instance.download(video, selectedVideoVariant, selectedAudioVariant, null);
|
||||||
|
slideUpMenuOverlay.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
slideUpMenuOverlay.setItems(newItems)
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
if (masterPlaylistContent.lines().any { it.startsWith("#EXTINF:") }) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
if (source is IHLSManifestSource) {
|
||||||
|
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")
|
||||||
|
slideUpMenuOverlay.hide()
|
||||||
|
} else if (source is IHLSManifestAudioSource) {
|
||||||
|
StateDownloads.instance.download(video, null, HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, sourceUrl), null)
|
||||||
|
UIDialogs.toast(container.context, "Variant audio HLS playlist download started")
|
||||||
|
slideUpMenuOverlay.hide()
|
||||||
|
} else {
|
||||||
|
throw NotImplementedError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return slideUpMenuOverlay.apply { show() }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showDownloadVideoOverlay(video: IPlatformVideoDetails, container: ViewGroup, contentResolver: ContentResolver? = null): SlideUpMenuOverlay? {
|
fun showDownloadVideoOverlay(video: IPlatformVideoDetails, container: ViewGroup, contentResolver: ContentResolver? = null): SlideUpMenuOverlay? {
|
||||||
@@ -63,70 +329,93 @@ class UISlideOverlays {
|
|||||||
val audioSources = if(descriptor is VideoUnMuxedSourceDescriptor) descriptor.audioSources else null;
|
val audioSources = if(descriptor is VideoUnMuxedSourceDescriptor) descriptor.audioSources else null;
|
||||||
val subtitleSources = video.subtitles;
|
val subtitleSources = video.subtitles;
|
||||||
|
|
||||||
if(videoSources.size == 0 && (audioSources?.size ?: 0) == 0) {
|
if(videoSources.isEmpty() && (audioSources?.size ?: 0) == 0) {
|
||||||
UIDialogs.toast("No downloads available", false);
|
UIDialogs.toast(container.context.getString(R.string.no_downloads_available), false);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!VideoHelper.isDownloadable(video)) {
|
if(!VideoHelper.isDownloadable(video)) {
|
||||||
Logger.i(TAG, "Attempted to open downloads without valid sources for [${video.name}]: ${video.url}");
|
Logger.i(TAG, "Attempted to open downloads without valid sources for [${video.name}]: ${video.url}");
|
||||||
UIDialogs.toast( "No downloadable sources (yet)");
|
UIDialogs.toast( container.context.getString(R.string.no_downloadable_sources_yet));
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
items.add(SlideUpMenuGroup(container.context, "Video", videoSources,
|
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.video), videoSources,
|
||||||
listOf(listOf(SlideUpMenuItem(container.context, R.drawable.ic_movie, "None", "Audio Only", "none", {
|
listOf(listOf(SlideUpMenuItem(container.context, R.drawable.ic_movie, container.context.getString(R.string.none), container.context.getString(R.string.audio_only), "none", {
|
||||||
selectedVideo = null;
|
selectedVideo = null;
|
||||||
menu?.selectOption(videoSources, "none");
|
menu?.selectOption(videoSources, "none");
|
||||||
if(selectedAudio != null || !requiresAudio)
|
if(selectedAudio != null || !requiresAudio)
|
||||||
menu?.setOk("Download");
|
menu?.setOk(container.context.getString(R.string.download));
|
||||||
}, false)) +
|
}, false)) +
|
||||||
videoSources
|
videoSources
|
||||||
.filter { it.isDownloadable() }
|
.filter { it.isDownloadable() }
|
||||||
.map {
|
.map {
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "${it.width}x${it.height}", it, {
|
when (it) {
|
||||||
selectedVideo = it as IVideoUrlSource;
|
is IVideoUrlSource -> {
|
||||||
menu?.selectOption(videoSources, it);
|
SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "${it.width}x${it.height}", it, {
|
||||||
if(selectedAudio != null || !requiresAudio)
|
selectedVideo = it
|
||||||
menu?.setOk("Download");
|
menu?.selectOption(videoSources, it);
|
||||||
}, false)
|
if(selectedAudio != null || !requiresAudio)
|
||||||
|
menu?.setOk(container.context.getString(R.string.download));
|
||||||
|
}, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
is IHLSManifestSource -> {
|
||||||
|
SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "HLS", it, {
|
||||||
|
showHlsPicker(video, it, it.url, container)
|
||||||
|
}, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
throw Exception("Unhandled source type")
|
||||||
|
}
|
||||||
|
}
|
||||||
}).flatten().toList()
|
}).flatten().toList()
|
||||||
));
|
));
|
||||||
|
|
||||||
if(Settings.instance.downloads.getDefaultVideoQualityPixels() > 0 && videoSources.size > 0)
|
if(Settings.instance.downloads.getDefaultVideoQualityPixels() > 0 && videoSources.isNotEmpty()) {
|
||||||
selectedVideo = VideoHelper.selectBestVideoSource(videoSources.filter { it.isDownloadable() }.asIterable(),
|
//TODO: Add HLS support here
|
||||||
|
selectedVideo = VideoHelper.selectBestVideoSource(
|
||||||
|
videoSources.filter { it is IVideoUrlSource && it.isDownloadable() }.asIterable(),
|
||||||
Settings.instance.downloads.getDefaultVideoQualityPixels(),
|
Settings.instance.downloads.getDefaultVideoQualityPixels(),
|
||||||
FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS) as IVideoUrlSource;
|
FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS
|
||||||
|
) as IVideoUrlSource?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (audioSources != null) {
|
||||||
audioSources?.let { audioSources ->
|
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.audio), audioSources, audioSources
|
||||||
items.add(SlideUpMenuGroup(container.context, "Audio", audioSources, audioSources
|
|
||||||
.filter { VideoHelper.isDownloadable(it) }
|
.filter { VideoHelper.isDownloadable(it) }
|
||||||
.map {
|
.map {
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, "${it.bitrate}", it, {
|
when (it) {
|
||||||
selectedAudio = it as IAudioUrlSource;
|
is IAudioUrlSource -> {
|
||||||
menu?.selectOption(audioSources, it);
|
SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, "${it.bitrate}", it, {
|
||||||
menu?.setOk("Download");
|
selectedAudio = it
|
||||||
}, false);
|
menu?.selectOption(audioSources, it);
|
||||||
|
menu?.setOk(container.context.getString(R.string.download));
|
||||||
|
}, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
is IHLSManifestAudioSource -> {
|
||||||
|
SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "HLS Audio", it, {
|
||||||
|
showHlsPicker(video, it, it.url, container)
|
||||||
|
}, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
throw Exception("Unhandled source type")
|
||||||
|
}
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
val asources = audioSources;
|
|
||||||
val preferredAudioSource = VideoHelper.selectBestAudioSource(asources.asIterable(),
|
|
||||||
FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS,
|
|
||||||
Settings.instance.playback.getPrimaryLanguage(container.context),
|
|
||||||
if(Settings.instance.downloads.isHighBitrateDefault()) 99999999 else 1);
|
|
||||||
menu?.selectOption(asources, preferredAudioSource);
|
|
||||||
|
|
||||||
|
//TODO: Add HLS support here
|
||||||
selectedAudio = VideoHelper.selectBestAudioSource(audioSources.filter { it.isDownloadable() }.asIterable(),
|
selectedAudio = VideoHelper.selectBestAudioSource(audioSources.filter { it is IAudioUrlSource && it.isDownloadable() }.asIterable(),
|
||||||
FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS,
|
FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS,
|
||||||
Settings.instance.playback.getPrimaryLanguage(container.context),
|
Settings.instance.playback.getPrimaryLanguage(container.context),
|
||||||
if(Settings.instance.downloads.isHighBitrateDefault()) 9999999 else 1) as IAudioUrlSource?;
|
if(Settings.instance.downloads.isHighBitrateDefault()) 9999999 else 1) as IAudioUrlSource?;
|
||||||
}
|
}
|
||||||
|
|
||||||
//ContentResolver is required for subtitles..
|
if(contentResolver != null && subtitleSources.isNotEmpty()) {
|
||||||
if(contentResolver != null) {
|
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.subtitles), subtitleSources, subtitleSources.map {
|
||||||
items.add(SlideUpMenuGroup(container.context, "Subtitles", subtitleSources, subtitleSources
|
|
||||||
.map {
|
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_edit, it.name, "", it, {
|
SlideUpMenuItem(container.context, R.drawable.ic_edit, it.name, "", it, {
|
||||||
if (selectedSubtitle == it) {
|
if (selectedSubtitle == it) {
|
||||||
selectedSubtitle = null;
|
selectedSubtitle = null;
|
||||||
@@ -136,10 +425,11 @@ class UISlideOverlays {
|
|||||||
menu?.selectOption(subtitleSources, it);
|
menu?.selectOption(subtitleSources, it);
|
||||||
}
|
}
|
||||||
}, false);
|
}, false);
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
menu = SlideUpMenuOverlay(container.context, container, "Download Video", null, true, items);
|
menu = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.download_video), null, true, items);
|
||||||
|
|
||||||
if(selectedVideo != null) {
|
if(selectedVideo != null) {
|
||||||
menu.selectOption(videoSources, selectedVideo);
|
menu.selectOption(videoSources, selectedVideo);
|
||||||
@@ -148,7 +438,7 @@ class UISlideOverlays {
|
|||||||
audioSources?.let { audioSources -> menu.selectOption(audioSources, selectedAudio); };
|
audioSources?.let { audioSources -> menu.selectOption(audioSources, selectedAudio); };
|
||||||
}
|
}
|
||||||
if(selectedAudio != null || (!requiresAudio && selectedVideo != null)) {
|
if(selectedAudio != null || (!requiresAudio && selectedVideo != null)) {
|
||||||
menu.setOk("Download");
|
menu.setOk(container.context.getString(R.string.download));
|
||||||
}
|
}
|
||||||
|
|
||||||
menu.onOK.subscribe {
|
menu.onOK.subscribe {
|
||||||
@@ -185,7 +475,7 @@ class UISlideOverlays {
|
|||||||
}
|
}
|
||||||
fun showDownloadVideoOverlay(video: IPlatformVideo, container: ViewGroup, useDetails: Boolean = false) {
|
fun showDownloadVideoOverlay(video: IPlatformVideo, container: ViewGroup, useDetails: Boolean = false) {
|
||||||
val handleUnknownDownload: ()->Unit = {
|
val handleUnknownDownload: ()->Unit = {
|
||||||
showUnknownVideoDownload("Video", container) { px, bitrate ->
|
showUnknownVideoDownload(container.context.getString(R.string.video), container) { px, bitrate ->
|
||||||
StateDownloads.instance.download(video, px, bitrate)
|
StateDownloads.instance.download(video, px, bitrate)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -195,7 +485,7 @@ class UISlideOverlays {
|
|||||||
val scope = StateApp.instance.scopeOrNull;
|
val scope = StateApp.instance.scopeOrNull;
|
||||||
|
|
||||||
if(scope != null) {
|
if(scope != null) {
|
||||||
val loader = showLoaderOverlay("Fetching video details", container);
|
val loader = showLoaderOverlay(container.context.getString(R.string.fetching_video_details), container);
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val videoDetails = StatePlatform.instance.getContentDetails(video.url, false).await();
|
val videoDetails = StatePlatform.instance.getContentDetails(video.url, false).await();
|
||||||
@@ -209,7 +499,7 @@ class UISlideOverlays {
|
|||||||
}
|
}
|
||||||
catch(ex: Throwable) {
|
catch(ex: Throwable) {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
UIDialogs.toast("Failed to fetch details for download");
|
UIDialogs.toast(container.context.getString(R.string.failed_to_fetch_details_for_download));
|
||||||
handleUnknownDownload();
|
handleUnknownDownload();
|
||||||
loader.hide(true);
|
loader.hide(true);
|
||||||
}
|
}
|
||||||
@@ -220,10 +510,15 @@ class UISlideOverlays {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun showDownloadPlaylistOverlay(playlist: Playlist, container: ViewGroup) {
|
fun showDownloadPlaylistOverlay(playlist: Playlist, container: ViewGroup) {
|
||||||
showUnknownVideoDownload("Video", container) { px, bitrate ->
|
showUnknownVideoDownload(container.context.getString(R.string.playlist), container) { px, bitrate ->
|
||||||
StateDownloads.instance.download(playlist, px, bitrate);
|
StateDownloads.instance.download(playlist, px, bitrate);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
fun showDownloadWatchlaterOverlay(container: ViewGroup) {
|
||||||
|
showUnknownVideoDownload(container.context.getString(R.string.watch_later), container, { px, bitrate ->
|
||||||
|
StateDownloads.instance.downloadWatchLater(px, bitrate);
|
||||||
|
})
|
||||||
|
}
|
||||||
private fun showUnknownVideoDownload(toDownload: String, container: ViewGroup, cb: (Long?, Long?)->Unit) {
|
private fun showUnknownVideoDownload(toDownload: String, container: ViewGroup, cb: (Long?, Long?)->Unit) {
|
||||||
val items = arrayListOf<View>();
|
val items = arrayListOf<View>();
|
||||||
var menu: SlideUpMenuOverlay? = null;
|
var menu: SlideUpMenuOverlay? = null;
|
||||||
@@ -232,7 +527,7 @@ class UISlideOverlays {
|
|||||||
var targetBitrate: Long = 0;
|
var targetBitrate: Long = 0;
|
||||||
|
|
||||||
val resolutions = listOf(
|
val resolutions = listOf(
|
||||||
Triple<String, String, Long>("None", "None", -1),
|
Triple<String, String, Long>(container.context.getString(R.string.none), container.context.getString(R.string.none), -1),
|
||||||
Triple<String, String, Long>("480P", "720x480", 720*480),
|
Triple<String, String, Long>("480P", "720x480", 720*480),
|
||||||
Triple<String, String, Long>("720P", "1280x720", 1280*720),
|
Triple<String, String, Long>("720P", "1280x720", 1280*720),
|
||||||
Triple<String, String, Long>("1080P", "1920x1080", 1920*1080),
|
Triple<String, String, Long>("1080P", "1920x1080", 1920*1080),
|
||||||
@@ -240,23 +535,23 @@ class UISlideOverlays {
|
|||||||
Triple<String, String, Long>("2160P", "3840x2160", 3840*2160)
|
Triple<String, String, Long>("2160P", "3840x2160", 3840*2160)
|
||||||
);
|
);
|
||||||
|
|
||||||
items.add(SlideUpMenuGroup(container.context, "Target Resolution", "Video", resolutions.map {
|
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.target_resolution), "Video", resolutions.map {
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_movie, it.first, it.second, it.third, {
|
SlideUpMenuItem(container.context, R.drawable.ic_movie, it.first, it.second, it.third, {
|
||||||
targetPxSize = it.third;
|
targetPxSize = it.third;
|
||||||
menu?.selectOption("Video", it.third);
|
menu?.selectOption("Video", it.third);
|
||||||
}, false)
|
}, false)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
items.add(SlideUpMenuGroup(container.context, "Target Bitrate", "Bitrate", listOf(
|
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.target_bitrate), "Bitrate", listOf(
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_movie, "Low Bitrate", "", 1, {
|
SlideUpMenuItem(container.context, R.drawable.ic_movie, container.context.getString(R.string.low_bitrate), "", 1, {
|
||||||
targetBitrate = 1;
|
targetBitrate = 1;
|
||||||
menu?.selectOption("Bitrate", 1);
|
menu?.selectOption("Bitrate", 1);
|
||||||
menu?.setOk("Download");
|
menu?.setOk(container.context.getString(R.string.download));
|
||||||
}, false),
|
}, false),
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_movie, "High Bitrate", "", 9999999, {
|
SlideUpMenuItem(container.context, R.drawable.ic_movie, container.context.getString(R.string.high_bitrate), "", 9999999, {
|
||||||
targetBitrate = 9999999;
|
targetBitrate = 9999999;
|
||||||
menu?.selectOption("Bitrate", 9999999);
|
menu?.selectOption("Bitrate", 9999999);
|
||||||
menu?.setOk("Download");
|
menu?.setOk(container.context.getString(R.string.download));
|
||||||
}, false)
|
}, false)
|
||||||
)));
|
)));
|
||||||
|
|
||||||
@@ -277,12 +572,12 @@ class UISlideOverlays {
|
|||||||
if(Settings.instance.downloads.isHighBitrateDefault()) {
|
if(Settings.instance.downloads.isHighBitrateDefault()) {
|
||||||
targetBitrate = 9999999;
|
targetBitrate = 9999999;
|
||||||
menu.selectOption("Bitrate", 9999999);
|
menu.selectOption("Bitrate", 9999999);
|
||||||
menu.setOk("Download");
|
menu.setOk(container.context.getString(R.string.download));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
targetBitrate = 1;
|
targetBitrate = 1;
|
||||||
menu.selectOption("Bitrate", 1);
|
menu.selectOption("Bitrate", 1);
|
||||||
menu.setOk("Download");
|
menu.setOk(container.context.getString(R.string.download));
|
||||||
}
|
}
|
||||||
|
|
||||||
menu.onOK.subscribe {
|
menu.onOK.subscribe {
|
||||||
@@ -296,7 +591,7 @@ class UISlideOverlays {
|
|||||||
val dp70 = 70.dp(container.context.resources);
|
val dp70 = 70.dp(container.context.resources);
|
||||||
val dp15 = 15.dp(container.context.resources);
|
val dp15 = 15.dp(container.context.resources);
|
||||||
val overlay = SlideUpMenuOverlay(container.context, container, text, null, true, listOf(
|
val overlay = SlideUpMenuOverlay(container.context, container, text, null, true, listOf(
|
||||||
Loader(container.context, true, dp70).apply {
|
LoaderView(container.context, true, dp70).apply {
|
||||||
this.setPadding(0, dp15, 0, dp15);
|
this.setPadding(0, dp15, 0, dp15);
|
||||||
}
|
}
|
||||||
), true);
|
), true);
|
||||||
@@ -304,14 +599,83 @@ class UISlideOverlays {
|
|||||||
return overlay;
|
return overlay;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showVideoOptionsOverlay(video: IPlatformVideo, container: ViewGroup, onVideoHidden: (()->Unit)? = null): SlideUpMenuOverlay {
|
fun showCreateSubscriptionGroup(container: ViewGroup, initialChannel: IPlatformChannel? = null, onCreate: ((String) -> Unit)? = null): SlideUpMenuOverlay {
|
||||||
|
val nameInput = SlideUpMenuTextInput(container.context, container.context.getString(R.string.name));
|
||||||
|
val addSubGroupOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.create_new_subgroup), container.context.getString(R.string.ok), false, nameInput);
|
||||||
|
|
||||||
|
addSubGroupOverlay.onOK.subscribe {
|
||||||
|
val text = nameInput.text;
|
||||||
|
if (text.isBlank()) {
|
||||||
|
return@subscribe;
|
||||||
|
}
|
||||||
|
|
||||||
|
addSubGroupOverlay.hide();
|
||||||
|
nameInput.deactivate();
|
||||||
|
nameInput.clear();
|
||||||
|
if(onCreate == null)
|
||||||
|
{
|
||||||
|
//TODO: Do this better, temp
|
||||||
|
StateApp.instance.contextOrNull?.let {
|
||||||
|
if(it is MainActivity) {
|
||||||
|
val subGroup = SubscriptionGroup(text);
|
||||||
|
if(initialChannel != null) {
|
||||||
|
subGroup.urls.add(initialChannel.url);
|
||||||
|
if(initialChannel.thumbnail != null)
|
||||||
|
subGroup.image = ImageVariable(initialChannel.thumbnail);
|
||||||
|
}
|
||||||
|
it.navigate(it.getFragment<SubscriptionGroupFragment>(), subGroup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
onCreate(text)
|
||||||
|
};
|
||||||
|
|
||||||
|
addSubGroupOverlay.onCancel.subscribe {
|
||||||
|
nameInput.deactivate();
|
||||||
|
nameInput.clear();
|
||||||
|
};
|
||||||
|
|
||||||
|
addSubGroupOverlay.show();
|
||||||
|
nameInput.activate();
|
||||||
|
|
||||||
|
return addSubGroupOverlay
|
||||||
|
}
|
||||||
|
fun showCreatePlaylistOverlay(container: ViewGroup, onCreate: (String) -> Unit): SlideUpMenuOverlay {
|
||||||
|
val nameInput = SlideUpMenuTextInput(container.context, container.context.getString(R.string.name));
|
||||||
|
val addPlaylistOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.create_new_playlist), container.context.getString(R.string.ok), false, nameInput);
|
||||||
|
|
||||||
|
addPlaylistOverlay.onOK.subscribe {
|
||||||
|
val text = nameInput.text;
|
||||||
|
if (text.isBlank()) {
|
||||||
|
return@subscribe;
|
||||||
|
}
|
||||||
|
|
||||||
|
addPlaylistOverlay.hide();
|
||||||
|
nameInput.deactivate();
|
||||||
|
nameInput.clear();
|
||||||
|
onCreate(text)
|
||||||
|
};
|
||||||
|
|
||||||
|
addPlaylistOverlay.onCancel.subscribe {
|
||||||
|
nameInput.deactivate();
|
||||||
|
nameInput.clear();
|
||||||
|
};
|
||||||
|
|
||||||
|
addPlaylistOverlay.show();
|
||||||
|
nameInput.activate();
|
||||||
|
|
||||||
|
return addPlaylistOverlay
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showVideoOptionsOverlay(video: IPlatformVideo, container: ViewGroup, vararg actions: SlideUpMenuItem): SlideUpMenuOverlay {
|
||||||
val items = arrayListOf<View>();
|
val items = arrayListOf<View>();
|
||||||
val lastUpdated = StatePlaylists.instance.getLastUpdatedPlaylist();
|
val lastUpdated = StatePlaylists.instance.getLastUpdatedPlaylist();
|
||||||
|
|
||||||
if (lastUpdated != null) {
|
if (lastUpdated != null) {
|
||||||
items.add(
|
items.add(
|
||||||
SlideUpMenuGroup(container.context, "Recently Used Playlist", "recentlyusedplaylist",
|
SlideUpMenuGroup(container.context, container.context.getString(R.string.recently_used_playlist), "recentlyusedplaylist",
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, lastUpdated.name, "${lastUpdated.videos.size} videos", "",
|
SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, lastUpdated.name, "${lastUpdated.videos.size} " + container.context.getString(R.string.videos), "",
|
||||||
{
|
{
|
||||||
StatePlaylists.instance.addToPlaylist(lastUpdated.id, video);
|
StatePlaylists.instance.addToPlaylist(lastUpdated.id, video);
|
||||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||||
@@ -322,23 +686,43 @@ class UISlideOverlays {
|
|||||||
val allPlaylists = StatePlaylists.instance.getPlaylists();
|
val allPlaylists = StatePlaylists.instance.getPlaylists();
|
||||||
val queue = StatePlayer.instance.getQueue();
|
val queue = StatePlayer.instance.getQueue();
|
||||||
val watchLater = StatePlaylists.instance.getWatchLater();
|
val watchLater = StatePlaylists.instance.getWatchLater();
|
||||||
items.add(SlideUpMenuGroup(container.context, "Actions", "actions",
|
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.actions), "actions",
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_visibility_off, "Hide", "Hide from Home", "hide",
|
(listOf(
|
||||||
{ StateMeta.instance.addHiddenVideo(video.url); onVideoHidden?.invoke() }),
|
SlideUpMenuItem(container.context, R.drawable.ic_download, container.context.getString(R.string.download), container.context.getString(R.string.download_the_video), "download", {
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_download, "Download", "Download the video", "download",
|
showDownloadVideoOverlay(video, container, true);
|
||||||
{ showDownloadVideoOverlay(video, container, true); }, false)
|
}, false),
|
||||||
))
|
SlideUpMenuItem(container.context, R.drawable.ic_share, container.context.getString(R.string.share), "Share the video", "share", {
|
||||||
|
val url = if(video.shareUrl.isNotEmpty()) video.shareUrl else video.url;
|
||||||
|
container.context.startActivity(Intent.createChooser(Intent().apply {
|
||||||
|
action = Intent.ACTION_SEND;
|
||||||
|
putExtra(Intent.EXTRA_TEXT, url);
|
||||||
|
type = "text/plain";
|
||||||
|
}, null));
|
||||||
|
}, false),
|
||||||
|
SlideUpMenuItem(container.context, R.drawable.ic_visibility_off, container.context.getString(R.string.hide_creator_from_home), "", "hide_creator", {
|
||||||
|
StateMeta.instance.addHiddenCreator(video.author.url);
|
||||||
|
UIDialogs.toast(container.context, "[${video.author.name}] hidden, you may need to reload home");
|
||||||
|
}))
|
||||||
|
+ actions)
|
||||||
|
));
|
||||||
items.add(
|
items.add(
|
||||||
SlideUpMenuGroup(container.context, "Add To", "addto",
|
SlideUpMenuGroup(container.context, container.context.getString(R.string.add_to), "addto",
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_queue_add, "Add to Queue", "${queue.size} videos", "queue",
|
SlideUpMenuItem(container.context, R.drawable.ic_queue_add, container.context.getString(R.string.add_to_queue), "${queue.size} " + container.context.getString(R.string.videos), "queue",
|
||||||
{ StatePlayer.instance.addToQueue(video); }),
|
{ StatePlayer.instance.addToQueue(video); }),
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_watchlist_add, "Add to " + StatePlayer.TYPE_WATCHLATER + "", "${watchLater.size} videos", "watch later",
|
SlideUpMenuItem(container.context, R.drawable.ic_watchlist_add, "${container.context.getString(R.string.add_to)} " + StatePlayer.TYPE_WATCHLATER + "", "${watchLater.size} " + container.context.getString(R.string.videos), "watch later",
|
||||||
{ StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video)); })
|
{ StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video)); })
|
||||||
));
|
));
|
||||||
|
|
||||||
val playlistItems = arrayListOf<SlideUpMenuItem>();
|
val playlistItems = arrayListOf<SlideUpMenuItem>();
|
||||||
|
playlistItems.add(SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, container.context.getString(R.string.new_playlist), container.context.getString(R.string.add_to_new_playlist), "add_to_new_playlist", {
|
||||||
|
showCreatePlaylistOverlay(container) {
|
||||||
|
val playlist = Playlist(it, arrayListOf(SerializedPlatformVideo.fromVideo(video)));
|
||||||
|
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
|
||||||
|
};
|
||||||
|
}, false))
|
||||||
|
|
||||||
for (playlist in allPlaylists) {
|
for (playlist in allPlaylists) {
|
||||||
playlistItems.add(SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, "Add to " + playlist.name + "", "${playlist.videos.size} videos", "",
|
playlistItems.add(SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, "${container.context.getString(R.string.add_to)} " + playlist.name + "", "${playlist.videos.size} " + container.context.getString(R.string.videos), "",
|
||||||
{
|
{
|
||||||
StatePlaylists.instance.addToPlaylist(playlist.id, video);
|
StatePlaylists.instance.addToPlaylist(playlist.id, video);
|
||||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||||
@@ -346,13 +730,13 @@ class UISlideOverlays {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(playlistItems.size > 0)
|
if(playlistItems.size > 0)
|
||||||
items.add(SlideUpMenuGroup(container.context, "Playlists", "", playlistItems));
|
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.playlists), "", playlistItems));
|
||||||
|
|
||||||
return SlideUpMenuOverlay(container.context, container, "Video Options", null, true, items).apply { show() };
|
return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.video_options), null, true, items).apply { show() };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun showAddToOverlay(video: IPlatformVideo, container: ViewGroup): SlideUpMenuOverlay {
|
fun showAddToOverlay(video: IPlatformVideo, container: ViewGroup, slideUpMenuOverlayUpdated: (SlideUpMenuOverlay) -> Unit): SlideUpMenuOverlay {
|
||||||
|
|
||||||
val items = arrayListOf<View>();
|
val items = arrayListOf<View>();
|
||||||
|
|
||||||
@@ -360,8 +744,8 @@ class UISlideOverlays {
|
|||||||
|
|
||||||
if (lastUpdated != null) {
|
if (lastUpdated != null) {
|
||||||
items.add(
|
items.add(
|
||||||
SlideUpMenuGroup(container.context, "Recently Used Playlist", "recentlyusedplaylist",
|
SlideUpMenuGroup(container.context, container.context.getString(R.string.recently_used_playlist), "recentlyusedplaylist",
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, lastUpdated.name, "${lastUpdated.videos.size} videos", "",
|
SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, lastUpdated.name, "${lastUpdated.videos.size} " + container.context.getString(R.string.videos), "",
|
||||||
{
|
{
|
||||||
StatePlaylists.instance.addToPlaylist(lastUpdated.id, video);
|
StatePlaylists.instance.addToPlaylist(lastUpdated.id, video);
|
||||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||||
@@ -373,18 +757,25 @@ 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(container.context, "Other", "other",
|
SlideUpMenuGroup(container.context, container.context.getString(R.string.other), "other",
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_queue_add, "Queue", "${queue.size} videos", "queue",
|
SlideUpMenuItem(container.context, R.drawable.ic_queue_add, container.context.getString(R.string.queue), "${queue.size} " + container.context.getString(R.string.videos), "queue",
|
||||||
{ StatePlayer.instance.addToQueue(video); }),
|
{ StatePlayer.instance.addToQueue(video); }),
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_watchlist_add, StatePlayer.TYPE_WATCHLATER, "${watchLater.size} videos", "watch later",
|
SlideUpMenuItem(container.context, R.drawable.ic_watchlist_add, StatePlayer.TYPE_WATCHLATER, "${watchLater.size} " + container.context.getString(R.string.videos), "watch later",
|
||||||
{ StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video)); }),
|
{ StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video)); }),
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_download, "Download", "Download the video", "download",
|
SlideUpMenuItem(container.context, R.drawable.ic_download, container.context.getString(R.string.download), container.context.getString(R.string.download_the_video), container.context.getString(R.string.download),
|
||||||
{ showDownloadVideoOverlay(video, container, true); }, false))
|
{ showDownloadVideoOverlay(video, container, true); }, false))
|
||||||
);
|
);
|
||||||
|
|
||||||
val playlistItems = arrayListOf<SlideUpMenuItem>();
|
val playlistItems = arrayListOf<SlideUpMenuItem>();
|
||||||
|
playlistItems.add(SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, container.context.getString(R.string.new_playlist), container.context.getString(R.string.add_to_new_playlist), "add_to_new_playlist", {
|
||||||
|
slideUpMenuOverlayUpdated(showCreatePlaylistOverlay(container) {
|
||||||
|
val playlist = Playlist(it, arrayListOf(SerializedPlatformVideo.fromVideo(video)));
|
||||||
|
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
|
||||||
|
});
|
||||||
|
}, false))
|
||||||
|
|
||||||
for (playlist in allPlaylists) {
|
for (playlist in allPlaylists) {
|
||||||
playlistItems.add(SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, playlist.name, "${playlist.videos.size} videos", "",
|
playlistItems.add(SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, playlist.name, "${playlist.videos.size} " + container.context.getString(R.string.videos), "",
|
||||||
{
|
{
|
||||||
StatePlaylists.instance.addToPlaylist(playlist.id, video);
|
StatePlaylists.instance.addToPlaylist(playlist.id, video);
|
||||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||||
@@ -392,28 +783,29 @@ class UISlideOverlays {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(playlistItems.size > 0)
|
if(playlistItems.size > 0)
|
||||||
items.add(SlideUpMenuGroup(container.context, "Playlists", "", playlistItems));
|
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.playlists), "", playlistItems));
|
||||||
|
|
||||||
return SlideUpMenuOverlay(container.context, container, "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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun showMoreButtonOverlay(container: ViewGroup, buttonGroup: RoundButtonGroup, ignoreTags: List<Any> = listOf(), onPinnedbuttons: ((List<RoundButton>)->Unit)? = null): SlideUpMenuOverlay {
|
fun showMoreButtonOverlay(container: ViewGroup, buttonGroup: RoundButtonGroup, ignoreTags: List<Any> = listOf(), invokeParents: Boolean = true, onPinnedbuttons: ((List<RoundButton>)->Unit)? = null): SlideUpMenuOverlay {
|
||||||
val visible = buttonGroup.getVisibleButtons().filter { !ignoreTags.contains(it.tagRef) };
|
val visible = buttonGroup.getVisibleButtons().filter { !ignoreTags.contains(it.tagRef) };
|
||||||
val hidden = buttonGroup.getInvisibleButtons().filter { !ignoreTags.contains(it.tagRef) };
|
val hidden = buttonGroup.getInvisibleButtons().filter { !ignoreTags.contains(it.tagRef) };
|
||||||
|
|
||||||
val views = arrayOf(hidden
|
val views = arrayOf(
|
||||||
.map { btn -> SlideUpMenuItem(container.context, btn.iconResource, btn.text.text.toString(), "", "", {
|
hidden
|
||||||
btn.handler?.invoke(btn);
|
.map { btn -> SlideUpMenuItem(container.context, btn.iconResource, btn.text.text.toString(), "", "", {
|
||||||
}, true) as View }.toTypedArray() ?: arrayOf(),
|
btn.handler?.invoke(btn);
|
||||||
arrayOf(SlideUpMenuItem(container.context, R.drawable.ic_pin, "Change Pins", "Decide which buttons should be pinned", "", {
|
}, invokeParents) as View }.toTypedArray(),
|
||||||
showOrderOverlay(container, "Select your pins in order", (visible + hidden).map { Pair(it.text.text.toString(), it.tagRef!!) }) {
|
arrayOf(SlideUpMenuItem(container.context, R.drawable.ic_pin, container.context.getString(R.string.change_pins), container.context.getString(R.string.decide_which_buttons_should_be_pinned), "", {
|
||||||
|
showOrderOverlay(container, container.context.getString(R.string.select_your_pins_in_order), (visible + hidden).map { Pair(it.text.text.toString(), it.tagRef!!) }) {
|
||||||
val selected = it
|
val selected = it
|
||||||
.map { x -> visible.find { it.tagRef == x } ?: hidden.find { it.tagRef == x } }
|
.map { x -> visible.find { it.tagRef == x } ?: hidden.find { it.tagRef == x } }
|
||||||
.filter { it != null }
|
.filter { it != null }
|
||||||
@@ -425,7 +817,7 @@ class UISlideOverlays {
|
|||||||
}, false))
|
}, false))
|
||||||
).flatten().toTypedArray();
|
).flatten().toTypedArray();
|
||||||
|
|
||||||
return SlideUpMenuOverlay(container.context, container, "More Options", null, true, *views).apply { show() };
|
return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.more_options), null, true, *views).apply { show() };
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showOrderOverlay(container: ViewGroup, title: String, options: List<Pair<String, Any>>, onOrdered: (List<Any>)->Unit) {
|
fun showOrderOverlay(container: ViewGroup, title: String, options: List<Pair<String, Any>>, onOrdered: (List<Any>)->Unit) {
|
||||||
@@ -433,7 +825,7 @@ class UISlideOverlays {
|
|||||||
|
|
||||||
var overlay: SlideUpMenuOverlay? = null;
|
var overlay: SlideUpMenuOverlay? = null;
|
||||||
|
|
||||||
overlay = SlideUpMenuOverlay(container.context, container, title, "Save", true,
|
overlay = SlideUpMenuOverlay(container.context, container, title, container.context.getString(R.string.save), true,
|
||||||
options.map { SlideUpMenuItem(container.context, R.drawable.ic_move_up, it.first, "", it.second, {
|
options.map { SlideUpMenuItem(container.context, R.drawable.ic_move_up, it.first, "", it.second, {
|
||||||
if(overlay!!.selectOption(null, it.second, true, true)) {
|
if(overlay!!.selectOption(null, it.second, true, true)) {
|
||||||
if(!selection.contains(it.second))
|
if(!selection.contains(it.second))
|
||||||
|
|||||||
@@ -68,6 +68,12 @@ fun ensureNotMainThread() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val _regexUrl = Regex("https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b([-a-zA-Z0-9()@:%_\\+.~#?&\\/\\/=]*)");
|
||||||
|
fun String.isHttpUrl(): Boolean {
|
||||||
|
return _regexUrl.matchEntire(this) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private val _regexHexColor = Regex("(#[a-fA-F0-9]{8})|(#[a-fA-F0-9]{6})|(#[a-fA-F0-9]{3})");
|
private val _regexHexColor = Regex("(#[a-fA-F0-9]{8})|(#[a-fA-F0-9]{6})|(#[a-fA-F0-9]{3})");
|
||||||
fun String.isHexColor(): Boolean {
|
fun String.isHexColor(): Boolean {
|
||||||
return _regexHexColor.matches(this);
|
return _regexHexColor.matches(this);
|
||||||
@@ -137,6 +143,7 @@ fun InputStream.copyToOutputStream(inputStreamLength: Long, outputStream: Output
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
fun Activity.setNavigationBarColorAndIcons() {
|
fun Activity.setNavigationBarColorAndIcons() {
|
||||||
window.navigationBarColor = ContextCompat.getColor(this, android.R.color.black);
|
window.navigationBarColor = ContextCompat.getColor(this, android.R.color.black);
|
||||||
|
|
||||||
@@ -158,9 +165,7 @@ fun Int.sp(resources: Resources): Int {
|
|||||||
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, this.toFloat(), resources.displayMetrics).toInt()
|
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, this.toFloat(), resources.displayMetrics).toInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun File.share(context: Context) {
|
fun DocumentFile.share(context: Context) {
|
||||||
val uri = FileProvider.getUriForFile(context, context.resources.getString(R.string.authority), this);
|
|
||||||
|
|
||||||
val shareIntent = Intent();
|
val shareIntent = Intent();
|
||||||
shareIntent.action = Intent.ACTION_SEND;
|
shareIntent.action = Intent.ACTION_SEND;
|
||||||
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||||
|
|||||||
@@ -1,16 +1,25 @@
|
|||||||
package com.futo.platformplayer.activities
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.drawable.Animatable
|
import android.graphics.drawable.Animatable
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.*
|
import android.widget.ImageButton
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.ScrollView
|
||||||
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.platformplayer.states.StatePlugins
|
import com.futo.platformplayer.states.StatePlugins
|
||||||
@@ -29,8 +38,10 @@ class AddSourceActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private lateinit var _sourceHeader: SourceHeaderView;
|
private lateinit var _sourceHeader: SourceHeaderView;
|
||||||
|
|
||||||
|
|
||||||
private lateinit var _sourcePermissions: LinearLayout;
|
private lateinit var _sourcePermissions: LinearLayout;
|
||||||
private lateinit var _sourceWarnings: LinearLayout;
|
private lateinit var _sourceWarnings: LinearLayout;
|
||||||
|
private lateinit var _sourceWarningsContainer: LinearLayout;
|
||||||
|
|
||||||
private lateinit var _container: ScrollView;
|
private lateinit var _container: ScrollView;
|
||||||
private lateinit var _loader: ImageView;
|
private lateinit var _loader: ImageView;
|
||||||
@@ -45,6 +56,10 @@ class AddSourceActivity : AppCompatActivity() {
|
|||||||
private var _config: SourcePluginConfig? = null;
|
private var _config: SourcePluginConfig? = null;
|
||||||
private var _script: String? = null;
|
private var _script: String? = null;
|
||||||
|
|
||||||
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
|
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
@@ -67,6 +82,7 @@ class AddSourceActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
_sourcePermissions = findViewById(R.id.source_permissions);
|
_sourcePermissions = findViewById(R.id.source_permissions);
|
||||||
_sourceWarnings = findViewById(R.id.source_warnings);
|
_sourceWarnings = findViewById(R.id.source_warnings);
|
||||||
|
_sourceWarningsContainer = findViewById(R.id.container_source_warnings);
|
||||||
|
|
||||||
_container = findViewById(R.id.configContainer);
|
_container = findViewById(R.id.configContainer);
|
||||||
_loader = findViewById(R.id.loader);
|
_loader = findViewById(R.id.loader);
|
||||||
@@ -96,8 +112,8 @@ class AddSourceActivity : AppCompatActivity() {
|
|||||||
var url = intent?.dataString;
|
var url = intent?.dataString;
|
||||||
|
|
||||||
if(url == null)
|
if(url == null)
|
||||||
UIDialogs.showDialog(this, R.drawable.ic_error, "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("Ok", { finish() }, UIDialogs.ActionStyle.PRIMARY));
|
0, UIDialogs.Action(getString(R.string.ok), { finish() }, UIDialogs.ActionStyle.PRIMARY));
|
||||||
else {
|
else {
|
||||||
if(url.startsWith("vfuto://"))
|
if(url.startsWith("vfuto://"))
|
||||||
url = "https://" + url.substring("vfuto://".length);
|
url = "https://" + url.substring("vfuto://".length);
|
||||||
@@ -129,14 +145,14 @@ class AddSourceActivity : AppCompatActivity() {
|
|||||||
Logger.e(TAG, "Failed decode config", ex);
|
Logger.e(TAG, "Failed decode config", ex);
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
UIDialogs.showDialog(this@AddSourceActivity, R.drawable.ic_error,
|
UIDialogs.showDialog(this@AddSourceActivity, R.drawable.ic_error,
|
||||||
"Invalid Config Format", null, null,
|
getString(R.string.invalid_config_format), null, null,
|
||||||
0, UIDialogs.Action("Ok", { finish() }, UIDialogs.ActionStyle.PRIMARY));
|
0, UIDialogs.Action("Ok", { finish() }, UIDialogs.ActionStyle.PRIMARY));
|
||||||
};
|
};
|
||||||
return@launch;
|
return@launch;
|
||||||
} catch(ex: Exception) {
|
} catch(ex: Exception) {
|
||||||
Logger.e(TAG, "Failed fetch config", ex);
|
Logger.e(TAG, "Failed fetch config", ex);
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
UIDialogs.showGeneralErrorDialog(this@AddSourceActivity, "Failed to fetch configuration", ex);
|
UIDialogs.showGeneralErrorDialog(this@AddSourceActivity, getString(R.string.failed_to_fetch_configuration), ex);
|
||||||
};
|
};
|
||||||
return@launch;
|
return@launch;
|
||||||
}
|
}
|
||||||
@@ -152,7 +168,7 @@ class AddSourceActivity : AppCompatActivity() {
|
|||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
Logger.e(TAG, "Failed fetch script", ex);
|
Logger.e(TAG, "Failed fetch script", ex);
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
UIDialogs.showGeneralErrorDialog(this@AddSourceActivity, "Failed to fetch script", ex);
|
UIDialogs.showGeneralErrorDialog(this@AddSourceActivity, getString(R.string.failed_to_fetch_script), ex);
|
||||||
};
|
};
|
||||||
return@launch;
|
return@launch;
|
||||||
}
|
}
|
||||||
@@ -175,8 +191,8 @@ class AddSourceActivity : AppCompatActivity() {
|
|||||||
_sourcePermissions.addView(
|
_sourcePermissions.addView(
|
||||||
SourceInfoView(this,
|
SourceInfoView(this,
|
||||||
R.drawable.ic_language,
|
R.drawable.ic_language,
|
||||||
"URL Access",
|
getString(R.string.url_access),
|
||||||
"The plugin will have access to the following domains",
|
getString(R.string.the_plugin_will_have_access_to_the_following_domains),
|
||||||
config.allowUrls, true)
|
config.allowUrls, true)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -184,28 +200,37 @@ class AddSourceActivity : AppCompatActivity() {
|
|||||||
_sourcePermissions.addView(
|
_sourcePermissions.addView(
|
||||||
SourceInfoView(this,
|
SourceInfoView(this,
|
||||||
R.drawable.ic_code,
|
R.drawable.ic_code,
|
||||||
"Eval Access",
|
getString(R.string.eval_access),
|
||||||
"The plugin will have access to eval capability (remote injection)",
|
getString(R.string.the_plugin_will_have_access_to_eval_capability_remote_injection),
|
||||||
config.allowUrls, true)
|
config.allowUrls, true)
|
||||||
)
|
)
|
||||||
|
|
||||||
val pastelRed = resources.getColor(R.color.pastel_red);
|
val pastelRed = ContextCompat.getColor(this, R.color.pastel_red);
|
||||||
|
|
||||||
for(warning in config.getWarnings(script))
|
val warnings = config.getWarnings(script);
|
||||||
|
for(warning in warnings)
|
||||||
_sourceWarnings.addView(
|
_sourceWarnings.addView(
|
||||||
SourceInfoView(this,
|
SourceInfoView(this,
|
||||||
R.drawable.ic_security_pred,
|
R.drawable.ic_security_pred,
|
||||||
warning.first,
|
warning.first,
|
||||||
warning.second)
|
warning.second)
|
||||||
.withDescriptionColor(pastelRed));
|
.withDescriptionColor(pastelRed));
|
||||||
|
_sourceWarningsContainer.isVisible = warnings.isNotEmpty();
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
fun install(config: SourcePluginConfig, script: String) {
|
fun install(config: SourcePluginConfig, script: String) {
|
||||||
|
val isNew = !StatePlatform.instance.getAvailableClients().any { it.id == config.id };
|
||||||
StatePlugins.instance.installPlugin(this, lifecycleScope, config, script) {
|
StatePlugins.instance.installPlugin(this, lifecycleScope, config, script) {
|
||||||
if(it)
|
if(it) {
|
||||||
|
StatePlatform.instance.clearUpdateAvailable(config)
|
||||||
|
if(isNew)
|
||||||
|
lifecycleScope.launch {
|
||||||
|
StatePlatform.instance.enableClient(listOf(config.id));
|
||||||
|
}
|
||||||
backToSources();
|
backToSources();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.futo.platformplayer.activities
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@@ -7,22 +8,24 @@ import android.widget.*
|
|||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.views.buttons.BigButton
|
import com.futo.platformplayer.views.buttons.BigButton
|
||||||
import com.google.zxing.integration.android.IntentIntegrator
|
import com.google.zxing.integration.android.IntentIntegrator
|
||||||
import com.journeyapps.barcodescanner.CaptureActivity
|
|
||||||
|
|
||||||
class AddSourceOptionsActivity : AppCompatActivity() {
|
class AddSourceOptionsActivity : AppCompatActivity() {
|
||||||
lateinit var _buttonBack: ImageButton;
|
lateinit var _buttonBack: ImageButton;
|
||||||
|
|
||||||
lateinit var _buttonQR: BigButton;
|
lateinit var _buttonQR: BigButton;
|
||||||
|
lateinit var _buttonBrowse: BigButton;
|
||||||
lateinit var _buttonURL: BigButton;
|
lateinit var _buttonURL: BigButton;
|
||||||
|
lateinit var _buttonPlugins: BigButton;
|
||||||
|
|
||||||
private val _qrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
private val _qrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
|
val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
|
||||||
scanResult?.let {
|
scanResult?.let {
|
||||||
val content = it.contents
|
val content = it.contents
|
||||||
if (content == null) {
|
if (content == null) {
|
||||||
UIDialogs.toast(this, "Failed to scan QR code")
|
UIDialogs.toast(this, getString(R.string.failed_to_scan_qr_code))
|
||||||
return@let
|
return@let
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,7 +34,7 @@ class AddSourceOptionsActivity : AppCompatActivity() {
|
|||||||
} else if (content.startsWith("grayjay://plugin/")) {
|
} else if (content.startsWith("grayjay://plugin/")) {
|
||||||
content.substring("grayjay://plugin/".length)
|
content.substring("grayjay://plugin/".length)
|
||||||
} else {
|
} else {
|
||||||
UIDialogs.toast(this, "Not a plugin URL")
|
UIDialogs.toast(this, getString(R.string.not_a_plugin_url))
|
||||||
return@let;
|
return@let;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,6 +45,10 @@ class AddSourceOptionsActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
|
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_add_source_options);
|
setContentView(R.layout.activity_add_source_options);
|
||||||
@@ -50,7 +57,9 @@ class AddSourceOptionsActivity : AppCompatActivity() {
|
|||||||
_buttonBack = findViewById(R.id.button_back);
|
_buttonBack = findViewById(R.id.button_back);
|
||||||
|
|
||||||
_buttonQR = findViewById(R.id.option_qr);
|
_buttonQR = findViewById(R.id.option_qr);
|
||||||
|
_buttonBrowse = findViewById(R.id.option_browse);
|
||||||
_buttonURL = findViewById(R.id.option_url);
|
_buttonURL = findViewById(R.id.option_url);
|
||||||
|
_buttonPlugins = findViewById(R.id.option_plugins);
|
||||||
|
|
||||||
_buttonBack.setOnClickListener {
|
_buttonBack.setOnClickListener {
|
||||||
finish();
|
finish();
|
||||||
@@ -59,7 +68,7 @@ class AddSourceOptionsActivity : AppCompatActivity() {
|
|||||||
_buttonQR.onClick.subscribe {
|
_buttonQR.onClick.subscribe {
|
||||||
val integrator = IntentIntegrator(this);
|
val integrator = IntentIntegrator(this);
|
||||||
integrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
|
integrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
|
||||||
integrator.setPrompt("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)
|
||||||
@@ -67,14 +76,12 @@ class AddSourceOptionsActivity : AppCompatActivity() {
|
|||||||
integrator.setCaptureActivity(QRCaptureActivity::class.java);
|
integrator.setCaptureActivity(QRCaptureActivity::class.java);
|
||||||
_qrCodeResultLauncher.launch(integrator.createScanIntent())
|
_qrCodeResultLauncher.launch(integrator.createScanIntent())
|
||||||
}
|
}
|
||||||
|
_buttonBrowse.onClick.subscribe {
|
||||||
|
startActivity(MainActivity.getTabIntent(this, "BROWSE_PLUGINS"));
|
||||||
|
}
|
||||||
|
|
||||||
_buttonURL.onClick.subscribe {
|
_buttonURL.onClick.subscribe {
|
||||||
UIDialogs.toast(this, "Not implemented yet..");
|
UIDialogs.toast(this, getString(R.string.not_implemented_yet));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class QRCaptureActivity: CaptureActivity() {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -18,6 +18,7 @@ import com.futo.platformplayer.engine.IV8PluginConfig
|
|||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.others.CaptchaWebViewClient
|
import com.futo.platformplayer.others.CaptchaWebViewClient
|
||||||
import com.futo.platformplayer.others.LoginWebViewClient
|
import com.futo.platformplayer.others.LoginWebViewClient
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
@@ -31,6 +32,10 @@ class CaptchaActivity : AppCompatActivity() {
|
|||||||
private lateinit var _webView: WebView;
|
private lateinit var _webView: WebView;
|
||||||
private lateinit var _buttonClose: Button;
|
private lateinit var _buttonClose: Button;
|
||||||
|
|
||||||
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
|
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_captcha);
|
setContentView(R.layout.activity_captcha);
|
||||||
|
|||||||
@@ -1,17 +1,24 @@
|
|||||||
package com.futo.platformplayer.activities
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
import com.futo.platformplayer.views.fields.FieldForm
|
import com.futo.platformplayer.views.fields.FieldForm
|
||||||
|
import com.futo.platformplayer.views.fields.IField
|
||||||
|
|
||||||
class DeveloperActivity : AppCompatActivity() {
|
class DeveloperActivity : AppCompatActivity() {
|
||||||
private lateinit var _form: FieldForm;
|
private lateinit var _form: FieldForm;
|
||||||
private lateinit var _buttonBack: ImageButton;
|
private lateinit var _buttonBack: ImageButton;
|
||||||
|
|
||||||
|
fun getField(id: String): IField? {
|
||||||
|
return _form.findField(id);
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
DeveloperActivity._lastActivity = this;
|
||||||
setContentView(R.layout.activity_dev);
|
setContentView(R.layout.activity_dev);
|
||||||
setNavigationBarColorAndIcons();
|
setNavigationBarColorAndIcons();
|
||||||
|
|
||||||
@@ -19,7 +26,7 @@ class DeveloperActivity : AppCompatActivity() {
|
|||||||
_form = findViewById(R.id.settings_form);
|
_form = findViewById(R.id.settings_form);
|
||||||
|
|
||||||
_form.fromObject(SettingsDev.instance);
|
_form.fromObject(SettingsDev.instance);
|
||||||
_form.onChanged.subscribe { field, value ->
|
_form.onChanged.subscribe { _, _ ->
|
||||||
_form.setObjectValues();
|
_form.setObjectValues();
|
||||||
SettingsDev.instance.save();
|
SettingsDev.instance.save();
|
||||||
};
|
};
|
||||||
@@ -33,4 +40,19 @@ class DeveloperActivity : AppCompatActivity() {
|
|||||||
super.finish()
|
super.finish()
|
||||||
overridePendingTransition(R.anim.slide_lighten, R.anim.slide_out_up)
|
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,16 +1,22 @@
|
|||||||
package com.futo.platformplayer.activities
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.BuildConfig
|
||||||
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.logging.LogLevel
|
import com.futo.platformplayer.logging.LogLevel
|
||||||
import com.futo.platformplayer.logging.Logging
|
import com.futo.platformplayer.logging.Logging
|
||||||
|
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.futo.platformplayer.states.StateUpdate
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -24,9 +30,14 @@ class ExceptionActivity : AppCompatActivity() {
|
|||||||
private lateinit var _buttonSubmit: LinearLayout;
|
private lateinit var _buttonSubmit: LinearLayout;
|
||||||
private lateinit var _buttonRestart: LinearLayout;
|
private lateinit var _buttonRestart: LinearLayout;
|
||||||
private lateinit var _buttonClose: LinearLayout;
|
private lateinit var _buttonClose: LinearLayout;
|
||||||
|
private lateinit var _buttonCheckForUpdates: LinearLayout;
|
||||||
private var _file: File? = null;
|
private var _file: File? = null;
|
||||||
private var _submitted = false;
|
private var _submitted = false;
|
||||||
|
|
||||||
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
|
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_exception);
|
setContentView(R.layout.activity_exception);
|
||||||
@@ -37,9 +48,10 @@ class ExceptionActivity : AppCompatActivity() {
|
|||||||
_buttonSubmit = findViewById(R.id.button_submit);
|
_buttonSubmit = findViewById(R.id.button_submit);
|
||||||
_buttonRestart = findViewById(R.id.button_restart);
|
_buttonRestart = findViewById(R.id.button_restart);
|
||||||
_buttonClose = findViewById(R.id.button_close);
|
_buttonClose = findViewById(R.id.button_close);
|
||||||
|
_buttonCheckForUpdates = findViewById(R.id.button_check_for_updates);
|
||||||
|
|
||||||
val context = intent.getStringExtra(EXTRA_CONTEXT) ?: "Unknown Context";
|
val context = intent.getStringExtra(EXTRA_CONTEXT) ?: getString(R.string.unknown_context);
|
||||||
val stack = intent.getStringExtra(EXTRA_STACK) ?: "Something went wrong... missing stack trace?";
|
val stack = intent.getStringExtra(EXTRA_STACK) ?: getString(R.string.something_went_wrong_missing_stack_trace);
|
||||||
|
|
||||||
val exceptionString = "Version information (version_name = ${BuildConfig.VERSION_NAME}, version_code = ${BuildConfig.VERSION_CODE}, flavor = ${BuildConfig.FLAVOR}, build_type = ${BuildConfig.BUILD_TYPE})\n" +
|
val exceptionString = "Version information (version_name = ${BuildConfig.VERSION_NAME}, version_code = ${BuildConfig.VERSION_CODE}, flavor = ${BuildConfig.FLAVOR}, build_type = ${BuildConfig.BUILD_TYPE})\n" +
|
||||||
"Device information (brand= ${Build.BRAND}, manufacturer = ${Build.MANUFACTURER}, device = ${Build.DEVICE}, version-sdk = ${Build.VERSION.SDK_INT}, version-os = ${Build.VERSION.BASE_OS})\n\n" +
|
"Device information (brand= ${Build.BRAND}, manufacturer = ${Build.MANUFACTURER}, device = ${Build.DEVICE}, version-sdk = ${Build.VERSION.SDK_INT}, version-os = ${Build.VERSION.BASE_OS})\n\n" +
|
||||||
@@ -75,17 +87,28 @@ class ExceptionActivity : AppCompatActivity() {
|
|||||||
_buttonClose.setOnClickListener {
|
_buttonClose.setOnClickListener {
|
||||||
finish();
|
finish();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!BuildConfig.IS_PLAYSTORE_BUILD) {
|
||||||
|
_buttonCheckForUpdates.visibility = View.VISIBLE
|
||||||
|
_buttonCheckForUpdates.setOnClickListener {
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
StateUpdate.instance.checkForUpdates(this@ExceptionActivity, true, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_buttonCheckForUpdates.visibility = View.GONE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun submitFile() {
|
private fun submitFile() {
|
||||||
if (_submitted) {
|
if (_submitted) {
|
||||||
Toast.makeText(this, "Logs already submitted.", Toast.LENGTH_LONG).show();
|
Toast.makeText(this, getString(R.string.logs_already_submitted), Toast.LENGTH_LONG).show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
val file = _file;
|
val file = _file;
|
||||||
if (file == null) {
|
if (file == null) {
|
||||||
Toast.makeText(this, "No logs found.", Toast.LENGTH_LONG).show();
|
Toast.makeText(this, getString(R.string.no_logs_found), Toast.LENGTH_LONG).show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,14 +124,14 @@ class ExceptionActivity : AppCompatActivity() {
|
|||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
if (id == null) {
|
if (id == null) {
|
||||||
try {
|
try {
|
||||||
Toast.makeText(this@ExceptionActivity, "Failed automated share, share manually?", Toast.LENGTH_LONG).show();
|
Toast.makeText(this@ExceptionActivity, getString(R.string.failed_automated_share_share_manually), Toast.LENGTH_LONG).show();
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
//Ignored
|
//Ignored
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_submitted = true;
|
_submitted = true;
|
||||||
file.delete();
|
file.delete();
|
||||||
Toast.makeText(this@ExceptionActivity, "Shared $id", Toast.LENGTH_LONG).show();
|
Toast.makeText(this@ExceptionActivity, getString(R.string.shared_id).replace("{id}", id), Toast.LENGTH_LONG).show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,10 +142,10 @@ class ExceptionActivity : AppCompatActivity() {
|
|||||||
val i = Intent(Intent.ACTION_SEND);
|
val i = Intent(Intent.ACTION_SEND);
|
||||||
i.type = "text/plain";
|
i.type = "text/plain";
|
||||||
i.putExtra(Intent.EXTRA_EMAIL, arrayOf("grayjay@futo.org"));
|
i.putExtra(Intent.EXTRA_EMAIL, arrayOf("grayjay@futo.org"));
|
||||||
i.putExtra(Intent.EXTRA_SUBJECT, "Unhandled exception in VS");
|
i.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.unhandled_exception_in_vs));
|
||||||
i.putExtra(Intent.EXTRA_TEXT, exceptionString);
|
i.putExtra(Intent.EXTRA_TEXT, exceptionString);
|
||||||
|
|
||||||
startActivity(Intent.createChooser(i, "Send exception to developers..."));
|
startActivity(Intent.createChooser(i, getString(R.string.send_exception_to_developers)));
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
//Ignored
|
//Ignored
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.Html
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
|
import com.futo.platformplayer.dialogs.CastingHelpDialog
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.futo.platformplayer.views.buttons.BigButton
|
||||||
|
|
||||||
|
class FCastGuideActivity : AppCompatActivity() {
|
||||||
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
|
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_fcast_guide);
|
||||||
|
setNavigationBarColorAndIcons();
|
||||||
|
|
||||||
|
findViewById<TextView>(R.id.text_explanation).apply {
|
||||||
|
val guideText = """
|
||||||
|
<h3>1. Install FCast Receiver:</h3>
|
||||||
|
<p>- Open Play Store, FireStore, or FCast website on your TV/desktop.<br>
|
||||||
|
- Search for "FCast Receiver", install and open it.</p>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<h3>2. Prepare the Grayjay App:</h3>
|
||||||
|
<p>- Ensure it's connected to the same network as the FCast Receiver.</p>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<h3>3. Initiate Casting from Grayjay:</h3>
|
||||||
|
<p>- Click the cast button in Grayjay.</p>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<h3>4. Connect to FCast Receiver:</h3>
|
||||||
|
<p>- Wait for your device to show in the list or add it manually with its IP address.</p>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<h3>5. Confirm Connection:</h3>
|
||||||
|
<p>- Click "OK" to confirm your device selection.</p>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<h3>6. Start Casting:</h3>
|
||||||
|
<p>- Press "start" next to the device you've added.</p>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<h3>7. Play Your Video:</h3>
|
||||||
|
<p>- Start any video in Grayjay to cast.</p>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<h3>Finding Your IP Address:</h3>
|
||||||
|
<p><b>On FCast Receiver (Android):</b> Displayed on the main screen.<br>
|
||||||
|
<b>On Windows:</b> Use 'ipconfig' in Command Prompt.<br>
|
||||||
|
<b>On Linux:</b> Use 'hostname -I' or 'ip addr' in Terminal.<br>
|
||||||
|
<b>On MacOS:</b> System Preferences > Network.</p>
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
text = Html.fromHtml(guideText, Html.FROM_HTML_MODE_COMPACT)
|
||||||
|
}
|
||||||
|
|
||||||
|
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
|
||||||
|
UIDialogs.showCastingTutorialDialog(this)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
findViewById<BigButton>(R.id.button_close).onClick.subscribe {
|
||||||
|
UIDialogs.showCastingTutorialDialog(this)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
findViewById<BigButton>(R.id.button_website).onClick.subscribe {
|
||||||
|
try {
|
||||||
|
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://fcast.org/"))
|
||||||
|
startActivity(browserIntent);
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.i(TAG, "Failed to open browser.", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
findViewById<BigButton>(R.id.button_technical).onClick.subscribe {
|
||||||
|
try {
|
||||||
|
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://gitlab.com/futo-org/fcast/-/wikis/Protocol-version-1"))
|
||||||
|
startActivity(browserIntent);
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.i(TAG, "Failed to open browser.", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBackPressed() {
|
||||||
|
UIDialogs.showCastingTutorialDialog(this)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "FCastGuideActivity";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@ package com.futo.platformplayer.activities
|
|||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import androidx.activity.result.ActivityResult
|
import androidx.activity.result.ActivityResult
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
|
||||||
|
|
||||||
interface IWithResultLauncher {
|
interface IWithResultLauncher {
|
||||||
fun launchForResult(intent: Intent, code: Int, handler: (ActivityResult)->Unit);
|
fun launchForResult(intent: Intent, code: Int, handler: (ActivityResult)->Unit);
|
||||||
|
|||||||
@@ -3,33 +3,48 @@ package com.futo.platformplayer.activities
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.webkit.ConsoleMessage
|
|
||||||
import android.webkit.CookieManager
|
import android.webkit.CookieManager
|
||||||
import android.webkit.WebChromeClient
|
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourceAuth
|
import com.futo.platformplayer.api.media.platforms.js.SourceAuth
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginAuthConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginAuthConfig
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.others.LoginWebViewClient
|
import com.futo.platformplayer.others.LoginWebViewClient
|
||||||
|
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
class LoginActivity : AppCompatActivity() {
|
class LoginActivity : AppCompatActivity() {
|
||||||
private lateinit var _webView: WebView;
|
private lateinit var _webView: WebView;
|
||||||
|
private lateinit var _textUrl: TextView;
|
||||||
|
private lateinit var _buttonClose: ImageButton;
|
||||||
|
|
||||||
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
|
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_login);
|
setContentView(R.layout.activity_login);
|
||||||
setNavigationBarColorAndIcons();
|
setNavigationBarColorAndIcons();
|
||||||
|
|
||||||
|
_textUrl = findViewById(R.id.text_url);
|
||||||
|
_buttonClose = findViewById(R.id.button_close);
|
||||||
|
_buttonClose.setOnClickListener {
|
||||||
|
UIDialogs.toast("Login cancelled", false);
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
_webView = findViewById(R.id.web_view);
|
_webView = findViewById(R.id.web_view);
|
||||||
_webView.settings.javaScriptEnabled = true;
|
_webView.settings.javaScriptEnabled = true;
|
||||||
CookieManager.getInstance().setAcceptCookie(true);
|
CookieManager.getInstance().setAcceptCookie(true);
|
||||||
@@ -60,6 +75,8 @@ class LoginActivity : AppCompatActivity() {
|
|||||||
};
|
};
|
||||||
var isFirstLoad = true;
|
var isFirstLoad = true;
|
||||||
webViewClient.onPageLoaded.subscribe { view, url ->
|
webViewClient.onPageLoaded.subscribe { view, url ->
|
||||||
|
_textUrl.setText(url ?: "");
|
||||||
|
|
||||||
if(!isFirstLoad)
|
if(!isFirstLoad)
|
||||||
return@subscribe;
|
return@subscribe;
|
||||||
isFirstLoad = false;
|
isFirstLoad = false;
|
||||||
@@ -85,7 +102,7 @@ class LoginActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
override fun finish() {
|
override fun finish() {
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
_webView?.loadUrl("about:blank");
|
_webView.loadUrl("about:blank");
|
||||||
}
|
}
|
||||||
_callback?.let {
|
_callback?.let {
|
||||||
_callback = null;
|
_callback = null;
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
package com.futo.platformplayer.activities
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
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.ActivityInfo
|
import android.content.pm.ActivityInfo
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
@@ -15,19 +18,20 @@ import androidx.activity.result.ActivityResultLauncher
|
|||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.constraintlayout.motion.widget.MotionLayout
|
import androidx.constraintlayout.motion.widget.MotionLayout
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.core.view.WindowInsetsControllerCompat
|
import androidx.core.view.WindowInsetsControllerCompat
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.Fragment
|
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 com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
import com.futo.platformplayer.api.media.PlatformID
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
|
|
||||||
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.constructs.Event3
|
|
||||||
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.*
|
import com.futo.platformplayer.fragment.mainactivity.main.*
|
||||||
import com.futo.platformplayer.fragment.mainactivity.topbar.AddTopBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.topbar.AddTopBarFragment
|
||||||
@@ -37,20 +41,23 @@ import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFrag
|
|||||||
import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
|
||||||
import com.futo.platformplayer.listeners.OrientationManager
|
import com.futo.platformplayer.listeners.OrientationManager
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.models.ImportCache
|
||||||
import com.futo.platformplayer.models.UrlVideoWithTime
|
import com.futo.platformplayer.models.UrlVideoWithTime
|
||||||
import com.futo.platformplayer.states.*
|
import com.futo.platformplayer.states.*
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
import com.futo.platformplayer.stores.SubscriptionStorage
|
import com.futo.platformplayer.stores.SubscriptionStorage
|
||||||
import com.futo.platformplayer.stores.v2.ManagedStore
|
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||||
|
import com.futo.platformplayer.views.ToastView
|
||||||
import com.google.gson.JsonParser
|
import com.google.gson.JsonParser
|
||||||
|
import com.google.zxing.integration.android.IntentIntegrator
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.PrintWriter
|
import java.io.PrintWriter
|
||||||
import java.io.StringWriter
|
import java.io.StringWriter
|
||||||
import java.lang.reflect.InvocationTargetException
|
import java.lang.reflect.InvocationTargetException
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import java.util.concurrent.ConcurrentLinkedQueue
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity, IWithResultLauncher {
|
class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||||
|
|
||||||
@@ -62,6 +69,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
lateinit var rootView : MotionLayout;
|
lateinit var rootView : MotionLayout;
|
||||||
|
|
||||||
private lateinit var _overlayContainer: FrameLayout;
|
private lateinit var _overlayContainer: FrameLayout;
|
||||||
|
private lateinit var _toastView: ToastView;
|
||||||
|
|
||||||
//Segment Containers
|
//Segment Containers
|
||||||
private lateinit var _fragContainerTopBar: FragmentContainerView;
|
private lateinit var _fragContainerTopBar: FragmentContainerView;
|
||||||
@@ -88,9 +96,11 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
lateinit var _fragMainPlaylistSearchResults: PlaylistSearchResultsFragment;
|
lateinit var _fragMainPlaylistSearchResults: PlaylistSearchResultsFragment;
|
||||||
lateinit var _fragMainSuggestions: SuggestionsFragment;
|
lateinit var _fragMainSuggestions: SuggestionsFragment;
|
||||||
lateinit var _fragMainSubscriptions: CreatorsFragment;
|
lateinit var _fragMainSubscriptions: CreatorsFragment;
|
||||||
|
lateinit var _fragMainComments: CommentsFragment;
|
||||||
lateinit var _fragMainSubscriptionsFeed: SubscriptionsFeedFragment;
|
lateinit var _fragMainSubscriptionsFeed: SubscriptionsFeedFragment;
|
||||||
lateinit var _fragMainChannel: ChannelFragment;
|
lateinit var _fragMainChannel: ChannelFragment;
|
||||||
lateinit var _fragMainSources: SourcesFragment;
|
lateinit var _fragMainSources: SourcesFragment;
|
||||||
|
lateinit var _fragMainTutorial: TutorialFragment;
|
||||||
lateinit var _fragMainPlaylists: PlaylistsFragment;
|
lateinit var _fragMainPlaylists: PlaylistsFragment;
|
||||||
lateinit var _fragMainPlaylist: PlaylistFragment;
|
lateinit var _fragMainPlaylist: PlaylistFragment;
|
||||||
lateinit var _fragWatchlist: WatchLaterFragment;
|
lateinit var _fragWatchlist: WatchLaterFragment;
|
||||||
@@ -100,6 +110,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
lateinit var _fragImportSubscriptions: ImportSubscriptionsFragment;
|
lateinit var _fragImportSubscriptions: ImportSubscriptionsFragment;
|
||||||
lateinit var _fragImportPlaylists: ImportPlaylistsFragment;
|
lateinit var _fragImportPlaylists: ImportPlaylistsFragment;
|
||||||
lateinit var _fragBuy: BuyFragment;
|
lateinit var _fragBuy: BuyFragment;
|
||||||
|
lateinit var _fragSubGroup: SubscriptionGroupFragment;
|
||||||
|
lateinit var _fragSubGroupList: SubscriptionGroupListFragment;
|
||||||
|
|
||||||
lateinit var _fragBrowser: BrowserFragment;
|
lateinit var _fragBrowser: BrowserFragment;
|
||||||
|
|
||||||
@@ -121,6 +133,26 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
private var _isVisible = true;
|
private var _isVisible = true;
|
||||||
private var _wasStopped = false;
|
private var _wasStopped = false;
|
||||||
|
|
||||||
|
private val _urlQrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
|
val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
|
||||||
|
scanResult?.let {
|
||||||
|
val content = it.contents
|
||||||
|
if (content == null) {
|
||||||
|
UIDialogs.toast(this, getString(R.string.failed_to_scan_qr_code))
|
||||||
|
return@let
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
runBlocking {
|
||||||
|
handleUrlAll(content)
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.i(TAG, "Failed to handle URL.", e)
|
||||||
|
UIDialogs.toast(this, "Failed to handle URL: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
constructor() : super() {
|
constructor() : super() {
|
||||||
Thread.setDefaultUncaughtExceptionHandler { _, throwable ->
|
Thread.setDefaultUncaughtExceptionHandler { _, throwable ->
|
||||||
val writer = StringWriter();
|
val writer = StringWriter();
|
||||||
@@ -154,6 +186,11 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
|
Logger.i(TAG, "MainActivity.attachBaseContext")
|
||||||
|
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
StateApp.instance.setGlobalContext(this, lifecycleScope);
|
StateApp.instance.setGlobalContext(this, lifecycleScope);
|
||||||
StateApp.instance.mainAppStarting(this);
|
StateApp.instance.mainAppStarting(this);
|
||||||
@@ -177,7 +214,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
_fragContainerVideoDetail = findViewById(R.id.fragment_overlay);
|
_fragContainerVideoDetail = findViewById(R.id.fragment_overlay);
|
||||||
_fragContainerOverlay = findViewById(R.id.fragment_overlay_container);
|
_fragContainerOverlay = findViewById(R.id.fragment_overlay_container);
|
||||||
_overlayContainer = findViewById(R.id.overlay_container);
|
_overlayContainer = findViewById(R.id.overlay_container);
|
||||||
//_overlayContainer.visibility = View.GONE;
|
_toastView = findViewById(R.id.toast_view);
|
||||||
|
|
||||||
//Initialize fragments
|
//Initialize fragments
|
||||||
|
|
||||||
@@ -193,11 +230,13 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
|
|
||||||
//Main
|
//Main
|
||||||
_fragMainHome = HomeFragment.newInstance();
|
_fragMainHome = HomeFragment.newInstance();
|
||||||
|
_fragMainTutorial = TutorialFragment.newInstance()
|
||||||
_fragMainSuggestions = SuggestionsFragment.newInstance();
|
_fragMainSuggestions = SuggestionsFragment.newInstance();
|
||||||
_fragMainVideoSearchResults = ContentSearchResultsFragment.newInstance();
|
_fragMainVideoSearchResults = ContentSearchResultsFragment.newInstance();
|
||||||
_fragMainCreatorSearchResults = CreatorSearchResultsFragment.newInstance();
|
_fragMainCreatorSearchResults = CreatorSearchResultsFragment.newInstance();
|
||||||
_fragMainPlaylistSearchResults = PlaylistSearchResultsFragment.newInstance();
|
_fragMainPlaylistSearchResults = PlaylistSearchResultsFragment.newInstance();
|
||||||
_fragMainSubscriptions = CreatorsFragment.newInstance();
|
_fragMainSubscriptions = CreatorsFragment.newInstance();
|
||||||
|
_fragMainComments = CommentsFragment.newInstance();
|
||||||
_fragMainChannel = ChannelFragment.newInstance();
|
_fragMainChannel = ChannelFragment.newInstance();
|
||||||
_fragMainSubscriptionsFeed = SubscriptionsFeedFragment.newInstance();
|
_fragMainSubscriptionsFeed = SubscriptionsFeedFragment.newInstance();
|
||||||
_fragMainSources = SourcesFragment.newInstance();
|
_fragMainSources = SourcesFragment.newInstance();
|
||||||
@@ -211,6 +250,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
_fragImportSubscriptions = ImportSubscriptionsFragment.newInstance();
|
_fragImportSubscriptions = ImportSubscriptionsFragment.newInstance();
|
||||||
_fragImportPlaylists = ImportPlaylistsFragment.newInstance();
|
_fragImportPlaylists = ImportPlaylistsFragment.newInstance();
|
||||||
_fragBuy = BuyFragment.newInstance();
|
_fragBuy = BuyFragment.newInstance();
|
||||||
|
_fragSubGroup = SubscriptionGroupFragment.newInstance();
|
||||||
|
_fragSubGroupList = SubscriptionGroupListFragment.newInstance();
|
||||||
|
|
||||||
_fragBrowser = BrowserFragment.newInstance();
|
_fragBrowser = BrowserFragment.newInstance();
|
||||||
|
|
||||||
@@ -275,11 +316,13 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
//Set top bars
|
//Set top bars
|
||||||
_fragMainHome.topBar = _fragTopBarGeneral;
|
_fragMainHome.topBar = _fragTopBarGeneral;
|
||||||
_fragMainSubscriptions.topBar = _fragTopBarGeneral;
|
_fragMainSubscriptions.topBar = _fragTopBarGeneral;
|
||||||
|
_fragMainComments.topBar = _fragTopBarGeneral;
|
||||||
_fragMainSuggestions.topBar = _fragTopBarSearch;
|
_fragMainSuggestions.topBar = _fragTopBarSearch;
|
||||||
_fragMainVideoSearchResults.topBar = _fragTopBarSearch;
|
_fragMainVideoSearchResults.topBar = _fragTopBarSearch;
|
||||||
_fragMainCreatorSearchResults.topBar = _fragTopBarSearch;
|
_fragMainCreatorSearchResults.topBar = _fragTopBarSearch;
|
||||||
_fragMainPlaylistSearchResults.topBar = _fragTopBarSearch;
|
_fragMainPlaylistSearchResults.topBar = _fragTopBarSearch;
|
||||||
_fragMainChannel.topBar = _fragTopBarNavigation;
|
_fragMainChannel.topBar = _fragTopBarNavigation;
|
||||||
|
_fragMainTutorial.topBar = _fragTopBarNavigation;
|
||||||
_fragMainSubscriptionsFeed.topBar = _fragTopBarGeneral;
|
_fragMainSubscriptionsFeed.topBar = _fragTopBarGeneral;
|
||||||
_fragMainSources.topBar = _fragTopBarAdd;
|
_fragMainSources.topBar = _fragTopBarAdd;
|
||||||
_fragMainPlaylists.topBar = _fragTopBarGeneral;
|
_fragMainPlaylists.topBar = _fragTopBarGeneral;
|
||||||
@@ -291,9 +334,10 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
_fragDownloads.topBar = _fragTopBarGeneral;
|
_fragDownloads.topBar = _fragTopBarGeneral;
|
||||||
_fragImportSubscriptions.topBar = _fragTopBarImport;
|
_fragImportSubscriptions.topBar = _fragTopBarImport;
|
||||||
_fragImportPlaylists.topBar = _fragTopBarImport;
|
_fragImportPlaylists.topBar = _fragTopBarImport;
|
||||||
|
_fragSubGroupList.topBar = _fragTopBarAdd;
|
||||||
|
|
||||||
_fragBrowser.topBar = _fragTopBarNavigation;
|
_fragBrowser.topBar = _fragTopBarNavigation;
|
||||||
|
|
||||||
fragCurrent = _fragMainHome;
|
fragCurrent = _fragMainHome;
|
||||||
|
|
||||||
val defaultTab = Settings.instance.tabs.mapNotNull {
|
val defaultTab = Settings.instance.tabs.mapNotNull {
|
||||||
@@ -321,6 +365,15 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
fragCurrent.onOrientationChanged(it);
|
fragCurrent.onOrientationChanged(it);
|
||||||
if(_fragVideoDetail.state == VideoDetailFragment.State.MAXIMIZED)
|
if(_fragVideoDetail.state == VideoDetailFragment.State.MAXIMIZED)
|
||||||
_fragVideoDetail.onOrientationChanged(it);
|
_fragVideoDetail.onOrientationChanged(it);
|
||||||
|
else if(Settings.instance.other.bypassRotationPrevention)
|
||||||
|
{
|
||||||
|
requestedOrientation = when(orientation) {
|
||||||
|
OrientationManager.Orientation.PORTRAIT -> ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||||
|
OrientationManager.Orientation.LANDSCAPE -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||||
|
OrientationManager.Orientation.REVERSED_PORTRAIT -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT
|
||||||
|
OrientationManager.Orientation.REVERSED_LANDSCAPE -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
_orientationManager.enable();
|
_orientationManager.enable();
|
||||||
|
|
||||||
@@ -366,6 +419,16 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
StateApp.instance.mainAppStartedWithExternalFiles(this);
|
StateApp.instance.mainAppStartedWithExternalFiles(this);
|
||||||
|
|
||||||
//startActivity(Intent(this, TestActivity::class.java));
|
//startActivity(Intent(this, TestActivity::class.java));
|
||||||
|
|
||||||
|
val sharedPreferences = getSharedPreferences("GrayjayFirstBoot", Context.MODE_PRIVATE)
|
||||||
|
val isFirstBoot = sharedPreferences.getBoolean("IsFirstBoot", true)
|
||||||
|
if (isFirstBoot) {
|
||||||
|
UIDialogs.showConfirmationDialog(this, getString(R.string.do_you_want_to_see_the_tutorials_you_can_find_them_at_any_time_through_the_more_button), {
|
||||||
|
navigate(_fragMainTutorial)
|
||||||
|
})
|
||||||
|
|
||||||
|
sharedPreferences.edit().putBoolean("IsFirstBoot", false).apply()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -390,6 +453,23 @@ 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");
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
|
fun showUrlQrCodeScanner() {
|
||||||
|
try {
|
||||||
|
val integrator = IntentIntegrator(this)
|
||||||
|
integrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
|
||||||
|
integrator.setPrompt(getString(R.string.scan_a_qr_code))
|
||||||
|
integrator.setOrientationLocked(true);
|
||||||
|
integrator.setCameraId(0)
|
||||||
|
integrator.setBeepEnabled(false)
|
||||||
|
integrator.setBarcodeImageEnabled(true)
|
||||||
|
integrator.captureActivity = QRCaptureActivity::class.java
|
||||||
|
_urlQrCodeResultLauncher.launch(integrator.createScanIntent())
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.i(TAG, "Failed to handle show QR scanner.", e)
|
||||||
|
UIDialogs.toast(this, "Failed to show QR scanner: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
Logger.v(TAG, "onResume")
|
Logger.v(TAG, "onResume")
|
||||||
@@ -405,21 +485,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_isVisible = true;
|
_isVisible = true;
|
||||||
val videoToOpen = StateSaved.instance.videoToOpen;
|
|
||||||
|
|
||||||
if (_wasStopped) {
|
|
||||||
_wasStopped = false;
|
|
||||||
|
|
||||||
if (videoToOpen != null && _fragVideoDetail.state == VideoDetailFragment.State.CLOSED) {
|
|
||||||
Logger.i(TAG, "onResume videoToOpen=$videoToOpen");
|
|
||||||
if (StatePlatform.instance.hasEnabledVideoClient(videoToOpen.url)) {
|
|
||||||
navigate(_fragVideoDetail, UrlVideoWithTime(videoToOpen.url, videoToOpen.timeSeconds, false));
|
|
||||||
_fragVideoDetail.maximizeVideoDetail(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
StateSaved.instance.setVideoToOpenNonBlocking(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
@@ -459,6 +524,13 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
Logger.i(TAG, "View Received: " + targetData);
|
Logger.i(TAG, "View Received: " + targetData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"VIDEO" -> {
|
||||||
|
val url = intent.getStringExtra("VIDEO");
|
||||||
|
navigate(_fragVideoDetail, url);
|
||||||
|
}
|
||||||
|
"IMPORT_OPTIONS" -> {
|
||||||
|
UIDialogs.showImportOptionsDialog(this);
|
||||||
|
}
|
||||||
"TAB" -> {
|
"TAB" -> {
|
||||||
when(intent.getStringExtra("TAB")){
|
when(intent.getStringExtra("TAB")){
|
||||||
"Sources" -> {
|
"Sources" -> {
|
||||||
@@ -467,98 +539,151 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
navigate(_fragMainSources);
|
navigate(_fragMainSources);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
"BROWSE_PLUGINS" -> {
|
||||||
|
navigate(_fragBrowser, BrowserFragment.NavigateOptions("https://plugins.grayjay.app/phone.html", mapOf(
|
||||||
|
Pair("grayjay") { req ->
|
||||||
|
StateApp.instance.contextOrNull?.let {
|
||||||
|
if(it is MainActivity) {
|
||||||
|
runBlocking {
|
||||||
|
it.handleUrlAll(req.url.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (targetData != null) {
|
if (targetData != null) {
|
||||||
when(intent.scheme) {
|
runBlocking {
|
||||||
"grayjay" -> {
|
handleUrlAll(targetData)
|
||||||
if(targetData.startsWith("grayjay://license/")) {
|
|
||||||
if(StatePayment.instance.setPaymentLicenseUrl(targetData))
|
|
||||||
{
|
|
||||||
UIDialogs.showDialogOk(this, R.drawable.ic_check, "Your license key has been set!\nAn app restart might be required.");
|
|
||||||
|
|
||||||
if(fragCurrent is BuyFragment)
|
|
||||||
closeSegment(fragCurrent);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
UIDialogs.toast("Invalid license format");
|
|
||||||
|
|
||||||
}
|
|
||||||
else if(targetData.startsWith("grayjay://plugin/")) {
|
|
||||||
val intent = Intent(this, AddSourceActivity::class.java).apply {
|
|
||||||
data = Uri.parse(targetData.substring("grayjay://plugin/".length));
|
|
||||||
};
|
|
||||||
startActivity(intent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"content" -> {
|
|
||||||
if(!handleContent(targetData, intent.type)) {
|
|
||||||
UIDialogs.showSingleButtonDialog(
|
|
||||||
this,
|
|
||||||
R.drawable.ic_play,
|
|
||||||
"Unknown content format [${targetData}]",
|
|
||||||
"Ok",
|
|
||||||
{ });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"file" -> {
|
|
||||||
if(!handleFile(targetData)) {
|
|
||||||
UIDialogs.showSingleButtonDialog(
|
|
||||||
this,
|
|
||||||
R.drawable.ic_play,
|
|
||||||
"Unknown file format [${targetData}]",
|
|
||||||
"Ok",
|
|
||||||
{ });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"polycentric" -> {
|
|
||||||
if(!handlePolycentric(targetData)) {
|
|
||||||
UIDialogs.showSingleButtonDialog(
|
|
||||||
this,
|
|
||||||
R.drawable.ic_play,
|
|
||||||
"Unknown Polycentric format [${targetData}]",
|
|
||||||
"Ok",
|
|
||||||
{ });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
if (!handleUrl(targetData)) {
|
|
||||||
UIDialogs.showSingleButtonDialog(
|
|
||||||
this,
|
|
||||||
R.drawable.ic_play,
|
|
||||||
"Unknown url format [${targetData}]",
|
|
||||||
"Ok",
|
|
||||||
{ });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch(ex: Throwable) {
|
catch(ex: Throwable) {
|
||||||
UIDialogs.showGeneralErrorDialog(this, "Failed to handle file", ex);
|
UIDialogs.showGeneralErrorDialog(this, getString(R.string.failed_to_handle_file), ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleUrl(url: String): Boolean {
|
suspend fun handleUrlAll(url: String) {
|
||||||
|
val uri = Uri.parse(url)
|
||||||
|
when (uri.scheme) {
|
||||||
|
"grayjay" -> {
|
||||||
|
if(url.startsWith("grayjay://license/")) {
|
||||||
|
if(StatePayment.instance.setPaymentLicenseUrl(url))
|
||||||
|
{
|
||||||
|
UIDialogs.showDialogOk(this, R.drawable.ic_check, getString(R.string.your_license_key_has_been_set_an_app_restart_might_be_required));
|
||||||
|
|
||||||
|
if(fragCurrent is BuyFragment)
|
||||||
|
closeSegment(fragCurrent);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
UIDialogs.toast(getString(R.string.invalid_license_format));
|
||||||
|
|
||||||
|
}
|
||||||
|
else if(url.startsWith("grayjay://plugin/")) {
|
||||||
|
val intent = Intent(this, AddSourceActivity::class.java).apply {
|
||||||
|
data = Uri.parse(url.substring("grayjay://plugin/".length));
|
||||||
|
};
|
||||||
|
startActivity(intent);
|
||||||
|
}
|
||||||
|
else if(url.startsWith("grayjay://video/")) {
|
||||||
|
val videoUrl = url.substring("grayjay://video/".length);
|
||||||
|
navigate(_fragVideoDetail, videoUrl);
|
||||||
|
}
|
||||||
|
else if(url.startsWith("grayjay://channel/")) {
|
||||||
|
val channelUrl = url.substring("grayjay://channel/".length);
|
||||||
|
navigate(_fragMainChannel, channelUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"content" -> {
|
||||||
|
if(!handleContent(url, intent.type)) {
|
||||||
|
UIDialogs.showSingleButtonDialog(
|
||||||
|
this,
|
||||||
|
R.drawable.ic_play,
|
||||||
|
getString(R.string.unknown_content_format) + " [${url}]\n[${intent.type}]",
|
||||||
|
"Ok",
|
||||||
|
{ });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"file" -> {
|
||||||
|
if(!handleFile(url)) {
|
||||||
|
UIDialogs.showSingleButtonDialog(
|
||||||
|
this,
|
||||||
|
R.drawable.ic_play,
|
||||||
|
getString(R.string.unknown_file_format) + " [${url}]",
|
||||||
|
"Ok",
|
||||||
|
{ });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"polycentric" -> {
|
||||||
|
if(!handlePolycentric(url)) {
|
||||||
|
UIDialogs.showSingleButtonDialog(
|
||||||
|
this,
|
||||||
|
R.drawable.ic_play,
|
||||||
|
getString(R.string.unknown_polycentric_format) + " [${url}]",
|
||||||
|
"Ok",
|
||||||
|
{ });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"fcast" -> {
|
||||||
|
if(!handleFCast(url)) {
|
||||||
|
UIDialogs.showSingleButtonDialog(
|
||||||
|
this,
|
||||||
|
R.drawable.ic_cast,
|
||||||
|
"Unknown FCast format [${url}]",
|
||||||
|
"Ok",
|
||||||
|
{ });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
if (!handleUrl(url)) {
|
||||||
|
UIDialogs.showSingleButtonDialog(
|
||||||
|
this,
|
||||||
|
R.drawable.ic_play,
|
||||||
|
getString(R.string.unknown_url_format) + " [${url}]",
|
||||||
|
"Ok",
|
||||||
|
{ });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun handleUrl(url: String): Boolean {
|
||||||
Logger.i(TAG, "handleUrl(url=$url)")
|
Logger.i(TAG, "handleUrl(url=$url)")
|
||||||
|
|
||||||
if (StatePlatform.instance.hasEnabledVideoClient(url)) {
|
return withContext(Dispatchers.IO) {
|
||||||
navigate(_fragVideoDetail, url);
|
Logger.i(TAG, "handleUrl(url=$url) on IO");
|
||||||
_fragVideoDetail.maximizeVideoDetail(true);
|
if (StatePlatform.instance.hasEnabledVideoClient(url)) {
|
||||||
return true;
|
Logger.i(TAG, "handleUrl(url=$url) found video client");
|
||||||
} else if(StatePlatform.instance.hasEnabledChannelClient(url)) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
navigate(_fragMainChannel, url);
|
navigate(_fragVideoDetail, url);
|
||||||
|
|
||||||
lifecycleScope.launch {
|
_fragVideoDetail.maximizeVideoDetail(true);
|
||||||
delay(100);
|
}
|
||||||
_fragVideoDetail.minimizeVideoDetail();
|
return@withContext true;
|
||||||
};
|
} else if (StatePlatform.instance.hasEnabledChannelClient(url)) {
|
||||||
return true;
|
Logger.i(TAG, "handleUrl(url=$url) found channel client");
|
||||||
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
navigate(_fragMainChannel, url);
|
||||||
|
delay(100);
|
||||||
|
_fragVideoDetail.minimizeVideoDetail();
|
||||||
|
};
|
||||||
|
return@withContext true;
|
||||||
|
} else if (StatePlatform.instance.hasEnabledPlaylistClient(url)) {
|
||||||
|
Logger.i(TAG, "handleUrl(url=$url) found playlist client");
|
||||||
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
navigate(_fragMainPlaylist, url);
|
||||||
|
delay(100);
|
||||||
|
_fragVideoDetail.minimizeVideoDetail();
|
||||||
|
};
|
||||||
|
return@withContext true;
|
||||||
|
}
|
||||||
|
return@withContext false;
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
fun handleContent(file: String, mime: String? = null): Boolean {
|
fun handleContent(file: String, mime: String? = null): Boolean {
|
||||||
Logger.i(TAG, "handleContent(url=$file)");
|
Logger.i(TAG, "handleContent(url=$file)");
|
||||||
@@ -567,43 +692,74 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
if(file.lowercase().endsWith(".json") || mime == "application/json") {
|
if(file.lowercase().endsWith(".json") || mime == "application/json") {
|
||||||
var recon = String(data);
|
var recon = String(data);
|
||||||
if(!recon.trim().startsWith("["))
|
if(!recon.trim().startsWith("["))
|
||||||
return handleUnknownJson(file, recon);
|
return handleUnknownJson(recon);
|
||||||
|
|
||||||
|
var reconLines = Json.decodeFromString<List<String>>(recon);
|
||||||
|
val cacheStr = reconLines.find { it.startsWith("__CACHE:") }?.substring("__CACHE:".length);
|
||||||
|
reconLines = reconLines.filter { !it.startsWith("__CACHE:") }; //TODO: constant prefix
|
||||||
|
var cache: ImportCache? = null;
|
||||||
|
try {
|
||||||
|
if(cacheStr != null)
|
||||||
|
cache = Json.decodeFromString(cacheStr);
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to deserialize cache");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
val reconLines = Json.decodeFromString<List<String>>(recon);
|
|
||||||
recon = reconLines.joinToString("\n");
|
recon = reconLines.joinToString("\n");
|
||||||
Logger.i(TAG, "Opened shared playlist reconstruction\n${recon}");
|
Logger.i(TAG, "Opened shared playlist reconstruction\n${recon}");
|
||||||
handleReconstruction(recon);
|
handleReconstruction(recon, cache);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else if(file.lowercase().endsWith(".zip") || mime == "application/zip") {
|
else if(file.lowercase().endsWith(".zip") || mime == "application/zip") {
|
||||||
StateBackup.importZipBytes(this, lifecycleScope, data);
|
StateBackup.importZipBytes(this, lifecycleScope, data);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
else if(file.lowercase().endsWith(".txt") || mime == "text/plain") {
|
||||||
|
return handleUnknownText(String(data));
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
fun handleFile(file: String): Boolean {
|
fun handleFile(file: String): Boolean {
|
||||||
Logger.i(TAG, "handleFile(url=$file)");
|
Logger.i(TAG, "handleFile(url=$file)");
|
||||||
if(file.lowercase().endsWith(".json")) {
|
if(file.lowercase().endsWith(".json")) {
|
||||||
val recon = String(readSharedFile(file));
|
var recon = String(readSharedFile(file));
|
||||||
if(!recon.startsWith("["))
|
if(!recon.startsWith("["))
|
||||||
return handleUnknownJson(file, recon);
|
return handleUnknownJson(recon);
|
||||||
|
|
||||||
|
var reconLines = Json.decodeFromString<List<String>>(recon);
|
||||||
|
val cacheStr = reconLines.find { it.startsWith("__CACHE:") }?.substring("__CACHE:".length);
|
||||||
|
reconLines = reconLines.filter { !it.startsWith("__CACHE:") }; //TODO: constant prefix
|
||||||
|
var cache: ImportCache? = null;
|
||||||
|
try {
|
||||||
|
if(cacheStr != null)
|
||||||
|
cache = Json.decodeFromString(cacheStr);
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to deserialize cache");
|
||||||
|
}
|
||||||
|
recon = reconLines.joinToString("\n");
|
||||||
|
|
||||||
Logger.i(TAG, "Opened shared playlist reconstruction\n${recon}");
|
Logger.i(TAG, "Opened shared playlist reconstruction\n${recon}");
|
||||||
handleReconstruction(recon);
|
handleReconstruction(recon, cache);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else if(file.lowercase().endsWith(".zip")) {
|
else if(file.lowercase().endsWith(".zip")) {
|
||||||
StateBackup.importZipBytes(this, lifecycleScope, readSharedFile(file));
|
StateBackup.importZipBytes(this, lifecycleScope, readSharedFile(file));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
else if(file.lowercase().endsWith(".txt")) {
|
||||||
|
return handleUnknownText(String(readSharedFile(file)));
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
fun handleReconstruction(recon: String) {
|
fun handleReconstruction(recon: String, cache: ImportCache? = null) {
|
||||||
val type = ManagedStore.getReconstructionIdentifier(recon);
|
val type = ManagedStore.getReconstructionIdentifier(recon);
|
||||||
val store: ManagedStore<*> = when(type) {
|
val store: ManagedStore<*> = when(type) {
|
||||||
"Playlist" -> StatePlaylists.instance.playlistStore
|
"Playlist" -> StatePlaylists.instance.playlistStore
|
||||||
else -> {
|
else -> {
|
||||||
UIDialogs.toast("Unknown reconstruction type ${type}", false);
|
UIDialogs.toast(getString(R.string.unknown_reconstruction_type) + " ${type}", false);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -615,13 +771,27 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
|
|
||||||
|
|
||||||
if(!type.isNullOrEmpty()) {
|
if(!type.isNullOrEmpty()) {
|
||||||
UIDialogs.showImportDialog(this, store, name, listOf(recon)) {
|
UIDialogs.showImportDialog(this, store, name, listOf(recon), cache) {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleUnknownJson(name: String?, json: String): Boolean {
|
fun handleUnknownText(text: String): Boolean {
|
||||||
|
try {
|
||||||
|
if(text.startsWith("@/Subscription") || text.startsWith("Subscriptions")) {
|
||||||
|
val lines = text.split("\n").map { it.trim() }.drop(1).filter { it.isNotEmpty() };
|
||||||
|
navigate(_fragImportSubscriptions, lines);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e(TAG, ex.message, ex);
|
||||||
|
UIDialogs.showGeneralErrorDialog(this, getString(R.string.failed_to_parse_text_file), ex);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
fun handleUnknownJson(json: String): Boolean {
|
||||||
|
|
||||||
val context = this;
|
val context = this;
|
||||||
|
|
||||||
@@ -631,22 +801,11 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
if (!newPipeSubsParsed.has("subscriptions") || !newPipeSubsParsed["subscriptions"].isJsonArray)
|
if (!newPipeSubsParsed.has("subscriptions") || !newPipeSubsParsed["subscriptions"].isJsonArray)
|
||||||
return false;//throw IllegalArgumentException("Invalid NewPipe json structure found");
|
return false;//throw IllegalArgumentException("Invalid NewPipe json structure found");
|
||||||
|
|
||||||
val jsonSubs = newPipeSubsParsed["subscriptions"]
|
StateBackup.importNewPipeSubs(this, newPipeSubsParsed);
|
||||||
val jsonSubsArray = jsonSubs.asJsonArray;
|
|
||||||
val jsonSubsArrayItt = jsonSubsArray.iterator();
|
|
||||||
val subs = mutableListOf<String>()
|
|
||||||
while(jsonSubsArrayItt.hasNext()) {
|
|
||||||
val jsonSubObj = jsonSubsArrayItt.next().asJsonObject;
|
|
||||||
|
|
||||||
if(jsonSubObj.has("url"))
|
|
||||||
subs.add(jsonSubObj["url"].asString);
|
|
||||||
}
|
|
||||||
|
|
||||||
navigate(_fragImportSubscriptions, subs);
|
|
||||||
}
|
}
|
||||||
catch(ex: Exception) {
|
catch(ex: Exception) {
|
||||||
Logger.e(TAG, ex.message, ex);
|
Logger.e(TAG, ex.message, ex);
|
||||||
UIDialogs.showGeneralErrorDialog(context, "Failed to parse NewPipe Subscriptions", ex);
|
UIDialogs.showGeneralErrorDialog(context, getString(R.string.failed_to_parse_newpipe_subscriptions), ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -668,6 +827,20 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
startActivity(Intent(this, PolycentricImportProfileActivity::class.java).apply { putExtra("url", url) })
|
startActivity(Intent(this, PolycentricImportProfileActivity::class.java).apply { putExtra("url", url) })
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun handleFCast(url: String): Boolean {
|
||||||
|
Logger.i(TAG, "handleFCast");
|
||||||
|
|
||||||
|
try {
|
||||||
|
StateCasting.instance.handleUrl(this, url)
|
||||||
|
return true;
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.e(TAG, "Failed to parse FCast URL '${url}'.", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
private fun readSharedContent(contentPath: String): ByteArray {
|
private fun readSharedContent(contentPath: String): ByteArray {
|
||||||
return contentResolver.openInputStream(Uri.parse(contentPath))?.use {
|
return contentResolver.openInputStream(Uri.parse(contentPath))?.use {
|
||||||
return it.readBytes();
|
return it.readBytes();
|
||||||
@@ -688,11 +861,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
if(_fragBotBarMenu.onBackPressed())
|
if(_fragBotBarMenu.onBackPressed())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if(_fragVideoDetail.state == VideoDetailFragment.State.MAXIMIZED &&
|
if(_fragVideoDetail.state == VideoDetailFragment.State.MAXIMIZED && _fragVideoDetail.onBackPressed())
|
||||||
_fragVideoDetail.onBackPressed())
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
||||||
if(!fragCurrent.onBackPressed())
|
if(!fragCurrent.onBackPressed())
|
||||||
closeSegment();
|
closeSegment();
|
||||||
}
|
}
|
||||||
@@ -727,7 +898,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
|
|
||||||
val isStop: Boolean = lifecycle.currentState == Lifecycle.State.CREATED;
|
val isStop: Boolean = lifecycle.currentState == Lifecycle.State.CREATED;
|
||||||
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");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -738,14 +909,17 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
_orientationManager.disable();
|
_orientationManager.disable();
|
||||||
|
|
||||||
StateApp.instance.mainAppDestroyed(this);
|
StateApp.instance.mainAppDestroyed(this);
|
||||||
StateSaved.instance.setVideoToOpenBlocking(null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline fun <reified T> isFragmentActive(): Boolean {
|
||||||
|
return fragCurrent is T;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
*/
|
*/
|
||||||
|
@SuppressLint("CommitTransaction")
|
||||||
fun navigate(segment: MainFragment, parameter: Any? = null, withHistory: Boolean = true, isBack: Boolean = false) {
|
fun navigate(segment: MainFragment, parameter: Any? = null, withHistory: Boolean = true, isBack: Boolean = false) {
|
||||||
Logger.i(TAG, "Navigate to $segment (parameter=$parameter, withHistory=$withHistory, isBack=$isBack)")
|
Logger.i(TAG, "Navigate to $segment (parameter=$parameter, withHistory=$withHistory, isBack=$isBack)")
|
||||||
|
|
||||||
@@ -781,7 +955,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
|
|
||||||
transaction = transaction.replace(R.id.fragment_main, segment);
|
transaction = transaction.replace(R.id.fragment_main, segment);
|
||||||
|
|
||||||
val extraBottomDP = if(_fragVideoDetail.state == VideoDetailFragment.State.MINIMIZED) HEIGHT_VIDEO_MINIMIZED_DP else 0f
|
|
||||||
if (segment.hasBottomBar) {
|
if (segment.hasBottomBar) {
|
||||||
if (!fragCurrent.hasBottomBar)
|
if (!fragCurrent.hasBottomBar)
|
||||||
transaction = transaction.show(_fragBotBarMenu);
|
transaction = transaction.show(_fragBotBarMenu);
|
||||||
@@ -791,13 +964,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
transaction = transaction.hide(_fragBotBarMenu);
|
transaction = transaction.hide(_fragBotBarMenu);
|
||||||
}
|
}
|
||||||
transaction.commitNow();
|
transaction.commitNow();
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
//Special cases
|
|
||||||
if(segment is VideoDetailFragment) {
|
|
||||||
_fragContainerVideoDetail.visibility = View.VISIBLE;
|
|
||||||
_fragVideoDetail.maximizeVideoDetail();
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!segment.hasBottomBar) {
|
if(!segment.hasBottomBar) {
|
||||||
supportFragmentManager.beginTransaction()
|
supportFragmentManager.beginTransaction()
|
||||||
@@ -834,15 +1001,20 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
|
|
||||||
if((fragment?.isOverlay ?: false) && fragBeforeOverlay != null) {
|
if((fragment?.isOverlay ?: false) && fragBeforeOverlay != null) {
|
||||||
navigate(fragBeforeOverlay!!, null, false, true);
|
navigate(fragBeforeOverlay!!, null, false, true);
|
||||||
|
} else {
|
||||||
}
|
|
||||||
else {
|
|
||||||
val last = _queue.lastOrNull();
|
val last = _queue.lastOrNull();
|
||||||
if (last != null) {
|
if (last != null) {
|
||||||
_queue.remove(last);
|
_queue.remove(last);
|
||||||
navigate(last.first, last.second, false, true);
|
navigate(last.first, last.second, false, true);
|
||||||
} else
|
} else {
|
||||||
finish();
|
if (_fragVideoDetail.state == VideoDetailFragment.State.CLOSED) {
|
||||||
|
finish();
|
||||||
|
} else {
|
||||||
|
UIDialogs.showConfirmationDialog(this, "There is a video playing, are you sure you want to exit the app?", {
|
||||||
|
finish();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -852,6 +1024,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
inline fun <reified T : Fragment> getFragment() : T {
|
inline fun <reified T : Fragment> getFragment() : T {
|
||||||
return when(T::class) {
|
return when(T::class) {
|
||||||
HomeFragment::class -> _fragMainHome as T;
|
HomeFragment::class -> _fragMainHome as T;
|
||||||
|
TutorialFragment::class -> _fragMainTutorial as T;
|
||||||
ContentSearchResultsFragment::class -> _fragMainVideoSearchResults as T;
|
ContentSearchResultsFragment::class -> _fragMainVideoSearchResults as T;
|
||||||
CreatorSearchResultsFragment::class -> _fragMainCreatorSearchResults as T;
|
CreatorSearchResultsFragment::class -> _fragMainCreatorSearchResults as T;
|
||||||
SuggestionsFragment::class -> _fragMainSuggestions as T;
|
SuggestionsFragment::class -> _fragMainSuggestions as T;
|
||||||
@@ -860,6 +1033,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
GeneralTopBarFragment::class -> _fragTopBarGeneral as T;
|
GeneralTopBarFragment::class -> _fragTopBarGeneral 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;
|
||||||
SubscriptionsFeedFragment::class -> _fragMainSubscriptionsFeed as T;
|
SubscriptionsFeedFragment::class -> _fragMainSubscriptionsFeed as T;
|
||||||
PlaylistSearchResultsFragment::class -> _fragMainPlaylistSearchResults as T;
|
PlaylistSearchResultsFragment::class -> _fragMainPlaylistSearchResults as T;
|
||||||
ChannelFragment::class -> _fragMainChannel as T;
|
ChannelFragment::class -> _fragMainChannel as T;
|
||||||
@@ -875,6 +1049,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
ImportPlaylistsFragment::class -> _fragImportPlaylists as T;
|
ImportPlaylistsFragment::class -> _fragImportPlaylists as T;
|
||||||
BrowserFragment::class -> _fragBrowser as T;
|
BrowserFragment::class -> _fragBrowser as T;
|
||||||
BuyFragment::class -> _fragBuy as T;
|
BuyFragment::class -> _fragBuy as T;
|
||||||
|
SubscriptionGroupFragment::class -> _fragSubGroup as T;
|
||||||
|
SubscriptionGroupListFragment::class -> _fragSubGroupList 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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -894,6 +1070,70 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
fun requestNotificationPermissions(reason: String) {
|
||||||
|
when {
|
||||||
|
ContextCompat.checkSelfPermission(this, notifPermission) == PackageManager.PERMISSION_GRANTED -> {
|
||||||
|
|
||||||
|
}
|
||||||
|
ActivityCompat.shouldShowRequestPermissionRationale(this, notifPermission) -> {
|
||||||
|
UIDialogs.showDialog(this, R.drawable.ic_notifications, "Notifications Required",
|
||||||
|
reason, null, 0,
|
||||||
|
UIDialogs.Action("Cancel", {}),
|
||||||
|
UIDialogs.Action("Enable", {
|
||||||
|
requestPermissionLauncher.launch(notifPermission);
|
||||||
|
}, UIDialogs.ActionStyle.PRIMARY));
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
requestPermissionLauncher.launch(notifPermission);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val _toastQueue = ConcurrentLinkedQueue<ToastView.Toast>();
|
||||||
|
private var _toastJob: Job? = null;
|
||||||
|
fun showAppToast(toast: ToastView.Toast) {
|
||||||
|
synchronized(_toastQueue) {
|
||||||
|
_toastQueue.add(toast);
|
||||||
|
if(_toastJob?.isActive != true)
|
||||||
|
_toastJob = lifecycleScope.launch(Dispatchers.Default) {
|
||||||
|
launchAppToastJob();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private suspend fun launchAppToastJob() {
|
||||||
|
Logger.i(TAG, "Starting appToast loop");
|
||||||
|
while(!_toastQueue.isEmpty()) {
|
||||||
|
val toast = _toastQueue.poll() ?: continue;
|
||||||
|
Logger.i(TAG, "Showing next toast (${toast.msg})");
|
||||||
|
|
||||||
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
if (!_toastView.isVisible) {
|
||||||
|
Logger.i(TAG, "First showing toast");
|
||||||
|
_toastView.setToast(toast);
|
||||||
|
_toastView.show(true);
|
||||||
|
} else {
|
||||||
|
_toastView.setToastAnimated(toast);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(toast.long)
|
||||||
|
delay(5000);
|
||||||
|
else
|
||||||
|
delay(3000);
|
||||||
|
}
|
||||||
|
Logger.i(TAG, "Ending appToast loop");
|
||||||
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
_toastView.hide(true) {
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//TODO: Only calls last handler due to missing request codes on ActivityResultLaunchers.
|
//TODO: Only calls last handler due to missing request codes on ActivityResultLaunchers.
|
||||||
private var resultLauncherMap = mutableMapOf<Int, (ActivityResult)->Unit>();
|
private var resultLauncherMap = mutableMapOf<Int, (ActivityResult)->Unit>();
|
||||||
@@ -925,5 +1165,19 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||||
return sourcesIntent;
|
return sourcesIntent;
|
||||||
}
|
}
|
||||||
|
fun getVideoIntent(context: Context, videoUrl: String) : Intent {
|
||||||
|
val sourcesIntent = Intent(context, MainActivity::class.java);
|
||||||
|
sourcesIntent.action = "VIDEO";
|
||||||
|
sourcesIntent.putExtra("VIDEO", videoUrl);
|
||||||
|
sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||||
|
return sourcesIntent;
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getImportOptionsIntent(context: Context): Intent {
|
||||||
|
val sourcesIntent = Intent(context, MainActivity::class.java);
|
||||||
|
sourcesIntent.action = "IMPORT_OPTIONS";
|
||||||
|
sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||||
|
return sourcesIntent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.futo.platformplayer.activities
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
@@ -10,6 +11,7 @@ import com.futo.platformplayer.R
|
|||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
|
||||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.views.AnyAdapterView
|
import com.futo.platformplayer.views.AnyAdapterView
|
||||||
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
|
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
|
||||||
import com.futo.platformplayer.views.adapters.ItemMoveCallback
|
import com.futo.platformplayer.views.adapters.ItemMoveCallback
|
||||||
@@ -23,6 +25,10 @@ class ManageTabsActivity : AppCompatActivity() {
|
|||||||
private lateinit var _recyclerTabs: RecyclerView;
|
private lateinit var _recyclerTabs: RecyclerView;
|
||||||
private lateinit var _touchHelper: ItemTouchHelper;
|
private lateinit var _touchHelper: ItemTouchHelper;
|
||||||
|
|
||||||
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
|
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_manage_tabs);
|
setContentView(R.layout.activity_manage_tabs);
|
||||||
@@ -49,10 +55,10 @@ class ManageTabsActivity : AppCompatActivity() {
|
|||||||
Settings.instance.save()
|
Settings.instance.save()
|
||||||
}
|
}
|
||||||
|
|
||||||
val items = Settings.instance.tabs.mapNotNull {
|
val items = ArrayList(Settings.instance.tabs.mapNotNull {
|
||||||
val buttonDefinition = MenuBottomBarFragment.buttonDefinitions.find { d -> it.id == d.id } ?: return@mapNotNull null
|
val buttonDefinition = MenuBottomBarFragment.buttonDefinitions.find { d -> it.id == d.id } ?: return@mapNotNull null
|
||||||
TabViewHolderData(buttonDefinition, it.enabled)
|
TabViewHolderData(buttonDefinition, it.enabled)
|
||||||
};
|
});
|
||||||
|
|
||||||
_listTabs = _recyclerTabs.asAny(items) {
|
_listTabs = _recyclerTabs.asAny(items) {
|
||||||
it.onDragDrop.subscribe { vh ->
|
it.onDragDrop.subscribe { vh ->
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import androidx.appcompat.app.AppCompatActivity
|
|||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
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.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
import com.futo.platformplayer.views.buttons.BigButton
|
import com.futo.platformplayer.views.buttons.BigButton
|
||||||
import com.futo.polycentric.core.*
|
import com.futo.polycentric.core.*
|
||||||
@@ -33,6 +34,10 @@ class PolycentricBackupActivity : AppCompatActivity() {
|
|||||||
private lateinit var _exportBundle: String;
|
private lateinit var _exportBundle: String;
|
||||||
private lateinit var _textQR: TextView;
|
private lateinit var _textQR: TextView;
|
||||||
|
|
||||||
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
|
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_backup);
|
setContentView(R.layout.activity_polycentric_backup);
|
||||||
@@ -53,7 +58,7 @@ class PolycentricBackupActivity : AppCompatActivity() {
|
|||||||
val qrCodeBitmap = generateQRCode(_exportBundle, dimension, dimension);
|
val qrCodeBitmap = generateQRCode(_exportBundle, dimension, dimension);
|
||||||
_imageQR.setImageBitmap(qrCodeBitmap);
|
_imageQR.setImageBitmap(qrCodeBitmap);
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Logger.e(TAG, "Failed to generate QR code", e);
|
Logger.e(TAG, getString(R.string.failed_to_generate_qr_code), e);
|
||||||
_imageQR.visibility = View.INVISIBLE;
|
_imageQR.visibility = View.INVISIBLE;
|
||||||
_textQR.visibility = View.INVISIBLE;
|
_textQR.visibility = View.INVISIBLE;
|
||||||
}
|
}
|
||||||
@@ -63,12 +68,12 @@ class PolycentricBackupActivity : AppCompatActivity() {
|
|||||||
type = "text/plain";
|
type = "text/plain";
|
||||||
putExtra(Intent.EXTRA_TEXT, _exportBundle);
|
putExtra(Intent.EXTRA_TEXT, _exportBundle);
|
||||||
}
|
}
|
||||||
startActivity(Intent.createChooser(shareIntent, "Share Text"));
|
startActivity(Intent.createChooser(shareIntent, getString(R.string.share_text)));
|
||||||
};
|
};
|
||||||
|
|
||||||
_buttonCopy.onClick.subscribe {
|
_buttonCopy.onClick.subscribe {
|
||||||
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager;
|
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager;
|
||||||
val clip = ClipData.newPlainText("Copied Text", _exportBundle);
|
val clip = ClipData.newPlainText(getString(R.string.copied_text), _exportBundle);
|
||||||
clipboard.setPrimaryClip(clip);
|
clipboard.setPrimaryClip(clip);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-6
@@ -1,5 +1,6 @@
|
|||||||
package com.futo.platformplayer.activities
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
@@ -9,14 +10,15 @@ import androidx.appcompat.app.AppCompatActivity
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
|
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.polycentric.PolycentricStorage
|
||||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
import com.futo.polycentric.core.ProcessHandle
|
import com.futo.polycentric.core.ProcessHandle
|
||||||
import com.futo.polycentric.core.Store
|
import com.futo.polycentric.core.Store
|
||||||
import com.futo.polycentric.core.Synchronization
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
@@ -28,6 +30,10 @@ class PolycentricCreateProfileActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private var _creating = false;
|
private var _creating = false;
|
||||||
|
|
||||||
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
|
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_create_profile);
|
setContentView(R.layout.activity_polycentric_create_profile);
|
||||||
@@ -54,7 +60,7 @@ class PolycentricCreateProfileActivity : AppCompatActivity() {
|
|||||||
try {
|
try {
|
||||||
val username = _profileName.text.toString();
|
val username = _profileName.text.toString();
|
||||||
if (username.length < 3) {
|
if (username.length < 3) {
|
||||||
UIDialogs.toast(this@PolycentricCreateProfileActivity, "Must be at least 3 characters long.");
|
UIDialogs.toast(this@PolycentricCreateProfileActivity, getString(R.string.must_be_at_least_3_characters_long));
|
||||||
return@setOnClickListener;
|
return@setOnClickListener;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,20 +70,29 @@ class PolycentricCreateProfileActivity : AppCompatActivity() {
|
|||||||
try {
|
try {
|
||||||
processHandle = ProcessHandle.create();
|
processHandle = ProcessHandle.create();
|
||||||
Store.instance.addProcessSecret(processHandle.processSecret);
|
Store.instance.addProcessSecret(processHandle.processSecret);
|
||||||
|
|
||||||
|
try {
|
||||||
|
PolycentricStorage.instance.addProcessSecret(processHandle.processSecret)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
|
||||||
|
}
|
||||||
|
|
||||||
processHandle.addServer("https://srv1-stg.polycentric.io");
|
processHandle.addServer("https://srv1-stg.polycentric.io");
|
||||||
processHandle.setUsername(username);
|
processHandle.setUsername(username);
|
||||||
StatePolycentric.instance.setProcessHandle(processHandle);
|
StatePolycentric.instance.setProcessHandle(processHandle);
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to create profile .", e);
|
Logger.e(TAG, getString(R.string.failed_to_create_profile), e);
|
||||||
return@launch;
|
return@launch;
|
||||||
} finally {
|
} finally {
|
||||||
_creating = false;
|
_creating = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
processHandle.fullyBackfillServers();
|
Logger.i(TAG, "Started backfill");
|
||||||
|
processHandle.fullyBackfillServersAnnounceExceptions();
|
||||||
|
Logger.i(TAG, "Finished backfill");
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to fully backfill servers.");
|
Logger.e(TAG, getString(R.string.failed_to_fully_backfill_servers), e);
|
||||||
}
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.futo.platformplayer.activities
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
@@ -15,6 +16,7 @@ import com.futo.platformplayer.R
|
|||||||
import com.futo.platformplayer.dp
|
import com.futo.platformplayer.dp
|
||||||
import com.futo.platformplayer.selectBestImage
|
import com.futo.platformplayer.selectBestImage
|
||||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
import com.futo.platformplayer.views.buttons.BigButton
|
import com.futo.platformplayer.views.buttons.BigButton
|
||||||
import com.futo.polycentric.core.Store
|
import com.futo.polycentric.core.Store
|
||||||
@@ -27,6 +29,10 @@ class PolycentricHomeActivity : AppCompatActivity() {
|
|||||||
private lateinit var _buttonImportProfile: BigButton;
|
private lateinit var _buttonImportProfile: BigButton;
|
||||||
private lateinit var _layoutButtons: LinearLayout;
|
private lateinit var _layoutButtons: LinearLayout;
|
||||||
|
|
||||||
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
|
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_home);
|
setContentView(R.layout.activity_polycentric_home);
|
||||||
@@ -47,7 +53,7 @@ class PolycentricHomeActivity : AppCompatActivity() {
|
|||||||
this.setMargins(0, 0, 0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8f, resources.displayMetrics).toInt());
|
this.setMargins(0, 0, 0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8f, resources.displayMetrics).toInt());
|
||||||
};
|
};
|
||||||
profileButton.withPrimaryText(systemState.username);
|
profileButton.withPrimaryText(systemState.username);
|
||||||
profileButton.withSecondaryText("Sign in to this identity");
|
profileButton.withSecondaryText(getString(R.string.sign_in_to_this_identity));
|
||||||
profileButton.onClick.subscribe {
|
profileButton.onClick.subscribe {
|
||||||
StatePolycentric.instance.setProcessHandle(processHandle);
|
StatePolycentric.instance.setProcessHandle(processHandle);
|
||||||
startActivity(Intent(this@PolycentricHomeActivity, PolycentricProfileActivity::class.java));
|
startActivity(Intent(this@PolycentricHomeActivity, PolycentricProfileActivity::class.java));
|
||||||
|
|||||||
+68
-37
@@ -1,5 +1,6 @@
|
|||||||
package com.futo.platformplayer.activities
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
@@ -11,13 +12,20 @@ import androidx.lifecycle.lifecycleScope
|
|||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||||
|
import com.futo.platformplayer.polycentric.PolycentricStorage
|
||||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
import com.futo.polycentric.core.*
|
import com.futo.platformplayer.views.overlays.LoaderOverlay
|
||||||
|
import com.futo.polycentric.core.KeyPair
|
||||||
|
import com.futo.polycentric.core.Process
|
||||||
|
import com.futo.polycentric.core.ProcessSecret
|
||||||
|
import com.futo.polycentric.core.SignedEvent
|
||||||
|
import com.futo.polycentric.core.Store
|
||||||
|
import com.futo.polycentric.core.base64UrlToByteArray
|
||||||
import com.google.zxing.integration.android.IntentIntegrator
|
import com.google.zxing.integration.android.IntentIntegrator
|
||||||
import com.journeyapps.barcodescanner.CaptureActivity
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import userpackage.Protocol
|
import userpackage.Protocol
|
||||||
@@ -28,6 +36,7 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
|||||||
private lateinit var _buttonScanProfile: LinearLayout;
|
private lateinit var _buttonScanProfile: LinearLayout;
|
||||||
private lateinit var _buttonImportProfile: LinearLayout;
|
private lateinit var _buttonImportProfile: LinearLayout;
|
||||||
private lateinit var _editProfile: EditText;
|
private lateinit var _editProfile: EditText;
|
||||||
|
private lateinit var _loaderOverlay: LoaderOverlay;
|
||||||
|
|
||||||
private val _qrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
private val _qrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
|
val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
|
||||||
@@ -39,6 +48,10 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
|
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);
|
||||||
@@ -47,6 +60,7 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
|||||||
_buttonHelp = findViewById(R.id.button_help);
|
_buttonHelp = findViewById(R.id.button_help);
|
||||||
_buttonScanProfile = findViewById(R.id.button_scan_profile);
|
_buttonScanProfile = findViewById(R.id.button_scan_profile);
|
||||||
_buttonImportProfile = findViewById(R.id.button_import_profile);
|
_buttonImportProfile = findViewById(R.id.button_import_profile);
|
||||||
|
_loaderOverlay = findViewById(R.id.loader_overlay);
|
||||||
_editProfile = findViewById(R.id.edit_profile);
|
_editProfile = findViewById(R.id.edit_profile);
|
||||||
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
|
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
|
||||||
finish();
|
finish();
|
||||||
@@ -59,7 +73,7 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
|||||||
_buttonScanProfile.setOnClickListener {
|
_buttonScanProfile.setOnClickListener {
|
||||||
val integrator = IntentIntegrator(this)
|
val integrator = IntentIntegrator(this)
|
||||||
integrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
|
integrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
|
||||||
integrator.setPrompt("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)
|
||||||
@@ -70,7 +84,7 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
_buttonImportProfile.setOnClickListener {
|
_buttonImportProfile.setOnClickListener {
|
||||||
if (_editProfile.text.isEmpty()) {
|
if (_editProfile.text.isEmpty()) {
|
||||||
UIDialogs.toast(this, "Text field does not contain any data");
|
UIDialogs.toast(this, getString(R.string.text_field_does_not_contain_any_data));
|
||||||
return@setOnClickListener;
|
return@setOnClickListener;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,54 +99,71 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private fun import(url: String) {
|
private fun import(url: String) {
|
||||||
if (!url.startsWith("polycentric://")) {
|
if (!url.startsWith("polycentric://")) {
|
||||||
UIDialogs.toast(this, "Not a valid URL");
|
UIDialogs.toast(this, getString(R.string.not_a_valid_url));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
_loaderOverlay.show()
|
||||||
val data = url.substring("polycentric://".length).base64UrlToByteArray();
|
|
||||||
val urlInfo = Protocol.URLInfo.parseFrom(data);
|
|
||||||
if (urlInfo.urlType != 3L) {
|
|
||||||
throw Exception("Expected urlInfo struct of type ExportBundle")
|
|
||||||
}
|
|
||||||
|
|
||||||
val exportBundle = ExportBundle.parseFrom(urlInfo.body);
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val keyPair = KeyPair.fromProto(exportBundle.keyPair);
|
try {
|
||||||
|
val data = url.substring("polycentric://".length).base64UrlToByteArray();
|
||||||
|
val urlInfo = Protocol.URLInfo.parseFrom(data);
|
||||||
|
if (urlInfo.urlType != 3L) {
|
||||||
|
throw Exception("Expected urlInfo struct of type ExportBundle")
|
||||||
|
}
|
||||||
|
|
||||||
val existingProcessSecret = Store.instance.getProcessSecret(keyPair.publicKey);
|
val exportBundle = ExportBundle.parseFrom(urlInfo.body);
|
||||||
if (existingProcessSecret != null) {
|
val keyPair = KeyPair.fromProto(exportBundle.keyPair);
|
||||||
UIDialogs.toast(this, "This profile is already imported");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
val processSecret = ProcessSecret(keyPair, Process.random());
|
val existingProcessSecret = Store.instance.getProcessSecret(keyPair.publicKey);
|
||||||
Store.instance.addProcessSecret(processSecret);
|
if (existingProcessSecret != null) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
UIDialogs.toast(this@PolycentricImportProfileActivity, getString(R.string.this_profile_is_already_imported));
|
||||||
|
}
|
||||||
|
return@launch;
|
||||||
|
}
|
||||||
|
|
||||||
val processHandle = processSecret.toProcessHandle();
|
val processSecret = ProcessSecret(keyPair, Process.random());
|
||||||
|
Store.instance.addProcessSecret(processSecret);
|
||||||
|
|
||||||
for (e in exportBundle.events.eventsList) {
|
|
||||||
try {
|
try {
|
||||||
val se = SignedEvent.fromProto(e);
|
PolycentricStorage.instance.addProcessSecret(processSecret)
|
||||||
Store.instance.putSignedEvent(se);
|
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.w(TAG, "Ignored invalid event", e);
|
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
val processHandle = processSecret.toProcessHandle();
|
||||||
|
|
||||||
|
for (e in exportBundle.events.eventsList) {
|
||||||
|
try {
|
||||||
|
val se = SignedEvent.fromProto(e);
|
||||||
|
Store.instance.putSignedEvent(se);
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.w(TAG, "Ignored invalid event", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StatePolycentric.instance.setProcessHandle(processHandle);
|
||||||
|
processHandle.fullyBackfillClient(PolycentricCache.SERVER);
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
startActivity(Intent(this@PolycentricImportProfileActivity, PolycentricProfileActivity::class.java));
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.w(TAG, "Failed to import profile", e);
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
UIDialogs.toast(this@PolycentricImportProfileActivity, getString(R.string.failed_to_import_profile) + " '${e.message}'");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
_loaderOverlay.hide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StatePolycentric.instance.setProcessHandle(processHandle);
|
|
||||||
startActivity(Intent(this@PolycentricImportProfileActivity, PolycentricProfileActivity::class.java));
|
|
||||||
finish();
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.w(TAG, "Failed to import profile", e);
|
|
||||||
UIDialogs.toast(this, "Failed to import profile: '${e.message}'");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "PolycentricImportProfileActivity";
|
private const val TAG = "PolycentricImportProfileActivity";
|
||||||
}
|
}
|
||||||
|
|
||||||
class QRCaptureActivity: CaptureActivity() {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+66
-30
@@ -1,7 +1,10 @@
|
|||||||
package com.futo.platformplayer.activities
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
@@ -11,23 +14,27 @@ import android.webkit.MimeTypeMap
|
|||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.dp
|
import com.futo.platformplayer.dp
|
||||||
|
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
||||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||||
import com.futo.platformplayer.selectBestImage
|
import com.futo.platformplayer.selectBestImage
|
||||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
import com.futo.platformplayer.views.buttons.BigButton
|
import com.futo.platformplayer.views.buttons.BigButton
|
||||||
|
import com.futo.platformplayer.views.overlays.LoaderOverlay
|
||||||
import com.futo.polycentric.core.Store
|
import com.futo.polycentric.core.Store
|
||||||
import com.futo.polycentric.core.Synchronization
|
|
||||||
import com.futo.polycentric.core.SystemState
|
import com.futo.polycentric.core.SystemState
|
||||||
import com.futo.polycentric.core.toURLInfoDataLink
|
import com.futo.polycentric.core.toBase64Url
|
||||||
|
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||||
import com.github.dhaval2404.imagepicker.ImagePicker
|
import com.github.dhaval2404.imagepicker.ImagePicker
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -44,8 +51,14 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
|||||||
private lateinit var _buttonDelete: BigButton;
|
private lateinit var _buttonDelete: BigButton;
|
||||||
private lateinit var _username: String;
|
private lateinit var _username: String;
|
||||||
private lateinit var _imagePolycentric: ImageView;
|
private lateinit var _imagePolycentric: ImageView;
|
||||||
|
private lateinit var _loaderOverlay: LoaderOverlay;
|
||||||
|
private lateinit var _textSystem: TextView;
|
||||||
private var _avatarUri: Uri? = null;
|
private var _avatarUri: Uri? = null;
|
||||||
|
|
||||||
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
|
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_profile);
|
setContentView(R.layout.activity_polycentric_profile);
|
||||||
@@ -57,28 +70,13 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
|||||||
_buttonExport = findViewById(R.id.button_export);
|
_buttonExport = findViewById(R.id.button_export);
|
||||||
_buttonLogout = findViewById(R.id.button_logout);
|
_buttonLogout = findViewById(R.id.button_logout);
|
||||||
_buttonDelete = findViewById(R.id.button_delete);
|
_buttonDelete = findViewById(R.id.button_delete);
|
||||||
|
_loaderOverlay = findViewById(R.id.loader_overlay);
|
||||||
|
_textSystem = findViewById(R.id.text_system)
|
||||||
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
|
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
|
||||||
saveIfRequired();
|
saveIfRequired();
|
||||||
finish();
|
finish();
|
||||||
};
|
};
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
val processHandle = StatePolycentric.instance.processHandle!!;
|
|
||||||
Synchronization.fullyBackFillClient(processHandle, processHandle.system, "https://srv1-stg.polycentric.io");
|
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
updateUI();
|
|
||||||
}
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
UIDialogs.toast(this@PolycentricProfileActivity, "Failed to backfill client");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateUI();
|
|
||||||
|
|
||||||
_imagePolycentric.setOnClickListener {
|
_imagePolycentric.setOnClickListener {
|
||||||
ImagePicker.with(this)
|
ImagePicker.with(this)
|
||||||
.cropSquare()
|
.cropSquare()
|
||||||
@@ -101,10 +99,10 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_buttonDelete.onClick.subscribe {
|
_buttonDelete.onClick.subscribe {
|
||||||
UIDialogs.showConfirmationDialog(this, "Are you sure you want to remove this profile?", {
|
UIDialogs.showConfirmationDialog(this, getString(R.string.are_you_sure_you_want_to_remove_this_profile), {
|
||||||
val processHandle = StatePolycentric.instance.processHandle;
|
val processHandle = StatePolycentric.instance.processHandle;
|
||||||
if (processHandle == null) {
|
if (processHandle == null) {
|
||||||
UIDialogs.toast(this, "No process handle set");
|
UIDialogs.toast(this, getString(R.string.no_process_handle_set));
|
||||||
return@showConfirmationDialog;
|
return@showConfirmationDialog;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,6 +112,37 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
|||||||
finish();
|
finish();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_textSystem.setOnLongClickListener {
|
||||||
|
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
|
val clip: ClipData = ClipData.newPlainText("system", _textSystem.text)
|
||||||
|
clipboard.setPrimaryClip(clip)
|
||||||
|
return@setOnLongClickListener true
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUI()
|
||||||
|
|
||||||
|
StatePolycentric.instance.processHandle?.let { processHandle ->
|
||||||
|
_loaderOverlay.show()
|
||||||
|
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
processHandle.fullyBackfillClient(PolycentricCache.SERVER)
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
updateUI();
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.failed_to_backfill_client));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
_loaderOverlay.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveIfRequired() {
|
private fun saveIfRequired() {
|
||||||
@@ -122,13 +151,17 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
|||||||
var hasChanges = false;
|
var hasChanges = false;
|
||||||
val username = _editName.text.toString();
|
val username = _editName.text.toString();
|
||||||
if (username.length < 3) {
|
if (username.length < 3) {
|
||||||
UIDialogs.toast(this@PolycentricProfileActivity, "Name must be at least 3 characters long");
|
withContext(Dispatchers.Main) {
|
||||||
|
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.name_must_be_at_least_3_characters_long));
|
||||||
|
}
|
||||||
return@launch;
|
return@launch;
|
||||||
}
|
}
|
||||||
|
|
||||||
val processHandle = StatePolycentric.instance.processHandle;
|
val processHandle = StatePolycentric.instance.processHandle;
|
||||||
if (processHandle == null) {
|
if (processHandle == null) {
|
||||||
UIDialogs.toast(this@PolycentricProfileActivity, "Process handle unset");
|
withContext(Dispatchers.Main) {
|
||||||
|
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.process_handle_unset));
|
||||||
|
}
|
||||||
return@launch;
|
return@launch;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,7 +176,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
|||||||
val bytes = readBytesFromUri(applicationContext.contentResolver, avatarUri);
|
val bytes = readBytesFromUri(applicationContext.contentResolver, avatarUri);
|
||||||
if (bytes == null) {
|
if (bytes == null) {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
UIDialogs.toast(this@PolycentricProfileActivity, "Failed to read image");
|
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.failed_to_read_image));
|
||||||
}
|
}
|
||||||
|
|
||||||
return@launch;
|
return@launch;
|
||||||
@@ -186,14 +219,16 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
if (hasChanges) {
|
if (hasChanges) {
|
||||||
try {
|
try {
|
||||||
processHandle.fullyBackfillServers();
|
Logger.i(TAG, "Started backfill");
|
||||||
|
processHandle.fullyBackfillServersAnnounceExceptions();
|
||||||
|
Logger.i(TAG, "Finished backfill");
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
UIDialogs.toast(this@PolycentricProfileActivity, "Changes have been saved");
|
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.changes_have_been_saved));
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.w(TAG, "Failed to synchronize changes", e);
|
Logger.w(TAG, "Failed to synchronize changes", e);
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
UIDialogs.toast(this@PolycentricProfileActivity, "Failed to synchronize changes");
|
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.failed_to_synchronize_changes));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -211,6 +246,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
|||||||
private fun updateUI() {
|
private fun updateUI() {
|
||||||
val processHandle = StatePolycentric.instance.processHandle!!;
|
val processHandle = StatePolycentric.instance.processHandle!!;
|
||||||
val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(processHandle.system))
|
val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(processHandle.system))
|
||||||
|
_textSystem.text = processHandle.system.key.toBase64Url()
|
||||||
_username = systemState.username;
|
_username = systemState.username;
|
||||||
_editName.text.clear();
|
_editName.text.clear();
|
||||||
_editName.text.append(_username);
|
_editName.text.append(_username);
|
||||||
@@ -219,7 +255,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
|||||||
val avatar = systemState.avatar.selectBestImage(dp_80 * dp_80);
|
val avatar = systemState.avatar.selectBestImage(dp_80 * dp_80);
|
||||||
|
|
||||||
Glide.with(_imagePolycentric)
|
Glide.with(_imagePolycentric)
|
||||||
.load(avatar?.toURLInfoDataLink(processHandle.system.toProto(), processHandle.processSecret.process.toProto(), systemState.servers.toList()))
|
.load(avatar?.toURLInfoSystemLinkUrl(processHandle.system.toProto(), avatar.process, systemState.servers.toList()))
|
||||||
.placeholder(R.drawable.placeholder_profile)
|
.placeholder(R.drawable.placeholder_profile)
|
||||||
.crossfade()
|
.crossfade()
|
||||||
.into(_imagePolycentric)
|
.into(_imagePolycentric)
|
||||||
@@ -235,12 +271,12 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
|||||||
} else if (resultCode == ImagePicker.RESULT_ERROR) {
|
} else if (resultCode == ImagePicker.RESULT_ERROR) {
|
||||||
UIDialogs.toast(this, ImagePicker.getError(data));
|
UIDialogs.toast(this, ImagePicker.getError(data));
|
||||||
} else {
|
} else {
|
||||||
UIDialogs.toast(this, "Image picker cancelled");
|
UIDialogs.toast(this, getString(R.string.image_picker_cancelled));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getMimeType(contentResolver: ContentResolver, uri: Uri): String? {
|
private fun getMimeType(contentResolver: ContentResolver, uri: Uri): String? {
|
||||||
var mimeType: String? = null;
|
var mimeType: String?;
|
||||||
|
|
||||||
// Try to get MIME type from the content URI
|
// Try to get MIME type from the content URI
|
||||||
mimeType = contentResolver.getType(uri);
|
mimeType = contentResolver.getType(uri);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.futo.platformplayer.activities
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@@ -7,12 +8,17 @@ import android.widget.ImageButton
|
|||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.views.buttons.BigButton
|
import com.futo.platformplayer.views.buttons.BigButton
|
||||||
|
|
||||||
class PolycentricWhyActivity : AppCompatActivity() {
|
class PolycentricWhyActivity : AppCompatActivity() {
|
||||||
private lateinit var _buttonVideo: BigButton;
|
private lateinit var _buttonVideo: BigButton;
|
||||||
private lateinit var _buttonTechnical: BigButton;
|
private lateinit var _buttonTechnical: BigButton;
|
||||||
|
|
||||||
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
|
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_why);
|
setContentView(R.layout.activity_polycentric_why);
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
|
import com.journeyapps.barcodescanner.CaptureActivity
|
||||||
|
|
||||||
|
class QRCaptureActivity : CaptureActivity() {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,19 +1,26 @@
|
|||||||
package com.futo.platformplayer.activities
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.widget.FrameLayout
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
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.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.views.Loader
|
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.FieldForm
|
||||||
import com.futo.platformplayer.views.fields.ReadOnlyTextField
|
import com.futo.platformplayer.views.fields.ReadOnlyTextField
|
||||||
import com.google.android.material.button.MaterialButton
|
import com.google.android.material.button.MaterialButton
|
||||||
@@ -21,13 +28,27 @@ import com.google.android.material.button.MaterialButton
|
|||||||
class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
|
class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
|
||||||
private lateinit var _form: FieldForm;
|
private lateinit var _form: FieldForm;
|
||||||
private lateinit var _buttonBack: ImageButton;
|
private lateinit var _buttonBack: ImageButton;
|
||||||
private lateinit var _loader: Loader;
|
private lateinit var _loaderView: LoaderView;
|
||||||
|
|
||||||
private lateinit var _devSets: LinearLayout;
|
private lateinit var _devSets: LinearLayout;
|
||||||
private lateinit var _buttonDev: MaterialButton;
|
private lateinit var _buttonDev: MaterialButton;
|
||||||
|
|
||||||
private var _isFinished = false;
|
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?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.activity_settings);
|
setContentView(R.layout.activity_settings);
|
||||||
@@ -37,12 +58,45 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
|
|||||||
_buttonBack = findViewById(R.id.button_back);
|
_buttonBack = findViewById(R.id.button_back);
|
||||||
_buttonDev = findViewById(R.id.button_dev);
|
_buttonDev = findViewById(R.id.button_dev);
|
||||||
_devSets = findViewById(R.id.dev_settings);
|
_devSets = findViewById(R.id.dev_settings);
|
||||||
_loader = findViewById(R.id.loader);
|
_loaderView = findViewById(R.id.loader);
|
||||||
|
overlay = findViewById(R.id.overlay_container);
|
||||||
|
|
||||||
_form.onChanged.subscribe { field, value ->
|
_form.onChanged.subscribe { field, _ ->
|
||||||
Logger.i("SettingsActivity", "Setting [${field.field?.name}] changed, saving");
|
Logger.i("SettingsActivity", "Setting [${field.field?.name}] changed, saving");
|
||||||
_form.setObjectValues();
|
_form.setObjectValues();
|
||||||
Settings.instance.save();
|
Settings.instance.save();
|
||||||
|
|
||||||
|
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 {
|
_buttonBack.setOnClickListener {
|
||||||
finish();
|
finish();
|
||||||
@@ -57,10 +111,15 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
|
|||||||
reloadSettings();
|
reloadSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isFirstLoad = true;
|
||||||
fun reloadSettings() {
|
fun reloadSettings() {
|
||||||
_loader.start();
|
val firstLoad = isFirstLoad;
|
||||||
|
isFirstLoad = false;
|
||||||
|
_form.setSearchVisible(false);
|
||||||
|
_loaderView.start();
|
||||||
_form.fromObject(lifecycleScope, Settings.instance) {
|
_form.fromObject(lifecycleScope, Settings.instance) {
|
||||||
_loader.stop();
|
_loaderView.stop();
|
||||||
|
_form.setSearchVisible(true);
|
||||||
|
|
||||||
var devCounter = 0;
|
var devCounter = 0;
|
||||||
_form.findField("code")?.assume<ReadOnlyTextField>()?.setOnClickListener {
|
_form.findField("code")?.assume<ReadOnlyTextField>()?.setOnClickListener {
|
||||||
@@ -70,9 +129,16 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
|
|||||||
SettingsDev.instance.developerMode = true;
|
SettingsDev.instance.developerMode = true;
|
||||||
SettingsDev.instance.save();
|
SettingsDev.instance.save();
|
||||||
updateDevMode();
|
updateDevMode();
|
||||||
UIDialogs.toast(this, "You are now in developer mode");
|
UIDialogs.toast(this, getString(R.string.you_are_now_in_developer_mode));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if(firstLoad) {
|
||||||
|
val query = intent.getStringExtra("query");
|
||||||
|
if(!query.isNullOrEmpty()) {
|
||||||
|
_form.setSearchQuery(query);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,6 +184,7 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
|
|||||||
resultLauncher.launch(intent);
|
resultLauncher.launch(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
//TODO: Temporary for solving Settings issues
|
//TODO: Temporary for solving Settings issues
|
||||||
@SuppressLint("StaticFieldLeak")
|
@SuppressLint("StaticFieldLeak")
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ import okhttp3.Response
|
|||||||
import okhttp3.ResponseBody
|
import okhttp3.ResponseBody
|
||||||
import okhttp3.WebSocket
|
import okhttp3.WebSocket
|
||||||
import okhttp3.WebSocketListener
|
import okhttp3.WebSocketListener
|
||||||
import java.util.Dictionary
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import kotlin.system.measureTimeMillis
|
import kotlin.system.measureTimeMillis
|
||||||
|
|
||||||
open class ManagedHttpClient {
|
open class ManagedHttpClient {
|
||||||
@@ -60,7 +58,7 @@ open class ManagedHttpClient {
|
|||||||
|
|
||||||
val requestBuilder: okhttp3.Request.Builder = okhttp3.Request.Builder()
|
val requestBuilder: okhttp3.Request.Builder = okhttp3.Request.Builder()
|
||||||
.url(url);
|
.url(url);
|
||||||
if(user_agent != null && !user_agent.isEmpty() && !headers.any { it.key.lowercase() == "user-agent" })
|
if(user_agent.isNotEmpty() && !headers.any { it.key.lowercase() == "user-agent" })
|
||||||
requestBuilder.addHeader("User-Agent", user_agent)
|
requestBuilder.addHeader("User-Agent", user_agent)
|
||||||
|
|
||||||
for (pair in headers.entries)
|
for (pair in headers.entries)
|
||||||
@@ -137,7 +135,7 @@ open class ManagedHttpClient {
|
|||||||
val requestBuilder: okhttp3.Request.Builder = okhttp3.Request.Builder()
|
val requestBuilder: okhttp3.Request.Builder = okhttp3.Request.Builder()
|
||||||
.method(request.method, requestBody)
|
.method(request.method, requestBody)
|
||||||
.url(request.url);
|
.url(request.url);
|
||||||
if(user_agent != null && !user_agent.isEmpty() && !request.headers.any { it.key.lowercase() == "user-agent" })
|
if(user_agent.isNotEmpty() && !request.headers.any { it.key.lowercase() == "user-agent" })
|
||||||
requestBuilder.addHeader("User-Agent", user_agent)
|
requestBuilder.addHeader("User-Agent", user_agent)
|
||||||
|
|
||||||
for (pair in request.headers.entries)
|
for (pair in request.headers.entries)
|
||||||
@@ -148,7 +146,7 @@ open class ManagedHttpClient {
|
|||||||
|
|
||||||
val time = measureTimeMillis {
|
val time = measureTimeMillis {
|
||||||
val call = client.newCall(requestBuilder.build());
|
val call = client.newCall(requestBuilder.build());
|
||||||
request.onCallCreated?.emit(call);
|
request.onCallCreated.emit(call);
|
||||||
response = call.execute()
|
response = call.execute()
|
||||||
resp = Response(
|
resp = Response(
|
||||||
response.code,
|
response.code,
|
||||||
|
|||||||
@@ -8,16 +8,20 @@ import kotlinx.serialization.decodeFromString
|
|||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.io.StringWriter
|
import java.io.StringWriter
|
||||||
import java.net.SocketTimeoutException
|
import java.net.SocketTimeoutException
|
||||||
|
|
||||||
class HttpContext : AutoCloseable {
|
class HttpContext : AutoCloseable {
|
||||||
private val _stream: BufferedReader;
|
private val _inputStream: InputStream;
|
||||||
private var _responseStream: OutputStream? = null;
|
private var _responseStream: OutputStream? = null;
|
||||||
|
|
||||||
var id: String? = null;
|
var id: String? = null;
|
||||||
|
|
||||||
var head: String = "";
|
var head: String = "";
|
||||||
var headers: HttpHeaders = HttpHeaders();
|
var headers: HttpHeaders = HttpHeaders();
|
||||||
|
|
||||||
@@ -39,76 +43,130 @@ class HttpContext : AutoCloseable {
|
|||||||
private val _responseHeaders: HttpHeaders = HttpHeaders();
|
private val _responseHeaders: HttpHeaders = HttpHeaders();
|
||||||
|
|
||||||
|
|
||||||
constructor(stream: BufferedReader, responseStream: OutputStream? = null, requestId: String? = null, timeout: Int? = null) {
|
constructor(inputStream: InputStream, responseStream: OutputStream? = null, requestId: String? = null, timeout: Int? = null) {
|
||||||
_stream = stream;
|
_inputStream = inputStream;
|
||||||
_responseStream = responseStream;
|
_responseStream = responseStream;
|
||||||
this.id = requestId;
|
this.id = requestId;
|
||||||
|
|
||||||
try {
|
val headerBytes = readHeaderBytes()
|
||||||
head = stream.readLine() ?: throw EmptyRequestException("No head found");
|
ByteArrayInputStream(headerBytes).use {
|
||||||
}
|
val reader = it.bufferedReader(Charsets.UTF_8)
|
||||||
catch(ex: SocketTimeoutException) {
|
try {
|
||||||
if((timeout ?: 0) > 0)
|
head = reader.readLine() ?: throw EmptyRequestException("No head found");
|
||||||
throw KeepAliveTimeoutException("Keep-Alive timedout", ex);
|
}
|
||||||
throw ex;
|
catch(ex: SocketTimeoutException) {
|
||||||
}
|
if((timeout ?: 0) > 0)
|
||||||
|
throw KeepAliveTimeoutException("Keep-Alive timedout", ex);
|
||||||
val methodEndIndex = head.indexOf(' ');
|
throw ex;
|
||||||
val urlEndIndex = head.indexOf(' ', methodEndIndex + 1);
|
|
||||||
if (methodEndIndex == -1 || urlEndIndex == -1) {
|
|
||||||
Logger.w(TAG, "Skipped request, wrong format.");
|
|
||||||
throw IllegalStateException("Invalid request");
|
|
||||||
}
|
|
||||||
|
|
||||||
method = head.substring(0, methodEndIndex);
|
|
||||||
path = head.substring(methodEndIndex + 1, urlEndIndex);
|
|
||||||
|
|
||||||
if (path.contains("?")) {
|
|
||||||
val queryPartIndex = path.indexOf("?");
|
|
||||||
val queryParts = path.substring(queryPartIndex + 1).split("&");
|
|
||||||
path = path.substring(0, queryPartIndex);
|
|
||||||
|
|
||||||
for(queryPart in queryParts) {
|
|
||||||
val eqIndex = queryPart.indexOf("=");
|
|
||||||
if(eqIndex > 0)
|
|
||||||
query.put(queryPart.substring(0, eqIndex), queryPart.substring(eqIndex + 1));
|
|
||||||
else
|
|
||||||
query.put(queryPart, "");
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
while (true) {
|
val methodEndIndex = head.indexOf(' ');
|
||||||
val line = stream.readLine();
|
val urlEndIndex = head.indexOf(' ', methodEndIndex + 1);
|
||||||
val headerEndIndex = line.indexOf(":");
|
if (methodEndIndex == -1 || urlEndIndex == -1) {
|
||||||
if (headerEndIndex == -1)
|
Logger.w(TAG, "Skipped request, wrong format.");
|
||||||
break;
|
throw IllegalStateException("Invalid request");
|
||||||
|
}
|
||||||
|
|
||||||
val headerKey = line.substring(0, headerEndIndex).lowercase()
|
method = head.substring(0, methodEndIndex);
|
||||||
val headerValue = line.substring(headerEndIndex + 1).trim();
|
path = head.substring(methodEndIndex + 1, urlEndIndex);
|
||||||
headers[headerKey] = headerValue;
|
|
||||||
|
|
||||||
when(headerKey) {
|
if (path.contains("?")) {
|
||||||
"content-length" -> contentLength = headerValue.toLong();
|
val queryPartIndex = path.indexOf("?");
|
||||||
"content-type" -> contentType = headerValue;
|
val queryParts = path.substring(queryPartIndex + 1).split("&");
|
||||||
"connection" -> keepAlive = headerValue.lowercase() == "keep-alive";
|
path = path.substring(0, queryPartIndex);
|
||||||
"keep-alive" -> {
|
|
||||||
val keepAliveParams = headerValue.split(",");
|
for(queryPart in queryParts) {
|
||||||
for(keepAliveParam in keepAliveParams) {
|
val eqIndex = queryPart.indexOf("=");
|
||||||
val eqIndex = keepAliveParam.indexOf("=");
|
if(eqIndex > 0)
|
||||||
if(eqIndex > 0){
|
query.put(queryPart.substring(0, eqIndex), queryPart.substring(eqIndex + 1));
|
||||||
when(keepAliveParam.substring(0, eqIndex)) {
|
else
|
||||||
"timeout" -> keepAliveTimeout = keepAliveParam.substring(eqIndex+1).toInt();
|
query.put(queryPart, "");
|
||||||
"max" -> keepAliveTimeout = keepAliveParam.substring(eqIndex+1).toInt();
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
val line = reader.readLine();
|
||||||
|
val headerEndIndex = line.indexOf(":");
|
||||||
|
if (headerEndIndex == -1)
|
||||||
|
break;
|
||||||
|
|
||||||
|
val headerKey = line.substring(0, headerEndIndex).lowercase()
|
||||||
|
val headerValue = line.substring(headerEndIndex + 1).trim();
|
||||||
|
headers[headerKey] = headerValue;
|
||||||
|
|
||||||
|
when(headerKey) {
|
||||||
|
"content-length" -> contentLength = headerValue.toLong();
|
||||||
|
"content-type" -> contentType = headerValue;
|
||||||
|
"connection" -> keepAlive = headerValue.lowercase() == "keep-alive";
|
||||||
|
"keep-alive" -> {
|
||||||
|
val keepAliveParams = headerValue.split(",");
|
||||||
|
for(keepAliveParam in keepAliveParams) {
|
||||||
|
val eqIndex = keepAliveParam.indexOf("=");
|
||||||
|
if(eqIndex > 0){
|
||||||
|
when(keepAliveParam.substring(0, eqIndex)) {
|
||||||
|
"timeout" -> keepAliveTimeout = keepAliveParam.substring(eqIndex+1).toInt();
|
||||||
|
"max" -> keepAliveTimeout = keepAliveParam.substring(eqIndex+1).toInt();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if(line.isNullOrEmpty())
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
if(line.isNullOrEmpty())
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun readHeaderBytes(): ByteArray {
|
||||||
|
val headerBytes = ByteArrayOutputStream()
|
||||||
|
var crlfCount = 0
|
||||||
|
|
||||||
|
while (crlfCount < 4) {
|
||||||
|
val b = _inputStream.read()
|
||||||
|
if (b == -1) {
|
||||||
|
throw IOException("Unexpected end of stream while reading headers")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (b == 0x0D || b == 0x0A) { // CR or LF
|
||||||
|
crlfCount++
|
||||||
|
} else {
|
||||||
|
crlfCount = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
headerBytes.write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
return headerBytes.toByteArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readContentBytes(buffer: ByteArray, length: Int): Int {
|
||||||
|
val remainingBytes = (contentLength - _totalRead).coerceAtMost(length.toLong()).toInt()
|
||||||
|
val read = _inputStream.read(buffer, 0, remainingBytes);
|
||||||
|
if (read > 0) {
|
||||||
|
_totalRead += read
|
||||||
|
}
|
||||||
|
|
||||||
|
return read;
|
||||||
|
}
|
||||||
|
fun readContentString(): String {
|
||||||
|
val byteArrayOutputStream = ByteArrayOutputStream()
|
||||||
|
val buffer = ByteArray(4096)
|
||||||
|
var read: Int
|
||||||
|
while (true) {
|
||||||
|
read = readContentBytes(buffer, buffer.size)
|
||||||
|
if (read <= 0) break
|
||||||
|
byteArrayOutputStream.write(buffer, 0, read)
|
||||||
|
}
|
||||||
|
return byteArrayOutputStream.toString(Charsets.UTF_8.name())
|
||||||
|
}
|
||||||
|
inline fun <reified T> readContentJson() : T {
|
||||||
|
return Serializer.json.decodeFromString(readContentString());
|
||||||
|
}
|
||||||
|
fun skipBody() {
|
||||||
|
if (contentLength > 0)
|
||||||
|
_inputStream.skip(contentLength - _totalRead)
|
||||||
|
}
|
||||||
|
|
||||||
fun getHttpHeaderString(): String {
|
fun getHttpHeaderString(): String {
|
||||||
val writer = StringWriter();
|
val writer = StringWriter();
|
||||||
writer.write(head + "\r\n");
|
writer.write(head + "\r\n");
|
||||||
@@ -139,8 +197,13 @@ class HttpContext : AutoCloseable {
|
|||||||
}
|
}
|
||||||
fun respondCode(status: Int, headers: HttpHeaders, body: String? = null) {
|
fun respondCode(status: Int, headers: HttpHeaders, body: String? = null) {
|
||||||
val bytes = body?.toByteArray(Charsets.UTF_8);
|
val bytes = body?.toByteArray(Charsets.UTF_8);
|
||||||
if(body != null && headers.get("content-length").isNullOrEmpty())
|
if(headers.get("content-length").isNullOrEmpty()) {
|
||||||
headers.put("content-length", bytes!!.size.toString());
|
if (body != null) {
|
||||||
|
headers.put("content-length", bytes!!.size.toString());
|
||||||
|
} else {
|
||||||
|
headers.put("content-length", "0")
|
||||||
|
}
|
||||||
|
}
|
||||||
respond(status, headers) { responseStream ->
|
respond(status, headers) { responseStream ->
|
||||||
if(body != null) {
|
if(body != null) {
|
||||||
responseStream.write(bytes!!);
|
responseStream.write(bytes!!);
|
||||||
@@ -161,8 +224,7 @@ class HttpContext : AutoCloseable {
|
|||||||
headersToRespond.put("keep-alive", "timeout=5, max=1000");
|
headersToRespond.put("keep-alive", "timeout=5, max=1000");
|
||||||
}
|
}
|
||||||
|
|
||||||
val responseHeader = HttpResponse(status, headers);
|
val responseHeader = HttpResponse(status, headersToRespond);
|
||||||
|
|
||||||
responseStream.write(responseHeader.getHttpHeaderBytes());
|
responseStream.write(responseHeader.getHttpHeaderBytes());
|
||||||
|
|
||||||
if(method != "HEAD") {
|
if(method != "HEAD") {
|
||||||
@@ -172,38 +234,9 @@ class HttpContext : AutoCloseable {
|
|||||||
statusCode = status;
|
statusCode = status;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun readContentBytes(buffer: CharArray, length: Int) : Int {
|
|
||||||
val reading = Math.min(length, (contentLength - _totalRead).toInt());
|
|
||||||
val read = _stream.read(buffer, 0, reading);
|
|
||||||
_totalRead += read;
|
|
||||||
|
|
||||||
//TODO: Fix this properly
|
|
||||||
if(contentLength - _totalRead < 400 && read < length) {
|
|
||||||
_totalRead = contentLength;
|
|
||||||
}
|
|
||||||
return read;
|
|
||||||
}
|
|
||||||
fun readContentString() : String{
|
|
||||||
val writer = StringWriter();
|
|
||||||
var read = 0;
|
|
||||||
val buffer = CharArray(4096);
|
|
||||||
do {
|
|
||||||
read = readContentBytes(buffer, buffer.size);
|
|
||||||
writer.write(buffer, 0, read);
|
|
||||||
} while(read > 0);
|
|
||||||
return writer.toString();
|
|
||||||
}
|
|
||||||
inline fun <reified T> readContentJson() : T {
|
|
||||||
return Serializer.json.decodeFromString(readContentString());
|
|
||||||
}
|
|
||||||
fun skipBody() {
|
|
||||||
if(contentLength > 0)
|
|
||||||
_stream.skip(contentLength - _totalRead);
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun close() {
|
override fun close() {
|
||||||
if(!keepAlive) {
|
if(!keepAlive) {
|
||||||
_stream?.close();
|
_inputStream.close();
|
||||||
_responseStream?.close();
|
_responseStream?.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
package com.futo.platformplayer.api.http.server
|
package com.futo.platformplayer.api.http.server
|
||||||
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.api.http.server.exceptions.EmptyRequestException
|
import com.futo.platformplayer.api.http.server.exceptions.EmptyRequestException
|
||||||
import com.futo.platformplayer.api.http.server.handlers.HttpFuntionHandler
|
import com.futo.platformplayer.api.http.server.handlers.HttpFuntionHandler
|
||||||
import com.futo.platformplayer.api.http.server.handlers.HttpHandler
|
import com.futo.platformplayer.api.http.server.handlers.HttpHandler
|
||||||
import java.io.BufferedReader
|
import com.futo.platformplayer.api.http.server.handlers.HttpOptionsAllowHandler
|
||||||
import java.io.InputStreamReader
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import java.io.BufferedInputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.lang.reflect.Field
|
import java.lang.reflect.Field
|
||||||
import java.lang.reflect.Method
|
import java.lang.reflect.Method
|
||||||
@@ -14,7 +14,7 @@ import java.net.InetAddress
|
|||||||
import java.net.NetworkInterface
|
import java.net.NetworkInterface
|
||||||
import java.net.ServerSocket
|
import java.net.ServerSocket
|
||||||
import java.net.Socket
|
import java.net.Socket
|
||||||
import java.util.*
|
import java.util.UUID
|
||||||
import java.util.concurrent.ExecutorService
|
import java.util.concurrent.ExecutorService
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
import java.util.stream.IntStream.range
|
import java.util.stream.IntStream.range
|
||||||
@@ -29,7 +29,8 @@ class ManagedHttpServer(private val _requestedPort: Int = 0) {
|
|||||||
var port = 0
|
var port = 0
|
||||||
private set;
|
private set;
|
||||||
|
|
||||||
private val _handlers = mutableListOf<HttpHandler>();
|
private val _handlers = hashMapOf<String, HashMap<String, HttpHandler>>()
|
||||||
|
private val _headHandlers = hashMapOf<String, HttpHandler>()
|
||||||
private var _workerPool: ExecutorService? = null;
|
private var _workerPool: ExecutorService? = null;
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
@@ -76,12 +77,12 @@ class ManagedHttpServer(private val _requestedPort: Int = 0) {
|
|||||||
|
|
||||||
private fun handleClientRequest(socket: Socket) {
|
private fun handleClientRequest(socket: Socket) {
|
||||||
_workerPool?.submit {
|
_workerPool?.submit {
|
||||||
val requestReader = BufferedReader(InputStreamReader(socket.getInputStream()))
|
val requestStream = BufferedInputStream(socket.getInputStream());
|
||||||
val responseStream = socket.getOutputStream();
|
val responseStream = socket.getOutputStream();
|
||||||
|
|
||||||
val requestId = UUID.randomUUID().toString().substring(0, 5);
|
val requestId = UUID.randomUUID().toString().substring(0, 5);
|
||||||
try {
|
try {
|
||||||
keepAliveLoop(requestReader, responseStream, requestId) { req ->
|
keepAliveLoop(requestStream, responseStream, requestId) { req ->
|
||||||
req.use { httpContext ->
|
req.use { httpContext ->
|
||||||
if(!httpContext.path.startsWith("/plugin/"))
|
if(!httpContext.path.startsWith("/plugin/"))
|
||||||
Logger.i(TAG, "[${req.id}] ${httpContext.method}: ${httpContext.path}")
|
Logger.i(TAG, "[${req.id}] ${httpContext.method}: ${httpContext.path}")
|
||||||
@@ -107,7 +108,7 @@ class ManagedHttpServer(private val _requestedPort: Int = 0) {
|
|||||||
Logger.e(TAG, "Failed to handle client request.", e);
|
Logger.e(TAG, "Failed to handle client request.", e);
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
requestReader.close();
|
requestStream.close();
|
||||||
responseStream.close();
|
responseStream.close();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -115,36 +116,82 @@ class ManagedHttpServer(private val _requestedPort: Int = 0) {
|
|||||||
|
|
||||||
fun getHandler(method: String, path: String) : HttpHandler? {
|
fun getHandler(method: String, path: String) : HttpHandler? {
|
||||||
synchronized(_handlers) {
|
synchronized(_handlers) {
|
||||||
//TODO: Support regex paths?
|
if (method == "HEAD") {
|
||||||
if(method == "HEAD")
|
return _headHandlers[path]
|
||||||
return _handlers.firstOrNull { it.path == path && (it.allowHEAD || it.method == "HEAD") }
|
}
|
||||||
return _handlers.firstOrNull { it.method == method && it.path == path };
|
|
||||||
|
val handlerMap = _handlers[method] ?: return null
|
||||||
|
return handlerMap[path]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun addHandler(handler: HttpHandler, withHEAD: Boolean = false) : HttpHandler {
|
fun addHandler(handler: HttpHandler, withHEAD: Boolean = false) : HttpHandler {
|
||||||
synchronized(_handlers) {
|
synchronized(_handlers) {
|
||||||
_handlers.add(handler);
|
|
||||||
handler.allowHEAD = withHEAD;
|
handler.allowHEAD = withHEAD;
|
||||||
|
|
||||||
|
var handlerMap: HashMap<String, HttpHandler>? = _handlers[handler.method];
|
||||||
|
if (handlerMap == null) {
|
||||||
|
handlerMap = hashMapOf()
|
||||||
|
_handlers[handler.method] = handlerMap
|
||||||
|
}
|
||||||
|
|
||||||
|
handlerMap[handler.path] = handler;
|
||||||
|
if (handler.allowHEAD || handler.method == "HEAD") {
|
||||||
|
_headHandlers[handler.path] = handler
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return handler;
|
return handler;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun addHandlerWithAllowAllOptions(handler: HttpHandler, withHEAD: Boolean = false) : HttpHandler {
|
||||||
|
val allowedMethods = arrayListOf(handler.method, "OPTIONS")
|
||||||
|
if (withHEAD) {
|
||||||
|
allowedMethods.add("HEAD")
|
||||||
|
}
|
||||||
|
|
||||||
|
val tag = handler.tag
|
||||||
|
if (tag != null) {
|
||||||
|
addHandler(HttpOptionsAllowHandler(handler.path, allowedMethods).withTag(tag))
|
||||||
|
} else {
|
||||||
|
addHandler(HttpOptionsAllowHandler(handler.path, allowedMethods))
|
||||||
|
}
|
||||||
|
|
||||||
|
return addHandler(handler, withHEAD)
|
||||||
|
}
|
||||||
|
|
||||||
fun removeHandler(method: String, path: String) {
|
fun removeHandler(method: String, path: String) {
|
||||||
synchronized(_handlers) {
|
synchronized(_handlers) {
|
||||||
val handler = getHandler(method, path);
|
val handlerMap = _handlers[method] ?: return
|
||||||
if(handler != null)
|
val handler = handlerMap.remove(path) ?: return
|
||||||
_handlers.remove(handler);
|
if (method == "HEAD" || handler.allowHEAD) {
|
||||||
|
_headHandlers.remove(path)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun removeAllHandlers(tag: String? = null) {
|
fun removeAllHandlers(tag: String? = null) {
|
||||||
synchronized(_handlers) {
|
synchronized(_handlers) {
|
||||||
if(tag == null)
|
if(tag == null)
|
||||||
_handlers.clear();
|
_handlers.clear();
|
||||||
else
|
else {
|
||||||
_handlers.removeIf { it.tag == tag };
|
for (pair in _handlers) {
|
||||||
|
val toRemove = ArrayList<String>()
|
||||||
|
for (innerPair in pair.value) {
|
||||||
|
if (innerPair.value.tag == tag) {
|
||||||
|
toRemove.add(innerPair.key)
|
||||||
|
|
||||||
|
if (pair.key == "HEAD" || innerPair.value.allowHEAD) {
|
||||||
|
_headHandlers.remove(innerPair.key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (x in toRemove)
|
||||||
|
pair.value.remove(x)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun addBridgeHandlers(obj: Any, tag: String? = null) {
|
fun addBridgeHandlers(obj: Any, tag: String? = null) {
|
||||||
val tagToUse = tag ?: obj.javaClass.name;
|
//val tagToUse = tag ?: obj.javaClass.name;
|
||||||
val getMethods = obj::class.java.declaredMethods
|
val getMethods = obj::class.java.declaredMethods
|
||||||
.filter { it.getAnnotation(HttpGET::class.java) != null }
|
.filter { it.getAnnotation(HttpGET::class.java) != null }
|
||||||
.map { Pair<Method, HttpGET>(it, it.getAnnotation(HttpGET::class.java)!!) }
|
.map { Pair<Method, HttpGET>(it, it.getAnnotation(HttpGET::class.java)!!) }
|
||||||
@@ -164,13 +211,13 @@ class ManagedHttpServer(private val _requestedPort: Int = 0) {
|
|||||||
addHandler(HttpFuntionHandler("GET", getMethod.second.path) { getMethod.first.invoke(obj, it) }).apply {
|
addHandler(HttpFuntionHandler("GET", getMethod.second.path) { getMethod.first.invoke(obj, it) }).apply {
|
||||||
if(!getMethod.second.contentType.isEmpty())
|
if(!getMethod.second.contentType.isEmpty())
|
||||||
this.withContentType(getMethod.second.contentType);
|
this.withContentType(getMethod.second.contentType);
|
||||||
}.withContentType(getMethod.second.contentType ?: "");
|
}.withContentType(getMethod.second.contentType);
|
||||||
for(postMethod in postMethods)
|
for(postMethod in postMethods)
|
||||||
if(postMethod.first.parameterTypes.firstOrNull() == HttpContext::class.java && postMethod.first.parameterCount == 1)
|
if(postMethod.first.parameterTypes.firstOrNull() == HttpContext::class.java && postMethod.first.parameterCount == 1)
|
||||||
addHandler(HttpFuntionHandler("POST", postMethod.second.path) { postMethod.first.invoke(obj, it) }).apply {
|
addHandler(HttpFuntionHandler("POST", postMethod.second.path) { postMethod.first.invoke(obj, it) }).apply {
|
||||||
if(!postMethod.second.contentType.isEmpty())
|
if(!postMethod.second.contentType.isEmpty())
|
||||||
this.withContentType(postMethod.second.contentType);
|
this.withContentType(postMethod.second.contentType);
|
||||||
}.withContentType(postMethod.second.contentType ?: "");
|
}.withContentType(postMethod.second.contentType);
|
||||||
|
|
||||||
for(getField in getFields) {
|
for(getField in getFields) {
|
||||||
getField.first.isAccessible = true;
|
getField.first.isAccessible = true;
|
||||||
@@ -184,13 +231,13 @@ class ManagedHttpServer(private val _requestedPort: Int = 0) {
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
it.respondCode(204);
|
it.respondCode(204);
|
||||||
}).withContentType(getField.second.contentType ?: "");
|
}).withContentType(getField.second.contentType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun keepAliveLoop(requestReader: BufferedReader, responseStream: OutputStream, requestId: String, handler: (HttpContext)->Unit) {
|
private fun keepAliveLoop(requestReader: BufferedInputStream, responseStream: OutputStream, requestId: String, handler: (HttpContext)->Unit) {
|
||||||
val stopCount = _stopCount;
|
val stopCount = _stopCount;
|
||||||
var keepAlive = false;
|
var keepAlive: Boolean;
|
||||||
var requestsMax = 0;
|
var requestsMax = 0;
|
||||||
var requestsTotal = 0;
|
var requestsTotal = 0;
|
||||||
do {
|
do {
|
||||||
@@ -240,11 +287,13 @@ class ManagedHttpServer(private val _requestedPort: Int = 0) {
|
|||||||
for (intf in NetworkInterface.getNetworkInterfaces()) {
|
for (intf in NetworkInterface.getNetworkInterfaces()) {
|
||||||
for (addr in intf.inetAddresses) {
|
for (addr in intf.inetAddresses) {
|
||||||
if (!addr.isLoopbackAddress) {
|
if (!addr.isLoopbackAddress) {
|
||||||
val ipString: String = addr.hostAddress;
|
val ipString: String = addr.hostAddress ?: continue
|
||||||
val isIPv4 = ipString.indexOf(':') < 0;
|
val isIPv4 = ipString.indexOf(':') < 0
|
||||||
if (!isIPv4)
|
if (!isIPv4) {
|
||||||
continue;
|
continue
|
||||||
addresses.add(addr);
|
}
|
||||||
|
|
||||||
|
addresses.add(addr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-3
@@ -1,6 +1,3 @@
|
|||||||
package com.futo.platformplayer.api.http.server.exceptions
|
package com.futo.platformplayer.api.http.server.exceptions
|
||||||
|
|
||||||
import java.net.SocketTimeoutException
|
|
||||||
import java.util.concurrent.TimeoutException
|
|
||||||
|
|
||||||
class EmptyRequestException(msg: String) : Exception(msg) {}
|
class EmptyRequestException(msg: String) : Exception(msg) {}
|
||||||
-1
@@ -7,7 +7,6 @@ class HttpConstantHandler(method: String, path: String, val content: String, val
|
|||||||
val headers = this.headers.clone();
|
val headers = this.headers.clone();
|
||||||
if(contentType != null)
|
if(contentType != null)
|
||||||
headers["Content-Type"] = contentType;
|
headers["Content-Type"] = contentType;
|
||||||
headers["Content-Length"] = content.length.toString();
|
|
||||||
|
|
||||||
httpContext.respondCode(200, headers, content);
|
httpContext.respondCode(200, headers, content);
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-22
@@ -1,14 +1,16 @@
|
|||||||
package com.futo.platformplayer.api.http.server.handlers
|
package com.futo.platformplayer.api.http.server.handlers
|
||||||
|
|
||||||
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.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.zip.GZIPOutputStream
|
import java.util.zip.GZIPOutputStream
|
||||||
|
|
||||||
class HttpFileHandler(method: String, path: String, private val contentType: String, private val filePath: String, private val closeAfterRequest: Boolean = false): HttpHandler(method, path) {
|
class HttpFileHandler(method: String, path: String, private val contentType: String, private val filePath: String): HttpHandler(method, path) {
|
||||||
override fun handle(httpContext: HttpContext) {
|
override fun handle(httpContext: HttpContext) {
|
||||||
val requestHeaders = httpContext.headers;
|
val requestHeaders = httpContext.headers;
|
||||||
val responseHeaders = this.headers.clone();
|
val responseHeaders = this.headers.clone();
|
||||||
@@ -30,19 +32,13 @@ class HttpFileHandler(method: String, path: String, private val contentType: Str
|
|||||||
|
|
||||||
responseHeaders["Content-Disposition"] = "attachment; filename=\"${file.name.replace("\"", "\\\"")}\""
|
responseHeaders["Content-Disposition"] = "attachment; filename=\"${file.name.replace("\"", "\\\"")}\""
|
||||||
|
|
||||||
val acceptEncoding = requestHeaders["Accept-Encoding"]
|
|
||||||
val shouldGzip = acceptEncoding != null && acceptEncoding.split(',').any { it.trim().equals("gzip", ignoreCase = true) || it == "*" }
|
|
||||||
if (shouldGzip) {
|
|
||||||
responseHeaders["Content-Encoding"] = "gzip"
|
|
||||||
}
|
|
||||||
|
|
||||||
val range = requestHeaders["Range"]
|
val range = requestHeaders["Range"]
|
||||||
var start: Long
|
val start: Long
|
||||||
val end: Long
|
val end: Long
|
||||||
if (range != null && range.startsWith("bytes=")) {
|
if (range != null && range.startsWith("bytes=")) {
|
||||||
val parts = range.substring(6).split("-")
|
val parts = range.substring(6).split("-")
|
||||||
start = parts[0].toLong()
|
start = parts[0].toLong()
|
||||||
end = parts.getOrNull(1)?.toLong() ?: (file.length() - 1)
|
end = parts.getOrNull(1)?.toLongOrNull() ?: (file.length() - 1)
|
||||||
responseHeaders["Content-Range"] = "bytes $start-$end/${file.length()}"
|
responseHeaders["Content-Range"] = "bytes $start-$end/${file.length()}"
|
||||||
} else {
|
} else {
|
||||||
start = 0
|
start = 0
|
||||||
@@ -51,18 +47,19 @@ class HttpFileHandler(method: String, path: String, private val contentType: Str
|
|||||||
|
|
||||||
var totalBytesSent = 0
|
var totalBytesSent = 0
|
||||||
val contentLength = end - start + 1
|
val contentLength = end - start + 1
|
||||||
Logger.i(TAG, "Sending $contentLength bytes (start: $start, end: $end, shouldGzip: $shouldGzip)")
|
|
||||||
responseHeaders["Content-Length"] = contentLength.toString()
|
responseHeaders["Content-Length"] = contentLength.toString()
|
||||||
|
Logger.i(TAG, "Sending $contentLength bytes (start: $start, end: $end)")
|
||||||
|
|
||||||
file.inputStream().use { inputStream ->
|
file.inputStream().use { inputStream ->
|
||||||
httpContext.respond(if (range == null) 200 else 206, responseHeaders) { responseStream ->
|
httpContext.respond(if (range != null) 206 else 200, responseHeaders) { responseStream ->
|
||||||
try {
|
try {
|
||||||
val buffer = ByteArray(8192)
|
val buffer = ByteArray(8192)
|
||||||
inputStream.skip(start)
|
inputStream.skip(start)
|
||||||
|
var current = start
|
||||||
|
|
||||||
val outputStream = if (shouldGzip) GZIPOutputStream(responseStream) else responseStream
|
val outputStream = responseStream
|
||||||
while (true) {
|
while (true) {
|
||||||
val expectedBytesRead = (end - start + 1).coerceAtMost(buffer.size.toLong());
|
val expectedBytesRead = (end - current + 1).coerceAtMost(buffer.size.toLong());
|
||||||
val bytesRead = inputStream.read(buffer);
|
val bytesRead = inputStream.read(buffer);
|
||||||
if (bytesRead < 0) {
|
if (bytesRead < 0) {
|
||||||
Logger.i(TAG, "End of file reached")
|
Logger.i(TAG, "End of file reached")
|
||||||
@@ -73,27 +70,21 @@ class HttpFileHandler(method: String, path: String, private val contentType: Str
|
|||||||
outputStream.write(buffer, 0, bytesToSend)
|
outputStream.write(buffer, 0, bytesToSend)
|
||||||
|
|
||||||
totalBytesSent += bytesToSend
|
totalBytesSent += bytesToSend
|
||||||
Logger.v(TAG, "Sent bytes $start-${start + bytesToSend}, totalBytesSent=$totalBytesSent")
|
Logger.v(TAG, "Sent bytes $current-${current + bytesToSend}, totalBytesSent=$totalBytesSent")
|
||||||
|
|
||||||
start += bytesToSend.toLong()
|
current += bytesToSend.toLong()
|
||||||
if (start >= end) {
|
if (current >= end) {
|
||||||
Logger.i(TAG, "Expected amount of bytes sent")
|
Logger.i(TAG, "Expected amount of bytes sent")
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.i(TAG, "Finished sending file (segment)")
|
Logger.i(TAG, "Finished sending file (segment)")
|
||||||
|
|
||||||
if (shouldGzip) (outputStream as GZIPOutputStream).finish()
|
|
||||||
outputStream.flush()
|
outputStream.flush()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
httpContext.respondCode(500, headers)
|
httpContext.respondCode(500, headers)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (closeAfterRequest) {
|
|
||||||
httpContext.keepAlive = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ abstract class HttpHandler(val method: String, val path: String) {
|
|||||||
headers.put(key, value);
|
headers.put(key, value);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun withContentType(contentType: String) = withHeader("Content-Type", contentType);
|
fun withContentType(contentType: String) = withHeader("Content-Type", contentType);
|
||||||
|
|
||||||
fun withTag(tag: String) : HttpHandler {
|
fun withTag(tag: String) : HttpHandler {
|
||||||
|
|||||||
+9
-10
@@ -2,19 +2,18 @@ package com.futo.platformplayer.api.http.server.handlers
|
|||||||
|
|
||||||
import com.futo.platformplayer.api.http.server.HttpContext
|
import com.futo.platformplayer.api.http.server.HttpContext
|
||||||
|
|
||||||
class HttpOptionsAllowHandler(path: String) : HttpHandler("OPTIONS", path) {
|
class HttpOptionsAllowHandler(path: String, val allowedMethods: List<String> = listOf()) : HttpHandler("OPTIONS", path) {
|
||||||
override fun handle(httpContext: HttpContext) {
|
override fun handle(httpContext: HttpContext) {
|
||||||
//Just allow whatever is requested
|
val newHeaders = headers.clone()
|
||||||
|
newHeaders.put("Access-Control-Allow-Origin", "*")
|
||||||
|
|
||||||
val requestedOrigin = httpContext.headers.getOrDefault("Access-Control-Request-Origin", "");
|
if (allowedMethods.isNotEmpty()) {
|
||||||
val requestedMethods = httpContext.headers.getOrDefault("Access-Control-Request-Method", "");
|
newHeaders.put("Access-Control-Allow-Methods", allowedMethods.map { it.uppercase() }.joinToString(", "))
|
||||||
val requestedHeaders = httpContext.headers.getOrDefault("Access-Control-Request-Headers", "");
|
} else {
|
||||||
|
newHeaders.put("Access-Control-Allow-Methods", "*")
|
||||||
val newHeaders = headers.clone();
|
}
|
||||||
newHeaders.put("Allow", requestedMethods);
|
|
||||||
newHeaders.put("Access-Control-Allow-Methods", requestedMethods);
|
|
||||||
newHeaders.put("Access-Control-Allow-Headers", "*");
|
|
||||||
|
|
||||||
|
newHeaders.put("Access-Control-Allow-Headers", "*")
|
||||||
httpContext.respondCode(200, newHeaders);
|
httpContext.respondCode(200, newHeaders);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+160
-6
@@ -1,11 +1,20 @@
|
|||||||
package com.futo.platformplayer.api.http.server.handlers
|
package com.futo.platformplayer.api.http.server.handlers
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
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.logging.Logger
|
||||||
|
import com.futo.platformplayer.parsers.HttpResponseParser
|
||||||
|
import com.futo.platformplayer.readLine
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.lang.Exception
|
||||||
|
import java.net.Socket
|
||||||
|
import javax.net.ssl.SSLSocketFactory
|
||||||
|
|
||||||
class HttpProxyHandler(method: String, path: String, val targetUrl: String): HttpHandler(method, path) {
|
class HttpProxyHandler(method: String, path: String, val targetUrl: String, private val useTcp: Boolean = false): HttpHandler(method, path) {
|
||||||
var content: String? = null;
|
var content: String? = null;
|
||||||
var contentType: String? = null;
|
var contentType: String? = null;
|
||||||
|
|
||||||
@@ -17,10 +26,17 @@ class HttpProxyHandler(method: String, path: String, val targetUrl: String): Htt
|
|||||||
private var _injectHost = false;
|
private var _injectHost = false;
|
||||||
private var _injectReferer = false;
|
private var _injectReferer = false;
|
||||||
|
|
||||||
|
|
||||||
private val _client = ManagedHttpClient();
|
private val _client = ManagedHttpClient();
|
||||||
|
|
||||||
override fun handle(context: HttpContext) {
|
override fun handle(context: HttpContext) {
|
||||||
|
if (useTcp) {
|
||||||
|
handleWithTcp(context)
|
||||||
|
} else {
|
||||||
|
handleWithOkHttp(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleWithOkHttp(context: HttpContext) {
|
||||||
val proxyHeaders = HashMap<String, String>();
|
val proxyHeaders = HashMap<String, String>();
|
||||||
for (header in context.headers.filter { !_ignoreRequestHeaders.contains(it.key.lowercase()) })
|
for (header in context.headers.filter { !_ignoreRequestHeaders.contains(it.key.lowercase()) })
|
||||||
proxyHeaders[header.key] = header.value;
|
proxyHeaders[header.key] = header.value;
|
||||||
@@ -34,8 +50,8 @@ class HttpProxyHandler(method: String, path: String, val targetUrl: String): Htt
|
|||||||
proxyHeaders.put("Referer", targetUrl);
|
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, "Proxied Request ${useMethod}: ${targetUrl}");
|
Logger.i(TAG, "handleWithOkHttp Proxied Request ${useMethod}: ${targetUrl}");
|
||||||
//Logger.i(TAG, "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(targetUrl, proxyHeaders);
|
"GET" -> _client.get(targetUrl, proxyHeaders);
|
||||||
@@ -44,8 +60,8 @@ class HttpProxyHandler(method: String, path: String, val targetUrl: String): Htt
|
|||||||
else -> _client.requestMethod(useMethod, targetUrl, proxyHeaders);
|
else -> _client.requestMethod(useMethod, targetUrl, proxyHeaders);
|
||||||
};
|
};
|
||||||
|
|
||||||
//Logger.i(TAG, "Proxied Response [${resp.code}]");
|
Logger.i(TAG, "Proxied Response [${resp.code}]");
|
||||||
val headersFiltered = HttpHeaders(resp.getHeadersFlat().filter { !_ignoreRequestHeaders.contains(it.key.lowercase()) });
|
val headersFiltered = HttpHeaders(resp.getHeadersFlat().filter { !_ignoreResponseHeaders.contains(it.key.lowercase()) });
|
||||||
for(newHeader in headers)
|
for(newHeader in headers)
|
||||||
headersFiltered.put(newHeader.key, newHeader.value);
|
headersFiltered.put(newHeader.key, newHeader.value);
|
||||||
|
|
||||||
@@ -65,6 +81,140 @@ class HttpProxyHandler(method: String, path: String, val targetUrl: String): Htt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleWithTcp(context: HttpContext) {
|
||||||
|
if (content != null)
|
||||||
|
throw NotImplementedError("Content body is not supported")
|
||||||
|
|
||||||
|
val proxyHeaders = HashMap<String, String>();
|
||||||
|
for (header in context.headers.filter { !_ignoreRequestHeaders.contains(it.key.lowercase()) })
|
||||||
|
proxyHeaders[header.key] = header.value;
|
||||||
|
for (injectHeader in _injectRequestHeader)
|
||||||
|
proxyHeaders[injectHeader.first] = injectHeader.second;
|
||||||
|
|
||||||
|
val parsed = Uri.parse(targetUrl);
|
||||||
|
if(_injectHost)
|
||||||
|
proxyHeaders.put("Host", parsed.host!!);
|
||||||
|
if(_injectReferer)
|
||||||
|
proxyHeaders.put("Referer", targetUrl);
|
||||||
|
|
||||||
|
val useMethod = if (method == "inherit") context.method else method;
|
||||||
|
Logger.i(TAG, "handleWithTcp Proxied Request ${useMethod}: ${parsed}");
|
||||||
|
Logger.i(TAG, "handleWithTcp Headers:" + proxyHeaders.map { "${it.key}: ${it.value}" }.joinToString("\n"));
|
||||||
|
|
||||||
|
makeTcpRequest(proxyHeaders, useMethod, parsed, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun makeTcpRequest(proxyHeaders: HashMap<String, String>, useMethod: String, parsed: Uri, context: HttpContext) {
|
||||||
|
val requestBuilder = StringBuilder()
|
||||||
|
requestBuilder.append("$useMethod $parsed HTTP/1.1\r\n")
|
||||||
|
proxyHeaders.forEach { (key, value) -> requestBuilder.append("$key: $value\r\n") }
|
||||||
|
requestBuilder.append("\r\n")
|
||||||
|
|
||||||
|
val port = if (parsed.port == -1) {
|
||||||
|
when (parsed.scheme) {
|
||||||
|
"https" -> 443
|
||||||
|
"http" -> 80
|
||||||
|
else -> throw Exception("Unhandled scheme")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
parsed.port
|
||||||
|
}
|
||||||
|
|
||||||
|
val socket = if (parsed.scheme == "https") {
|
||||||
|
val sslSocketFactory = SSLSocketFactory.getDefault() as SSLSocketFactory
|
||||||
|
sslSocketFactory.createSocket(parsed.host, port)
|
||||||
|
} else {
|
||||||
|
Socket(parsed.host, port)
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.use { s ->
|
||||||
|
s.getOutputStream().write(requestBuilder.toString().encodeToByteArray())
|
||||||
|
|
||||||
|
val inputStream = s.getInputStream()
|
||||||
|
val resp = HttpResponseParser(inputStream)
|
||||||
|
if (resp.statusCode == 302) {
|
||||||
|
val location = resp.location!!
|
||||||
|
Logger.i(TAG, "handleWithTcp Proxied ${resp.statusCode} following redirect to $location");
|
||||||
|
makeTcpRequest(proxyHeaders, useMethod, Uri.parse(location)!!, context)
|
||||||
|
} else {
|
||||||
|
val isChunked = resp.transferEncoding.equals("chunked", ignoreCase = true)
|
||||||
|
val contentLength = resp.contentLength.toInt()
|
||||||
|
|
||||||
|
val headersFiltered = HttpHeaders(resp.headers.filter { !_ignoreResponseHeaders.contains(it.key.lowercase()) });
|
||||||
|
for (newHeader in headers)
|
||||||
|
headersFiltered.put(newHeader.key, newHeader.value);
|
||||||
|
|
||||||
|
context.respond(resp.statusCode, headersFiltered) { responseStream ->
|
||||||
|
if (isChunked) {
|
||||||
|
Logger.i(TAG, "handleWithTcp handleChunkedTransfer");
|
||||||
|
handleChunkedTransfer(inputStream, responseStream)
|
||||||
|
} else if (contentLength > 0) {
|
||||||
|
Logger.i(TAG, "handleWithTcp transferFixedLengthContent $contentLength");
|
||||||
|
transferFixedLengthContent(inputStream, responseStream, contentLength)
|
||||||
|
} else if (contentLength == -1) {
|
||||||
|
Logger.i(TAG, "handleWithTcp transferUntilEndOfStream");
|
||||||
|
transferUntilEndOfStream(inputStream, responseStream)
|
||||||
|
} else {
|
||||||
|
Logger.i(TAG, "handleWithTcp no content");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleChunkedTransfer(inputStream: InputStream, responseStream: OutputStream) {
|
||||||
|
var line: String?
|
||||||
|
val buffer = ByteArray(8192)
|
||||||
|
|
||||||
|
while (inputStream.readLine().also { line = it } != null) {
|
||||||
|
val size = line!!.trim().toInt(16)
|
||||||
|
|
||||||
|
responseStream.write(line!!.encodeToByteArray())
|
||||||
|
responseStream.write("\r\n".encodeToByteArray())
|
||||||
|
|
||||||
|
if (size == 0) {
|
||||||
|
inputStream.skip(2)
|
||||||
|
responseStream.write("\r\n".encodeToByteArray())
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalRead = 0
|
||||||
|
while (totalRead < size) {
|
||||||
|
val read = inputStream.read(buffer, 0, minOf(buffer.size, size - totalRead))
|
||||||
|
if (read == -1) break
|
||||||
|
responseStream.write(buffer, 0, read)
|
||||||
|
totalRead += read
|
||||||
|
}
|
||||||
|
|
||||||
|
inputStream.skip(2)
|
||||||
|
responseStream.write("\r\n".encodeToByteArray())
|
||||||
|
responseStream.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun transferFixedLengthContent(inputStream: InputStream, responseStream: OutputStream, contentLength: Int) {
|
||||||
|
val buffer = ByteArray(8192)
|
||||||
|
var totalRead = 0
|
||||||
|
while (totalRead < contentLength) {
|
||||||
|
val read = inputStream.read(buffer, 0, minOf(buffer.size, contentLength - totalRead))
|
||||||
|
if (read == -1) break
|
||||||
|
responseStream.write(buffer, 0, read)
|
||||||
|
totalRead += read
|
||||||
|
}
|
||||||
|
|
||||||
|
responseStream.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun transferUntilEndOfStream(inputStream: InputStream, responseStream: OutputStream) {
|
||||||
|
val buffer = ByteArray(8192)
|
||||||
|
var read: Int
|
||||||
|
while (inputStream.read(buffer).also { read = it } >= 0) {
|
||||||
|
responseStream.write(buffer, 0, read)
|
||||||
|
}
|
||||||
|
|
||||||
|
responseStream.flush()
|
||||||
|
}
|
||||||
|
|
||||||
fun withContent(body: String) : HttpProxyHandler {
|
fun withContent(body: String) : HttpProxyHandler {
|
||||||
this.content = body;
|
this.content = body;
|
||||||
return this;
|
return this;
|
||||||
@@ -92,4 +242,8 @@ class HttpProxyHandler(method: String, path: String, val targetUrl: String): Htt
|
|||||||
_ignoreRequestHeaders.add("referer");
|
_ignoreRequestHeaders.add("referer");
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "HttpProxyHandler"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -3,18 +3,16 @@ package com.futo.platformplayer.api.media
|
|||||||
import androidx.collection.LruCache
|
import androidx.collection.LruCache
|
||||||
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
|
||||||
|
import com.futo.platformplayer.api.media.models.chapters.IChapter
|
||||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||||
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.contents.IPlatformContentDetails
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
|
||||||
import com.futo.platformplayer.api.media.models.live.ILiveChatWindowDescriptor
|
import com.futo.platformplayer.api.media.models.live.ILiveChatWindowDescriptor
|
||||||
import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
|
import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
|
||||||
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
||||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
|
||||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.models.ImageVariable
|
import com.futo.platformplayer.models.ImageVariable
|
||||||
import com.futo.platformplayer.models.Playlist
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A temporary class that caches video results
|
* A temporary class that caches video results
|
||||||
@@ -43,12 +41,12 @@ class CachedPlatformClient : IPlatformClient {
|
|||||||
var result = _cache.get(url);
|
var result = _cache.get(url);
|
||||||
if(result == null) {
|
if(result == null) {
|
||||||
result = _client.getContentDetails(url);
|
result = _client.getContentDetails(url);
|
||||||
if (result != null)
|
_cache.put(url, result);
|
||||||
_cache.put(url, result);
|
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getContentChapters(url: String): List<IChapter> = _client.getContentChapters(url);
|
||||||
override fun getPlaybackTracker(url: String): IPlaybackTracker? = _client.getPlaybackTracker(url);
|
override fun getPlaybackTracker(url: String): IPlaybackTracker? = _client.getPlaybackTracker(url);
|
||||||
|
|
||||||
override fun isChannelUrl(url: String): Boolean = _client.isChannelUrl(url);
|
override fun isChannelUrl(url: String): Boolean = _client.isChannelUrl(url);
|
||||||
@@ -62,6 +60,9 @@ class CachedPlatformClient : IPlatformClient {
|
|||||||
filters: Map<String, List<String>>?
|
filters: Map<String, List<String>>?
|
||||||
): IPager<IPlatformContent> = _client.getChannelContents(channelUrl);
|
): IPager<IPlatformContent> = _client.getChannelContents(channelUrl);
|
||||||
|
|
||||||
|
override fun getPeekChannelTypes(): List<String> = _client.getPeekChannelTypes();
|
||||||
|
override fun peekChannelContents(channelUrl: String, type: String?): List<IPlatformContent> = _client.peekChannelContents(channelUrl, type);
|
||||||
|
|
||||||
override fun getChannelUrlByClaim(claimType: Int, claimValues: Map<Int, String>): String? = _client.getChannelUrlByClaim(claimType, claimValues)
|
override fun getChannelUrlByClaim(claimType: Int, claimValues: Map<Int, String>): String? = _client.getChannelUrlByClaim(claimType, claimValues)
|
||||||
|
|
||||||
override fun searchSuggestions(query: String): Array<String> = _client.searchSuggestions(query);
|
override fun searchSuggestions(query: String): Array<String> = _client.searchSuggestions(query);
|
||||||
|
|||||||
@@ -3,17 +3,16 @@ package com.futo.platformplayer.api.media
|
|||||||
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
|
||||||
|
import com.futo.platformplayer.api.media.models.chapters.IChapter
|
||||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||||
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.contents.IPlatformContentDetails
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
|
||||||
import com.futo.platformplayer.api.media.models.live.ILiveChatWindowDescriptor
|
import com.futo.platformplayer.api.media.models.live.ILiveChatWindowDescriptor
|
||||||
import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
|
import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
|
||||||
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
||||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
|
||||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.models.ImageVariable
|
import com.futo.platformplayer.models.ImageVariable
|
||||||
import com.futo.platformplayer.models.Playlist
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A client for a specific platform
|
* A client for a specific platform
|
||||||
@@ -85,6 +84,15 @@ interface IPlatformClient {
|
|||||||
*/
|
*/
|
||||||
fun getChannelContents(channelUrl: String, type: String? = null, order: String? = null, filters: Map<String, List<String>>? = null): IPager<IPlatformContent>;
|
fun getChannelContents(channelUrl: String, type: String? = null, order: String? = null, filters: Map<String, List<String>>? = null): IPager<IPlatformContent>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes what the plugin is capable on peek channel results
|
||||||
|
*/
|
||||||
|
fun getPeekChannelTypes(): List<String>;
|
||||||
|
/**
|
||||||
|
* Peeks contents of a channel, upload time descending
|
||||||
|
*/
|
||||||
|
fun peekChannelContents(channelUrl: String, type: String? = null): List<IPlatformContent>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the channel url associated with a claimType
|
* Gets the channel url associated with a claimType
|
||||||
*/
|
*/
|
||||||
@@ -100,6 +108,8 @@ interface IPlatformClient {
|
|||||||
*/
|
*/
|
||||||
fun getContentDetails(url: String): IPlatformContentDetails;
|
fun getContentDetails(url: String): IPlatformContentDetails;
|
||||||
|
|
||||||
|
fun getContentChapters(url: String): List<IChapter>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the playback tracker for a piece of content
|
* Gets the playback tracker for a piece of content
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import com.caverock.androidsvg.SVG
|
|||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
|
import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
|
||||||
import com.futo.platformplayer.api.media.models.live.LiveEventComment
|
import com.futo.platformplayer.api.media.models.live.LiveEventComment
|
||||||
import com.futo.platformplayer.api.media.models.live.LiveEventDonation
|
|
||||||
import com.futo.platformplayer.api.media.models.live.LiveEventEmojis
|
import com.futo.platformplayer.api.media.models.live.LiveEventEmojis
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSLiveEventPager
|
import com.futo.platformplayer.api.media.platforms.js.models.JSLiveEventPager
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
@@ -195,7 +194,7 @@ class LiveChatManager {
|
|||||||
|
|
||||||
fun getEmojiDrawable(emoji: String, cb: (drawable: Drawable?)->Unit) {
|
fun getEmojiDrawable(emoji: String, cb: (drawable: Drawable?)->Unit) {
|
||||||
var drawable: Drawable? = null;
|
var drawable: Drawable? = null;
|
||||||
var url: String? = null;
|
var url: String?;
|
||||||
synchronized(_cache_lock) {
|
synchronized(_cache_lock) {
|
||||||
url = _cache_urls[emoji];
|
url = _cache_urls[emoji];
|
||||||
if(url != null)
|
if(url != null)
|
||||||
|
|||||||
@@ -13,9 +13,12 @@ data class PlatformClientCapabilities(
|
|||||||
val hasGetChannelUrlByClaim: Boolean = false,
|
val hasGetChannelUrlByClaim: Boolean = false,
|
||||||
val hasGetChannelTemplateByClaimMap: Boolean = false,
|
val hasGetChannelTemplateByClaimMap: Boolean = false,
|
||||||
val hasGetSearchCapabilities: Boolean = false,
|
val hasGetSearchCapabilities: Boolean = false,
|
||||||
|
val hasGetSearchChannelContentsCapabilities: Boolean = false,
|
||||||
val hasGetChannelCapabilities: Boolean = false,
|
val hasGetChannelCapabilities: Boolean = false,
|
||||||
val hasGetLiveEvents: Boolean = false,
|
val hasGetLiveEvents: Boolean = false,
|
||||||
val hasGetLiveChatWindow: Boolean = false
|
val hasGetLiveChatWindow: Boolean = false,
|
||||||
|
val hasGetContentChapters: Boolean = false,
|
||||||
|
val hasPeekChannelContents: Boolean = false
|
||||||
) {
|
) {
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -20,7 +20,7 @@ class PlatformMultiClientPool {
|
|||||||
val pool = synchronized(_clientPools) {
|
val pool = synchronized(_clientPools) {
|
||||||
if(!_clientPools.containsKey(parentClient))
|
if(!_clientPools.containsKey(parentClient))
|
||||||
_clientPools[parentClient] = PlatformClientPool(parentClient, _name).apply {
|
_clientPools[parentClient] = PlatformClientPool(parentClient, _name).apply {
|
||||||
this.onDead.subscribe { client, pool ->
|
this.onDead.subscribe { _, pool ->
|
||||||
synchronized(_clientPools) {
|
synchronized(_clientPools) {
|
||||||
if(_clientPools[parentClient] == pool)
|
if(_clientPools[parentClient] == pool)
|
||||||
_clientPools.remove(parentClient);
|
_clientPools.remove(parentClient);
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ import com.futo.platformplayer.getOrThrow
|
|||||||
* A link to a channel, often with its own name and thumbnail
|
* A link to a channel, often with its own name and thumbnail
|
||||||
*/
|
*/
|
||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
class PlatformAuthorLink {
|
open class PlatformAuthorLink {
|
||||||
val id: PlatformID;
|
val id: PlatformID;
|
||||||
val name: String;
|
val name: String;
|
||||||
val url: String;
|
val url: String;
|
||||||
val thumbnail: String?;
|
var thumbnail: String?;
|
||||||
var subscribers: Long? = null; //Optional
|
var subscribers: Long? = null; //Optional
|
||||||
|
|
||||||
constructor(id: PlatformID, name: String, url: String, thumbnail: String? = null, subscribers: Long? = null)
|
constructor(id: PlatformID, name: String, url: String, thumbnail: String? = null, subscribers: Long? = null)
|
||||||
@@ -28,6 +28,9 @@ class PlatformAuthorLink {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformAuthorLink {
|
fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformAuthorLink {
|
||||||
|
if(value.has("membershipUrl"))
|
||||||
|
return PlatformAuthorMembershipLink.fromV8(config, value);
|
||||||
|
|
||||||
val context = "AuthorLink"
|
val context = "AuthorLink"
|
||||||
return PlatformAuthorLink(PlatformID.fromV8(config, value.getOrThrow(config, "id", context, false)),
|
return PlatformAuthorLink(PlatformID.fromV8(config, value.getOrThrow(config, "id", context, false)),
|
||||||
value.getOrThrow(config ,"name", context),
|
value.getOrThrow(config ,"name", context),
|
||||||
|
|||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
package com.futo.platformplayer.api.media.models
|
||||||
|
|
||||||
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
|
import com.futo.platformplayer.getOrDefault
|
||||||
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A link to a channel, often with its own name and thumbnail
|
||||||
|
*/
|
||||||
|
@kotlinx.serialization.Serializable
|
||||||
|
class PlatformAuthorMembershipLink: PlatformAuthorLink {
|
||||||
|
val membershipUrl: String?;
|
||||||
|
|
||||||
|
constructor(id: PlatformID, name: String, url: String, thumbnail: String? = null, subscribers: Long? = null, membershipUrl: String? = null): super(id, name, url, thumbnail, subscribers)
|
||||||
|
{
|
||||||
|
this.membershipUrl = membershipUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformAuthorMembershipLink {
|
||||||
|
val context = "AuthorMembershipLink"
|
||||||
|
return PlatformAuthorMembershipLink(PlatformID.fromV8(config, value.getOrThrow(config, "id", context, false)),
|
||||||
|
value.getOrThrow(config ,"name", context),
|
||||||
|
value.getOrThrow(config, "url", context),
|
||||||
|
value.getOrDefault<String>(config, "thumbnail", context, null),
|
||||||
|
if(value.has("subscribers")) value.getOrThrow(config,"subscribers", context) else null,
|
||||||
|
if(value.has("membershipUrl")) value.getOrThrow(config, "membershipUrl", context) else null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@ class ResultCapabilities(
|
|||||||
const val TYPE_LIVE = "LIVE";
|
const val TYPE_LIVE = "LIVE";
|
||||||
const val TYPE_POSTS = "POSTS";
|
const val TYPE_POSTS = "POSTS";
|
||||||
const val TYPE_MIXED = "MIXED";
|
const val TYPE_MIXED = "MIXED";
|
||||||
|
const val TYPE_SUBSCRIPTIONS = "SUBSCRIPTIONS";
|
||||||
|
|
||||||
const val ORDER_CHONOLOGICAL = "CHRONOLOGICAL";
|
const val ORDER_CHONOLOGICAL = "CHRONOLOGICAL";
|
||||||
|
|
||||||
@@ -63,7 +64,6 @@ class FilterGroup(
|
|||||||
val isMultiSelect: Boolean,
|
val isMultiSelect: Boolean,
|
||||||
val id: String? = null
|
val id: String? = null
|
||||||
) {
|
) {
|
||||||
@kotlinx.serialization.Transient
|
|
||||||
val idOrName: String get() = id ?: name;
|
val idOrName: String get() = id ?: name;
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ class Thumbnails {
|
|||||||
fun getLQThumbnail() : String? {
|
fun getLQThumbnail() : String? {
|
||||||
return sources.firstOrNull()?.url;
|
return sources.firstOrNull()?.url;
|
||||||
}
|
}
|
||||||
|
fun getMinimumThumbnail(quality: Int): String? {
|
||||||
|
return sources.firstOrNull { it.quality >= quality }?.url ?: getHQThumbnail();
|
||||||
|
}
|
||||||
|
|
||||||
fun hasMultiple() = sources.size > 1;
|
fun hasMultiple() = sources.size > 1;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+4
@@ -37,6 +37,10 @@ class SerializedChannel(
|
|||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isSameUrl(url: String): Boolean {
|
||||||
|
return this.url == url || urlAlternatives.contains(url);
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromChannel(channel: IPlatformChannel): SerializedChannel {
|
fun fromChannel(channel: IPlatformChannel): SerializedChannel {
|
||||||
return SerializedChannel(
|
return SerializedChannel(
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.futo.platformplayer.api.media.models.chapters
|
||||||
|
|
||||||
|
import com.futo.platformplayer.api.media.exceptions.UnknownPlatformException
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||||
|
|
||||||
|
interface IChapter {
|
||||||
|
val name: String;
|
||||||
|
val type: ChapterType;
|
||||||
|
val timeStart: Double;
|
||||||
|
val timeEnd: Double;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class ChapterType(val value: Int) {
|
||||||
|
NORMAL(0),
|
||||||
|
|
||||||
|
SKIPPABLE(5),
|
||||||
|
SKIP(6),
|
||||||
|
SKIPONCE(7);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromInt(value: Int): ChapterType
|
||||||
|
{
|
||||||
|
val result = ChapterType.values().firstOrNull { it.value == value };
|
||||||
|
if(result == null)
|
||||||
|
throw UnknownPlatformException(value.toString());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
-6
@@ -4,10 +4,7 @@ import com.futo.platformplayer.api.media.IPlatformClient
|
|||||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
import com.futo.platformplayer.api.media.models.ratings.IRating
|
import com.futo.platformplayer.api.media.models.ratings.IRating
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
|
||||||
import com.futo.platformplayer.states.StatePolycentric
|
|
||||||
import com.futo.polycentric.core.Pointer
|
import com.futo.polycentric.core.Pointer
|
||||||
import com.futo.polycentric.core.SignedEvent
|
|
||||||
import userpackage.Protocol.Reference
|
import userpackage.Protocol.Reference
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
@@ -20,16 +17,20 @@ class PolycentricPlatformComment : IPlatformComment {
|
|||||||
|
|
||||||
override val replyCount: Int?;
|
override val replyCount: Int?;
|
||||||
|
|
||||||
|
val eventPointer: Pointer;
|
||||||
val reference: Reference;
|
val reference: Reference;
|
||||||
|
val parentReference: Reference?;
|
||||||
|
|
||||||
constructor(contextUrl: String, author: PlatformAuthorLink, msg: String, rating: IRating, date: OffsetDateTime, reference: Reference, replyCount: Int? = null) {
|
constructor(contextUrl: String, author: PlatformAuthorLink, msg: String, rating: IRating, date: OffsetDateTime, eventPointer: Pointer, parentReference: Reference?, replyCount: Int? = null) {
|
||||||
this.contextUrl = contextUrl;
|
this.contextUrl = contextUrl;
|
||||||
this.author = author;
|
this.author = author;
|
||||||
this.message = msg;
|
this.message = msg;
|
||||||
this.rating = rating;
|
this.rating = rating;
|
||||||
this.date = date;
|
this.date = date;
|
||||||
this.replyCount = replyCount;
|
this.replyCount = replyCount;
|
||||||
this.reference = reference;
|
this.eventPointer = eventPointer;
|
||||||
|
this.reference = eventPointer.toReference();
|
||||||
|
this.parentReference = parentReference;
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getReplies(client: IPlatformClient): IPager<IPlatformComment> {
|
override fun getReplies(client: IPlatformClient): IPager<IPlatformComment> {
|
||||||
@@ -37,10 +38,11 @@ class PolycentricPlatformComment : IPlatformComment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun cloneWithUpdatedReplyCount(replyCount: Int?): PolycentricPlatformComment {
|
fun cloneWithUpdatedReplyCount(replyCount: Int?): PolycentricPlatformComment {
|
||||||
return PolycentricPlatformComment(contextUrl, author, message, rating, date, reference, replyCount);
|
return PolycentricPlatformComment(contextUrl, author, message, rating, date, eventPointer, parentReference, replyCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private const val TAG = "PolycentricPlatformComment"
|
||||||
val MAX_COMMENT_SIZE = 2000
|
val MAX_COMMENT_SIZE = 2000
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13,6 +13,7 @@ enum class ContentType(val value: Int) {
|
|||||||
|
|
||||||
NESTED_VIDEO(11),
|
NESTED_VIDEO(11),
|
||||||
|
|
||||||
|
LOCKED(70),
|
||||||
|
|
||||||
PLACEHOLDER(90),
|
PLACEHOLDER(90),
|
||||||
DEFERRED(91);
|
DEFERRED(91);
|
||||||
|
|||||||
+4
-12
@@ -1,31 +1,23 @@
|
|||||||
package com.futo.platformplayer.api.media.models.live
|
package com.futo.platformplayer.api.media.models.live
|
||||||
|
|
||||||
import com.caoccao.javet.values.V8Value
|
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
import com.futo.platformplayer.api.media.models.ratings.IRating
|
|
||||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes
|
|
||||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
|
||||||
import com.futo.platformplayer.api.media.models.ratings.RatingScaler
|
|
||||||
import com.futo.platformplayer.api.media.models.ratings.RatingType
|
|
||||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
import com.futo.platformplayer.orDefault
|
|
||||||
|
|
||||||
interface IPlatformLiveEvent {
|
interface IPlatformLiveEvent {
|
||||||
val type : LiveEventType;
|
val type : LiveEventType;
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject, contextName: String = "Unknown") : IPlatformLiveEvent {
|
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject, contextName: String = "LiveEvent") : IPlatformLiveEvent {
|
||||||
val contextName = "LiveEvent";
|
val t = LiveEventType.fromInt(obj.getOrThrow<Int>(config, "type", contextName));
|
||||||
val type = LiveEventType.fromInt(obj.getOrThrow<Int>(config, "type", contextName));
|
return when(t) {
|
||||||
return when(type) {
|
|
||||||
LiveEventType.COMMENT -> LiveEventComment.fromV8(config, obj);
|
LiveEventType.COMMENT -> LiveEventComment.fromV8(config, obj);
|
||||||
LiveEventType.EMOJIS -> LiveEventEmojis.fromV8(config, obj);
|
LiveEventType.EMOJIS -> LiveEventEmojis.fromV8(config, obj);
|
||||||
LiveEventType.DONATION -> LiveEventDonation.fromV8(config, obj);
|
LiveEventType.DONATION -> LiveEventDonation.fromV8(config, obj);
|
||||||
LiveEventType.VIEWCOUNT -> LiveEventViewCount.fromV8(config, obj);
|
LiveEventType.VIEWCOUNT -> LiveEventViewCount.fromV8(config, obj);
|
||||||
LiveEventType.RAID -> LiveEventRaid.fromV8(config, obj);
|
LiveEventType.RAID -> LiveEventRaid.fromV8(config, obj);
|
||||||
else -> throw NotImplementedError("Unknown type ${type}");
|
else -> throw NotImplementedError("Unknown type $t");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
package com.futo.platformplayer.api.media.models.locked
|
||||||
|
|
||||||
|
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.IPlatformContent
|
||||||
|
|
||||||
|
interface IPlatformLockedContent: IPlatformContent {
|
||||||
|
val lockContentType: ContentType;
|
||||||
|
val lockDescription: String?;
|
||||||
|
val unlockUrl: String?;
|
||||||
|
val contentName: String?;
|
||||||
|
val contentThumbnails: Thumbnails;
|
||||||
|
}
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
package com.futo.platformplayer.api.media.models.modifier
|
||||||
|
|
||||||
|
class AdhocRequestModifier: IRequestModifier {
|
||||||
|
val _handler: (String, Map<String,String>)->IRequest;
|
||||||
|
override var allowByteSkip: Boolean = false;
|
||||||
|
|
||||||
|
constructor(modifyReq: (String, Map<String,String>)->IRequest) {
|
||||||
|
_handler = modifyReq;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun modifyRequest(url: String, headers: Map<String, String>): IRequest {
|
||||||
|
return _handler(url, headers);
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
package com.futo.platformplayer.api.media.models.modifier
|
||||||
|
|
||||||
|
interface IModifierOptions {
|
||||||
|
val applyAuthClient: String?;
|
||||||
|
val applyCookieClient: String?;
|
||||||
|
val applyOtherHeaders: Boolean;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.futo.platformplayer.api.media.models.modifier
|
||||||
|
|
||||||
|
interface IRequest {
|
||||||
|
val url: String?;
|
||||||
|
val headers: Map<String, String>;
|
||||||
|
}
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
package com.futo.platformplayer.api.media.models.modifier
|
||||||
|
|
||||||
|
|
||||||
|
interface IRequestModifier {
|
||||||
|
var allowByteSkip: Boolean;
|
||||||
|
fun modifyRequest(url: String, headers: Map<String, String>): IRequest
|
||||||
|
}
|
||||||
-3
@@ -1,9 +1,6 @@
|
|||||||
package com.futo.platformplayer.api.media.models.playlists
|
package com.futo.platformplayer.api.media.models.playlists
|
||||||
|
|
||||||
import com.futo.platformplayer.api.media.models.Thumbnails
|
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
|
||||||
|
|
||||||
interface IPlatformPlaylist : IPlatformContent {
|
interface IPlatformPlaylist : IPlatformContent {
|
||||||
val thumbnail: String?;
|
val thumbnail: String?;
|
||||||
|
|||||||
-4
@@ -2,10 +2,6 @@ package com.futo.platformplayer.api.media.models.post
|
|||||||
|
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
|
||||||
import com.futo.platformplayer.api.media.models.ratings.IRating
|
import com.futo.platformplayer.api.media.models.ratings.IRating
|
||||||
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
|
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.*
|
|
||||||
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A detailed video model with data including video/audio sources
|
* A detailed video model with data including video/audio sources
|
||||||
|
|||||||
@@ -14,14 +14,13 @@ interface IRating {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromV8OrDefault(config: IV8PluginConfig, obj: V8Value?, default: IRating) = obj.orDefault(default) { fromV8(config, it as V8ValueObject) };
|
fun fromV8OrDefault(config: IV8PluginConfig, obj: V8Value?, default: IRating) = obj.orDefault(default) { fromV8(config, it as V8ValueObject) };
|
||||||
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject, contextName: String = "Unknown") : IRating {
|
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject, contextName: String = "Rating") : IRating {
|
||||||
val contextName = "Rating";
|
val t = RatingType.fromInt(obj.getOrThrow<Int>(config, "type", contextName));
|
||||||
val type = RatingType.fromInt(obj.getOrThrow<Int>(config, "type", contextName));
|
return when(t) {
|
||||||
return when(type) {
|
|
||||||
RatingType.LIKES -> RatingLikes.fromV8(config, obj);
|
RatingType.LIKES -> RatingLikes.fromV8(config, obj);
|
||||||
RatingType.LIKEDISLIKES -> RatingLikeDislikes.fromV8(config, obj);
|
RatingType.LIKEDISLIKES -> RatingLikeDislikes.fromV8(config, obj);
|
||||||
RatingType.SCALE -> RatingScaler.fromV8(config, obj);
|
RatingType.SCALE -> RatingScaler.fromV8(config, obj);
|
||||||
else -> throw NotImplementedError("Unknown type ${type}");
|
else -> throw NotImplementedError("Unknown type $t");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
package com.futo.platformplayer.api.media.models.streams.sources
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
||||||
|
|
||||||
|
class HLSVariantVideoUrlSource(
|
||||||
|
override val name: String,
|
||||||
|
override val width: Int,
|
||||||
|
override val height: Int,
|
||||||
|
override val container: String,
|
||||||
|
override val codec: String,
|
||||||
|
override val bitrate: Int?,
|
||||||
|
override val duration: Long,
|
||||||
|
override val priority: Boolean,
|
||||||
|
val url: String
|
||||||
|
) : IVideoUrlSource {
|
||||||
|
override fun getVideoUrl(): String {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HLSVariantAudioUrlSource(
|
||||||
|
override val name: String,
|
||||||
|
override val bitrate: Int,
|
||||||
|
override val container: String,
|
||||||
|
override val codec: String,
|
||||||
|
override val language: String,
|
||||||
|
override val duration: Long?,
|
||||||
|
override val priority: Boolean,
|
||||||
|
val url: String
|
||||||
|
) : IAudioUrlSource {
|
||||||
|
override fun getAudioUrl(): String {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HLSVariantSubtitleUrlSource(
|
||||||
|
override val name: String,
|
||||||
|
override val url: String,
|
||||||
|
override val format: String,
|
||||||
|
) : ISubtitleSource {
|
||||||
|
override val hasFetch: Boolean = false
|
||||||
|
|
||||||
|
override fun getSubtitles(): String? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getSubtitlesURI(): Uri? {
|
||||||
|
return Uri.parse(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
-2
@@ -1,8 +1,6 @@
|
|||||||
package com.futo.platformplayer.api.media.models.subtitles
|
package com.futo.platformplayer.api.media.models.subtitles
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Deferred
|
|
||||||
|
|
||||||
interface ISubtitleSource {
|
interface ISubtitleSource {
|
||||||
val name: String;
|
val name: String;
|
||||||
|
|||||||
+3
-4
@@ -1,13 +1,12 @@
|
|||||||
package com.futo.platformplayer.api.media.models.video
|
package com.futo.platformplayer.api.media.models.video
|
||||||
|
|
||||||
import com.futo.platformplayer.api.media.IPlatformClient
|
|
||||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
|
||||||
import com.futo.platformplayer.api.media.models.ratings.IRating
|
import com.futo.platformplayer.api.media.models.ratings.IRating
|
||||||
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
|
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.*
|
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.subtitles.ISubtitleSource
|
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A detailed video model with data including video/audio sources
|
* A detailed video model with data including video/audio sources
|
||||||
|
|||||||
+6
@@ -2,12 +2,17 @@ package com.futo.platformplayer.api.media.models.video
|
|||||||
|
|
||||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
|
import com.futo.platformplayer.api.media.models.locked.IPlatformLockedContent
|
||||||
import com.futo.platformplayer.api.media.models.nested.IPlatformNestedContent
|
import com.futo.platformplayer.api.media.models.nested.IPlatformNestedContent
|
||||||
import com.futo.platformplayer.api.media.models.post.IPlatformPost
|
import com.futo.platformplayer.api.media.models.post.IPlatformPost
|
||||||
import com.futo.platformplayer.serializers.PlatformContentSerializer
|
import com.futo.platformplayer.serializers.PlatformContentSerializer
|
||||||
|
import kotlinx.serialization.EncodeDefault
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
|
||||||
@kotlinx.serialization.Serializable(with = PlatformContentSerializer::class)
|
@kotlinx.serialization.Serializable(with = PlatformContentSerializer::class)
|
||||||
interface SerializedPlatformContent: IPlatformContent {
|
interface SerializedPlatformContent: IPlatformContent {
|
||||||
|
override val contentType: ContentType;
|
||||||
|
|
||||||
fun toJson() : String;
|
fun toJson() : String;
|
||||||
fun fromJson(str : String) : SerializedPlatformContent;
|
fun fromJson(str : String) : SerializedPlatformContent;
|
||||||
fun fromJsonArray(str : String) : Array<SerializedPlatformContent>;
|
fun fromJsonArray(str : String) : Array<SerializedPlatformContent>;
|
||||||
@@ -18,6 +23,7 @@ interface SerializedPlatformContent: IPlatformContent {
|
|||||||
ContentType.MEDIA -> SerializedPlatformVideo.fromVideo(content as IPlatformVideo);
|
ContentType.MEDIA -> SerializedPlatformVideo.fromVideo(content as IPlatformVideo);
|
||||||
ContentType.NESTED_VIDEO -> SerializedPlatformNestedContent.fromNested(content as IPlatformNestedContent);
|
ContentType.NESTED_VIDEO -> SerializedPlatformNestedContent.fromNested(content as IPlatformNestedContent);
|
||||||
ContentType.POST -> SerializedPlatformPost.fromPost(content as IPlatformPost);
|
ContentType.POST -> SerializedPlatformPost.fromPost(content as IPlatformPost);
|
||||||
|
ContentType.LOCKED -> SerializedPlatformLockedContent.fromLocked(content as IPlatformLockedContent);
|
||||||
else -> throw NotImplementedError("Content type ${content.contentType} not implemented");
|
else -> throw NotImplementedError("Content type ${content.contentType} not implemented");
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+62
@@ -0,0 +1,62 @@
|
|||||||
|
package com.futo.platformplayer.api.media.models.video
|
||||||
|
|
||||||
|
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.contents.ContentType
|
||||||
|
import com.futo.platformplayer.api.media.models.locked.IPlatformLockedContent
|
||||||
|
import com.futo.platformplayer.api.media.models.nested.IPlatformNestedContent
|
||||||
|
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||||
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
|
import com.futo.polycentric.core.combineHashCodes
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
|
@kotlinx.serialization.Serializable
|
||||||
|
open class SerializedPlatformLockedContent(
|
||||||
|
override val id: PlatformID,
|
||||||
|
override val name: String,
|
||||||
|
override val author: PlatformAuthorLink,
|
||||||
|
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
||||||
|
override val datetime: OffsetDateTime?,
|
||||||
|
override val url: String,
|
||||||
|
override val shareUrl: String,
|
||||||
|
override val lockContentType: ContentType,
|
||||||
|
override val contentName: String?,
|
||||||
|
override val lockDescription: String? = null,
|
||||||
|
override val unlockUrl: String? = null,
|
||||||
|
override val contentThumbnails: Thumbnails
|
||||||
|
) : IPlatformLockedContent, SerializedPlatformContent {
|
||||||
|
override val contentType: ContentType = ContentType.LOCKED;
|
||||||
|
|
||||||
|
override fun toJson() : String {
|
||||||
|
return Json.encodeToString(this);
|
||||||
|
}
|
||||||
|
override fun fromJson(str : String) : SerializedPlatformLockedContent {
|
||||||
|
return Serializer.json.decodeFromString<SerializedPlatformLockedContent>(str);
|
||||||
|
}
|
||||||
|
override fun fromJsonArray(str : String) : Array<SerializedPlatformContent> {
|
||||||
|
return Serializer.json.decodeFromString<Array<SerializedPlatformContent>>(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromLocked(content: IPlatformLockedContent) : SerializedPlatformLockedContent {
|
||||||
|
return SerializedPlatformLockedContent(
|
||||||
|
content.id,
|
||||||
|
content.name,
|
||||||
|
content.author,
|
||||||
|
content.datetime,
|
||||||
|
content.url,
|
||||||
|
content.shareUrl,
|
||||||
|
content.lockContentType,
|
||||||
|
content.contentName,
|
||||||
|
content.lockDescription,
|
||||||
|
content.unlockUrl,
|
||||||
|
content.contentThumbnails
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -30,7 +30,7 @@ open class SerializedPlatformNestedContent(
|
|||||||
override val contentProvider: String?,
|
override val contentProvider: String?,
|
||||||
override val contentThumbnails: Thumbnails
|
override val contentThumbnails: Thumbnails
|
||||||
) : IPlatformNestedContent, SerializedPlatformContent {
|
) : IPlatformNestedContent, SerializedPlatformContent {
|
||||||
final override val contentType: ContentType get() = ContentType.NESTED_VIDEO;
|
final override val contentType: ContentType = ContentType.NESTED_VIDEO;
|
||||||
|
|
||||||
override val contentPlugin: String? = StatePlatform.instance.getContentClientOrNull(contentUrl)?.id;
|
override val contentPlugin: String? = StatePlatform.instance.getContentClientOrNull(contentUrl)?.id;
|
||||||
override val contentSupported: Boolean get() = contentPlugin != null;
|
override val contentSupported: Boolean get() = contentPlugin != null;
|
||||||
|
|||||||
+2
-1
@@ -8,6 +8,7 @@ import com.futo.platformplayer.api.media.models.contents.ContentType
|
|||||||
import com.futo.platformplayer.api.media.models.post.IPlatformPost
|
import com.futo.platformplayer.api.media.models.post.IPlatformPost
|
||||||
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||||
import com.futo.polycentric.core.combineHashCodes
|
import com.futo.polycentric.core.combineHashCodes
|
||||||
|
import kotlinx.serialization.EncodeDefault
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
@@ -26,7 +27,7 @@ open class SerializedPlatformPost(
|
|||||||
override val thumbnails: List<Thumbnails?>,
|
override val thumbnails: List<Thumbnails?>,
|
||||||
override val images: List<String>
|
override val images: List<String>
|
||||||
) : IPlatformPost, SerializedPlatformContent {
|
) : IPlatformPost, SerializedPlatformContent {
|
||||||
final override val contentType: ContentType get() = ContentType.POST;
|
override val contentType: ContentType = ContentType.POST;
|
||||||
|
|
||||||
override fun toJson() : String {
|
override fun toJson() : String {
|
||||||
return Json.encodeToString(this);
|
return Json.encodeToString(this);
|
||||||
|
|||||||
+1
-1
@@ -26,7 +26,7 @@ open class SerializedPlatformVideo(
|
|||||||
override val duration: Long,
|
override val duration: Long,
|
||||||
override val viewCount: Long,
|
override val viewCount: Long,
|
||||||
) : IPlatformVideo, SerializedPlatformContent {
|
) : IPlatformVideo, SerializedPlatformContent {
|
||||||
final override val contentType: ContentType get() = ContentType.MEDIA;
|
override val contentType: ContentType = ContentType.MEDIA;
|
||||||
|
|
||||||
override val isLive: Boolean = false;
|
override val isLive: Boolean = false;
|
||||||
|
|
||||||
|
|||||||
-1
@@ -8,6 +8,5 @@ import com.futo.platformplayer.api.media.models.streams.sources.VideoUrlSource
|
|||||||
class SerializedVideoMuxedSourceDescriptor(
|
class SerializedVideoMuxedSourceDescriptor(
|
||||||
val _videoSources: Array<VideoUrlSource>
|
val _videoSources: Array<VideoUrlSource>
|
||||||
): VideoMuxedSourceDescriptor(), ISerializedVideoSourceDescriptor {
|
): VideoMuxedSourceDescriptor(), ISerializedVideoSourceDescriptor {
|
||||||
@kotlinx.serialization.Transient
|
|
||||||
override val videoSources: Array<IVideoSource> get() = _videoSources.map { it }.toTypedArray();
|
override val videoSources: Array<IVideoSource> get() = _videoSources.map { it }.toTypedArray();
|
||||||
};
|
};
|
||||||
+4
-3
@@ -1,15 +1,16 @@
|
|||||||
package com.futo.platformplayer.api.media.models.video
|
package com.futo.platformplayer.api.media.models.video
|
||||||
|
|
||||||
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
|
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.*
|
import com.futo.platformplayer.api.media.models.streams.sources.AudioUrlSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.VideoUrlSource
|
||||||
|
|
||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
class SerializedVideoNonMuxedSourceDescriptor(
|
class SerializedVideoNonMuxedSourceDescriptor(
|
||||||
val _videoSources: Array<VideoUrlSource>,
|
val _videoSources: Array<VideoUrlSource>,
|
||||||
val _audioSources: Array<AudioUrlSource>
|
val _audioSources: Array<AudioUrlSource>
|
||||||
): VideoUnMuxedSourceDescriptor(), ISerializedVideoSourceDescriptor {
|
): VideoUnMuxedSourceDescriptor(), ISerializedVideoSourceDescriptor {
|
||||||
@kotlinx.serialization.Transient
|
|
||||||
override val videoSources: Array<IVideoSource> get() = _videoSources.map { it }.toTypedArray();
|
override val videoSources: Array<IVideoSource> get() = _videoSources.map { it }.toTypedArray();
|
||||||
@kotlinx.serialization.Transient
|
|
||||||
override val audioSources: Array<IAudioSource> get() = _audioSources.map { it }.toTypedArray();
|
override val audioSources: Array<IAudioSource> get() = _audioSources.map { it }.toTypedArray();
|
||||||
};
|
};
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
package com.futo.platformplayer.api.media.platforms.js
|
package com.futo.platformplayer.api.media.platforms.js
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.futo.platformplayer.states.StateDeveloper
|
|
||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.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.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import java.util.*
|
import com.futo.platformplayer.states.StateDeveloper
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
class DevJSClient : JSClient {
|
class DevJSClient : JSClient {
|
||||||
override val id: String
|
override val id: String
|
||||||
@@ -20,14 +20,14 @@ class DevJSClient : JSClient {
|
|||||||
|
|
||||||
val devID: String;
|
val devID: String;
|
||||||
|
|
||||||
constructor(context: Context, config: SourcePluginConfig, script: String, auth: SourceAuth? = null, captcha: SourceCaptchaData? = null, devID: String? = null): super(context, SourcePluginDescriptor(config, auth?.toEncrypted(), captcha?.toEncrypted(), listOf("DEV")), null, script) {
|
constructor(context: Context, config: SourcePluginConfig, script: String, auth: SourceAuth? = null, captcha: SourceCaptchaData? = null, devID: String? = null, settings: HashMap<String, String?>? = null): super(context, SourcePluginDescriptor(config, auth?.toEncrypted(), captcha?.toEncrypted(), listOf("DEV"), settings), null, script) {
|
||||||
_devScript = script;
|
_devScript = script;
|
||||||
_auth = auth;
|
_auth = auth;
|
||||||
_captcha = captcha;
|
_captcha = captcha;
|
||||||
this.devID = devID ?: UUID.randomUUID().toString().substring(0, 5);
|
this.devID = devID ?: UUID.randomUUID().toString().substring(0, 5);
|
||||||
|
|
||||||
onCaptchaException.subscribe { client, captcha ->
|
onCaptchaException.subscribe { client, c ->
|
||||||
StateApp.instance.handleCaptchaException(client, captcha);
|
StateApp.instance.handleCaptchaException(client, c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//TODO: Misisng auth/captcha pass on purpose?
|
//TODO: Misisng auth/captcha pass on purpose?
|
||||||
@@ -37,8 +37,8 @@ class DevJSClient : JSClient {
|
|||||||
_captcha = captcha;
|
_captcha = captcha;
|
||||||
this.devID = devID ?: UUID.randomUUID().toString().substring(0, 5);
|
this.devID = devID ?: UUID.randomUUID().toString().substring(0, 5);
|
||||||
|
|
||||||
onCaptchaException.subscribe { client, captcha ->
|
onCaptchaException.subscribe { client, c ->
|
||||||
StateApp.instance.handleCaptchaException(client, captcha);
|
StateApp.instance.handleCaptchaException(client, c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ class DevJSClient : JSClient {
|
|||||||
_auth = auth;
|
_auth = auth;
|
||||||
}
|
}
|
||||||
fun recreate(context: Context): DevJSClient {
|
fun recreate(context: Context): DevJSClient {
|
||||||
return DevJSClient(context, config, _devScript, _auth, _captcha, devID);
|
return DevJSClient(context, config, _devScript, _auth, _captcha, devID, descriptor.settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getCopy(): JSClient {
|
override fun getCopy(): JSClient {
|
||||||
|
|||||||
@@ -4,17 +4,16 @@ import android.content.Context
|
|||||||
import com.caoccao.javet.values.V8Value
|
import com.caoccao.javet.values.V8Value
|
||||||
import com.caoccao.javet.values.primitive.V8ValueBoolean
|
import com.caoccao.javet.values.primitive.V8ValueBoolean
|
||||||
import com.caoccao.javet.values.primitive.V8ValueInteger
|
import com.caoccao.javet.values.primitive.V8ValueInteger
|
||||||
import com.caoccao.javet.values.primitive.V8ValueNull
|
|
||||||
import com.caoccao.javet.values.primitive.V8ValueString
|
import com.caoccao.javet.values.primitive.V8ValueString
|
||||||
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.states.StatePlatform
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.states.StatePlugins
|
|
||||||
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.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
|
||||||
|
import com.futo.platformplayer.api.media.models.chapters.IChapter
|
||||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||||
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.contents.IPlatformContentDetails
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
|
||||||
@@ -22,15 +21,30 @@ import com.futo.platformplayer.api.media.models.live.ILiveChatWindowDescriptor
|
|||||||
import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
|
import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
|
||||||
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
||||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
||||||
import com.futo.platformplayer.api.media.platforms.js.internal.*
|
import com.futo.platformplayer.api.media.platforms.js.internal.JSCallDocs
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.*
|
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.JSHttpClient
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.internal.JSOptional
|
||||||
|
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.IJSContentDetails
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.JSChannel
|
||||||
|
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.JSComment
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.JSCommentPager
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.JSContentPager
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.JSLiveChatWindowDescriptor
|
||||||
|
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.JSPlaylistDetails
|
||||||
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
|
||||||
import com.futo.platformplayer.constructs.Event2
|
import com.futo.platformplayer.constructs.Event2
|
||||||
import com.futo.platformplayer.engine.V8Plugin
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
import com.futo.platformplayer.engine.exceptions.PluginEngineException
|
import com.futo.platformplayer.engine.exceptions.PluginEngineException
|
||||||
import com.futo.platformplayer.engine.exceptions.PluginEngineStoppedException
|
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptValidationException
|
import com.futo.platformplayer.engine.exceptions.ScriptValidationException
|
||||||
@@ -38,26 +52,30 @@ import com.futo.platformplayer.logging.Logger
|
|||||||
import com.futo.platformplayer.models.ImageVariable
|
import com.futo.platformplayer.models.ImageVariable
|
||||||
import com.futo.platformplayer.states.AnnouncementType
|
import com.futo.platformplayer.states.AnnouncementType
|
||||||
import com.futo.platformplayer.states.StateAnnouncement
|
import com.futo.platformplayer.states.StateAnnouncement
|
||||||
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
|
import com.futo.platformplayer.states.StatePlugins
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import kotlin.reflect.full.findAnnotations
|
import kotlin.reflect.full.findAnnotations
|
||||||
import kotlin.reflect.jvm.kotlinFunction
|
import kotlin.reflect.jvm.kotlinFunction
|
||||||
|
import kotlin.streams.asSequence
|
||||||
|
|
||||||
open class JSClient : IPlatformClient {
|
open class JSClient : IPlatformClient {
|
||||||
val config: SourcePluginConfig;
|
val config: SourcePluginConfig;
|
||||||
protected val _context: Context;
|
protected val _context: Context;
|
||||||
private val _plugin: V8Plugin;
|
private val _plugin: V8Plugin;
|
||||||
private val plugin: V8Plugin get() = _plugin ?: throw IllegalStateException("Client not enabled");
|
private val plugin: V8Plugin get() = _plugin
|
||||||
|
|
||||||
var descriptor: SourcePluginDescriptor
|
var descriptor: SourcePluginDescriptor
|
||||||
private set;
|
private set;
|
||||||
|
|
||||||
private val _client: JSHttpClient;
|
private val _httpClient: JSHttpClient;
|
||||||
private val _clientAuth: JSHttpClient?;
|
private val _httpClientAuth: JSHttpClient?;
|
||||||
private var _searchCapabilities: ResultCapabilities? = null;
|
private var _searchCapabilities: ResultCapabilities? = null;
|
||||||
private var _searchChannelContentsCapabilities: ResultCapabilities? = null;
|
private var _searchChannelContentsCapabilities: ResultCapabilities? = null;
|
||||||
private var _channelCapabilities: ResultCapabilities? = null;
|
private var _channelCapabilities: ResultCapabilities? = null;
|
||||||
|
private var _peekChannelTypes: List<String>? = null;
|
||||||
|
|
||||||
protected val _script: String;
|
protected val _script: String;
|
||||||
|
|
||||||
@@ -76,7 +94,11 @@ open class JSClient : IPlatformClient {
|
|||||||
|
|
||||||
private val _busyLock = Object();
|
private val _busyLock = Object();
|
||||||
private var _busyCounter = 0;
|
private var _busyCounter = 0;
|
||||||
|
private var _busyAction = "";
|
||||||
val isBusy: Boolean get() = _busyCounter > 0;
|
val isBusy: Boolean get() = _busyCounter > 0;
|
||||||
|
val isBusyAction: String get() {
|
||||||
|
return _busyAction;
|
||||||
|
}
|
||||||
|
|
||||||
val settings: HashMap<String, String?> get() = descriptor.settings;
|
val settings: HashMap<String, String?> get() = descriptor.settings;
|
||||||
|
|
||||||
@@ -91,6 +113,19 @@ 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
|
||||||
|
|
||||||
|
fun getSubscriptionRateLimit(): Int? {
|
||||||
|
val pluginRateLimit = config.subscriptionRateLimit;
|
||||||
|
val settingsRateLimit = descriptor.appSettings.rateLimit.getSubRateLimit();
|
||||||
|
if(settingsRateLimit > 0) {
|
||||||
|
if(pluginRateLimit != null)
|
||||||
|
return settingsRateLimit.coerceAtMost(pluginRateLimit);
|
||||||
|
else
|
||||||
|
return settingsRateLimit;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return pluginRateLimit;
|
||||||
|
}
|
||||||
|
|
||||||
val onDisabled = Event1<JSClient>();
|
val onDisabled = Event1<JSClient>();
|
||||||
val onCaptchaException = Event2<JSClient, ScriptCaptchaRequiredException>();
|
val onCaptchaException = Event2<JSClient, ScriptCaptchaRequiredException>();
|
||||||
|
|
||||||
@@ -104,9 +139,9 @@ open class JSClient : IPlatformClient {
|
|||||||
_captcha = descriptor.getCaptchaData();
|
_captcha = descriptor.getCaptchaData();
|
||||||
flags = descriptor.flags.toTypedArray();
|
flags = descriptor.flags.toTypedArray();
|
||||||
|
|
||||||
_client = JSHttpClient(this, null, _captcha);
|
_httpClient = JSHttpClient(this, null, _captcha);
|
||||||
_clientAuth = JSHttpClient(this, _auth, _captcha);
|
_httpClientAuth = JSHttpClient(this, _auth, _captcha);
|
||||||
_plugin = V8Plugin(context, descriptor.config, null, _client, _clientAuth);
|
_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");
|
||||||
|
|
||||||
@@ -122,6 +157,8 @@ open class JSClient : IPlatformClient {
|
|||||||
if(it is ScriptCaptchaRequiredException)
|
if(it is ScriptCaptchaRequiredException)
|
||||||
onCaptchaException.emit(this, it);
|
onCaptchaException.emit(this, it);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_plugin.changeAllowDevSubmit(descriptor.appSettings.allowDeveloperSubmit);
|
||||||
}
|
}
|
||||||
constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String?, script: String) {
|
constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String?, script: String) {
|
||||||
this._context = context;
|
this._context = context;
|
||||||
@@ -133,9 +170,9 @@ open class JSClient : IPlatformClient {
|
|||||||
_captcha = descriptor.getCaptchaData();
|
_captcha = descriptor.getCaptchaData();
|
||||||
flags = descriptor.flags.toTypedArray();
|
flags = descriptor.flags.toTypedArray();
|
||||||
|
|
||||||
_client = JSHttpClient(this, null, _captcha);
|
_httpClient = JSHttpClient(this, null, _captcha);
|
||||||
_clientAuth = JSHttpClient(this, _auth, _captcha);
|
_httpClientAuth = JSHttpClient(this, _auth, _captcha);
|
||||||
_plugin = V8Plugin(context, descriptor.config, script, _client, _clientAuth);
|
_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");
|
||||||
_plugin.withScript(script);
|
_plugin.withScript(script);
|
||||||
@@ -145,6 +182,8 @@ open class JSClient : IPlatformClient {
|
|||||||
if(it is ScriptCaptchaRequiredException)
|
if(it is ScriptCaptchaRequiredException)
|
||||||
onCaptchaException.emit(this, it);
|
onCaptchaException.emit(this, it);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_plugin.changeAllowDevSubmit(descriptor.appSettings.allowDeveloperSubmit);
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun getCopy(): JSClient {
|
open fun getCopy(): JSClient {
|
||||||
@@ -154,6 +193,13 @@ open class JSClient : IPlatformClient {
|
|||||||
fun getUnderlyingPlugin(): V8Plugin {
|
fun getUnderlyingPlugin(): V8Plugin {
|
||||||
return _plugin;
|
return _plugin;
|
||||||
}
|
}
|
||||||
|
fun getHttpClientById(id: String): JSHttpClient? {
|
||||||
|
if(_httpClient.clientId == id)
|
||||||
|
return _httpClient;
|
||||||
|
if(_httpClientAuth?.clientId == id)
|
||||||
|
return _httpClientAuth;
|
||||||
|
return plugin.httpClientOthers[id];
|
||||||
|
}
|
||||||
|
|
||||||
override fun initialize() {
|
override fun initialize() {
|
||||||
Logger.i(TAG, "Plugin [${config.name}] initializing");
|
Logger.i(TAG, "Plugin [${config.name}] initializing");
|
||||||
@@ -179,8 +225,11 @@ open class JSClient : IPlatformClient {
|
|||||||
hasGetChannelTemplateByClaimMap = plugin.executeBoolean("!!source.getChannelTemplateByClaimMap") ?: false,
|
hasGetChannelTemplateByClaimMap = plugin.executeBoolean("!!source.getChannelTemplateByClaimMap") ?: false,
|
||||||
hasGetSearchCapabilities = plugin.executeBoolean("!!source.getSearchCapabilities") ?: false,
|
hasGetSearchCapabilities = plugin.executeBoolean("!!source.getSearchCapabilities") ?: false,
|
||||||
hasGetChannelCapabilities = plugin.executeBoolean("!!source.getChannelCapabilities") ?: false,
|
hasGetChannelCapabilities = plugin.executeBoolean("!!source.getChannelCapabilities") ?: false,
|
||||||
|
hasGetSearchChannelContentsCapabilities = plugin.executeBoolean("!!source.getSearchChannelContentsCapabilities") ?: false,
|
||||||
hasGetLiveEvents = plugin.executeBoolean("!!source.getLiveEvents") ?: false,
|
hasGetLiveEvents = plugin.executeBoolean("!!source.getLiveEvents") ?: false,
|
||||||
hasGetLiveChatWindow = plugin.executeBoolean("!!source.getLiveChatWindow") ?: false,
|
hasGetLiveChatWindow = plugin.executeBoolean("!!source.getLiveChatWindow") ?: false,
|
||||||
|
hasGetContentChapters = plugin.executeBoolean("!!source.getContentChapters") ?: false,
|
||||||
|
hasPeekChannelContents = plugin.executeBoolean("!!source.peekChannelContents") ?: false
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -224,15 +273,15 @@ open class JSClient : IPlatformClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@JSDocs(2, "source.getHome()", "Gets the HomeFeed of the platform")
|
@JSDocs(2, "source.getHome()", "Gets the HomeFeed of the platform")
|
||||||
override fun getHome(): IPager<IPlatformContent> = isBusyWith {
|
override fun getHome(): IPager<IPlatformContent> = isBusyWith("getHome") {
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
return@isBusyWith JSContentPager(config, plugin,
|
return@isBusyWith JSContentPager(config, this,
|
||||||
plugin.executeTyped("source.getHome()"));
|
plugin.executeTyped("source.getHome()"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@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 {
|
override fun searchSuggestions(query: String): Array<String> = isBusyWith("searchSuggestions") {
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
return@isBusyWith plugin.executeTyped<V8ValueArray>("source.searchSuggestions(${Json.encodeToString(query)})")
|
return@isBusyWith plugin.executeTyped<V8ValueArray>("source.searchSuggestions(${Json.encodeToString(query)})")
|
||||||
.toArray()
|
.toArray()
|
||||||
@@ -262,14 +311,17 @@ open class JSClient : IPlatformClient {
|
|||||||
@JSDocsParameter("order", "(optional) Order in which contents should be returned")
|
@JSDocsParameter("order", "(optional) Order in which contents should be returned")
|
||||||
@JSDocsParameter("filters", "(optional) Filters to apply on contents")
|
@JSDocsParameter("filters", "(optional) Filters to apply on contents")
|
||||||
@JSDocsParameter("channelId", "(optional) Channel id to search in")
|
@JSDocsParameter("channelId", "(optional) Channel id to search in")
|
||||||
override fun search(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith {
|
override fun search(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith("search") {
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
return@isBusyWith JSContentPager(config, plugin,
|
return@isBusyWith JSContentPager(config, this,
|
||||||
plugin.executeTyped("source.search(${Json.encodeToString(query)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})"));
|
plugin.executeTyped("source.search(${Json.encodeToString(query)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JSDocs(4, "source.getSearchChannelContentsCapabilities()", "Gets capabilities this plugin has for search videos")
|
@JSDocs(4, "source.getSearchChannelContentsCapabilities()", "Gets capabilities this plugin has for search videos")
|
||||||
override fun getSearchChannelContentsCapabilities(): ResultCapabilities {
|
override fun getSearchChannelContentsCapabilities(): ResultCapabilities {
|
||||||
|
if(!capabilities.hasGetSearchChannelContentsCapabilities)
|
||||||
|
return ResultCapabilities(listOf(ResultCapabilities.TYPE_MIXED));
|
||||||
|
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
if (_searchChannelContentsCapabilities != null)
|
if (_searchChannelContentsCapabilities != null)
|
||||||
return _searchChannelContentsCapabilities!!;
|
return _searchChannelContentsCapabilities!!;
|
||||||
@@ -283,21 +335,21 @@ open class JSClient : IPlatformClient {
|
|||||||
@JSDocsParameter("type", "(optional) Type of contents to get from search ")
|
@JSDocsParameter("type", "(optional) Type of contents to get from search ")
|
||||||
@JSDocsParameter("order", "(optional) Order in which contents should be returned")
|
@JSDocsParameter("order", "(optional) Order in which contents should be returned")
|
||||||
@JSDocsParameter("filters", "(optional) Filters to apply on contents")
|
@JSDocsParameter("filters", "(optional) Filters to apply on contents")
|
||||||
override fun searchChannelContents(channelUrl: String, query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith {
|
override fun searchChannelContents(channelUrl: String, query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith("searchChannelContents") {
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
if(!capabilities.hasSearchChannelContents)
|
if(!capabilities.hasSearchChannelContents)
|
||||||
throw IllegalStateException("This plugin does not support channel search");
|
throw IllegalStateException("This plugin does not support channel search");
|
||||||
|
|
||||||
return@isBusyWith JSContentPager(config, plugin,
|
return@isBusyWith JSContentPager(config, this,
|
||||||
plugin.executeTyped("source.searchChannelContents(${Json.encodeToString(channelUrl)}, ${Json.encodeToString(query)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})"));
|
plugin.executeTyped("source.searchChannelContents(${Json.encodeToString(channelUrl)}, ${Json.encodeToString(query)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JSOptional
|
@JSOptional
|
||||||
@JSDocs(5, "source.searchChannels(query)", "Searches for channels on the platform")
|
@JSDocs(5, "source.searchChannels(query)", "Searches for channels on the platform")
|
||||||
@JSDocsParameter("query", "Query that channels should match")
|
@JSDocsParameter("query", "Query that channels should match")
|
||||||
override fun searchChannels(query: String): IPager<PlatformAuthorLink> = isBusyWith {
|
override fun searchChannels(query: String): IPager<PlatformAuthorLink> = isBusyWith("searchChannels") {
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
return@isBusyWith JSChannelPager(config, plugin,
|
return@isBusyWith JSChannelPager(config, this,
|
||||||
plugin.executeTyped("source.searchChannels(${Json.encodeToString(query)})"));
|
plugin.executeTyped("source.searchChannels(${Json.encodeToString(query)})"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,7 +367,7 @@ open class JSClient : IPlatformClient {
|
|||||||
}
|
}
|
||||||
@JSDocs(7, "source.getChannel(channelUrl)", "Gets a channel by its url")
|
@JSDocs(7, "source.getChannel(channelUrl)", "Gets a channel by its url")
|
||||||
@JSDocsParameter("channelUrl", "A channel url (this platform)")
|
@JSDocsParameter("channelUrl", "A channel url (this platform)")
|
||||||
override fun getChannel(channelUrl: String): IPlatformChannel = isBusyWith {
|
override fun getChannel(channelUrl: String): IPlatformChannel = isBusyWith("getChannel") {
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
return@isBusyWith JSChannel(config,
|
return@isBusyWith JSChannel(config,
|
||||||
plugin.executeTyped("source.getChannel(${Json.encodeToString(channelUrl)})"));
|
plugin.executeTyped("source.getChannel(${Json.encodeToString(channelUrl)})"));
|
||||||
@@ -342,12 +394,46 @@ open class JSClient : IPlatformClient {
|
|||||||
@JSDocsParameter("type", "(optional) Type of contents to get from channel")
|
@JSDocsParameter("type", "(optional) Type of contents to get from channel")
|
||||||
@JSDocsParameter("order", "(optional) Order in which contents should be returned")
|
@JSDocsParameter("order", "(optional) Order in which contents should be returned")
|
||||||
@JSDocsParameter("filters", "(optional) Filters to apply on contents")
|
@JSDocsParameter("filters", "(optional) Filters to apply on contents")
|
||||||
override fun getChannelContents(channelUrl: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith {
|
override fun getChannelContents(channelUrl: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith("getChannelContents") {
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
return@isBusyWith JSContentPager(config, plugin,
|
return@isBusyWith JSContentPager(config, this,
|
||||||
plugin.executeTyped("source.getChannelContents(${Json.encodeToString(channelUrl)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})"));
|
plugin.executeTyped("source.getChannelContents(${Json.encodeToString(channelUrl)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JSDocs(10, "source.getPeekChannelTypes()", "Gets types this plugin has for peek channel contents")
|
||||||
|
override fun getPeekChannelTypes(): List<String> {
|
||||||
|
if(!capabilities.hasPeekChannelContents)
|
||||||
|
return listOf();
|
||||||
|
try {
|
||||||
|
if (_peekChannelTypes != null) {
|
||||||
|
return _peekChannelTypes!!;
|
||||||
|
}
|
||||||
|
val arr: V8ValueArray = plugin.executeTyped("source.getPeekChannelTypes()");
|
||||||
|
|
||||||
|
_peekChannelTypes = arr.keys.mapNotNull {
|
||||||
|
val str = arr.get<V8ValueString>(it);
|
||||||
|
return@mapNotNull str.value;
|
||||||
|
};
|
||||||
|
return _peekChannelTypes ?: listOf();
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
announcePluginUnhandledException("getPeekChannelTypes", ex);
|
||||||
|
return listOf();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@JSDocs(10, "source.peekChannelContents(url, type)", "Peek contents of a channel (reverse chronological order)")
|
||||||
|
@JSDocsParameter("channelUrl", "A channel url (this platform)")
|
||||||
|
@JSDocsParameter("type", "(optional) Type of contents to get from channel")
|
||||||
|
override fun peekChannelContents(channelUrl: String, type: String?): List<IPlatformContent> = isBusyWith("peekChannelContents") {
|
||||||
|
ensureEnabled();
|
||||||
|
|
||||||
|
val items: V8ValueArray = plugin.executeTyped("source.peekChannelContents(${Json.encodeToString(channelUrl)}, ${Json.encodeToString(type)})");
|
||||||
|
return@isBusyWith items.keys.mapNotNull {
|
||||||
|
val obj = items.get<V8ValueObject>(it);
|
||||||
|
return@mapNotNull IJSContent.fromV8(this, obj);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@JSOptional
|
@JSOptional
|
||||||
@JSDocs(11, "source.getChannelUrlByClaim(claimType, claimValues)", "Gets the channel url that should be used to fetch a given polycentric claim")
|
@JSDocs(11, "source.getChannelUrlByClaim(claimType, claimValues)", "Gets the channel url that should be used to fetch a given polycentric claim")
|
||||||
@JSDocsParameter("claimType", "Polycentric claimtype id")
|
@JSDocsParameter("claimType", "Polycentric claimtype id")
|
||||||
@@ -408,16 +494,27 @@ open class JSClient : IPlatformClient {
|
|||||||
}
|
}
|
||||||
@JSDocs(14, "source.getContentDetails(url)", "Gets content details by its url")
|
@JSDocs(14, "source.getContentDetails(url)", "Gets content details by its url")
|
||||||
@JSDocsParameter("url", "A content url (this platform)")
|
@JSDocsParameter("url", "A content url (this platform)")
|
||||||
override fun getContentDetails(url: String): IPlatformContentDetails = isBusyWith {
|
override fun getContentDetails(url: String): IPlatformContentDetails = isBusyWith("getContentDetails") {
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
return@isBusyWith IJSContentDetails.fromV8(config,
|
return@isBusyWith IJSContentDetails.fromV8(this,
|
||||||
plugin.executeTyped("source.getContentDetails(${Json.encodeToString(url)})"));
|
plugin.executeTyped("source.getContentDetails(${Json.encodeToString(url)})"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JSOptional //getContentChapters = function(url, initialData)
|
||||||
|
@JSDocs(15, "source.getContentChapters(url)", "Gets chapters for content details")
|
||||||
|
@JSDocsParameter("url", "A content url (this platform)")
|
||||||
|
override fun getContentChapters(url: String): List<IChapter> = isBusyWith("getContentChapters") {
|
||||||
|
if(!capabilities.hasGetContentChapters)
|
||||||
|
return@isBusyWith listOf();
|
||||||
|
ensureEnabled();
|
||||||
|
return@isBusyWith JSChapter.fromV8(config,
|
||||||
|
plugin.executeTyped("source.getContentChapters(${Json.encodeToString(url)})"));
|
||||||
|
}
|
||||||
|
|
||||||
@JSOptional
|
@JSOptional
|
||||||
@JSDocs(15, "source.getPlaybackTracker(url)", "Gets a playback tracker for given content url")
|
@JSDocs(15, "source.getPlaybackTracker(url)", "Gets a playback tracker for given content url")
|
||||||
@JSDocsParameter("url", "A content url (this platform)")
|
@JSDocsParameter("url", "A content url (this platform)")
|
||||||
override fun getPlaybackTracker(url: String): IPlaybackTracker? = isBusyWith {
|
override fun getPlaybackTracker(url: String): IPlaybackTracker? = isBusyWith("getPlaybackTracker") {
|
||||||
if(!capabilities.hasGetPlaybackTracker)
|
if(!capabilities.hasGetPlaybackTracker)
|
||||||
return@isBusyWith null;
|
return@isBusyWith null;
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
@@ -431,25 +528,25 @@ open class JSClient : IPlatformClient {
|
|||||||
|
|
||||||
@JSDocs(16, "source.getComments(url)", "Gets comments for a content by its url")
|
@JSDocs(16, "source.getComments(url)", "Gets comments for a content by its url")
|
||||||
@JSDocsParameter("url", "A content url (this platform)")
|
@JSDocsParameter("url", "A content url (this platform)")
|
||||||
override fun getComments(url: String): IPager<IPlatformComment> = isBusyWith {
|
override fun getComments(url: String): IPager<IPlatformComment> = isBusyWith("getComments") {
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
val pager = plugin.executeTyped<V8Value>("source.getComments(${Json.encodeToString(url)})");
|
val pager = plugin.executeTyped<V8Value>("source.getComments(${Json.encodeToString(url)})");
|
||||||
if (pager !is V8ValueObject) { //TODO: Maybe solve this better
|
if (pager !is V8ValueObject) { //TODO: Maybe solve this better
|
||||||
return@isBusyWith EmptyPager<IPlatformComment>();
|
return@isBusyWith EmptyPager<IPlatformComment>();
|
||||||
}
|
}
|
||||||
return@isBusyWith JSCommentPager(config, plugin, pager);
|
return@isBusyWith JSCommentPager(config, this, pager);
|
||||||
}
|
}
|
||||||
@JSDocs(17, "source.getSubComments(comment)", "Gets replies for a given comment")
|
@JSDocs(17, "source.getSubComments(comment)", "Gets replies for a given comment")
|
||||||
@JSDocsParameter("comment", "Comment object that was returned by getComments")
|
@JSDocsParameter("comment", "Comment object that was returned by getComments")
|
||||||
override fun getSubComments(comment: IPlatformComment): IPager<IPlatformComment> {
|
override fun getSubComments(comment: IPlatformComment): IPager<IPlatformComment> {
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
return comment.getReplies(this) ?: JSCommentPager(config, plugin,
|
return comment.getReplies(this) ?: JSCommentPager(config, this,
|
||||||
plugin.executeTyped("source.getSubComments(${Json.encodeToString(comment as JSComment)})"));
|
plugin.executeTyped("source.getSubComments(${Json.encodeToString(comment as JSComment)})"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JSDocs(16, "source.getLiveChatWindow(url)", "Gets live events for a livestream")
|
@JSDocs(16, "source.getLiveChatWindow(url)", "Gets live events for a livestream")
|
||||||
@JSDocsParameter("url", "Url of live stream")
|
@JSDocsParameter("url", "Url of live stream")
|
||||||
override fun getLiveChatWindow(url: String): ILiveChatWindowDescriptor? = isBusyWith {
|
override fun getLiveChatWindow(url: String): ILiveChatWindowDescriptor? = isBusyWith("getLiveChatWindow") {
|
||||||
if(!capabilities.hasGetLiveChatWindow)
|
if(!capabilities.hasGetLiveChatWindow)
|
||||||
return@isBusyWith null;
|
return@isBusyWith null;
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
@@ -458,11 +555,11 @@ open class JSClient : IPlatformClient {
|
|||||||
}
|
}
|
||||||
@JSDocs(16, "source.getLiveEvents(url)", "Gets live events for a livestream")
|
@JSDocs(16, "source.getLiveEvents(url)", "Gets live events for a livestream")
|
||||||
@JSDocsParameter("url", "Url of live stream")
|
@JSDocsParameter("url", "Url of live stream")
|
||||||
override fun getLiveEvents(url: String): IPager<IPlatformLiveEvent>? = isBusyWith {
|
override fun getLiveEvents(url: String): IPager<IPlatformLiveEvent>? = isBusyWith("getLiveEvents") {
|
||||||
if(!capabilities.hasGetLiveEvents)
|
if(!capabilities.hasGetLiveEvents)
|
||||||
return@isBusyWith null;
|
return@isBusyWith null;
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
return@isBusyWith JSLiveEventPager(config, plugin,
|
return@isBusyWith JSLiveEventPager(config, this,
|
||||||
plugin.executeTyped("source.getLiveEvents(${Json.encodeToString(url)})"));
|
plugin.executeTyped("source.getLiveEvents(${Json.encodeToString(url)})"));
|
||||||
}
|
}
|
||||||
@JSDocs(19, "source.searchPlaylists(query)", "Searches for playlists on the platform")
|
@JSDocs(19, "source.searchPlaylists(query)", "Searches for playlists on the platform")
|
||||||
@@ -471,27 +568,34 @@ open class JSClient : IPlatformClient {
|
|||||||
@JSDocsParameter("order", "(optional) Order in which contents should be returned")
|
@JSDocsParameter("order", "(optional) Order in which contents should be returned")
|
||||||
@JSDocsParameter("filters", "(optional) Filters to apply on contents")
|
@JSDocsParameter("filters", "(optional) Filters to apply on contents")
|
||||||
@JSDocsParameter("channelId", "(optional) Channel id to search in")
|
@JSDocsParameter("channelId", "(optional) Channel id to search in")
|
||||||
override fun searchPlaylists(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith {
|
override fun searchPlaylists(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith("searchPlaylists") {
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
if(!capabilities.hasSearchPlaylists)
|
if(!capabilities.hasSearchPlaylists)
|
||||||
throw IllegalStateException("This plugin does not support playlist search");
|
throw IllegalStateException("This plugin does not support playlist search");
|
||||||
return@isBusyWith JSContentPager(config, plugin, plugin.executeTyped("source.searchPlaylists(${Json.encodeToString(query)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})"));
|
return@isBusyWith JSContentPager(config, this, plugin.executeTyped("source.searchPlaylists(${Json.encodeToString(query)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})"));
|
||||||
}
|
}
|
||||||
@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 {
|
override fun isPlaylistUrl(url: String): Boolean {
|
||||||
ensureEnabled();
|
|
||||||
if (!capabilities.hasGetPlaylist)
|
if (!capabilities.hasGetPlaylist)
|
||||||
return false;
|
return false;
|
||||||
return plugin.executeBoolean("source.isPlaylistUrl(${Json.encodeToString(url)})") ?: false;
|
|
||||||
|
try {
|
||||||
|
return plugin.executeTyped<V8ValueBoolean>("source.isPlaylistUrl(${Json.encodeToString(url)})")
|
||||||
|
.value;
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
announcePluginUnhandledException("isPlaylistUrl", ex);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@JSOptional
|
@JSOptional
|
||||||
@JSDocs(21, "source.getPlaylist(url)", "Gets the playlist of the current user")
|
@JSDocs(21, "source.getPlaylist(url)", "Gets the playlist of the current user")
|
||||||
@JSDocsParameter("url", "Url of playlist")
|
@JSDocsParameter("url", "Url of playlist")
|
||||||
override fun getPlaylist(url: String): IPlatformPlaylistDetails = isBusyWith {
|
override fun getPlaylist(url: String): IPlatformPlaylistDetails = isBusyWith("getPlaylist") {
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
return@isBusyWith JSPlaylistDetails(plugin, plugin.config as SourcePluginConfig, plugin.executeTyped("source.getPlaylist(${Json.encodeToString(url)})"));
|
return@isBusyWith JSPlaylistDetails(this, plugin.config as SourcePluginConfig, plugin.executeTyped("source.getPlaylist(${Json.encodeToString(url)})"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JSOptional
|
@JSOptional
|
||||||
@@ -558,7 +662,7 @@ open class JSClient : IPlatformClient {
|
|||||||
if(it.containsKey(claimType)) {
|
if(it.containsKey(claimType)) {
|
||||||
val templates = it[claimType];
|
val templates = it[claimType];
|
||||||
if(templates != null)
|
if(templates != null)
|
||||||
for(value in values.keys.sortedBy { it }) {
|
for(value in values.keys.sortedBy { if(it == config.primaryClaimFieldType) Int.MIN_VALUE else it }) {
|
||||||
if(templates.containsKey(value)) {
|
if(templates.containsKey(value)) {
|
||||||
return templates[value]!!.replace("{{CLAIMVALUE}}", values[value]!!);
|
return templates[value]!!.replace("{{CLAIMVALUE}}", values[value]!!);
|
||||||
}
|
}
|
||||||
@@ -568,20 +672,42 @@ open class JSClient : IPlatformClient {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun resolveChannelUrlsByClaimTemplates(claimType: Int, values: Map<Int, String>): List<String> {
|
||||||
|
val urls = arrayListOf<String>();
|
||||||
|
channelClaimTemplates?.let {
|
||||||
|
if(it.containsKey(claimType)) {
|
||||||
|
val templates = it[claimType];
|
||||||
|
if(templates != null)
|
||||||
|
for(value in values.keys.sortedBy { it }) {
|
||||||
|
if(templates.containsKey(value)) {
|
||||||
|
urls.add(templates[value]!!.replace("{{CLAIMVALUE}}", values[value]!!));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private fun <T> isBusyWith(handle: ()->T): T {
|
return urls;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun <T> isBusyWith(actionName: String, handle: ()->T): T {
|
||||||
try {
|
try {
|
||||||
synchronized(_busyLock) {
|
synchronized(_busyLock) {
|
||||||
_busyCounter++;
|
_busyCounter++;
|
||||||
}
|
}
|
||||||
|
_busyAction = actionName;
|
||||||
return handle();
|
return handle();
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
|
_busyAction = "";
|
||||||
synchronized(_busyLock) {
|
synchronized(_busyLock) {
|
||||||
_busyCounter--;
|
_busyCounter--;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
private fun <T> isBusyWith(handle: ()->T): T {
|
||||||
|
return isBusyWith("Unknown", handle);
|
||||||
|
}
|
||||||
|
|
||||||
private fun announcePluginUnhandledException(method: String, ex: Throwable) {
|
private fun announcePluginUnhandledException(method: String, ex: Throwable) {
|
||||||
if(ex is PluginEngineException)
|
if(ex is PluginEngineException)
|
||||||
@@ -598,10 +724,43 @@ open class JSClient : IPlatformClient {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val TAG = "JSClient";
|
val TAG = "JSClient";
|
||||||
|
private val _lock = Object();
|
||||||
|
private var _docs: Map<String, String>? = null;
|
||||||
|
|
||||||
|
fun getMethodDocs(names: List<String>): Map<String, String>? {
|
||||||
|
synchronized(_lock) {
|
||||||
|
if(_docs == null) {
|
||||||
|
val client = ManagedHttpClient();
|
||||||
|
val docs = names
|
||||||
|
.map { stringWithoutBrackets(it) }
|
||||||
|
.distinct()
|
||||||
|
.parallelStream()
|
||||||
|
.map {
|
||||||
|
val url = "https://github.com/futo-org/grayjay-android/blob/master/docs/source/${it}.md";
|
||||||
|
val resp = client.head(url);
|
||||||
|
if(resp.isOk)
|
||||||
|
return@map Pair(it, url);
|
||||||
|
else
|
||||||
|
return@map null;
|
||||||
|
}.asSequence()
|
||||||
|
.filterNotNull()
|
||||||
|
.toMap();
|
||||||
|
_docs = docs;
|
||||||
|
}
|
||||||
|
return _docs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun getMethodDocUrls(): Map<String, String>? {
|
||||||
|
if(_docs != null)
|
||||||
|
return _docs;
|
||||||
|
val methods = JSClient::class.java.declaredMethods.filter { it.getAnnotation(JSDocs::class.java) != null }
|
||||||
|
return getMethodDocs(methods.map { it.name });
|
||||||
|
}
|
||||||
|
|
||||||
fun getJSDocs(): List<JSCallDocs> {
|
fun getJSDocs(): List<JSCallDocs> {
|
||||||
val docs = mutableListOf<JSCallDocs>();
|
val docs = mutableListOf<JSCallDocs>();
|
||||||
val methods = JSClient::class.java.declaredMethods.filter { it.getAnnotation(JSDocs::class.java) != null }
|
val methods = JSClient::class.java.declaredMethods.filter { it.getAnnotation(JSDocs::class.java) != null }
|
||||||
|
|
||||||
for(method in methods.sortedBy { it.getAnnotation(JSDocs::class.java)?.order }) {
|
for(method in methods.sortedBy { it.getAnnotation(JSDocs::class.java)?.order }) {
|
||||||
val doc = method.getAnnotation(JSDocs::class.java);
|
val doc = method.getAnnotation(JSDocs::class.java);
|
||||||
val parameters = method.kotlinFunction!!.findAnnotations<JSDocsParameter>();
|
val parameters = method.kotlinFunction!!.findAnnotations<JSDocsParameter>();
|
||||||
@@ -614,5 +773,12 @@ open class JSClient : IPlatformClient {
|
|||||||
}
|
}
|
||||||
return docs;
|
return docs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun stringWithoutBrackets(name: String): String {
|
||||||
|
val index = name.indexOf('(');
|
||||||
|
if(index >= 0)
|
||||||
|
return name.substring(0, index);
|
||||||
|
return name;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,20 +1,17 @@
|
|||||||
package com.futo.platformplayer.api.media.platforms.js
|
package com.futo.platformplayer.api.media.platforms.js
|
||||||
|
|
||||||
import com.futo.platformplayer.encryption.EncryptionProvider
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
|
|
||||||
data class SourceAuth(val cookieMap: HashMap<String, HashMap<String, String>>? = null, val headers: Map<String, Map<String, String>> = mapOf()) {
|
data class SourceAuth(val cookieMap: HashMap<String, HashMap<String, String>>? = null, val headers: Map<String, Map<String, String>> = mapOf()) {
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return "(headers: '$headers', cookieString: '$cookieMap')";
|
return "(headers: '$headers', cookieString: '$cookieMap')";
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toEncrypted(): String{
|
fun toEncrypted(): String{
|
||||||
return EncryptionProvider.instance.encrypt(serialize());
|
return SourceEncrypted.fromDecrypted { serialize() }.toJson();
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun serialize(): String {
|
private fun serialize(): String {
|
||||||
@@ -25,20 +22,10 @@ data class SourceAuth(val cookieMap: HashMap<String, HashMap<String, String>>? =
|
|||||||
val TAG = "SourceAuth";
|
val TAG = "SourceAuth";
|
||||||
|
|
||||||
fun fromEncrypted(encrypted: String?): SourceAuth? {
|
fun fromEncrypted(encrypted: String?): SourceAuth? {
|
||||||
if(encrypted == null)
|
return SourceEncrypted.decryptEncrypted(encrypted) { deserialize(it) };
|
||||||
return null;
|
|
||||||
|
|
||||||
val decrypted = EncryptionProvider.instance.decrypt(encrypted);
|
|
||||||
try {
|
|
||||||
return deserialize(decrypted);
|
|
||||||
}
|
|
||||||
catch(ex: Throwable) {
|
|
||||||
Logger.e(TAG, "Failed to deserialize authentication", ex);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deserialize(str: String): SourceAuth {
|
private fun deserialize(str: String): SourceAuth {
|
||||||
val data = Json.decodeFromString<SerializedAuth>(str);
|
val data = Json.decodeFromString<SerializedAuth>(str);
|
||||||
return SourceAuth(data.cookieMap, data.headers);
|
return SourceAuth(data.cookieMap, data.headers);
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-15
@@ -1,7 +1,5 @@
|
|||||||
package com.futo.platformplayer.api.media.platforms.js
|
package com.futo.platformplayer.api.media.platforms.js
|
||||||
|
|
||||||
import com.futo.platformplayer.encryption.EncryptionProvider
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
@@ -13,7 +11,7 @@ data class SourceCaptchaData(val cookieMap: HashMap<String, HashMap<String, Stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun toEncrypted(): String{
|
fun toEncrypted(): String{
|
||||||
return EncryptionProvider.instance.encrypt(serialize());
|
return SourceEncrypted.fromDecrypted { serialize() }.toJson();
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun serialize(): String {
|
private fun serialize(): String {
|
||||||
@@ -21,20 +19,10 @@ data class SourceCaptchaData(val cookieMap: HashMap<String, HashMap<String, Stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val TAG = "SourceAuth";
|
val TAG = "SourceCaptchaData";
|
||||||
|
|
||||||
fun fromEncrypted(encrypted: String?): SourceCaptchaData? {
|
fun fromEncrypted(encrypted: String?): SourceCaptchaData? {
|
||||||
if(encrypted == null)
|
return SourceEncrypted.decryptEncrypted(encrypted) { deserialize(it) };
|
||||||
return null;
|
|
||||||
|
|
||||||
val decrypted = EncryptionProvider.instance.decrypt(encrypted);
|
|
||||||
try {
|
|
||||||
return deserialize(decrypted);
|
|
||||||
}
|
|
||||||
catch(ex: Throwable) {
|
|
||||||
Logger.e(TAG, "Failed to deserialize authentication", ex);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deserialize(str: String): SourceCaptchaData {
|
fun deserialize(str: String): SourceCaptchaData {
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package com.futo.platformplayer.api.media.platforms.js
|
||||||
|
|
||||||
|
import com.futo.platformplayer.encryption.GEncryptionProvider
|
||||||
|
import com.futo.platformplayer.encryption.GEncryptionProviderV0
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import java.lang.Exception
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SourceEncrypted(
|
||||||
|
val encrypted: String,
|
||||||
|
val version: Int = GEncryptionProvider.version
|
||||||
|
) {
|
||||||
|
fun toJson(): String {
|
||||||
|
return Json.encodeToString(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromDecrypted(serializer: () -> String): SourceEncrypted {
|
||||||
|
return SourceEncrypted(GEncryptionProvider.instance.encrypt(serializer()));
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> decryptEncrypted(encrypted: String?, deserializer: (decrypted: String) -> T): T? {
|
||||||
|
if(encrypted == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
val encryptedSourceAuth = Json.decodeFromString<SourceEncrypted>(encrypted)
|
||||||
|
if (encryptedSourceAuth.version != GEncryptionProvider.version) {
|
||||||
|
throw Exception("Invalid encryption version.");
|
||||||
|
}
|
||||||
|
|
||||||
|
val decrypted = GEncryptionProvider.instance.decrypt(encryptedSourceAuth.encrypted);
|
||||||
|
try {
|
||||||
|
return deserializer(decrypted);
|
||||||
|
} catch(ex: Throwable) {
|
||||||
|
Logger.e(SourceAuth.TAG, "Failed to deserialize SourceEncrypted<T>", ex);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
//Try to fall back to old mechanism, remove this eventually
|
||||||
|
if (!encrypted.contains("version")) {
|
||||||
|
val decrypted = GEncryptionProviderV0.instance.decrypt(encrypted);
|
||||||
|
try {
|
||||||
|
return deserializer(decrypted);
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
Logger.e(SourceAuth.TAG, "Failed to deserialize SourceEncrypted<T>", ex);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
@@ -11,4 +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
|
||||||
) { }
|
) { }
|
||||||
+9
-5
@@ -5,9 +5,8 @@ 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.states.StatePlugins
|
import com.futo.platformplayer.states.StatePlugins
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.util.*
|
import java.util.UUID
|
||||||
|
|
||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
class SourcePluginConfig(
|
class SourcePluginConfig(
|
||||||
@@ -41,10 +40,13 @@ class SourcePluginConfig(
|
|||||||
val constants: HashMap<String, String> = hashMapOf(),
|
val constants: HashMap<String, String> = hashMapOf(),
|
||||||
|
|
||||||
//TODO: These should be vals...but prob for serialization reasons cannot be changed.
|
//TODO: These should be vals...but prob for serialization reasons cannot be changed.
|
||||||
|
var platformUrl: String? = null,
|
||||||
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 supportedClaimTypes: List<Int> = listOf()
|
var supportedClaimTypes: List<Int> = listOf(),
|
||||||
|
var primaryClaimFieldType: Int? = null,
|
||||||
|
var developerSubmitUrl: String? = null
|
||||||
) : IV8PluginConfig {
|
) : IV8PluginConfig {
|
||||||
|
|
||||||
val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl);
|
val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl);
|
||||||
@@ -142,9 +144,11 @@ class SourcePluginConfig(
|
|||||||
val description: String,
|
val description: String,
|
||||||
val type: String,
|
val type: String,
|
||||||
val default: String? = null,
|
val default: String? = null,
|
||||||
val variable: String? = null
|
val variable: String? = null,
|
||||||
|
val dependency: String? = null,
|
||||||
|
val warningDialog: String? = null,
|
||||||
|
val options: List<String>? = null
|
||||||
) {
|
) {
|
||||||
@kotlinx.serialization.Transient
|
|
||||||
val variableOrName: String get() = variable ?: name;
|
val variableOrName: String get() = variable ?: name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+66
-10
@@ -1,9 +1,14 @@
|
|||||||
package com.futo.platformplayer.api.media.platforms.js
|
package com.futo.platformplayer.api.media.platforms.js
|
||||||
|
|
||||||
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.constructs.Event0
|
import com.futo.platformplayer.constructs.Event0
|
||||||
import com.futo.platformplayer.serializers.FlexibleBooleanSerializer
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.states.AnnouncementType
|
||||||
|
import com.futo.platformplayer.states.StateAnnouncement
|
||||||
|
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.FormFieldWarning
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -25,17 +30,19 @@ class SourcePluginDescriptor {
|
|||||||
@kotlinx.serialization.Transient
|
@kotlinx.serialization.Transient
|
||||||
val onCaptchaChanged = Event0();
|
val onCaptchaChanged = Event0();
|
||||||
|
|
||||||
constructor(config :SourcePluginConfig, authEncrypted: String? = null, captchaEncrypted: String? = null) {
|
constructor(config :SourcePluginConfig, authEncrypted: String? = null, captchaEncrypted: String? = null, settings: HashMap<String, String?>? = null) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.authEncrypted = authEncrypted;
|
this.authEncrypted = authEncrypted;
|
||||||
this.captchaEncrypted = captchaEncrypted;
|
this.captchaEncrypted = captchaEncrypted;
|
||||||
this.flags = listOf();
|
this.flags = listOf();
|
||||||
|
this.settings = settings ?: hashMapOf();
|
||||||
}
|
}
|
||||||
constructor(config :SourcePluginConfig, authEncrypted: String? = null, captchaEncrypted: String? = null, flags: List<String>) {
|
constructor(config :SourcePluginConfig, authEncrypted: String? = null, captchaEncrypted: String? = null, flags: List<String>, settings: HashMap<String, String?>? = null) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.authEncrypted = authEncrypted;
|
this.authEncrypted = authEncrypted;
|
||||||
this.captchaEncrypted = captchaEncrypted;
|
this.captchaEncrypted = captchaEncrypted;
|
||||||
this.flags = flags;
|
this.flags = flags;
|
||||||
|
this.settings = settings ?: hashMapOf();
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSettingsWithDefaults(): HashMap<String, String?> {
|
fun getSettingsWithDefaults(): HashMap<String, String?> {
|
||||||
@@ -52,7 +59,16 @@ class SourcePluginDescriptor {
|
|||||||
onCaptchaChanged.emit();
|
onCaptchaChanged.emit();
|
||||||
}
|
}
|
||||||
fun getCaptchaData(): SourceCaptchaData? {
|
fun getCaptchaData(): SourceCaptchaData? {
|
||||||
return SourceCaptchaData.fromEncrypted(captchaEncrypted);
|
try {
|
||||||
|
return SourceCaptchaData.fromEncrypted(captchaEncrypted);
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e("SourcePluginDescriptor", "Captcha decode failed, disabling auth.", ex);
|
||||||
|
StateAnnouncement.instance.registerAnnouncement("CAP_BROKEN_" + config.id,
|
||||||
|
"Captcha corrupted for plugin [${config.name}]",
|
||||||
|
"Something went wrong in the stored captcha, you'll have to login again", AnnouncementType.SESSION);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateAuth(str: SourceAuth?) {
|
fun updateAuth(str: SourceAuth?) {
|
||||||
@@ -60,31 +76,71 @@ class SourcePluginDescriptor {
|
|||||||
onAuthChanged.emit();
|
onAuthChanged.emit();
|
||||||
}
|
}
|
||||||
fun getAuth(): SourceAuth? {
|
fun getAuth(): SourceAuth? {
|
||||||
return SourceAuth.fromEncrypted(authEncrypted);
|
try {
|
||||||
|
return SourceAuth.fromEncrypted(authEncrypted);
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e("SourcePluginDescriptor", "Authentication decode failed, disabling auth.", ex);
|
||||||
|
StateAnnouncement.instance.registerAnnouncement("AUTH_BROKEN_" + config.id,
|
||||||
|
"Authentication corrupted for plugin [${config.name}]",
|
||||||
|
"Something went wrong in the stored authentication, you'll have to login again", AnnouncementType.SESSION);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class AppPluginSettings {
|
class AppPluginSettings {
|
||||||
|
|
||||||
@FormField("Visibility", "group", "Enable where this plugin's content are visible.", 2)
|
@FormField(R.string.check_for_updates_setting, FieldForm.TOGGLE, R.string.check_for_updates_setting_description, 0)
|
||||||
|
var checkForUpdates: Boolean = true;
|
||||||
|
|
||||||
|
@FormField(R.string.visibility, "group", R.string.enable_where_this_plugins_content_are_visible, 2)
|
||||||
var tabEnabled = TabEnabled();
|
var tabEnabled = TabEnabled();
|
||||||
@Serializable
|
@Serializable
|
||||||
class TabEnabled {
|
class TabEnabled {
|
||||||
@FormField("Home", FieldForm.TOGGLE, "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("Search", FieldForm.TOGGLE, "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.ratelimit, "group", R.string.ratelimit_description, 3)
|
||||||
|
var rateLimit = RateLimit();
|
||||||
|
@Serializable
|
||||||
|
class RateLimit {
|
||||||
|
@FormField(R.string.subscriptions, FieldForm.DROPDOWN, R.string.ratelimit_sub_setting_description, 1)
|
||||||
|
@DropdownFieldOptions("Plugin defined", "25", "50", "75", "100", "125", "150", "200")
|
||||||
|
var rateLimitSubs: Int = 0;
|
||||||
|
|
||||||
|
fun getSubRateLimit(): Int {
|
||||||
|
return when(rateLimitSubs) {
|
||||||
|
0 -> -1
|
||||||
|
1 -> 25
|
||||||
|
2 -> 50
|
||||||
|
3 -> 75
|
||||||
|
4 -> 100
|
||||||
|
5 -> 125
|
||||||
|
6 -> 150
|
||||||
|
7 -> 200
|
||||||
|
else -> -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@FormField(R.string.allow_developer_submit, FieldForm.TOGGLE, R.string.allow_developer_submit_description, 1, "devSubmit")
|
||||||
|
var allowDeveloperSubmit: Boolean = false;
|
||||||
|
|
||||||
|
|
||||||
fun loadDefaults(config: SourcePluginConfig) {
|
fun loadDefaults(config: SourcePluginConfig) {
|
||||||
if(tabEnabled.enableHome == null)
|
if(tabEnabled.enableHome == null)
|
||||||
tabEnabled.enableHome = config.enableInHome ?: true;
|
tabEnabled.enableHome = config.enableInHome
|
||||||
if(tabEnabled.enableSearch == null)
|
if(tabEnabled.enableSearch == null)
|
||||||
tabEnabled.enableSearch = config.enableInSearch ?: true;
|
tabEnabled.enableSearch = config.enableInSearch
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,6 @@ annotation class JSOptional()
|
|||||||
annotation class JSDocsParameter(val name: String, val description: String, val order: Int = 0)
|
annotation class JSDocsParameter(val name: String, val description: String, val order: Int = 0)
|
||||||
|
|
||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
data class JSCallDocs(val title: String, val code: String, val description: String, val parameters: List<JSParameterDocs>, val isOptional: Boolean = false);
|
data class JSCallDocs(val title: String, val code: String, val description: String, val parameters: List<JSParameterDocs>, val isOptional: Boolean = false, val docsUrl: String? = null);
|
||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
data class JSParameterDocs(val name: String, val description: String);
|
data class JSParameterDocs(val name: String, val description: String);
|
||||||
+103
-73
@@ -1,31 +1,41 @@
|
|||||||
package com.futo.platformplayer.api.media.platforms.js.internal
|
package com.futo.platformplayer.api.media.platforms.js.internal
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.futo.platformplayer.logging.Logger
|
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourceAuth
|
import com.futo.platformplayer.api.media.platforms.js.SourceAuth
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourceCaptchaData
|
import com.futo.platformplayer.api.media.platforms.js.SourceCaptchaData
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.JSRequest
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestModifier
|
||||||
|
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
||||||
import com.futo.platformplayer.matchesDomain
|
import com.futo.platformplayer.matchesDomain
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
class JSHttpClient : ManagedHttpClient {
|
class JSHttpClient : ManagedHttpClient {
|
||||||
private val _jsClient: JSClient?;
|
private val _jsClient: JSClient?;
|
||||||
|
private val _jsConfig: SourcePluginConfig?;
|
||||||
private val _auth: SourceAuth?;
|
private val _auth: SourceAuth?;
|
||||||
private val _captcha: SourceCaptchaData?;
|
private val _captcha: SourceCaptchaData?;
|
||||||
|
|
||||||
|
val clientId = UUID.randomUUID().toString();
|
||||||
|
|
||||||
var doUpdateCookies: Boolean = true;
|
var doUpdateCookies: Boolean = true;
|
||||||
var doApplyCookies: Boolean = true;
|
var doApplyCookies: Boolean = true;
|
||||||
var doAllowNewCookies: Boolean = true;
|
var doAllowNewCookies: Boolean = true;
|
||||||
val isLoggedIn: Boolean get() = _auth != null;
|
val isLoggedIn: Boolean get() = _auth != null;
|
||||||
|
|
||||||
private var _currentCookieMap: HashMap<String, HashMap<String, String>>;
|
private var _currentCookieMap: HashMap<String, HashMap<String, String>>;
|
||||||
|
private var _otherCookieMap: HashMap<String, HashMap<String, String>>;
|
||||||
|
|
||||||
constructor(jsClient: JSClient?, auth: SourceAuth? = null, captcha: SourceCaptchaData? = null) : super() {
|
constructor(jsClient: JSClient?, auth: SourceAuth? = null, captcha: SourceCaptchaData? = null, config: SourcePluginConfig? = null) : super() {
|
||||||
_jsClient = jsClient;
|
_jsClient = jsClient;
|
||||||
|
_jsConfig = config;
|
||||||
_auth = auth;
|
_auth = auth;
|
||||||
_captcha = captcha;
|
_captcha = captcha;
|
||||||
|
|
||||||
_currentCookieMap = hashMapOf();
|
_currentCookieMap = hashMapOf();
|
||||||
|
_otherCookieMap = hashMapOf();
|
||||||
if(!auth?.cookieMap.isNullOrEmpty()) {
|
if(!auth?.cookieMap.isNullOrEmpty()) {
|
||||||
for(domainCookies in auth!!.cookieMap!!)
|
for(domainCookies in auth!!.cookieMap!!)
|
||||||
_currentCookieMap.put(domainCookies.key, HashMap(domainCookies.value));
|
_currentCookieMap.put(domainCookies.key, HashMap(domainCookies.value));
|
||||||
@@ -43,13 +53,49 @@ class JSHttpClient : ManagedHttpClient {
|
|||||||
|
|
||||||
override fun clone(): ManagedHttpClient {
|
override fun clone(): ManagedHttpClient {
|
||||||
val newClient = JSHttpClient(_jsClient, _auth);
|
val newClient = JSHttpClient(_jsClient, _auth);
|
||||||
newClient._currentCookieMap = if(_currentCookieMap != null)
|
newClient._currentCookieMap = HashMap(_currentCookieMap.toList().associate { Pair(it.first, HashMap(it.second)) })
|
||||||
HashMap(_currentCookieMap.toList().associate { Pair(it.first, HashMap(it.second)) })
|
|
||||||
else
|
|
||||||
hashMapOf();
|
|
||||||
return newClient;
|
return newClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO: Use this in beforeRequest to remove dup code
|
||||||
|
fun applyHeaders(url: Uri, headers: MutableMap<String, String>, applyAuth: Boolean = false, applyOtherCookies: Boolean = false) {
|
||||||
|
val domain = url.host!!.lowercase();
|
||||||
|
val auth = _auth;
|
||||||
|
if (applyAuth && auth != null) {
|
||||||
|
//TODO: Possibly add doApplyHeaders
|
||||||
|
for (header in auth.headers.filter { domain.matchesDomain(it.key) }.flatMap { it.value.entries })
|
||||||
|
headers.put(header.key, header.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(doApplyCookies && (applyAuth || applyOtherCookies)) {
|
||||||
|
val cookiesToApply = hashMapOf<String, String>();
|
||||||
|
if(applyOtherCookies)
|
||||||
|
synchronized(_otherCookieMap) {
|
||||||
|
for(cookie in _otherCookieMap
|
||||||
|
.filter { domain.matchesDomain(it.key) }
|
||||||
|
.flatMap { it.value.toList() })
|
||||||
|
cookiesToApply[cookie.first] = cookie.second;
|
||||||
|
}
|
||||||
|
if(applyAuth)
|
||||||
|
synchronized(_currentCookieMap) {
|
||||||
|
for(cookie in _currentCookieMap
|
||||||
|
.filter { domain.matchesDomain(it.key) }
|
||||||
|
.flatMap { it.value.toList() })
|
||||||
|
cookiesToApply[cookie.first] = cookie.second;
|
||||||
|
};
|
||||||
|
|
||||||
|
if(cookiesToApply.size > 0) {
|
||||||
|
val cookieString = cookiesToApply.map { it.key + "=" + it.value }.joinToString("; ");
|
||||||
|
|
||||||
|
val existingCookies = headers["Cookie"];
|
||||||
|
if(!existingCookies.isNullOrEmpty())
|
||||||
|
headers.put("Cookie", existingCookies.trim(';') + "; " + cookieString);
|
||||||
|
else
|
||||||
|
headers.put("Cookie", cookieString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun beforeRequest(request: okhttp3.Request): okhttp3.Request {
|
override fun beforeRequest(request: okhttp3.Request): okhttp3.Request {
|
||||||
val domain = request.url.host.lowercase();
|
val domain = request.url.host.lowercase();
|
||||||
val auth = _auth;
|
val auth = _auth;
|
||||||
@@ -65,10 +111,10 @@ class JSHttpClient : ManagedHttpClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(doApplyCookies) {
|
if(doApplyCookies) {
|
||||||
if (!_currentCookieMap.isNullOrEmpty()) {
|
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
|
||||||
.filter { domain.matchesDomain(it.key) }
|
.filter { domain.matchesDomain(it.key) }
|
||||||
.flatMap { it.value.toList() })
|
.flatMap { it.value.toList() })
|
||||||
cookiesToApply[cookie.first] = cookie.second;
|
cookiesToApply[cookie.first] = cookie.second;
|
||||||
@@ -87,8 +133,12 @@ class JSHttpClient : ManagedHttpClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_jsClient?.validateUrlOrThrow(request.url.toString());
|
if(_jsClient != null)
|
||||||
return newBuilder?.let { it.build() } ?: request;
|
_jsClient.validateUrlOrThrow(request.url.toString());
|
||||||
|
else if (_jsConfig != null && !_jsConfig.isUrlAllowed(request.url.toString()))
|
||||||
|
throw ScriptImplementationException(_jsConfig, "Attempted to access non-whitelisted url: ${request.url.toString()}\nAdd it to your config");
|
||||||
|
|
||||||
|
return newBuilder?.build() ?: request;
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun afterRequest(resp: okhttp3.Response): okhttp3.Response {
|
override fun afterRequest(resp: okhttp3.Response): okhttp3.Response {
|
||||||
@@ -98,85 +148,65 @@ class JSHttpClient : ManagedHttpClient {
|
|||||||
val defaultCookieDomain =
|
val defaultCookieDomain =
|
||||||
"." + domainParts.drop(domainParts.size - 2).joinToString(".");
|
"." + domainParts.drop(domainParts.size - 2).joinToString(".");
|
||||||
for (header in resp.headers) {
|
for (header in resp.headers) {
|
||||||
if ((_auth != null || _currentCookieMap.isNotEmpty()) && header.first.lowercase() == "set-cookie") {
|
if(header.first.lowercase() == "set-cookie") {
|
||||||
//val newCookies = cookieStringToMap(header.second.split("; "));
|
var domainToUse = domain;
|
||||||
val cookie = cookieStringToPair(header.second);
|
val cookie = cookieStringToPair(header.second);
|
||||||
//for (cookie in newCookies) {
|
var cookieValue = cookie.second;
|
||||||
var cookieValue = cookie.second;
|
|
||||||
var domainToUse = domain;
|
|
||||||
|
|
||||||
if (!cookie.first.isNullOrEmpty() && !cookie.second.isNullOrEmpty()) {
|
if (cookie.first.isNotEmpty() && cookie.second.isNotEmpty()) {
|
||||||
val cookieParts = cookie.second.split(";");
|
val cookieParts = cookie.second.split(";");
|
||||||
if (cookieParts.size == 0)
|
if (cookieParts.size == 0)
|
||||||
continue;
|
continue;
|
||||||
cookieValue = cookieParts[0].trim();
|
cookieValue = cookieParts[0].trim();
|
||||||
|
|
||||||
val cookieVariables = cookieParts.drop(1).map {
|
val cookieVariables = cookieParts.drop(1).map {
|
||||||
val splitIndex = it.indexOf("=");
|
val splitIndex = it.indexOf("=");
|
||||||
if (splitIndex < 0)
|
if (splitIndex < 0)
|
||||||
return@map Pair(it.trim().lowercase(), "");
|
return@map Pair(it.trim().lowercase(), "");
|
||||||
return@map Pair<String, String>(
|
return@map Pair<String, String>(
|
||||||
it.substring(0, splitIndex).lowercase().trim(),
|
it.substring(0, splitIndex).lowercase().trim(),
|
||||||
it.substring(splitIndex + 1).trim()
|
it.substring(splitIndex + 1).trim()
|
||||||
);
|
);
|
||||||
}.toMap();
|
}.toMap();
|
||||||
domainToUse = if (cookieVariables.containsKey("domain"))
|
domainToUse = if (cookieVariables.containsKey("domain"))
|
||||||
cookieVariables["domain"]!!.lowercase();
|
cookieVariables["domain"]!!.lowercase();
|
||||||
else defaultCookieDomain;
|
else defaultCookieDomain;
|
||||||
}
|
//TODO: Make sure this has no negative effect besides apply cookies to root domain
|
||||||
|
if(!domainToUse.startsWith("."))
|
||||||
|
domainToUse = ".${domainToUse}";
|
||||||
|
}
|
||||||
|
|
||||||
val cookieMap = if (_currentCookieMap!!.containsKey(domainToUse))
|
if ((_auth != null || _currentCookieMap.isNotEmpty())) {
|
||||||
_currentCookieMap!![domainToUse]!!;
|
val cookieMap = if (_currentCookieMap.containsKey(domainToUse))
|
||||||
|
_currentCookieMap[domainToUse]!!;
|
||||||
else {
|
else {
|
||||||
val newMap = hashMapOf<String, String>();
|
val newMap = hashMapOf<String, String>();
|
||||||
_currentCookieMap!!.put(domainToUse, newMap)
|
_currentCookieMap[domainToUse] = newMap
|
||||||
newMap;
|
newMap;
|
||||||
}
|
}
|
||||||
if(cookieMap.containsKey(cookie.first) || doAllowNewCookies)
|
if (cookieMap.containsKey(cookie.first) || doAllowNewCookies)
|
||||||
cookieMap.put(cookie.first, cookieValue);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun cookieStringToMap(parts: List<String>): Map<String, String> {
|
|
||||||
val map = hashMapOf<String, String>();
|
|
||||||
for(cookie in parts) {
|
|
||||||
val pair = cookieStringToPair(cookie)
|
|
||||||
map.put(pair.first, pair.second);
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
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("="));
|
||||||
val cookieVal = cookie.substring(cookie.indexOf("=") + 1);
|
val cookieVal = cookie.substring(cookie.indexOf("=") + 1);
|
||||||
return Pair(cookieKey.trim(), cookieVal.trim());
|
return Pair(cookieKey.trim(), cookieVal.trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
//Prints out code for test reproduction..
|
|
||||||
fun printTestCode(url: String, body: ByteArray?, headers: Map<String, String>, cookieString: String, allHeaders: Map<String, String>? = null) {
|
|
||||||
var code = "Code: \n";
|
|
||||||
code += "\nurl = \"${url}\";";
|
|
||||||
if(body != null)
|
|
||||||
code += "\nbody = \"${String(body).replace("\"", "\\\"")}\";";
|
|
||||||
if(headers != null)
|
|
||||||
for(header in headers) {
|
|
||||||
code += "\nclient.Headers.Add(\"${header.key}\", \"${header.value}\");";
|
|
||||||
}
|
|
||||||
if(cookieString != null)
|
|
||||||
code += "\nclient.Headers.Add(\"Cookie\", \"${cookieString}\");";
|
|
||||||
|
|
||||||
if(allHeaders != null) {
|
|
||||||
code += "\n//OTHER HEADERS:"
|
|
||||||
for (header in allHeaders) {
|
|
||||||
code += "\nclient.Headers.Add(\"${header.key}\", \"${header.value}\");";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.i("Testing", code);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
+5
-2
@@ -3,6 +3,7 @@ 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.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.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.getOrDefault
|
import com.futo.platformplayer.getOrDefault
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
@@ -10,19 +11,21 @@ import com.futo.platformplayer.getOrThrow
|
|||||||
interface IJSContent: IPlatformContent {
|
interface IJSContent: IPlatformContent {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromV8(config: SourcePluginConfig, obj: V8ValueObject): IPlatformContent {
|
fun fromV8(plugin: JSClient, obj: V8ValueObject): IPlatformContent {
|
||||||
|
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);
|
||||||
|
|
||||||
//TODO: Temporary workaround for intercepting details in lists
|
//TODO: Temporary workaround for intercepting details in lists
|
||||||
if(pluginType != null && pluginType.endsWith("Details"))
|
if(pluginType != null && pluginType.endsWith("Details"))
|
||||||
return IJSContentDetails.fromV8(config, obj);
|
return IJSContentDetails.fromV8(plugin, obj);
|
||||||
|
|
||||||
return when(ContentType.fromInt(type)) {
|
return when(ContentType.fromInt(type)) {
|
||||||
ContentType.MEDIA -> JSVideo(config, obj);
|
ContentType.MEDIA -> JSVideo(config, obj);
|
||||||
ContentType.POST -> JSPost(config, obj);
|
ContentType.POST -> JSPost(config, obj);
|
||||||
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);
|
||||||
else -> throw NotImplementedError("Unknown content type ${type}");
|
else -> throw NotImplementedError("Unknown content type ${type}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-4
@@ -4,17 +4,18 @@ import com.caoccao.javet.values.reference.V8ValueObject
|
|||||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
import com.futo.platformplayer.api.media.models.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.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
|
||||||
interface IJSContentDetails: IPlatformContent {
|
interface IJSContentDetails: IPlatformContent {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromV8(config: SourcePluginConfig, obj: V8ValueObject): IPlatformContentDetails {
|
fun fromV8(plugin: JSClient, obj: V8ValueObject): IPlatformContentDetails {
|
||||||
val type: Int = obj.getOrThrow(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(config, obj);
|
ContentType.MEDIA -> JSVideoDetails(plugin, obj);
|
||||||
ContentType.POST -> JSPostDetails(config, obj);
|
ContentType.POST -> JSPostDetails(plugin.config, obj);
|
||||||
else -> throw NotImplementedError("Unknown content type ${type}");
|
else -> throw NotImplementedError("Unknown content type ${type}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -2,13 +2,14 @@ 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.PlatformAuthorLink
|
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.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
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
|
|
||||||
class JSChannelPager : JSPager<PlatformAuthorLink>, IPager<PlatformAuthorLink> {
|
class JSChannelPager : JSPager<PlatformAuthorLink>, IPager<PlatformAuthorLink> {
|
||||||
|
|
||||||
constructor(config: SourcePluginConfig, plugin: V8Plugin, pager: V8ValueObject) : super(config, plugin, pager) {}
|
constructor(config: SourcePluginConfig, plugin: JSClient, pager: V8ValueObject) : super(config, plugin, pager) {}
|
||||||
|
|
||||||
override fun convertResult(obj: V8ValueObject): PlatformAuthorLink {
|
override fun convertResult(obj: V8ValueObject): PlatformAuthorLink {
|
||||||
return PlatformAuthorLink.fromV8(config, obj);
|
return PlatformAuthorLink.fromV8(config, obj);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user