mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-29 11:03:01 +02:00
Compare commits
189 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5f1c0209a8 | |||
| 819e81b7a6 | |||
| 8193234c2f | |||
| 6263a31f41 | |||
| 481a0cda99 | |||
| b39b89e908 | |||
| ce0f98055f | |||
| 3dddf68766 | |||
| 88d687f26e | |||
| d44df42727 | |||
| 88c8dbcb7c | |||
| b4fddbe26a | |||
| ab6d7669d7 | |||
| 3f22c7f717 | |||
| f36e9588cb | |||
| 8f99f399ee | |||
| 56166a7948 | |||
| 4edd8ee1ea | |||
| a830c918ab | |||
| 53f74c4b6e | |||
| 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 |
+6
-3
@@ -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
|
||||||
@@ -61,3 +58,9 @@
|
|||||||
[submodule "dep/futopay"]
|
[submodule "dep/futopay"]
|
||||||
path = dep/futopay
|
path = dep/futopay
|
||||||
url = ../futopayclientlibraries.git
|
url = ../futopayclientlibraries.git
|
||||||
|
[submodule "app/src/unstable/assets/sources/bilibili"]
|
||||||
|
path = app/src/unstable/assets/sources/bilibili
|
||||||
|
url = ../plugins/bilibili.git
|
||||||
|
[submodule "app/src/stable/assets/sources/bilibili"]
|
||||||
|
path = app/src/stable/assets/sources/bilibili
|
||||||
|
url = ../plugins/bilibili.git
|
||||||
|
|||||||
+11
-11
@@ -151,7 +151,7 @@ dependencies {
|
|||||||
//Core
|
//Core
|
||||||
implementation 'androidx.core:core-ktx:1.12.0'
|
implementation 'androidx.core:core-ktx:1.12.0'
|
||||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||||
implementation 'com.google.android.material:material:1.10.0'
|
implementation 'com.google.android.material:material:1.11.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||||
|
|
||||||
//Images
|
//Images
|
||||||
@@ -169,18 +169,18 @@ dependencies {
|
|||||||
implementation 'com.google.code.gson:gson:2.10.1' //Used for complex/anonymous cases like during development conversions (eg. V8RemoteObject)
|
implementation 'com.google.code.gson:gson:2.10.1' //Used for complex/anonymous cases like during development conversions (eg. V8RemoteObject)
|
||||||
|
|
||||||
//JS
|
//JS
|
||||||
implementation("com.caoccao.javet:javet-android:2.2.1")
|
implementation("com.caoccao.javet:javet-android:3.0.2")
|
||||||
|
|
||||||
//Exoplayer
|
//Exoplayer
|
||||||
implementation 'androidx.media3:media3-exoplayer:1.2.0'
|
implementation 'androidx.media3:media3-exoplayer:1.2.1'
|
||||||
implementation 'androidx.media3:media3-exoplayer-dash:1.2.0'
|
implementation 'androidx.media3:media3-exoplayer-dash:1.2.1'
|
||||||
implementation 'androidx.media3:media3-ui:1.2.0'
|
implementation 'androidx.media3:media3-ui:1.2.1'
|
||||||
implementation 'androidx.media3:media3-exoplayer-hls:1.2.0'
|
implementation 'androidx.media3:media3-exoplayer-hls:1.2.1'
|
||||||
implementation 'androidx.media3:media3-exoplayer-rtsp:1.2.0'
|
implementation 'androidx.media3:media3-exoplayer-rtsp:1.2.1'
|
||||||
implementation 'androidx.media3:media3-exoplayer-smoothstreaming:1.2.0'
|
implementation 'androidx.media3:media3-exoplayer-smoothstreaming:1.2.1'
|
||||||
implementation 'androidx.media3:media3-transformer:1.2.0'
|
implementation 'androidx.media3:media3-transformer:1.2.1'
|
||||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.5'
|
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.6'
|
||||||
implementation 'androidx.navigation:navigation-ui-ktx:2.7.5'
|
implementation 'androidx.navigation:navigation-ui-ktx:2.7.6'
|
||||||
implementation 'androidx.media:media:1.7.0'
|
implementation 'androidx.media:media:1.7.0'
|
||||||
|
|
||||||
//Other
|
//Other
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_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_MEDIA_PLAYBACK"/>
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
|
||||||
|
<uses-permission android:name="android.permission.WRITE_SETTINGS" tools:ignore="ProtectedPermissions"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
@@ -24,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"
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_287_2206)">
|
||||||
|
<path d="M22.0557 38.25L43.1117 6H1L22.0557 38.25Z" fill="url(#paint0_linear_287_2206)"/>
|
||||||
|
<path d="M6 28.2444C6.85811 27.3291 8.98625 25.2353 10.6338 24.1827C12.2814 23.13 14.257 20.1209 15.0388 18.7479C17.4224 15.2392 22.7618 7.91286 25.0501 6.67716C25.462 6.35678 26.0608 5.85718 26.3087 5.64745C27.1668 3.7405 30.0844 0.498738 34.8898 2.78706C35.3017 2.64974 36.32 2.61542 36.7777 2.61542C36.4153 2.86334 35.6564 3.58795 35.5191 4.50328C35.153 7.02039 33.7647 8.48874 33.1164 8.90825C32.6587 11.8259 32.0294 14.4002 30.6564 15.3155L31.915 17.5466C33.8029 19.5489 37.7159 23.8737 38.2649 25.1552C36.4344 24.5603 35.2521 23.992 34.8898 23.7822L38.2649 28.416C36.2818 28.2635 31.8235 26.9744 29.8556 23.0385C30.6336 25.1438 31.4001 27.7677 31.6862 28.8165C30.6183 27.9393 28.3224 25.3955 27.6816 22.2376C27.8647 25.304 27.8342 27.4816 27.7961 28.1872C27.2812 27.7105 26.0913 26.2307 25.4505 24.1255V27.6723C24.6821 26.604 23.1363 24.0104 22.9967 22.0533C23.1255 24.2716 23.047 25.3115 22.9906 25.5556L20.0731 22.8097C19.2912 23.2292 17.1898 24.1827 15.0388 24.6403C13.5743 25.876 11.797 28.969 11.0915 30.3611V28.5877L9.14643 30.5327L9.83291 28.4733L8.57433 29.5602C8.28828 29.7318 7.62468 30.0751 7.25857 30.0751C7.39585 29.7547 7.65904 29.4076 7.77345 29.2741L6.11441 29.9034C6.3051 29.3504 6.90388 28.13 7.77345 27.6723C6.58351 28.13 6.09536 28.2444 6 28.2444Z" fill="white"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="paint0_linear_287_2206" x1="22.0557" y1="38.25" x2="22.0557" y2="-4.75" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#01D6E6"/>
|
||||||
|
<stop offset="1" stop-color="#0182E7"/>
|
||||||
|
</linearGradient>
|
||||||
|
<clipPath id="clip0_287_2206">
|
||||||
|
<rect width="44" height="44" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -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", {
|
||||||
@@ -259,6 +262,17 @@ function getDevLogs(lastIndex, cb) {
|
|||||||
.then(x=>x.json())
|
.then(x=>x.json())
|
||||||
.then(y=> cb && cb(y));
|
.then(y=> cb && cb(y));
|
||||||
}
|
}
|
||||||
|
function getDevHttpExchanges(cb) {
|
||||||
|
fetch("/plugin/getDevHttpExchanges", {
|
||||||
|
timeout: 1000
|
||||||
|
})
|
||||||
|
.then(x=>x.json())
|
||||||
|
.then(y=> cb && cb(y));
|
||||||
|
}
|
||||||
|
function setDevHttpProxy(url, port) {
|
||||||
|
return fetch("/dev/setDevProxy?url=" + encodeURIComponent(url) + "&port=" + port)
|
||||||
|
.then(x=>x.json());
|
||||||
|
}
|
||||||
function sendFakeDevLog(devId, msg) {
|
function sendFakeDevLog(devId, msg) {
|
||||||
return syncGET("/plugin/fakeDevLog?devId=" + devId + "&msg=" + msg, {});
|
return syncGET("/plugin/fakeDevLog?devId=" + devId + "&msg=" + msg, {});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,9 @@
|
|||||||
<!--<link href="./dependencies/vuetify.min.css" rel="stylesheet">-->
|
<!--<link href="./dependencies/vuetify.min.css" rel="stylesheet">-->
|
||||||
<link href="https://cdn.jsdelivr.net/npm/vuetify@2.7.1/dist/vuetify.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/vuetify@2.7.1/dist/vuetify.min.css" rel="stylesheet">
|
||||||
|
|
||||||
|
<title>DevPortal</title>
|
||||||
|
<link rel="icon" type="image/x-icon" href="/favicon.svg">
|
||||||
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -150,7 +153,7 @@
|
|||||||
.pastPluginUrl {
|
.pastPluginUrl {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
width: 500px;
|
width: 700px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
@@ -160,13 +163,122 @@
|
|||||||
box-shadow: 0px 1px 2px #131313;
|
box-shadow: 0px 1px 2px #131313;
|
||||||
font-weight: lighter;
|
font-weight: lighter;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.pastPluginUrl .deleteButton {
|
||||||
|
position: absolute;
|
||||||
|
right: 15px;
|
||||||
|
height: 100%;
|
||||||
|
width: 30px;
|
||||||
|
top: 0px;
|
||||||
|
padding-top: 2px;
|
||||||
|
display: grid;
|
||||||
|
justify-items: center;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 400;
|
||||||
|
transform: scaleX(1.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
[v-cloak] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
#cloakLoader {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
text-align: center;
|
||||||
|
left: 0px;
|
||||||
|
top: 0px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: black;
|
||||||
|
color: white;
|
||||||
|
padding-top: 50px;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
.httpContainer {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.httpLine {
|
||||||
|
}
|
||||||
|
.httpLine .request {
|
||||||
|
height: 50px;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.httpLine .request .status {
|
||||||
|
position: absolute;
|
||||||
|
left: 10px;
|
||||||
|
width: 40px;
|
||||||
|
top: 10px;
|
||||||
|
padding: 5px;
|
||||||
|
background-color: #333;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.httpLine .request .status.error {
|
||||||
|
background-color: #880000;
|
||||||
|
}
|
||||||
|
.httpLine .request .status.success {
|
||||||
|
background-color: #008800;
|
||||||
|
}
|
||||||
|
.httpLine .request .status.warn {
|
||||||
|
background-color: #803500;
|
||||||
|
}
|
||||||
|
.httpLine .request .method {
|
||||||
|
position: absolute;
|
||||||
|
left: 55px;
|
||||||
|
top: 10px;
|
||||||
|
padding: 5px;
|
||||||
|
background-color: #333;
|
||||||
|
border-radius: 5px;
|
||||||
|
width: 50px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.httpLine .request .url {
|
||||||
|
position: absolute;
|
||||||
|
left: 110px;
|
||||||
|
top: 10px;
|
||||||
|
padding: 5px;
|
||||||
|
background-color: #333;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.httpLine .response {
|
||||||
|
background-color: #111;
|
||||||
|
margin-left: 55px;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
.httpLine .response .body{
|
||||||
|
white-space: pre-wrap;
|
||||||
|
font-family: monospace;
|
||||||
|
background-color: black;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
.httpLine .response .headers {
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
.httpLine .response .headers .key {
|
||||||
|
display: inline-block;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #FFF;
|
||||||
|
}
|
||||||
|
.httpLine .response .headers .value {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #AAA;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<v-app>
|
<v-app>
|
||||||
<v-main>
|
<div v-cloak id="cloakLoader" v-if="!page">
|
||||||
|
<h2>Loading..</h2>
|
||||||
|
First load may take longer
|
||||||
|
</div>
|
||||||
|
<v-main v-cloak>
|
||||||
<div id="topMenu">
|
<div id="topMenu">
|
||||||
<div style="height: 100%; display: inline-block; padding-left: 10px; padding-right: 20px;">
|
<div style="height: 100%; display: inline-block; padding-left: 10px; padding-right: 20px;">
|
||||||
<img src="./dependencies/FutoMainLogo.svg"
|
<img src="./dependencies/FutoMainLogo.svg"
|
||||||
@@ -250,10 +362,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div v-if="pastPluginUrls" style="margin-top: 60px;">
|
<div v-if="pastPluginUrls" style="margin-top: 60px; margin-left: 25px;">
|
||||||
<h2 style="font-weight: lighter; text-align: center;">Past Plugins</h2>
|
<h2 style="font-weight: lighter; text-align: center;">Past Plugins</h2>
|
||||||
<div class="pastPluginUrl" v-for="pastPluginUrl in pastPluginUrls" @click="this.Plugin.newPluginUrl = pastPluginUrl; loadPlugin(pastPluginUrl)">
|
<div class="pastPluginUrl" v-for="pastPluginUrl in pastPluginUrls" @click="this.Plugin.newPluginUrl = pastPluginUrl; loadPlugin(pastPluginUrl)">
|
||||||
{{pastPluginUrl}}
|
{{pastPluginUrl}}
|
||||||
|
<div class="deleteButton" @click="(ev)=>{ev.stopPropagation(); deletePastPlugin(pastPluginUrl)}">
|
||||||
|
X
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -385,8 +500,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 +517,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 +536,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>
|
||||||
@@ -497,7 +620,62 @@
|
|||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-card-actions>
|
<v-card-actions>
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
<v-btn>Clear</v-btn>
|
<v-btn @click="Integration.logs = []">Clear</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
<v-card style="margin: 20px;" v-if="Plugin.currentPlugin && Integration.httpExchanges">
|
||||||
|
<v-card-title>
|
||||||
|
Http Logs
|
||||||
|
</v-card-title>
|
||||||
|
</v-card-header>
|
||||||
|
<v-card-text>
|
||||||
|
<div style="position: absolute; top: 0px; right: 15px;">
|
||||||
|
<v-checkbox v-model="Integration.showHttpRequests" label="Show Http Requests"></v-checkbox>
|
||||||
|
</div>
|
||||||
|
<div class="httpContainer" v-if="Integration.showHttpRequests">
|
||||||
|
<div class="httpLine" v-for="exchange of Integration.httpExchanges">
|
||||||
|
<div class="request" @click="toggleHttpExchange(exchange)">
|
||||||
|
<div :class="[{ success: exchange.response.status < 300, warn: exchange.response.status >= 300 && exchange.response.status < 400, error: exchange.response.status >= 400 }, 'status']">
|
||||||
|
{{exchange.response.status}}
|
||||||
|
</div>
|
||||||
|
<div class="method">
|
||||||
|
{{exchange.request.method}}
|
||||||
|
</div>
|
||||||
|
<div class="url">
|
||||||
|
{{exchange.request.url}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="response" v-if="exchange.response.show">
|
||||||
|
<h2>Request Headers</h2>
|
||||||
|
<div class="headers">
|
||||||
|
<div class="header" v-for="(headerValue, header) in exchange.request.headers">
|
||||||
|
<div class="key">
|
||||||
|
{{header}}
|
||||||
|
</div>
|
||||||
|
<div class="value">
|
||||||
|
{{headerValue}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2>Response</h2>
|
||||||
|
<div class="headers">
|
||||||
|
<div class="header" v-for="(headerValue, header) in exchange.response.headers">
|
||||||
|
<div class="key">
|
||||||
|
{{header}}
|
||||||
|
</div>
|
||||||
|
<div class="value">
|
||||||
|
{{headerValue}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="body">{{exchange.response.body}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn v-if="Integration.showHttpRequests" @click="Integration.httpExchanges = []">Clear</v-btn>
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
</v-card>
|
</v-card>
|
||||||
</div>
|
</div>
|
||||||
@@ -535,6 +713,7 @@
|
|||||||
<!--<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>
|
||||||
@@ -545,6 +724,7 @@
|
|||||||
new Vue({
|
new Vue({
|
||||||
el: '#app',
|
el: '#app',
|
||||||
data: {
|
data: {
|
||||||
|
searchTestMethods: "",
|
||||||
page: "Plugin",
|
page: "Plugin",
|
||||||
pastPluginUrls: [],
|
pastPluginUrls: [],
|
||||||
settings: {},
|
settings: {},
|
||||||
@@ -552,7 +732,9 @@
|
|||||||
lastLogIndex: -1,
|
lastLogIndex: -1,
|
||||||
lastLogDevID: "",
|
lastLogDevID: "",
|
||||||
logs: [],
|
logs: [],
|
||||||
lastInjectTime: ""
|
httpExchanges: [],
|
||||||
|
lastInjectTime: "",
|
||||||
|
showHttpRequests: false
|
||||||
},
|
},
|
||||||
Plugin: {
|
Plugin: {
|
||||||
loadUsingTag: false,
|
loadUsingTag: false,
|
||||||
@@ -570,6 +752,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: "",
|
||||||
@@ -633,6 +818,16 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
if(this.Integration.showHttpRequests) {
|
||||||
|
getDevHttpExchanges((exchanges)=>{
|
||||||
|
Vue.nextTick(()=>{
|
||||||
|
for(i = 0; i < exchanges.length; i++) {
|
||||||
|
exchanges[i].response.show = false;
|
||||||
|
this.Integration.httpExchanges.unshift(exchanges[i]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch(ex) {
|
catch(ex) {
|
||||||
console.error("Failed update", ex);
|
console.error("Failed update", ex);
|
||||||
@@ -674,6 +869,12 @@
|
|||||||
this.reloadPlugin();
|
this.reloadPlugin();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
deletePastPlugin(url) {
|
||||||
|
let currentPastPlugins = this.pastPluginUrls;
|
||||||
|
currentPastPlugins = currentPastPlugins.filter(x=>x.toLowerCase() != url.toLowerCase());
|
||||||
|
this.pastPluginUrls = currentPastPlugins;
|
||||||
|
localStorage.setItem("pastPlugins", JSON.stringify(currentPastPlugins));
|
||||||
|
},
|
||||||
loginTestPlugin() {
|
loginTestPlugin() {
|
||||||
pluginLoginTestPlugin();
|
pluginLoginTestPlugin();
|
||||||
setTimeout(()=>{
|
setTimeout(()=>{
|
||||||
@@ -860,8 +1061,58 @@
|
|||||||
"Error: " + ex;
|
"Error: " + ex;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
testSourceRemotely(req) {
|
||||||
|
const name = req.title;
|
||||||
|
const parameterVals = req.parameters.map(x=>{
|
||||||
|
if(x.value && x.value.startsWith && x.value.startsWith("json:"))
|
||||||
|
return JSON.parse(x.value.substring(5));
|
||||||
|
return x.value
|
||||||
|
});
|
||||||
|
|
||||||
|
if(name == "enable") {
|
||||||
|
if(parameterVals.length > 0)
|
||||||
|
parameterVals[0] = this.Plugin.currentPlugin;
|
||||||
|
else
|
||||||
|
parameterVals.push(this.Plugin.currentPlugin);
|
||||||
|
if(parameterVals.length > 1)
|
||||||
|
parameterVals[1] = __DEV_SETTINGS;
|
||||||
|
else
|
||||||
|
parameterVals.push(__DEV_SETTINGS);
|
||||||
|
}
|
||||||
|
|
||||||
|
const func = source[name];
|
||||||
|
if(!func)
|
||||||
|
alert("Test func not found");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const remoteResult = pluginRemoteTest(name, parameterVals);
|
||||||
|
console.log("Result for " + req.title, remoteResult);
|
||||||
|
this.Testing.lastResult = "//Results [" + name + "]\n" +
|
||||||
|
JSON.stringify(remoteResult, null, 3);
|
||||||
|
this.Testing.lastResultError = "";
|
||||||
|
}
|
||||||
|
catch(ex) {
|
||||||
|
if(ex.plugin_type == "CaptchaRequiredException") {
|
||||||
|
let shouldCaptcha = confirm("Do you want to request captcha?");
|
||||||
|
if(shouldCaptcha) {
|
||||||
|
pluginCaptchaTestPlugin(ex.url, ex.body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.error("Failed to run test for " + req.title, ex);
|
||||||
|
this.Testing.lastResult = ""
|
||||||
|
if(ex.message)
|
||||||
|
this.Testing.lastResultError = "//Results [" + name + "]\n\n" +
|
||||||
|
"Error: " + ex.message + "\n\n" + ex.stack;
|
||||||
|
else
|
||||||
|
this.Testing.lastResultError = "//Results [" + name + "]\n\n" +
|
||||||
|
"Error: " + ex;
|
||||||
|
}
|
||||||
|
},
|
||||||
showTestResults(results) {
|
showTestResults(results) {
|
||||||
|
|
||||||
|
},
|
||||||
|
toggleHttpExchange(exchange) {
|
||||||
|
exchange.response.show = !exchange.response.show;
|
||||||
},
|
},
|
||||||
copyClipboard(cpy) {
|
copyClipboard(cpy) {
|
||||||
if(navigator.clipboard)
|
if(navigator.clipboard)
|
||||||
|
|||||||
+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
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ let Type = {
|
|||||||
NORMAL: 0,
|
NORMAL: 0,
|
||||||
|
|
||||||
SKIPPABLE: 5,
|
SKIPPABLE: 5,
|
||||||
SKIP: 6
|
SKIP: 6,
|
||||||
|
SKIPONCE: 7
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -77,6 +78,11 @@ class ScriptLoginRequiredException extends ScriptException {
|
|||||||
super("ScriptLoginRequiredException", msg);
|
super("ScriptLoginRequiredException", msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
class LoginRequiredException extends ScriptException {
|
||||||
|
constructor(msg) {
|
||||||
|
super("ScriptLoginRequiredException", msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
class CaptchaRequiredException extends Error {
|
class CaptchaRequiredException extends Error {
|
||||||
constructor(url, body) {
|
constructor(url, body) {
|
||||||
super(JSON.stringify({ 'plugin_type': 'CaptchaRequiredException', url, body }));
|
super(JSON.stringify({ 'plugin_type': 'CaptchaRequiredException', url, body }));
|
||||||
@@ -248,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
|
||||||
@@ -320,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 {
|
||||||
@@ -345,6 +353,17 @@ class AudioUrlSource {
|
|||||||
this.duration = obj.duration ?? 0;
|
this.duration = obj.duration ?? 0;
|
||||||
this.url = obj.url;
|
this.url = obj.url;
|
||||||
this.language = obj.language ?? Language.UNKNOWN;
|
this.language = obj.language ?? Language.UNKNOWN;
|
||||||
|
if(obj.requestModifier)
|
||||||
|
this.requestModifier = obj.requestModifier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class AudioUrlWidevineSource extends AudioUrlSource {
|
||||||
|
constructor(obj) {
|
||||||
|
super(obj);
|
||||||
|
this.plugin_type = "AudioUrlWidevineSource";
|
||||||
|
|
||||||
|
this.bearerToken = obj.bearerToken;
|
||||||
|
this.licenseUri = obj.licenseUri;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class AudioUrlRangeSource extends AudioUrlSource {
|
class AudioUrlRangeSource extends AudioUrlSource {
|
||||||
@@ -370,6 +389,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 {
|
||||||
@@ -381,13 +402,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +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.math.roundToInt
|
||||||
|
import kotlin.math.roundToLong
|
||||||
|
|
||||||
|
|
||||||
//Long
|
//Long
|
||||||
@@ -119,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 {
|
||||||
@@ -150,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) {
|
||||||
@@ -232,7 +236,11 @@ fun Long.formatDuration(): String {
|
|||||||
val minutes = (this % 3600000) / 60000
|
val minutes = (this % 3600000) / 60000
|
||||||
val seconds = (this % 60000) / 1000
|
val seconds = (this % 60000) / 1000
|
||||||
|
|
||||||
return String.format("%02d:%02d:%02d", hours, minutes, seconds)
|
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 {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.futo.platformplayer
|
package com.futo.platformplayer
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import com.google.common.base.CharMatcher
|
import com.google.common.base.CharMatcher
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
@@ -9,7 +10,6 @@ import java.net.InetAddress
|
|||||||
import java.net.InetSocketAddress
|
import java.net.InetSocketAddress
|
||||||
import java.net.Socket
|
import java.net.Socket
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.nio.charset.Charset
|
|
||||||
|
|
||||||
|
|
||||||
private const val IPV4_PART_COUNT = 4;
|
private const val IPV4_PART_COUNT = 4;
|
||||||
@@ -216,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;
|
||||||
@@ -249,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) {
|
||||||
@@ -263,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)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
package com.futo.platformplayer
|
package com.futo.platformplayer
|
||||||
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||||
import com.futo.platformplayer.states.AnnouncementType
|
import com.futo.platformplayer.states.AnnouncementType
|
||||||
import com.futo.platformplayer.states.StateAnnouncement
|
import com.futo.platformplayer.states.StateAnnouncement
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.platformplayer.views.adapters.CommentViewHolder
|
|
||||||
import com.futo.polycentric.core.ProcessHandle
|
import com.futo.polycentric.core.ProcessHandle
|
||||||
|
import com.futo.polycentric.core.Store
|
||||||
|
import com.futo.polycentric.core.SystemState
|
||||||
import userpackage.Protocol
|
import userpackage.Protocol
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
@@ -47,6 +49,12 @@ fun Protocol.Claim.resolveChannelUrls(): List<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun ProcessHandle.fullyBackfillServersAnnounceExceptions() {
|
suspend fun ProcessHandle.fullyBackfillServersAnnounceExceptions() {
|
||||||
|
val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(system))
|
||||||
|
if (!systemState.servers.contains(PolycentricCache.SERVER)) {
|
||||||
|
Logger.w("Backfill", "Polycentric prod server not added, adding it.")
|
||||||
|
addServer(PolycentricCache.SERVER)
|
||||||
|
}
|
||||||
|
|
||||||
val exceptions = fullyBackfillServers()
|
val exceptions = fullyBackfillServers()
|
||||||
for (pair in exceptions) {
|
for (pair in exceptions) {
|
||||||
val server = pair.key
|
val server = pair.key
|
||||||
|
|||||||
@@ -277,7 +277,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@FormField(R.string.fetch_on_tab_opened, FieldForm.TOGGLE, R.string.fetch_on_tab_opened_description, 9)
|
@FormField(R.string.fetch_on_tab_opened, FieldForm.TOGGLE, R.string.fetch_on_tab_opened_description, 9)
|
||||||
var fetchOnTabOpen: Boolean = true;
|
var fetchOnTabOpen: Boolean = true;
|
||||||
|
|
||||||
@FormField(R.string.background_update, FieldForm.DROPDOWN, R.string.experimental_background_update_for_subscriptions_cache, 10)
|
@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;
|
||||||
|
|
||||||
@@ -311,7 +311,10 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@FormField(R.string.always_reload_from_cache, FieldForm.TOGGLE, R.string.always_reload_from_cache_description, 14)
|
@FormField(R.string.always_reload_from_cache, FieldForm.TOGGLE, R.string.always_reload_from_cache_description, 14)
|
||||||
var alwaysReloadFromCache: Boolean = false;
|
var alwaysReloadFromCache: Boolean = false;
|
||||||
|
|
||||||
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON, R.string.clear_channel_cache_description, 15)
|
@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() {
|
fun clearChannelCache() {
|
||||||
UIDialogs.toast(SettingsActivity.getActivity()!!, "Started clearing..");
|
UIDialogs.toast(SettingsActivity.getActivity()!!, "Started clearing..");
|
||||||
StateCache.instance.clear();
|
StateCache.instance.clear();
|
||||||
@@ -546,6 +549,8 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@DropdownFieldOptionsId(R.array.log_levels)
|
@DropdownFieldOptionsId(R.array.log_levels)
|
||||||
var logLevel: Int = 0;
|
var logLevel: Int = 0;
|
||||||
|
|
||||||
|
fun isVerbose() = logLevel >= 4;
|
||||||
|
|
||||||
@FormField(R.string.submit_logs, FieldForm.BUTTON, R.string.submit_logs_to_help_us_narrow_down_issues, 1)
|
@FormField(R.string.submit_logs, FieldForm.BUTTON, R.string.submit_logs_to_help_us_narrow_down_issues, 1)
|
||||||
fun submitLogs() {
|
fun submitLogs() {
|
||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
@@ -685,7 +690,9 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
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 {
|
||||||
@@ -807,7 +814,36 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
var polycentricEnabled: Boolean = true;
|
var polycentricEnabled: Boolean = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.info, FieldForm.GROUP, -1, 19)
|
@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 {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import android.widget.Toast
|
|||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.casting.StateCasting
|
import com.futo.platformplayer.casting.StateCasting
|
||||||
import com.futo.platformplayer.dialogs.AutoUpdateDialog
|
import com.futo.platformplayer.dialogs.AutoUpdateDialog
|
||||||
import com.futo.platformplayer.dialogs.AutomaticBackupDialog
|
import com.futo.platformplayer.dialogs.AutomaticBackupDialog
|
||||||
@@ -31,12 +32,19 @@ import com.futo.platformplayer.dialogs.ConnectedCastingDialog
|
|||||||
import com.futo.platformplayer.dialogs.ImportDialog
|
import com.futo.platformplayer.dialogs.ImportDialog
|
||||||
import com.futo.platformplayer.dialogs.ImportOptionsDialog
|
import com.futo.platformplayer.dialogs.ImportOptionsDialog
|
||||||
import com.futo.platformplayer.dialogs.MigrateDialog
|
import com.futo.platformplayer.dialogs.MigrateDialog
|
||||||
|
import com.futo.platformplayer.dialogs.PluginUpdateDialog
|
||||||
import com.futo.platformplayer.dialogs.ProgressDialog
|
import com.futo.platformplayer.dialogs.ProgressDialog
|
||||||
import com.futo.platformplayer.engine.exceptions.PluginException
|
import com.futo.platformplayer.engine.exceptions.PluginException
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.MainFragment
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.SourceDetailFragment
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.VideoDetailFragment
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.models.ImportCache
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StateBackup
|
import com.futo.platformplayer.states.StateBackup
|
||||||
|
import com.futo.platformplayer.states.StatePlugins
|
||||||
import com.futo.platformplayer.stores.v2.ManagedStore
|
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||||
|
import com.futo.platformplayer.views.ToastView
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -182,6 +190,14 @@ class UIDialogs {
|
|||||||
dialog.show();
|
dialog.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun showPluginUpdateDialog(context: Context, oldConfig: SourcePluginConfig, newConfig: SourcePluginConfig) {
|
||||||
|
val dialog = PluginUpdateDialog(context, oldConfig, newConfig);
|
||||||
|
registerDialogOpened(dialog);
|
||||||
|
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||||
|
dialog.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fun showDialog(context: Context, icon: Int, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action) {
|
fun showDialog(context: Context, icon: Int, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action) {
|
||||||
val builder = AlertDialog.Builder(context);
|
val builder = AlertDialog.Builder(context);
|
||||||
val view = LayoutInflater.from(context).inflate(R.layout.dialog_multi_button, null);
|
val view = LayoutInflater.from(context).inflate(R.layout.dialog_multi_button, null);
|
||||||
@@ -267,22 +283,48 @@ class UIDialogs {
|
|||||||
}, UIDialogs.ActionStyle.PRIMARY)
|
}, UIDialogs.ActionStyle.PRIMARY)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
fun showGeneralRetryErrorDialog(context: Context, msg: String, ex: Throwable? = null, retryAction: (() -> Unit)? = null, closeAction: (() -> Unit)? = null) {
|
fun showGeneralRetryErrorDialog(context: Context, msg: String, ex: Throwable? = null, retryAction: (() -> Unit)? = null, closeAction: (() -> Unit)? = null, mainFragment: MainFragment? = null) {
|
||||||
|
val pluginConfig = if(ex is PluginException) ex.config else null;
|
||||||
val pluginInfo = if(ex is PluginException)
|
val pluginInfo = if(ex is PluginException)
|
||||||
"\nPlugin [${ex.config.name}]" else "";
|
"\nPlugin [${ex.config.name}]" else "";
|
||||||
showDialog(context,
|
|
||||||
R.drawable.ic_error_pred,
|
var exMsg = if(ex != null ) "${ex.message}" else "";
|
||||||
"${msg}${pluginInfo}",
|
if(pluginConfig != null && pluginConfig is SourcePluginConfig && StatePlugins.instance.hasUpdateAvailable(pluginConfig))
|
||||||
(if(ex != null ) "${ex.message}" else ""),
|
exMsg += "\n\nAn update is available"
|
||||||
if(ex is PluginException) ex.code else null,
|
|
||||||
0,
|
if(mainFragment != null && pluginConfig != null && pluginConfig is SourcePluginConfig && StatePlugins.instance.hasUpdateAvailable(pluginConfig))
|
||||||
UIDialogs.Action(context.getString(R.string.retry), {
|
showDialog(context,
|
||||||
retryAction?.invoke();
|
R.drawable.ic_error_pred,
|
||||||
}, UIDialogs.ActionStyle.PRIMARY),
|
"${msg}${pluginInfo}",
|
||||||
UIDialogs.Action(context.getString(R.string.close), {
|
exMsg,
|
||||||
closeAction?.invoke()
|
if(ex is PluginException) ex.code else null,
|
||||||
}, UIDialogs.ActionStyle.NONE)
|
1,
|
||||||
);
|
UIDialogs.Action(context.getString(R.string.update), {
|
||||||
|
mainFragment.navigate<SourceDetailFragment>(SourceDetailFragment.UpdatePluginAction(pluginConfig));
|
||||||
|
if(mainFragment is VideoDetailFragment)
|
||||||
|
mainFragment.minimizeVideoDetail();
|
||||||
|
}, UIDialogs.ActionStyle.ACCENT),
|
||||||
|
UIDialogs.Action(context.getString(R.string.close), {
|
||||||
|
closeAction?.invoke()
|
||||||
|
}, UIDialogs.ActionStyle.NONE),
|
||||||
|
UIDialogs.Action(context.getString(R.string.retry), {
|
||||||
|
retryAction?.invoke();
|
||||||
|
}, UIDialogs.ActionStyle.PRIMARY)
|
||||||
|
);
|
||||||
|
else
|
||||||
|
showDialog(context,
|
||||||
|
R.drawable.ic_error_pred,
|
||||||
|
"${msg}${pluginInfo}",
|
||||||
|
exMsg,
|
||||||
|
if(ex is PluginException) ex.code else null,
|
||||||
|
0,
|
||||||
|
UIDialogs.Action(context.getString(R.string.close), {
|
||||||
|
closeAction?.invoke()
|
||||||
|
}, UIDialogs.ActionStyle.NONE),
|
||||||
|
UIDialogs.Action(context.getString(R.string.retry), {
|
||||||
|
retryAction?.invoke();
|
||||||
|
}, UIDialogs.ActionStyle.PRIMARY)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showSingleButtonDialog(context: Context, icon: Int, text: String, buttonText: String, action: (() -> Unit)) {
|
fun showSingleButtonDialog(context: Context, icon: Int, text: String, buttonText: String, action: (() -> Unit)) {
|
||||||
@@ -303,12 +345,16 @@ class UIDialogs {
|
|||||||
showDialog(context, R.drawable.ic_error, text, null, null, 0, cancelButtonAction, confirmButtonAction)
|
showDialog(context, R.drawable.ic_error, text, null, null, 0, cancelButtonAction, confirmButtonAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun 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) {
|
||||||
@@ -338,8 +384,8 @@ 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);
|
registerDialogOpened(dialog);
|
||||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||||
dialog.show();
|
dialog.show();
|
||||||
@@ -398,13 +444,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,9 +1,14 @@
|
|||||||
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.MainActivity
|
||||||
|
import com.futo.platformplayer.activities.SettingsActivity
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
@@ -33,11 +38,17 @@ import com.futo.platformplayer.states.StateMeta
|
|||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.platformplayer.states.StatePlayer
|
import com.futo.platformplayer.states.StatePlayer
|
||||||
import com.futo.platformplayer.states.StatePlaylists
|
import com.futo.platformplayer.states.StatePlaylists
|
||||||
|
import com.futo.platformplayer.states.StateSubscriptionGroups
|
||||||
|
import com.futo.platformplayer.states.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.LoaderView
|
||||||
|
import com.futo.platformplayer.views.adapters.viewholders.SubscriptionGroupBarViewHolder
|
||||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuFilters
|
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.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
|
||||||
@@ -63,7 +74,7 @@ class UISlideOverlays {
|
|||||||
return menu;
|
return menu;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup) {
|
fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup): SlideUpMenuOverlay {
|
||||||
val items = arrayListOf<View>();
|
val items = arrayListOf<View>();
|
||||||
|
|
||||||
val originalNotif = subscription.doNotifications;
|
val originalNotif = subscription.doNotifications;
|
||||||
@@ -72,20 +83,48 @@ class UISlideOverlays {
|
|||||||
val originalVideo = subscription.doFetchVideos;
|
val originalVideo = subscription.doFetchVideos;
|
||||||
val originalPosts = subscription.doFetchPosts;
|
val originalPosts = subscription.doFetchPosts;
|
||||||
|
|
||||||
|
val menu = SlideUpMenuOverlay(container.context, container, "Subscription Settings", null, true, listOf());
|
||||||
|
|
||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO){
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO){
|
||||||
val plugin = StatePlatform.instance.getChannelClient(subscription.channel.url);
|
val plugin = StatePlatform.instance.getChannelClient(subscription.channel.url);
|
||||||
val capabilities = plugin.getChannelCapabilities();
|
val capabilities = plugin.getChannelCapabilities();
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
|
|
||||||
var menu: SlideUpMenuOverlay? = null;
|
|
||||||
|
|
||||||
|
|
||||||
items.addAll(listOf(
|
items.addAll(listOf(
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_notifications, "Notifications", "", "notifications", {
|
SlideUpMenuItem(container.context, R.drawable.ic_notifications, "Notifications", "", "notifications", {
|
||||||
subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications;
|
subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications;
|
||||||
}, false),
|
}, 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",
|
SlideUpMenuGroup(container.context, "Fetch Settings",
|
||||||
"Depending on the platform you might not need to enable a type for it to be available.",
|
"Depending on the platform you might not need to enable a type for it to be available.",
|
||||||
-1, listOf()),
|
-1, listOf()),
|
||||||
@@ -114,7 +153,7 @@ class UISlideOverlays {
|
|||||||
}, false)*/
|
}, false)*/
|
||||||
).filterNotNull());
|
).filterNotNull());
|
||||||
|
|
||||||
menu = SlideUpMenuOverlay(container.context, container, "Subscription Settings", null, true, items);
|
menu.setItems(items);
|
||||||
|
|
||||||
if(subscription.doNotifications)
|
if(subscription.doNotifications)
|
||||||
menu.selectOption(null, "notifications", true, true);
|
menu.selectOption(null, "notifications", true, true);
|
||||||
@@ -131,8 +170,29 @@ class UISlideOverlays {
|
|||||||
subscription.save();
|
subscription.save();
|
||||||
menu.hide(true);
|
menu.hide(true);
|
||||||
|
|
||||||
if(subscription.doNotifications && !originalNotif && Settings.instance.subscriptions.subscriptionsBackgroundUpdateInterval == 0) {
|
if(subscription.doNotifications && !originalNotif) {
|
||||||
UIDialogs.toast(container.context, "Enable 'Background Update' in settings for notifications to work");
|
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 {
|
menu.onCancel.subscribe {
|
||||||
@@ -148,6 +208,8 @@ class UISlideOverlays {
|
|||||||
menu.show();
|
menu.show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return menu;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showAddToGroupOverlay(channel: IPlatformVideo, container: ViewGroup) {
|
fun showAddToGroupOverlay(channel: IPlatformVideo, container: ViewGroup) {
|
||||||
@@ -317,7 +379,7 @@ class UISlideOverlays {
|
|||||||
videoSources.filter { it is IVideoUrlSource && it.isDownloadable() }.asIterable(),
|
videoSources.filter { it is IVideoUrlSource && it.isDownloadable() }.asIterable(),
|
||||||
Settings.instance.downloads.getDefaultVideoQualityPixels(),
|
Settings.instance.downloads.getDefaultVideoQualityPixels(),
|
||||||
FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS
|
FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS
|
||||||
) as IVideoUrlSource;
|
) as IVideoUrlSource?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (audioSources != null) {
|
if (audioSources != null) {
|
||||||
@@ -448,10 +510,15 @@ class UISlideOverlays {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun showDownloadPlaylistOverlay(playlist: Playlist, container: ViewGroup) {
|
fun showDownloadPlaylistOverlay(playlist: Playlist, container: ViewGroup) {
|
||||||
showUnknownVideoDownload(container.context.getString(R.string.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;
|
||||||
@@ -621,9 +688,17 @@ class UISlideOverlays {
|
|||||||
val watchLater = StatePlaylists.instance.getWatchLater();
|
val watchLater = StatePlaylists.instance.getWatchLater();
|
||||||
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.actions), "actions",
|
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.actions), "actions",
|
||||||
(listOf(
|
(listOf(
|
||||||
SlideUpMenuItem(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), {
|
SlideUpMenuItem(container.context, R.drawable.ic_download, container.context.getString(R.string.download), container.context.getString(R.string.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", {
|
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);
|
StateMeta.instance.addHiddenCreator(video.author.url);
|
||||||
UIDialogs.toast(container.context, "[${video.author.name}] hidden, you may need to reload home");
|
UIDialogs.toast(container.context, "[${video.author.name}] hidden, you may need to reload home");
|
||||||
@@ -661,7 +736,7 @@ class UISlideOverlays {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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>();
|
||||||
|
|
||||||
@@ -692,6 +767,13 @@ class UISlideOverlays {
|
|||||||
);
|
);
|
||||||
|
|
||||||
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} " + container.context.getString(R.string.videos), "",
|
playlistItems.add(SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, playlist.name, "${playlist.videos.size} " + container.context.getString(R.string.videos), "",
|
||||||
{
|
{
|
||||||
@@ -706,14 +788,14 @@ class UISlideOverlays {
|
|||||||
return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.add_to), null, true, items).apply { show() };
|
return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.add_to), null, true, items).apply { show() };
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showFiltersOverlay(lifecycleScope: CoroutineScope, container: ViewGroup, enabledClientsIds: List<String>, filterValues: HashMap<String, List<String>>): SlideUpMenuFilters {
|
fun showFiltersOverlay(lifecycleScope: CoroutineScope, container: ViewGroup, enabledClientsIds: List<String>, filterValues: HashMap<String, List<String>>, isChannelSearch: Boolean = false): SlideUpMenuFilters {
|
||||||
val overlay = SlideUpMenuFilters(lifecycleScope, container, enabledClientsIds, filterValues);
|
val overlay = SlideUpMenuFilters(lifecycleScope, container, enabledClientsIds, filterValues, isChannelSearch);
|
||||||
overlay.show();
|
overlay.show();
|
||||||
return overlay;
|
return overlay;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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) };
|
||||||
|
|
||||||
@@ -721,7 +803,7 @@ class UISlideOverlays {
|
|||||||
hidden
|
hidden
|
||||||
.map { btn -> SlideUpMenuItem(container.context, btn.iconResource, btn.text.text.toString(), "", "", {
|
.map { btn -> SlideUpMenuItem(container.context, btn.iconResource, btn.text.text.toString(), "", "", {
|
||||||
btn.handler?.invoke(btn);
|
btn.handler?.invoke(btn);
|
||||||
}, true) as View }.toTypedArray(),
|
}, invokeParents) as View }.toTypedArray(),
|
||||||
arrayOf(SlideUpMenuItem(container.context, R.drawable.ic_pin, container.context.getString(R.string.change_pins), container.context.getString(R.string.decide_which_buttons_should_be_pinned), "", {
|
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!!) }) {
|
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
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import android.widget.ScrollView
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
@@ -37,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;
|
||||||
@@ -79,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);
|
||||||
@@ -203,21 +207,30 @@ class AddSourceActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
val pastelRed = ContextCompat.getColor(this, 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) {
|
||||||
|
StatePlugins.instance.clearUpdateAvailable(config)
|
||||||
|
if(isNew)
|
||||||
|
lifecycleScope.launch {
|
||||||
|
StatePlatform.instance.enableClient(listOf(config.id));
|
||||||
|
}
|
||||||
backToSources();
|
backToSources();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ 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;
|
lateinit var _buttonPlugins: BigButton;
|
||||||
|
|
||||||
@@ -56,6 +57,7 @@ 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);
|
_buttonPlugins = findViewById(R.id.option_plugins);
|
||||||
|
|
||||||
@@ -74,6 +76,9 @@ class AddSourceOptionsActivity : AppCompatActivity() {
|
|||||||
integrator.setCaptureActivity(QRCaptureActivity::class.java);
|
integrator.setCaptureActivity(QRCaptureActivity::class.java);
|
||||||
_qrCodeResultLauncher.launch(integrator.createScanIntent())
|
_qrCodeResultLauncher.launch(integrator.createScanIntent())
|
||||||
}
|
}
|
||||||
|
_buttonBrowse.onClick.subscribe {
|
||||||
|
startActivity(MainActivity.getTabIntent(this, "BROWSE_PLUGINS"));
|
||||||
|
}
|
||||||
|
|
||||||
_buttonURL.onClick.subscribe {
|
_buttonURL.onClick.subscribe {
|
||||||
UIDialogs.toast(this, getString(R.string.not_implemented_yet));
|
UIDialogs.toast(this, getString(R.string.not_implemented_yet));
|
||||||
|
|||||||
@@ -4,15 +4,19 @@ 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.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
|
||||||
@@ -26,6 +30,7 @@ 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;
|
||||||
|
|
||||||
@@ -43,6 +48,7 @@ class ExceptionActivity : AppCompatActivity() {
|
|||||||
_buttonSubmit = findViewById(R.id.button_submit);
|
_buttonSubmit = findViewById(R.id.button_submit);
|
||||||
_buttonRestart = findViewById(R.id.button_restart);
|
_buttonRestart = findViewById(R.id.button_restart);
|
||||||
_buttonClose = findViewById(R.id.button_close);
|
_buttonClose = findViewById(R.id.button_close);
|
||||||
|
_buttonCheckForUpdates = findViewById(R.id.button_check_for_updates);
|
||||||
|
|
||||||
val context = intent.getStringExtra(EXTRA_CONTEXT) ?: getString(R.string.unknown_context);
|
val context = intent.getStringExtra(EXTRA_CONTEXT) ?: getString(R.string.unknown_context);
|
||||||
val stack = intent.getStringExtra(EXTRA_STACK) ?: getString(R.string.something_went_wrong_missing_stack_trace);
|
val stack = intent.getStringExtra(EXTRA_STACK) ?: getString(R.string.something_went_wrong_missing_stack_trace);
|
||||||
@@ -81,6 +87,17 @@ 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() {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import android.widget.TextView
|
|||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.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
|
||||||
@@ -39,6 +40,7 @@ class LoginActivity : AppCompatActivity() {
|
|||||||
_textUrl = findViewById(R.id.text_url);
|
_textUrl = findViewById(R.id.text_url);
|
||||||
_buttonClose = findViewById(R.id.button_close);
|
_buttonClose = findViewById(R.id.button_close);
|
||||||
_buttonClose.setOnClickListener {
|
_buttonClose.setOnClickListener {
|
||||||
|
UIDialogs.toast("Login cancelled", false);
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ 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
|
||||||
@@ -17,14 +18,18 @@ 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.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.casting.StateCasting
|
import com.futo.platformplayer.casting.StateCasting
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
|
||||||
@@ -36,12 +41,14 @@ 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.SubscriptionGroup
|
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.futo.polycentric.core.ApiMethods
|
||||||
import com.google.gson.JsonParser
|
import com.google.gson.JsonParser
|
||||||
import com.google.zxing.integration.android.IntentIntegrator
|
import com.google.zxing.integration.android.IntentIntegrator
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
@@ -51,6 +58,7 @@ import java.io.PrintWriter
|
|||||||
import java.io.StringWriter
|
import java.io.StringWriter
|
||||||
import java.lang.reflect.InvocationTargetException
|
import java.lang.reflect.InvocationTargetException
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import java.util.concurrent.ConcurrentLinkedQueue
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity, IWithResultLauncher {
|
class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||||
|
|
||||||
@@ -62,6 +70,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;
|
||||||
@@ -92,6 +101,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
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;
|
||||||
@@ -134,7 +144,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
handleUrlAll(content)
|
runBlocking {
|
||||||
|
handleUrlAll(content)
|
||||||
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.i(TAG, "Failed to handle URL.", e)
|
Logger.i(TAG, "Failed to handle URL.", e)
|
||||||
UIDialogs.toast(this, "Failed to handle URL: ${e.message}")
|
UIDialogs.toast(this, "Failed to handle URL: ${e.message}")
|
||||||
@@ -143,6 +155,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
constructor() : super() {
|
constructor() : super() {
|
||||||
|
ApiMethods.UserAgent = "Grayjay Android (${BuildConfig.VERSION_CODE})";
|
||||||
|
|
||||||
Thread.setDefaultUncaughtExceptionHandler { _, throwable ->
|
Thread.setDefaultUncaughtExceptionHandler { _, throwable ->
|
||||||
val writer = StringWriter();
|
val writer = StringWriter();
|
||||||
|
|
||||||
@@ -181,6 +195,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
Logger.i(TAG, "MainActivity Starting");
|
||||||
StateApp.instance.setGlobalContext(this, lifecycleScope);
|
StateApp.instance.setGlobalContext(this, lifecycleScope);
|
||||||
StateApp.instance.mainAppStarting(this);
|
StateApp.instance.mainAppStarting(this);
|
||||||
|
|
||||||
@@ -203,7 +218,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
|
||||||
|
|
||||||
@@ -219,6 +234,7 @@ 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();
|
||||||
@@ -310,6 +326,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
_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;
|
||||||
@@ -321,11 +338,10 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
_fragDownloads.topBar = _fragTopBarGeneral;
|
_fragDownloads.topBar = _fragTopBarGeneral;
|
||||||
_fragImportSubscriptions.topBar = _fragTopBarImport;
|
_fragImportSubscriptions.topBar = _fragTopBarImport;
|
||||||
_fragImportPlaylists.topBar = _fragTopBarImport;
|
_fragImportPlaylists.topBar = _fragTopBarImport;
|
||||||
_fragSubGroup.topBar = _fragTopBarNavigation;
|
|
||||||
_fragSubGroupList.topBar = _fragTopBarAdd;
|
_fragSubGroupList.topBar = _fragTopBarAdd;
|
||||||
|
|
||||||
_fragBrowser.topBar = _fragTopBarNavigation;
|
_fragBrowser.topBar = _fragTopBarNavigation;
|
||||||
|
|
||||||
fragCurrent = _fragMainHome;
|
fragCurrent = _fragMainHome;
|
||||||
|
|
||||||
val defaultTab = Settings.instance.tabs.mapNotNull {
|
val defaultTab = Settings.instance.tabs.mapNotNull {
|
||||||
@@ -407,6 +423,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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -463,21 +489,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() {
|
||||||
@@ -532,13 +543,28 @@ 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) {
|
||||||
handleUrlAll(targetData)
|
runBlocking {
|
||||||
|
handleUrlAll(targetData)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch(ex: Throwable) {
|
catch(ex: Throwable) {
|
||||||
@@ -546,7 +572,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleUrlAll(url: String) {
|
suspend fun handleUrlAll(url: String) {
|
||||||
val uri = Uri.parse(url)
|
val uri = Uri.parse(url)
|
||||||
when (uri.scheme) {
|
when (uri.scheme) {
|
||||||
"grayjay" -> {
|
"grayjay" -> {
|
||||||
@@ -582,7 +608,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
UIDialogs.showSingleButtonDialog(
|
UIDialogs.showSingleButtonDialog(
|
||||||
this,
|
this,
|
||||||
R.drawable.ic_play,
|
R.drawable.ic_play,
|
||||||
getString(R.string.unknown_content_format) + " [${url}]",
|
getString(R.string.unknown_content_format) + " [${url}]\n[${intent.type}]",
|
||||||
"Ok",
|
"Ok",
|
||||||
{ });
|
{ });
|
||||||
}
|
}
|
||||||
@@ -630,23 +656,38 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleUrl(url: String): Boolean {
|
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)");
|
||||||
@@ -657,10 +698,22 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
if(!recon.trim().startsWith("["))
|
if(!recon.trim().startsWith("["))
|
||||||
return handleUnknownJson(recon);
|
return handleUnknownJson(recon);
|
||||||
|
|
||||||
val reconLines = Json.decodeFromString<List<String>>(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");
|
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") {
|
||||||
@@ -675,12 +728,25 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
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(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")) {
|
||||||
@@ -692,7 +758,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
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
|
||||||
@@ -709,7 +775,7 @@ 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) {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -799,11 +865,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();
|
||||||
}
|
}
|
||||||
@@ -849,7 +913,6 @@ 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 {
|
inline fun <reified T> isFragmentActive(): Boolean {
|
||||||
@@ -965,6 +1028,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;
|
||||||
@@ -1010,6 +1074,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>();
|
||||||
|
|||||||
+10
-3
@@ -12,14 +12,14 @@ import com.futo.platformplayer.R
|
|||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
||||||
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.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
|
||||||
|
|
||||||
@@ -71,7 +71,14 @@ class PolycentricCreateProfileActivity : AppCompatActivity() {
|
|||||||
try {
|
try {
|
||||||
processHandle = ProcessHandle.create();
|
processHandle = ProcessHandle.create();
|
||||||
Store.instance.addProcessSecret(processHandle.processSecret);
|
Store.instance.addProcessSecret(processHandle.processSecret);
|
||||||
processHandle.addServer("https://srv1-stg.polycentric.io");
|
|
||||||
|
try {
|
||||||
|
PolycentricStorage.instance.addProcessSecret(processHandle.processSecret)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
processHandle.addServer(PolycentricCache.SERVER);
|
||||||
processHandle.setUsername(username);
|
processHandle.setUsername(username);
|
||||||
StatePolycentric.instance.setProcessHandle(processHandle);
|
StatePolycentric.instance.setProcessHandle(processHandle);
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
|
|||||||
+57
-27
@@ -8,12 +8,16 @@ import android.widget.ImageButton
|
|||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.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.StateApp
|
||||||
import com.futo.platformplayer.states.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
|
import com.futo.platformplayer.views.overlays.LoaderOverlay
|
||||||
import com.futo.polycentric.core.KeyPair
|
import com.futo.polycentric.core.KeyPair
|
||||||
import com.futo.polycentric.core.Process
|
import com.futo.polycentric.core.Process
|
||||||
import com.futo.polycentric.core.ProcessSecret
|
import com.futo.polycentric.core.ProcessSecret
|
||||||
@@ -21,6 +25,9 @@ import com.futo.polycentric.core.SignedEvent
|
|||||||
import com.futo.polycentric.core.Store
|
import com.futo.polycentric.core.Store
|
||||||
import com.futo.polycentric.core.base64UrlToByteArray
|
import com.futo.polycentric.core.base64UrlToByteArray
|
||||||
import com.google.zxing.integration.android.IntentIntegrator
|
import com.google.zxing.integration.android.IntentIntegrator
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import userpackage.Protocol
|
import userpackage.Protocol
|
||||||
import userpackage.Protocol.ExportBundle
|
import userpackage.Protocol.ExportBundle
|
||||||
|
|
||||||
@@ -29,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)
|
||||||
@@ -52,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();
|
||||||
@@ -94,42 +103,63 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
|||||||
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, getString(R.string.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, getString(R.string.failed_to_import_profile) + " '${e.message}'");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+48
-20
@@ -1,6 +1,8 @@
|
|||||||
package com.futo.platformplayer.activities
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
@@ -12,6 +14,7 @@ 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
|
||||||
@@ -21,14 +24,16 @@ import com.futo.platformplayer.dp
|
|||||||
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
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.toBase64Url
|
||||||
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||||
import com.github.dhaval2404.imagepicker.ImagePicker
|
import com.github.dhaval2404.imagepicker.ImagePicker
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -46,6 +51,8 @@ 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?) {
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
@@ -63,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, getString(R.string.failed_to_backfill_client));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateUI();
|
|
||||||
|
|
||||||
_imagePolycentric.setOnClickListener {
|
_imagePolycentric.setOnClickListener {
|
||||||
ImagePicker.with(this)
|
ImagePicker.with(this)
|
||||||
.cropSquare()
|
.cropSquare()
|
||||||
@@ -120,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() {
|
||||||
@@ -128,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, getString(R.string.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, getString(R.string.process_handle_unset));
|
withContext(Dispatchers.Main) {
|
||||||
|
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.process_handle_unset));
|
||||||
|
}
|
||||||
return@launch;
|
return@launch;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,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);
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package com.futo.platformplayer.activities
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.NotificationManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
@@ -12,6 +14,8 @@ import androidx.activity.result.ActivityResult
|
|||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
@@ -33,6 +37,14 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
|
|||||||
|
|
||||||
lateinit var overlay: FrameLayout;
|
lateinit var overlay: FrameLayout;
|
||||||
|
|
||||||
|
val notifPermission = "android.permission.POST_NOTIFICATIONS";
|
||||||
|
val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
|
||||||
|
if (isGranted)
|
||||||
|
UIDialogs.toast(this, "Notification permission granted");
|
||||||
|
else
|
||||||
|
UIDialogs.toast(this, "Notification permission denied");
|
||||||
|
}
|
||||||
|
|
||||||
override fun attachBaseContext(newBase: Context?) {
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
Logger.i("SettingsActivity", "SettingsActivity.attachBaseContext")
|
Logger.i("SettingsActivity", "SettingsActivity.attachBaseContext")
|
||||||
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
||||||
@@ -58,6 +70,33 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
|
|||||||
Logger.i("SettingsActivity", "App language change detected, propogating to shared preferences");
|
Logger.i("SettingsActivity", "App language change detected, propogating to shared preferences");
|
||||||
StateApp.instance.setLocaleSetting(this, Settings.instance.language.getAppLanguageLocaleString());
|
StateApp.instance.setLocaleSetting(this, Settings.instance.language.getAppLanguageLocaleString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(field.descriptor?.id == "background_update") {
|
||||||
|
Logger.i("SettingsActivity", "Detected change in background work ${field.value}");
|
||||||
|
if(Settings.instance.subscriptions.subscriptionsBackgroundUpdateInterval > 0) {
|
||||||
|
val notifManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
|
||||||
|
if(!notifManager.areNotificationsEnabled()) {
|
||||||
|
UIDialogs.toast(this, "Notifications aren't enabled");
|
||||||
|
|
||||||
|
when {
|
||||||
|
ContextCompat.checkSelfPermission(this, notifPermission) == PackageManager.PERMISSION_GRANTED -> {
|
||||||
|
|
||||||
|
}
|
||||||
|
ActivityCompat.shouldShowRequestPermissionRationale(this, notifPermission) -> {
|
||||||
|
UIDialogs.showDialog(this, R.drawable.ic_notifications, "Notifications Required",
|
||||||
|
"Notifications need to be enabled for background updating to function", null, 0,
|
||||||
|
UIDialogs.Action("Cancel", {}),
|
||||||
|
UIDialogs.Action("Enable", {
|
||||||
|
requestPermissionLauncher.launch(notifPermission);
|
||||||
|
}, UIDialogs.ActionStyle.PRIMARY));
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
requestPermissionLauncher.launch(notifPermission);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
_buttonBack.setOnClickListener {
|
_buttonBack.setOnClickListener {
|
||||||
finish();
|
finish();
|
||||||
@@ -72,7 +111,10 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
|
|||||||
reloadSettings();
|
reloadSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isFirstLoad = true;
|
||||||
fun reloadSettings() {
|
fun reloadSettings() {
|
||||||
|
val firstLoad = isFirstLoad;
|
||||||
|
isFirstLoad = false;
|
||||||
_form.setSearchVisible(false);
|
_form.setSearchVisible(false);
|
||||||
_loaderView.start();
|
_loaderView.start();
|
||||||
_form.fromObject(lifecycleScope, Settings.instance) {
|
_form.fromObject(lifecycleScope, Settings.instance) {
|
||||||
@@ -90,6 +132,13 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
|
|||||||
UIDialogs.toast(this, getString(R.string.you_are_now_in_developer_mode));
|
UIDialogs.toast(this, getString(R.string.you_are_now_in_developer_mode));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if(firstLoad) {
|
||||||
|
val query = intent.getStringExtra("query");
|
||||||
|
if(!query.isNullOrEmpty()) {
|
||||||
|
_form.setSearchQuery(query);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,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")
|
||||||
|
|||||||
@@ -60,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);
|
||||||
|
|||||||
@@ -84,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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -13,10 +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 hasGetContentChapters: Boolean = false,
|
||||||
|
val hasPeekChannelContents: Boolean = false
|
||||||
) {
|
) {
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -14,7 +14,7 @@ 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)
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package com.futo.platformplayer.api.media.models
|
|||||||
import com.caoccao.javet.values.reference.V8ValueArray
|
import com.caoccao.javet.values.reference.V8ValueArray
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
|
import com.futo.platformplayer.engine.V8PluginConfig
|
||||||
|
import com.futo.platformplayer.getOrDefault
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
|
||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
@@ -31,7 +33,7 @@ class Thumbnails {
|
|||||||
fun fromV8(config: IV8PluginConfig, value: V8ValueObject): Thumbnails {
|
fun fromV8(config: IV8PluginConfig, value: V8ValueObject): Thumbnails {
|
||||||
return Thumbnails((value.getOrThrow<V8ValueArray>(config, "sources", "Thumbnails"))
|
return Thumbnails((value.getOrThrow<V8ValueArray>(config, "sources", "Thumbnails"))
|
||||||
.toArray()
|
.toArray()
|
||||||
.map { Thumbnail.fromV8(it as V8ValueObject) }
|
.map { Thumbnail.fromV8(config, it as V8ValueObject) }
|
||||||
.toTypedArray());
|
.toTypedArray());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -40,10 +42,10 @@ class Thumbnails {
|
|||||||
data class Thumbnail(val url : String?, val quality : Int = 0) {
|
data class Thumbnail(val url : String?, val quality : Int = 0) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromV8(value: V8ValueObject): Thumbnail {
|
fun fromV8(config: IV8PluginConfig, value: V8ValueObject): Thumbnail {
|
||||||
return Thumbnail(
|
return Thumbnail(
|
||||||
value.getString("url"),
|
value.getOrDefault<String>(config,"url", "Thumbnail", null),
|
||||||
value.getInteger("quality"));
|
value.getOrDefault(config, "quality", "Thumbnail", 0) ?: 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
+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(
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ enum class ChapterType(val value: Int) {
|
|||||||
NORMAL(0),
|
NORMAL(0),
|
||||||
|
|
||||||
SKIPPABLE(5),
|
SKIPPABLE(5),
|
||||||
SKIP(6);
|
SKIP(6),
|
||||||
|
SKIPONCE(7);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+5
-2
@@ -19,8 +19,9 @@ class PolycentricPlatformComment : IPlatformComment {
|
|||||||
|
|
||||||
val eventPointer: Pointer;
|
val eventPointer: Pointer;
|
||||||
val reference: Reference;
|
val reference: Reference;
|
||||||
|
val parentReference: Reference?;
|
||||||
|
|
||||||
constructor(contextUrl: String, author: PlatformAuthorLink, msg: String, rating: IRating, date: OffsetDateTime, eventPointer: Pointer, 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;
|
||||||
@@ -29,6 +30,7 @@ class PolycentricPlatformComment : IPlatformComment {
|
|||||||
this.replyCount = replyCount;
|
this.replyCount = replyCount;
|
||||||
this.eventPointer = eventPointer;
|
this.eventPointer = eventPointer;
|
||||||
this.reference = eventPointer.toReference();
|
this.reference = eventPointer.toReference();
|
||||||
|
this.parentReference = parentReference;
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getReplies(client: IPlatformClient): IPager<IPlatformComment> {
|
override fun getReplies(client: IPlatformClient): IPager<IPlatformComment> {
|
||||||
@@ -36,10 +38,11 @@ class PolycentricPlatformComment : IPlatformComment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun cloneWithUpdatedReplyCount(replyCount: Int?): PolycentricPlatformComment {
|
fun cloneWithUpdatedReplyCount(replyCount: Int?): PolycentricPlatformComment {
|
||||||
return PolycentricPlatformComment(contextUrl, author, message, rating, date, eventPointer, 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+1
@@ -3,4 +3,5 @@ package com.futo.platformplayer.api.media.models.modifier
|
|||||||
interface IModifierOptions {
|
interface IModifierOptions {
|
||||||
val applyAuthClient: String?;
|
val applyAuthClient: String?;
|
||||||
val applyCookieClient: String?;
|
val applyCookieClient: String?;
|
||||||
|
val applyOtherHeaders: Boolean;
|
||||||
}
|
}
|
||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
package com.futo.platformplayer.api.media.models.streams.sources
|
||||||
|
|
||||||
|
interface IAudioUrlWidevineSource : IAudioUrlSource {
|
||||||
|
val bearerToken: String
|
||||||
|
val licenseUri: String
|
||||||
|
}
|
||||||
@@ -27,6 +27,7 @@ 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.JSHttpClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSOptional
|
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.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.IJSContentDetails
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSChannel
|
import com.futo.platformplayer.api.media.platforms.js.models.JSChannel
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSChannelPager
|
import com.futo.platformplayer.api.media.platforms.js.models.JSChannelPager
|
||||||
@@ -45,6 +46,7 @@ import com.futo.platformplayer.constructs.Event2
|
|||||||
import com.futo.platformplayer.engine.V8Plugin
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
import com.futo.platformplayer.engine.exceptions.PluginEngineException
|
import com.futo.platformplayer.engine.exceptions.PluginEngineException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||||
|
import com.futo.platformplayer.engine.exceptions.ScriptException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptValidationException
|
import com.futo.platformplayer.engine.exceptions.ScriptValidationException
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
@@ -56,8 +58,10 @@ import com.futo.platformplayer.states.StatePlugins
|
|||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
import kotlin.Exception
|
||||||
import kotlin.reflect.full.findAnnotations
|
import kotlin.reflect.full.findAnnotations
|
||||||
import kotlin.reflect.jvm.kotlinFunction
|
import kotlin.reflect.jvm.kotlinFunction
|
||||||
|
import kotlin.streams.asSequence
|
||||||
|
|
||||||
open class JSClient : IPlatformClient {
|
open class JSClient : IPlatformClient {
|
||||||
val config: SourcePluginConfig;
|
val config: SourcePluginConfig;
|
||||||
@@ -73,6 +77,7 @@ open class JSClient : IPlatformClient {
|
|||||||
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;
|
||||||
|
|
||||||
@@ -91,7 +96,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;
|
||||||
|
|
||||||
@@ -150,6 +159,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;
|
||||||
@@ -173,6 +184,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 {
|
||||||
@@ -214,9 +227,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,
|
hasGetContentChapters = plugin.executeBoolean("!!source.getContentChapters") ?: false,
|
||||||
|
hasPeekChannelContents = plugin.executeBoolean("!!source.peekChannelContents") ?: false
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -260,7 +275,7 @@ 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, this,
|
return@isBusyWith JSContentPager(config, this,
|
||||||
plugin.executeTyped("source.getHome()"));
|
plugin.executeTyped("source.getHome()"));
|
||||||
@@ -268,7 +283,7 @@ open class JSClient : IPlatformClient {
|
|||||||
|
|
||||||
@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()
|
||||||
@@ -298,7 +313,7 @@ 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, this,
|
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)})"));
|
||||||
@@ -306,6 +321,9 @@ open class JSClient : IPlatformClient {
|
|||||||
|
|
||||||
@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!!;
|
||||||
@@ -319,7 +337,7 @@ 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");
|
||||||
@@ -331,7 +349,7 @@ open class JSClient : IPlatformClient {
|
|||||||
@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, this,
|
return@isBusyWith JSChannelPager(config, this,
|
||||||
plugin.executeTyped("source.searchChannels(${Json.encodeToString(query)})"));
|
plugin.executeTyped("source.searchChannels(${Json.encodeToString(query)})"));
|
||||||
@@ -351,7 +369,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)})"));
|
||||||
@@ -378,12 +396,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, this,
|
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")
|
||||||
@@ -444,7 +496,7 @@ 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(this,
|
return@isBusyWith IJSContentDetails.fromV8(this,
|
||||||
plugin.executeTyped("source.getContentDetails(${Json.encodeToString(url)})"));
|
plugin.executeTyped("source.getContentDetails(${Json.encodeToString(url)})"));
|
||||||
@@ -453,7 +505,7 @@ open class JSClient : IPlatformClient {
|
|||||||
@JSOptional //getContentChapters = function(url, initialData)
|
@JSOptional //getContentChapters = function(url, initialData)
|
||||||
@JSDocs(15, "source.getContentChapters(url)", "Gets chapters for content details")
|
@JSDocs(15, "source.getContentChapters(url)", "Gets chapters for content details")
|
||||||
@JSDocsParameter("url", "A content url (this platform)")
|
@JSDocsParameter("url", "A content url (this platform)")
|
||||||
override fun getContentChapters(url: String): List<IChapter> = isBusyWith {
|
override fun getContentChapters(url: String): List<IChapter> = isBusyWith("getContentChapters") {
|
||||||
if(!capabilities.hasGetContentChapters)
|
if(!capabilities.hasGetContentChapters)
|
||||||
return@isBusyWith listOf();
|
return@isBusyWith listOf();
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
@@ -464,7 +516,7 @@ open class JSClient : IPlatformClient {
|
|||||||
@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();
|
||||||
@@ -478,7 +530,7 @@ 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
|
||||||
@@ -496,7 +548,7 @@ open class JSClient : IPlatformClient {
|
|||||||
|
|
||||||
@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();
|
||||||
@@ -505,7 +557,7 @@ 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();
|
||||||
@@ -518,7 +570,7 @@ 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");
|
||||||
@@ -528,15 +580,22 @@ open class JSClient : IPlatformClient {
|
|||||||
@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(this, 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)})"));
|
||||||
}
|
}
|
||||||
@@ -633,19 +692,24 @@ open class JSClient : IPlatformClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun <T> isBusyWith(handle: ()->T): T {
|
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)
|
||||||
@@ -662,10 +726,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>();
|
||||||
@@ -678,5 +775,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
@@ -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
|
||||||
) { }
|
) { }
|
||||||
+46
-1
@@ -45,7 +45,9 @@ class SourcePluginConfig(
|
|||||||
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 primaryClaimFieldType: Int? = null,
|
||||||
|
var developerSubmitUrl: String? = null,
|
||||||
|
var allowAllHttpHeaderAccess: Boolean = false,
|
||||||
) : IV8PluginConfig {
|
) : IV8PluginConfig {
|
||||||
|
|
||||||
val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl);
|
val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl);
|
||||||
@@ -79,6 +81,44 @@ class SourcePluginConfig(
|
|||||||
return _allowUrlsLowerVal!!;
|
return _allowUrlsLowerVal!!;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fun isLowRiskUpdate(oldScript: String, newConfig: SourcePluginConfig, newScript: String): Boolean{
|
||||||
|
//New allow header access
|
||||||
|
if(!allowAllHttpHeaderAccess && newConfig.allowAllHttpHeaderAccess)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
//All urls should already be allowed
|
||||||
|
for(url in newConfig.allowUrls) {
|
||||||
|
if(!allowUrls.contains(url))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
//All packages should already be allowed
|
||||||
|
for(pack in newConfig.packages) {
|
||||||
|
if(!packages.contains(pack))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
//Developer Submit Url should be same or empty
|
||||||
|
if(!newConfig.developerSubmitUrl.isNullOrEmpty() && developerSubmitUrl != newConfig.developerSubmitUrl)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
//Should have a public key
|
||||||
|
if(scriptPublicKey.isNullOrEmpty() || scriptSignature.isNullOrEmpty())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
//Should be same public key
|
||||||
|
if(scriptPublicKey != newConfig.scriptPublicKey)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
//Old signature should be valid
|
||||||
|
if(!validate(oldScript))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
//New signature should be valid
|
||||||
|
if(!newConfig.validate(newScript))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
fun getWarnings(scriptToCheck: String? = null) : List<Pair<String,String>> {
|
fun getWarnings(scriptToCheck: String? = null) : List<Pair<String,String>> {
|
||||||
val list = mutableListOf<Pair<String,String>>();
|
val list = mutableListOf<Pair<String,String>>();
|
||||||
|
|
||||||
@@ -107,6 +147,11 @@ class SourcePluginConfig(
|
|||||||
list.add(Pair(
|
list.add(Pair(
|
||||||
"Unrestricted Web Access",
|
"Unrestricted Web Access",
|
||||||
"This plugin requires access to all URLs, this may include malicious URLs."));
|
"This plugin requires access to all URLs, this may include malicious URLs."));
|
||||||
|
if(allowAllHttpHeaderAccess)
|
||||||
|
list.add(Pair(
|
||||||
|
"Unrestricted Http Header access",
|
||||||
|
"Allows this plugin to access all headers (including cookies and authorization headers) for unauthenticated requests."
|
||||||
|
))
|
||||||
|
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|||||||
+34
-2
@@ -2,9 +2,13 @@ package com.futo.platformplayer.api.media.platforms.js
|
|||||||
|
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.constructs.Event0
|
import com.futo.platformplayer.constructs.Event0
|
||||||
|
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.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
|
||||||
@@ -55,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?) {
|
||||||
@@ -63,12 +76,26 @@ 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(R.string.check_for_updates_setting, FieldForm.TOGGLE, R.string.check_for_updates_setting_description, -1)
|
||||||
|
var checkForUpdates: Boolean = true;
|
||||||
|
@FormField(R.string.automatic_update_setting, FieldForm.TOGGLE, R.string.automatic_update_setting_description, 0)
|
||||||
|
var automaticUpdate: Boolean = false;
|
||||||
|
|
||||||
@FormField(R.string.visibility, "group", R.string.enable_where_this_plugins_content_are_visible, 2)
|
@FormField(R.string.visibility, "group", R.string.enable_where_this_plugins_content_are_visible, 2)
|
||||||
var tabEnabled = TabEnabled();
|
var tabEnabled = TabEnabled();
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -106,6 +133,11 @@ class SourcePluginDescriptor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@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
|
tabEnabled.enableHome = config.enableInHome
|
||||||
|
|||||||
@@ -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);
|
||||||
+27
-1
@@ -2,14 +2,22 @@ package com.futo.platformplayer.api.media.platforms.js.internal
|
|||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourceAuth
|
import com.futo.platformplayer.api.media.platforms.js.SourceAuth
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourceCaptchaData
|
import com.futo.platformplayer.api.media.platforms.js.SourceCaptchaData
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSRequest
|
import com.futo.platformplayer.api.media.platforms.js.models.JSRequest
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestModifier
|
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestModifier
|
||||||
|
import com.futo.platformplayer.developer.DeveloperEndpoints
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
||||||
import com.futo.platformplayer.matchesDomain
|
import com.futo.platformplayer.matchesDomain
|
||||||
|
import com.futo.platformplayer.states.StateDeveloper
|
||||||
|
import com.google.common.net.MediaType
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okio.GzipSource
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
import java.net.Proxy
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
class JSHttpClient : ManagedHttpClient {
|
class JSHttpClient : ManagedHttpClient {
|
||||||
@@ -28,7 +36,15 @@ class JSHttpClient : ManagedHttpClient {
|
|||||||
private var _currentCookieMap: HashMap<String, HashMap<String, String>>;
|
private var _currentCookieMap: HashMap<String, HashMap<String, String>>;
|
||||||
private var _otherCookieMap: HashMap<String, HashMap<String, String>>;
|
private var _otherCookieMap: HashMap<String, HashMap<String, String>>;
|
||||||
|
|
||||||
constructor(jsClient: JSClient?, auth: SourceAuth? = null, captcha: SourceCaptchaData? = null, config: SourcePluginConfig? = null) : super() {
|
constructor(jsClient: JSClient?, auth: SourceAuth? = null, captcha: SourceCaptchaData? = null, config: SourcePluginConfig? = null) : super(
|
||||||
|
//Temporary ugly solution for DevPortal proxy support
|
||||||
|
(if(jsClient?.config?.id == StateDeveloper.DEV_ID && StateDeveloper.instance.devProxy != null)
|
||||||
|
OkHttpClient.Builder().proxy(Proxy(Proxy.Type.HTTP,
|
||||||
|
InetSocketAddress(StateDeveloper.instance.devProxy!!.url, StateDeveloper.instance.devProxy!!.port)
|
||||||
|
))
|
||||||
|
else
|
||||||
|
OkHttpClient.Builder())
|
||||||
|
) {
|
||||||
_jsClient = jsClient;
|
_jsClient = jsClient;
|
||||||
_jsConfig = config;
|
_jsConfig = config;
|
||||||
_auth = auth;
|
_auth = auth;
|
||||||
@@ -201,6 +217,16 @@ class JSHttpClient : ManagedHttpClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(_jsClient is DevJSClient) {
|
||||||
|
//val peekBody = resp.peekBody(1000 * 1000).string();
|
||||||
|
StateDeveloper.instance.addDevHttpExchange(
|
||||||
|
StateDeveloper.DevHttpExchange(
|
||||||
|
StateDeveloper.DevHttpRequest(resp.request.method, resp.request.url.toString(), mapOf(*resp.request.headers.map { Pair(it.first, it.second) }.toTypedArray()), ""),
|
||||||
|
StateDeveloper.DevHttpRequest("RESP", resp.request.url.toString(), mapOf(*resp.headers.map { Pair(it.first, it.second) }.toTypedArray()), "", resp.code)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+23
-5
@@ -23,21 +23,31 @@ class JSRequest : IRequest {
|
|||||||
_v8Options = options;
|
_v8Options = options;
|
||||||
initialize(plugin, originalUrl, originalHeaders);
|
initialize(plugin, originalUrl, originalHeaders);
|
||||||
}
|
}
|
||||||
constructor(plugin: JSClient, obj: V8ValueObject, originalUrl: String?, originalHeaders: Map<String, String>?) {
|
constructor(plugin: JSClient, obj: V8ValueObject, originalUrl: String?, originalHeaders: Map<String, String>?, applyOtherHeadersByDefault: Boolean = false) {
|
||||||
val contextName = "ModifyRequestResponse";
|
val contextName = "ModifyRequestResponse";
|
||||||
val config = plugin.config;
|
val config = plugin.config;
|
||||||
_v8Url = obj.getOrDefault<String>(config, "url", contextName, null);
|
_v8Url = obj.getOrDefault<String>(config, "url", contextName, null);
|
||||||
_v8Headers = obj.getOrDefault<Map<String, String>>(config, "headers", contextName, null);
|
_v8Headers = obj.getOrDefault<Map<String, String>>(config, "headers", contextName, null);
|
||||||
_v8Options = obj.getOrDefault<V8ValueObject>(config, "options", "JSRequestModifier.options", null)?.let {
|
_v8Options = obj.getOrDefault<V8ValueObject>(config, "options", "JSRequestModifier.options", null)?.let {
|
||||||
Options(config, it);
|
Options(config, it, applyOtherHeadersByDefault);
|
||||||
}
|
} ?: Options(null, null, applyOtherHeadersByDefault);
|
||||||
initialize(plugin, originalUrl, originalHeaders);
|
initialize(plugin, originalUrl, originalHeaders);
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initialize(plugin: JSClient, originalUrl: String?, originalHeaders: Map<String, String>?) {
|
private fun initialize(plugin: JSClient, originalUrl: String?, originalHeaders: Map<String, String>?) {
|
||||||
val config = plugin.config;
|
val config = plugin.config;
|
||||||
url = _v8Url ?: originalUrl;
|
url = _v8Url ?: originalUrl;
|
||||||
headers = _v8Headers ?: originalHeaders ?: mapOf();
|
|
||||||
|
if(_v8Options?.applyOtherHeaders ?: false) {
|
||||||
|
val headersToSet = _v8Headers?.toMutableMap() ?: mutableMapOf();
|
||||||
|
if (originalHeaders != null)
|
||||||
|
for (head in originalHeaders)
|
||||||
|
if (!headersToSet.containsKey(head.key))
|
||||||
|
headersToSet[head.key] = head.value;
|
||||||
|
headers = headersToSet;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
headers = _v8Headers ?: originalHeaders ?: mapOf();
|
||||||
|
|
||||||
if(_v8Options != null) {
|
if(_v8Options != null) {
|
||||||
if(_v8Options.applyCookieClient != null && url != null) {
|
if(_v8Options.applyCookieClient != null && url != null) {
|
||||||
@@ -68,10 +78,18 @@ class JSRequest : IRequest {
|
|||||||
class Options: IModifierOptions {
|
class Options: IModifierOptions {
|
||||||
override val applyAuthClient: String?;
|
override val applyAuthClient: String?;
|
||||||
override val applyCookieClient: String?;
|
override val applyCookieClient: String?;
|
||||||
|
override val applyOtherHeaders: Boolean;
|
||||||
|
|
||||||
constructor(config: IV8PluginConfig, obj: V8ValueObject) {
|
|
||||||
|
constructor(config: IV8PluginConfig, obj: V8ValueObject, applyOtherHeadersByDefault: Boolean = false) {
|
||||||
applyAuthClient = obj.getOrDefault(config, "applyAuthClient", "JSRequestModifier.options.applyAuthClient", null);
|
applyAuthClient = obj.getOrDefault(config, "applyAuthClient", "JSRequestModifier.options.applyAuthClient", null);
|
||||||
applyCookieClient = obj.getOrDefault(config, "applyCookieClient", "JSRequestModifier.options.applyCookieClient", null);
|
applyCookieClient = obj.getOrDefault(config, "applyCookieClient", "JSRequestModifier.options.applyCookieClient", null);
|
||||||
|
applyOtherHeaders = obj.getOrDefault(config, "applyOtherHeaders", "JSRequestModifier.options.applyOtherHeaders", applyOtherHeadersByDefault) ?: applyOtherHeadersByDefault;
|
||||||
|
}
|
||||||
|
constructor(applyAuthClient: String? = null, applyCookieClient: String? = null, applyOtherHeaders: Boolean = false) {
|
||||||
|
this.applyAuthClient = applyAuthClient;
|
||||||
|
this.applyCookieClient = applyCookieClient;
|
||||||
|
this.applyOtherHeaders = applyOtherHeaders;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
@@ -40,6 +40,7 @@ class JSRequestModifier: IRequestModifier {
|
|||||||
} as V8ValueObject;
|
} as V8ValueObject;
|
||||||
|
|
||||||
val req = JSRequest(_plugin, result, url, headers);
|
val req = JSRequest(_plugin, result, url, headers);
|
||||||
|
result.close();
|
||||||
return req;
|
return req;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
package com.futo.platformplayer.api.media.platforms.js.models.sources
|
||||||
|
|
||||||
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlWidevineSource
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
|
||||||
|
class JSAudioUrlWidevineSource : JSAudioUrlSource, IAudioUrlWidevineSource {
|
||||||
|
override val bearerToken: String
|
||||||
|
override val licenseUri: String
|
||||||
|
|
||||||
|
@Suppress("ConvertSecondaryConstructorToPrimary")
|
||||||
|
constructor(plugin: JSClient, obj: V8ValueObject) : super(plugin, obj) {
|
||||||
|
val contextName = "JSAudioUrlWidevineSource"
|
||||||
|
val config = plugin.config
|
||||||
|
bearerToken = _obj.getOrThrow(config, "bearerToken", contextName)
|
||||||
|
licenseUri = _obj.getOrThrow(config, "licenseUri", contextName)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
val url = getAudioUrl()
|
||||||
|
return "(name=$name, container=$container, bitrate=$bitrate, codec=$codec, url=$url, language=$language, duration=$duration, bearerToken=$bearerToken, licenseUri=$licenseUri)"
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-1
@@ -33,7 +33,7 @@ abstract class JSSource {
|
|||||||
this.type = type;
|
this.type = type;
|
||||||
|
|
||||||
_requestModifier = obj.getOrDefault<V8ValueObject>(_config, "requestModifier", "JSSource.requestModifier", null)?.let {
|
_requestModifier = obj.getOrDefault<V8ValueObject>(_config, "requestModifier", "JSSource.requestModifier", null)?.let {
|
||||||
JSRequest(plugin, it, null, null);
|
JSRequest(plugin, it, null, null, true);
|
||||||
}
|
}
|
||||||
hasRequestModifier = _requestModifier != null || obj.has("getRequestModifier");
|
hasRequestModifier = _requestModifier != null || obj.has("getRequestModifier");
|
||||||
}
|
}
|
||||||
@@ -66,6 +66,7 @@ abstract class JSSource {
|
|||||||
const val TYPE_VIDEO_WITH_METADATA = "VideoUrlRangeSource";
|
const val TYPE_VIDEO_WITH_METADATA = "VideoUrlRangeSource";
|
||||||
const val TYPE_DASH = "DashSource";
|
const val TYPE_DASH = "DashSource";
|
||||||
const val TYPE_HLS = "HLSSource";
|
const val TYPE_HLS = "HLSSource";
|
||||||
|
const val TYPE_AUDIOURL_WIDEVINE = "AudioUrlWidevineSource"
|
||||||
|
|
||||||
fun fromV8VideoNullable(plugin: JSClient, obj: V8Value?) : IVideoSource? = obj.orNull { fromV8Video(plugin, it as V8ValueObject) };
|
fun fromV8VideoNullable(plugin: JSClient, obj: V8Value?) : IVideoSource? = obj.orNull { fromV8Video(plugin, it as V8ValueObject) };
|
||||||
fun fromV8Video(plugin: JSClient, obj: V8ValueObject) : IVideoSource {
|
fun fromV8Video(plugin: JSClient, obj: V8ValueObject) : IVideoSource {
|
||||||
@@ -88,6 +89,7 @@ abstract class JSSource {
|
|||||||
return when(type) {
|
return when(type) {
|
||||||
TYPE_HLS -> JSHLSManifestAudioSource.fromV8HLS(plugin, obj);
|
TYPE_HLS -> JSHLSManifestAudioSource.fromV8HLS(plugin, obj);
|
||||||
TYPE_AUDIOURL -> JSAudioUrlSource(plugin, obj);
|
TYPE_AUDIOURL -> JSAudioUrlSource(plugin, obj);
|
||||||
|
TYPE_AUDIOURL_WIDEVINE -> JSAudioUrlWidevineSource(plugin, obj);
|
||||||
TYPE_AUDIO_WITH_METADATA -> JSAudioUrlRangeSource(plugin, obj);
|
TYPE_AUDIO_WITH_METADATA -> JSAudioUrlRangeSource(plugin, obj);
|
||||||
else -> throw NotImplementedError("Unknown type ${type}");
|
else -> throw NotImplementedError("Unknown type ${type}");
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-3
@@ -6,11 +6,9 @@ import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
|
|||||||
import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor
|
import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
|
||||||
import com.futo.platformplayer.engine.V8Plugin
|
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
|
||||||
class JSVideoSourceDescriptor: VideoMuxedSourceDescriptor {
|
class JSVideoSourceDescriptor : VideoMuxedSourceDescriptor {
|
||||||
protected val _obj: V8ValueObject;
|
protected val _obj: V8ValueObject;
|
||||||
|
|
||||||
override val isUnMuxed: Boolean;
|
override val isUnMuxed: Boolean;
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ class AirPlayCastingDevice : CastingDevice {
|
|||||||
try {
|
try {
|
||||||
val connectedSocket = getConnectedSocket(adrs.toList(), port);
|
val connectedSocket = getConnectedSocket(adrs.toList(), port);
|
||||||
if (connectedSocket == null) {
|
if (connectedSocket == null) {
|
||||||
delay(3000);
|
delay(1000);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,24 +163,25 @@ class AirPlayCastingDevice : CastingDevice {
|
|||||||
}
|
}
|
||||||
|
|
||||||
connectionState = CastConnectionState.CONNECTED;
|
connectionState = CastConnectionState.CONNECTED;
|
||||||
delay(1000);
|
|
||||||
|
|
||||||
val progressIndex = progressInfo.lowercase().indexOf("position: ");
|
val progressIndex = progressInfo.lowercase().indexOf("position: ");
|
||||||
if (progressIndex == -1) {
|
if (progressIndex == -1) {
|
||||||
|
delay(1000);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
val progress = progressInfo.substring(progressIndex + "position: ".length).toDoubleOrNull() ?: continue;
|
val progress = progressInfo.substring(progressIndex + "position: ".length).toDoubleOrNull() ?: continue;
|
||||||
setTime(progress);
|
setTime(progress);
|
||||||
|
|
||||||
|
|
||||||
val durationIndex = progressInfo.lowercase().indexOf("duration: ");
|
val durationIndex = progressInfo.lowercase().indexOf("duration: ");
|
||||||
if (durationIndex == -1) {
|
if (durationIndex == -1) {
|
||||||
|
delay(1000);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
val duration = progressInfo.substring(durationIndex + "duration: ".length).toDoubleOrNull() ?: continue;
|
val duration = progressInfo.substring(durationIndex + "duration: ".length).toDoubleOrNull() ?: continue;
|
||||||
setDuration(duration);
|
setDuration(duration);
|
||||||
|
delay(1000);
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.w(TAG, "Failed to get server info from AirPlay device.", e)
|
Logger.w(TAG, "Failed to get server info from AirPlay device.", e)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import org.json.JSONObject
|
|||||||
import java.io.DataInputStream
|
import java.io.DataInputStream
|
||||||
import java.io.DataOutputStream
|
import java.io.DataOutputStream
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
import java.net.Socket
|
||||||
import java.security.cert.X509Certificate
|
import java.security.cert.X509Certificate
|
||||||
import javax.net.ssl.SSLContext
|
import javax.net.ssl.SSLContext
|
||||||
import javax.net.ssl.SSLSocket
|
import javax.net.ssl.SSLSocket
|
||||||
@@ -42,7 +44,9 @@ class ChromecastCastingDevice : CastingDevice {
|
|||||||
|
|
||||||
private var _socket: SSLSocket? = null;
|
private var _socket: SSLSocket? = null;
|
||||||
private var _outputStream: DataOutputStream? = null;
|
private var _outputStream: DataOutputStream? = null;
|
||||||
|
private var _outputStreamLock = Object();
|
||||||
private var _inputStream: DataInputStream? = null;
|
private var _inputStream: DataInputStream? = null;
|
||||||
|
private var _inputStreamLock = Object();
|
||||||
private var _scopeIO: CoroutineScope? = null;
|
private var _scopeIO: CoroutineScope? = null;
|
||||||
private var _requestId = 1;
|
private var _requestId = 1;
|
||||||
private var _started: Boolean = false;
|
private var _started: Boolean = false;
|
||||||
@@ -50,6 +54,8 @@ class ChromecastCastingDevice : CastingDevice {
|
|||||||
private var _transportId: String? = null;
|
private var _transportId: String? = null;
|
||||||
private var _launching = false;
|
private var _launching = false;
|
||||||
private var _mediaSessionId: Int? = null;
|
private var _mediaSessionId: Int? = null;
|
||||||
|
private var _thread: Thread? = null;
|
||||||
|
private var _pingThread: Thread? = null;
|
||||||
|
|
||||||
constructor(name: String, addresses: Array<InetAddress>, port: Int) : super() {
|
constructor(name: String, addresses: Array<InetAddress>, port: Int) : super() {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
@@ -270,7 +276,6 @@ class ChromecastCastingDevice : CastingDevice {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun start() {
|
override fun start() {
|
||||||
val adrs = addresses ?: return;
|
|
||||||
if (_started) {
|
if (_started) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -283,152 +288,183 @@ class ChromecastCastingDevice : CastingDevice {
|
|||||||
|
|
||||||
_launching = true;
|
_launching = true;
|
||||||
|
|
||||||
_scopeIO?.cancel();
|
ensureThreadsStarted();
|
||||||
Logger.i(TAG, "Cancelled previous scopeIO because a new one is starting.")
|
Logger.i(TAG, "Started.");
|
||||||
_scopeIO = CoroutineScope(Dispatchers.IO);
|
}
|
||||||
|
|
||||||
Thread {
|
fun ensureThreadsStarted() {
|
||||||
connectionState = CastConnectionState.CONNECTING;
|
val adrs = addresses ?: return;
|
||||||
|
|
||||||
while (_scopeIO?.isActive == true) {
|
val thread = _thread
|
||||||
try {
|
val pingThread = _pingThread
|
||||||
val connectedSocket = getConnectedSocket(adrs.toList(), port);
|
if (thread == null || !thread.isAlive || pingThread == null || !pingThread.isAlive) {
|
||||||
if (connectedSocket == null) {
|
Log.i(TAG, "Restarting threads because one of the threads has died")
|
||||||
Thread.sleep(3000);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
usedRemoteAddress = connectedSocket.inetAddress;
|
_scopeIO?.cancel();
|
||||||
localAddress = connectedSocket.localAddress;
|
Logger.i(TAG, "Cancelled previous scopeIO because a new one is starting.")
|
||||||
connectedSocket.close();
|
_scopeIO = CoroutineScope(Dispatchers.IO);
|
||||||
break;
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.w(TAG, "Failed to get setup initial connection to ChromeCast device.", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val sslContext = SSLContext.getInstance("TLS");
|
_thread = Thread {
|
||||||
sslContext.init(null, trustAllCerts, null);
|
|
||||||
|
|
||||||
val factory = sslContext.socketFactory;
|
|
||||||
|
|
||||||
//Connection loop
|
|
||||||
while (_scopeIO?.isActive == true) {
|
|
||||||
Logger.i(TAG, "Connecting to Chromecast.");
|
|
||||||
connectionState = CastConnectionState.CONNECTING;
|
connectionState = CastConnectionState.CONNECTING;
|
||||||
|
|
||||||
try {
|
var connectedSocket: Socket? = null
|
||||||
_socket?.close()
|
|
||||||
_socket = factory.createSocket(usedRemoteAddress, port) as SSLSocket;
|
|
||||||
_socket?.startHandshake();
|
|
||||||
Logger.i(TAG, "Successfully connected to Chromecast at $usedRemoteAddress:$port");
|
|
||||||
|
|
||||||
try {
|
|
||||||
_outputStream = DataOutputStream(_socket?.outputStream);
|
|
||||||
_inputStream = DataInputStream(_socket?.inputStream);
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.i(TAG, "Failed to authenticate to Chromecast.", e);
|
|
||||||
}
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
_socket?.close();
|
|
||||||
Logger.i(TAG, "Failed to connect to Chromecast.", e);
|
|
||||||
|
|
||||||
connectionState = CastConnectionState.CONNECTING;
|
|
||||||
Thread.sleep(3000);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
localAddress = _socket?.localAddress;
|
|
||||||
|
|
||||||
try {
|
|
||||||
val connectObject = JSONObject();
|
|
||||||
connectObject.put("type", "CONNECT");
|
|
||||||
connectObject.put("connType", 0);
|
|
||||||
sendChannelMessage("sender-0", "receiver-0", "urn:x-cast:com.google.cast.tp.connection", connectObject.toString());
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.i(TAG, "Failed to send connect message to Chromecast.", e);
|
|
||||||
_socket?.close();
|
|
||||||
|
|
||||||
connectionState = CastConnectionState.CONNECTING;
|
|
||||||
Thread.sleep(3000);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
getStatus();
|
|
||||||
|
|
||||||
val buffer = ByteArray(4096);
|
|
||||||
|
|
||||||
Logger.i(TAG, "Started receiving.");
|
|
||||||
while (_scopeIO?.isActive == true) {
|
while (_scopeIO?.isActive == true) {
|
||||||
try {
|
try {
|
||||||
val inputStream = _inputStream ?: break;
|
val resultSocket = getConnectedSocket(adrs.toList(), port);
|
||||||
Log.d(TAG, "Receiving next packet...");
|
if (resultSocket == null) {
|
||||||
val b1 = inputStream.readUnsignedByte();
|
Thread.sleep(1000);
|
||||||
val b2 = inputStream.readUnsignedByte();
|
|
||||||
val b3 = inputStream.readUnsignedByte();
|
|
||||||
val b4 = inputStream.readUnsignedByte();
|
|
||||||
val size = ((b1.toLong() shl 24) or (b2.toLong() shl 16) or (b3.toLong() shl 8) or b4.toLong()).toInt();
|
|
||||||
if (size > buffer.size) {
|
|
||||||
Logger.w(TAG, "Skipping packet that is too large $size bytes.")
|
|
||||||
inputStream.skip(size.toLong());
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "Received header indicating $size bytes. Waiting for message.");
|
connectedSocket = resultSocket
|
||||||
inputStream.read(buffer, 0, size);
|
usedRemoteAddress = connectedSocket.inetAddress;
|
||||||
|
localAddress = connectedSocket.localAddress;
|
||||||
//TODO: In the future perhaps this size-1 will cause issues, why is there a 0 on the end?
|
|
||||||
val messageBytes = buffer.sliceArray(IntRange(0, size - 1));
|
|
||||||
Log.d(TAG, "Received $size bytes: ${messageBytes.toHexString()}.");
|
|
||||||
val message = ChromeCast.CastMessage.parseFrom(messageBytes);
|
|
||||||
if (message.namespace != "urn:x-cast:com.google.cast.tp.heartbeat") {
|
|
||||||
Logger.i(TAG, "Received message: $message");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
handleMessage(message);
|
|
||||||
} catch (e:Throwable) {
|
|
||||||
Logger.w(TAG, "Failed to handle message.", e);
|
|
||||||
}
|
|
||||||
} catch (e: java.net.SocketException) {
|
|
||||||
Logger.e(TAG, "Socket exception while receiving.", e);
|
|
||||||
break;
|
break;
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Exception while receiving.", e);
|
Logger.w(TAG, "Failed to get setup initial connection to ChromeCast device.", e)
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_socket?.close();
|
|
||||||
Logger.i(TAG, "Socket disconnected.");
|
|
||||||
|
|
||||||
connectionState = CastConnectionState.CONNECTING;
|
val sslContext = SSLContext.getInstance("TLS");
|
||||||
Thread.sleep(3000);
|
sslContext.init(null, trustAllCerts, null);
|
||||||
}
|
|
||||||
|
|
||||||
Logger.i(TAG, "Stopped connection loop.");
|
val factory = sslContext.socketFactory;
|
||||||
connectionState = CastConnectionState.DISCONNECTED;
|
|
||||||
}.start();
|
|
||||||
|
|
||||||
//Start ping loop
|
val address = InetSocketAddress(usedRemoteAddress, port)
|
||||||
Thread {
|
|
||||||
Logger.i(TAG, "Started ping loop.")
|
|
||||||
|
|
||||||
val pingObject = JSONObject();
|
//Connection loop
|
||||||
pingObject.put("type", "PING");
|
while (_scopeIO?.isActive == true) {
|
||||||
|
Logger.i(TAG, "Connecting to Chromecast.");
|
||||||
|
connectionState = CastConnectionState.CONNECTING;
|
||||||
|
|
||||||
while (_scopeIO?.isActive == true) {
|
try {
|
||||||
try {
|
_socket?.close()
|
||||||
sendChannelMessage("sender-0", "receiver-0", "urn:x-cast:com.google.cast.tp.heartbeat", pingObject.toString());
|
if (connectedSocket != null) {
|
||||||
Thread.sleep(5000);
|
Logger.i(TAG, "Using connected socket.")
|
||||||
} catch (e: Throwable) {
|
_socket = factory.createSocket(connectedSocket, connectedSocket.inetAddress.hostAddress, connectedSocket.port, true) as SSLSocket
|
||||||
|
connectedSocket = null
|
||||||
|
} else {
|
||||||
|
Logger.i(TAG, "Using new socket.")
|
||||||
|
val s = Socket().apply { this.connect(address, 2000) }
|
||||||
|
_socket = factory.createSocket(s, s.inetAddress.hostAddress, s.port, true) as SSLSocket
|
||||||
|
}
|
||||||
|
|
||||||
|
_socket?.startHandshake();
|
||||||
|
Logger.i(TAG, "Successfully connected to Chromecast at $usedRemoteAddress:$port");
|
||||||
|
|
||||||
|
try {
|
||||||
|
_outputStream = DataOutputStream(_socket?.outputStream);
|
||||||
|
_inputStream = DataInputStream(_socket?.inputStream);
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.i(TAG, "Failed to authenticate to Chromecast.", e);
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
_socket?.close();
|
||||||
|
Logger.i(TAG, "Failed to connect to Chromecast.", e);
|
||||||
|
|
||||||
|
connectionState = CastConnectionState.CONNECTING;
|
||||||
|
Thread.sleep(1000);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
localAddress = _socket?.localAddress;
|
||||||
|
|
||||||
|
try {
|
||||||
|
val connectObject = JSONObject();
|
||||||
|
connectObject.put("type", "CONNECT");
|
||||||
|
connectObject.put("connType", 0);
|
||||||
|
sendChannelMessage("sender-0", "receiver-0", "urn:x-cast:com.google.cast.tp.connection", connectObject.toString());
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.i(TAG, "Failed to send connect message to Chromecast.", e);
|
||||||
|
_socket?.close();
|
||||||
|
|
||||||
|
connectionState = CastConnectionState.CONNECTING;
|
||||||
|
Thread.sleep(1000);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
getStatus();
|
||||||
|
|
||||||
|
val buffer = ByteArray(409600);
|
||||||
|
|
||||||
|
Logger.i(TAG, "Started receiving.");
|
||||||
|
while (_scopeIO?.isActive == true) {
|
||||||
|
try {
|
||||||
|
val inputStream = _inputStream ?: break;
|
||||||
|
|
||||||
|
synchronized(_inputStreamLock)
|
||||||
|
{
|
||||||
|
Log.d(TAG, "Receiving next packet...");
|
||||||
|
val b1 = inputStream.readUnsignedByte();
|
||||||
|
val b2 = inputStream.readUnsignedByte();
|
||||||
|
val b3 = inputStream.readUnsignedByte();
|
||||||
|
val b4 = inputStream.readUnsignedByte();
|
||||||
|
val size =
|
||||||
|
((b1.toLong() shl 24) or (b2.toLong() shl 16) or (b3.toLong() shl 8) or b4.toLong()).toInt();
|
||||||
|
if (size > buffer.size) {
|
||||||
|
Logger.w(TAG, "Skipping packet that is too large $size bytes.")
|
||||||
|
inputStream.skip(size.toLong());
|
||||||
|
return@synchronized
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "Received header indicating $size bytes. Waiting for message.");
|
||||||
|
inputStream.read(buffer, 0, size);
|
||||||
|
|
||||||
|
//TODO: In the future perhaps this size-1 will cause issues, why is there a 0 on the end?
|
||||||
|
val messageBytes = buffer.sliceArray(IntRange(0, size - 1));
|
||||||
|
Log.d(TAG, "Received $size bytes: ${messageBytes.toHexString()}.");
|
||||||
|
val message = ChromeCast.CastMessage.parseFrom(messageBytes);
|
||||||
|
if (message.namespace != "urn:x-cast:com.google.cast.tp.heartbeat") {
|
||||||
|
Logger.i(TAG, "Received message: $message");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
handleMessage(message);
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.w(TAG, "Failed to handle message.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: java.net.SocketException) {
|
||||||
|
Logger.e(TAG, "Socket exception while receiving.", e);
|
||||||
|
break;
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Exception while receiving.", e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_socket?.close();
|
||||||
|
Logger.i(TAG, "Socket disconnected.");
|
||||||
|
|
||||||
|
connectionState = CastConnectionState.CONNECTING;
|
||||||
|
Thread.sleep(1000);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Logger.i(TAG, "Stopped ping loop.");
|
Logger.i(TAG, "Stopped connection loop.");
|
||||||
}.start();
|
connectionState = CastConnectionState.DISCONNECTED;
|
||||||
|
}.apply { start() };
|
||||||
|
|
||||||
Logger.i(TAG, "Started.");
|
//Start ping loop
|
||||||
|
_pingThread = Thread {
|
||||||
|
Logger.i(TAG, "Started ping loop.")
|
||||||
|
|
||||||
|
val pingObject = JSONObject();
|
||||||
|
pingObject.put("type", "PING");
|
||||||
|
|
||||||
|
while (_scopeIO?.isActive == true) {
|
||||||
|
try {
|
||||||
|
sendChannelMessage("sender-0", "receiver-0", "urn:x-cast:com.google.cast.tp.heartbeat", pingObject.toString());
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.w(TAG, "Failed to send ping.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Thread.sleep(5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.i(TAG, "Stopped ping loop.");
|
||||||
|
}.apply { start() };
|
||||||
|
} else {
|
||||||
|
Log.i(TAG, "Threads still alive, not restarted")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sendChannelMessage(sourceId: String, destinationId: String, namespace: String, json: String) {
|
private fun sendChannelMessage(sourceId: String, destinationId: String, namespace: String, json: String) {
|
||||||
@@ -559,13 +595,16 @@ class ChromecastCastingDevice : CastingDevice {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
val serializedSizeBE = ByteArray(4);
|
synchronized(_outputStreamLock)
|
||||||
serializedSizeBE[0] = (data.size shr 24 and 0xff).toByte();
|
{
|
||||||
serializedSizeBE[1] = (data.size shr 16 and 0xff).toByte();
|
val serializedSizeBE = ByteArray(4);
|
||||||
serializedSizeBE[2] = (data.size shr 8 and 0xff).toByte();
|
serializedSizeBE[0] = (data.size shr 24 and 0xff).toByte();
|
||||||
serializedSizeBE[3] = (data.size and 0xff).toByte();
|
serializedSizeBE[1] = (data.size shr 16 and 0xff).toByte();
|
||||||
outputStream.write(serializedSizeBE);
|
serializedSizeBE[2] = (data.size shr 8 and 0xff).toByte();
|
||||||
outputStream.write(data);
|
serializedSizeBE[3] = (data.size and 0xff).toByte();
|
||||||
|
outputStream.write(serializedSizeBE);
|
||||||
|
outputStream.write(data);
|
||||||
|
}
|
||||||
|
|
||||||
//Log.d(TAG, "Sent ${data.size} bytes.");
|
//Log.d(TAG, "Sent ${data.size} bytes.");
|
||||||
}
|
}
|
||||||
@@ -593,6 +632,8 @@ class ChromecastCastingDevice : CastingDevice {
|
|||||||
Logger.i(TAG, "Cancelled scopeIO without open socket.")
|
Logger.i(TAG, "Cancelled scopeIO without open socket.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_pingThread = null;
|
||||||
|
_thread = null;
|
||||||
_scopeIO = null;
|
_scopeIO = null;
|
||||||
_socket = null;
|
_socket = null;
|
||||||
_outputStream = null;
|
_outputStream = null;
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
package com.futo.platformplayer.casting
|
package com.futo.platformplayer.casting
|
||||||
|
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
|
import android.util.Base64
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
|
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 com.futo.platformplayer.casting.models.FCastPlayMessage
|
||||||
import com.futo.platformplayer.casting.models.FCastPlaybackErrorMessage
|
import com.futo.platformplayer.casting.models.FCastPlaybackErrorMessage
|
||||||
import com.futo.platformplayer.casting.models.FCastPlaybackUpdateMessage
|
import com.futo.platformplayer.casting.models.FCastPlaybackUpdateMessage
|
||||||
@@ -11,6 +15,7 @@ import com.futo.platformplayer.casting.models.FCastSetSpeedMessage
|
|||||||
import com.futo.platformplayer.casting.models.FCastSetVolumeMessage
|
import com.futo.platformplayer.casting.models.FCastSetVolumeMessage
|
||||||
import com.futo.platformplayer.casting.models.FCastVersionMessage
|
import com.futo.platformplayer.casting.models.FCastVersionMessage
|
||||||
import com.futo.platformplayer.casting.models.FCastVolumeUpdateMessage
|
import com.futo.platformplayer.casting.models.FCastVolumeUpdateMessage
|
||||||
|
import com.futo.platformplayer.ensureNotMainThread
|
||||||
import com.futo.platformplayer.getConnectedSocket
|
import com.futo.platformplayer.getConnectedSocket
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.CastingDeviceInfo
|
import com.futo.platformplayer.models.CastingDeviceInfo
|
||||||
@@ -23,25 +28,45 @@ import kotlinx.coroutines.isActive
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.io.DataInputStream
|
|
||||||
import java.io.DataOutputStream
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.math.BigInteger
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
|
import java.net.InetSocketAddress
|
||||||
import java.net.Socket
|
import java.net.Socket
|
||||||
|
import java.security.KeyFactory
|
||||||
|
import java.security.KeyPair
|
||||||
|
import java.security.KeyPairGenerator
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import java.security.PrivateKey
|
||||||
|
import java.security.spec.X509EncodedKeySpec
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.KeyAgreement
|
||||||
|
import javax.crypto.spec.DHParameterSpec
|
||||||
|
import javax.crypto.spec.IvParameterSpec
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
enum class Opcode(val value: Byte) {
|
enum class Opcode(val value: Byte) {
|
||||||
NONE(0),
|
None(0),
|
||||||
PLAY(1),
|
Play(1),
|
||||||
PAUSE(2),
|
Pause(2),
|
||||||
RESUME(3),
|
Resume(3),
|
||||||
STOP(4),
|
Stop(4),
|
||||||
SEEK(5),
|
Seek(5),
|
||||||
PLAYBACK_UPDATE(6),
|
PlaybackUpdate(6),
|
||||||
VOLUME_UPDATE(7),
|
VolumeUpdate(7),
|
||||||
SET_VOLUME(8),
|
SetVolume(8),
|
||||||
PLAYBACK_ERROR(9),
|
PlaybackError(9),
|
||||||
SET_SPEED(10),
|
SetSpeed(10),
|
||||||
VERSION(11)
|
Version(11),
|
||||||
|
Ping(12),
|
||||||
|
Pong(13);
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val _map = entries.associateBy { it.value }
|
||||||
|
fun find(value: Byte): Opcode = _map[value] ?: Opcode.None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class FCastCastingDevice : CastingDevice {
|
class FCastCastingDevice : CastingDevice {
|
||||||
@@ -58,11 +83,15 @@ class FCastCastingDevice : CastingDevice {
|
|||||||
var port: Int = 0;
|
var port: Int = 0;
|
||||||
|
|
||||||
private var _socket: Socket? = null;
|
private var _socket: Socket? = null;
|
||||||
private var _outputStream: DataOutputStream? = null;
|
private var _outputStream: OutputStream? = null;
|
||||||
private var _inputStream: DataInputStream? = null;
|
private var _inputStream: InputStream? = null;
|
||||||
private var _scopeIO: CoroutineScope? = null;
|
private var _scopeIO: CoroutineScope? = null;
|
||||||
private var _started: Boolean = false;
|
private var _started: Boolean = false;
|
||||||
private var _version: Long = 1;
|
private var _version: Long = 1;
|
||||||
|
private var _thread: Thread? = null
|
||||||
|
private var _pingThread: Thread? = null
|
||||||
|
private var _lastPongTime = -1L
|
||||||
|
private var _outputStreamLock = Object()
|
||||||
|
|
||||||
constructor(name: String, addresses: Array<InetAddress>, port: Int) : super() {
|
constructor(name: String, addresses: Array<InetAddress>, port: Int) : super() {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
@@ -94,7 +123,7 @@ class FCastCastingDevice : CastingDevice {
|
|||||||
|
|
||||||
setTime(resumePosition);
|
setTime(resumePosition);
|
||||||
setDuration(duration);
|
setDuration(duration);
|
||||||
sendMessage(Opcode.PLAY, FCastPlayMessage(
|
send(Opcode.Play, FCastPlayMessage(
|
||||||
container = contentType,
|
container = contentType,
|
||||||
url = contentId,
|
url = contentId,
|
||||||
time = resumePosition,
|
time = resumePosition,
|
||||||
@@ -118,7 +147,7 @@ class FCastCastingDevice : CastingDevice {
|
|||||||
|
|
||||||
setTime(resumePosition);
|
setTime(resumePosition);
|
||||||
setDuration(duration);
|
setDuration(duration);
|
||||||
sendMessage(Opcode.PLAY, FCastPlayMessage(
|
send(Opcode.Play, FCastPlayMessage(
|
||||||
container = contentType,
|
container = contentType,
|
||||||
content = content,
|
content = content,
|
||||||
time = resumePosition,
|
time = resumePosition,
|
||||||
@@ -134,7 +163,7 @@ class FCastCastingDevice : CastingDevice {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setVolume(volume);
|
setVolume(volume);
|
||||||
sendMessage(Opcode.SET_VOLUME, FCastSetVolumeMessage(volume))
|
send(Opcode.SetVolume, FCastSetVolumeMessage(volume))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun changeSpeed(speed: Double) {
|
override fun changeSpeed(speed: Double) {
|
||||||
@@ -143,7 +172,7 @@ class FCastCastingDevice : CastingDevice {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setSpeed(speed);
|
setSpeed(speed);
|
||||||
sendMessage(Opcode.SET_SPEED, FCastSetSpeedMessage(speed))
|
send(Opcode.SetSpeed, FCastSetSpeedMessage(speed))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun seekVideo(timeSeconds: Double) {
|
override fun seekVideo(timeSeconds: Double) {
|
||||||
@@ -151,7 +180,7 @@ class FCastCastingDevice : CastingDevice {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
sendMessage(Opcode.SEEK, FCastSeekMessage(
|
send(Opcode.Seek, FCastSeekMessage(
|
||||||
time = timeSeconds
|
time = timeSeconds
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -161,7 +190,7 @@ class FCastCastingDevice : CastingDevice {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
sendMessage(Opcode.RESUME);
|
send(Opcode.Resume);
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun pauseVideo() {
|
override fun pauseVideo() {
|
||||||
@@ -169,7 +198,7 @@ class FCastCastingDevice : CastingDevice {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
sendMessage(Opcode.PAUSE);
|
send(Opcode.Pause);
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun stopVideo() {
|
override fun stopVideo() {
|
||||||
@@ -177,12 +206,18 @@ class FCastCastingDevice : CastingDevice {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
sendMessage(Opcode.STOP);
|
send(Opcode.Stop);
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun invokeInIOScopeIfRequired(action: () -> Unit): Boolean {
|
private fun invokeInIOScopeIfRequired(action: () -> Unit): Boolean {
|
||||||
if(Looper.getMainLooper().thread == Thread.currentThread()) {
|
if(Looper.getMainLooper().thread == Thread.currentThread()) {
|
||||||
_scopeIO?.launch { action(); }
|
_scopeIO?.launch {
|
||||||
|
try {
|
||||||
|
action();
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to invoke in IO scope.", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,7 +236,6 @@ class FCastCastingDevice : CastingDevice {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun start() {
|
override fun start() {
|
||||||
val adrs = addresses ?: return;
|
|
||||||
if (_started) {
|
if (_started) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -209,123 +243,206 @@ class FCastCastingDevice : CastingDevice {
|
|||||||
_started = true;
|
_started = true;
|
||||||
Logger.i(TAG, "Starting...");
|
Logger.i(TAG, "Starting...");
|
||||||
|
|
||||||
_scopeIO?.cancel();
|
ensureThreadStarted();
|
||||||
Logger.i(TAG, "Cancelled previous scopeIO because a new one is starting.")
|
|
||||||
_scopeIO = CoroutineScope(Dispatchers.IO);
|
|
||||||
|
|
||||||
Thread {
|
|
||||||
connectionState = CastConnectionState.CONNECTING;
|
|
||||||
|
|
||||||
while (_scopeIO?.isActive == true) {
|
|
||||||
try {
|
|
||||||
val connectedSocket = getConnectedSocket(adrs.toList(), port);
|
|
||||||
if (connectedSocket == null) {
|
|
||||||
Thread.sleep(3000);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
usedRemoteAddress = connectedSocket.inetAddress;
|
|
||||||
localAddress = connectedSocket.localAddress;
|
|
||||||
connectedSocket.close();
|
|
||||||
break;
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.w(ChromecastCastingDevice.TAG, "Failed to get setup initial connection to FastCast device.", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//Connection loop
|
|
||||||
while (_scopeIO?.isActive == true) {
|
|
||||||
Logger.i(TAG, "Connecting to FastCast.");
|
|
||||||
connectionState = CastConnectionState.CONNECTING;
|
|
||||||
|
|
||||||
try {
|
|
||||||
_socket = Socket(usedRemoteAddress, port);
|
|
||||||
Logger.i(TAG, "Successfully connected to FastCast at $usedRemoteAddress:$port");
|
|
||||||
|
|
||||||
try {
|
|
||||||
_outputStream = DataOutputStream(_socket?.outputStream);
|
|
||||||
_inputStream = DataInputStream(_socket?.inputStream);
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.i(TAG, "Failed to authenticate to FastCast.", e);
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
_socket?.close();
|
|
||||||
Logger.i(TAG, "Failed to connect to FastCast.", e);
|
|
||||||
|
|
||||||
connectionState = CastConnectionState.CONNECTING;
|
|
||||||
Thread.sleep(3000);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
localAddress = _socket?.localAddress;
|
|
||||||
connectionState = CastConnectionState.CONNECTED;
|
|
||||||
|
|
||||||
val buffer = ByteArray(4096);
|
|
||||||
|
|
||||||
Logger.i(TAG, "Started receiving.");
|
|
||||||
var exceptionOccurred = false;
|
|
||||||
while (_scopeIO?.isActive == true && !exceptionOccurred) {
|
|
||||||
try {
|
|
||||||
val inputStream = _inputStream ?: break;
|
|
||||||
Log.d(TAG, "Receiving next packet...");
|
|
||||||
val b1 = inputStream.readUnsignedByte();
|
|
||||||
val b2 = inputStream.readUnsignedByte();
|
|
||||||
val b3 = inputStream.readUnsignedByte();
|
|
||||||
val b4 = inputStream.readUnsignedByte();
|
|
||||||
val size = ((b4.toLong() shl 24) or (b3.toLong() shl 16) or (b2.toLong() shl 8) or b1.toLong()).toInt();
|
|
||||||
if (size > buffer.size) {
|
|
||||||
Logger.w(TAG, "Skipping packet that is too large $size bytes.")
|
|
||||||
inputStream.skip(size.toLong());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.d(TAG, "Received header indicating $size bytes. Waiting for message.");
|
|
||||||
inputStream.read(buffer, 0, size);
|
|
||||||
|
|
||||||
val messageBytes = buffer.sliceArray(IntRange(0, size));
|
|
||||||
Log.d(TAG, "Received $size bytes: ${messageBytes.toHexString()}.");
|
|
||||||
|
|
||||||
val opcode = messageBytes[0];
|
|
||||||
var json: String? = null;
|
|
||||||
if (size > 1) {
|
|
||||||
json = messageBytes.sliceArray(IntRange(1, size - 1)).decodeToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
handleMessage(Opcode.entries.first { it.value == opcode }, json);
|
|
||||||
} catch (e:Throwable) {
|
|
||||||
Logger.w(TAG, "Failed to handle message.", e);
|
|
||||||
}
|
|
||||||
} catch (e: java.net.SocketException) {
|
|
||||||
Logger.e(TAG, "Socket exception while receiving.", e);
|
|
||||||
exceptionOccurred = true;
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.e(TAG, "Exception while receiving.", e);
|
|
||||||
exceptionOccurred = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
_socket?.close();
|
|
||||||
Logger.i(TAG, "Socket disconnected.");
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.e(TAG, "Failed to close socket.", e)
|
|
||||||
}
|
|
||||||
|
|
||||||
connectionState = CastConnectionState.CONNECTING;
|
|
||||||
Thread.sleep(3000);
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.i(TAG, "Stopped connection loop.");
|
|
||||||
connectionState = CastConnectionState.DISCONNECTED;
|
|
||||||
}.start();
|
|
||||||
|
|
||||||
Logger.i(TAG, "Started.");
|
Logger.i(TAG, "Started.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun ensureThreadStarted() {
|
||||||
|
val adrs = addresses ?: return;
|
||||||
|
|
||||||
|
val thread = _thread
|
||||||
|
val pingThread = _pingThread
|
||||||
|
if (_started && (thread == null || !thread.isAlive || pingThread == null || !pingThread.isAlive)) {
|
||||||
|
Log.i(TAG, "(Re)starting thread because the thread has died")
|
||||||
|
|
||||||
|
_scopeIO?.let {
|
||||||
|
it.cancel()
|
||||||
|
Logger.i(TAG, "Cancelled previous scopeIO because a new one is starting.")
|
||||||
|
}
|
||||||
|
|
||||||
|
_scopeIO = CoroutineScope(Dispatchers.IO);
|
||||||
|
|
||||||
|
_thread = Thread {
|
||||||
|
connectionState = CastConnectionState.CONNECTING;
|
||||||
|
Log.i(TAG, "Connection thread started.")
|
||||||
|
|
||||||
|
var connectedSocket: Socket? = null
|
||||||
|
while (_scopeIO?.isActive == true) {
|
||||||
|
try {
|
||||||
|
Log.i(TAG, "getConnectedSocket (adrs = [ ${adrs.joinToString(", ")} ], port = ${port}).")
|
||||||
|
|
||||||
|
val resultSocket = getConnectedSocket(adrs.toList(), port);
|
||||||
|
|
||||||
|
if (resultSocket == null) {
|
||||||
|
Log.i(TAG, "Connection failed, waiting 1 seconds.")
|
||||||
|
Thread.sleep(1000);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.i(TAG, "Connection succeeded.")
|
||||||
|
|
||||||
|
connectedSocket = resultSocket
|
||||||
|
usedRemoteAddress = connectedSocket.inetAddress
|
||||||
|
localAddress = connectedSocket.localAddress
|
||||||
|
break;
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.w(TAG, "Failed to get setup initial connection to FastCast device.", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val address = InetSocketAddress(usedRemoteAddress, port)
|
||||||
|
|
||||||
|
//Connection loop
|
||||||
|
while (_scopeIO?.isActive == true) {
|
||||||
|
Logger.i(TAG, "Connecting to FastCast.");
|
||||||
|
connectionState = CastConnectionState.CONNECTING;
|
||||||
|
|
||||||
|
try {
|
||||||
|
_socket?.close()
|
||||||
|
_inputStream?.close()
|
||||||
|
_outputStream?.close()
|
||||||
|
if (connectedSocket != null) {
|
||||||
|
Logger.i(TAG, "Using connected socket.");
|
||||||
|
_socket = connectedSocket
|
||||||
|
connectedSocket = null
|
||||||
|
} else {
|
||||||
|
Logger.i(TAG, "Using new socket.");
|
||||||
|
_socket = Socket().apply { this.connect(address, 2000) };
|
||||||
|
}
|
||||||
|
Logger.i(TAG, "Successfully connected to FastCast at $usedRemoteAddress:$port");
|
||||||
|
|
||||||
|
_outputStream = _socket?.outputStream;
|
||||||
|
_inputStream = _socket?.inputStream;
|
||||||
|
} catch (e: IOException) {
|
||||||
|
_socket?.close()
|
||||||
|
_inputStream?.close()
|
||||||
|
_outputStream?.close()
|
||||||
|
Logger.i(TAG, "Failed to connect to FastCast.", e);
|
||||||
|
|
||||||
|
connectionState = CastConnectionState.CONNECTING;
|
||||||
|
Thread.sleep(1000);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
localAddress = _socket?.localAddress;
|
||||||
|
connectionState = CastConnectionState.CONNECTED;
|
||||||
|
_lastPongTime = -1L
|
||||||
|
|
||||||
|
val buffer = ByteArray(4096);
|
||||||
|
|
||||||
|
Logger.i(TAG, "Started receiving.");
|
||||||
|
while (_scopeIO?.isActive == true) {
|
||||||
|
try {
|
||||||
|
val inputStream = _inputStream ?: break;
|
||||||
|
Log.d(TAG, "Receiving next packet...");
|
||||||
|
|
||||||
|
var headerBytesRead = 0
|
||||||
|
while (headerBytesRead < 4) {
|
||||||
|
val read = inputStream.read(buffer, headerBytesRead, 4 - headerBytesRead)
|
||||||
|
if (read == -1)
|
||||||
|
throw Exception("Stream closed")
|
||||||
|
headerBytesRead += read
|
||||||
|
}
|
||||||
|
|
||||||
|
val size = ((buffer[3].toLong() shl 24) or (buffer[2].toLong() shl 16) or (buffer[1].toLong() shl 8) or buffer[0].toLong()).toInt();
|
||||||
|
if (size > buffer.size) {
|
||||||
|
Logger.w(TAG, "Packets larger than $size bytes are not supported.")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "Received header indicating $size bytes. Waiting for message.");
|
||||||
|
var bytesRead = 0
|
||||||
|
while (bytesRead < size) {
|
||||||
|
val read = inputStream.read(buffer, bytesRead, size - bytesRead)
|
||||||
|
if (read == -1)
|
||||||
|
throw Exception("Stream closed")
|
||||||
|
bytesRead += read
|
||||||
|
}
|
||||||
|
|
||||||
|
val messageBytes = buffer.sliceArray(IntRange(0, size));
|
||||||
|
Log.d(TAG, "Received $size bytes: ${messageBytes.toHexString()}.");
|
||||||
|
|
||||||
|
val opcode = messageBytes[0];
|
||||||
|
var json: String? = null;
|
||||||
|
if (size > 1) {
|
||||||
|
json = messageBytes.sliceArray(IntRange(1, size - 1)).decodeToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
handleMessage(Opcode.find(opcode), json);
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.w(TAG, "Failed to handle message.", e)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} catch (e: java.net.SocketException) {
|
||||||
|
Logger.e(TAG, "Socket exception while receiving.", e);
|
||||||
|
break
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Exception while receiving.", e);
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
_socket?.close()
|
||||||
|
_inputStream?.close()
|
||||||
|
_outputStream?.close()
|
||||||
|
Logger.i(TAG, "Socket disconnected.");
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to close socket.", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
connectionState = CastConnectionState.CONNECTING;
|
||||||
|
Thread.sleep(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.i(TAG, "Stopped connection loop.");
|
||||||
|
connectionState = CastConnectionState.DISCONNECTED;
|
||||||
|
}.apply { start() }
|
||||||
|
|
||||||
|
_pingThread = Thread {
|
||||||
|
Logger.i(TAG, "Started ping loop.")
|
||||||
|
|
||||||
|
while (_scopeIO?.isActive == true) {
|
||||||
|
try {
|
||||||
|
send(Opcode.Ping)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.w(TAG, "Failed to send ping.")
|
||||||
|
|
||||||
|
try {
|
||||||
|
_socket?.close()
|
||||||
|
_inputStream?.close()
|
||||||
|
_outputStream?.close()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.w(TAG, "Failed to close socket.", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*if (_lastPongTime != -1L && System.currentTimeMillis() - _lastPongTime > 6000) {
|
||||||
|
Logger.w(TAG, "Closing socket due to last pong time being larger than 6 seconds.")
|
||||||
|
|
||||||
|
try {
|
||||||
|
_socket?.close()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.w(TAG, "Failed to close socket.", e)
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
|
||||||
|
Thread.sleep(2000)
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.i(TAG, "Stopped ping loop.");
|
||||||
|
}.apply { start() }
|
||||||
|
} else {
|
||||||
|
Log.i(TAG, "Thread was still alive, not restarted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleMessage(opcode: Opcode, json: String? = null) {
|
private fun handleMessage(opcode: Opcode, json: String? = null) {
|
||||||
|
Log.i(TAG, "Processing packet (opcode: $opcode, size: ${json?.length ?: 0})")
|
||||||
|
|
||||||
when (opcode) {
|
when (opcode) {
|
||||||
Opcode.PLAYBACK_UPDATE -> {
|
Opcode.PlaybackUpdate -> {
|
||||||
if (json == null) {
|
if (json == null) {
|
||||||
Logger.w(TAG, "Got playback update without JSON, ignoring.");
|
Logger.w(TAG, "Got playback update without JSON, ignoring.");
|
||||||
return;
|
return;
|
||||||
@@ -339,7 +456,7 @@ class FCastCastingDevice : CastingDevice {
|
|||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Opcode.VOLUME_UPDATE -> {
|
Opcode.VolumeUpdate -> {
|
||||||
if (json == null) {
|
if (json == null) {
|
||||||
Logger.w(TAG, "Got volume update without JSON, ignoring.");
|
Logger.w(TAG, "Got volume update without JSON, ignoring.");
|
||||||
return;
|
return;
|
||||||
@@ -348,7 +465,7 @@ class FCastCastingDevice : CastingDevice {
|
|||||||
val volumeUpdate = FCastCastingDevice.json.decodeFromString<FCastVolumeUpdateMessage>(json);
|
val volumeUpdate = FCastCastingDevice.json.decodeFromString<FCastVolumeUpdateMessage>(json);
|
||||||
setVolume(volumeUpdate.volume, volumeUpdate.generationTime);
|
setVolume(volumeUpdate.volume, volumeUpdate.generationTime);
|
||||||
}
|
}
|
||||||
Opcode.PLAYBACK_ERROR -> {
|
Opcode.PlaybackError -> {
|
||||||
if (json == null) {
|
if (json == null) {
|
||||||
Logger.w(TAG, "Got playback error without JSON, ignoring.");
|
Logger.w(TAG, "Got playback error without JSON, ignoring.");
|
||||||
return;
|
return;
|
||||||
@@ -357,7 +474,7 @@ class FCastCastingDevice : CastingDevice {
|
|||||||
val playbackError = FCastCastingDevice.json.decodeFromString<FCastPlaybackErrorMessage>(json);
|
val playbackError = FCastCastingDevice.json.decodeFromString<FCastPlaybackErrorMessage>(json);
|
||||||
Logger.e(TAG, "Remote casting playback error received: $playbackError")
|
Logger.e(TAG, "Remote casting playback error received: $playbackError")
|
||||||
}
|
}
|
||||||
Opcode.VERSION -> {
|
Opcode.Version -> {
|
||||||
if (json == null) {
|
if (json == null) {
|
||||||
Logger.w(TAG, "Got version without JSON, ignoring.");
|
Logger.w(TAG, "Got version without JSON, ignoring.");
|
||||||
return;
|
return;
|
||||||
@@ -367,72 +484,54 @@ class FCastCastingDevice : CastingDevice {
|
|||||||
_version = version.version;
|
_version = version.version;
|
||||||
Logger.i(TAG, "Remote version received: $version")
|
Logger.i(TAG, "Remote version received: $version")
|
||||||
}
|
}
|
||||||
|
Opcode.Ping -> send(Opcode.Pong)
|
||||||
|
Opcode.Pong -> _lastPongTime = System.currentTimeMillis()
|
||||||
else -> { }
|
else -> { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sendMessage(opcode: Opcode) {
|
private fun send(opcode: Opcode, message: String? = null) {
|
||||||
try {
|
ensureNotMainThread()
|
||||||
val size = 1;
|
|
||||||
val outputStream = _outputStream;
|
synchronized (_outputStreamLock) {
|
||||||
if (outputStream == null) {
|
try {
|
||||||
Logger.w(TAG, "Failed to send $size bytes, output stream is null.");
|
val data: ByteArray = message?.encodeToByteArray() ?: ByteArray(0)
|
||||||
return;
|
val size = 1 + data.size
|
||||||
|
val outputStream = _outputStream
|
||||||
|
if (outputStream == null) {
|
||||||
|
Log.w(TAG, "Failed to send $size bytes, output stream is null.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val serializedSizeLE = ByteArray(4)
|
||||||
|
serializedSizeLE[0] = (size and 0xff).toByte()
|
||||||
|
serializedSizeLE[1] = (size shr 8 and 0xff).toByte()
|
||||||
|
serializedSizeLE[2] = (size shr 16 and 0xff).toByte()
|
||||||
|
serializedSizeLE[3] = (size shr 24 and 0xff).toByte()
|
||||||
|
outputStream.write(serializedSizeLE)
|
||||||
|
|
||||||
|
val opcodeBytes = ByteArray(1)
|
||||||
|
opcodeBytes[0] = opcode.value
|
||||||
|
outputStream.write(opcodeBytes)
|
||||||
|
|
||||||
|
if (data.isNotEmpty()) {
|
||||||
|
outputStream.write(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "Sent $size bytes: (opcode: $opcode, body: $message).")
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.i(TAG, "Failed to send message.", e)
|
||||||
|
throw e
|
||||||
}
|
}
|
||||||
|
|
||||||
val serializedSizeLE = ByteArray(4);
|
|
||||||
serializedSizeLE[0] = (size and 0xff).toByte();
|
|
||||||
serializedSizeLE[1] = (size shr 8 and 0xff).toByte();
|
|
||||||
serializedSizeLE[2] = (size shr 16 and 0xff).toByte();
|
|
||||||
serializedSizeLE[3] = (size shr 24 and 0xff).toByte();
|
|
||||||
outputStream.write(serializedSizeLE);
|
|
||||||
|
|
||||||
val opcodeBytes = ByteArray(1);
|
|
||||||
opcodeBytes[0] = opcode.value;
|
|
||||||
outputStream.write(opcodeBytes);
|
|
||||||
|
|
||||||
Log.d(TAG, "Sent $size bytes.");
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.i(TAG, "Failed to send message.", e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private inline fun <reified T> sendMessage(opcode: Opcode, message: T) {
|
private inline fun <reified T> send(opcode: Opcode, message: T) {
|
||||||
try {
|
try {
|
||||||
val data: ByteArray;
|
send(opcode, message?.let { Json.encodeToString(it) })
|
||||||
var jsonString: String? = null;
|
|
||||||
if (message != null) {
|
|
||||||
jsonString = json.encodeToString(message);
|
|
||||||
data = jsonString.encodeToByteArray();
|
|
||||||
} else {
|
|
||||||
data = ByteArray(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
val size = 1 + data.size;
|
|
||||||
val outputStream = _outputStream;
|
|
||||||
if (outputStream == null) {
|
|
||||||
Logger.w(TAG, "Failed to send $size bytes, output stream is null.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
val serializedSizeLE = ByteArray(4);
|
|
||||||
serializedSizeLE[0] = (size and 0xff).toByte();
|
|
||||||
serializedSizeLE[1] = (size shr 8 and 0xff).toByte();
|
|
||||||
serializedSizeLE[2] = (size shr 16 and 0xff).toByte();
|
|
||||||
serializedSizeLE[3] = (size shr 24 and 0xff).toByte();
|
|
||||||
outputStream.write(serializedSizeLE);
|
|
||||||
|
|
||||||
val opcodeBytes = ByteArray(1);
|
|
||||||
opcodeBytes[0] = opcode.value;
|
|
||||||
outputStream.write(opcodeBytes);
|
|
||||||
|
|
||||||
if (data.isNotEmpty()) {
|
|
||||||
outputStream.write(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.d(TAG, "Sent $size bytes: '$jsonString'.");
|
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.i(TAG, "Failed to send message.", e);
|
Log.i(TAG, "Failed to encode message to string.", e)
|
||||||
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -441,6 +540,9 @@ class FCastCastingDevice : CastingDevice {
|
|||||||
usedRemoteAddress = null;
|
usedRemoteAddress = null;
|
||||||
localAddress = null;
|
localAddress = null;
|
||||||
_started = false;
|
_started = false;
|
||||||
|
//TODO: Kill and/or join thread?
|
||||||
|
_thread = null;
|
||||||
|
_pingThread = null;
|
||||||
|
|
||||||
val socket = _socket;
|
val socket = _socket;
|
||||||
val scopeIO = _scopeIO;
|
val scopeIO = _scopeIO;
|
||||||
@@ -450,6 +552,8 @@ class FCastCastingDevice : CastingDevice {
|
|||||||
|
|
||||||
scopeIO.launch {
|
scopeIO.launch {
|
||||||
socket.close();
|
socket.close();
|
||||||
|
_inputStream?.close()
|
||||||
|
_outputStream?.close()
|
||||||
connectionState = CastConnectionState.DISCONNECTED;
|
connectionState = CastConnectionState.DISCONNECTED;
|
||||||
scopeIO.cancel();
|
scopeIO.cancel();
|
||||||
Logger.i(TAG, "Cancelled scopeIO with open socket.")
|
Logger.i(TAG, "Cancelled scopeIO with open socket.")
|
||||||
@@ -471,7 +575,65 @@ class FCastCastingDevice : CastingDevice {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val TAG = "FastCastCastingDevice";
|
val TAG = "FCastCastingDevice";
|
||||||
private val json = Json { ignoreUnknownKeys = true }
|
private val json = Json { ignoreUnknownKeys = true }
|
||||||
|
|
||||||
|
fun getKeyExchangeMessage(keyPair: KeyPair): FCastKeyExchangeMessage {
|
||||||
|
return FCastKeyExchangeMessage(1, Base64.encodeToString(keyPair.public.encoded, Base64.NO_WRAP))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun generateKeyPair(): KeyPair {
|
||||||
|
//modp14
|
||||||
|
val p = BigInteger("ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece45b3dc2007cb8a163bf0598da48361c55d39a69163fa8fd24cf5f83655d23dca3ad961c62f356208552bb9ed529077096966d670c354e4abc9804f1746c08ca18217c32905e462e36ce3be39e772c180e86039b2783a2ec07a28fb5c55df06f4c52c9de2bcbf6955817183995497cea956ae515d2261898fa051015728e5a8aacaa68ffffffffffffffff", 16)
|
||||||
|
val g = BigInteger("2", 16)
|
||||||
|
val dhSpec = DHParameterSpec(p, g)
|
||||||
|
|
||||||
|
val keyGen = KeyPairGenerator.getInstance("DH")
|
||||||
|
keyGen.initialize(dhSpec)
|
||||||
|
|
||||||
|
return keyGen.generateKeyPair()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun computeSharedSecret(privateKey: PrivateKey, keyExchangeMessage: FCastKeyExchangeMessage): SecretKeySpec {
|
||||||
|
val keyFactory = KeyFactory.getInstance("DH")
|
||||||
|
val receivedPublicKeyBytes = Base64.decode(keyExchangeMessage.publicKey, Base64.NO_WRAP)
|
||||||
|
val receivedPublicKeySpec = X509EncodedKeySpec(receivedPublicKeyBytes)
|
||||||
|
val receivedPublicKey = keyFactory.generatePublic(receivedPublicKeySpec)
|
||||||
|
|
||||||
|
val keyAgreement = KeyAgreement.getInstance("DH")
|
||||||
|
keyAgreement.init(privateKey)
|
||||||
|
keyAgreement.doPhase(receivedPublicKey, true)
|
||||||
|
|
||||||
|
val sharedSecret = keyAgreement.generateSecret()
|
||||||
|
Log.i(TAG, "sharedSecret ${Base64.encodeToString(sharedSecret, Base64.NO_WRAP)}")
|
||||||
|
val sha256 = MessageDigest.getInstance("SHA-256")
|
||||||
|
val hashedSecret = sha256.digest(sharedSecret)
|
||||||
|
Log.i(TAG, "hashedSecret ${Base64.encodeToString(hashedSecret, Base64.NO_WRAP)}")
|
||||||
|
|
||||||
|
return SecretKeySpec(hashedSecret, "AES")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun encryptMessage(aesKey: SecretKeySpec, decryptedMessage: FCastDecryptedMessage): FCastEncryptedMessage {
|
||||||
|
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, aesKey)
|
||||||
|
val iv = cipher.iv
|
||||||
|
val json = Json.encodeToString(decryptedMessage)
|
||||||
|
val encrypted = cipher.doFinal(json.toByteArray(Charsets.UTF_8))
|
||||||
|
return FCastEncryptedMessage(
|
||||||
|
version = 1,
|
||||||
|
iv = Base64.encodeToString(iv, Base64.NO_WRAP),
|
||||||
|
blob = Base64.encodeToString(encrypted, Base64.NO_WRAP)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decryptMessage(aesKey: SecretKeySpec, encryptedMessage: FCastEncryptedMessage): FCastDecryptedMessage {
|
||||||
|
val iv = Base64.decode(encryptedMessage.iv, Base64.NO_WRAP)
|
||||||
|
val encrypted = Base64.decode(encryptedMessage.blob, Base64.NO_WRAP)
|
||||||
|
|
||||||
|
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, aesKey, IvParameterSpec(iv))
|
||||||
|
val decryptedJson = cipher.doFinal(encrypted)
|
||||||
|
return Json.decodeFromString(String(decryptedJson, Charsets.UTF_8))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -205,11 +205,20 @@ class StateCasting {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun onResume() {
|
fun onResume() {
|
||||||
val resumeCastingDevice = _resumeCastingDevice
|
val ad = activeDevice
|
||||||
if (resumeCastingDevice != null) {
|
if (ad != null) {
|
||||||
connectDevice(deviceFromCastingDeviceInfo(resumeCastingDevice))
|
if (ad is FCastCastingDevice) {
|
||||||
_resumeCastingDevice = null
|
ad.ensureThreadStarted()
|
||||||
Log.i(TAG, "_resumeCastingDevice set to null onResume")
|
} else if (ad is ChromecastCastingDevice) {
|
||||||
|
ad.ensureThreadsStarted()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val resumeCastingDevice = _resumeCastingDevice
|
||||||
|
if (resumeCastingDevice != null) {
|
||||||
|
connectDevice(deviceFromCastingDeviceInfo(resumeCastingDevice))
|
||||||
|
_resumeCastingDevice = null
|
||||||
|
Log.i(TAG, "_resumeCastingDevice set to null onResume")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,6 +242,7 @@ class StateCasting {
|
|||||||
jmDNS.addServiceListener("_googlecast._tcp.local.", _chromecastServiceListener);
|
jmDNS.addServiceListener("_googlecast._tcp.local.", _chromecastServiceListener);
|
||||||
jmDNS.addServiceListener("_airplay._tcp.local.", _airPlayServiceListener);
|
jmDNS.addServiceListener("_airplay._tcp.local.", _airPlayServiceListener);
|
||||||
jmDNS.addServiceListener("_fastcast._tcp.local.", _fastCastServiceListener);
|
jmDNS.addServiceListener("_fastcast._tcp.local.", _fastCastServiceListener);
|
||||||
|
jmDNS.addServiceListener("_fcast._tcp.local.", _fastCastServiceListener);
|
||||||
|
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
jmDNS.addServiceTypeListener(_serviceTypeListener);
|
jmDNS.addServiceTypeListener(_serviceTypeListener);
|
||||||
|
|||||||
@@ -50,4 +50,23 @@ data class FCastPlaybackErrorMessage(
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class FCastVersionMessage(
|
data class FCastVersionMessage(
|
||||||
val version: Long
|
val version: Long
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class FCastKeyExchangeMessage(
|
||||||
|
val version: Long,
|
||||||
|
val publicKey: String
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class FCastDecryptedMessage(
|
||||||
|
val opcode: Long,
|
||||||
|
val message: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class FCastEncryptedMessage(
|
||||||
|
val version: Long,
|
||||||
|
val iv: String?,
|
||||||
|
val blob: String
|
||||||
)
|
)
|
||||||
@@ -9,7 +9,10 @@ import com.futo.platformplayer.api.http.server.HttpGET
|
|||||||
import com.futo.platformplayer.api.http.server.HttpPOST
|
import com.futo.platformplayer.api.http.server.HttpPOST
|
||||||
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.internal.JSDocs
|
||||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
|
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
|
||||||
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.engine.V8Plugin
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
import com.futo.platformplayer.engine.dev.V8RemoteObject
|
import com.futo.platformplayer.engine.dev.V8RemoteObject
|
||||||
import com.futo.platformplayer.engine.dev.V8RemoteObject.Companion.gsonStandard
|
import com.futo.platformplayer.engine.dev.V8RemoteObject.Companion.gsonStandard
|
||||||
@@ -20,18 +23,29 @@ import com.futo.platformplayer.states.StateApp
|
|||||||
import com.futo.platformplayer.states.StateAssets
|
import com.futo.platformplayer.states.StateAssets
|
||||||
import com.futo.platformplayer.states.StateDeveloper
|
import com.futo.platformplayer.states.StateDeveloper
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
|
import com.google.gson.ExclusionStrategy
|
||||||
|
import com.google.gson.FieldAttributes
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.GsonBuilder
|
||||||
import com.google.gson.JsonArray
|
import com.google.gson.JsonArray
|
||||||
|
import com.google.gson.JsonElement
|
||||||
import com.google.gson.JsonParser
|
import com.google.gson.JsonParser
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import java.lang.reflect.Field
|
||||||
import java.lang.reflect.InvocationTargetException
|
import java.lang.reflect.InvocationTargetException
|
||||||
|
import java.lang.reflect.Modifier
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
import kotlin.reflect.full.findAnnotation
|
||||||
|
import kotlin.reflect.full.memberFunctions
|
||||||
|
import kotlin.reflect.jvm.javaType
|
||||||
import kotlin.reflect.jvm.jvmErasure
|
import kotlin.reflect.jvm.jvmErasure
|
||||||
|
|
||||||
class DeveloperEndpoints(private val context: Context) {
|
class DeveloperEndpoints(private val context: Context) {
|
||||||
private val TAG = "DeveloperEndpoints";
|
private val TAG = "DeveloperEndpoints";
|
||||||
private val _client = ManagedHttpClient();
|
private val _client = ManagedHttpClient();
|
||||||
private var _testPlugin: V8Plugin? = null;
|
private var _testPlugin: V8Plugin? = null;
|
||||||
|
private var _testPluginFull: JSClient? = null;
|
||||||
private val testPluginOrThrow: V8Plugin get() = _testPlugin ?: throw IllegalStateException("Attempted to use test plugin without plugin");
|
private val testPluginOrThrow: V8Plugin get() = _testPlugin ?: throw IllegalStateException("Attempted to use test plugin without plugin");
|
||||||
private val _testPluginVariables: HashMap<String, V8RemoteObject> = hashMapOf();
|
private val _testPluginVariables: HashMap<String, V8RemoteObject> = hashMapOf();
|
||||||
|
|
||||||
@@ -90,15 +104,22 @@ class DeveloperEndpoints(private val context: Context) {
|
|||||||
@HttpGET("/source_docs.js", "application/javascript")
|
@HttpGET("/source_docs.js", "application/javascript")
|
||||||
val devSourceDocsJS = "const sourceDocs = $devSourceDocsJson";
|
val devSourceDocsJS = "const sourceDocs = $devSourceDocsJson";
|
||||||
|
|
||||||
|
@HttpGET("/source_doc_urls.json", "application/json")
|
||||||
|
fun devSourceDocUrlsJson(httpContext: HttpContext) {;
|
||||||
|
val docs = JSClient.getMethodDocUrls();
|
||||||
|
httpContext.respondCode(200, Json.encodeToString(docs), "application/json");
|
||||||
|
}
|
||||||
|
@HttpGET("/source_doc_urls.js", "application/javascript")
|
||||||
|
fun devSourceDocUrlsJs(httpContext: HttpContext) {;
|
||||||
|
val docs = JSClient.getMethodDocUrls();
|
||||||
|
httpContext.respondCode(200, "const sourceDocUrls = " + Json.encodeToString(docs), "application/javascript");
|
||||||
|
}
|
||||||
|
|
||||||
//Dependencies
|
//Dependencies
|
||||||
//@HttpGET("/dependencies/vue.js", "application/javascript")
|
|
||||||
//val depVue = StateAssets.readAsset(context, "devportal/dependencies/vue.js", true);
|
|
||||||
//@HttpGET("/dependencies/vuetify.js", "application/javascript")
|
|
||||||
//val depVuetify = StateAssets.readAsset(context, "devportal/dependencies/vuetify.js", true);
|
|
||||||
//@HttpGET("/dependencies/vuetify.min.css", "text/css")
|
|
||||||
//val depVuetifyCss = StateAssets.readAsset(context, "devportal/dependencies/vuetify.min.css", true);
|
|
||||||
@HttpGET("/dependencies/FutoMainLogo.svg", "image/svg+xml")
|
@HttpGET("/dependencies/FutoMainLogo.svg", "image/svg+xml")
|
||||||
val depFutoLogo = StateAssets.readAsset(context, "devportal/dependencies/FutoMainLogo.svg");
|
val depFutoLogo = StateAssets.readAsset(context, "devportal/dependencies/FutoMainLogo.svg");
|
||||||
|
@HttpGET("/favicon.svg", "image/svg+xml")
|
||||||
|
val favicon = StateAssets.readAsset(context, "devportal/dependencies/favicon.svg");
|
||||||
|
|
||||||
@HttpGET("/reference_plugin.d.ts", "text/plain")
|
@HttpGET("/reference_plugin.d.ts", "text/plain")
|
||||||
fun devSourceTSWithRefs(httpContext: HttpContext) {
|
fun devSourceTSWithRefs(httpContext: HttpContext) {
|
||||||
@@ -190,6 +211,17 @@ class DeveloperEndpoints(private val context: Context) {
|
|||||||
val client = JSHttpClient(null, null, null, config);
|
val client = JSHttpClient(null, null, null, config);
|
||||||
val clientAuth = JSHttpClient(null, null, null, config);
|
val clientAuth = JSHttpClient(null, null, null, config);
|
||||||
_testPlugin = V8Plugin(StateApp.instance.context, config, null, client, clientAuth);
|
_testPlugin = V8Plugin(StateApp.instance.context, config, null, client, clientAuth);
|
||||||
|
try {
|
||||||
|
val script = _client.get(config.absoluteScriptUrl);
|
||||||
|
_testPluginFull = JSClient(StateApp.instance.context, SourcePluginDescriptor(
|
||||||
|
config, null, null, null
|
||||||
|
), null, script.body?.string() ?: "");
|
||||||
|
_testPluginFull!!.initialize();
|
||||||
|
}
|
||||||
|
catch (ex: Throwable) {
|
||||||
|
Logger.e(TAG, "Loading full client failed", ex);
|
||||||
|
_testPluginFull = null;
|
||||||
|
}
|
||||||
|
|
||||||
context.respondJson(200, testPluginOrThrow.getPackageVariables());
|
context.respondJson(200, testPluginOrThrow.getPackageVariables());
|
||||||
}
|
}
|
||||||
@@ -412,6 +444,25 @@ class DeveloperEndpoints(private val context: Context) {
|
|||||||
context.respondCode(500, ex::class.simpleName + ":" + ex.message, "text/plain")
|
context.respondCode(500, ex::class.simpleName + ":" + ex.message, "text/plain")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@HttpGET("/dev/setDevProxy")
|
||||||
|
fun devSetDevProxy(context: HttpContext) {
|
||||||
|
try {
|
||||||
|
val url = context.query.getOrDefault("url", "");
|
||||||
|
val port = context.query.getOrDefault("port", "");
|
||||||
|
if(url.isNullOrEmpty() || port.isNullOrEmpty() || port.toIntOrNull() == null)
|
||||||
|
{
|
||||||
|
StateDeveloper.instance.devProxy = null;
|
||||||
|
context.respondCode(400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
StateDeveloper.instance.devProxy = StateDeveloper.DevProxySettings(url, port.toInt());
|
||||||
|
context.respondCode(200, "true", "application/json");
|
||||||
|
}
|
||||||
|
catch(ex: Exception) {
|
||||||
|
Logger.e("DeveloperEndpoints", ex.message, ex);
|
||||||
|
context.respondCode(500, ex::class.simpleName + ":" + ex.message, "text/plain")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@HttpGET("/plugin/getDevLogs")
|
@HttpGET("/plugin/getDevLogs")
|
||||||
fun pluginGetDevLogs(context: HttpContext) {
|
fun pluginGetDevLogs(context: HttpContext) {
|
||||||
@@ -423,6 +474,15 @@ class DeveloperEndpoints(private val context: Context) {
|
|||||||
context.respondCode(500, ex.message ?: "", "text/plain")
|
context.respondCode(500, ex.message ?: "", "text/plain")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@HttpGET("/plugin/getDevHttpExchanges")
|
||||||
|
fun pluginGetDevExchanges(context: HttpContext) {
|
||||||
|
try {
|
||||||
|
context.respondJson(200, StateDeveloper.instance.getHttpExchangesAndClear());
|
||||||
|
}
|
||||||
|
catch(ex: Exception) {
|
||||||
|
context.respondCode(500, ex.message ?: "", "text/plain")
|
||||||
|
}
|
||||||
|
}
|
||||||
@HttpGET("/plugin/fakeDevLog")
|
@HttpGET("/plugin/fakeDevLog")
|
||||||
fun pluginFakeDevLog(context: HttpContext) {
|
fun pluginFakeDevLog(context: HttpContext) {
|
||||||
try {
|
try {
|
||||||
@@ -440,6 +500,68 @@ class DeveloperEndpoints(private val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val _fieldAttributesField = FieldAttributes::class.java.getDeclaredField("field");
|
||||||
|
init {
|
||||||
|
_fieldAttributesField.isAccessible = true;
|
||||||
|
}
|
||||||
|
private val _remoteTestGson = GsonBuilder()
|
||||||
|
.setExclusionStrategies(object : ExclusionStrategy {
|
||||||
|
override fun shouldSkipClass(clazz: Class<*>?): Boolean {
|
||||||
|
return clazz?.simpleName == "JSClient" ||
|
||||||
|
clazz?.simpleName == "KSerializer[]" ||
|
||||||
|
clazz?.simpleName == "V8ValueObject";
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun shouldSkipField(f: FieldAttributes?): Boolean {
|
||||||
|
val isPublic = f?.hasModifier(Modifier.PUBLIC) ?: true;
|
||||||
|
if(!isPublic) {
|
||||||
|
val underlyingField = _fieldAttributesField.get(f) as Field;
|
||||||
|
return !(underlyingField.declaringClass as Class).methods.any { it.name == "get" + underlyingField.name.replaceFirstChar { it.uppercaseChar() } && Modifier.isPublic(it.modifiers) };
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return !isPublic;
|
||||||
|
}
|
||||||
|
}).create();
|
||||||
|
@HttpPOST("/plugin/remoteTest")
|
||||||
|
fun pluginRemoteTest(context: HttpContext) {
|
||||||
|
val method = context.query.getOrDefault("method", "");
|
||||||
|
try {
|
||||||
|
|
||||||
|
val parameters = context.readContentString();
|
||||||
|
val paras = JsonParser.parseString(parameters);
|
||||||
|
if(!paras.isJsonArray)
|
||||||
|
throw IllegalArgumentException("Expected json array as body");
|
||||||
|
|
||||||
|
val plugin = _testPluginFull ?: throw IllegalStateException("Plugin not loaded");
|
||||||
|
|
||||||
|
val function = plugin::class.memberFunctions.filter { it.findAnnotation<JSDocs>() != null }
|
||||||
|
.find { it.name == method };
|
||||||
|
if(function == null)
|
||||||
|
throw java.lang.IllegalArgumentException("Plugin method [${function}] not found");
|
||||||
|
val callResult = function.call(*(listOf(plugin) + paras.asJsonArray.take(function.parameters.size - 1).mapIndexed { index, jsonElement ->
|
||||||
|
//For now, manual conversion.
|
||||||
|
val parameter = function.parameters[index + 1];
|
||||||
|
val value = _remoteTestGson.fromJson<Any>(jsonElement, parameter.type.javaType);
|
||||||
|
return@mapIndexed value;
|
||||||
|
}).toTypedArray());
|
||||||
|
val json = if(callResult is IPager<*>)
|
||||||
|
_remoteTestGson.toJson(callResult.getResults())
|
||||||
|
else
|
||||||
|
_remoteTestGson.toJson(callResult);
|
||||||
|
//val json = wrapRemoteResult(callResult, false);
|
||||||
|
|
||||||
|
context.respondCode(200, json);
|
||||||
|
}
|
||||||
|
catch(ex: InvocationTargetException) {
|
||||||
|
Logger.e(TAG, "Remote test for [${method}] is failed", ex.targetException);
|
||||||
|
context.respondCode(500, ex.targetException.message ?: "", "text/plain")
|
||||||
|
}
|
||||||
|
catch(ex: Exception) {
|
||||||
|
Logger.e(TAG, "Remote test for [${method}] is failed", ex);
|
||||||
|
context.respondCode(500, ex.message ?: "", "text/plain")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//Internal calls
|
//Internal calls
|
||||||
@HttpPOST("/get")
|
@HttpPOST("/get")
|
||||||
fun get(context: HttpContext) {
|
fun get(context: HttpContext) {
|
||||||
|
|||||||
@@ -96,6 +96,11 @@ class AutoUpdateDialog(context: Context?) : AlertDialog(context) {
|
|||||||
Logger.i(TAG, "Cleared InstallReceiver.onReceiveResult handler.")
|
Logger.i(TAG, "Cleared InstallReceiver.onReceiveResult handler.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun hideExceptionButtons() {
|
||||||
|
_buttonNever.visibility = View.GONE
|
||||||
|
_buttonShowChangelog.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
private fun update() {
|
private fun update() {
|
||||||
_buttonShowChangelog.visibility = Button.GONE;
|
_buttonShowChangelog.visibility = Button.GONE;
|
||||||
_buttonNever.visibility = Button.GONE;
|
_buttonNever.visibility = Button.GONE;
|
||||||
|
|||||||
@@ -6,11 +6,12 @@ import android.graphics.Color
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
import android.util.Log
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
import android.widget.*
|
import android.widget.EditText
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.api.media.PlatformID
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
@@ -25,7 +26,11 @@ import com.futo.platformplayer.logging.Logger
|
|||||||
import com.futo.platformplayer.selectBestImage
|
import com.futo.platformplayer.selectBestImage
|
||||||
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.polycentric.core.*
|
import com.futo.polycentric.core.ClaimType
|
||||||
|
import com.futo.polycentric.core.Store
|
||||||
|
import com.futo.polycentric.core.SystemState
|
||||||
|
import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl
|
||||||
|
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||||
import com.google.android.material.button.MaterialButton
|
import com.google.android.material.button.MaterialButton
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -93,7 +98,7 @@ class CommentDialog(context: Context?, val contextUrl: String, val ref: Protocol
|
|||||||
|
|
||||||
val comment = _editComment.text.toString();
|
val comment = _editComment.text.toString();
|
||||||
val processHandle = StatePolycentric.instance.processHandle!!
|
val processHandle = StatePolycentric.instance.processHandle!!
|
||||||
val eventPointer = processHandle.post(comment, null, ref)
|
val eventPointer = processHandle.post(comment, ref)
|
||||||
|
|
||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
@@ -118,7 +123,8 @@ class CommentDialog(context: Context?, val contextUrl: String, val ref: Protocol
|
|||||||
msg = comment,
|
msg = comment,
|
||||||
rating = RatingLikeDislikes(0, 0),
|
rating = RatingLikeDislikes(0, 0),
|
||||||
date = OffsetDateTime.now(),
|
date = OffsetDateTime.now(),
|
||||||
eventPointer = eventPointer
|
eventPointer = eventPointer,
|
||||||
|
parentReference = ref
|
||||||
));
|
));
|
||||||
|
|
||||||
dismiss();
|
dismiss();
|
||||||
|
|||||||
@@ -1,43 +1,34 @@
|
|||||||
package com.futo.platformplayer.dialogs
|
package com.futo.platformplayer.dialogs
|
||||||
|
|
||||||
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.drawable.Animatable
|
import android.graphics.drawable.Animatable
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
|
import android.widget.ImageButton
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.futo.platformplayer.logging.Logger
|
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.activities.AddSourceActivity
|
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.activities.QRCaptureActivity
|
|
||||||
import com.futo.platformplayer.casting.CastConnectionState
|
import com.futo.platformplayer.casting.CastConnectionState
|
||||||
import com.futo.platformplayer.casting.CastingDevice
|
import com.futo.platformplayer.casting.CastingDevice
|
||||||
import com.futo.platformplayer.casting.StateCasting
|
import com.futo.platformplayer.casting.StateCasting
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.views.adapters.DeviceAdapter
|
import com.futo.platformplayer.views.adapters.DeviceAdapter
|
||||||
import com.google.zxing.integration.android.IntentIntegrator
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.UUID
|
|
||||||
|
|
||||||
class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
||||||
private lateinit var _imageLoader: ImageView;
|
private lateinit var _imageLoader: ImageView;
|
||||||
private lateinit var _buttonClose: Button;
|
private lateinit var _buttonClose: Button;
|
||||||
private lateinit var _buttonAdd: Button;
|
private lateinit var _buttonAdd: ImageButton;
|
||||||
private lateinit var _buttonScanQR: Button;
|
private lateinit var _buttonScanQR: ImageButton;
|
||||||
private lateinit var _textNoDevicesFound: TextView;
|
private lateinit var _textNoDevicesFound: TextView;
|
||||||
private lateinit var _textNoDevicesRemembered: TextView;
|
private lateinit var _textNoDevicesRemembered: TextView;
|
||||||
private lateinit var _recyclerDevices: RecyclerView;
|
private lateinit var _recyclerDevices: RecyclerView;
|
||||||
@@ -80,6 +71,14 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
|||||||
_textNoDevicesRemembered.visibility = if (_rememberedDevices.isEmpty()) View.VISIBLE else View.GONE;
|
_textNoDevicesRemembered.visibility = if (_rememberedDevices.isEmpty()) View.VISIBLE else View.GONE;
|
||||||
_recyclerRememberedDevices.visibility = if (_rememberedDevices.isNotEmpty()) View.VISIBLE else View.GONE;
|
_recyclerRememberedDevices.visibility = if (_rememberedDevices.isNotEmpty()) View.VISIBLE else View.GONE;
|
||||||
};
|
};
|
||||||
|
_rememberedAdapter.onConnect.subscribe { _ ->
|
||||||
|
dismiss()
|
||||||
|
UIDialogs.showCastingDialog(context)
|
||||||
|
}
|
||||||
|
_adapter.onConnect.subscribe { _ ->
|
||||||
|
dismiss()
|
||||||
|
UIDialogs.showCastingDialog(context)
|
||||||
|
}
|
||||||
_recyclerRememberedDevices.adapter = _rememberedAdapter;
|
_recyclerRememberedDevices.adapter = _rememberedAdapter;
|
||||||
_recyclerRememberedDevices.layoutManager = LinearLayoutManager(context);
|
_recyclerRememberedDevices.layoutManager = LinearLayoutManager(context);
|
||||||
|
|
||||||
|
|||||||
@@ -133,17 +133,19 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
|||||||
|
|
||||||
StateCasting.instance.onActiveDeviceVolumeChanged.remove(this);
|
StateCasting.instance.onActiveDeviceVolumeChanged.remove(this);
|
||||||
StateCasting.instance.onActiveDeviceVolumeChanged.subscribe {
|
StateCasting.instance.onActiveDeviceVolumeChanged.subscribe {
|
||||||
_sliderVolume.value = it.toFloat();
|
_sliderVolume.value = it.toFloat().coerceAtLeast(0.0f).coerceAtMost(_sliderVolume.valueTo);
|
||||||
};
|
};
|
||||||
|
|
||||||
StateCasting.instance.onActiveDeviceTimeChanged.remove(this);
|
StateCasting.instance.onActiveDeviceTimeChanged.remove(this);
|
||||||
StateCasting.instance.onActiveDeviceTimeChanged.subscribe {
|
StateCasting.instance.onActiveDeviceTimeChanged.subscribe {
|
||||||
_sliderPosition.value = it.toFloat();
|
_sliderPosition.value = it.toFloat().coerceAtLeast(0.0f).coerceAtMost(_sliderPosition.valueTo);
|
||||||
};
|
};
|
||||||
|
|
||||||
StateCasting.instance.onActiveDeviceDurationChanged.remove(this);
|
StateCasting.instance.onActiveDeviceDurationChanged.remove(this);
|
||||||
StateCasting.instance.onActiveDeviceDurationChanged.subscribe {
|
StateCasting.instance.onActiveDeviceDurationChanged.subscribe {
|
||||||
_sliderPosition.valueTo = it.toFloat();
|
val dur = it.toFloat().coerceAtLeast(1.0f)
|
||||||
|
_sliderPosition.value = _sliderPosition.value.coerceAtLeast(0.0f).coerceAtMost(dur);
|
||||||
|
_sliderPosition.valueTo = dur
|
||||||
};
|
};
|
||||||
|
|
||||||
_device = StateCasting.instance.activeDevice;
|
_device = StateCasting.instance.activeDevice;
|
||||||
@@ -152,6 +154,7 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
|||||||
setLoading(!isConnected);
|
setLoading(!isConnected);
|
||||||
StateCasting.instance.onActiveDeviceConnectionStateChanged.subscribe(this) { _, connectionState ->
|
StateCasting.instance.onActiveDeviceConnectionStateChanged.subscribe(this) { _, connectionState ->
|
||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) { setLoading(connectionState != CastConnectionState.CONNECTED); };
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) { setLoading(connectionState != CastConnectionState.CONNECTED); };
|
||||||
|
updateDevice();
|
||||||
};
|
};
|
||||||
|
|
||||||
updateDevice();
|
updateDevice();
|
||||||
@@ -181,10 +184,13 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_textName.text = d.name;
|
_textName.text = d.name;
|
||||||
_sliderVolume.value = d.volume.toFloat();
|
|
||||||
_sliderPosition.valueFrom = 0.0f;
|
_sliderPosition.valueFrom = 0.0f;
|
||||||
_sliderPosition.valueTo = d.duration.toFloat();
|
_sliderVolume.valueFrom = 0.0f;
|
||||||
_sliderPosition.value = d.time.toFloat();
|
_sliderVolume.value = d.volume.toFloat().coerceAtLeast(0.0f).coerceAtMost(_sliderVolume.valueTo);
|
||||||
|
|
||||||
|
val dur = d.duration.toFloat().coerceAtLeast(1.0f)
|
||||||
|
_sliderPosition.value = d.time.toFloat().coerceAtLeast(0.0f).coerceAtMost(dur)
|
||||||
|
_sliderPosition.valueTo = dur
|
||||||
|
|
||||||
if (d.canSetVolume) {
|
if (d.canSetVolume) {
|
||||||
_layoutVolumeAdjustable.visibility = View.VISIBLE;
|
_layoutVolumeAdjustable.visibility = View.VISIBLE;
|
||||||
@@ -193,6 +199,44 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
|||||||
_layoutVolumeAdjustable.visibility = View.GONE;
|
_layoutVolumeAdjustable.visibility = View.GONE;
|
||||||
_layoutVolumeFixed.visibility = View.VISIBLE;
|
_layoutVolumeFixed.visibility = View.VISIBLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val interactiveControls = listOf(
|
||||||
|
_sliderPosition,
|
||||||
|
_sliderVolume,
|
||||||
|
_buttonPrevious,
|
||||||
|
_buttonPlay,
|
||||||
|
_buttonPause,
|
||||||
|
_buttonStop,
|
||||||
|
_buttonNext
|
||||||
|
)
|
||||||
|
|
||||||
|
when (d.connectionState) {
|
||||||
|
CastConnectionState.CONNECTED -> {
|
||||||
|
enableControls(interactiveControls)
|
||||||
|
}
|
||||||
|
CastConnectionState.CONNECTING,
|
||||||
|
CastConnectionState.DISCONNECTED -> {
|
||||||
|
disableControls(interactiveControls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun enableControls(views: List<View>) {
|
||||||
|
views.forEach { enableControl(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun enableControl(view: View) {
|
||||||
|
view.alpha = 1.0f
|
||||||
|
view.isEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun disableControls(views: List<View>) {
|
||||||
|
views.forEach { disableControl(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun disableControl(view: View) {
|
||||||
|
view.alpha = 0.4f
|
||||||
|
view.isEnabled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setLoading(isLoading: Boolean) {
|
private fun setLoading(isLoading: Boolean) {
|
||||||
|
|||||||
@@ -22,7 +22,9 @@ import com.futo.platformplayer.UIDialogs
|
|||||||
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
|
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
|
||||||
import com.futo.platformplayer.assume
|
import com.futo.platformplayer.assume
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.models.ImportCache
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.futo.platformplayer.states.StateBackup
|
||||||
import com.futo.platformplayer.stores.v2.ManagedStore
|
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -66,13 +68,15 @@ class ImportDialog : AlertDialog {
|
|||||||
private val _name: String;
|
private val _name: String;
|
||||||
private val _toImport: List<String>;
|
private val _toImport: List<String>;
|
||||||
|
|
||||||
|
private val _cache: ImportCache?;
|
||||||
|
|
||||||
constructor(context: Context, importStore: ManagedStore<*>, name: String, toReconstruct: List<String>, onConcluded: ()->Unit): super(context) {
|
constructor(context: Context, importStore: ManagedStore<*>, name: String, toReconstruct: List<String>, cache: ImportCache?, onConcluded: ()->Unit): super(context) {
|
||||||
_context = context;
|
_context = context;
|
||||||
_store = importStore;
|
_store = importStore;
|
||||||
_onConcluded = onConcluded;
|
_onConcluded = onConcluded;
|
||||||
_name = name;
|
_name = name;
|
||||||
_toImport = ArrayList(toReconstruct);
|
_toImport = ArrayList(toReconstruct);
|
||||||
|
_cache = cache;
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -146,7 +150,7 @@ class ImportDialog : AlertDialog {
|
|||||||
val scope = StateApp.instance.scopeOrNull;
|
val scope = StateApp.instance.scopeOrNull;
|
||||||
scope?.launch(Dispatchers.IO) {
|
scope?.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val migrationResult = _store.importReconstructions(_toImport) { finished, total ->
|
val migrationResult = _store.importReconstructions(_toImport, _cache) { finished, total ->
|
||||||
scope.launch(Dispatchers.Main) {
|
scope.launch(Dispatchers.Main) {
|
||||||
_textProgress.text = "${finished}/${total}";
|
_textProgress.text = "${finished}/${total}";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,253 @@
|
|||||||
|
package com.futo.platformplayer.dialogs
|
||||||
|
|
||||||
|
import android.app.AlertDialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.drawable.Animatable
|
||||||
|
import android.media.MediaCas.PluginDescriptor
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.Spannable
|
||||||
|
import android.text.SpannableString
|
||||||
|
import android.text.method.ScrollingMovementMethod
|
||||||
|
import android.text.style.ForegroundColorSpan
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.WindowManager
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
|
import com.futo.platformplayer.activities.AddSourceActivity
|
||||||
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
|
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginDescriptor
|
||||||
|
import com.futo.platformplayer.assume
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.models.ImportCache
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.futo.platformplayer.states.StateBackup
|
||||||
|
import com.futo.platformplayer.states.StatePlugins
|
||||||
|
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
class PluginUpdateDialog : AlertDialog {
|
||||||
|
companion object {
|
||||||
|
private val TAG = "PluginUpdateDialog";
|
||||||
|
}
|
||||||
|
private val _context: Context;
|
||||||
|
|
||||||
|
private lateinit var _buttonCancel1: Button;
|
||||||
|
private lateinit var _buttonCancel2: Button;
|
||||||
|
private lateinit var _buttonUpdate: LinearLayout;
|
||||||
|
|
||||||
|
private lateinit var _buttonOk: LinearLayout;
|
||||||
|
private lateinit var _buttonInstall: LinearLayout;
|
||||||
|
|
||||||
|
private lateinit var _textPlugin: TextView;
|
||||||
|
private lateinit var _textProgres: TextView;
|
||||||
|
private lateinit var _textError: TextView;
|
||||||
|
private lateinit var _textResult: TextView;
|
||||||
|
|
||||||
|
private lateinit var _uiChoiceTop: FrameLayout;
|
||||||
|
private lateinit var _uiProgressTop: FrameLayout;
|
||||||
|
private lateinit var _uiRiskTop: FrameLayout;
|
||||||
|
|
||||||
|
private lateinit var _uiChoiceBot: LinearLayout;
|
||||||
|
private lateinit var _uiResultBot: LinearLayout;
|
||||||
|
private lateinit var _uiRiskBot: LinearLayout;
|
||||||
|
private lateinit var _uiProgressBot: LinearLayout;
|
||||||
|
|
||||||
|
private lateinit var _iconPlugin: ImageView;
|
||||||
|
private lateinit var _updateSpinner: ImageView;
|
||||||
|
|
||||||
|
private var _isUpdating = false;
|
||||||
|
|
||||||
|
private val _oldConfig: SourcePluginConfig;
|
||||||
|
private val _newConfig: SourcePluginConfig;
|
||||||
|
|
||||||
|
|
||||||
|
constructor(context: Context, oldConfig: SourcePluginConfig, newConfig: SourcePluginConfig): super(context) {
|
||||||
|
_context = context;
|
||||||
|
_oldConfig = oldConfig;
|
||||||
|
_newConfig = newConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(LayoutInflater.from(context).inflate(R.layout.dialog_plugin_update, null));
|
||||||
|
|
||||||
|
_buttonCancel1 = findViewById(R.id.button_cancel_1);
|
||||||
|
_buttonCancel2 = findViewById(R.id.button_cancel_2);
|
||||||
|
_buttonUpdate = findViewById(R.id.button_update);
|
||||||
|
|
||||||
|
_buttonOk = findViewById(R.id.button_ok);
|
||||||
|
_buttonInstall = findViewById(R.id.button_install);
|
||||||
|
|
||||||
|
_textPlugin = findViewById(R.id.text_plugin);
|
||||||
|
_textProgres = findViewById(R.id.text_progress);
|
||||||
|
_textError = findViewById(R.id.text_error);
|
||||||
|
_textResult = findViewById(R.id.text_result);
|
||||||
|
|
||||||
|
_uiChoiceTop = findViewById(R.id.dialog_ui_choice_top);
|
||||||
|
_uiProgressTop = findViewById(R.id.dialog_ui_progress_top);
|
||||||
|
_uiRiskTop = findViewById(R.id.dialog_ui_risk_top);
|
||||||
|
|
||||||
|
_uiChoiceBot = findViewById(R.id.dialog_ui_bottom_choice);
|
||||||
|
_uiResultBot = findViewById(R.id.dialog_ui_bottom_result);
|
||||||
|
_uiRiskBot = findViewById(R.id.dialog_ui_bottom_risk);
|
||||||
|
_uiProgressBot = findViewById(R.id.dialog_ui_bottom_progress);
|
||||||
|
|
||||||
|
_updateSpinner = findViewById(R.id.update_spinner);
|
||||||
|
_iconPlugin = findViewById(R.id.icon_plugin);
|
||||||
|
|
||||||
|
_buttonCancel1.setOnClickListener {
|
||||||
|
dismiss();
|
||||||
|
};
|
||||||
|
_buttonCancel2.setOnClickListener {
|
||||||
|
dismiss();
|
||||||
|
};
|
||||||
|
_buttonUpdate.setOnClickListener {
|
||||||
|
if (_isUpdating)
|
||||||
|
return@setOnClickListener;
|
||||||
|
_isUpdating = true;
|
||||||
|
update();
|
||||||
|
};
|
||||||
|
|
||||||
|
Glide.with(_iconPlugin)
|
||||||
|
.load(_oldConfig.absoluteIconUrl)
|
||||||
|
.fallback(R.drawable.ic_sources)
|
||||||
|
.into(_iconPlugin);
|
||||||
|
_textPlugin.text = _oldConfig.name;
|
||||||
|
|
||||||
|
val descriptor = StatePlugins.instance.getPlugin(_oldConfig.id);
|
||||||
|
if(descriptor != null) {
|
||||||
|
if(descriptor.appSettings.automaticUpdate) {
|
||||||
|
if (_isUpdating)
|
||||||
|
return;
|
||||||
|
_isUpdating = true;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dismiss() {
|
||||||
|
super.dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun update() {
|
||||||
|
_uiChoiceTop.visibility = View.GONE;
|
||||||
|
_uiRiskTop.visibility = View.GONE;
|
||||||
|
_uiChoiceBot.visibility = View.GONE;
|
||||||
|
_uiResultBot.visibility = View.GONE;
|
||||||
|
_uiRiskBot.visibility = View.GONE;
|
||||||
|
_uiProgressTop.visibility = View.VISIBLE;
|
||||||
|
_uiProgressBot.visibility = View.VISIBLE;
|
||||||
|
|
||||||
|
setCancelable(false);
|
||||||
|
setCanceledOnTouchOutside(false);
|
||||||
|
|
||||||
|
Logger.i(TAG, "Keep screen on set import")
|
||||||
|
window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||||
|
|
||||||
|
_updateSpinner.drawable?.assume<Animatable>()?.start();
|
||||||
|
|
||||||
|
val scope = StateApp.instance.scopeOrNull;
|
||||||
|
scope?.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val client = ManagedHttpClient();
|
||||||
|
val script = StatePlugins.instance.getScript(_oldConfig.id) ?: "";
|
||||||
|
|
||||||
|
val newScript = client.get(_newConfig.absoluteScriptUrl)?.body?.string();
|
||||||
|
if(newScript.isNullOrEmpty())
|
||||||
|
throw IllegalStateException("No script found");
|
||||||
|
|
||||||
|
if(_oldConfig.isLowRiskUpdate(script, _newConfig, newScript)){
|
||||||
|
|
||||||
|
StatePlugins.instance.installPluginBackground(context, StateApp.instance.scope, _newConfig, newScript,
|
||||||
|
{ text: String, progress: Double ->
|
||||||
|
_textProgres.setText(text);
|
||||||
|
},
|
||||||
|
{ ex ->
|
||||||
|
if(ex == null) {
|
||||||
|
StatePlugins.instance.clearUpdateAvailable(_newConfig);
|
||||||
|
_iconPlugin.setImageResource(R.drawable.ic_check);
|
||||||
|
_textError.visibility = View.GONE;
|
||||||
|
_textResult.visibility = View.VISIBLE;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
_iconPlugin.setImageResource(R.drawable.ic_error_pred);
|
||||||
|
_textError.text = ex.message + "\n\nYou can retry inside the sources tab";
|
||||||
|
_textError.visibility = View.VISIBLE;
|
||||||
|
_textResult.visibility = View.GONE;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
_buttonOk.setOnClickListener {
|
||||||
|
dismiss();
|
||||||
|
}
|
||||||
|
_uiProgressTop.visibility = View.GONE;
|
||||||
|
_uiProgressBot.visibility = View.GONE;
|
||||||
|
_uiChoiceTop.visibility = View.VISIBLE;
|
||||||
|
_uiResultBot.visibility = View.VISIBLE;
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to update UI.", e)
|
||||||
|
} finally {
|
||||||
|
Logger.i(TAG, "Keep screen on unset update")
|
||||||
|
window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
try {
|
||||||
|
_buttonInstall.setOnClickListener {
|
||||||
|
dismiss();
|
||||||
|
|
||||||
|
val intent = Intent(_context, AddSourceActivity::class.java).apply {
|
||||||
|
data = Uri.parse(_newConfig.sourceUrl)
|
||||||
|
};
|
||||||
|
|
||||||
|
_context.startActivity(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
_uiProgressTop.visibility = View.GONE;
|
||||||
|
_uiProgressBot.visibility = View.GONE;
|
||||||
|
_uiRiskTop.visibility = View.VISIBLE;
|
||||||
|
_uiRiskBot.visibility = View.VISIBLE;
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to update UI.", e)
|
||||||
|
} finally {
|
||||||
|
Logger.i(TAG, "Keep screen on unset update")
|
||||||
|
window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to update.", e);
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
_buttonOk.setOnClickListener {
|
||||||
|
dismiss();
|
||||||
|
}
|
||||||
|
_iconPlugin.setImageResource(R.drawable.ic_error_pred);
|
||||||
|
_textResult.visibility = View.GONE;
|
||||||
|
_uiProgressTop.visibility = View.GONE;
|
||||||
|
_uiProgressBot.visibility = View.GONE;
|
||||||
|
_uiChoiceTop.visibility = View.VISIBLE;
|
||||||
|
_uiResultBot.visibility = View.VISIBLE;
|
||||||
|
_textError.visibility = View.VISIBLE;
|
||||||
|
_textError.text = e.message + "\n\nYou can retry inside the sources tab"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -337,8 +337,10 @@ class VideoDownload {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var wasSuccesful = false;
|
||||||
try {
|
try {
|
||||||
awaitAll(*sourcesToDownload.toTypedArray());
|
awaitAll(*sourcesToDownload.toTypedArray());
|
||||||
|
wasSuccesful = true;
|
||||||
}
|
}
|
||||||
catch(runtimeEx: RuntimeException) {
|
catch(runtimeEx: RuntimeException) {
|
||||||
if(runtimeEx.cause != null)
|
if(runtimeEx.cause != null)
|
||||||
@@ -349,6 +351,29 @@ class VideoDownload {
|
|||||||
catch(ex: Throwable) {
|
catch(ex: Throwable) {
|
||||||
throw ex;
|
throw ex;
|
||||||
}
|
}
|
||||||
|
finally {
|
||||||
|
if(!wasSuccesful) {
|
||||||
|
try {
|
||||||
|
if(videoFilePath != null) {
|
||||||
|
val remainingVideo = File(videoFilePath!!);
|
||||||
|
if (remainingVideo.exists()) {
|
||||||
|
Logger.i(TAG, "Deleting remaining video file");
|
||||||
|
remainingVideo.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(audioFilePath != null) {
|
||||||
|
val remainingAudio = File(audioFilePath!!);
|
||||||
|
if (remainingAudio.exists()) {
|
||||||
|
Logger.i(TAG, "Deleting remaining audio file");
|
||||||
|
remainingAudio.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(iex: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to delete files after failure:\n${iex.message}", iex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun downloadHlsSource(context: Context, name: String, client: ManagedHttpClient, hlsUrl: String, targetFile: File, onProgress: (Long, Long, Long) -> Unit): Long {
|
private suspend fun downloadHlsSource(context: Context, name: String, client: ManagedHttpClient, hlsUrl: String, targetFile: File, onProgress: (Long, Long, Long) -> Unit): Long {
|
||||||
@@ -730,6 +755,7 @@ class VideoDownload {
|
|||||||
companion object {
|
companion object {
|
||||||
const val TAG = "VideoDownload";
|
const val TAG = "VideoDownload";
|
||||||
const val GROUP_PLAYLIST = "Playlist";
|
const val GROUP_PLAYLIST = "Playlist";
|
||||||
|
const val GROUP_WATCHLATER= "WatchLater";
|
||||||
|
|
||||||
fun videoContainerToExtension(container: String): String? {
|
fun videoContainerToExtension(container: String): String? {
|
||||||
if (container.contains("video/mp4") || container == "application/vnd.apple.mpegurl")
|
if (container.contains("video/mp4") || container == "application/vnd.apple.mpegurl")
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import androidx.documentfile.provider.DocumentFile
|
|||||||
import com.arthenica.ffmpegkit.*
|
import com.arthenica.ffmpegkit.*
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.*
|
import com.futo.platformplayer.api.media.models.streams.sources.*
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.helpers.FileHelper.Companion.sanitizeFileName
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.toHumanBitrate
|
import com.futo.platformplayer.toHumanBitrate
|
||||||
@@ -63,7 +64,7 @@ class VideoExport {
|
|||||||
val outputFile: DocumentFile?;
|
val outputFile: DocumentFile?;
|
||||||
val downloadRoot = StateApp.instance.getExternalDownloadDirectory(context) ?: throw Exception("External download directory is not set");
|
val downloadRoot = StateApp.instance.getExternalDownloadDirectory(context) ?: throw Exception("External download directory is not set");
|
||||||
if (sourceCount > 1) {
|
if (sourceCount > 1) {
|
||||||
val outputFileName = toSafeFileName(videoLocal.name) + ".mp4"// + VideoDownload.videoContainerToExtension(v.container);
|
val outputFileName = videoLocal.name.sanitizeFileName(true) + ".mp4"// + VideoDownload.videoContainerToExtension(v.container);
|
||||||
val f = downloadRoot.createFile("video/mp4", outputFileName)
|
val f = downloadRoot.createFile("video/mp4", outputFileName)
|
||||||
?: throw Exception("Failed to create file in external directory.");
|
?: throw Exception("Failed to create file in external directory.");
|
||||||
|
|
||||||
@@ -79,7 +80,7 @@ class VideoExport {
|
|||||||
}
|
}
|
||||||
outputFile = f;
|
outputFile = f;
|
||||||
} else if (v != null) {
|
} else if (v != null) {
|
||||||
val outputFileName = toSafeFileName(videoLocal.name) + "." + VideoDownload.videoContainerToExtension(v.container);
|
val outputFileName = videoLocal.name.sanitizeFileName(true) + "." + VideoDownload.videoContainerToExtension(v.container);
|
||||||
val f = downloadRoot.createFile(v.container, outputFileName)
|
val f = downloadRoot.createFile(v.container, outputFileName)
|
||||||
?: throw Exception("Failed to create file in external directory.");
|
?: throw Exception("Failed to create file in external directory.");
|
||||||
|
|
||||||
@@ -91,7 +92,7 @@ class VideoExport {
|
|||||||
|
|
||||||
outputFile = f;
|
outputFile = f;
|
||||||
} else if (a != null) {
|
} else if (a != null) {
|
||||||
val outputFileName = toSafeFileName(videoLocal.name) + "." + VideoDownload.audioContainerToExtension(a.container);
|
val outputFileName = videoLocal.name.sanitizeFileName(true) + "." + VideoDownload.audioContainerToExtension(a.container);
|
||||||
val f = downloadRoot.createFile(a.container, outputFileName)
|
val f = downloadRoot.createFile(a.container, outputFileName)
|
||||||
?: throw Exception("Failed to create file in external directory.");
|
?: throw Exception("Failed to create file in external directory.");
|
||||||
|
|
||||||
@@ -110,11 +111,6 @@ class VideoExport {
|
|||||||
return@coroutineScope outputFile;
|
return@coroutineScope outputFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun toSafeFileName(input: String): String {
|
|
||||||
val safeCharacters = ('a'..'z') + ('A'..'Z') + ('0'..'9') + listOf('-', '_')
|
|
||||||
return input.map { if (it in safeCharacters) it else '_' }.joinToString(separator = "")
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun combine(inputPathAudio: String?, inputPathVideo: String?, inputPathSubtitles: String?, outputPath: String, duration: Double, onProgress: ((Double) -> Unit)? = null) = withContext(Dispatchers.IO) {
|
private suspend fun combine(inputPathAudio: String?, inputPathVideo: String?, inputPathSubtitles: String?, outputPath: String, duration: Double, onProgress: ((Double) -> Unit)? = null) = withContext(Dispatchers.IO) {
|
||||||
suspendCancellableCoroutine { continuation ->
|
suspendCancellableCoroutine { continuation ->
|
||||||
//ffmpeg -i a.mp4 -i b.m4a -scodec mov_text -i c.vtt -map 0:v -map 1:a -map 2 -c:v copy -c:a copy -c:s mov_text output.mp4
|
//ffmpeg -i a.mp4 -i b.m4a -scodec mov_text -i c.vtt -map 0:v -map 1:a -map 2 -c:v copy -c:a copy -c:s mov_text output.mp4
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.futo.platformplayer.engine
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.caoccao.javet.exceptions.JavetCompilationException
|
import com.caoccao.javet.exceptions.JavetCompilationException
|
||||||
|
import com.caoccao.javet.exceptions.JavetException
|
||||||
import com.caoccao.javet.exceptions.JavetExecutionException
|
import com.caoccao.javet.exceptions.JavetExecutionException
|
||||||
import com.caoccao.javet.interop.V8Host
|
import com.caoccao.javet.interop.V8Host
|
||||||
import com.caoccao.javet.interop.V8Runtime
|
import com.caoccao.javet.interop.V8Runtime
|
||||||
@@ -43,7 +44,6 @@ class V8Plugin {
|
|||||||
private val _clientAuth: ManagedHttpClient;
|
private val _clientAuth: ManagedHttpClient;
|
||||||
private val _clientOthers: ConcurrentHashMap<String, JSHttpClient> = ConcurrentHashMap();
|
private val _clientOthers: ConcurrentHashMap<String, JSHttpClient> = ConcurrentHashMap();
|
||||||
|
|
||||||
|
|
||||||
val httpClient: ManagedHttpClient get() = _client;
|
val httpClient: ManagedHttpClient get() = _client;
|
||||||
val httpClientAuth: ManagedHttpClient get() = _clientAuth;
|
val httpClientAuth: ManagedHttpClient get() = _clientAuth;
|
||||||
val httpClientOthers: Map<String, JSHttpClient> get() = _clientOthers;
|
val httpClientOthers: Map<String, JSHttpClient> get() = _clientOthers;
|
||||||
@@ -69,6 +69,11 @@ class V8Plugin {
|
|||||||
private var _busyCounter = 0;
|
private var _busyCounter = 0;
|
||||||
val isBusy get() = synchronized(_busyCounterLock) { _busyCounter > 0 };
|
val isBusy get() = synchronized(_busyCounterLock) { _busyCounter > 0 };
|
||||||
|
|
||||||
|
var allowDevSubmit: Boolean = false
|
||||||
|
private set(value) {
|
||||||
|
field = value;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called before a busy counter is about to be removed.
|
* Called before a busy counter is about to be removed.
|
||||||
* Is primarily used to prevent additional calls to dead runtimes.
|
* Is primarily used to prevent additional calls to dead runtimes.
|
||||||
@@ -90,6 +95,10 @@ class V8Plugin {
|
|||||||
withDependency(getPackage(pack));
|
withDependency(getPackage(pack));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun changeAllowDevSubmit(isAllowed: Boolean) {
|
||||||
|
allowDevSubmit = isAllowed;
|
||||||
|
}
|
||||||
|
|
||||||
fun withDependency(context: Context, assetPath: String) : V8Plugin {
|
fun withDependency(context: Context, assetPath: String) : V8Plugin {
|
||||||
if(!_deps.containsKey(assetPath))
|
if(!_deps.containsKey(assetPath))
|
||||||
_deps.put(assetPath, getAssetFile(context, assetPath));
|
_deps.put(assetPath, getAssetFile(context, assetPath));
|
||||||
@@ -173,8 +182,16 @@ class V8Plugin {
|
|||||||
isStopped = true;
|
isStopped = true;
|
||||||
_runtime?.let {
|
_runtime?.let {
|
||||||
_runtime = null;
|
_runtime = null;
|
||||||
if(!it.isClosed && !it.isDead)
|
if(!it.isClosed && !it.isDead) {
|
||||||
it.close();
|
try {
|
||||||
|
it.close();
|
||||||
|
}
|
||||||
|
catch(ex: JavetException) {
|
||||||
|
//In case race conditions are going on, already closed runtimes are fine.
|
||||||
|
if(ex.message?.contains("Runtime is already closed") != true)
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
}
|
||||||
Logger.i(TAG, "Stopped plugin [${config.name}]");
|
Logger.i(TAG, "Stopped plugin [${config.name}]");
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import com.futo.platformplayer.logging.Logger
|
|||||||
import com.futo.platformplayer.states.StateDeveloper
|
import com.futo.platformplayer.states.StateDeveloper
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
|
import com.futo.platformplayer.api.media.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.internal.JSHttpClient
|
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
|
||||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
@@ -12,6 +13,9 @@ import com.futo.platformplayer.engine.V8Plugin
|
|||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
class PackageBridge : V8Package {
|
class PackageBridge : V8Package {
|
||||||
@Transient
|
@Transient
|
||||||
@@ -21,6 +25,7 @@ class PackageBridge : V8Package {
|
|||||||
@Transient
|
@Transient
|
||||||
private val _clientAuth: ManagedHttpClient
|
private val _clientAuth: ManagedHttpClient
|
||||||
|
|
||||||
|
|
||||||
override val name: String get() = "Bridge";
|
override val name: String get() = "Bridge";
|
||||||
override val variableName: String get() = "bridge";
|
override val variableName: String get() = "bridge";
|
||||||
|
|
||||||
@@ -47,6 +52,44 @@ class PackageBridge : V8Package {
|
|||||||
StateDeveloper.instance.logDevInfo(StateDeveloper.instance.currentDevID ?: "", str ?: "null");
|
StateDeveloper.instance.logDevInfo(StateDeveloper.instance.currentDevID ?: "", str ?: "null");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val _jsonSerializer = Json { this.prettyPrintIndent = " "; this.prettyPrint = true; };
|
||||||
|
private var _devSubmitClient: ManagedHttpClient? = null;
|
||||||
|
@V8Function
|
||||||
|
fun devSubmit(label: String, data: String) {
|
||||||
|
if(_plugin.config !is SourcePluginConfig)
|
||||||
|
return;
|
||||||
|
if(!_plugin.allowDevSubmit)
|
||||||
|
return;
|
||||||
|
val devUrl = _plugin.config.developerSubmitUrl ?: return;
|
||||||
|
if(_devSubmitClient == null)
|
||||||
|
_devSubmitClient = ManagedHttpClient();
|
||||||
|
|
||||||
|
val stackTrace = Thread.currentThread().stackTrace;
|
||||||
|
val callerMethod = stackTrace.findLast {
|
||||||
|
it.className == JSClient::class.java.name
|
||||||
|
}?.methodName ?: "";
|
||||||
|
val session = StateApp.instance.sessionId;
|
||||||
|
val pluginId = _plugin.config.id;
|
||||||
|
val pluginVersion = _plugin.config.version;
|
||||||
|
|
||||||
|
val obj = DevSubmitData(pluginId, pluginVersion, callerMethod, session, label, data);
|
||||||
|
|
||||||
|
UIDialogs.toast("DevSubmit [${callerMethod}] (${_plugin.config.name})", false);
|
||||||
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val json = _jsonSerializer.encodeToString(obj);
|
||||||
|
Logger.i(TAG, "DevSubmit [${callerMethod}] - ${devUrl}\n" + json);
|
||||||
|
val resp = _devSubmitClient?.post(devUrl, json, mutableMapOf(Pair("Content-Type", "application/json")));
|
||||||
|
Logger.i(TAG, "DevSubmit [${callerMethod}] - ${devUrl} Status: " + (resp?.code?.toString() ?: "-1"))
|
||||||
|
}
|
||||||
|
catch(ex: Exception) {
|
||||||
|
Logger.e(TAG, "DevSubmission to [${devUrl}] failed due to:\n" + ex.message, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Serializable
|
||||||
|
class DevSubmitData(val pluginId: String, val pluginVersion: Int, val caller: String, val session: String, val label: String, val data: String)
|
||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
fun throwTest(str: String) {
|
fun throwTest(str: String) {
|
||||||
throw IllegalStateException(str);
|
throw IllegalStateException(str);
|
||||||
|
|||||||
@@ -5,8 +5,11 @@ import com.caoccao.javet.annotations.V8Function
|
|||||||
import com.caoccao.javet.annotations.V8Property
|
import com.caoccao.javet.annotations.V8Property
|
||||||
import com.caoccao.javet.enums.V8ConversionMode
|
import com.caoccao.javet.enums.V8ConversionMode
|
||||||
import com.caoccao.javet.enums.V8ProxyMode
|
import com.caoccao.javet.enums.V8ProxyMode
|
||||||
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
import com.futo.platformplayer.engine.V8Plugin
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
import com.futo.platformplayer.engine.internal.V8BindObject
|
import com.futo.platformplayer.engine.internal.V8BindObject
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
|
|
||||||
@@ -65,7 +68,7 @@ class PackageDOMParser : V8Package {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@V8Property
|
@V8Property
|
||||||
fun attributes(): Map<String, String> = _element.attributes().dataset();
|
fun attributes(): Map<String, String> = _element.attributes().associate { Pair(it.key, it.value) }
|
||||||
@V8Property
|
@V8Property
|
||||||
fun innerHTML(): String = _element.html();
|
fun innerHTML(): String = _element.html();
|
||||||
@V8Property
|
@V8Property
|
||||||
@@ -138,10 +141,32 @@ class PackageDOMParser : V8Package {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun toNodeTree(): SerializedNode {
|
||||||
|
return SerializedNode(
|
||||||
|
childNodes().map { it.toNodeTree() },
|
||||||
|
_element.tagName(),
|
||||||
|
_element.text(),
|
||||||
|
attributes()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@V8Function
|
||||||
|
fun toNodeTreeJson(): String {
|
||||||
|
return Json.encodeToString(SerializedNode.serializer(), toNodeTree());
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun parse(parser: PackageDOMParser, str: String): DOMNode {
|
fun parse(parser: PackageDOMParser, str: String): DOMNode {
|
||||||
return DOMNode(parser, Jsoup.parse(str));
|
return DOMNode(parser, Jsoup.parse(str));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class SerializedNode(
|
||||||
|
val children: List<SerializedNode>,
|
||||||
|
val name: String,
|
||||||
|
val value: String,
|
||||||
|
val attributes: Map<String, String>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,7 @@ import com.caoccao.javet.interop.V8Runtime
|
|||||||
import com.caoccao.javet.values.V8Value
|
import com.caoccao.javet.values.V8Value
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
import com.futo.platformplayer.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.internal.JSHttpClient
|
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
|
||||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
import com.futo.platformplayer.engine.V8Plugin
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
@@ -242,7 +243,8 @@ class PackageHttp: V8Package {
|
|||||||
val resp = client.requestMethod(method, url, headers);
|
val resp = client.requestMethod(method, url, headers);
|
||||||
val responseBody = resp.body?.string();
|
val responseBody = resp.body?.string();
|
||||||
//logResponse(method, url, resp.code, resp.headers, responseBody);
|
//logResponse(method, url, resp.code, resp.headers, responseBody);
|
||||||
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers));
|
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers,
|
||||||
|
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -256,7 +258,8 @@ class PackageHttp: V8Package {
|
|||||||
val resp = client.requestMethod(method, url, body, headers);
|
val resp = client.requestMethod(method, url, body, headers);
|
||||||
val responseBody = resp.body?.string();
|
val responseBody = resp.body?.string();
|
||||||
//logResponse(method, url, resp.code, resp.headers, responseBody);
|
//logResponse(method, url, resp.code, resp.headers, responseBody);
|
||||||
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers));
|
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers,
|
||||||
|
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -271,7 +274,8 @@ class PackageHttp: V8Package {
|
|||||||
val resp = client.get(url, headers);
|
val resp = client.get(url, headers);
|
||||||
val responseBody = resp.body?.string();
|
val responseBody = resp.body?.string();
|
||||||
//logResponse("GET", url, resp.code, resp.headers, responseBody);
|
//logResponse("GET", url, resp.code, resp.headers, responseBody);
|
||||||
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers));
|
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers,
|
||||||
|
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -285,7 +289,8 @@ class PackageHttp: V8Package {
|
|||||||
val resp = client.post(url, body, headers);
|
val resp = client.post(url, body, headers);
|
||||||
val responseBody = resp.body?.string();
|
val responseBody = resp.body?.string();
|
||||||
//logResponse("POST", url, resp.code, resp.headers, responseBody);
|
//logResponse("POST", url, resp.code, resp.headers, responseBody);
|
||||||
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers));
|
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers,
|
||||||
|
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -305,18 +310,31 @@ class PackageHttp: V8Package {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sanitizeResponseHeaders(headers: Map<String, List<String>>?): Map<String, List<String>> {
|
private fun sanitizeResponseHeaders(headers: Map<String, List<String>>?, onlyWhitelisted: Boolean = false): Map<String, List<String>> {
|
||||||
val result = mutableMapOf<String, List<String>>()
|
val result = mutableMapOf<String, List<String>>()
|
||||||
headers?.forEach { (header, values) ->
|
if(onlyWhitelisted)
|
||||||
val lowerCaseHeader = header.lowercase()
|
headers?.forEach { (header, values) ->
|
||||||
if (WHITELISTED_RESPONSE_HEADERS.contains(lowerCaseHeader)) {
|
val lowerCaseHeader = header.lowercase()
|
||||||
result[lowerCaseHeader] = values
|
if (WHITELISTED_RESPONSE_HEADERS.contains(lowerCaseHeader)) {
|
||||||
|
result[lowerCaseHeader] = values
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
headers?.forEach { (header, values) ->
|
||||||
|
val lowerCaseHeader = header.lowercase()
|
||||||
|
if(lowerCaseHeader == "set-cookie") {
|
||||||
|
result[lowerCaseHeader] = values.filter{
|
||||||
|
!it.lowercase().contains("httponly")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
result[lowerCaseHeader] = values;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
/*private fun logRequest(method: String, url: String, headers: Map<String, String> = HashMap(), body: String?) {
|
private fun logRequest(method: String, url: String, headers: Map<String, String> = HashMap(), body: String?) {
|
||||||
Logger.v(TAG) {
|
Logger.v(TAG) {
|
||||||
val stringBuilder = StringBuilder();
|
val stringBuilder = StringBuilder();
|
||||||
stringBuilder.appendLine("HTTP request (useAuth = )");
|
stringBuilder.appendLine("HTTP request (useAuth = )");
|
||||||
@@ -333,7 +351,7 @@ class PackageHttp: V8Package {
|
|||||||
|
|
||||||
return@v stringBuilder.toString();
|
return@v stringBuilder.toString();
|
||||||
};
|
};
|
||||||
}*/
|
}
|
||||||
|
|
||||||
/*private fun logResponse(method: String, url: String, responseCode: Int? = null, responseHeaders: Map<String, List<String>> = HashMap(), responseBody: String? = null) {
|
/*private fun logResponse(method: String, url: String, responseCode: Int? = null, responseHeaders: Map<String, List<String>> = HashMap(), responseBody: String? = null) {
|
||||||
Logger.v(TAG) {
|
Logger.v(TAG) {
|
||||||
|
|||||||
@@ -4,8 +4,11 @@ import android.util.Base64
|
|||||||
import com.caoccao.javet.annotations.V8Function
|
import com.caoccao.javet.annotations.V8Function
|
||||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
import com.futo.platformplayer.engine.V8Plugin
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
|
import com.google.common.hash.Hashing.md5
|
||||||
|
import java.security.MessageDigest
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
|
|
||||||
class PackageUtilities : V8Package {
|
class PackageUtilities : V8Package {
|
||||||
@Transient
|
@Transient
|
||||||
private val _config: IV8PluginConfig;
|
private val _config: IV8PluginConfig;
|
||||||
@@ -19,7 +22,31 @@ class PackageUtilities : V8Package {
|
|||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
fun toBase64(arr: ByteArray): String {
|
fun toBase64(arr: ByteArray): String {
|
||||||
return Base64.encodeToString(arr, Base64.NO_WRAP);
|
return Base64.encodeToString(arr, Base64.NO_PADDING or Base64.NO_WRAP);
|
||||||
|
}
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun fromBase64(str: String): ByteArray {
|
||||||
|
return Base64.decode(str, Base64.NO_PADDING or Base64.NO_WRAP)
|
||||||
|
}
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun md5(arr: ByteArray): ByteArray {
|
||||||
|
return MessageDigest.getInstance("MD5").digest(arr);
|
||||||
|
}
|
||||||
|
@V8Function
|
||||||
|
fun md5String(str: String): String {
|
||||||
|
return md5(str.toByteArray(Charsets.UTF_8)).fold("") { str, it -> str + "%02x".format(it) };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun sha256(arr: ByteArray): ByteArray {
|
||||||
|
return MessageDigest.getInstance("SHA-256").digest(arr);
|
||||||
|
}
|
||||||
|
@V8Function
|
||||||
|
fun sha256String(str: String): String {
|
||||||
|
return sha256(str.toByteArray(Charsets.UTF_8)).fold("") { str, it -> str + "%02x".format(it) };
|
||||||
}
|
}
|
||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
|
|||||||
+11
-4
@@ -27,6 +27,7 @@ import com.futo.platformplayer.constructs.Event2
|
|||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
import com.futo.platformplayer.engine.exceptions.PluginException
|
import com.futo.platformplayer.engine.exceptions.PluginException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||||
|
import com.futo.platformplayer.exceptions.ChannelException
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.FeedView
|
import com.futo.platformplayer.fragment.mainactivity.main.FeedView
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
@@ -59,8 +60,10 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
|||||||
val onChannelClicked = Event1<PlatformAuthorLink>();
|
val onChannelClicked = Event1<PlatformAuthorLink>();
|
||||||
val onAddToClicked = Event1<IPlatformContent>();
|
val onAddToClicked = Event1<IPlatformContent>();
|
||||||
val onAddToQueueClicked = Event1<IPlatformContent>();
|
val onAddToQueueClicked = Event1<IPlatformContent>();
|
||||||
|
val onAddToWatchLaterClicked = Event1<IPlatformContent>();
|
||||||
val onLongPress = Event1<IPlatformContent>();
|
val onLongPress = Event1<IPlatformContent>();
|
||||||
|
|
||||||
|
|
||||||
private fun getContentPager(channel: IPlatformChannel): IPager<IPlatformContent> {
|
private fun getContentPager(channel: IPlatformChannel): IPager<IPlatformContent> {
|
||||||
Logger.i(TAG, "getContentPager");
|
Logger.i(TAG, "getContentPager");
|
||||||
|
|
||||||
@@ -102,9 +105,9 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
|||||||
}).success {
|
}).success {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
val posBefore = _results.size;
|
val posBefore = _results.size;
|
||||||
val toAdd = it.filter { it is IPlatformVideo }.map { it as IPlatformVideo }
|
//val toAdd = it.filter { it is IPlatformVideo }.map { it as IPlatformVideo }
|
||||||
_results.addAll(toAdd);
|
_results.addAll(it);
|
||||||
_adapterResults?.let { adapterVideo -> adapterVideo.notifyItemRangeInserted(adapterVideo.childToParentPosition(posBefore), toAdd.size); };
|
_adapterResults?.let { adapterVideo -> adapterVideo.notifyItemRangeInserted(adapterVideo.childToParentPosition(posBefore), it.size); };
|
||||||
}.exception<Throwable> {
|
}.exception<Throwable> {
|
||||||
Logger.w(TAG, "Failed to load next page.", it);
|
Logger.w(TAG, "Failed to load next page.", it);
|
||||||
UIDialogs.showGeneralRetryErrorDialog(requireContext(), it.message ?: "", it, { loadNextPage() });
|
UIDialogs.showGeneralRetryErrorDialog(requireContext(), it.message ?: "", it, { loadNextPage() });
|
||||||
@@ -156,6 +159,7 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
|||||||
this.onChannelClicked.subscribe(this@ChannelContentsFragment.onChannelClicked::emit);
|
this.onChannelClicked.subscribe(this@ChannelContentsFragment.onChannelClicked::emit);
|
||||||
this.onAddToClicked.subscribe(this@ChannelContentsFragment.onAddToClicked::emit);
|
this.onAddToClicked.subscribe(this@ChannelContentsFragment.onAddToClicked::emit);
|
||||||
this.onAddToQueueClicked.subscribe(this@ChannelContentsFragment.onAddToQueueClicked::emit);
|
this.onAddToQueueClicked.subscribe(this@ChannelContentsFragment.onAddToQueueClicked::emit);
|
||||||
|
this.onAddToWatchLaterClicked.subscribe(this@ChannelContentsFragment.onAddToWatchLaterClicked::emit);
|
||||||
this.onLongPress.subscribe(this@ChannelContentsFragment.onLongPress::emit);
|
this.onLongPress.subscribe(this@ChannelContentsFragment.onLongPress::emit);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,8 +340,11 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
|||||||
context?.let {
|
context?.let {
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
try {
|
try {
|
||||||
|
val channel = if(kv.value is ChannelException) (kv.value as ChannelException).channelNameOrUrl else null;
|
||||||
if(jsVideoPager != null)
|
if(jsVideoPager != null)
|
||||||
UIDialogs.toast(it, "Plugin ${jsVideoPager.getPluginConfig().name} failed:\n${kv.value.message}", false);
|
UIDialogs.toast(it, "Plugin ${jsVideoPager.getPluginConfig().name} failed:\n" +
|
||||||
|
(if(!channel.isNullOrEmpty()) "(${channel}) " else "") +
|
||||||
|
"${kv.value.message}", false);
|
||||||
else
|
else
|
||||||
UIDialogs.toast(it, kv.value.message ?: "", false);
|
UIDialogs.toast(it, kv.value.message ?: "", false);
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
|
|||||||
+18
-7
@@ -247,11 +247,14 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
val defs = currentButtonDefinitions?.toMutableList() ?: return
|
val defs = currentButtonDefinitions?.toMutableList() ?: return
|
||||||
val metrics = StateApp.instance.displayMetrics ?: resources.displayMetrics;
|
val metrics = StateApp.instance.displayMetrics ?: resources.displayMetrics;
|
||||||
_buttonsVisible = floor(metrics.widthPixels.toDouble() / 65.dp(resources).toDouble()).roundToInt();
|
_buttonsVisible = floor(metrics.widthPixels.toDouble() / 65.dp(resources).toDouble()).roundToInt();
|
||||||
if (_buttonsVisible - 1 >= defs.size) {
|
if (_buttonsVisible >= defs.size) {
|
||||||
updateBottomMenuButtons(defs.toMutableList(), false);
|
updateBottomMenuButtons(defs.toMutableList(), false);
|
||||||
|
} else if (_buttonsVisible > 0) {
|
||||||
|
updateBottomMenuButtons(defs.take(_buttonsVisible - 1).toMutableList(), true);
|
||||||
|
updateMoreButtons(defs.drop(_buttonsVisible - 1).toMutableList());
|
||||||
} else {
|
} else {
|
||||||
updateBottomMenuButtons(defs.slice(IntRange(0, _buttonsVisible - 2)).toMutableList(), true);
|
updateBottomMenuButtons(mutableListOf(), false)
|
||||||
updateMoreButtons(defs.slice(IntRange(_buttonsVisible - 1, defs.size - 1)).toMutableList());
|
updateMoreButtons(defs.toMutableList())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,10 +292,17 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
buttonDefinitions.find { d -> d.id == it.id }
|
buttonDefinitions.find { d -> d.id == it.id }
|
||||||
}.toMutableList()
|
}.toMutableList()
|
||||||
|
|
||||||
if (!StatePayment.instance.hasPaid) {
|
//Add unconfigured tabs with default values
|
||||||
newCurrentButtonDefinitions.add(ButtonDefinition(98, R.drawable.ic_paid, R.drawable.ic_paid, R.string.buy, canToggle = false, { it.currentMain is BuyFragment }, { it.navigate<BuyFragment>() }))
|
buttonDefinitions.forEach { buttonDefinition ->
|
||||||
|
if (!Settings.instance.tabs.any { it.id == buttonDefinition.id }) {
|
||||||
|
newCurrentButtonDefinitions.add(buttonDefinition)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
newCurrentButtonDefinitions.add(ButtonDefinition(97, R.drawable.ic_quiz, R.drawable.ic_quiz, R.string.faq, canToggle = false, { false }, {
|
|
||||||
|
if (!StatePayment.instance.hasPaid) {
|
||||||
|
newCurrentButtonDefinitions.add(ButtonDefinition(98, R.drawable.ic_paid, R.drawable.ic_paid_filled, R.string.buy, canToggle = false, { it.currentMain is BuyFragment }, { it.navigate<BuyFragment>() }))
|
||||||
|
}
|
||||||
|
newCurrentButtonDefinitions.add(ButtonDefinition(97, R.drawable.ic_quiz, R.drawable.ic_quiz_fill, R.string.faq, canToggle = false, { false }, {
|
||||||
it.navigate<BrowserFragment>(Settings.URL_FAQ);
|
it.navigate<BrowserFragment>(Settings.URL_FAQ);
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -349,7 +359,8 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
ButtonDefinition(6, R.drawable.ic_download, R.drawable.ic_download, R.string.downloads, canToggle = false, { it.currentMain is DownloadsFragment }, { it.navigate<DownloadsFragment>() }),
|
ButtonDefinition(6, R.drawable.ic_download, R.drawable.ic_download, R.string.downloads, canToggle = false, { it.currentMain is DownloadsFragment }, { it.navigate<DownloadsFragment>() }),
|
||||||
ButtonDefinition(8, R.drawable.ic_chat, R.drawable.ic_chat_filled, R.string.comments, canToggle = true, { it.currentMain is CommentsFragment }, { it.navigate<CommentsFragment>() }),
|
ButtonDefinition(8, R.drawable.ic_chat, R.drawable.ic_chat_filled, R.string.comments, canToggle = true, { it.currentMain is CommentsFragment }, { it.navigate<CommentsFragment>() }),
|
||||||
ButtonDefinition(9, R.drawable.ic_subscriptions, R.drawable.ic_subscriptions_filled, R.string.subscription_group_menu, canToggle = true, { it.currentMain is SubscriptionGroupListFragment }, { it.navigate<SubscriptionGroupListFragment>() }),
|
ButtonDefinition(9, R.drawable.ic_subscriptions, R.drawable.ic_subscriptions_filled, R.string.subscription_group_menu, canToggle = true, { it.currentMain is SubscriptionGroupListFragment }, { it.navigate<SubscriptionGroupListFragment>() }),
|
||||||
ButtonDefinition(7, R.drawable.ic_settings, R.drawable.ic_settings, R.string.settings, canToggle = false, { false }, {
|
ButtonDefinition(10, R.drawable.ic_help_square, R.drawable.ic_help_square_fill, R.string.tutorials, canToggle = true, { it.currentMain is TutorialFragment }, { it.navigate<TutorialFragment>() }),
|
||||||
|
ButtonDefinition(7, R.drawable.ic_settings, R.drawable.ic_settings_filled, R.string.settings, canToggle = false, { false }, {
|
||||||
val c = it.context ?: return@ButtonDefinition;
|
val c = it.context ?: return@ButtonDefinition;
|
||||||
Logger.i(TAG, "settings preventPictureInPicture()");
|
Logger.i(TAG, "settings preventPictureInPicture()");
|
||||||
it.requireFragment<VideoDetailFragment>().preventPictureInPicture();
|
it.requireFragment<VideoDetailFragment>().preventPictureInPicture();
|
||||||
|
|||||||
+31
-6
@@ -22,15 +22,17 @@ class BrowserFragment : MainFragment() {
|
|||||||
override val hasBottomBar: Boolean get() = true;
|
override val hasBottomBar: Boolean get() = true;
|
||||||
|
|
||||||
private var _webview: WebView? = null;
|
private var _webview: WebView? = null;
|
||||||
|
private val _webviewWithoutHandling = object: WebViewClient() {
|
||||||
|
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
val view = inflater.inflate(R.layout.fragment_browser, container, false);
|
val view = inflater.inflate(R.layout.fragment_browser, container, false);
|
||||||
_webview = view.findViewById<WebView?>(R.id.webview).apply {
|
_webview = view.findViewById<WebView?>(R.id.webview).apply {
|
||||||
this.webViewClient = object: WebViewClient() {
|
this.webViewClient = _webviewWithoutHandling;
|
||||||
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this.settings.javaScriptEnabled = true;
|
this.settings.javaScriptEnabled = true;
|
||||||
CookieManager.getInstance().setAcceptCookie(true);
|
CookieManager.getInstance().setAcceptCookie(true);
|
||||||
this.settings.domStorageEnabled = true;
|
this.settings.domStorageEnabled = true;
|
||||||
@@ -41,8 +43,26 @@ class BrowserFragment : MainFragment() {
|
|||||||
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||||
super.onShownWithView(parameter, isBack)
|
super.onShownWithView(parameter, isBack)
|
||||||
|
|
||||||
if(parameter is String)
|
if(parameter is String) {
|
||||||
|
_webview?.webViewClient = _webviewWithoutHandling;
|
||||||
_webview?.loadUrl(parameter);
|
_webview?.loadUrl(parameter);
|
||||||
|
}
|
||||||
|
else if(parameter is NavigateOptions) {
|
||||||
|
if(parameter.urlHandlers != null && parameter.urlHandlers.isNotEmpty())
|
||||||
|
_webview?.webViewClient = object: WebViewClient() {
|
||||||
|
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
|
||||||
|
val schema = request?.url?.scheme;
|
||||||
|
if(schema != null && parameter.urlHandlers.containsKey(schema)) {
|
||||||
|
parameter.urlHandlers[schema]?.invoke(request);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
else
|
||||||
|
_webview?.webViewClient = _webviewWithoutHandling;
|
||||||
|
_webview?.loadUrl(parameter.url);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
_webview?.loadUrl("about:blank");
|
_webview?.loadUrl("about:blank");
|
||||||
}
|
}
|
||||||
@@ -59,4 +79,9 @@ class BrowserFragment : MainFragment() {
|
|||||||
companion object {
|
companion object {
|
||||||
fun newInstance() = BrowserFragment().apply {}
|
fun newInstance() = BrowserFragment().apply {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class NavigateOptions(
|
||||||
|
val url: String,
|
||||||
|
val urlHandlers: Map<String, (WebResourceRequest)->Unit>? = null
|
||||||
|
)
|
||||||
}
|
}
|
||||||
+11
-3
@@ -26,6 +26,7 @@ import com.futo.platformplayer.api.media.models.contents.ContentType
|
|||||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
||||||
import com.futo.platformplayer.api.media.models.post.IPlatformPost
|
import com.futo.platformplayer.api.media.models.post.IPlatformPost
|
||||||
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.SerializedPlatformVideo
|
||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
import com.futo.platformplayer.fragment.channel.tab.ChannelAboutFragment
|
import com.futo.platformplayer.fragment.channel.tab.ChannelAboutFragment
|
||||||
import com.futo.platformplayer.fragment.channel.tab.ChannelContentsFragment
|
import com.futo.platformplayer.fragment.channel.tab.ChannelContentsFragment
|
||||||
@@ -151,7 +152,7 @@ class ChannelFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
Logger.e(TAG, "Failed to load channel.", it);
|
Logger.e(TAG, "Failed to load channel.", it);
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadChannel() });
|
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadChannel() }, null, fragment);
|
||||||
}
|
}
|
||||||
|
|
||||||
val tabs: TabLayout = findViewById(R.id.tabs);
|
val tabs: TabLayout = findViewById(R.id.tabs);
|
||||||
@@ -206,6 +207,12 @@ class ChannelFragment : MainFragment() {
|
|||||||
StatePlayer.instance.addToQueue(content);
|
StatePlayer.instance.addToQueue(content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
adapter.onAddToWatchLaterClicked.subscribe { content ->
|
||||||
|
if(content is IPlatformVideo) {
|
||||||
|
StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(content));
|
||||||
|
UIDialogs.toast("Added to watch later\n[${content.name}]");
|
||||||
|
}
|
||||||
|
}
|
||||||
adapter.onUrlClicked.subscribe { url ->
|
adapter.onUrlClicked.subscribe { url ->
|
||||||
fragment.navigate<BrowserFragment>(url);
|
fragment.navigate<BrowserFragment>(url);
|
||||||
}
|
}
|
||||||
@@ -264,7 +271,7 @@ class ChannelFragment : MainFragment() {
|
|||||||
_taskLoadPolycentricProfile.cancel();
|
_taskLoadPolycentricProfile.cancel();
|
||||||
_selectedTabIndex = -1;
|
_selectedTabIndex = -1;
|
||||||
|
|
||||||
if (!isBack) {
|
if (!isBack || _url == null) {
|
||||||
_imageBanner.setImageDrawable(null);
|
_imageBanner.setImageDrawable(null);
|
||||||
|
|
||||||
if (parameter is String) {
|
if (parameter is String) {
|
||||||
@@ -418,6 +425,7 @@ class ChannelFragment : MainFragment() {
|
|||||||
|
|
||||||
_buttonSubscribe.setSubscribeChannel(channel);
|
_buttonSubscribe.setSubscribeChannel(channel);
|
||||||
_buttonSubscriptionSettings.visibility = if(_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE;
|
_buttonSubscriptionSettings.visibility = if(_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE;
|
||||||
|
_textChannel.text = channel.name;
|
||||||
_textChannelSub.text = if(channel.subscribers > 0) "${channel.subscribers.toHumanNumber()} " + context.getString(R.string.subscribers).lowercase() else "";
|
_textChannelSub.text = if(channel.subscribers > 0) "${channel.subscribers.toHumanNumber()} " + context.getString(R.string.subscribers).lowercase() else "";
|
||||||
|
|
||||||
//TODO: Find a better way to access the adapter fragments..
|
//TODO: Find a better way to access the adapter fragments..
|
||||||
@@ -465,7 +473,7 @@ class ChannelFragment : MainFragment() {
|
|||||||
_creatorThumbnail.setThumbnail(avatar, animate);
|
_creatorThumbnail.setThumbnail(avatar, animate);
|
||||||
} else {
|
} else {
|
||||||
_creatorThumbnail.setThumbnail(channel?.thumbnail, animate);
|
_creatorThumbnail.setThumbnail(channel?.thumbnail, animate);
|
||||||
_creatorThumbnail.setHarborAvailable(profile != null, animate);
|
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
||||||
}
|
}
|
||||||
|
|
||||||
val banner = profile?.systemState?.banner?.selectHighestResolutionImage()
|
val banner = profile?.systemState?.banner?.selectHighestResolutionImage()
|
||||||
|
|||||||
+28
@@ -1,7 +1,9 @@
|
|||||||
package com.futo.platformplayer.fragment.mainactivity.main
|
package com.futo.platformplayer.fragment.mainactivity.main
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.provider.Browser
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
@@ -117,6 +119,8 @@ class CommentsFragment : MainFragment() {
|
|||||||
val holder = CommentWithReferenceViewHolder(viewGroup, _cache);
|
val holder = CommentWithReferenceViewHolder(viewGroup, _cache);
|
||||||
holder.onDelete.subscribe(::onDelete);
|
holder.onDelete.subscribe(::onDelete);
|
||||||
holder.onRepliesClick.subscribe(::onRepliesClick);
|
holder.onRepliesClick.subscribe(::onRepliesClick);
|
||||||
|
holder.onClick.subscribe(::onClick);
|
||||||
|
holder.onAuthorClick.subscribe(::onAuthorClick);
|
||||||
return@InsertedViewAdapterWithLoader holder;
|
return@InsertedViewAdapterWithLoader holder;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -200,6 +204,30 @@ class CommentsFragment : MainFragment() {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onClick(c: IPlatformComment) {
|
||||||
|
if (c !is PolycentricPlatformComment) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val parentRef = c.parentReference
|
||||||
|
if (parentRef != null && _repliesOverlay.handleParentClick(c.contextUrl, parentRef)) {
|
||||||
|
setRepliesOverlayVisible(true, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private fun onAuthorClick(c: IPlatformComment) {
|
||||||
|
if (c !is PolycentricPlatformComment) {
|
||||||
|
return@onAuthorClick;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.i(TAG, "onAuthorClick: " + c.author.id.value);
|
||||||
|
if(c.author.id.value?.startsWith("polycentric://") ?: false) {
|
||||||
|
//val navUrl = "https://harbor.social/" + c.author.id.value?.substring("polycentric://".length);
|
||||||
|
val navUrl = "https://polycentric.io/user/" + c.author.id.value?.substring("polycentric://".length);
|
||||||
|
_fragment.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(navUrl)))
|
||||||
|
//_fragment.navigate<BrowserFragment>(navUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun onRepliesClick(c: IPlatformComment) {
|
private fun onRepliesClick(c: IPlatformComment) {
|
||||||
val replyCount = c.replyCount ?: 0;
|
val replyCount = c.replyCount ?: 0;
|
||||||
var metadata = "";
|
var metadata = "";
|
||||||
|
|||||||
+9
@@ -12,10 +12,12 @@ import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
|||||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
||||||
import com.futo.platformplayer.api.media.models.post.IPlatformPost
|
import com.futo.platformplayer.api.media.models.post.IPlatformPost
|
||||||
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.SerializedPlatformVideo
|
||||||
import com.futo.platformplayer.api.media.structures.*
|
import com.futo.platformplayer.api.media.structures.*
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateMeta
|
import com.futo.platformplayer.states.StateMeta
|
||||||
import com.futo.platformplayer.states.StatePlayer
|
import com.futo.platformplayer.states.StatePlayer
|
||||||
|
import com.futo.platformplayer.states.StatePlaylists
|
||||||
import com.futo.platformplayer.video.PlayerManager
|
import com.futo.platformplayer.video.PlayerManager
|
||||||
import com.futo.platformplayer.views.FeedStyle
|
import com.futo.platformplayer.views.FeedStyle
|
||||||
import com.futo.platformplayer.views.adapters.feedtypes.PreviewContentListAdapter
|
import com.futo.platformplayer.views.adapters.feedtypes.PreviewContentListAdapter
|
||||||
@@ -81,6 +83,12 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
|||||||
StatePlayer.instance.addToQueue(it);
|
StatePlayer.instance.addToQueue(it);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
adapter.onAddToWatchLaterClicked.subscribe(this) {
|
||||||
|
if(it is IPlatformVideo) {
|
||||||
|
StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it));
|
||||||
|
UIDialogs.toast("Added to watch later\n[${it.name}]");
|
||||||
|
}
|
||||||
|
};
|
||||||
adapter.onLongPress.subscribe(this) {
|
adapter.onLongPress.subscribe(this) {
|
||||||
if (it is IPlatformVideo) {
|
if (it is IPlatformVideo) {
|
||||||
showVideoOptionsOverlay(it)
|
showVideoOptionsOverlay(it)
|
||||||
@@ -135,6 +143,7 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
|||||||
adapter.onChannelClicked.remove(this);
|
adapter.onChannelClicked.remove(this);
|
||||||
adapter.onAddToClicked.remove(this);
|
adapter.onAddToClicked.remove(this);
|
||||||
adapter.onAddToQueueClicked.remove(this);
|
adapter.onAddToQueueClicked.remove(this);
|
||||||
|
adapter.onAddToWatchLaterClicked.remove(this);
|
||||||
adapter.onLongPress.remove(this);
|
adapter.onLongPress.remove(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+15
-5
@@ -99,7 +99,7 @@ class ContentSearchResultsFragment : MainFragment() {
|
|||||||
.success { loadedResult(it); }.exception<ScriptCaptchaRequiredException> { }
|
.success { loadedResult(it); }.exception<ScriptCaptchaRequiredException> { }
|
||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
Logger.w(TAG, "Failed to load results.", it);
|
Logger.w(TAG, "Failed to load results.", it);
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults() });
|
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults() }, null, fragment);
|
||||||
}
|
}
|
||||||
|
|
||||||
setPreviewsEnabled(Settings.instance.search.previewFeedItems);
|
setPreviewsEnabled(Settings.instance.search.previewFeedItems);
|
||||||
@@ -129,7 +129,7 @@ class ContentSearchResultsFragment : MainFragment() {
|
|||||||
onFilterClick.subscribe(this) {
|
onFilterClick.subscribe(this) {
|
||||||
_overlayContainer.let {
|
_overlayContainer.let {
|
||||||
val filterValuesCopy = HashMap(_filterValues);
|
val filterValuesCopy = HashMap(_filterValues);
|
||||||
val filtersOverlay = UISlideOverlays.showFiltersOverlay(lifecycleScope, it, _enabledClientIds!!, filterValuesCopy);
|
val filtersOverlay = UISlideOverlays.showFiltersOverlay(lifecycleScope, it, _enabledClientIds!!, filterValuesCopy, _channelUrl != null);
|
||||||
filtersOverlay.onOK.subscribe { enabledClientIds, changed ->
|
filtersOverlay.onOK.subscribe { enabledClientIds, changed ->
|
||||||
if (changed) {
|
if (changed) {
|
||||||
setFilterValues(filtersOverlay.commonCapabilities, filterValuesCopy);
|
setFilterValues(filtersOverlay.commonCapabilities, filterValuesCopy);
|
||||||
@@ -154,8 +154,14 @@ class ContentSearchResultsFragment : MainFragment() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onSearch.subscribe(this) {
|
onSearch.subscribe(this) {
|
||||||
if(it.isHttpUrl())
|
if(it.isHttpUrl()) {
|
||||||
navigate<VideoDetailFragment>(it);
|
if(StatePlatform.instance.hasEnabledPlaylistClient(it))
|
||||||
|
navigate<PlaylistFragment>(it);
|
||||||
|
else if(StatePlatform.instance.hasEnabledChannelClient(it))
|
||||||
|
navigate<ChannelFragment>(it);
|
||||||
|
else
|
||||||
|
navigate<VideoDetailFragment>(it);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
setQuery(it, true);
|
setQuery(it, true);
|
||||||
};
|
};
|
||||||
@@ -164,7 +170,11 @@ class ContentSearchResultsFragment : MainFragment() {
|
|||||||
|
|
||||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val commonCapabilities = StatePlatform.instance.getCommonSearchCapabilities(StatePlatform.instance.getEnabledClients().map { it.id });
|
val commonCapabilities =
|
||||||
|
if(_channelUrl == null)
|
||||||
|
StatePlatform.instance.getCommonSearchCapabilities(StatePlatform.instance.getEnabledClients().map { it.id });
|
||||||
|
else
|
||||||
|
StatePlatform.instance.getCommonSearchChannelContentsCapabilities(StatePlatform.instance.getEnabledClients().map { it.id });
|
||||||
val sorts = commonCapabilities?.sorts ?: listOf();
|
val sorts = commonCapabilities?.sorts ?: listOf();
|
||||||
if (sorts.size > 1) {
|
if (sorts.size > 1) {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
|
|||||||
+1
-1
@@ -60,7 +60,7 @@ class CreatorSearchResultsFragment : MainFragment() {
|
|||||||
.exception<ScriptCaptchaRequiredException> { }
|
.exception<ScriptCaptchaRequiredException> { }
|
||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
Logger.w(ChannelFragment.TAG, "Failed to load results.", it);
|
Logger.w(ChannelFragment.TAG, "Failed to load results.", it);
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults() });
|
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults() }, null, fragment);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+21
-6
@@ -12,8 +12,10 @@ import com.futo.platformplayer.*
|
|||||||
import com.futo.platformplayer.downloads.VideoDownload
|
import com.futo.platformplayer.downloads.VideoDownload
|
||||||
import com.futo.platformplayer.downloads.VideoLocal
|
import com.futo.platformplayer.downloads.VideoLocal
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.models.Playlist
|
||||||
import com.futo.platformplayer.states.StateDownloads
|
import com.futo.platformplayer.states.StateDownloads
|
||||||
import com.futo.platformplayer.states.StatePlayer
|
import com.futo.platformplayer.states.StatePlayer
|
||||||
|
import com.futo.platformplayer.states.StatePlaylists
|
||||||
import com.futo.platformplayer.views.AnyInsertedAdapterView
|
import com.futo.platformplayer.views.AnyInsertedAdapterView
|
||||||
import com.futo.platformplayer.views.AnyInsertedAdapterView.Companion.asAnyWithTop
|
import com.futo.platformplayer.views.AnyInsertedAdapterView.Companion.asAnyWithTop
|
||||||
import com.futo.platformplayer.views.others.ProgressBar
|
import com.futo.platformplayer.views.others.ProgressBar
|
||||||
@@ -143,6 +145,7 @@ class DownloadsFragment : MainFragment() {
|
|||||||
|
|
||||||
val activeDownloads = StateDownloads.instance.getDownloading();
|
val activeDownloads = StateDownloads.instance.getDownloading();
|
||||||
val playlists = StateDownloads.instance.getCachedPlaylists();
|
val playlists = StateDownloads.instance.getCachedPlaylists();
|
||||||
|
val watchLaterDownload = StateDownloads.instance.getWatchLaterDescriptor();
|
||||||
val downloaded = StateDownloads.instance.getDownloadedVideos()
|
val downloaded = StateDownloads.instance.getDownloadedVideos()
|
||||||
.filter { it.groupType != VideoDownload.GROUP_PLAYLIST || it.groupID == null || !StateDownloads.instance.hasCachedPlaylist(it.groupID!!) };
|
.filter { it.groupType != VideoDownload.GROUP_PLAYLIST || it.groupID == null || !StateDownloads.instance.hasCachedPlaylist(it.groupID!!) };
|
||||||
|
|
||||||
@@ -150,23 +153,35 @@ class DownloadsFragment : MainFragment() {
|
|||||||
_listActiveDownloadsContainer.visibility = GONE;
|
_listActiveDownloadsContainer.visibility = GONE;
|
||||||
else {
|
else {
|
||||||
_listActiveDownloadsContainer.visibility = VISIBLE;
|
_listActiveDownloadsContainer.visibility = VISIBLE;
|
||||||
_listActiveDownloadsMeta.text = "(${activeDownloads.size})";
|
_listActiveDownloadsMeta.text = "(${activeDownloads.size} videos)";
|
||||||
|
|
||||||
_listActiveDownloads.removeAllViews();
|
_listActiveDownloads.removeAllViews();
|
||||||
for(view in activeDownloads.map { ActiveDownloadItem(context, it, _frag.lifecycleScope) })
|
for(view in activeDownloads.take(4).map { ActiveDownloadItem(context, it, _frag.lifecycleScope) })
|
||||||
_listActiveDownloads.addView(view);
|
_listActiveDownloads.addView(view);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(playlists.isEmpty())
|
if(playlists.isEmpty() && watchLaterDownload == null)
|
||||||
_listPlaylistsContainer.visibility = GONE;
|
_listPlaylistsContainer.visibility = GONE;
|
||||||
else {
|
else {
|
||||||
_listPlaylistsContainer.visibility = VISIBLE;
|
_listPlaylistsContainer.visibility = VISIBLE;
|
||||||
_listPlaylistsMeta.text = "(${playlists.size} ${context.getString(R.string.playlists).lowercase()}, ${playlists.sumOf { it.playlist.videos.size }} ${context.getString(R.string.videos).lowercase()})";
|
|
||||||
|
val watchLater = if(watchLaterDownload != null) StatePlaylists.instance.getWatchLater() else listOf();
|
||||||
|
|
||||||
|
_listPlaylistsMeta.text = "(${playlists.size + (if(watchLaterDownload != null) 1 else 0)} ${context.getString(R.string.playlists).lowercase()}, ${playlists.sumOf { it.playlist.videos.size } + watchLater.size} ${context.getString(R.string.videos).lowercase()})";
|
||||||
|
|
||||||
_listPlaylists.removeAllViews();
|
_listPlaylists.removeAllViews();
|
||||||
for(view in playlists.map { PlaylistDownloadItem(context, it) }) {
|
if(watchLaterDownload != null) {
|
||||||
|
val pdView = PlaylistDownloadItem(context, "Watch Later", watchLater.firstOrNull()?.thumbnails?.getHQThumbnail(), "WATCHLATER");
|
||||||
|
pdView.setOnClickListener {
|
||||||
|
_frag.navigate<WatchLaterFragment>();
|
||||||
|
}
|
||||||
|
_listPlaylists.addView(pdView);
|
||||||
|
}
|
||||||
|
for(view in playlists.map { PlaylistDownloadItem(context, it.playlist.name, it.playlist.videos.firstOrNull()?.thumbnails?.getHQThumbnail(), it.playlist) }) {
|
||||||
view.setOnClickListener {
|
view.setOnClickListener {
|
||||||
_frag.navigate<PlaylistFragment>(view.playlist.playlist);
|
if(view.obj is Playlist) {
|
||||||
|
_frag.navigate<PlaylistFragment>(view.obj);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
_listPlaylists.addView(view);
|
_listPlaylists.addView(view);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||||||
Logger.w(TAG, "Failed to load next page.", it);
|
Logger.w(TAG, "Failed to load next page.", it);
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_next_page), it, {
|
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_next_page), it, {
|
||||||
loadNextPage();
|
loadNextPage();
|
||||||
});
|
}, null, fragment);
|
||||||
//UIDialogs.showDataRetryDialog(layoutInflater, it.message, { loadNextPage() });
|
//UIDialogs.showDataRetryDialog(layoutInflater, it.message, { loadNextPage() });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -15,18 +15,17 @@ import androidx.lifecycle.lifecycleScope
|
|||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
|
||||||
import com.futo.platformplayer.api.media.structures.IAsyncPager
|
import com.futo.platformplayer.api.media.structures.IAsyncPager
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.api.media.structures.PlatformContentPager
|
|
||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFragment
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.HistoryVideo
|
import com.futo.platformplayer.models.HistoryVideo
|
||||||
import com.futo.platformplayer.states.StateHistory
|
import com.futo.platformplayer.states.StateHistory
|
||||||
import com.futo.platformplayer.states.StatePlayer
|
import com.futo.platformplayer.states.StatePlayer
|
||||||
import com.futo.platformplayer.views.others.TagsView
|
|
||||||
import com.futo.platformplayer.views.adapters.HistoryListViewHolder
|
import com.futo.platformplayer.views.adapters.HistoryListViewHolder
|
||||||
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||||
|
import com.futo.platformplayer.views.others.TagsView
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -52,6 +51,7 @@ class HistoryFragment : MainFragment() {
|
|||||||
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||||
super.onShownWithView(parameter, isBack)
|
super.onShownWithView(parameter, isBack)
|
||||||
_view?.setPager(StateHistory.instance.getHistoryPager());
|
_view?.setPager(StateHistory.instance.getHistoryPager());
|
||||||
|
(topBar as NavigationTopBarFragment?)?.onShown("History");
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("ViewConstructor")
|
@SuppressLint("ViewConstructor")
|
||||||
@@ -174,7 +174,7 @@ class HistoryFragment : MainFragment() {
|
|||||||
Logger.w(TAG, "Failed to load next page.", it);
|
Logger.w(TAG, "Failed to load next page.", it);
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_next_page), it, {
|
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_next_page), it, {
|
||||||
loadNextPage();
|
loadNextPage();
|
||||||
});
|
}, null, fragment);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+50
-3
@@ -8,6 +8,7 @@ import android.view.ViewGroup
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.api.media.structures.EmptyPager
|
import com.futo.platformplayer.api.media.structures.EmptyPager
|
||||||
@@ -17,15 +18,23 @@ import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
|||||||
import com.futo.platformplayer.engine.exceptions.ScriptExecutionException
|
import com.futo.platformplayer.engine.exceptions.ScriptExecutionException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.models.SearchType
|
||||||
import com.futo.platformplayer.states.AnnouncementType
|
import com.futo.platformplayer.states.AnnouncementType
|
||||||
import com.futo.platformplayer.states.StateAnnouncement
|
import com.futo.platformplayer.states.StateAnnouncement
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StateMeta
|
import com.futo.platformplayer.states.StateMeta
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
|
import com.futo.platformplayer.states.StateSubscriptions
|
||||||
import com.futo.platformplayer.views.FeedStyle
|
import com.futo.platformplayer.views.FeedStyle
|
||||||
|
import com.futo.platformplayer.views.NoResultsView
|
||||||
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
|
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
|
||||||
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||||
import com.futo.platformplayer.views.adapters.InsertedViewHolder
|
import com.futo.platformplayer.views.adapters.InsertedViewHolder
|
||||||
import com.futo.platformplayer.views.announcements.AnnouncementView
|
import com.futo.platformplayer.views.announcements.AnnouncementView
|
||||||
|
import com.futo.platformplayer.views.buttons.BigButton
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@@ -117,10 +126,10 @@ class HomeFragment : MainFragment() {
|
|||||||
Logger.w(TAG, "Failed to load channel.", it);
|
Logger.w(TAG, "Failed to load channel.", it);
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_get_home), it, {
|
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_get_home), it, {
|
||||||
loadResults()
|
loadResults()
|
||||||
}) {
|
}, {
|
||||||
finishRefreshLayoutLoader();
|
finishRefreshLayoutLoader();
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
}, fragment);
|
||||||
};
|
};
|
||||||
|
|
||||||
setPreviewsEnabled(Settings.instance.home.previewFeedItems);
|
setPreviewsEnabled(Settings.instance.home.previewFeedItems);
|
||||||
@@ -147,6 +156,42 @@ class HomeFragment : MainFragment() {
|
|||||||
finishRefreshLayoutLoader();
|
finishRefreshLayoutLoader();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getEmptyPagerView(): View? {
|
||||||
|
val dp10 = 10.dp(resources);
|
||||||
|
val dp30 = 30.dp(resources);
|
||||||
|
|
||||||
|
val pluginsExist = StatePlatform.instance.getAvailableClients().isNotEmpty();
|
||||||
|
if(StatePlatform.instance.getEnabledClients().isEmpty())
|
||||||
|
//Initial setup
|
||||||
|
return NoResultsView(context, "No enabled sources", if(pluginsExist)
|
||||||
|
"Enable or install some sources"
|
||||||
|
else "This Grayjay version comes without any sources, install sources externally or using the button below.", R.drawable.ic_sources,
|
||||||
|
listOf(BigButton(context, "Browse Online Sources", "View official sources online", R.drawable.ic_explore) {
|
||||||
|
fragment.navigate<BrowserFragment>(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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)));
|
||||||
|
}.withMargin(dp10, dp30),
|
||||||
|
if(pluginsExist) BigButton(context, "Sources", "Go to the sources tab", R.drawable.ic_creators) {
|
||||||
|
fragment.navigate<SourcesFragment>();
|
||||||
|
}.withMargin(dp10, dp30) else null).filterNotNull()
|
||||||
|
);
|
||||||
|
else
|
||||||
|
return NoResultsView(context, "Nothing to see here", "The enabled sources do not have any results.", R.drawable.ic_help,
|
||||||
|
listOf(BigButton(context, "Sources", "Go to the sources tab", R.drawable.ic_creators) {
|
||||||
|
fragment.navigate<SourcesFragment>();
|
||||||
|
}.withMargin(dp10, dp30))
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
override fun reload() {
|
override fun reload() {
|
||||||
loadResults();
|
loadResults();
|
||||||
}
|
}
|
||||||
@@ -161,13 +206,15 @@ class HomeFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
private fun loadedResult(pager : IPager<IPlatformContent>) {
|
private fun loadedResult(pager : IPager<IPlatformContent>) {
|
||||||
if (pager is EmptyPager<IPlatformContent>) {
|
if (pager is EmptyPager<IPlatformContent>) {
|
||||||
StateAnnouncement.instance.registerAnnouncement(UUID.randomUUID().toString(), context.getString(R.string.no_home_available), context.getString(R.string.no_home_page_is_available_please_check_if_you_are_connected_to_the_internet_and_refresh), AnnouncementType.SESSION);
|
//StateAnnouncement.instance.registerAnnouncement(UUID.randomUUID().toString(), context.getString(R.string.no_home_available), context.getString(R.string.no_home_page_is_available_please_check_if_you_are_connected_to_the_internet_and_refresh), AnnouncementType.SESSION);
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.i(TAG, "Got new home pager ${pager}");
|
Logger.i(TAG, "Got new home pager ${pager}");
|
||||||
finishRefreshLayoutLoader();
|
finishRefreshLayoutLoader();
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setPager(pager);
|
setPager(pager);
|
||||||
|
if(pager.getResults().isEmpty() && !pager.hasMorePages())
|
||||||
|
setEmptyPager(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+16
-41
@@ -146,7 +146,7 @@ class PlaylistFragment : MainFragment() {
|
|||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
Logger.w(TAG, "Failed to load playlist.", it);
|
Logger.w(TAG, "Failed to load playlist.", it);
|
||||||
val c = context ?: return@exception;
|
val c = context ?: return@exception;
|
||||||
UIDialogs.showGeneralRetryErrorDialog(c, context.getString(R.string.failed_to_load_playlist), it, ::fetchPlaylist);
|
UIDialogs.showGeneralRetryErrorDialog(c, context.getString(R.string.failed_to_load_playlist), it, ::fetchPlaylist, null, fragment);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,14 +201,18 @@ class PlaylistFragment : MainFragment() {
|
|||||||
showConvertPlaylistButton();
|
showConvertPlaylistButton();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateDownloadState();
|
_playlist?.let {
|
||||||
|
updateDownloadState(VideoDownload.GROUP_PLAYLIST, it.id, this::download);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onResume() {
|
fun onResume() {
|
||||||
StateDownloads.instance.onDownloadsChanged.subscribe(this) {
|
StateDownloads.instance.onDownloadsChanged.subscribe(this) {
|
||||||
_fragment.lifecycleScope.launch(Dispatchers.Main) {
|
_fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||||
try {
|
try {
|
||||||
updateDownloadState();
|
_playlist?.let {
|
||||||
|
updateDownloadState(VideoDownload.GROUP_PLAYLIST, it.id, this@PlaylistView::download);
|
||||||
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to update download state onDownloadedChanged.")
|
Logger.e(TAG, "Failed to update download state onDownloadedChanged.")
|
||||||
}
|
}
|
||||||
@@ -217,7 +221,9 @@ class PlaylistFragment : MainFragment() {
|
|||||||
StateDownloads.instance.onDownloadedChanged.subscribe(this) {
|
StateDownloads.instance.onDownloadedChanged.subscribe(this) {
|
||||||
_fragment.lifecycleScope.launch(Dispatchers.Main) {
|
_fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||||
try {
|
try {
|
||||||
updateDownloadState();
|
_playlist?.let {
|
||||||
|
updateDownloadState(VideoDownload.GROUP_PLAYLIST, it.id, this@PlaylistView::download);
|
||||||
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to update download state onDownloadedChanged.")
|
Logger.e(TAG, "Failed to update download state onDownloadedChanged.")
|
||||||
}
|
}
|
||||||
@@ -225,6 +231,12 @@ class PlaylistFragment : MainFragment() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun download() {
|
||||||
|
_playlist?.let {
|
||||||
|
UISlideOverlays.showDownloadPlaylistOverlay(it, overlayContainer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun onPause() {
|
fun onPause() {
|
||||||
StateDownloads.instance.onDownloadsChanged.remove(this);
|
StateDownloads.instance.onDownloadsChanged.remove(this);
|
||||||
StateDownloads.instance.onDownloadedChanged.remove(this);
|
StateDownloads.instance.onDownloadedChanged.remove(this);
|
||||||
@@ -268,43 +280,6 @@ class PlaylistFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateDownloadState() {
|
|
||||||
val playlist = _playlist ?: return;
|
|
||||||
val isDownloading = StateDownloads.instance.getDownloading().any { it.groupType == VideoDownload.GROUP_PLAYLIST && it.groupID == playlist.id };
|
|
||||||
val isDownloaded = StateDownloads.instance.isPlaylistCached(playlist.id);
|
|
||||||
|
|
||||||
val dp10 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10f, resources.displayMetrics);
|
|
||||||
|
|
||||||
if(isDownloaded && !isDownloading)
|
|
||||||
_buttonDownload.setBackgroundResource(R.drawable.background_button_round_green);
|
|
||||||
else
|
|
||||||
_buttonDownload.setBackgroundResource(R.drawable.background_button_round);
|
|
||||||
|
|
||||||
if(isDownloading) {
|
|
||||||
_buttonDownload.setImageResource(R.drawable.ic_loader_animated);
|
|
||||||
_buttonDownload.drawable.assume<Animatable, Unit> { it.start() };
|
|
||||||
_buttonDownload.setOnClickListener {
|
|
||||||
UIDialogs.showConfirmationDialog(context, context.getString(R.string.are_you_sure_you_want_to_delete_the_downloaded_videos), {
|
|
||||||
StateDownloads.instance.deleteCachedPlaylist(playlist.id);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if(isDownloaded) {
|
|
||||||
_buttonDownload.setImageResource(R.drawable.ic_download_off);
|
|
||||||
_buttonDownload.setOnClickListener {
|
|
||||||
UIDialogs.showConfirmationDialog(context, context.getString(R.string.are_you_sure_you_want_to_delete_the_downloaded_videos), {
|
|
||||||
StateDownloads.instance.deleteCachedPlaylist(playlist.id);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
_buttonDownload.setImageResource(R.drawable.ic_download);
|
|
||||||
_buttonDownload.setOnClickListener {
|
|
||||||
UISlideOverlays.showDownloadPlaylistOverlay(playlist, overlayContainer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_buttonDownload.setPadding(dp10.toInt());
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun canEdit(): Boolean { return _playlist != null; }
|
override fun canEdit(): Boolean { return _playlist != null; }
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -69,7 +69,7 @@ class PlaylistSearchResultsFragment : MainFragment() {
|
|||||||
.success { loadedResult(it); }
|
.success { loadedResult(it); }
|
||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
Logger.w(ChannelFragment.TAG, "Failed to load results.", it);
|
Logger.w(ChannelFragment.TAG, "Failed to load results.", it);
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults() });
|
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults() }, null, fragment);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+21
-18
@@ -35,6 +35,7 @@ import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
|||||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||||
|
import com.futo.platformplayer.setPlatformPlayerLinkMovementMethod
|
||||||
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.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
@@ -161,7 +162,7 @@ class PostDetailFragment : MainFragment {
|
|||||||
.success { setPostDetails(it) }
|
.success { setPostDetails(it) }
|
||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
Logger.w(ChannelFragment.TAG, context.getString(R.string.failed_to_load_post), it);
|
Logger.w(ChannelFragment.TAG, context.getString(R.string.failed_to_load_post), it);
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_post), it, ::fetchPost);
|
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_post), it, ::fetchPost, null, _fragment);
|
||||||
} else TaskHandler(IPlatformPostDetails::class.java) { _fragment.lifecycleScope };
|
} else TaskHandler(IPlatformPostDetails::class.java) { _fragment.lifecycleScope };
|
||||||
|
|
||||||
private val _taskLoadPolycentricProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(StateApp.instance.scopeGetter, { PolycentricCache.instance.getProfileAsync(it) })
|
private val _taskLoadPolycentricProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(StateApp.instance.scopeGetter, { PolycentricCache.instance.getProfileAsync(it) })
|
||||||
@@ -211,6 +212,8 @@ class PostDetailFragment : MainFragment {
|
|||||||
|
|
||||||
_repliesOverlay = findViewById(R.id.replies_overlay);
|
_repliesOverlay = findViewById(R.id.replies_overlay);
|
||||||
|
|
||||||
|
_textContent.setPlatformPlayerLinkMovementMethod(context);
|
||||||
|
|
||||||
_buttonSubscribe.onSubscribed.subscribe {
|
_buttonSubscribe.onSubscribed.subscribe {
|
||||||
//TODO: add overlay to layout
|
//TODO: add overlay to layout
|
||||||
//UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer);
|
//UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer);
|
||||||
@@ -314,8 +317,8 @@ class PostDetailFragment : MainFragment {
|
|||||||
private fun updatePolycentricRating() {
|
private fun updatePolycentricRating() {
|
||||||
_rating.visibility = View.GONE;
|
_rating.visibility = View.GONE;
|
||||||
|
|
||||||
val value = _post?.id?.value ?: _postOverview?.id?.value ?: return;
|
val ref = Models.referenceFromBuffer((_post?.url ?: _postOverview?.url)?.toByteArray() ?: return)
|
||||||
val ref = Models.referenceFromBuffer(value.toByteArray());
|
val extraBytesRef = (_post?.id?.value ?: _postOverview?.id?.value)?.let { if (it.isNotEmpty()) it.toByteArray() else null }
|
||||||
val version = _version;
|
val version = _version;
|
||||||
|
|
||||||
_rating.onLikeDislikeUpdated.remove(this);
|
_rating.onLikeDislikeUpdated.remove(this);
|
||||||
@@ -333,7 +336,8 @@ class PostDetailFragment : MainFragment {
|
|||||||
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder().setFromType(
|
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder().setFromType(
|
||||||
ContentType.OPINION.value).setValue(
|
ContentType.OPINION.value).setValue(
|
||||||
ByteString.copyFrom(Opinion.dislike.data)).build()
|
ByteString.copyFrom(Opinion.dislike.data)).build()
|
||||||
)
|
),
|
||||||
|
extraByteReferences = listOfNotNull(extraBytesRef)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (version != _version) {
|
if (version != _version) {
|
||||||
@@ -342,8 +346,8 @@ class PostDetailFragment : MainFragment {
|
|||||||
|
|
||||||
val likes = queryReferencesResponse.countsList[0];
|
val likes = queryReferencesResponse.countsList[0];
|
||||||
val dislikes = queryReferencesResponse.countsList[1];
|
val dislikes = queryReferencesResponse.countsList[1];
|
||||||
val hasLiked = StatePolycentric.instance.hasLiked(ref);
|
val hasLiked = StatePolycentric.instance.hasLiked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasLiked(it) } ?: false*/;
|
||||||
val hasDisliked = StatePolycentric.instance.hasDisliked(ref);
|
val hasDisliked = StatePolycentric.instance.hasDisliked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasDisliked(it) } ?: false*/;
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
if (version != _version) {
|
if (version != _version) {
|
||||||
@@ -468,12 +472,11 @@ class PostDetailFragment : MainFragment {
|
|||||||
if (_postOverview == null) {
|
if (_postOverview == null) {
|
||||||
fetchPolycentricProfile();
|
fetchPolycentricProfile();
|
||||||
updatePolycentricRating();
|
updatePolycentricRating();
|
||||||
|
_addCommentView.setContext(value.url, Models.referenceFromBuffer(value.url.toByteArray()));
|
||||||
val ref = value.id.value?.let { Models.referenceFromBuffer(it.toByteArray()); };
|
|
||||||
_addCommentView.setContext(value.url, ref);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCommentType(true);
|
updateCommentType(true);
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setPostOverview(value: IPlatformPost) {
|
fun setPostOverview(value: IPlatformPost) {
|
||||||
@@ -489,9 +492,7 @@ class PostDetailFragment : MainFragment {
|
|||||||
_textMeta.text = value.datetime?.toHumanNowDiffString()?.let { "$it ago" } ?: "" //TODO: Include view count?
|
_textMeta.text = value.datetime?.toHumanNowDiffString()?.let { "$it ago" } ?: "" //TODO: Include view count?
|
||||||
_textContent.text = value.description.fixHtmlWhitespace();
|
_textContent.text = value.description.fixHtmlWhitespace();
|
||||||
_platformIndicator.setPlatformFromClientID(value.id.pluginId);
|
_platformIndicator.setPlatformFromClientID(value.id.pluginId);
|
||||||
|
_addCommentView.setContext(value.url, Models.referenceFromBuffer(value.url.toByteArray()));
|
||||||
val ref = value.id.value?.let { Models.referenceFromBuffer(it.toByteArray()); };
|
|
||||||
_addCommentView.setContext(value.url, ref);
|
|
||||||
|
|
||||||
updatePolycentricRating();
|
updatePolycentricRating();
|
||||||
fetchPolycentricProfile();
|
fetchPolycentricProfile();
|
||||||
@@ -636,12 +637,12 @@ class PostDetailFragment : MainFragment {
|
|||||||
|
|
||||||
if (cachedPolycentricProfile?.profile == null) {
|
if (cachedPolycentricProfile?.profile == null) {
|
||||||
_layoutMonetization.visibility = View.GONE;
|
_layoutMonetization.visibility = View.GONE;
|
||||||
_creatorThumbnail.setHarborAvailable(false, animate);
|
_creatorThumbnail.setHarborAvailable(false, animate, null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_layoutMonetization.visibility = View.VISIBLE;
|
_layoutMonetization.visibility = View.VISIBLE;
|
||||||
_creatorThumbnail.setHarborAvailable(true, animate);
|
_creatorThumbnail.setHarborAvailable(true, animate, cachedPolycentricProfile.profile.system.toProto());
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchPost() {
|
private fun fetchPost() {
|
||||||
@@ -665,14 +666,16 @@ class PostDetailFragment : MainFragment {
|
|||||||
private fun fetchPolycentricComments() {
|
private fun fetchPolycentricComments() {
|
||||||
Logger.i(TAG, "fetchPolycentricComments")
|
Logger.i(TAG, "fetchPolycentricComments")
|
||||||
val post = _post;
|
val post = _post;
|
||||||
val idValue = post?.id?.value
|
val ref = (_post?.url ?: _postOverview?.url)?.toByteArray()?.let { Models.referenceFromBuffer(it) }
|
||||||
if (idValue == null) {
|
val extraBytesRef = (_post?.id?.value ?: _postOverview?.id?.value)?.let { if (it.isNotEmpty()) it.toByteArray() else null }
|
||||||
Logger.w(TAG, "Failed to fetch polycentric comments because id was null")
|
|
||||||
|
if (ref == null) {
|
||||||
|
Logger.w(TAG, "Failed to fetch polycentric comments because url was not set null")
|
||||||
_commentsList.clear();
|
_commentsList.clear();
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_commentsList.load(false) { StatePolycentric.instance.getCommentPager(post.url, Models.referenceFromBuffer(idValue.toByteArray())); };
|
_commentsList.load(false) { StatePolycentric.instance.getCommentPager(post!!.url, ref, listOfNotNull(extraBytesRef)); };
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateCommentType(reloadComments: Boolean) {
|
private fun updateCommentType(reloadComments: Boolean) {
|
||||||
|
|||||||
+71
-18
@@ -12,6 +12,7 @@ import android.webkit.CookieManager
|
|||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
@@ -100,6 +101,11 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
loadConfig(parameter);
|
loadConfig(parameter);
|
||||||
updateSourceViews();
|
updateSourceViews();
|
||||||
}
|
}
|
||||||
|
else if(parameter is UpdatePluginAction) {
|
||||||
|
loadConfig(parameter.config);
|
||||||
|
updateSourceViews();
|
||||||
|
checkForUpdatesSource();
|
||||||
|
}
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -107,17 +113,20 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
fun onHide() {
|
fun onHide() {
|
||||||
val id = _config?.id ?: return;
|
val id = _config?.id ?: return;
|
||||||
|
|
||||||
if(_settingsChanged && _settings != null) {
|
var shouldReload = false;
|
||||||
_settingsChanged = false;
|
|
||||||
StatePlugins.instance.setPluginSettings(id, _settings!!);
|
|
||||||
reloadSource(id);
|
|
||||||
|
|
||||||
UIDialogs.toast(context.getString(R.string.plugin_settings_saved), false);
|
|
||||||
}
|
|
||||||
if(_settingsAppChanged) {
|
if(_settingsAppChanged) {
|
||||||
_settingsAppForm.setObjectValues();
|
_settingsAppForm.setObjectValues();
|
||||||
StatePlugins.instance.savePlugin(id);
|
StatePlugins.instance.savePlugin(id);
|
||||||
|
shouldReload = true;
|
||||||
}
|
}
|
||||||
|
if(_settingsChanged && _settings != null) {
|
||||||
|
_settingsChanged = false;
|
||||||
|
StatePlugins.instance.setPluginSettings(id, _settings!!);
|
||||||
|
shouldReload = true;
|
||||||
|
UIDialogs.toast(context.getString(R.string.plugin_settings_saved), false);
|
||||||
|
}
|
||||||
|
if(shouldReload)
|
||||||
|
reloadSource(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -137,9 +146,25 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
//App settings
|
//App settings
|
||||||
try {
|
try {
|
||||||
_settingsAppForm.fromObject(source.descriptor.appSettings);
|
_settingsAppForm.fromObject(source.descriptor.appSettings);
|
||||||
|
if(source.config.developerSubmitUrl.isNullOrEmpty()) {
|
||||||
|
val field = _settingsAppForm.findField("devSubmit");
|
||||||
|
field?.setValue(false);
|
||||||
|
if(field is View)
|
||||||
|
field.isVisible = false;
|
||||||
|
}
|
||||||
_settingsAppForm.onChanged.clear();
|
_settingsAppForm.onChanged.clear();
|
||||||
_settingsAppForm.onChanged.subscribe { _, _ ->
|
_settingsAppForm.onChanged.subscribe { field, value ->
|
||||||
_settingsAppChanged = true;
|
_settingsAppChanged = true;
|
||||||
|
if(field.descriptor?.id == "devSubmit") {
|
||||||
|
if(value is Boolean && value) {
|
||||||
|
UIDialogs.showDialog(context, R.drawable.ic_warning_yellow,
|
||||||
|
"Are you sure you trust the developer?",
|
||||||
|
"Developers may gain access to sensitive data. Only enable this when you are trying to help the developer fix a bug.\nThe following domain is used:",
|
||||||
|
source.config.developerSubmitUrl ?: "", 0,
|
||||||
|
UIDialogs.Action("Cancel", { field.setValue(false); }, UIDialogs.ActionStyle.NONE),
|
||||||
|
UIDialogs.Action("Enable", { }, UIDialogs.ActionStyle.DANGEROUS));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to load app settings form from plugin settings", e)
|
Logger.e(TAG, "Failed to load app settings form from plugin settings", e)
|
||||||
@@ -295,17 +320,24 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val isEmbedded = StatePlugins.instance.getEmbeddedSources(context).any { it.key == config.id };
|
||||||
|
|
||||||
val clientIfExists = if(config.id != StateDeveloper.DEV_ID)
|
val clientIfExists = if(config.id != StateDeveloper.DEV_ID)
|
||||||
StatePlugins.instance.getPlugin(config.id);
|
StatePlugins.instance.getPlugin(config.id);
|
||||||
else null;
|
else null;
|
||||||
groups.add(
|
groups.add(
|
||||||
BigButtonGroup(c, context.getString(R.string.management),
|
BigButtonGroup(c, context.getString(R.string.management),
|
||||||
BigButton(c, context.getString(R.string.uninstall), context.getString(R.string.removes_the_plugin_from_the_app), R.drawable.ic_block) {
|
if(!isEmbedded) BigButton(c, context.getString(R.string.uninstall), context.getString(R.string.removes_the_plugin_from_the_app), R.drawable.ic_block) {
|
||||||
uninstallSource();
|
uninstallSource();
|
||||||
}.withBackground(R.drawable.background_big_button_red).apply {
|
}.withBackground(R.drawable.background_big_button_red).apply {
|
||||||
this.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT).apply {
|
this.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT).apply {
|
||||||
setMargins(0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics).toInt(), 0, 0);
|
setMargins(0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics).toInt(), 0, 0);
|
||||||
};
|
};
|
||||||
|
} else BigButton(c, context.getString(R.string.uninstall), "Cannot uninstall embedded plugins", R.drawable.ic_block, {}).apply {
|
||||||
|
this.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT).apply {
|
||||||
|
setMargins(0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics).toInt(), 0, 0);
|
||||||
|
};
|
||||||
|
this.alpha = 0.5f
|
||||||
},
|
},
|
||||||
if(clientIfExists?.captchaEncrypted != null)
|
if(clientIfExists?.captchaEncrypted != null)
|
||||||
BigButton(c, context.getString(R.string.delete_captcha), context.getString(R.string.deletes_stored_captcha_answer_for_this_plugin), R.drawable.ic_block) {
|
BigButton(c, context.getString(R.string.delete_captcha), context.getString(R.string.deletes_stored_captcha_answer_for_this_plugin), R.drawable.ic_block) {
|
||||||
@@ -325,7 +357,6 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
_sourceButtons.addView(group);
|
_sourceButtons.addView(group);
|
||||||
}
|
}
|
||||||
|
|
||||||
val isEmbedded = StatePlugins.instance.getEmbeddedSources(context).any { it.key == config.id };
|
|
||||||
val advancedButtons = BigButtonGroup(c, "Advanced",
|
val advancedButtons = BigButtonGroup(c, "Advanced",
|
||||||
BigButton(c, "Edit Code", "Modify the source of this plugin", R.drawable.ic_code) {
|
BigButton(c, "Edit Code", "Modify the source of this plugin", R.drawable.ic_code) {
|
||||||
|
|
||||||
@@ -333,9 +364,15 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
this.alpha = 0.5f;
|
this.alpha = 0.5f;
|
||||||
},
|
},
|
||||||
if(isEmbedded) BigButton(c, "Reinstall", "Modify the source of this plugin", R.drawable.ic_refresh) {
|
if(isEmbedded) BigButton(c, "Reinstall", "Modify the source of this plugin", R.drawable.ic_refresh) {
|
||||||
StatePlugins.instance.updateEmbeddedPlugins(context, listOf(config.id), true);
|
val embeddedConfig = StatePlugins.instance.getEmbeddedPluginConfigFromID(context, config.id);
|
||||||
reloadSource(config.id);
|
|
||||||
UIDialogs.toast(context, "Embedded plugin reinstalled, may require refresh");
|
UIDialogs.showDialog(context, R.drawable.ic_warning_yellow, "Are you sure you want to downgrade (${config.version}=>${embeddedConfig?.version})?",
|
||||||
|
"This will revert the plugin back to the originally embedded version.\nVersion change: ${config.version}=>${embeddedConfig?.version}", null,
|
||||||
|
0, UIDialogs.Action("Cancel", {}), UIDialogs.Action("Reinstall", {
|
||||||
|
StatePlugins.instance.updateEmbeddedPlugins(context, listOf(config.id), true);
|
||||||
|
reloadSource(config.id);
|
||||||
|
UIDialogs.toast(context, "Embedded plugin reinstalled, may require refresh");
|
||||||
|
}, UIDialogs.ActionStyle.DANGEROUS));
|
||||||
}.apply {
|
}.apply {
|
||||||
this.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT).apply {
|
this.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT).apply {
|
||||||
setMargins(0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics).toInt(), 0, 0);
|
setMargins(0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics).toInt(), 0, 0);
|
||||||
@@ -354,11 +391,22 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
if(config.authentication == null)
|
if(config.authentication == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
LoginActivity.showLogin(StateApp.instance.context, config) {
|
if(config.authentication.loginWarning != null) {
|
||||||
StatePlugins.instance.setPluginAuth(config.id, it);
|
UIDialogs.showDialog(context, R.drawable.ic_warning_yellow, "Login Warning",
|
||||||
|
config.authentication.loginWarning, null, 0,
|
||||||
reloadSource(config.id);
|
UIDialogs.Action("Cancel", {}, UIDialogs.ActionStyle.NONE),
|
||||||
};
|
UIDialogs.Action("Login", {
|
||||||
|
LoginActivity.showLogin(StateApp.instance.context, config) {
|
||||||
|
StatePlugins.instance.setPluginAuth(config.id, it);
|
||||||
|
reloadSource(config.id);
|
||||||
|
};
|
||||||
|
}, UIDialogs.ActionStyle.PRIMARY))
|
||||||
|
}
|
||||||
|
else
|
||||||
|
LoginActivity.showLogin(StateApp.instance.context, config) {
|
||||||
|
StatePlugins.instance.setPluginAuth(config.id, it);
|
||||||
|
reloadSource(config.id);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
private fun logoutSource(clear: Boolean = true) {
|
private fun logoutSource(clear: Boolean = true) {
|
||||||
val config = _config ?: return;
|
val config = _config ?: return;
|
||||||
@@ -454,6 +502,7 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkForUpdatesSource() {
|
private fun checkForUpdatesSource() {
|
||||||
val c = _config ?: return;
|
val c = _config ?: return;
|
||||||
val sourceUrl = c.sourceUrl ?: return;
|
val sourceUrl = c.sourceUrl ?: return;
|
||||||
@@ -523,4 +572,8 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
const val TAG = "SourceDetailFragment";
|
const val TAG = "SourceDetailFragment";
|
||||||
fun newInstance() = SourceDetailFragment().apply {}
|
fun newInstance() = SourceDetailFragment().apply {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class UpdatePluginAction(val config: SourcePluginConfig) {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+13
-8
@@ -8,6 +8,7 @@ import android.view.LayoutInflater
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
@@ -25,6 +26,7 @@ import com.futo.platformplayer.views.adapters.DisabledSourceView
|
|||||||
import com.futo.platformplayer.views.adapters.EnabledSourceAdapter
|
import com.futo.platformplayer.views.adapters.EnabledSourceAdapter
|
||||||
import com.futo.platformplayer.views.adapters.EnabledSourceViewHolder
|
import com.futo.platformplayer.views.adapters.EnabledSourceViewHolder
|
||||||
import com.futo.platformplayer.views.adapters.ItemMoveCallback
|
import com.futo.platformplayer.views.adapters.ItemMoveCallback
|
||||||
|
import com.futo.platformplayer.views.buttons.BigButton
|
||||||
import com.futo.platformplayer.views.sources.SourceUnderConstructionView
|
import com.futo.platformplayer.views.sources.SourceUnderConstructionView
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
@@ -39,10 +41,12 @@ class SourcesFragment : MainFragment() {
|
|||||||
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||||
super.onShownWithView(parameter, isBack)
|
super.onShownWithView(parameter, isBack)
|
||||||
|
|
||||||
if(topBar is AddTopBarFragment)
|
if(topBar is AddTopBarFragment) {
|
||||||
|
(topBar as AddTopBarFragment).onAdd.clear();
|
||||||
(topBar as AddTopBarFragment).onAdd.subscribe {
|
(topBar as AddTopBarFragment).onAdd.subscribe {
|
||||||
startActivity(Intent(requireContext(), AddSourceOptionsActivity::class.java));
|
startActivity(Intent(requireContext(), AddSourceOptionsActivity::class.java));
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
_view?.reloadSources();
|
_view?.reloadSources();
|
||||||
}
|
}
|
||||||
@@ -84,6 +88,14 @@ class SourcesFragment : MainFragment() {
|
|||||||
_containerDisabledViews = findViewById(R.id.container_disabled_views);
|
_containerDisabledViews = findViewById(R.id.container_disabled_views);
|
||||||
_containerConstruction = findViewById(R.id.container_construction);
|
_containerConstruction = findViewById(R.id.container_construction);
|
||||||
|
|
||||||
|
if(StatePlatform.instance.getAvailableClients().isEmpty()) {
|
||||||
|
findViewById<LinearLayout>(R.id.no_sources).isVisible = true;
|
||||||
|
findViewById<LinearLayout>(R.id.plugin_disclaimer).isVisible = false;
|
||||||
|
}
|
||||||
|
findViewById<BigButton>(R.id.button_add_sources).onClick.subscribe {
|
||||||
|
fragment.startActivity(Intent(context, AddSourceOptionsActivity::class.java));
|
||||||
|
};
|
||||||
|
|
||||||
for(inConstructSource in StatePlugins.instance.getSourcesUnderConstruction(context))
|
for(inConstructSource in StatePlugins.instance.getSourcesUnderConstruction(context))
|
||||||
_containerConstruction.addView(SourceUnderConstructionView(context, inConstructSource.key, inConstructSource.value));
|
_containerConstruction.addView(SourceUnderConstructionView(context, inConstructSource.key, inConstructSource.value));
|
||||||
|
|
||||||
@@ -109,8 +121,6 @@ class SourcesFragment : MainFragment() {
|
|||||||
|
|
||||||
adapterSourcesEnabled.notifyItemMoved(fromPosition, toPosition);
|
adapterSourcesEnabled.notifyItemMoved(fromPosition, toPosition);
|
||||||
onEnabledChanged(enabledSources);
|
onEnabledChanged(enabledSources);
|
||||||
if(toPosition == 0)
|
|
||||||
onPrimaryChanged(enabledSources.first());
|
|
||||||
|
|
||||||
StatePlatform.instance.setPlatformOrder(enabledSources.map { it.name });
|
StatePlatform.instance.setPlatformOrder(enabledSources.map { it.name });
|
||||||
};
|
};
|
||||||
@@ -131,8 +141,6 @@ class SourcesFragment : MainFragment() {
|
|||||||
|
|
||||||
updateContainerVisibility();
|
updateContainerVisibility();
|
||||||
onEnabledChanged(enabledSources);
|
onEnabledChanged(enabledSources);
|
||||||
if(index == 0)
|
|
||||||
onPrimaryChanged(enabledSources.first());
|
|
||||||
|
|
||||||
if(enabledSources.size <= 1)
|
if(enabledSources.size <= 1)
|
||||||
setCanRemove(false);
|
setCanRemove(false);
|
||||||
@@ -219,9 +227,6 @@ class SourcesFragment : MainFragment() {
|
|||||||
_adapterSourcesEnabled.canRemove = canRemove;
|
_adapterSourcesEnabled.canRemove = canRemove;
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onPrimaryChanged(client: IPlatformClient) {
|
|
||||||
StatePlatform.instance.selectPrimaryClient(client.id);
|
|
||||||
}
|
|
||||||
private fun onEnabledChanged(clients: List<IPlatformClient>) {
|
private fun onEnabledChanged(clients: List<IPlatformClient>) {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
StatePlatform.instance.selectClients(*clients.map { it.id }.toTypedArray());
|
StatePlatform.instance.selectClients(*clients.map { it.id }.toTypedArray());
|
||||||
|
|||||||
+97
-66
@@ -4,35 +4,34 @@ import android.annotation.SuppressLint
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.TypedValue
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.inputmethod.InputMethodManager
|
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.core.content.getSystemService
|
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.UISlideOverlays
|
import com.futo.platformplayer.UISlideOverlays
|
||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
import com.futo.platformplayer.dp
|
import com.futo.platformplayer.dp
|
||||||
import com.futo.platformplayer.models.Subscription
|
import com.futo.platformplayer.models.ImageVariable
|
||||||
import com.futo.platformplayer.models.SubscriptionGroup
|
import com.futo.platformplayer.models.SubscriptionGroup
|
||||||
import com.futo.platformplayer.states.StateSubscriptionGroups
|
import com.futo.platformplayer.states.StateSubscriptionGroups
|
||||||
import com.futo.platformplayer.states.StateSubscriptions
|
import com.futo.platformplayer.states.StateSubscriptions
|
||||||
import com.futo.platformplayer.views.AnyAdapterView
|
import com.futo.platformplayer.views.AnyAdapterView
|
||||||
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
|
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
|
||||||
import com.futo.platformplayer.views.SearchView
|
import com.futo.platformplayer.views.SearchView
|
||||||
import com.futo.platformplayer.views.adapters.AnyAdapter
|
|
||||||
import com.futo.platformplayer.views.adapters.viewholders.CreatorBarViewHolder
|
import com.futo.platformplayer.views.adapters.viewholders.CreatorBarViewHolder
|
||||||
|
import com.futo.platformplayer.views.overlays.CreatorSelectOverlay
|
||||||
import com.futo.platformplayer.views.overlays.ImageVariableOverlay
|
import com.futo.platformplayer.views.overlays.ImageVariableOverlay
|
||||||
|
import com.futo.platformplayer.views.overlays.OverlayTopbar
|
||||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTextInput
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTextInput
|
||||||
import com.google.android.material.imageview.ShapeableImageView
|
import com.google.android.material.imageview.ShapeableImageView
|
||||||
import com.google.android.material.shape.CornerFamily
|
import com.google.android.material.shape.CornerFamily
|
||||||
@@ -60,6 +59,11 @@ class SubscriptionGroupFragment : MainFragment() {
|
|||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onHide() {
|
||||||
|
super.onHide();
|
||||||
|
_view?.onHide();
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "SourcesFragment";
|
private const val TAG = "SourcesFragment";
|
||||||
fun newInstance() = SubscriptionGroupFragment().apply {}
|
fun newInstance() = SubscriptionGroupFragment().apply {}
|
||||||
@@ -69,6 +73,7 @@ class SubscriptionGroupFragment : MainFragment() {
|
|||||||
private class SubscriptionGroupView: ConstraintLayout {
|
private class SubscriptionGroupView: ConstraintLayout {
|
||||||
private val _fragment: SubscriptionGroupFragment;
|
private val _fragment: SubscriptionGroupFragment;
|
||||||
|
|
||||||
|
private val _topbar: OverlayTopbar;
|
||||||
private val _textGroupTitleContainer: LinearLayout;
|
private val _textGroupTitleContainer: LinearLayout;
|
||||||
private val _textGroupTitle: TextView;
|
private val _textGroupTitle: TextView;
|
||||||
private val _imageGroup: ShapeableImageView;
|
private val _imageGroup: ShapeableImageView;
|
||||||
@@ -81,26 +86,25 @@ class SubscriptionGroupFragment : MainFragment() {
|
|||||||
private val _buttonSettings: ImageButton;
|
private val _buttonSettings: ImageButton;
|
||||||
private val _buttonDelete: ImageButton;
|
private val _buttonDelete: ImageButton;
|
||||||
|
|
||||||
private val _enabledCreators: ArrayList<IPlatformChannel> = arrayListOf();
|
private val _buttonAddCreator: FrameLayout;
|
||||||
private val _disabledCreators: ArrayList<IPlatformChannel> = arrayListOf();
|
|
||||||
private val _enabledCreatorsFiltered: ArrayList<IPlatformChannel> = arrayListOf();
|
|
||||||
private val _disabledCreatorsFiltered: ArrayList<IPlatformChannel> = arrayListOf();
|
|
||||||
|
|
||||||
private val _containerEnabled: LinearLayout;
|
private val _enabledCreators: ArrayList<IPlatformChannel> = arrayListOf();
|
||||||
private val _containerDisabled: LinearLayout;
|
private val _enabledCreatorsFiltered: ArrayList<IPlatformChannel> = arrayListOf();
|
||||||
|
|
||||||
private val _recyclerCreatorsEnabled: AnyAdapterView<IPlatformChannel, CreatorBarViewHolder>;
|
private val _recyclerCreatorsEnabled: AnyAdapterView<IPlatformChannel, CreatorBarViewHolder>;
|
||||||
private val _recyclerCreatorsDisabled: AnyAdapterView<IPlatformChannel, CreatorBarViewHolder>;
|
|
||||||
|
|
||||||
private val _overlay: FrameLayout;
|
private val _overlay: FrameLayout;
|
||||||
|
|
||||||
private var _group: SubscriptionGroup? = null;
|
private var _group: SubscriptionGroup? = null;
|
||||||
|
|
||||||
|
private var _didDelete: Boolean = false;
|
||||||
|
|
||||||
constructor(context: Context, fragment: SubscriptionGroupFragment): super(context) {
|
constructor(context: Context, fragment: SubscriptionGroupFragment): super(context) {
|
||||||
inflate(context, R.layout.fragment_subscriptions_group, this);
|
inflate(context, R.layout.fragment_subscriptions_group, this);
|
||||||
_fragment = fragment;
|
_fragment = fragment;
|
||||||
|
|
||||||
_overlay = findViewById(R.id.overlay);
|
_overlay = findViewById(R.id.overlay);
|
||||||
|
_topbar = findViewById(R.id.topbar);
|
||||||
_searchBar = findViewById(R.id.search_bar);
|
_searchBar = findViewById(R.id.search_bar);
|
||||||
_textGroupTitleContainer = findViewById(R.id.text_group_title_container);
|
_textGroupTitleContainer = findViewById(R.id.text_group_title_container);
|
||||||
_textGroupTitle = findViewById(R.id.text_group_title);
|
_textGroupTitle = findViewById(R.id.text_group_title);
|
||||||
@@ -110,33 +114,51 @@ class SubscriptionGroupFragment : MainFragment() {
|
|||||||
_textGroupMeta = findViewById(R.id.text_group_meta);
|
_textGroupMeta = findViewById(R.id.text_group_meta);
|
||||||
_buttonSettings = findViewById(R.id.button_settings);
|
_buttonSettings = findViewById(R.id.button_settings);
|
||||||
_buttonDelete = findViewById(R.id.button_delete);
|
_buttonDelete = findViewById(R.id.button_delete);
|
||||||
|
_buttonAddCreator = findViewById(R.id.button_creator_add);
|
||||||
_imageGroup.setBackgroundColor(Color.GRAY);
|
_imageGroup.setBackgroundColor(Color.GRAY);
|
||||||
|
|
||||||
|
_topbar.onClose.subscribe {
|
||||||
|
fragment.close(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
_buttonAddCreator.setOnClickListener {
|
||||||
|
addCreators();
|
||||||
|
}
|
||||||
|
|
||||||
val dp6 = 6.dp(resources);
|
val dp6 = 6.dp(resources);
|
||||||
_imageGroup.shapeAppearanceModel = ShapeAppearanceModel.builder()
|
_imageGroup.shapeAppearanceModel = ShapeAppearanceModel.builder()
|
||||||
.setAllCorners(CornerFamily.ROUNDED, dp6.toFloat())
|
.setAllCorners(CornerFamily.ROUNDED, dp6.toFloat())
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
_containerEnabled = findViewById(R.id.container_enabled);
|
|
||||||
_containerDisabled = findViewById(R.id.container_disabled);
|
|
||||||
_recyclerCreatorsEnabled = findViewById<RecyclerView>(R.id.recycler_creators_enabled).asAny(_enabledCreatorsFiltered) {
|
_recyclerCreatorsEnabled = findViewById<RecyclerView>(R.id.recycler_creators_enabled).asAny(_enabledCreatorsFiltered) {
|
||||||
it.itemView.setPadding(0, dp6, 0, dp6);
|
it.itemView.setPadding(0, dp6, 0, dp6);
|
||||||
it.onClick.subscribe { channel ->
|
it.onClick.subscribe { channel ->
|
||||||
disableCreator(channel);
|
//disableCreator(channel);
|
||||||
|
UIDialogs.showDialog(context, R.drawable.ic_trash, "Delete", "Are you sure you want to delete\n[${channel.name}]?", null, 0,
|
||||||
|
UIDialogs.Action("Cancel", {}),
|
||||||
|
UIDialogs.Action("Delete", {
|
||||||
|
_group?.let {
|
||||||
|
it.urls.remove(channel.url);
|
||||||
|
save();
|
||||||
|
reloadCreators(it);
|
||||||
|
}
|
||||||
|
}, UIDialogs.ActionStyle.DANGEROUS))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
_recyclerCreatorsDisabled = findViewById<RecyclerView>(R.id.recycler_creators_disabled).asAny(_disabledCreatorsFiltered) {
|
_recyclerCreatorsDisabled = findViewById<RecyclerView>(R.id.recycler_creators_disabled).asAny(_disabledCreatorsFiltered) {
|
||||||
it.itemView.setPadding(0, dp6, 0, dp6);
|
it.itemView.setPadding(0, dp6, 0, dp6);
|
||||||
it.onClick.subscribe { channel ->
|
it.onClick.subscribe { channel ->
|
||||||
enableCreator(channel);
|
enableCreator(channel);
|
||||||
};
|
};
|
||||||
}
|
}*/
|
||||||
_recyclerCreatorsEnabled.view.layoutManager = GridLayoutManager(context, 5).apply {
|
_recyclerCreatorsEnabled.view.layoutManager = GridLayoutManager(context, 5).apply {
|
||||||
this.orientation = LinearLayoutManager.VERTICAL;
|
this.orientation = LinearLayoutManager.VERTICAL;
|
||||||
};
|
};
|
||||||
|
/*
|
||||||
_recyclerCreatorsDisabled.view.layoutManager = GridLayoutManager(context, 5).apply {
|
_recyclerCreatorsDisabled.view.layoutManager = GridLayoutManager(context, 5).apply {
|
||||||
this.orientation = LinearLayoutManager.VERTICAL;
|
this.orientation = LinearLayoutManager.VERTICAL;
|
||||||
};
|
};*/
|
||||||
|
|
||||||
_textGroupTitleContainer.setOnClickListener {
|
_textGroupTitleContainer.setOnClickListener {
|
||||||
_group?.let { editName(it) };
|
_group?.let { editName(it) };
|
||||||
@@ -154,10 +176,15 @@ class SubscriptionGroupFragment : MainFragment() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
_buttonDelete.setOnClickListener {
|
_buttonDelete.setOnClickListener {
|
||||||
_group?.let {
|
_group?.let { g ->
|
||||||
StateSubscriptionGroups.instance.deleteSubscriptionGroup(it.id);
|
UIDialogs.showDialog(context, R.drawable.ic_trash, "Delete Group", "Are you sure you want to this group?\n[${g.name}]?", null, 0,
|
||||||
|
UIDialogs.Action("Cancel", {}),
|
||||||
|
UIDialogs.Action("Delete", {
|
||||||
|
StateSubscriptionGroups.instance.deleteSubscriptionGroup(g.id);
|
||||||
|
_didDelete = true;
|
||||||
|
fragment.close(true);
|
||||||
|
}, UIDialogs.ActionStyle.DANGEROUS))
|
||||||
};
|
};
|
||||||
fragment.close(true);
|
|
||||||
}
|
}
|
||||||
_buttonSettings.visibility = View.GONE;
|
_buttonSettings.visibility = View.GONE;
|
||||||
|
|
||||||
@@ -165,6 +192,12 @@ class SubscriptionGroupFragment : MainFragment() {
|
|||||||
filterCreators();
|
filterCreators();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_topbar.setButtons(
|
||||||
|
Pair(R.drawable.ic_share) {
|
||||||
|
UIDialogs.toast(context, "Coming soon");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
setGroup(null);
|
setGroup(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,9 +241,44 @@ class SubscriptionGroupFragment : MainFragment() {
|
|||||||
overlay.removeAllViews();
|
overlay.removeAllViews();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fun addCreators() {
|
||||||
|
val overlay = CreatorSelectOverlay(context, _enabledCreators.map { it.url });
|
||||||
|
_overlay.removeAllViews();
|
||||||
|
_overlay.addView(overlay);
|
||||||
|
_overlay.alpha = 0f
|
||||||
|
_overlay.visibility = View.VISIBLE;
|
||||||
|
_overlay.animate().alpha(1f).setDuration(300).start();
|
||||||
|
overlay.onSelected.subscribe {
|
||||||
|
_group?.let { g ->
|
||||||
|
if(g.urls.isEmpty() && g.image == null) {
|
||||||
|
//Obtain image
|
||||||
|
for(sub in it) {
|
||||||
|
val sub = StateSubscriptions.instance.getSubscription(sub);
|
||||||
|
if(sub != null && sub.channel.thumbnail != null) {
|
||||||
|
g.image = ImageVariable.fromUrl(sub.channel.thumbnail!!);
|
||||||
|
g.image?.setImageView(_imageGroup);
|
||||||
|
g.image?.setImageView(_imageGroupBackground);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for(url in it) {
|
||||||
|
if(!g.urls.contains(url))
|
||||||
|
g.urls.add(url);
|
||||||
|
}
|
||||||
|
save();
|
||||||
|
reloadCreators(g);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
overlay.onClose.subscribe {
|
||||||
|
_overlay.visibility = View.GONE;
|
||||||
|
overlay.removeAllViews();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fun setGroup(group: SubscriptionGroup?) {
|
fun setGroup(group: SubscriptionGroup?) {
|
||||||
|
_didDelete = false;
|
||||||
_group = group;
|
_group = group;
|
||||||
_textGroupTitle.text = group?.name;
|
_textGroupTitle.text = group?.name;
|
||||||
|
|
||||||
@@ -227,76 +295,39 @@ class SubscriptionGroupFragment : MainFragment() {
|
|||||||
reloadCreators(group);
|
reloadCreators(group);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onHide() {
|
||||||
|
if(!_didDelete && _group != null && StateSubscriptionGroups.instance.getSubscriptionGroup(_group!!.id) === null) {
|
||||||
|
UIDialogs.toast(context, "Group creation cancelled");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
private fun reloadCreators(group: SubscriptionGroup?) {
|
private fun reloadCreators(group: SubscriptionGroup?) {
|
||||||
_enabledCreators.clear();
|
_enabledCreators.clear();
|
||||||
_disabledCreators.clear();
|
//_disabledCreators.clear();
|
||||||
|
|
||||||
if(group != null) {
|
if(group != null) {
|
||||||
val urls = group.urls.toList();
|
val urls = group.urls.toList();
|
||||||
val subs = StateSubscriptions.instance.getSubscriptions().map { it.channel }
|
val subs = StateSubscriptions.instance.getSubscriptions().map { it.channel }
|
||||||
_enabledCreators.addAll(subs.filter { urls.contains(it.url) });
|
_enabledCreators.addAll(subs.filter { urls.contains(it.url) });
|
||||||
_disabledCreators.addAll(subs.filter { !urls.contains(it.url) });
|
|
||||||
}
|
}
|
||||||
|
updateMeta();
|
||||||
filterCreators();
|
filterCreators();
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun filterCreators() {
|
private fun filterCreators() {
|
||||||
val query = _searchBar.textSearch.text.toString().lowercase();
|
val query = _searchBar.textSearch.text.toString().lowercase();
|
||||||
val filteredEnabled = _enabledCreators.filter { it.name.lowercase().contains(query) };
|
val filteredEnabled = _enabledCreators.filter { it.name.lowercase().contains(query) };
|
||||||
val filteredDisabled = _disabledCreators.filter { it.name.lowercase().contains(query) };
|
|
||||||
|
|
||||||
//Optimize
|
//Optimize
|
||||||
_enabledCreatorsFiltered.clear();
|
_enabledCreatorsFiltered.clear();
|
||||||
_enabledCreatorsFiltered.addAll(filteredEnabled);
|
_enabledCreatorsFiltered.addAll(filteredEnabled);
|
||||||
_disabledCreatorsFiltered.clear();
|
|
||||||
_disabledCreatorsFiltered.addAll(filteredDisabled);
|
|
||||||
|
|
||||||
_recyclerCreatorsEnabled.notifyContentChanged();
|
_recyclerCreatorsEnabled.notifyContentChanged();
|
||||||
_recyclerCreatorsDisabled.notifyContentChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun enableCreator(channel: IPlatformChannel) {
|
|
||||||
val index = _disabledCreatorsFiltered.indexOf(channel);
|
|
||||||
if (index >= 0) {
|
|
||||||
_disabledCreators.remove(channel)
|
|
||||||
_disabledCreatorsFiltered.remove(channel);
|
|
||||||
_recyclerCreatorsDisabled.adapter.notifyItemRangeRemoved(index);
|
|
||||||
|
|
||||||
_enabledCreators.add(channel);
|
|
||||||
_enabledCreatorsFiltered.add(channel);
|
|
||||||
_recyclerCreatorsEnabled.adapter.notifyItemInserted(_enabledCreatorsFiltered.size - 1);
|
|
||||||
|
|
||||||
_group?.let {
|
|
||||||
if(!it.urls.contains(channel.url)) {
|
|
||||||
it.urls.add(channel.url);
|
|
||||||
save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
updateMeta();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private fun disableCreator(channel: IPlatformChannel) {
|
|
||||||
val index = _enabledCreatorsFiltered.indexOf(channel);
|
|
||||||
if (index >= 0) {
|
|
||||||
_enabledCreators.remove(channel)
|
|
||||||
_enabledCreatorsFiltered.removeAt(index);
|
|
||||||
_recyclerCreatorsEnabled.adapter.notifyItemRangeRemoved(index);
|
|
||||||
|
|
||||||
_disabledCreators.add(channel);
|
|
||||||
_disabledCreatorsFiltered.add(channel);
|
|
||||||
_recyclerCreatorsDisabled.adapter.notifyItemInserted(_disabledCreatorsFiltered.size - 1);
|
|
||||||
|
|
||||||
_group?.let {
|
|
||||||
it.urls.remove(channel.url);
|
|
||||||
save();
|
|
||||||
}
|
|
||||||
updateMeta();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateMeta() {
|
private fun updateMeta() {
|
||||||
_textGroupMeta.text = "${_enabledCreators.size} creators";
|
_textGroupMeta.text = "${_group?.urls?.size} creators";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+3
-1
@@ -107,12 +107,14 @@ class SubscriptionGroupListFragment : MainFragment() {
|
|||||||
updateGroups();
|
updateGroups();
|
||||||
}
|
}
|
||||||
|
|
||||||
if(topBar is AddTopBarFragment)
|
if(topBar is AddTopBarFragment) {
|
||||||
|
(topBar as AddTopBarFragment).onAdd.clear();
|
||||||
(topBar as AddTopBarFragment).onAdd.subscribe {
|
(topBar as AddTopBarFragment).onAdd.subscribe {
|
||||||
_overlay?.let {
|
_overlay?.let {
|
||||||
UISlideOverlays.showCreateSubscriptionGroup(it)
|
UISlideOverlays.showCreateSubscriptionGroup(it)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateGroups() {
|
private fun updateGroups() {
|
||||||
|
|||||||
+66
-26
@@ -10,6 +10,7 @@ import androidx.lifecycle.lifecycleScope
|
|||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
|
import com.futo.platformplayer.api.media.IPlatformClient
|
||||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
@@ -24,12 +25,14 @@ import com.futo.platformplayer.models.SearchType
|
|||||||
import com.futo.platformplayer.models.SubscriptionGroup
|
import com.futo.platformplayer.models.SubscriptionGroup
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StateCache
|
import com.futo.platformplayer.states.StateCache
|
||||||
|
import com.futo.platformplayer.states.StateHistory
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.platformplayer.states.StateSubscriptions
|
import com.futo.platformplayer.states.StateSubscriptions
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
import com.futo.platformplayer.stores.FragmentedStorageFileJson
|
import com.futo.platformplayer.stores.FragmentedStorageFileJson
|
||||||
import com.futo.platformplayer.views.FeedStyle
|
import com.futo.platformplayer.views.FeedStyle
|
||||||
import com.futo.platformplayer.views.NoResultsView
|
import com.futo.platformplayer.views.NoResultsView
|
||||||
|
import com.futo.platformplayer.views.ToastView
|
||||||
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
|
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
|
||||||
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||||
import com.futo.platformplayer.views.adapters.InsertedViewHolder
|
import com.futo.platformplayer.views.adapters.InsertedViewHolder
|
||||||
@@ -43,6 +46,7 @@ import kotlinx.coroutines.withContext
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import java.nio.channels.Channel
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import kotlin.system.measureTimeMillis
|
import kotlin.system.measureTimeMillis
|
||||||
|
|
||||||
@@ -52,6 +56,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
override val hasBottomBar: Boolean get() = true;
|
override val hasBottomBar: Boolean get() = true;
|
||||||
|
|
||||||
private var _view: SubscriptionsFeedView? = null;
|
private var _view: SubscriptionsFeedView? = null;
|
||||||
|
private var _group: SubscriptionGroup? = null;
|
||||||
private var _cachedRecyclerData: FeedView.RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null;
|
private var _cachedRecyclerData: FeedView.RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null;
|
||||||
|
|
||||||
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||||
@@ -72,6 +77,8 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
val view = SubscriptionsFeedView(this, inflater, _cachedRecyclerData);
|
val view = SubscriptionsFeedView(this, inflater, _cachedRecyclerData);
|
||||||
_view = view;
|
_view = view;
|
||||||
|
if(_group != null)
|
||||||
|
view.selectSubgroup(_group);
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,6 +87,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
val view = _view;
|
val view = _view;
|
||||||
if (view != null) {
|
if (view != null) {
|
||||||
_cachedRecyclerData = view.recyclerData;
|
_cachedRecyclerData = view.recyclerData;
|
||||||
|
_group = view.subGroup;
|
||||||
view.cleanup();
|
view.cleanup();
|
||||||
_view = null;
|
_view = null;
|
||||||
}
|
}
|
||||||
@@ -100,18 +108,11 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
class SubscriptionsFeedView : ContentFeedView<SubscriptionsFeedFragment> {
|
class SubscriptionsFeedView : ContentFeedView<SubscriptionsFeedFragment> {
|
||||||
override val shouldShowTimeBar: Boolean get() = Settings.instance.subscriptions.progressBar
|
override val shouldShowTimeBar: Boolean get() = Settings.instance.subscriptions.progressBar
|
||||||
|
|
||||||
private var _subGroup: SubscriptionGroup? = null;
|
var subGroup: SubscriptionGroup? = null;
|
||||||
|
|
||||||
constructor(fragment: SubscriptionsFeedFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData) {
|
constructor(fragment: SubscriptionsFeedFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData) {
|
||||||
Logger.i(TAG, "SubscriptionsFeedFragment constructor()");
|
Logger.i(TAG, "SubscriptionsFeedFragment constructor()");
|
||||||
StateSubscriptions.instance.onGlobalSubscriptionsUpdateProgress.subscribe(this) { progress, total ->
|
StateSubscriptions.instance.global.onUpdateProgress.subscribe(this) { progress, total ->
|
||||||
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
|
||||||
try {
|
|
||||||
setProgress(progress, total);
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.e(TAG, "Failed to set progress", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
StateSubscriptions.instance.onSubscriptionsChanged.subscribe(this) { _, added ->
|
StateSubscriptions.instance.onSubscriptionsChanged.subscribe(this) { _, added ->
|
||||||
@@ -137,8 +138,9 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
recyclerData.lastLoad.getNowDiffSeconds() > 60 ) {
|
recyclerData.lastLoad.getNowDiffSeconds() > 60 ) {
|
||||||
recyclerData.lastLoad = OffsetDateTime.now();
|
recyclerData.lastLoad = OffsetDateTime.now();
|
||||||
|
|
||||||
if(StateSubscriptions.instance.getOldestUpdateTime().getNowDiffMinutes() > 5 && Settings.instance.subscriptions.fetchOnTabOpen)
|
if(StateSubscriptions.instance.getOldestUpdateTime().getNowDiffMinutes() > 5 && Settings.instance.subscriptions.fetchOnTabOpen) {
|
||||||
loadResults(false);
|
loadResults(false);
|
||||||
|
}
|
||||||
else if(recyclerData.results.size == 0) {
|
else if(recyclerData.results.size == 0) {
|
||||||
loadCache();
|
loadCache();
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -162,15 +164,27 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!StateSubscriptions.instance.isGlobalUpdating) {
|
if (!StateSubscriptions.instance.global.isGlobalUpdating) {
|
||||||
finishRefreshLayoutLoader();
|
finishRefreshLayoutLoader();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StateSubscriptions.instance.onFeedProgress.subscribe(this) { id, progress, total ->
|
||||||
|
if(subGroup?.id == id)
|
||||||
|
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
try {
|
||||||
|
setProgress(progress, total);
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to set progress", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun cleanup() {
|
override fun cleanup() {
|
||||||
super.cleanup()
|
super.cleanup()
|
||||||
StateSubscriptions.instance.onGlobalSubscriptionsUpdateProgress.remove(this);
|
StateSubscriptions.instance.global.onUpdateProgress.remove(this);
|
||||||
StateSubscriptions.instance.onSubscriptionsChanged.remove(this);
|
StateSubscriptions.instance.onSubscriptionsChanged.remove(this);
|
||||||
|
StateSubscriptions.instance.onFeedProgress.remove(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
override val feedStyle: FeedStyle get() = Settings.instance.subscriptions.getSubscriptionsFeedStyle();
|
override val feedStyle: FeedStyle get() = Settings.instance.subscriptions.getSubscriptionsFeedStyle();
|
||||||
@@ -184,6 +198,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
val allowContentTypes: MutableList<ContentType> = mutableListOf(ContentType.MEDIA, ContentType.POST);
|
val allowContentTypes: MutableList<ContentType> = mutableListOf(ContentType.MEDIA, ContentType.POST);
|
||||||
var allowLive: Boolean = true;
|
var allowLive: Boolean = true;
|
||||||
var allowPlanned: Boolean = false;
|
var allowPlanned: Boolean = false;
|
||||||
|
var allowWatched: Boolean = true;
|
||||||
override fun encode(): String {
|
override fun encode(): String {
|
||||||
return Json.encodeToString(this);
|
return Json.encodeToString(this);
|
||||||
}
|
}
|
||||||
@@ -194,8 +209,9 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
private var _bypassRateLimit = false;
|
private var _bypassRateLimit = false;
|
||||||
private val _lastExceptions: List<Throwable>? = null;
|
private val _lastExceptions: List<Throwable>? = null;
|
||||||
private val _taskGetPager = TaskHandler<Boolean, IPager<IPlatformContent>>({StateApp.instance.scope}, { withRefresh ->
|
private val _taskGetPager = TaskHandler<Boolean, IPager<IPlatformContent>>({StateApp.instance.scope}, { withRefresh ->
|
||||||
|
val group = subGroup;
|
||||||
if(!_bypassRateLimit) {
|
if(!_bypassRateLimit) {
|
||||||
val subRequestCounts = StateSubscriptions.instance.getSubscriptionRequestCount();
|
val subRequestCounts = StateSubscriptions.instance.getSubscriptionRequestCount(group);
|
||||||
val reqCountStr = subRequestCounts.map { " ${it.key.config.name}: ${it.value}/${it.key.getSubscriptionRateLimit()}" }.joinToString("\n");
|
val reqCountStr = subRequestCounts.map { " ${it.key.config.name}: ${it.value}/${it.key.getSubscriptionRateLimit()}" }.joinToString("\n");
|
||||||
val rateLimitPlugins = subRequestCounts.filter { clientCount -> clientCount.key.getSubscriptionRateLimit()?.let { rateLimit -> clientCount.value > rateLimit } == true }
|
val rateLimitPlugins = subRequestCounts.filter { clientCount -> clientCount.key.getSubscriptionRateLimit()?.let { rateLimit -> clientCount.value > rateLimit } == true }
|
||||||
Logger.w(TAG, "Trying to refreshing subscriptions with requests:\n" + reqCountStr);
|
Logger.w(TAG, "Trying to refreshing subscriptions with requests:\n" + reqCountStr);
|
||||||
@@ -203,9 +219,10 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
throw RateLimitException(rateLimitPlugins.map { it.key.id });
|
throw RateLimitException(rateLimitPlugins.map { it.key.id });
|
||||||
}
|
}
|
||||||
_bypassRateLimit = false;
|
_bypassRateLimit = false;
|
||||||
val resp = StateSubscriptions.instance.getGlobalSubscriptionFeed(StateApp.instance.scope, withRefresh);
|
val resp = StateSubscriptions.instance.getGlobalSubscriptionFeed(StateApp.instance.scope, withRefresh, group);
|
||||||
|
val feed = StateSubscriptions.instance.getFeed(group?.id);
|
||||||
|
|
||||||
val currentExs = StateSubscriptions.instance.globalSubscriptionExceptions;
|
val currentExs = feed?.exceptions ?: listOf();
|
||||||
if(currentExs != _lastExceptions && currentExs.any())
|
if(currentExs != _lastExceptions && currentExs.any())
|
||||||
handleExceptions(currentExs);
|
handleExceptions(currentExs);
|
||||||
|
|
||||||
@@ -245,13 +262,18 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
Logger.w(ChannelFragment.TAG, "Failed to load channel.", it);
|
Logger.w(ChannelFragment.TAG, "Failed to load channel.", it);
|
||||||
if(it !is CancellationException)
|
if(it !is CancellationException)
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults(true) });
|
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults(true) }, null, fragment);
|
||||||
else {
|
else {
|
||||||
finishRefreshLayoutLoader();
|
finishRefreshLayoutLoader();
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fun selectSubgroup(g: SubscriptionGroup?) {
|
||||||
|
if(g != null)
|
||||||
|
_subscriptionBar?.selectGroup(g);
|
||||||
|
}
|
||||||
|
|
||||||
private fun initializeToolbarContent() {
|
private fun initializeToolbarContent() {
|
||||||
_subscriptionBar = SubscriptionBar(context).apply {
|
_subscriptionBar = SubscriptionBar(context).apply {
|
||||||
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||||
@@ -261,8 +283,17 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
if(g is SubscriptionGroup.Add)
|
if(g is SubscriptionGroup.Add)
|
||||||
UISlideOverlays.showCreateSubscriptionGroup(_overlayContainer);
|
UISlideOverlays.showCreateSubscriptionGroup(_overlayContainer);
|
||||||
else {
|
else {
|
||||||
_subGroup = g;
|
subGroup = g;
|
||||||
loadCache(); //TODO: Proper subset update
|
setProgress(0, 0);
|
||||||
|
if(Settings.instance.subscriptions.fetchOnTabOpen) {
|
||||||
|
loadCache();
|
||||||
|
loadResults(false);
|
||||||
|
}
|
||||||
|
else if(g != null && StateSubscriptions.instance.getFeed(g.id) != null) {
|
||||||
|
loadResults(false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
loadCache();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
_subscriptionBar?.onHoldGroup?.subscribe { g ->
|
_subscriptionBar?.onHoldGroup?.subscribe { g ->
|
||||||
@@ -275,7 +306,8 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
SubscriptionBar.Toggle(context.getString(R.string.videos), _filterSettings.allowContentTypes.contains(ContentType.MEDIA)) { toggleFilterContentTypes(listOf(ContentType.MEDIA, ContentType.NESTED_VIDEO), it); },
|
SubscriptionBar.Toggle(context.getString(R.string.videos), _filterSettings.allowContentTypes.contains(ContentType.MEDIA)) { toggleFilterContentTypes(listOf(ContentType.MEDIA, ContentType.NESTED_VIDEO), it); },
|
||||||
SubscriptionBar.Toggle(context.getString(R.string.posts), _filterSettings.allowContentTypes.contains(ContentType.POST)) { toggleFilterContentType(ContentType.POST, it); },
|
SubscriptionBar.Toggle(context.getString(R.string.posts), _filterSettings.allowContentTypes.contains(ContentType.POST)) { toggleFilterContentType(ContentType.POST, it); },
|
||||||
SubscriptionBar.Toggle(context.getString(R.string.live), _filterSettings.allowLive) { _filterSettings.allowLive = it; _filterSettings.save(); loadResults(false); },
|
SubscriptionBar.Toggle(context.getString(R.string.live), _filterSettings.allowLive) { _filterSettings.allowLive = it; _filterSettings.save(); loadResults(false); },
|
||||||
SubscriptionBar.Toggle(context.getString(R.string.planned), _filterSettings.allowPlanned) { _filterSettings.allowPlanned = it; _filterSettings.save(); loadResults(false); }
|
SubscriptionBar.Toggle(context.getString(R.string.planned), _filterSettings.allowPlanned) { _filterSettings.allowPlanned = it; _filterSettings.save(); loadResults(false); },
|
||||||
|
SubscriptionBar.Toggle(context.getString(R.string.watched), _filterSettings.allowWatched) { _filterSettings.allowWatched = it; _filterSettings.save(); loadResults(false); }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,10 +335,13 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
|
|
||||||
override fun filterResults(results: List<IPlatformContent>): List<IPlatformContent> {
|
override fun filterResults(results: List<IPlatformContent>): List<IPlatformContent> {
|
||||||
val nowSoon = OffsetDateTime.now().plusMinutes(5);
|
val nowSoon = OffsetDateTime.now().plusMinutes(5);
|
||||||
val filterGroup = _subGroup;
|
val filterGroup = subGroup;
|
||||||
return results.filter {
|
return results.filter {
|
||||||
val allowedContentType = _filterSettings.allowContentTypes.contains(if(it.contentType == ContentType.NESTED_VIDEO || it.contentType == ContentType.LOCKED) ContentType.MEDIA else it.contentType);
|
val allowedContentType = _filterSettings.allowContentTypes.contains(if(it.contentType == ContentType.NESTED_VIDEO || it.contentType == ContentType.LOCKED) ContentType.MEDIA else it.contentType);
|
||||||
|
|
||||||
|
if(it is IPlatformVideo && it.duration > 0 && !_filterSettings.allowWatched && StateHistory.instance.isHistoryWatched(it.url, it.duration))
|
||||||
|
return@filter false;
|
||||||
|
|
||||||
//TODO: Check against a sub cache
|
//TODO: Check against a sub cache
|
||||||
if(filterGroup != null && !filterGroup.urls.contains(it.author.url))
|
if(filterGroup != null && !filterGroup.urls.contains(it.author.url))
|
||||||
return@filter false;
|
return@filter false;
|
||||||
@@ -403,7 +438,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
context?.let {
|
context?.let {
|
||||||
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||||
try {
|
try {
|
||||||
if (exs.size <= 8) {
|
if (exs.size <= 3) {
|
||||||
for (ex in exs) {
|
for (ex in exs) {
|
||||||
var toShow = ex;
|
var toShow = ex;
|
||||||
var channel: String? = null;
|
var channel: String? = null;
|
||||||
@@ -413,15 +448,17 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
Logger.e(TAG, "Channel [${channel}] failed", ex);
|
Logger.e(TAG, "Channel [${channel}] failed", ex);
|
||||||
if (toShow is PluginException)
|
if (toShow is PluginException)
|
||||||
UIDialogs.toast(
|
UIDialogs.appToast(ToastView.Toast(
|
||||||
it,
|
toShow.message +
|
||||||
context.getString(R.string.plugin_pluginname_failed_message).replace("{pluginName}", toShow.config.name).replace("{message}", toShow.message ?: "")
|
(if(channel != null) "\nChannel: " + channel else ""), false, null,
|
||||||
|
"Plugin ${toShow.config.name} failed")
|
||||||
);
|
);
|
||||||
else
|
else
|
||||||
UIDialogs.toast(it, ex.message ?: "");
|
UIDialogs.appToast(ex.message ?: "");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
val failedChannels = exs.filterIsInstance<ChannelException>().map { it.channelNameOrUrl }.distinct().toList();
|
||||||
val failedPlugins = exs.filter { it is PluginException || (it is ChannelException && it.cause is PluginException) }
|
val failedPlugins = exs.filter { it is PluginException || (it is ChannelException && it.cause is PluginException) }
|
||||||
.map { if(it is ChannelException) (it.cause as PluginException) else if(it is PluginException) it else null }
|
.map { if(it is ChannelException) (it.cause as PluginException) else if(it is PluginException) it else null }
|
||||||
.filter { it != null }
|
.filter { it != null }
|
||||||
@@ -429,7 +466,10 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
.map { it!! }
|
.map { it!! }
|
||||||
.toList();
|
.toList();
|
||||||
for(distinctPluginFail in failedPlugins)
|
for(distinctPluginFail in failedPlugins)
|
||||||
UIDialogs.toast(it, context.getString(R.string.plugin_pluginname_failed_message).replace("{pluginName}", distinctPluginFail.config.name).replace("{message}", distinctPluginFail.message ?: ""));
|
UIDialogs.appToast(context.getString(R.string.plugin_pluginname_failed_message).replace("{pluginName}", distinctPluginFail.config.name).replace("{message}", distinctPluginFail.message ?: ""));
|
||||||
|
if(failedChannels.isNotEmpty())
|
||||||
|
UIDialogs.appToast(ToastView.Toast(failedChannels.take(3).map { "- ${it}" }.joinToString("\n") +
|
||||||
|
(if(failedChannels.size >= 3) "\nAnd ${failedChannels.size - 3} more" else ""), false, null, "Failed Channels"));
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to handle exceptions", e)
|
Logger.e(TAG, "Failed to handle exceptions", e)
|
||||||
|
|||||||
+1
-1
@@ -40,7 +40,7 @@ class SuggestionsFragment : MainFragment {
|
|||||||
.success { suggestions -> updateSuggestions(suggestions, false) }
|
.success { suggestions -> updateSuggestions(suggestions, false) }
|
||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
Logger.w(ChannelFragment.TAG, "Failed to load suggestions.", it);
|
Logger.w(ChannelFragment.TAG, "Failed to load suggestions.", it);
|
||||||
UIDialogs.showGeneralRetryErrorDialog(requireContext(), it.message ?: "", it, { loadSuggestions() });
|
UIDialogs.showGeneralRetryErrorDialog(requireContext(), it.message ?: "", it, { loadSuggestions() }, null, this);
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(): super() {
|
constructor(): super() {
|
||||||
|
|||||||
+221
@@ -0,0 +1,221 @@
|
|||||||
|
package com.futo.platformplayer.fragment.mainactivity.main
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import com.futo.platformplayer.*
|
||||||
|
import com.futo.platformplayer.api.media.IPlatformClient
|
||||||
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
|
import com.futo.platformplayer.api.media.models.Thumbnail
|
||||||
|
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||||
|
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||||
|
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
||||||
|
import com.futo.platformplayer.api.media.models.ratings.IRating
|
||||||
|
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.VideoUrlSource
|
||||||
|
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
||||||
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||||
|
import com.futo.platformplayer.api.media.structures.EmptyPager
|
||||||
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFragment
|
||||||
|
import com.futo.platformplayer.views.pills.WidePillButton
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
|
class TutorialFragment : MainFragment() {
|
||||||
|
override val isMainView : Boolean = true;
|
||||||
|
override val isTab: Boolean = true;
|
||||||
|
override val hasBottomBar: Boolean get() = true;
|
||||||
|
|
||||||
|
private var _view: TutorialView? = null;
|
||||||
|
|
||||||
|
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||||
|
super.onShownWithView(parameter, isBack);
|
||||||
|
(topBar as NavigationTopBarFragment?)?.onShown(getString(R.string.tutorials));
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
|
val view = TutorialView(this, inflater);
|
||||||
|
_view = view;
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyMainView() {
|
||||||
|
super.onDestroyMainView();
|
||||||
|
_view = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ViewConstructor")
|
||||||
|
class TutorialView : LinearLayout {
|
||||||
|
val fragment: TutorialFragment
|
||||||
|
|
||||||
|
constructor(fragment: TutorialFragment, inflater: LayoutInflater) : super(inflater.context) {
|
||||||
|
this.fragment = fragment
|
||||||
|
|
||||||
|
orientation = VERTICAL
|
||||||
|
|
||||||
|
addView(createHeader("Initial setup"))
|
||||||
|
initialSetupVideos.forEach {
|
||||||
|
addView(createTutorialPill(R.drawable.ic_movie, it.name, it.description).apply {
|
||||||
|
onClick.subscribe {
|
||||||
|
fragment.navigate<VideoDetailFragment>(it)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
addView(createHeader("Features"))
|
||||||
|
featuresVideos.forEach {
|
||||||
|
addView(createTutorialPill(R.drawable.ic_movie, it.name, it.description).apply {
|
||||||
|
onClick.subscribe {
|
||||||
|
fragment.navigate<VideoDetailFragment>(it)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createHeader(t: String): TextView {
|
||||||
|
return TextView(context).apply {
|
||||||
|
textSize = 24.0f
|
||||||
|
typeface = resources.getFont(R.font.inter_regular)
|
||||||
|
text = t
|
||||||
|
layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT).apply {
|
||||||
|
setMargins(15.dp(resources), 10.dp(resources), 15.dp(resources), 12.dp(resources))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createTutorialPill(iconPrefix: Int, t: String, d: String): WidePillButton {
|
||||||
|
return WidePillButton(context).apply {
|
||||||
|
setIconPrefix(iconPrefix)
|
||||||
|
setText(t)
|
||||||
|
setDescription(d)
|
||||||
|
setIconSuffix(R.drawable.ic_play_notif)
|
||||||
|
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).apply {
|
||||||
|
setMargins(15.dp(resources), 0, 15.dp(resources), 12.dp(resources))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TutorialVideoSourceDescriptor(url: String, duration: Long, width: Int, height: Int) : VideoUnMuxedSourceDescriptor() {
|
||||||
|
override val videoSources: Array<IVideoSource> = arrayOf(
|
||||||
|
VideoUrlSource("Original", url, width, height, duration, "video/mp4")
|
||||||
|
)
|
||||||
|
override val audioSources: Array<IAudioSource> = arrayOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
class TutorialVideo(
|
||||||
|
uuid: String,
|
||||||
|
override val name: String,
|
||||||
|
override val description: String,
|
||||||
|
thumbnailUrl: String,
|
||||||
|
videoUrl: String,
|
||||||
|
override val duration: Long,
|
||||||
|
width: Int = 1920,
|
||||||
|
height: Int = 1080
|
||||||
|
) : IPlatformVideoDetails {
|
||||||
|
override val id: PlatformID = PlatformID("tutorial", uuid)
|
||||||
|
override val contentType: ContentType = ContentType.MEDIA
|
||||||
|
override val preview: IVideoSourceDescriptor? = null
|
||||||
|
override val live: IVideoSource? = null
|
||||||
|
override val dash: IDashManifestSource? = null
|
||||||
|
override val hls: IHLSManifestSource? = null
|
||||||
|
override val subtitles: List<ISubtitleSource> = emptyList()
|
||||||
|
override val shareUrl: String = videoUrl
|
||||||
|
override val url: String = videoUrl
|
||||||
|
override val datetime: OffsetDateTime? = OffsetDateTime.parse("2023-12-18T00:00:00Z")
|
||||||
|
override val thumbnails: Thumbnails = Thumbnails(arrayOf(Thumbnail(thumbnailUrl)))
|
||||||
|
override val author: PlatformAuthorLink = PlatformAuthorLink(PlatformID("tutorial", "f422ced6-b551-4b62-818e-27a4f5f4918a"), "Grayjay", "", "https://releases.grayjay.app/tutorials/author.jpeg")
|
||||||
|
override val isLive: Boolean = false
|
||||||
|
override val rating: IRating = RatingLikes(-1)
|
||||||
|
override val viewCount: Long = -1
|
||||||
|
override val video: IVideoSourceDescriptor = TutorialVideoSourceDescriptor(videoUrl, duration, width, height)
|
||||||
|
override fun getComments(client: IPlatformClient): IPager<IPlatformComment> {
|
||||||
|
return EmptyPager()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getPlaybackTracker(): IPlaybackTracker? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val TAG = "HomeFragment";
|
||||||
|
|
||||||
|
fun newInstance() = TutorialFragment().apply {}
|
||||||
|
val initialSetupVideos = listOf(
|
||||||
|
TutorialVideo(
|
||||||
|
uuid = "228be579-ec52-4d93-b9eb-ca74ec08c58a",
|
||||||
|
name = "How to install",
|
||||||
|
description = "Learn how to install Grayjay.",
|
||||||
|
thumbnailUrl = "https://releases.grayjay.app/tutorials/how-to-install.jpg",
|
||||||
|
videoUrl = "https://releases.grayjay.app/tutorials/how-to-install.mp4",
|
||||||
|
duration = 52
|
||||||
|
),
|
||||||
|
TutorialVideo(
|
||||||
|
uuid = "3b99ebfe-2640-4643-bfe0-a0cf04261fc5",
|
||||||
|
name = "Getting started",
|
||||||
|
description = "Learn how to get started with Grayjay. How do you install plugins?",
|
||||||
|
thumbnailUrl = "https://releases.grayjay.app/tutorials/getting-started.jpg",
|
||||||
|
videoUrl = "https://releases.grayjay.app/tutorials/getting-started.mp4",
|
||||||
|
duration = 50
|
||||||
|
),
|
||||||
|
TutorialVideo(
|
||||||
|
uuid = "793aa009-516c-4581-b82f-a8efdfef4c27",
|
||||||
|
name = "Is Grayjay free?",
|
||||||
|
description = "Learn how Grayjay is monetized. How do we make money?",
|
||||||
|
thumbnailUrl = "https://releases.grayjay.app/tutorials/pay.jpg",
|
||||||
|
videoUrl = "https://releases.grayjay.app/tutorials/pay.mp4",
|
||||||
|
duration = 52
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val featuresVideos = listOf(
|
||||||
|
TutorialVideo(
|
||||||
|
uuid = "d2238d88-4252-4a91-a12d-b90c049bb7cf",
|
||||||
|
name = "Searching",
|
||||||
|
description = "Learn about searching in Grayjay. How can I find channels, videos or playlists?",
|
||||||
|
thumbnailUrl = "https://releases.grayjay.app/tutorials/search.jpg",
|
||||||
|
videoUrl = "https://releases.grayjay.app/tutorials/search.mp4",
|
||||||
|
duration = 39
|
||||||
|
),
|
||||||
|
TutorialVideo(
|
||||||
|
uuid = "d2238d88-4252-4a91-a12d-b90c049bb7cf",
|
||||||
|
name = "Comments",
|
||||||
|
description = "Learn about Polycentric comments in Grayjay.",
|
||||||
|
thumbnailUrl = "https://releases.grayjay.app/tutorials/polycentric.jpg",
|
||||||
|
videoUrl = "https://releases.grayjay.app/tutorials/polycentric.mp4",
|
||||||
|
duration = 64
|
||||||
|
),
|
||||||
|
TutorialVideo(
|
||||||
|
uuid = "94d36959-e3fc-4c24-a988-89147067a179",
|
||||||
|
name = "Casting",
|
||||||
|
description = "Learn about casting in Grayjay. How do I show video on my TV?",
|
||||||
|
thumbnailUrl = "https://releases.grayjay.app/tutorials/how-to-cast.jpg",
|
||||||
|
videoUrl = "https://releases.grayjay.app/tutorials/how-to-cast.mp4",
|
||||||
|
duration = 79
|
||||||
|
),
|
||||||
|
TutorialVideo(
|
||||||
|
uuid = "5128c2e3-852b-4281-869b-efea2ec82a0e",
|
||||||
|
name = "Monetization",
|
||||||
|
description = "How can I monetize as a creator?",
|
||||||
|
thumbnailUrl = "https://releases.grayjay.app/tutorials/monetization.jpg",
|
||||||
|
videoUrl = "https://releases.grayjay.app/tutorials/monetization.mp4",
|
||||||
|
duration = 47,
|
||||||
|
1080,
|
||||||
|
1920
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-10
@@ -25,8 +25,6 @@ import com.futo.platformplayer.logging.Logger
|
|||||||
import com.futo.platformplayer.models.PlatformVideoWithTime
|
import com.futo.platformplayer.models.PlatformVideoWithTime
|
||||||
import com.futo.platformplayer.models.UrlVideoWithTime
|
import com.futo.platformplayer.models.UrlVideoWithTime
|
||||||
import com.futo.platformplayer.states.StatePlayer
|
import com.futo.platformplayer.states.StatePlayer
|
||||||
import com.futo.platformplayer.states.StateSaved
|
|
||||||
import com.futo.platformplayer.states.VideoToOpen
|
|
||||||
import com.futo.platformplayer.views.containers.SingleViewTouchableMotionLayout
|
import com.futo.platformplayer.views.containers.SingleViewTouchableMotionLayout
|
||||||
|
|
||||||
class VideoDetailFragment : MainFragment {
|
class VideoDetailFragment : MainFragment {
|
||||||
@@ -171,14 +169,14 @@ class VideoDetailFragment : MainFragment {
|
|||||||
_view!!.transitionToStart();
|
_view!!.transitionToStart();
|
||||||
}
|
}
|
||||||
fun maximizeVideoDetail(instant: Boolean = false) {
|
fun maximizeVideoDetail(instant: Boolean = false) {
|
||||||
if(_maximizeProgress > 0.9f && state != State.MAXIMIZED) {
|
if((_maximizeProgress > 0.9f || instant) && state != State.MAXIMIZED) {
|
||||||
state = State.MAXIMIZED;
|
state = State.MAXIMIZED;
|
||||||
onMaximized.emit();
|
onMaximized.emit();
|
||||||
}
|
}
|
||||||
_view?.let {
|
_view?.let {
|
||||||
if(!instant)
|
if(!instant) {
|
||||||
it.transitionToEnd();
|
it.transitionToEnd();
|
||||||
else {
|
} else {
|
||||||
it.progress = 1f;
|
it.progress = 1f;
|
||||||
onTransitioning.emit(true);
|
onTransitioning.emit(true);
|
||||||
}
|
}
|
||||||
@@ -372,11 +370,6 @@ class VideoDetailFragment : MainFragment {
|
|||||||
|
|
||||||
Logger.v(TAG, "shouldStop: $shouldStop");
|
Logger.v(TAG, "shouldStop: $shouldStop");
|
||||||
if(shouldStop) {
|
if(shouldStop) {
|
||||||
_viewDetail?.let {
|
|
||||||
val v = it.video ?: return@let;
|
|
||||||
StateSaved.instance.setVideoToOpenBlocking(VideoToOpen(v.url, (it.lastPositionMilliseconds / 1000.0f).toLong()));
|
|
||||||
}
|
|
||||||
|
|
||||||
_viewDetail?.onStop();
|
_viewDetail?.onStop();
|
||||||
StateCasting.instance.onStop();
|
StateCasting.instance.onStop();
|
||||||
Logger.v(TAG, "called onStop() shouldStop: $shouldStop");
|
Logger.v(TAG, "called onStop() shouldStop: $shouldStop");
|
||||||
@@ -431,6 +424,7 @@ class VideoDetailFragment : MainFragment {
|
|||||||
changeOrientation(OrientationManager.Orientation.PORTRAIT);
|
changeOrientation(OrientationManager.Orientation.PORTRAIT);
|
||||||
}
|
}
|
||||||
isFullscreen = fullscreen;
|
isFullscreen = fullscreen;
|
||||||
|
_view?.allowMotion = !fullscreen;
|
||||||
}
|
}
|
||||||
private fun changeOrientation(orientation: OrientationManager.Orientation) {
|
private fun changeOrientation(orientation: OrientationManager.Orientation) {
|
||||||
Logger.i(TAG, "Orientation Change:" + orientation.name);
|
Logger.i(TAG, "Orientation Change:" + orientation.name);
|
||||||
|
|||||||
+209
-110
@@ -23,6 +23,7 @@ import android.view.View
|
|||||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
|
import android.webkit.WebView
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
@@ -50,6 +51,7 @@ import com.futo.platformplayer.api.media.exceptions.ContentNotAvailableYetExcept
|
|||||||
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
|
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
|
||||||
import com.futo.platformplayer.api.media.models.PlatformAuthorMembershipLink
|
import com.futo.platformplayer.api.media.models.PlatformAuthorMembershipLink
|
||||||
import com.futo.platformplayer.api.media.models.chapters.ChapterType
|
import com.futo.platformplayer.api.media.models.chapters.ChapterType
|
||||||
|
import com.futo.platformplayer.api.media.models.chapters.IChapter
|
||||||
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
|
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
|
||||||
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
|
||||||
@@ -123,6 +125,7 @@ import com.futo.platformplayer.views.overlays.LiveChatOverlay
|
|||||||
import com.futo.platformplayer.views.overlays.QueueEditorOverlay
|
import com.futo.platformplayer.views.overlays.QueueEditorOverlay
|
||||||
import com.futo.platformplayer.views.overlays.RepliesOverlay
|
import com.futo.platformplayer.views.overlays.RepliesOverlay
|
||||||
import com.futo.platformplayer.views.overlays.SupportOverlay
|
import com.futo.platformplayer.views.overlays.SupportOverlay
|
||||||
|
import com.futo.platformplayer.views.overlays.WebviewOverlay
|
||||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuButtonList
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuButtonList
|
||||||
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
|
||||||
@@ -144,10 +147,11 @@ import com.futo.polycentric.core.Opinion
|
|||||||
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||||
import com.google.protobuf.ByteString
|
import com.google.protobuf.ByteString
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import userpackage.Protocol
|
import userpackage.Protocol
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
@@ -242,6 +246,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
private val _container_content_replies: RepliesOverlay;
|
private val _container_content_replies: RepliesOverlay;
|
||||||
private val _container_content_description: DescriptionOverlay;
|
private val _container_content_description: DescriptionOverlay;
|
||||||
private val _container_content_liveChat: LiveChatOverlay;
|
private val _container_content_liveChat: LiveChatOverlay;
|
||||||
|
private val _container_content_browser: WebviewOverlay;
|
||||||
private val _container_content_support: SupportOverlay;
|
private val _container_content_support: SupportOverlay;
|
||||||
|
|
||||||
private var _container_content_current: View;
|
private var _container_content_current: View;
|
||||||
@@ -251,6 +256,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
private val _layoutRating: LinearLayout;
|
private val _layoutRating: LinearLayout;
|
||||||
private val _imageDislikeIcon: ImageView;
|
private val _imageDislikeIcon: ImageView;
|
||||||
private val _imageLikeIcon: ImageView;
|
private val _imageLikeIcon: ImageView;
|
||||||
|
private val _layoutToggleCommentSection: LinearLayout;
|
||||||
|
|
||||||
private val _monetization: MonetizationView;
|
private val _monetization: MonetizationView;
|
||||||
|
|
||||||
@@ -327,6 +333,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_upNext = findViewById(R.id.up_next);
|
_upNext = findViewById(R.id.up_next);
|
||||||
_textCommentType = findViewById(R.id.text_comment_type);
|
_textCommentType = findViewById(R.id.text_comment_type);
|
||||||
_toggleCommentType = findViewById(R.id.toggle_comment_type);
|
_toggleCommentType = findViewById(R.id.toggle_comment_type);
|
||||||
|
_layoutToggleCommentSection = findViewById(R.id.layout_toggle_comment_section);
|
||||||
|
|
||||||
_overlayContainer = findViewById(R.id.overlay_container);
|
_overlayContainer = findViewById(R.id.overlay_container);
|
||||||
_overlay_quality_container = findViewById(R.id.videodetail_quality_overview);
|
_overlay_quality_container = findViewById(R.id.videodetail_quality_overview);
|
||||||
@@ -345,7 +352,8 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_container_content_replies = findViewById(R.id.videodetail_container_replies);
|
_container_content_replies = findViewById(R.id.videodetail_container_replies);
|
||||||
_container_content_description = findViewById(R.id.videodetail_container_description);
|
_container_content_description = findViewById(R.id.videodetail_container_description);
|
||||||
_container_content_liveChat = findViewById(R.id.videodetail_container_livechat);
|
_container_content_liveChat = findViewById(R.id.videodetail_container_livechat);
|
||||||
_container_content_support = findViewById(R.id.videodetail_container_support)
|
_container_content_support = findViewById(R.id.videodetail_container_support);
|
||||||
|
_container_content_browser = findViewById(R.id.videodetail_container_webview)
|
||||||
|
|
||||||
_textComments = findViewById(R.id.text_comments);
|
_textComments = findViewById(R.id.text_comments);
|
||||||
_addCommentView = findViewById(R.id.add_comment_view);
|
_addCommentView = findViewById(R.id.add_comment_view);
|
||||||
@@ -369,7 +377,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
|
|
||||||
|
|
||||||
_buttonSubscribe.onSubscribed.subscribe {
|
_buttonSubscribe.onSubscribed.subscribe {
|
||||||
UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer);
|
_slideUpOverlay = UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer);
|
||||||
};
|
};
|
||||||
|
|
||||||
_container_content_liveChat.onRaidNow.subscribe {
|
_container_content_liveChat.onRaidNow.subscribe {
|
||||||
@@ -394,6 +402,10 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
_monetization.onUrlTap.subscribe {
|
||||||
|
fragment.navigate<BrowserFragment>(it);
|
||||||
|
onMinimize.emit();
|
||||||
|
}
|
||||||
|
|
||||||
_player.attachPlayer();
|
_player.attachPlayer();
|
||||||
|
|
||||||
@@ -433,18 +445,21 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_buttonPins.alwaysShowLastButton = true;
|
_buttonPins.alwaysShowLastButton = true;
|
||||||
|
|
||||||
var buttonMore: RoundButton? = null;
|
var buttonMore: RoundButton? = null;
|
||||||
buttonMore = RoundButton(context, R.drawable.ic_menu, "More", TAG_MORE) {
|
buttonMore = RoundButton(context, R.drawable.ic_menu, context.getString(R.string.more), TAG_MORE) {
|
||||||
_slideUpOverlay = UISlideOverlays.showMoreButtonOverlay(_overlayContainer, _buttonPins, listOf(TAG_MORE)) {selected ->
|
_slideUpOverlay = UISlideOverlays.showMoreButtonOverlay(_overlayContainer, _buttonPins, listOf(TAG_MORE), false) {selected ->
|
||||||
_buttonPins.setButtons(*(selected + listOf(buttonMore!!)).toTypedArray());
|
_buttonPins.setButtons(*(selected + listOf(buttonMore!!)).toTypedArray());
|
||||||
_buttonPinStore.set(*selected.filter { it.tagRef is String }.map{ it.tagRef as String }.toTypedArray())
|
_buttonPinStore.set(*selected.filter { it.tagRef is String }.map{ it.tagRef as String }.toTypedArray())
|
||||||
_buttonPinStore.save();
|
_buttonPinStore.save();
|
||||||
}
|
};
|
||||||
};
|
};
|
||||||
_buttonMore = buttonMore;
|
_buttonMore = buttonMore;
|
||||||
updateMoreButtons();
|
updateMoreButtons();
|
||||||
|
|
||||||
|
|
||||||
_channelButton.setOnClickListener {
|
_channelButton.setOnClickListener {
|
||||||
|
if (video is TutorialFragment.TutorialVideo) {
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
|
|
||||||
(video?.author ?: _searchVideo?.author)?.let {
|
(video?.author ?: _searchVideo?.author)?.let {
|
||||||
fragment.navigate<ChannelFragment>(it);
|
fragment.navigate<ChannelFragment>(it);
|
||||||
fragment.lifecycleScope.launch {
|
fragment.lifecycleScope.launch {
|
||||||
@@ -459,20 +474,29 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_cast.onSettingsClick.subscribe { showVideoSettings() };
|
_cast.onSettingsClick.subscribe { showVideoSettings() };
|
||||||
_player.onVideoSettings.subscribe { showVideoSettings() };
|
_player.onVideoSettings.subscribe { showVideoSettings() };
|
||||||
_player.onToggleFullScreen.subscribe(::handleFullScreen);
|
_player.onToggleFullScreen.subscribe(::handleFullScreen);
|
||||||
_player.onChapterChanged.subscribe { chapter, isScrub ->
|
|
||||||
|
val onChapterChanged = { chapter: IChapter?, isScrub: Boolean ->
|
||||||
if(_layoutSkip.visibility == VISIBLE && chapter?.type != ChapterType.SKIPPABLE)
|
if(_layoutSkip.visibility == VISIBLE && chapter?.type != ChapterType.SKIPPABLE)
|
||||||
_layoutSkip.visibility = GONE;
|
_layoutSkip.visibility = GONE;
|
||||||
|
|
||||||
if(!isScrub) {
|
if(!isScrub) {
|
||||||
if(chapter?.type == ChapterType.SKIPPABLE) {
|
if(chapter?.type == ChapterType.SKIPPABLE) {
|
||||||
_layoutSkip.visibility = VISIBLE;
|
_layoutSkip.visibility = VISIBLE;
|
||||||
}
|
} else if(chapter?.type == ChapterType.SKIP || chapter?.type == ChapterType.SKIPONCE) {
|
||||||
else if(chapter?.type == ChapterType.SKIP) {
|
val ad = StateCasting.instance.activeDevice
|
||||||
_player.seekTo((chapter.timeEnd * 1000).toLong());
|
if (ad != null) {
|
||||||
|
ad.seekVideo(chapter.timeEnd)
|
||||||
|
} else {
|
||||||
|
_player.seekTo((chapter.timeEnd * 1000).toLong());
|
||||||
|
}
|
||||||
|
|
||||||
UIDialogs.toast(context, "Skipped chapter [${chapter.name}]", false);
|
UIDialogs.toast(context, "Skipped chapter [${chapter.name}]", false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
_player.onChapterChanged.subscribe(onChapterChanged);
|
||||||
|
_cast.onChapterChanged.subscribe(onChapterChanged);
|
||||||
|
|
||||||
_cast.onMinimizeClick.subscribe {
|
_cast.onMinimizeClick.subscribe {
|
||||||
_player.setFullScreen(false);
|
_player.setFullScreen(false);
|
||||||
@@ -604,6 +628,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_container_content_queue.onClose.subscribe { switchContentView(_container_content_main); };
|
_container_content_queue.onClose.subscribe { switchContentView(_container_content_main); };
|
||||||
_container_content_replies.onClose.subscribe { switchContentView(_container_content_main); };
|
_container_content_replies.onClose.subscribe { switchContentView(_container_content_main); };
|
||||||
_container_content_support.onClose.subscribe { switchContentView(_container_content_main); };
|
_container_content_support.onClose.subscribe { switchContentView(_container_content_main); };
|
||||||
|
_container_content_browser.onClose.subscribe { switchContentView(_container_content_main); };
|
||||||
|
|
||||||
_description_viewMore.setOnClickListener {
|
_description_viewMore.setOnClickListener {
|
||||||
switchContentView(_container_content_description);
|
switchContentView(_container_content_description);
|
||||||
@@ -624,6 +649,20 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
|
|
||||||
_container_content_current = _container_content_main;
|
_container_content_current = _container_content_main;
|
||||||
|
|
||||||
|
_commentsList.onAuthorClick.subscribe { c ->
|
||||||
|
if (c !is PolycentricPlatformComment) {
|
||||||
|
return@subscribe;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.i(TAG, "onAuthorClick: " + c.author.id.value);
|
||||||
|
if(c.author.id.value?.startsWith("polycentric://") ?: false) {
|
||||||
|
//val navUrl = "https://harbor.social/" + c.author.id.value?.substring("polycentric://".length);
|
||||||
|
val navUrl = "https://polycentric.io/user/" + c.author.id.value?.substring("polycentric://".length);
|
||||||
|
fragment.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(navUrl)))
|
||||||
|
//_container_content_browser.goto(navUrl);
|
||||||
|
//switchContentView(_container_content_browser);
|
||||||
|
}
|
||||||
|
};
|
||||||
_commentsList.onRepliesClick.subscribe { c ->
|
_commentsList.onRepliesClick.subscribe { c ->
|
||||||
val replyCount = c.replyCount ?: 0;
|
val replyCount = c.replyCount ?: 0;
|
||||||
var metadata = "";
|
var metadata = "";
|
||||||
@@ -667,9 +706,17 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
};
|
};
|
||||||
|
|
||||||
_layoutSkip.setOnClickListener {
|
_layoutSkip.setOnClickListener {
|
||||||
val currentChapter = _player.getCurrentChapter(_player.position);
|
val ad = StateCasting.instance.activeDevice;
|
||||||
if(currentChapter?.type == ChapterType.SKIPPABLE) {
|
if (ad != null) {
|
||||||
_player.seekTo((currentChapter.timeEnd * 1000).toLong());
|
val currentChapter = _cast.getCurrentChapter((ad.time * 1000).toLong());
|
||||||
|
if(currentChapter?.type == ChapterType.SKIPPABLE) {
|
||||||
|
ad.seekVideo(currentChapter.timeEnd);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val currentChapter = _player.getCurrentChapter(_player.position);
|
||||||
|
if(currentChapter?.type == ChapterType.SKIPPABLE) {
|
||||||
|
_player.seekTo((currentChapter.timeEnd * 1000).toLong());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -737,7 +784,9 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
fun updateMoreButtons() {
|
fun updateMoreButtons() {
|
||||||
val buttons = listOf(RoundButton(context, R.drawable.ic_add, context.getString(R.string.add), TAG_ADD) {
|
val buttons = listOf(RoundButton(context, R.drawable.ic_add, context.getString(R.string.add), TAG_ADD) {
|
||||||
(video ?: _searchVideo)?.let {
|
(video ?: _searchVideo)?.let {
|
||||||
_slideUpOverlay = UISlideOverlays.showAddToOverlay(it, _overlayContainer);
|
_slideUpOverlay = UISlideOverlays.showAddToOverlay(it, _overlayContainer) {
|
||||||
|
_slideUpOverlay = it
|
||||||
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
if(video?.isLive ?: false)
|
if(video?.isLive ?: false)
|
||||||
@@ -750,6 +799,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
Logger.e(TAG, "Failed to reopen live chat", ex);
|
Logger.e(TAG, "Failed to reopen live chat", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
_slideUpOverlay?.hide();
|
||||||
} else null,
|
} else null,
|
||||||
RoundButton(context, R.drawable.ic_screen_share, context.getString(R.string.background), TAG_BACKGROUND) {
|
RoundButton(context, R.drawable.ic_screen_share, context.getString(R.string.background), TAG_BACKGROUND) {
|
||||||
if(!allowBackground) {
|
if(!allowBackground) {
|
||||||
@@ -762,6 +812,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
allowBackground = false;
|
allowBackground = false;
|
||||||
it.text.text = resources.getString(R.string.background);
|
it.text.text = resources.getString(R.string.background);
|
||||||
}
|
}
|
||||||
|
_slideUpOverlay?.hide();
|
||||||
},
|
},
|
||||||
RoundButton(context, R.drawable.ic_download, context.getString(R.string.download), TAG_DOWNLOAD) {
|
RoundButton(context, R.drawable.ic_download, context.getString(R.string.download), TAG_DOWNLOAD) {
|
||||||
video?.let {
|
video?.let {
|
||||||
@@ -774,11 +825,13 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
preventPictureInPicture = true;
|
preventPictureInPicture = true;
|
||||||
shareVideo();
|
shareVideo();
|
||||||
};
|
};
|
||||||
|
_slideUpOverlay?.hide();
|
||||||
},
|
},
|
||||||
RoundButton(context, R.drawable.ic_screen_share, context.getString(R.string.overlay), TAG_OVERLAY) {
|
RoundButton(context, R.drawable.ic_screen_share, context.getString(R.string.overlay), TAG_OVERLAY) {
|
||||||
this.startPictureInPicture();
|
this.startPictureInPicture();
|
||||||
fragment.forcePictureInPicture();
|
fragment.forcePictureInPicture();
|
||||||
//PiPActivity.startPiP(context);
|
//PiPActivity.startPiP(context);
|
||||||
|
_slideUpOverlay?.hide();
|
||||||
},
|
},
|
||||||
RoundButton(context, R.drawable.ic_export, context.getString(R.string.page), TAG_OPEN) {
|
RoundButton(context, R.drawable.ic_export, context.getString(R.string.page), TAG_OPEN) {
|
||||||
video?.let {
|
video?.let {
|
||||||
@@ -786,9 +839,11 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
fragment.navigate<BrowserFragment>(url);
|
fragment.navigate<BrowserFragment>(url);
|
||||||
fragment.minimizeVideoDetail();
|
fragment.minimizeVideoDetail();
|
||||||
};
|
};
|
||||||
|
_slideUpOverlay?.hide();
|
||||||
},
|
},
|
||||||
RoundButton(context, R.drawable.ic_refresh, context.getString(R.string.reload), "Reload") {
|
RoundButton(context, R.drawable.ic_refresh, context.getString(R.string.reload), "Reload") {
|
||||||
reloadVideo();
|
reloadVideo();
|
||||||
|
_slideUpOverlay?.hide();
|
||||||
}).filterNotNull();
|
}).filterNotNull();
|
||||||
if(!_buttonPinStore.getAllValues().any())
|
if(!_buttonPinStore.getAllValues().any())
|
||||||
_buttonPins.setButtons(*(buttons + listOf(_buttonMore)).toTypedArray());
|
_buttonPins.setButtons(*(buttons + listOf(_buttonMore)).toTypedArray());
|
||||||
@@ -824,14 +879,19 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
suspend fun getHistoryIndex(video: IPlatformVideo): DBHistory.Index = withContext(Dispatchers.IO){
|
|
||||||
val current = _historyIndex;
|
|
||||||
if(current == null || current.url != video.url) {
|
private val _historyIndexLock = Mutex(false);
|
||||||
val index = StateHistory.instance.getHistoryByVideo(video, true)!!;
|
suspend fun getHistoryIndex(video: IPlatformVideo): DBHistory.Index? = withContext(Dispatchers.IO){
|
||||||
_historyIndex = index;
|
_historyIndexLock.withLock {
|
||||||
return@withContext index;
|
val current = _historyIndex;
|
||||||
|
if(current == null || current.url != video.url) {
|
||||||
|
val index = StateHistory.instance.getHistoryByVideo(video, true);
|
||||||
|
_historyIndex = index;
|
||||||
|
return@withContext index;
|
||||||
|
}
|
||||||
|
return@withContext current;
|
||||||
}
|
}
|
||||||
return@withContext current;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -968,6 +1028,9 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
fun setVideo(url: String, resumeSeconds: Long = 0, playWhenReady: Boolean = true) {
|
fun setVideo(url: String, resumeSeconds: Long = 0, playWhenReady: Boolean = true) {
|
||||||
Logger.i(TAG, "setVideo url=$url resumeSeconds=$resumeSeconds playWhenReady=$playWhenReady")
|
Logger.i(TAG, "setVideo url=$url resumeSeconds=$resumeSeconds playWhenReady=$playWhenReady")
|
||||||
|
|
||||||
|
if(this.video?.url == url)
|
||||||
|
return;
|
||||||
|
|
||||||
_searchVideo = null;
|
_searchVideo = null;
|
||||||
video = null;
|
video = null;
|
||||||
_playbackTracker = null;
|
_playbackTracker = null;
|
||||||
@@ -995,9 +1058,12 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
|
|
||||||
switchContentView(_container_content_main);
|
switchContentView(_container_content_main);
|
||||||
}
|
}
|
||||||
fun setVideoOverview(video: IPlatformVideo, fetch: Boolean = true, resumeSeconds: Long = 0) {
|
fun setVideoOverview(video: IPlatformVideo, fetch: Boolean = true, resumeSeconds: Long = 0, bypassSameVideoCheck: Boolean = false) {
|
||||||
Logger.i(TAG, "setVideoOverview")
|
Logger.i(TAG, "setVideoOverview")
|
||||||
|
|
||||||
|
if(!bypassSameVideoCheck && this.video?.url == video.url)
|
||||||
|
return;
|
||||||
|
|
||||||
val cachedVideo = StateDownloads.instance.getCachedVideo(video.id);
|
val cachedVideo = StateDownloads.instance.getCachedVideo(video.id);
|
||||||
if(cachedVideo != null) {
|
if(cachedVideo != null) {
|
||||||
setVideoDetails(cachedVideo, true);
|
setVideoDetails(cachedVideo, true);
|
||||||
@@ -1092,10 +1158,13 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
|
|
||||||
switchContentView(_container_content_main);
|
switchContentView(_container_content_main);
|
||||||
}
|
}
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
//@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
fun setVideoDetails(videoDetail: IPlatformVideoDetails, newVideo: Boolean = false) {
|
fun setVideoDetails(videoDetail: IPlatformVideoDetails, newVideo: Boolean = false) {
|
||||||
Logger.i(TAG, "setVideoDetails (${videoDetail.name})")
|
Logger.i(TAG, "setVideoDetails (${videoDetail.name})")
|
||||||
|
|
||||||
|
if(newVideo && this.video?.url == videoDetail.url)
|
||||||
|
return;
|
||||||
|
|
||||||
if (newVideo) {
|
if (newVideo) {
|
||||||
_lastVideoSource = null;
|
_lastVideoSource = null;
|
||||||
_lastAudioSource = null;
|
_lastAudioSource = null;
|
||||||
@@ -1115,6 +1184,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
if(videoDetail is VideoLocal) {
|
if(videoDetail is VideoLocal) {
|
||||||
videoLocal = videoDetail;
|
videoLocal = videoDetail;
|
||||||
video = videoDetail;
|
video = videoDetail;
|
||||||
|
this.video = video;
|
||||||
val videoTask = StatePlatform.instance.getContentDetails(videoDetail.url);
|
val videoTask = StatePlatform.instance.getContentDetails(videoDetail.url);
|
||||||
videoTask.invokeOnCompletion { ex ->
|
videoTask.invokeOnCompletion { ex ->
|
||||||
if(ex != null) {
|
if(ex != null) {
|
||||||
@@ -1145,10 +1215,12 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
//TODO: Implement video.getContentChapters()
|
//TODO: Implement video.getContentChapters()
|
||||||
val chapters = null ?: StatePlatform.instance.getContentChapters(video.url);
|
val chapters = null ?: StatePlatform.instance.getContentChapters(video.url);
|
||||||
_player.setChapters(chapters);
|
_player.setChapters(chapters);
|
||||||
|
_cast.setChapters(chapters);
|
||||||
}
|
}
|
||||||
catch(ex: Throwable) {
|
catch(ex: Throwable) {
|
||||||
Logger.e(TAG, "Failed to get chapters", ex);
|
Logger.e(TAG, "Failed to get chapters", ex);
|
||||||
_player.setChapters(null);
|
_player.setChapters(null);
|
||||||
|
_cast.setChapters(null);
|
||||||
|
|
||||||
/*withContext(Dispatchers.Main) {
|
/*withContext(Dispatchers.Main) {
|
||||||
UIDialogs.toast(context, "Failed to get chapters\n" + ex.message);
|
UIDialogs.toast(context, "Failed to get chapters\n" + ex.message);
|
||||||
@@ -1180,12 +1252,17 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
val ref = video.id.value?.let { Models.referenceFromBuffer(it.toByteArray()) };
|
val ref = Models.referenceFromBuffer(video.url.toByteArray())
|
||||||
_addCommentView.setContext(video.url, ref);
|
val extraBytesRef = video.id.value?.let { if (it.isNotEmpty()) it.toByteArray() else null }
|
||||||
|
_addCommentView.setContext(video.url, ref)
|
||||||
_player.setMetadata(video.name, video.author.name);
|
_player.setMetadata(video.name, video.author.name);
|
||||||
|
|
||||||
_toggleCommentType.setValue(!Settings.instance.other.polycentricEnabled || Settings.instance.comments.defaultCommentSection == 1, false);
|
if (video is TutorialFragment.TutorialVideo) {
|
||||||
|
_toggleCommentType.setValue(false, false);
|
||||||
|
} else {
|
||||||
|
_toggleCommentType.setValue(!Settings.instance.other.polycentricEnabled || Settings.instance.comments.defaultCommentSection == 1, false);
|
||||||
|
}
|
||||||
|
|
||||||
updateCommentType(true);
|
updateCommentType(true);
|
||||||
|
|
||||||
//UI
|
//UI
|
||||||
@@ -1238,57 +1315,54 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
|
|
||||||
_rating.onLikeDislikeUpdated.remove(this);
|
_rating.onLikeDislikeUpdated.remove(this);
|
||||||
|
|
||||||
if (ref != null) {
|
_rating.visibility = View.GONE;
|
||||||
_rating.visibility = View.GONE;
|
|
||||||
|
|
||||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val queryReferencesResponse = ApiMethods.getQueryReferences(PolycentricCache.SERVER, ref, null,null,
|
val queryReferencesResponse = ApiMethods.getQueryReferences(PolycentricCache.SERVER, ref, null,null,
|
||||||
arrayListOf(
|
arrayListOf(
|
||||||
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder().setFromType(ContentType.OPINION.value).setValue(
|
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder().setFromType(ContentType.OPINION.value).setValue(
|
||||||
ByteString.copyFrom(Opinion.like.data)).build(),
|
ByteString.copyFrom(Opinion.like.data)).build(),
|
||||||
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder().setFromType(ContentType.OPINION.value).setValue(
|
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder().setFromType(ContentType.OPINION.value).setValue(
|
||||||
ByteString.copyFrom(Opinion.dislike.data)).build()
|
ByteString.copyFrom(Opinion.dislike.data)).build()
|
||||||
)
|
),
|
||||||
);
|
extraByteReferences = listOfNotNull(extraBytesRef)
|
||||||
|
);
|
||||||
|
|
||||||
val likes = queryReferencesResponse.countsList[0];
|
val likes = queryReferencesResponse.countsList[0];
|
||||||
val dislikes = queryReferencesResponse.countsList[1];
|
val dislikes = queryReferencesResponse.countsList[1];
|
||||||
val hasLiked = StatePolycentric.instance.hasLiked(ref);
|
val hasLiked = StatePolycentric.instance.hasLiked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasLiked(it) } ?: false*/;
|
||||||
val hasDisliked = StatePolycentric.instance.hasDisliked(ref);
|
val hasDisliked = StatePolycentric.instance.hasDisliked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasDisliked(it) } ?: false*/;
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
_rating.visibility = View.VISIBLE;
|
_rating.visibility = View.VISIBLE;
|
||||||
_rating.setRating(RatingLikeDislikes(likes, dislikes), hasLiked, hasDisliked);
|
_rating.setRating(RatingLikeDislikes(likes, dislikes), hasLiked, hasDisliked);
|
||||||
_rating.onLikeDislikeUpdated.subscribe(this) { args ->
|
_rating.onLikeDislikeUpdated.subscribe(this) { args ->
|
||||||
if (args.hasLiked) {
|
if (args.hasLiked) {
|
||||||
args.processHandle.opinion(ref, Opinion.like);
|
args.processHandle.opinion(ref, Opinion.like);
|
||||||
} else if (args.hasDisliked) {
|
} else if (args.hasDisliked) {
|
||||||
args.processHandle.opinion(ref, Opinion.dislike);
|
args.processHandle.opinion(ref, Opinion.dislike);
|
||||||
} else {
|
} else {
|
||||||
args.processHandle.opinion(ref, Opinion.neutral);
|
args.processHandle.opinion(ref, Opinion.neutral);
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
Logger.i(TAG, "Started backfill");
|
||||||
|
args.processHandle.fullyBackfillServersAnnounceExceptions();
|
||||||
|
Logger.i(TAG, "Finished backfill");
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to backfill servers", e)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
StatePolycentric.instance.updateLikeMap(ref, args.hasLiked, args.hasDisliked)
|
||||||
try {
|
};
|
||||||
Logger.i(TAG, "Started backfill");
|
|
||||||
args.processHandle.fullyBackfillServersAnnounceExceptions();
|
|
||||||
Logger.i(TAG, "Finished backfill");
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.e(TAG, "Failed to backfill servers", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StatePolycentric.instance.updateLikeMap(ref, args.hasLiked, args.hasDisliked)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.e(TAG, "Failed to get polycentric likes/dislikes.", e);
|
|
||||||
_rating.visibility = View.GONE;
|
|
||||||
}
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to get polycentric likes/dislikes.", e);
|
||||||
|
_rating.visibility = View.GONE;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
_rating.visibility = View.GONE;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
when (video.rating) {
|
when (video.rating) {
|
||||||
@@ -1331,32 +1405,36 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
val toResume = _videoResumePositionMilliseconds;
|
val toResume = _videoResumePositionMilliseconds;
|
||||||
_videoResumePositionMilliseconds = 0;
|
_videoResumePositionMilliseconds = 0;
|
||||||
loadCurrentVideo(toResume);
|
loadCurrentVideo(toResume);
|
||||||
_player.setGestureSoundFactor(1.0f);
|
if (!Settings.instance.gestureControls.useSystemVolume) {
|
||||||
|
_player.setGestureSoundFactor(1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
updateQueueState();
|
updateQueueState();
|
||||||
|
|
||||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
if (video !is TutorialFragment.TutorialVideo) {
|
||||||
val historyItem = getHistoryIndex(videoDetail);
|
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
val historyItem = getHistoryIndex(videoDetail) ?: return@launch;
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
_historicalPosition = StateHistory.instance.updateHistoryPosition(video, historyItem,false, (toResume.toFloat() / 1000.0f).toLong());
|
_historicalPosition = StateHistory.instance.updateHistoryPosition(video, historyItem,false, (toResume.toFloat() / 1000.0f).toLong());
|
||||||
Logger.i(TAG, "Historical position: $_historicalPosition, last position: $lastPositionMilliseconds");
|
Logger.i(TAG, "Historical position: $_historicalPosition, last position: $lastPositionMilliseconds");
|
||||||
if (_historicalPosition > 60 && video.duration - _historicalPosition > 5 && Math.abs(_historicalPosition - lastPositionMilliseconds / 1000) > 5.0) {
|
if (_historicalPosition > 60 && video.duration - _historicalPosition > 5 && Math.abs(_historicalPosition - lastPositionMilliseconds / 1000) > 5.0) {
|
||||||
_layoutResume.visibility = View.VISIBLE;
|
_layoutResume.visibility = View.VISIBLE;
|
||||||
_textResume.text = "Resume at ${_historicalPosition.toHumanTime(false)}";
|
_textResume.text = "Resume at ${_historicalPosition.toHumanTime(false)}";
|
||||||
|
|
||||||
_jobHideResume = fragment.lifecycleScope.launch(Dispatchers.Main) {
|
_jobHideResume = fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||||
try {
|
try {
|
||||||
delay(8000);
|
delay(8000);
|
||||||
_layoutResume.visibility = View.GONE;
|
_layoutResume.visibility = View.GONE;
|
||||||
_textResume.text = "";
|
_textResume.text = "";
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to set resume changes.", e);
|
Logger.e(TAG, "Failed to set resume changes.", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
_layoutResume.visibility = View.GONE;
|
||||||
|
_textResume.text = "";
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
_layoutResume.visibility = View.GONE;
|
|
||||||
_textResume.text = "";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1372,6 +1450,20 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
|
|
||||||
_player.updateNextPrevious();
|
_player.updateNextPrevious();
|
||||||
updateMoreButtons();
|
updateMoreButtons();
|
||||||
|
|
||||||
|
if (videoDetail is TutorialFragment.TutorialVideo) {
|
||||||
|
_buttonSubscribe.visibility = View.GONE
|
||||||
|
_buttonMore.visibility = View.GONE
|
||||||
|
_buttonPins.visibility = View.GONE
|
||||||
|
_layoutRating.visibility = View.GONE
|
||||||
|
_layoutToggleCommentSection.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
_buttonSubscribe.visibility = View.VISIBLE
|
||||||
|
_buttonMore.visibility = View.VISIBLE
|
||||||
|
_buttonPins.visibility = View.VISIBLE
|
||||||
|
_layoutRating.visibility = View.VISIBLE
|
||||||
|
_layoutToggleCommentSection.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fun loadLiveChat(video: IPlatformVideoDetails) {
|
fun loadLiveChat(video: IPlatformVideoDetails) {
|
||||||
_liveChat?.stop();
|
_liveChat?.stop();
|
||||||
@@ -1434,12 +1526,12 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
private fun loadCurrentVideo(resumePositionMs: Long = 0) {
|
private fun loadCurrentVideo(resumePositionMs: Long = 0) {
|
||||||
_didStop = false;
|
_didStop = false;
|
||||||
|
|
||||||
val video = video ?: return;
|
val video = (videoLocal ?: video) ?: return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val videoSource = _lastVideoSource ?: _player.getPreferredVideoSource(video, Settings.instance.playback.getCurrentPreferredQualityPixelCount());
|
val videoSource = _lastVideoSource ?: _player.getPreferredVideoSource(video, Settings.instance.playback.getCurrentPreferredQualityPixelCount());
|
||||||
val audioSource = _lastAudioSource ?: _player.getPreferredAudioSource(video, Settings.instance.playback.getPrimaryLanguage(context));
|
val audioSource = _lastAudioSource ?: _player.getPreferredAudioSource(video, Settings.instance.playback.getPrimaryLanguage(context));
|
||||||
val subtitleSource = _lastSubtitleSource;
|
val subtitleSource = _lastSubtitleSource ?: (if(video is VideoLocal) video.subtitlesSources.firstOrNull() else null);
|
||||||
Logger.i(TAG, "loadCurrentVideo(videoSource=$videoSource, audioSource=$audioSource, subtitleSource=$subtitleSource, resumePositionMs=$resumePositionMs)")
|
Logger.i(TAG, "loadCurrentVideo(videoSource=$videoSource, audioSource=$audioSource, subtitleSource=$subtitleSource, resumePositionMs=$resumePositionMs)")
|
||||||
|
|
||||||
if(videoSource == null && audioSource == null) {
|
if(videoSource == null && audioSource == null) {
|
||||||
@@ -1467,6 +1559,8 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_player.setArtwork(null);
|
_player.setArtwork(null);
|
||||||
|
|
||||||
_player.setSource(videoSource, audioSource, _playWhenReady, false);
|
_player.setSource(videoSource, audioSource, _playWhenReady, false);
|
||||||
|
if(subtitleSource != null)
|
||||||
|
_player.swapSubtitles(fragment.lifecycleScope, subtitleSource);
|
||||||
_player.seekTo(resumePositionMs);
|
_player.seekTo(resumePositionMs);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -1474,6 +1568,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
|
|
||||||
_lastVideoSource = videoSource;
|
_lastVideoSource = videoSource;
|
||||||
_lastAudioSource = audioSource;
|
_lastAudioSource = audioSource;
|
||||||
|
_lastSubtitleSource = subtitleSource;
|
||||||
}
|
}
|
||||||
catch(ex: UnsupportedCastException) {
|
catch(ex: UnsupportedCastException) {
|
||||||
Logger.e(TAG, "Failed to load cast media", ex);
|
Logger.e(TAG, "Failed to load cast media", ex);
|
||||||
@@ -1591,7 +1686,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
Logger.i(TAG, "prevVideo")
|
Logger.i(TAG, "prevVideo")
|
||||||
val next = StatePlayer.instance.prevQueueItem(withoutRemoval || _player.duration < 100 || (_player.position.toFloat() / _player.duration) < 0.9);
|
val next = StatePlayer.instance.prevQueueItem(withoutRemoval || _player.duration < 100 || (_player.position.toFloat() / _player.duration) < 0.9);
|
||||||
if(next != null) {
|
if(next != null) {
|
||||||
setVideoOverview(next);
|
setVideoOverview(next, true, 0, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1601,7 +1696,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
if(next == null && forceLoop)
|
if(next == null && forceLoop)
|
||||||
next = StatePlayer.instance.restartQueue();
|
next = StatePlayer.instance.restartQueue();
|
||||||
if(next != null) {
|
if(next != null) {
|
||||||
setVideoOverview(next);
|
setVideoOverview(next, true, 0, true);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -1908,13 +2003,15 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
Logger.i(TAG, "fetchPolycentricComments")
|
Logger.i(TAG, "fetchPolycentricComments")
|
||||||
val video = video;
|
val video = video;
|
||||||
val idValue = video?.id?.value
|
val idValue = video?.id?.value
|
||||||
if (idValue == null) {
|
if (video?.url?.isEmpty() != false) {
|
||||||
Logger.w(TAG, "Failed to fetch polycentric comments because id was null")
|
Logger.w(TAG, "Failed to fetch polycentric comments because url was null")
|
||||||
_commentsList.clear()
|
_commentsList.clear()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_commentsList.load(false) { StatePolycentric.instance.getCommentPager(video.url, Models.referenceFromBuffer(idValue.toByteArray())); };
|
val ref = Models.referenceFromBuffer(video.url.toByteArray())
|
||||||
|
val extraBytesRef = idValue?.let { if (it.isNotEmpty()) it.toByteArray() else null }
|
||||||
|
_commentsList.load(false) { StatePolycentric.instance.getCommentPager(video.url, ref, listOfNotNull(extraBytesRef)); };
|
||||||
}
|
}
|
||||||
private fun fetchVideo() {
|
private fun fetchVideo() {
|
||||||
Logger.i(TAG, "fetchVideo")
|
Logger.i(TAG, "fetchVideo")
|
||||||
@@ -2134,11 +2231,11 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
videoSourceHeight = 9;
|
videoSourceHeight = 9;
|
||||||
}
|
}
|
||||||
val aspectRatio = videoSourceWidth.toDouble() / videoSourceHeight;
|
val aspectRatio = videoSourceWidth.toDouble() / videoSourceHeight;
|
||||||
if(aspectRatio > 3) {
|
if(aspectRatio > 2.38) {
|
||||||
videoSourceWidth = 16;
|
videoSourceWidth = 16;
|
||||||
videoSourceHeight = 9;
|
videoSourceHeight = 9;
|
||||||
}
|
}
|
||||||
else if(aspectRatio < 0.3) {
|
else if(aspectRatio < 0.43) {
|
||||||
videoSourceHeight = 16;
|
videoSourceHeight = 16;
|
||||||
videoSourceWidth = 9;
|
videoSourceWidth = 9;
|
||||||
}
|
}
|
||||||
@@ -2176,9 +2273,11 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
val v = video ?: return;
|
val v = video ?: return;
|
||||||
val currentTime = System.currentTimeMillis();
|
val currentTime = System.currentTimeMillis();
|
||||||
if (updateHistory && (_lastPositionSaveTime == -1L || currentTime - _lastPositionSaveTime > 5000)) {
|
if (updateHistory && (_lastPositionSaveTime == -1L || currentTime - _lastPositionSaveTime > 5000)) {
|
||||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
if (v !is TutorialFragment.TutorialVideo) {
|
||||||
val history = getHistoryIndex(v);
|
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
StateHistory.instance.updateHistoryPosition(v, history, true, (positionMilliseconds.toFloat() / 1000.0f).toLong());
|
val history = getHistoryIndex(v) ?: return@launch;
|
||||||
|
StateHistory.instance.updateHistoryPosition(v, history, true, (positionMilliseconds.toFloat() / 1000.0f).toLong());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_lastPositionSaveTime = currentTime;
|
_lastPositionSaveTime = currentTime;
|
||||||
}
|
}
|
||||||
@@ -2261,7 +2360,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_creatorThumbnail.setThumbnail(avatar, animate);
|
_creatorThumbnail.setThumbnail(avatar, animate);
|
||||||
} else {
|
} else {
|
||||||
_creatorThumbnail.setThumbnail(video?.author?.thumbnail, animate);
|
_creatorThumbnail.setThumbnail(video?.author?.thumbnail, animate);
|
||||||
_creatorThumbnail.setHarborAvailable(profile != null, animate);
|
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
||||||
}
|
}
|
||||||
|
|
||||||
val username = cachedPolycentricProfile?.profile?.systemState?.username
|
val username = cachedPolycentricProfile?.profile?.systemState?.username
|
||||||
@@ -2285,7 +2384,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
}
|
}
|
||||||
else if(isOverlayed) {
|
else if(isOverlayed) {
|
||||||
_playerProgress.layoutParams = _playerProgress.layoutParams.apply {
|
_playerProgress.layoutParams = _playerProgress.layoutParams.apply {
|
||||||
(this as MarginLayoutParams).bottomMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, -6f, resources.displayMetrics).toInt();
|
(this as MarginLayoutParams).bottomMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, -2f, resources.displayMetrics).toInt();
|
||||||
};
|
};
|
||||||
_playerProgress.elevation = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics);
|
_playerProgress.elevation = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics);
|
||||||
}
|
}
|
||||||
@@ -2377,7 +2476,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
Logger.w(TAG, "exception<ScriptImplementationException>", it)
|
Logger.w(TAG, "exception<ScriptImplementationException>", it)
|
||||||
|
|
||||||
if (!nextVideo()) {
|
if (!nextVideo()) {
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video_scriptimplementationexception), it, ::fetchVideo);
|
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video_scriptimplementationexception), it, ::fetchVideo, null, fragment);
|
||||||
} else {
|
} else {
|
||||||
StateAnnouncement.instance.registerAnnouncement(video?.id?.value + "_Q_INVALIDVIDEO", context.getString(R.string.invalid_video), context.getString(
|
StateAnnouncement.instance.registerAnnouncement(video?.id?.value + "_Q_INVALIDVIDEO", context.getString(R.string.invalid_video), context.getString(
|
||||||
R.string.there_was_an_invalid_video_in_your_queue_videoname_by_authorname_playback_was_skipped).replace("{videoName}", video?.name ?: "").replace("{authorName}", video?.author?.name ?: ""), AnnouncementType.SESSION)
|
R.string.there_was_an_invalid_video_in_your_queue_videoname_by_authorname_playback_was_skipped).replace("{videoName}", video?.name ?: "").replace("{authorName}", video?.author?.name ?: ""), AnnouncementType.SESSION)
|
||||||
@@ -2413,7 +2512,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_retryJob = null;
|
_retryJob = null;
|
||||||
_liveTryJob?.cancel();
|
_liveTryJob?.cancel();
|
||||||
_liveTryJob = null;
|
_liveTryJob = null;
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video_scriptexception), it, ::fetchVideo);
|
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video_scriptexception), it, ::fetchVideo, null, fragment);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
@@ -2425,7 +2524,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_retryJob = null;
|
_retryJob = null;
|
||||||
_liveTryJob?.cancel();
|
_liveTryJob?.cancel();
|
||||||
_liveTryJob = null;
|
_liveTryJob = null;
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video), it, ::fetchVideo);
|
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video), it, ::fetchVideo, null, fragment);
|
||||||
}
|
}
|
||||||
} else TaskHandler(IPlatformVideoDetails::class.java, {fragment.lifecycleScope});
|
} else TaskHandler(IPlatformVideoDetails::class.java, {fragment.lifecycleScope});
|
||||||
|
|
||||||
@@ -2486,7 +2585,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
setVideoDetails(videoDetail);
|
setVideoDetails(videoDetail, false);
|
||||||
_liveTryJob = null;
|
_liveTryJob = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+46
@@ -1,6 +1,7 @@
|
|||||||
package com.futo.platformplayer.fragment.mainactivity.main
|
package com.futo.platformplayer.fragment.mainactivity.main
|
||||||
|
|
||||||
import android.graphics.drawable.Animatable
|
import android.graphics.drawable.Animatable
|
||||||
|
import android.util.TypedValue
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
@@ -8,10 +9,17 @@ import android.widget.ImageButton
|
|||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.core.view.setPadding
|
||||||
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.UISlideOverlays
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
|
import com.futo.platformplayer.assume
|
||||||
|
import com.futo.platformplayer.downloads.VideoDownload
|
||||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||||
|
import com.futo.platformplayer.states.StateDownloads
|
||||||
|
import com.futo.platformplayer.states.StatePlaylists
|
||||||
import com.futo.platformplayer.views.lists.VideoListEditorView
|
import com.futo.platformplayer.views.lists.VideoListEditorView
|
||||||
|
|
||||||
abstract class VideoListEditorView : LinearLayout {
|
abstract class VideoListEditorView : LinearLayout {
|
||||||
@@ -85,6 +93,44 @@ abstract class VideoListEditorView : LinearLayout {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected fun updateDownloadState(groupType: String, playlistId: String, onDownload: ()->Unit) {
|
||||||
|
//val playlist = _playlist ?: return;
|
||||||
|
val isDownloading = StateDownloads.instance.getDownloading().any { it.groupType == groupType && it.groupID == playlistId };
|
||||||
|
val isDownloaded = StateDownloads.instance.isPlaylistCached(playlistId);
|
||||||
|
|
||||||
|
val dp10 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10f, resources.displayMetrics);
|
||||||
|
|
||||||
|
if(isDownloaded && !isDownloading)
|
||||||
|
_buttonDownload.setBackgroundResource(R.drawable.background_button_round_green);
|
||||||
|
else
|
||||||
|
_buttonDownload.setBackgroundResource(R.drawable.background_button_round);
|
||||||
|
|
||||||
|
if(isDownloading) {
|
||||||
|
_buttonDownload.setImageResource(R.drawable.ic_loader_animated);
|
||||||
|
_buttonDownload.drawable.assume<Animatable, Unit> { it.start() };
|
||||||
|
_buttonDownload.setOnClickListener {
|
||||||
|
UIDialogs.showConfirmationDialog(context, context.getString(R.string.are_you_sure_you_want_to_delete_the_downloaded_videos), {
|
||||||
|
StateDownloads.instance.deleteCachedPlaylist(playlistId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(isDownloaded) {
|
||||||
|
_buttonDownload.setImageResource(R.drawable.ic_download_off);
|
||||||
|
_buttonDownload.setOnClickListener {
|
||||||
|
UIDialogs.showConfirmationDialog(context, context.getString(R.string.are_you_sure_you_want_to_delete_the_downloaded_videos), {
|
||||||
|
StateDownloads.instance.deleteCachedPlaylist(playlistId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
_buttonDownload.setImageResource(R.drawable.ic_download);
|
||||||
|
_buttonDownload.setOnClickListener {
|
||||||
|
onDownload();
|
||||||
|
//UISlideOverlays.showDownloadPlaylistOverlay(playlist, overlayContainer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_buttonDownload.setPadding(dp10.toInt());
|
||||||
|
}
|
||||||
|
|
||||||
protected fun setName(name: String?) {
|
protected fun setName(name: String?) {
|
||||||
_textName.text = name ?: "";
|
_textName.text = name ?: "";
|
||||||
|
|||||||
+41
@@ -5,10 +5,17 @@ import android.os.Bundle
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.futo.platformplayer.UISlideOverlays
|
||||||
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.SerializedPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||||
|
import com.futo.platformplayer.downloads.VideoDownload
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.states.StateDownloads
|
||||||
import com.futo.platformplayer.states.StatePlayer
|
import com.futo.platformplayer.states.StatePlayer
|
||||||
import com.futo.platformplayer.states.StatePlaylists
|
import com.futo.platformplayer.states.StatePlaylists
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class WatchLaterFragment : MainFragment() {
|
class WatchLaterFragment : MainFragment() {
|
||||||
override val isMainView : Boolean = true;
|
override val isMainView : Boolean = true;
|
||||||
@@ -28,6 +35,11 @@ class WatchLaterFragment : MainFragment() {
|
|||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
_view?.onResume();
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDestroyMainView() {
|
override fun onDestroyMainView() {
|
||||||
super.onDestroyMainView();
|
super.onDestroyMainView();
|
||||||
_view = null;
|
_view = null;
|
||||||
@@ -45,6 +57,34 @@ class WatchLaterFragment : MainFragment() {
|
|||||||
fun onShown() {
|
fun onShown() {
|
||||||
setName("Watch Later");
|
setName("Watch Later");
|
||||||
setVideos(StatePlaylists.instance.getWatchLater(), true);
|
setVideos(StatePlaylists.instance.getWatchLater(), true);
|
||||||
|
|
||||||
|
setButtonDownloadVisible(true);
|
||||||
|
updateDownloadState(VideoDownload.GROUP_WATCHLATER, VideoDownload.GROUP_WATCHLATER, this@WatchLaterView::download);
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onResume(){
|
||||||
|
StateDownloads.instance.onDownloadsChanged.subscribe(this) {
|
||||||
|
_fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
try {
|
||||||
|
updateDownloadState(VideoDownload.GROUP_WATCHLATER, VideoDownload.GROUP_WATCHLATER, this@WatchLaterView::download);
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to update download state onDownloadedChanged.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
StateDownloads.instance.onDownloadedChanged.subscribe(this) {
|
||||||
|
_fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
try {
|
||||||
|
updateDownloadState(VideoDownload.GROUP_WATCHLATER, VideoDownload.GROUP_WATCHLATER, this@WatchLaterView::download);
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to update download state onDownloadedChanged.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fun download(){
|
||||||
|
UISlideOverlays.showDownloadWatchlaterOverlay(overlayContainer);
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPlayAllClick() {
|
override fun onPlayAllClick() {
|
||||||
@@ -76,6 +116,7 @@ class WatchLaterFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
val TAG = "WatchLaterFragment";
|
||||||
fun newInstance() = WatchLaterFragment().apply {}
|
fun newInstance() = WatchLaterFragment().apply {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+9
-8
@@ -13,17 +13,17 @@ import android.view.inputmethod.InputMethodManager
|
|||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import com.futo.platformplayer.logging.Logger
|
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.stores.SearchHistoryStorage
|
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.api.media.PlatformID
|
|
||||||
import com.futo.platformplayer.constructs.Event0
|
import com.futo.platformplayer.constructs.Event0
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.*
|
import com.futo.platformplayer.fragment.mainactivity.main.SuggestionsFragment
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.SuggestionsFragmentData
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.SearchType
|
import com.futo.platformplayer.models.SearchType
|
||||||
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
|
import com.futo.platformplayer.stores.SearchHistoryStorage
|
||||||
|
|
||||||
class SearchTopBarFragment : TopFragment() {
|
class SearchTopBarFragment : TopFragment() {
|
||||||
private val TAG = "SearchTopBarFragment"
|
private val TAG = "SearchTopBarFragment"
|
||||||
@@ -54,11 +54,12 @@ class SearchTopBarFragment : TopFragment() {
|
|||||||
|
|
||||||
private val _searchDoneListener = object : TextView.OnEditorActionListener {
|
private val _searchDoneListener = object : TextView.OnEditorActionListener {
|
||||||
override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean {
|
override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean {
|
||||||
if (actionId != EditorInfo.IME_ACTION_DONE)
|
val isEnterPress = event?.keyCode == KeyEvent.KEYCODE_ENTER && event.action == KeyEvent.ACTION_DOWN
|
||||||
|
if (actionId != EditorInfo.IME_ACTION_DONE && !isEnterPress)
|
||||||
return false
|
return false
|
||||||
|
|
||||||
onDone();
|
onDone()
|
||||||
return true;
|
return true
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.futo.platformplayer.functional
|
||||||
|
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
|
import com.futo.platformplayer.api.media.structures.ReusablePager
|
||||||
|
import com.futo.platformplayer.constructs.Event0
|
||||||
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.constructs.Event2
|
||||||
|
|
||||||
|
//TODO: Integrate this better?
|
||||||
|
class CentralizedFeed {
|
||||||
|
var lock = Object();
|
||||||
|
var feed: ReusablePager<IPlatformContent>? = null;
|
||||||
|
var isGlobalUpdating: Boolean = false;
|
||||||
|
var exceptions: List<Throwable> = listOf();
|
||||||
|
|
||||||
|
|
||||||
|
var lastProgress: Int = 0;
|
||||||
|
var lastTotal: Int = 0;
|
||||||
|
val onUpdateProgress = Event2<Int, Int>();
|
||||||
|
val onUpdated = Event0();
|
||||||
|
val onUpdatedOnce = Event1<Throwable?>();
|
||||||
|
val onException = Event1<List<Throwable>>();
|
||||||
|
}
|
||||||
@@ -2,11 +2,17 @@ package com.futo.platformplayer.helpers
|
|||||||
|
|
||||||
class FileHelper {
|
class FileHelper {
|
||||||
companion object {
|
companion object {
|
||||||
val allowedCharacters = HashSet("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz-.".toCharArray().toList());
|
fun String.sanitizeFileName(allowSpace: Boolean = false): String {
|
||||||
|
return this.filter {
|
||||||
|
(it in '0' .. '9') ||
|
||||||
fun String.sanitizeFileName(): String {
|
(it in 'a'..'z') ||
|
||||||
return this.filter { allowedCharacters.contains(it) };
|
(it in 'A'..'Z') ||
|
||||||
|
(it == '-' || it == '.' || it == '_' || (it == ' ' && allowSpace)) ||
|
||||||
|
(it in '丁'..'龤') || //Chinese/Kanji
|
||||||
|
(it in '\u3040'..'\u309f') || //Hiragana
|
||||||
|
(it in '\u30A0'..'\u30ff') || //Katakana
|
||||||
|
(it in '\u0600'..'\u06FF') //Arabic
|
||||||
|
}; //Chinese
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,7 +43,8 @@ class FileLogConsumer : ILogConsumer, Closeable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
while (_linesToWrite.isNotEmpty()) {
|
while (_linesToWrite.isNotEmpty()) {
|
||||||
_writer?.appendLine(_linesToWrite.remove());
|
val todo = _linesToWrite.remove()
|
||||||
|
_writer?.appendLine(todo);
|
||||||
}
|
}
|
||||||
|
|
||||||
_writer?.flush();
|
_writer?.flush();
|
||||||
@@ -85,7 +86,7 @@ class FileLogConsumer : ILogConsumer, Closeable {
|
|||||||
_running = false;
|
_running = false;
|
||||||
_writer?.close();
|
_writer?.close();
|
||||||
_writer = null;
|
_writer = null;
|
||||||
_logThread?.join();
|
//_logThread?.join();
|
||||||
_logThread = null;
|
_logThread = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class HistoryVideo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromReconString(str: String, resolve: ((url: String)->SerializedPlatformVideo)? = null): HistoryVideo {
|
fun fromReconString(str: String, resolve: ((url: String)->SerializedPlatformVideo?)? = null): HistoryVideo {
|
||||||
var index = str.indexOf("|||");
|
var index = str.indexOf("|||");
|
||||||
if(index < 0) throw IllegalArgumentException("Invalid history string: " + str);
|
if(index < 0) throw IllegalArgumentException("Invalid history string: " + str);
|
||||||
val url = str.substring(0, index);
|
val url = str.substring(0, index);
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.futo.platformplayer.models
|
||||||
|
|
||||||
|
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
|
||||||
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class ImportCache(
|
||||||
|
var videos: List<SerializedPlatformVideo>? = null,
|
||||||
|
var channels: List<SerializedChannel>? = null
|
||||||
|
);
|
||||||
@@ -40,6 +40,9 @@ class Subscription {
|
|||||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
|
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
|
||||||
var lastPostUpdate : OffsetDateTime = OffsetDateTime.MIN;
|
var lastPostUpdate : OffsetDateTime = OffsetDateTime.MIN;
|
||||||
|
|
||||||
|
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
|
||||||
|
var lastPeekVideo : OffsetDateTime = OffsetDateTime.MIN;
|
||||||
|
|
||||||
//Last video interval
|
//Last video interval
|
||||||
var uploadInterval : Int = 0;
|
var uploadInterval : Int = 0;
|
||||||
var uploadStreamInterval : Int = 0;
|
var uploadStreamInterval : Int = 0;
|
||||||
@@ -48,11 +51,16 @@ class Subscription {
|
|||||||
var playbackSeconds: Int = 0;
|
var playbackSeconds: Int = 0;
|
||||||
var playbackViews: Int = 0;
|
var playbackViews: Int = 0;
|
||||||
|
|
||||||
|
var isOther = false;
|
||||||
|
|
||||||
constructor(channel : SerializedChannel) {
|
constructor(channel : SerializedChannel) {
|
||||||
this.channel = channel;
|
this.channel = channel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isChannel(url: String): Boolean {
|
||||||
|
return channel.url == url || channel.urlAlternatives.contains(url);
|
||||||
|
}
|
||||||
|
|
||||||
fun shouldFetchVideos() = doFetchVideos &&
|
fun shouldFetchVideos() = doFetchVideos &&
|
||||||
(lastVideo.getNowDiffDays() < 30 || lastVideoUpdate.getNowDiffDays() >= 1) &&
|
(lastVideo.getNowDiffDays() < 30 || lastVideoUpdate.getNowDiffDays() >= 1) &&
|
||||||
(lastVideo.getNowDiffDays() < 180 || lastVideoUpdate.getNowDiffDays() >= 3);
|
(lastVideo.getNowDiffDays() < 180 || lastVideoUpdate.getNowDiffDays() >= 3);
|
||||||
@@ -63,10 +71,16 @@ class Subscription {
|
|||||||
fun getClient() = StatePlatform.instance.getChannelClientOrNull(channel.url);
|
fun getClient() = StatePlatform.instance.getChannelClientOrNull(channel.url);
|
||||||
|
|
||||||
fun save() {
|
fun save() {
|
||||||
StateSubscriptions.instance.saveSubscription(this);
|
if(isOther)
|
||||||
|
StateSubscriptions.instance.saveSubscriptionOther(this);
|
||||||
|
else
|
||||||
|
StateSubscriptions.instance.saveSubscription(this);
|
||||||
}
|
}
|
||||||
fun saveAsync() {
|
fun saveAsync() {
|
||||||
StateSubscriptions.instance.saveSubscription(this);
|
if(isOther)
|
||||||
|
StateSubscriptions.instance.saveSubscriptionOtherAsync(this);
|
||||||
|
else
|
||||||
|
StateSubscriptions.instance.saveSubscriptionAsync(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateChannel(channel: IPlatformChannel) {
|
fun updateChannel(channel: IPlatformChannel) {
|
||||||
@@ -115,6 +129,7 @@ class Subscription {
|
|||||||
else if(lastVideo.year > 3000)
|
else if(lastVideo.year > 3000)
|
||||||
lastVideo = OffsetDateTime.MIN;
|
lastVideo = OffsetDateTime.MIN;
|
||||||
lastVideoUpdate = OffsetDateTime.now();
|
lastVideoUpdate = OffsetDateTime.now();
|
||||||
|
lastPeekVideo = OffsetDateTime.MIN;
|
||||||
}
|
}
|
||||||
ResultCapabilities.TYPE_MIXED -> {
|
ResultCapabilities.TYPE_MIXED -> {
|
||||||
uploadInterval = interval;
|
uploadInterval = interval;
|
||||||
@@ -123,6 +138,7 @@ class Subscription {
|
|||||||
else if(lastVideo.year > 3000)
|
else if(lastVideo.year > 3000)
|
||||||
lastVideo = OffsetDateTime.MIN;
|
lastVideo = OffsetDateTime.MIN;
|
||||||
lastVideoUpdate = OffsetDateTime.now();
|
lastVideoUpdate = OffsetDateTime.now();
|
||||||
|
lastPeekVideo = OffsetDateTime.MIN;
|
||||||
}
|
}
|
||||||
ResultCapabilities.TYPE_SUBSCRIPTIONS -> {
|
ResultCapabilities.TYPE_SUBSCRIPTIONS -> {
|
||||||
uploadInterval = interval;
|
uploadInterval = interval;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import com.futo.platformplayer.activities.MainActivity
|
|||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.receivers.MediaControlReceiver
|
import com.futo.platformplayer.receivers.MediaControlReceiver
|
||||||
import com.futo.platformplayer.timestampRegex
|
import com.futo.platformplayer.timestampRegex
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
class PlatformLinkMovementMethod : LinkMovementMethod {
|
class PlatformLinkMovementMethod : LinkMovementMethod {
|
||||||
private val _context: Context;
|
private val _context: Context;
|
||||||
@@ -32,33 +33,36 @@ class PlatformLinkMovementMethod : LinkMovementMethod {
|
|||||||
val links = buffer.getSpans(off, off, URLSpan::class.java);
|
val links = buffer.getSpans(off, off, URLSpan::class.java);
|
||||||
|
|
||||||
if (links.isNotEmpty()) {
|
if (links.isNotEmpty()) {
|
||||||
for (link in links) {
|
runBlocking {
|
||||||
Logger.i(TAG) { "Link clicked '${link.url}'." };
|
for (link in links) {
|
||||||
|
Logger.i(TAG) { "Link clicked '${link.url}'." };
|
||||||
|
|
||||||
if (_context is MainActivity) {
|
if (_context is MainActivity) {
|
||||||
if (_context.handleUrl(link.url)) {
|
if (_context.handleUrl(link.url)) {
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timestampRegex.matches(link.url)) {
|
|
||||||
val tokens = link.url.split(':');
|
|
||||||
|
|
||||||
var time_s = -1L;
|
|
||||||
if (tokens.size == 2) {
|
|
||||||
time_s = tokens[0].toLong() * 60 + tokens[1].toLong();
|
|
||||||
} else if (tokens.size == 3) {
|
|
||||||
time_s = tokens[0].toLong() * 60 * 60 + tokens[1].toLong() * 60 + tokens[2].toLong();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (time_s != -1L) {
|
|
||||||
MediaControlReceiver.onSeekToReceived.emit(time_s * 1000);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (timestampRegex.matches(link.url)) {
|
||||||
|
val tokens = link.url.split(':');
|
||||||
|
|
||||||
|
var time_s = -1L;
|
||||||
|
if (tokens.size == 2) {
|
||||||
|
time_s = tokens[0].toLong() * 60 + tokens[1].toLong();
|
||||||
|
} else if (tokens.size == 3) {
|
||||||
|
time_s =
|
||||||
|
tokens[0].toLong() * 60 * 60 + tokens[1].toLong() * 60 + tokens[2].toLong();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (time_s != -1L) {
|
||||||
|
MediaControlReceiver.onSeekToReceived.emit(time_s * 1000);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
_context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ class PolycentricCache {
|
|||||||
|
|
||||||
private val _taskGetProfile = BatchedTaskHandler<PublicKey, CachedPolycentricProfile>(_scope,
|
private val _taskGetProfile = BatchedTaskHandler<PublicKey, CachedPolycentricProfile>(_scope,
|
||||||
{ system ->
|
{ system ->
|
||||||
val signedProfileEvents = ApiMethods.getQueryLatest(
|
val signedEventsList = ApiMethods.getQueryLatest(
|
||||||
SERVER,
|
SERVER,
|
||||||
system.toProto(),
|
system.toProto(),
|
||||||
listOf(
|
listOf(
|
||||||
@@ -72,8 +72,9 @@ class PolycentricCache {
|
|||||||
ContentType.MEMBERSHIP_URLS.value,
|
ContentType.MEMBERSHIP_URLS.value,
|
||||||
ContentType.DONATION_DESTINATIONS.value
|
ContentType.DONATION_DESTINATIONS.value
|
||||||
)
|
)
|
||||||
).eventsList.map { e -> SignedEvent.fromProto(e) }
|
).eventsList.map { e -> SignedEvent.fromProto(e) };
|
||||||
.groupBy { e -> e.event.contentType }
|
|
||||||
|
val signedProfileEvents = signedEventsList.groupBy { e -> e.event.contentType }
|
||||||
.map { (_, events) -> events.maxBy { it.event.unixMilliseconds ?: 0 } };
|
.map { (_, events) -> events.maxBy { it.event.unixMilliseconds ?: 0 } };
|
||||||
|
|
||||||
val storageSystemState = StorageTypeSystemState.create()
|
val storageSystemState = StorageTypeSystemState.create()
|
||||||
@@ -151,17 +152,7 @@ class PolycentricCache {
|
|||||||
|
|
||||||
private val _batchTaskGetData = BatchedTaskHandler<String, ByteBuffer>(_scope,
|
private val _batchTaskGetData = BatchedTaskHandler<String, ByteBuffer>(_scope,
|
||||||
{
|
{
|
||||||
val urlData = if (it.startsWith("polycentric://")) {
|
val dataLink = getDataLinkFromUrl(it) ?: throw Exception("Only URLInfoDataLink is supported");
|
||||||
it.substring("polycentric://".length)
|
|
||||||
} else it;
|
|
||||||
|
|
||||||
val urlBytes = urlData.base64UrlToByteArray();
|
|
||||||
val urlInfo = Protocol.URLInfo.parseFrom(urlBytes);
|
|
||||||
if (urlInfo.urlType != 4L) {
|
|
||||||
throw Exception("Only URLInfoDataLink is supported");
|
|
||||||
}
|
|
||||||
|
|
||||||
val dataLink = Protocol.URLInfoDataLink.parseFrom(urlInfo.body);
|
|
||||||
return@BatchedTaskHandler ApiMethods.getDataFromServerAndReassemble(dataLink);
|
return@BatchedTaskHandler ApiMethods.getDataFromServerAndReassemble(dataLink);
|
||||||
},
|
},
|
||||||
{ return@BatchedTaskHandler null },
|
{ return@BatchedTaskHandler null },
|
||||||
@@ -325,9 +316,9 @@ class PolycentricCache {
|
|||||||
.build();
|
.build();
|
||||||
|
|
||||||
private const val TAG = "PolycentricCache"
|
private const val TAG = "PolycentricCache"
|
||||||
const val SERVER = "https://srv1-stg.polycentric.io"
|
const val SERVER = "https://srv1-prod.polycentric.io"
|
||||||
private var _instance: PolycentricCache? = null;
|
private var _instance: PolycentricCache? = null;
|
||||||
private val CACHE_EXPIRATION_SECONDS = 60 * 60 * 3;
|
private val CACHE_EXPIRATION_SECONDS = 60 * 5;
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
val instance: PolycentricCache
|
val instance: PolycentricCache
|
||||||
@@ -343,5 +334,20 @@ class PolycentricCache {
|
|||||||
it._scope.cancel("PolycentricCache finished");
|
it._scope.cancel("PolycentricCache finished");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getDataLinkFromUrl(it: String): Protocol.URLInfoDataLink? {
|
||||||
|
val urlData = if (it.startsWith("polycentric://")) {
|
||||||
|
it.substring("polycentric://".length)
|
||||||
|
} else it;
|
||||||
|
|
||||||
|
val urlBytes = urlData.base64UrlToByteArray();
|
||||||
|
val urlInfo = Protocol.URLInfo.parseFrom(urlBytes);
|
||||||
|
if (urlInfo.urlType != 4L) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val dataLink = Protocol.URLInfoDataLink.parseFrom(urlInfo.body);
|
||||||
|
return dataLink
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.futo.platformplayer.polycentric
|
||||||
|
|
||||||
|
import com.futo.platformplayer.encryption.GEncryptionProviderV1
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
|
import com.futo.platformplayer.stores.StringArrayStorage
|
||||||
|
import com.futo.polycentric.core.ProcessSecret
|
||||||
|
import com.futo.polycentric.core.base64ToByteArray
|
||||||
|
import com.futo.polycentric.core.toBase64
|
||||||
|
import userpackage.Protocol
|
||||||
|
|
||||||
|
class PolycentricStorage {
|
||||||
|
private val _processSecrets = FragmentedStorage.get<StringArrayStorage>("processSecrets");
|
||||||
|
|
||||||
|
fun addProcessSecret(processSecret: ProcessSecret) {
|
||||||
|
_processSecrets.addDistinct(GEncryptionProviderV1.instance.encrypt(processSecret.toProto().toByteArray()).toBase64())
|
||||||
|
_processSecrets.saveBlocking()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getProcessSecrets(): List<ProcessSecret> {
|
||||||
|
val processSecrets = arrayListOf<ProcessSecret>()
|
||||||
|
for (p in _processSecrets.getAllValues()) {
|
||||||
|
try {
|
||||||
|
processSecrets.add(ProcessSecret.fromProto(Protocol.StorageTypeProcessSecret.parseFrom(GEncryptionProviderV1.instance.decrypt(p.base64ToByteArray()))))
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.i(TAG, "Failed to decrypt process secret", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return processSecrets
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val TAG = "PolycentricStorage";
|
||||||
|
private var _instance : PolycentricStorage? = null;
|
||||||
|
val instance : PolycentricStorage
|
||||||
|
get(){
|
||||||
|
if(_instance == null)
|
||||||
|
_instance = PolycentricStorage();
|
||||||
|
return _instance!!;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user