mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-29 11:03:01 +02:00
Compare commits
153 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -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
|
||||||
|
|||||||
+14
-14
@@ -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
|
||||||
@@ -162,25 +162,25 @@ dependencies {
|
|||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
|
||||||
|
|
||||||
//HTTP
|
//HTTP
|
||||||
implementation "com.squareup.okhttp3:okhttp:4.11.0"
|
implementation "com.squareup.okhttp3:okhttp:4.12.0"
|
||||||
|
|
||||||
//JSON
|
//JSON
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2" //Used for structured json
|
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2" //Used for structured json
|
||||||
implementation 'com.google.code.gson:gson:2.10.1' //Used for complex/anonymous cases like during development conversions (eg. V8RemoteObject)
|
implementation 'com.google.code.gson:gson:2.10.1' //Used for complex/anonymous cases like during development conversions (eg. V8RemoteObject)
|
||||||
|
|
||||||
//JS
|
//JS
|
||||||
implementation("com.caoccao.javet:javet-android:2.2.1")
|
implementation("com.caoccao.javet:javet-android:3.0.3")
|
||||||
|
|
||||||
//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.7'
|
||||||
implementation 'androidx.navigation:navigation-ui-ktx:2.7.5'
|
implementation 'androidx.navigation:navigation-ui-ktx:2.7.7'
|
||||||
implementation 'androidx.media:media:1.7.0'
|
implementation 'androidx.media:media:1.7.0'
|
||||||
|
|
||||||
//Other
|
//Other
|
||||||
@@ -189,7 +189,7 @@ dependencies {
|
|||||||
implementation 'com.google.android.flexbox:flexbox:3.0.0'
|
implementation 'com.google.android.flexbox:flexbox:3.0.0'
|
||||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||||
implementation 'com.arthenica:ffmpeg-kit-full:5.1'
|
implementation 'com.arthenica:ffmpeg-kit-full:5.1'
|
||||||
implementation 'org.jetbrains.kotlin:kotlin-reflect:1.9.0'
|
implementation 'org.jetbrains.kotlin:kotlin-reflect:1.9.21'
|
||||||
implementation 'com.github.dhaval2404:imagepicker:2.1'
|
implementation 'com.github.dhaval2404:imagepicker:2.1'
|
||||||
implementation 'com.google.zxing:core:3.4.1'
|
implementation 'com.google.zxing:core:3.4.1'
|
||||||
implementation 'com.journeyapps:zxing-android-embedded:4.3.0'
|
implementation 'com.journeyapps:zxing-android-embedded:4.3.0'
|
||||||
@@ -214,7 +214,7 @@ dependencies {
|
|||||||
|
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
|
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
|
||||||
testImplementation "org.jetbrains.kotlin:kotlin-test:1.8.22"
|
testImplementation "org.jetbrains.kotlin:kotlin-test:1.9.21"
|
||||||
testImplementation "org.xmlunit:xmlunit-core:2.9.1"
|
testImplementation "org.xmlunit:xmlunit-core:2.9.1"
|
||||||
testImplementation "org.mockito:mockito-core:5.4.0"
|
testImplementation "org.mockito:mockito-core:5.4.0"
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -233,6 +233,9 @@ function pluginRemoteProp(objID, propName) {
|
|||||||
function pluginRemoteCall(objID, methodName, args) {
|
function pluginRemoteCall(objID, methodName, args) {
|
||||||
return JSON.parse(syncPOST("/plugin/remoteCall?id=" + objID + "&method=" + methodName, {}, JSON.stringify(args)));
|
return JSON.parse(syncPOST("/plugin/remoteCall?id=" + objID + "&method=" + methodName, {}, JSON.stringify(args)));
|
||||||
}
|
}
|
||||||
|
function pluginRemoteTest(methodName, args) {
|
||||||
|
return JSON.parse(syncPOST("/plugin/remoteTest?method=" + methodName, {}, JSON.stringify(args)));
|
||||||
|
}
|
||||||
|
|
||||||
function pluginIsLoggedIn(cb, err) {
|
function pluginIsLoggedIn(cb, err) {
|
||||||
fetch("/plugin/isLoggedIn", {
|
fetch("/plugin/isLoggedIn", {
|
||||||
|
|||||||
@@ -385,8 +385,8 @@
|
|||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
<div style="width: 50%" v-if="Plugin.currentPlugin">
|
<div style="width: 50%" v-if="Plugin.currentPlugin">
|
||||||
<!--Get Home-->
|
<v-text-field v-model="searchTestMethods" label="Search for source methods.." style="margin-left: 35px; margin-right: 35px;"></v-text-field>
|
||||||
<v-card class="requestCard" v-for="req in Testing.requests">
|
<v-card class="requestCard" v-for="req in Testing.requests" v-show="req.title.indexOf(searchTestMethods) >= 0">
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<span v-if="req.isOptional">(Optional)</span>
|
<span v-if="req.isOptional">(Optional)</span>
|
||||||
@@ -416,6 +416,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>
|
||||||
@@ -545,6 +548,7 @@
|
|||||||
new Vue({
|
new Vue({
|
||||||
el: '#app',
|
el: '#app',
|
||||||
data: {
|
data: {
|
||||||
|
searchTestMethods: "",
|
||||||
page: "Plugin",
|
page: "Plugin",
|
||||||
pastPluginUrls: [],
|
pastPluginUrls: [],
|
||||||
settings: {},
|
settings: {},
|
||||||
@@ -860,6 +864,53 @@
|
|||||||
"Error: " + ex;
|
"Error: " + ex;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
testSourceRemotely(req) {
|
||||||
|
const name = req.title;
|
||||||
|
const parameterVals = req.parameters.map(x=>{
|
||||||
|
if(x.value && x.value.startsWith && x.value.startsWith("json:"))
|
||||||
|
return JSON.parse(x.value.substring(5));
|
||||||
|
return x.value
|
||||||
|
});
|
||||||
|
|
||||||
|
if(name == "enable") {
|
||||||
|
if(parameterVals.length > 0)
|
||||||
|
parameterVals[0] = this.Plugin.currentPlugin;
|
||||||
|
else
|
||||||
|
parameterVals.push(this.Plugin.currentPlugin);
|
||||||
|
if(parameterVals.length > 1)
|
||||||
|
parameterVals[1] = __DEV_SETTINGS;
|
||||||
|
else
|
||||||
|
parameterVals.push(__DEV_SETTINGS);
|
||||||
|
}
|
||||||
|
|
||||||
|
const func = source[name];
|
||||||
|
if(!func)
|
||||||
|
alert("Test func not found");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const remoteResult = pluginRemoteTest(name, parameterVals);
|
||||||
|
console.log("Result for " + req.title, remoteResult);
|
||||||
|
this.Testing.lastResult = "//Results [" + name + "]\n" +
|
||||||
|
JSON.stringify(remoteResult, null, 3);
|
||||||
|
this.Testing.lastResultError = "";
|
||||||
|
}
|
||||||
|
catch(ex) {
|
||||||
|
if(ex.plugin_type == "CaptchaRequiredException") {
|
||||||
|
let shouldCaptcha = confirm("Do you want to request captcha?");
|
||||||
|
if(shouldCaptcha) {
|
||||||
|
pluginCaptchaTestPlugin(ex.url, ex.body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.error("Failed to run test for " + req.title, ex);
|
||||||
|
this.Testing.lastResult = ""
|
||||||
|
if(ex.message)
|
||||||
|
this.Testing.lastResultError = "//Results [" + name + "]\n\n" +
|
||||||
|
"Error: " + ex.message + "\n\n" + ex.stack;
|
||||||
|
else
|
||||||
|
this.Testing.lastResultError = "//Results [" + name + "]\n\n" +
|
||||||
|
"Error: " + ex;
|
||||||
|
}
|
||||||
|
},
|
||||||
showTestResults(results) {
|
showTestResults(results) {
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|||||||
+120
-46
@@ -1,13 +1,37 @@
|
|||||||
|
|
||||||
declare class ScriptException extends Error {
|
declare class ScriptException extends Error {
|
||||||
|
//If only one parameter is provided, acts as msg
|
||||||
constructor(type: string, msg: string);
|
constructor(type: string, msg: string);
|
||||||
}
|
}
|
||||||
declare class TimeoutException extends ScriptException {
|
|
||||||
|
declare class LoginRequiredException extends ScriptException {
|
||||||
constructor(msg: string);
|
constructor(msg: string);
|
||||||
}
|
}
|
||||||
|
//Alias
|
||||||
|
declare class ScriptLoginRequiredException extends ScriptException {
|
||||||
|
constructor(msg: string);
|
||||||
|
}
|
||||||
|
|
||||||
|
declare class CaptchaRequiredException extends ScriptException {
|
||||||
|
constructor(url: string, body: string);
|
||||||
|
}
|
||||||
|
|
||||||
|
declare class CriticalException extends ScriptException {
|
||||||
|
constructor(msg: string);
|
||||||
|
}
|
||||||
|
|
||||||
declare class UnavailableException extends ScriptException {
|
declare class UnavailableException extends ScriptException {
|
||||||
constructor(msg: string);
|
constructor(msg: string);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare class AgeException extends ScriptException {
|
||||||
|
constructor(msg: string);
|
||||||
|
}
|
||||||
|
|
||||||
|
declare class TimeoutException extends ScriptException {
|
||||||
|
constructor(msg: string);
|
||||||
|
}
|
||||||
|
|
||||||
declare class ScriptImplementationException extends ScriptException {
|
declare class ScriptImplementationException extends ScriptException {
|
||||||
constructor(msg: string);
|
constructor(msg: string);
|
||||||
}
|
}
|
||||||
@@ -38,16 +62,23 @@ declare class FilterCapability {
|
|||||||
|
|
||||||
|
|
||||||
declare class PlatformAuthorLink {
|
declare class PlatformAuthorLink {
|
||||||
constructor(id: PlatformID, name: string, url: string, thumbnail: string, subscribers: integer?);
|
constructor(id: PlatformID, name: string, url: string, thumbnail: string, subscribers: integer?, membershipUrl: string?);
|
||||||
|
}
|
||||||
|
|
||||||
|
declare class PlatformAuthorMembershipLink {
|
||||||
|
constructor(id: PlatformID, name: string, url: string, thumbnail: string, subscribers: integer?, membershipUrl: string?);
|
||||||
}
|
}
|
||||||
|
|
||||||
declare interface PlatformContentDef {
|
declare interface PlatformContentDef {
|
||||||
id: PlatformID,
|
id: PlatformID,
|
||||||
name: string,
|
name: string,
|
||||||
|
thumbnails: Thumbnails,
|
||||||
author: PlatformAuthorLink,
|
author: PlatformAuthorLink,
|
||||||
datetime: integer,
|
datetime: integer,
|
||||||
url: string
|
url: string
|
||||||
}
|
}
|
||||||
|
declare interface PlatformContent {}
|
||||||
|
|
||||||
declare interface PlatformNestedMediaContentDef extends PlatformContentDef {
|
declare interface PlatformNestedMediaContentDef extends PlatformContentDef {
|
||||||
contentUrl: string,
|
contentUrl: string,
|
||||||
contentName: string?,
|
contentName: string?,
|
||||||
@@ -59,16 +90,26 @@ declare class PlatformNestedMediaContent {
|
|||||||
constructor(obj: PlatformNestedMediaContentDef);
|
constructor(obj: PlatformNestedMediaContentDef);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare interface PlatformLockedContentDef extends PlatformContentDef {
|
||||||
|
contentName: string?,
|
||||||
|
contentThumbnails: Thumbnails?,
|
||||||
|
unlockUrl: string,
|
||||||
|
lockDescription: string?,
|
||||||
|
}
|
||||||
|
declare class PlatformLockedContent {
|
||||||
|
constructor(obj: PlatformLockedContentDef);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
declare interface PlatformVideoDef extends PlatformContentDef {
|
declare interface PlatformVideoDef extends PlatformContentDef {
|
||||||
thumbnails: Thumbnails,
|
thumbnails: Thumbnails,
|
||||||
author: PlatformAuthorLink,
|
author: PlatformAuthorLink,
|
||||||
|
|
||||||
duration: int,
|
duration: int,
|
||||||
viewCount: long,
|
viewCount: long,
|
||||||
isLive: boolean
|
isLive: boolean,
|
||||||
|
shareUrl: string?
|
||||||
}
|
}
|
||||||
declare interface PlatformContent {}
|
|
||||||
|
|
||||||
declare class PlatformVideo implements PlatformContent {
|
declare class PlatformVideo implements PlatformContent {
|
||||||
constructor(obj: PlatformVideoDef);
|
constructor(obj: PlatformVideoDef);
|
||||||
}
|
}
|
||||||
@@ -77,14 +118,15 @@ declare class PlatformVideo implements PlatformContent {
|
|||||||
declare interface PlatformVideoDetailsDef extends PlatformVideoDef {
|
declare interface PlatformVideoDetailsDef extends PlatformVideoDef {
|
||||||
description: string,
|
description: string,
|
||||||
video: VideoSourceDescriptor,
|
video: VideoSourceDescriptor,
|
||||||
live: SubtitleSource[],
|
live: IVideoSource,
|
||||||
rating: IRating
|
rating: IRating,
|
||||||
|
subtitles: SubtitleSource[]
|
||||||
}
|
}
|
||||||
declare class PlatformVideoDetails extends PlatformVideo {
|
declare class PlatformVideoDetails extends PlatformVideo {
|
||||||
constructor(obj: PlatformVideoDetailsDef);
|
constructor(obj: PlatformVideoDetailsDef);
|
||||||
}
|
}
|
||||||
|
|
||||||
declare class PlatformPostDef extends PlatformContentDef {
|
declare interface PlatformPostDef extends PlatformContentDef {
|
||||||
thumbnails: string[],
|
thumbnails: string[],
|
||||||
images: string[],
|
images: string[],
|
||||||
description: string
|
description: string
|
||||||
@@ -93,7 +135,7 @@ declare class PlatformPost extends PlatformContent {
|
|||||||
constructor(obj: PlatformPostDef)
|
constructor(obj: PlatformPostDef)
|
||||||
}
|
}
|
||||||
|
|
||||||
declare class PlatformPostDetailsDef extends PlatformPostDef {
|
declare interface PlatformPostDetailsDef extends PlatformPostDef {
|
||||||
rating: IRating,
|
rating: IRating,
|
||||||
textType: int,
|
textType: int,
|
||||||
content: String
|
content: String
|
||||||
@@ -110,8 +152,8 @@ declare interface MuxVideoSourceDescriptorDef {
|
|||||||
isUnMuxed: boolean,
|
isUnMuxed: boolean,
|
||||||
videoSources: VideoSource[]
|
videoSources: VideoSource[]
|
||||||
}
|
}
|
||||||
declare class MuxVideoSourceDescriptor implements IVideoSourceDescriptor {
|
declare class VideoSourceDescriptor implements IVideoSourceDescriptor {
|
||||||
constructor(obj: VideoSourceDescriptorDef);
|
constructor(videoSourcesOrObj: VideoSource[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
declare interface UnMuxVideoSourceDescriptorDef {
|
declare interface UnMuxVideoSourceDescriptorDef {
|
||||||
@@ -129,7 +171,7 @@ declare interface IVideoSource {
|
|||||||
declare interface IAudioSource {
|
declare interface IAudioSource {
|
||||||
|
|
||||||
}
|
}
|
||||||
interface VideoUrlSourceDef implements IVideoSource {
|
declare interface VideoUrlSourceDef implements IVideoSource {
|
||||||
width: integer,
|
width: integer,
|
||||||
height: integer,
|
height: integer,
|
||||||
container: string,
|
container: string,
|
||||||
@@ -139,22 +181,22 @@ interface VideoUrlSourceDef implements IVideoSource {
|
|||||||
duration: integer,
|
duration: integer,
|
||||||
url: string
|
url: string
|
||||||
}
|
}
|
||||||
class VideoUrlSource {
|
declare class VideoUrlSource {
|
||||||
constructor(obj: VideoUrlSourceDef);
|
constructor(obj: VideoUrlSourceDef);
|
||||||
|
|
||||||
getRequestModifier(): RequestModifier?;
|
getRequestModifier(): RequestModifier?;
|
||||||
}
|
}
|
||||||
interface VideoUrlRangeSourceDef extends VideoUrlSource {
|
declare interface VideoUrlRangeSourceDef extends VideoUrlSource {
|
||||||
itagId: integer,
|
itagId: integer,
|
||||||
initStart: integer,
|
initStart: integer,
|
||||||
initEnd: integer,
|
initEnd: integer,
|
||||||
indexStart: integer,
|
indexStart: integer,
|
||||||
indexEnd: integer,
|
indexEnd: integer,
|
||||||
}
|
}
|
||||||
class VideoUrlRangeSource extends VideoUrlSource {
|
declare class VideoUrlRangeSource extends VideoUrlSource {
|
||||||
constructor(obj: YTVideoSourceDef);
|
constructor(obj: YTVideoSourceDef);
|
||||||
}
|
}
|
||||||
interface AudioUrlSourceDef {
|
declare interface AudioUrlSourceDef {
|
||||||
name: string,
|
name: string,
|
||||||
bitrate: integer,
|
bitrate: integer,
|
||||||
container: string,
|
container: string,
|
||||||
@@ -163,24 +205,12 @@ interface AudioUrlSourceDef {
|
|||||||
url: string,
|
url: string,
|
||||||
language: string
|
language: string
|
||||||
}
|
}
|
||||||
class AudioUrlSource implements IAudioSource {
|
declare class AudioUrlSource implements IAudioSource {
|
||||||
constructor(obj: AudioUrlSourceDef);
|
constructor(obj: AudioUrlSourceDef);
|
||||||
|
|
||||||
getRequestModifier(): RequestModifier?;
|
getRequestModifier(): RequestModifier?;
|
||||||
}
|
}
|
||||||
interface IRequest {
|
declare interface AudioUrlRangeSourceDef extends AudioUrlSource {
|
||||||
url: string,
|
|
||||||
headers: Map<string, string>
|
|
||||||
}
|
|
||||||
interface IRequestModifierDef {
|
|
||||||
allowByteSkip: boolean
|
|
||||||
}
|
|
||||||
class RequestModifier {
|
|
||||||
constructor(obj: IRequestModifierDef) { }
|
|
||||||
|
|
||||||
modifyRequest(url: string, headers: Map<string, string>): IRequest;
|
|
||||||
}
|
|
||||||
interface AudioUrlRangeSourceDef extends AudioUrlSource {
|
|
||||||
itagId: integer,
|
itagId: integer,
|
||||||
initStart: integer,
|
initStart: integer,
|
||||||
initEnd: integer,
|
initEnd: integer,
|
||||||
@@ -188,28 +218,44 @@ interface AudioUrlRangeSourceDef extends AudioUrlSource {
|
|||||||
indexEnd: integer,
|
indexEnd: integer,
|
||||||
audioChannels: integer
|
audioChannels: integer
|
||||||
}
|
}
|
||||||
class AudioUrlRangeSource extends AudioUrlSource {
|
declare class AudioUrlRangeSource extends AudioUrlSource {
|
||||||
constructor(obj: AudioUrlRangeSourceDef);
|
constructor(obj: AudioUrlRangeSourceDef);
|
||||||
}
|
}
|
||||||
interface HLSSourceDef {
|
declare interface HLSSourceDef {
|
||||||
name: string,
|
name: string,
|
||||||
duration: integer,
|
duration: integer,
|
||||||
url: string
|
url: string,
|
||||||
|
priority: boolean?,
|
||||||
|
language: string?
|
||||||
}
|
}
|
||||||
class HLSSource implements IVideoSource {
|
declare class HLSSource implements IVideoSource {
|
||||||
constructor(obj: HLSSourceDef);
|
constructor(obj: HLSSourceDef);
|
||||||
}
|
}
|
||||||
interface DashSourceDef {
|
declare interface DashSourceDef {
|
||||||
name: string,
|
name: string,
|
||||||
duration: integer,
|
duration: integer,
|
||||||
url: string
|
url: string,
|
||||||
|
language: string?
|
||||||
}
|
}
|
||||||
class DashSource implements IVideoSource {
|
declare class DashSource implements IVideoSource {
|
||||||
constructor(obj: DashSourceDef)
|
constructor(obj: DashSourceDef)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare interface IRequest {
|
||||||
|
url: string,
|
||||||
|
headers: Map<string, string>
|
||||||
|
}
|
||||||
|
declare interface IRequestModifierDef {
|
||||||
|
allowByteSkip: boolean
|
||||||
|
}
|
||||||
|
declare class RequestModifier {
|
||||||
|
constructor(obj: IRequestModifierDef) { }
|
||||||
|
|
||||||
|
modifyRequest(url: string, headers: Map<string, string>): IRequest;
|
||||||
|
}
|
||||||
|
|
||||||
//Channel
|
//Channel
|
||||||
interface PlatformChannelDef {
|
declare interface PlatformChannelDef {
|
||||||
id: PlatformID,
|
id: PlatformID,
|
||||||
name: string,
|
name: string,
|
||||||
thumbnail: string,
|
thumbnail: string,
|
||||||
@@ -217,12 +263,29 @@ interface PlatformChannelDef {
|
|||||||
subscribers: integer,
|
subscribers: integer,
|
||||||
description: string,
|
description: string,
|
||||||
url: string,
|
url: string,
|
||||||
|
urlAlternatives: string[],
|
||||||
links: Map<string>?
|
links: Map<string>?
|
||||||
}
|
}
|
||||||
class PlatformChannel {
|
declare class PlatformChannel {
|
||||||
constructor(obj: PlatformChannelDef);
|
constructor(obj: PlatformChannelDef);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Playlist
|
||||||
|
declare interface PlatformPlaylistDef implements PlatformContent {
|
||||||
|
videoCount: integer,
|
||||||
|
thumbnail: string
|
||||||
|
}
|
||||||
|
declare class PlatformPlaylist extends PlatformContent {
|
||||||
|
constructor(obj: PlatformPlaylistDef);
|
||||||
|
}
|
||||||
|
declare interface PlatformPlaylistDetailsDef implements PlatformPlaylistDef {
|
||||||
|
contents: ContentPager
|
||||||
|
}
|
||||||
|
declare class PlatformPlaylistDetails extends PlatformContent {
|
||||||
|
constructor(obj: PlatformPlaylistDetailsDef);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//Ratings
|
//Ratings
|
||||||
interface IRating {
|
interface IRating {
|
||||||
type: integer
|
type: integer
|
||||||
@@ -250,7 +313,11 @@ declare class PlatformComment {
|
|||||||
constructor(obj: CommentDef);
|
constructor(obj: CommentDef);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare class PlaybackTracker {
|
||||||
|
constructor(interval: integer);
|
||||||
|
|
||||||
|
setProgress(seconds: integer);
|
||||||
|
}
|
||||||
|
|
||||||
declare class LiveEventPager {
|
declare class LiveEventPager {
|
||||||
nextRequest = 4000;
|
nextRequest = 4000;
|
||||||
@@ -261,8 +328,8 @@ declare class LiveEventPager {
|
|||||||
nextPage(): LiveEventPager; //Could be self
|
nextPage(): LiveEventPager; //Could be self
|
||||||
}
|
}
|
||||||
|
|
||||||
class LiveEvent {
|
declare class LiveEvent {
|
||||||
type: String
|
constructor(type: integer);
|
||||||
}
|
}
|
||||||
declare class LiveEventComment extends LiveEvent {
|
declare class LiveEventComment extends LiveEvent {
|
||||||
constructor(name: string, message: string, thumbnail: string?, colorName: string?, badges: string[]);
|
constructor(name: string, message: string, thumbnail: string?, colorName: string?, badges: string[]);
|
||||||
@@ -287,25 +354,31 @@ declare class ContentPager {
|
|||||||
constructor(results: PlatformContent[], hasMore: boolean);
|
constructor(results: PlatformContent[], hasMore: boolean);
|
||||||
|
|
||||||
hasMorePagers(): boolean
|
hasMorePagers(): boolean
|
||||||
nextPage(): VideoPager; //Could be self
|
nextPage(): ContentPager?; //Could be self
|
||||||
}
|
}
|
||||||
declare class VideoPager {
|
declare class VideoPager {
|
||||||
constructor(results: PlatformVideo[], hasMore: boolean);
|
constructor(results: PlatformVideo[], hasMore: boolean);
|
||||||
|
|
||||||
hasMorePagers(): boolean
|
hasMorePagers(): boolean
|
||||||
nextPage(): VideoPager; //Could be self
|
nextPage(): VideoPager?; //Could be self
|
||||||
}
|
}
|
||||||
declare class ChannelPager {
|
declare class ChannelPager {
|
||||||
constructor(results: PlatformChannel[], hasMore: boolean);
|
constructor(results: PlatformChannel[], hasMore: boolean);
|
||||||
|
|
||||||
hasMorePagers(): boolean;
|
hasMorePagers(): boolean;
|
||||||
nextPage(): ChannelPager; //Could be self
|
nextPage(): ChannelPager?; //Could be self
|
||||||
|
}
|
||||||
|
declare class PlaylistPager {
|
||||||
|
constructor(results: PlatformPlaylist[], hasMore: boolean);
|
||||||
|
|
||||||
|
hasMorePagers(): boolean;
|
||||||
|
nextPage(): PlaylistPager?;
|
||||||
}
|
}
|
||||||
declare class CommentPager {
|
declare class CommentPager {
|
||||||
constructor(results: PlatformComment[], hasMore: boolean);
|
constructor(results: PlatformComment[], hasMore: boolean);
|
||||||
|
|
||||||
hasMorePagers(): boolean
|
hasMorePagers(): boolean
|
||||||
nextPage(): CommentPager; //Could be self
|
nextPage(): CommentPager?; //Could be self
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Map<T> {
|
interface Map<T> {
|
||||||
@@ -341,8 +414,9 @@ interface Source {
|
|||||||
getChannelCapabilities(): ResultCapabilities;
|
getChannelCapabilities(): ResultCapabilities;
|
||||||
|
|
||||||
isContentDetailsUrl(url: string): boolean;
|
isContentDetailsUrl(url: string): boolean;
|
||||||
getContentDetails(url: string): PlatformVideoDetails;
|
getContentDetails(url: string): PlatformContentDetails;
|
||||||
|
|
||||||
|
//Optional
|
||||||
getLiveEvents(url: string): LiveEventPager;
|
getLiveEvents(url: string): LiveEventPager;
|
||||||
|
|
||||||
//Optional
|
//Optional
|
||||||
|
|||||||
@@ -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,8 @@ class AudioUrlSource {
|
|||||||
this.duration = obj.duration ?? 0;
|
this.duration = obj.duration ?? 0;
|
||||||
this.url = obj.url;
|
this.url = obj.url;
|
||||||
this.language = obj.language ?? Language.UNKNOWN;
|
this.language = obj.language ?? Language.UNKNOWN;
|
||||||
|
if(obj.requestModifier)
|
||||||
|
this.requestModifier = obj.requestModifier;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class AudioUrlRangeSource extends AudioUrlSource {
|
class AudioUrlRangeSource extends AudioUrlSource {
|
||||||
@@ -370,6 +380,8 @@ class HLSSource {
|
|||||||
this.priority = obj.priority ?? false;
|
this.priority = obj.priority ?? false;
|
||||||
if(obj.language)
|
if(obj.language)
|
||||||
this.language = obj.language;
|
this.language = obj.language;
|
||||||
|
if(obj.requestModifier)
|
||||||
|
this.requestModifier = obj.requestModifier;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class DashSource {
|
class DashSource {
|
||||||
@@ -381,13 +393,15 @@ class DashSource {
|
|||||||
this.url = obj.url;
|
this.url = obj.url;
|
||||||
if(obj.language)
|
if(obj.language)
|
||||||
this.language = obj.language;
|
this.language = obj.language;
|
||||||
|
if(obj.requestModifier)
|
||||||
|
this.requestModifier = obj.requestModifier;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class RequestModifier {
|
class RequestModifier {
|
||||||
constructor(obj) {
|
constructor(obj) {
|
||||||
obj = obj ?? {};
|
obj = obj ?? {};
|
||||||
this.allowByteSkip = obj.allowByteSkip;
|
this.allowByteSkip = obj.allowByteSkip; //Kinda deprecated.. wip
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -232,7 +232,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,15 @@ 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.STAGING_SERVER)) {
|
||||||
|
removeServer(PolycentricCache.STAGING_SERVER)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!systemState.servers.contains(PolycentricCache.SERVER)) {
|
||||||
|
removeServer(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;
|
||||||
|
|
||||||
@@ -685,7 +685,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 +809,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 {
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import com.futo.platformplayer.logging.Logger
|
|||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StateBackup
|
import com.futo.platformplayer.states.StateBackup
|
||||||
import com.futo.platformplayer.stores.v2.ManagedStore
|
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||||
|
import com.futo.platformplayer.views.ToastView
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -303,12 +304,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) {
|
||||||
@@ -398,13 +403,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) {
|
||||||
@@ -621,9 +683,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 +731,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 +762,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), "",
|
||||||
{
|
{
|
||||||
@@ -713,7 +790,7 @@ class UISlideOverlays {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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 +798,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) {
|
||||||
|
StatePlatform.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,12 @@ 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.UrlVideoWithTime
|
import com.futo.platformplayer.models.UrlVideoWithTime
|
||||||
import com.futo.platformplayer.states.*
|
import com.futo.platformplayer.states.*
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
import com.futo.platformplayer.stores.SubscriptionStorage
|
import com.futo.platformplayer.stores.SubscriptionStorage
|
||||||
import com.futo.platformplayer.stores.v2.ManagedStore
|
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||||
|
import com.futo.platformplayer.views.ToastView
|
||||||
import com.google.gson.JsonParser
|
import com.google.gson.JsonParser
|
||||||
import com.google.zxing.integration.android.IntentIntegrator
|
import com.google.zxing.integration.android.IntentIntegrator
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
@@ -51,6 +56,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 +68,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 +99,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 +142,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}")
|
||||||
@@ -203,7 +213,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 +229,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 +321,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 +333,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 +418,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 +484,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 +538,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 +567,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" -> {
|
||||||
@@ -630,23 +651,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)");
|
||||||
@@ -799,11 +835,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 +883,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 +998,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 +1044,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>();
|
||||||
|
|||||||
+8
-2
@@ -12,14 +12,13 @@ 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.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,6 +70,13 @@ class PolycentricCreateProfileActivity : AppCompatActivity() {
|
|||||||
try {
|
try {
|
||||||
processHandle = ProcessHandle.create();
|
processHandle = ProcessHandle.create();
|
||||||
Store.instance.addProcessSecret(processHandle.processSecret);
|
Store.instance.addProcessSecret(processHandle.processSecret);
|
||||||
|
|
||||||
|
try {
|
||||||
|
PolycentricStorage.instance.addProcessSecret(processHandle.processSecret)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
|
||||||
|
}
|
||||||
|
|
||||||
processHandle.addServer("https://srv1-stg.polycentric.io");
|
processHandle.addServer("https://srv1-stg.polycentric.io");
|
||||||
processHandle.setUsername(username);
|
processHandle.setUsername(username);
|
||||||
StatePolycentric.instance.setProcessHandle(processHandle);
|
StatePolycentric.instance.setProcessHandle(processHandle);
|
||||||
|
|||||||
+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")
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
+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
|
||||||
) { }
|
) { }
|
||||||
+26
-2
@@ -2,6 +2,9 @@ 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
|
||||||
@@ -55,7 +58,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 +75,24 @@ 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.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
|
||||||
|
|||||||
+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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-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");
|
||||||
}
|
}
|
||||||
|
|||||||
+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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -50,6 +52,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 +274,6 @@ class ChromecastCastingDevice : CastingDevice {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun start() {
|
override fun start() {
|
||||||
val adrs = addresses ?: return;
|
|
||||||
if (_started) {
|
if (_started) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -283,152 +286,178 @@ 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(4096);
|
||||||
|
|
||||||
|
Logger.i(TAG, "Started receiving.");
|
||||||
|
while (_scopeIO?.isActive == true) {
|
||||||
|
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 = ((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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
@@ -593,6 +622,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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
@@ -190,6 +204,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());
|
||||||
}
|
}
|
||||||
@@ -440,6 +465,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) {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -10,6 +11,7 @@ import com.caoccao.javet.values.primitive.V8ValueBoolean
|
|||||||
import com.caoccao.javet.values.primitive.V8ValueInteger
|
import com.caoccao.javet.values.primitive.V8ValueInteger
|
||||||
import com.caoccao.javet.values.primitive.V8ValueString
|
import com.caoccao.javet.values.primitive.V8ValueString
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
|
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.internal.JSHttpClient
|
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
@@ -173,8 +175,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,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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+5
-1
@@ -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
|
||||||
@@ -336,8 +337,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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
+2
-1
@@ -418,6 +418,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 +466,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()
|
||||||
|
|||||||
+12
@@ -117,6 +117,7 @@ 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);
|
||||||
return@InsertedViewAdapterWithLoader holder;
|
return@InsertedViewAdapterWithLoader holder;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -200,6 +201,17 @@ 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 onRepliesClick(c: IPlatformComment) {
|
private fun onRepliesClick(c: IPlatformComment) {
|
||||||
val replyCount = c.replyCount ?: 0;
|
val replyCount = c.replyCount ?: 0;
|
||||||
var metadata = "";
|
var metadata = "";
|
||||||
|
|||||||
+8
-2
@@ -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);
|
||||||
};
|
};
|
||||||
|
|||||||
+3
-3
@@ -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")
|
||||||
|
|||||||
+48
-1
@@ -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
|
||||||
|
|
||||||
@@ -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
-17
@@ -314,8 +314,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 +333,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 +343,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,9 +469,7 @@ 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);
|
||||||
@@ -489,9 +488,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 +633,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 +662,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) {
|
||||||
|
|||||||
+34
-10
@@ -295,17 +295,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 +332,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 +339,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 +366,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 +477,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;
|
||||||
|
|||||||
+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() {
|
||||||
|
|||||||
+65
-25
@@ -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);
|
||||||
|
|
||||||
@@ -252,6 +269,11 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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)
|
||||||
|
|||||||
+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);
|
||||||
|
|||||||
+184
-104
@@ -50,6 +50,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
|
||||||
@@ -144,10 +145,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
|
||||||
@@ -251,6 +253,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 +330,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);
|
||||||
@@ -369,7 +373,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 +398,10 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
_monetization.onUrlTap.subscribe {
|
||||||
|
fragment.navigate<BrowserFragment>(it);
|
||||||
|
onMinimize.emit();
|
||||||
|
}
|
||||||
|
|
||||||
_player.attachPlayer();
|
_player.attachPlayer();
|
||||||
|
|
||||||
@@ -433,18 +441,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 +470,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);
|
||||||
@@ -667,9 +687,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 +765,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 +780,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 +793,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 +806,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 +820,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 +860,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 +1009,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 +1039,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 +1139,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 +1165,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 +1196,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 +1233,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 +1296,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 +1386,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 +1431,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 +1507,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 +1540,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 +1549,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 +1667,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 +1677,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 +1984,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")
|
||||||
@@ -2176,9 +2254,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 +2341,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 +2365,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);
|
||||||
}
|
}
|
||||||
@@ -2486,7 +2566,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
setVideoDetails(videoDetail);
|
setVideoDetails(videoDetail, false);
|
||||||
_liveTryJob = null;
|
_liveTryJob = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+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>>();
|
||||||
|
}
|
||||||
@@ -48,11 +48,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 +68,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) {
|
||||||
|
|||||||
@@ -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,10 @@ class PolycentricCache {
|
|||||||
.build();
|
.build();
|
||||||
|
|
||||||
private const val TAG = "PolycentricCache"
|
private const val TAG = "PolycentricCache"
|
||||||
const val SERVER = "https://srv1-stg.polycentric.io"
|
const val STAGING_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 +335,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!!;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,13 +4,18 @@ import android.content.BroadcastReceiver
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
|
||||||
class AudioNoisyReceiver : BroadcastReceiver() {
|
class AudioNoisyReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
Logger.i(TAG, "Audio Noisy received");
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
||||||
MediaControlReceiver.onPauseReceived.emit();
|
Logger.i(TAG, "Audio Noisy received");
|
||||||
|
MediaControlReceiver.onPauseReceived.emit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ class DownloadService : Service() {
|
|||||||
private val DOWNLOAD_NOTIF_ID = 3;
|
private val DOWNLOAD_NOTIF_ID = 3;
|
||||||
private val DOWNLOAD_NOTIF_TAG = "download";
|
private val DOWNLOAD_NOTIF_TAG = "download";
|
||||||
private val DOWNLOAD_NOTIF_CHANNEL_ID = "downloadChannel";
|
private val DOWNLOAD_NOTIF_CHANNEL_ID = "downloadChannel";
|
||||||
|
private val DOWNLOAD_NOTIF_CHANNEL_NAME = "Downloads";
|
||||||
|
|
||||||
//Context
|
//Context
|
||||||
private val _scope: CoroutineScope = CoroutineScope(Dispatchers.Default);
|
private val _scope: CoroutineScope = CoroutineScope(Dispatchers.Default);
|
||||||
@@ -95,7 +96,7 @@ class DownloadService : Service() {
|
|||||||
}
|
}
|
||||||
fun setupNotificationRequirements() {
|
fun setupNotificationRequirements() {
|
||||||
_notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
|
_notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
|
||||||
_notificationChannel = NotificationChannel(DOWNLOAD_NOTIF_CHANNEL_ID, "Temp", NotificationManager.IMPORTANCE_DEFAULT).apply {
|
_notificationChannel = NotificationChannel(DOWNLOAD_NOTIF_CHANNEL_ID, DOWNLOAD_NOTIF_CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT).apply {
|
||||||
this.enableVibration(false);
|
this.enableVibration(false);
|
||||||
this.setSound(null, null);
|
this.setSound(null, null);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ class ExportingService : Service() {
|
|||||||
private val EXPORT_NOTIF_ID = 4;
|
private val EXPORT_NOTIF_ID = 4;
|
||||||
private val EXPORT_NOTIF_TAG = "export";
|
private val EXPORT_NOTIF_TAG = "export";
|
||||||
private val EXPORT_NOTIF_CHANNEL_ID = "exportChannel";
|
private val EXPORT_NOTIF_CHANNEL_ID = "exportChannel";
|
||||||
|
private val EXPORT_NOTIF_CHANNEL_NAME = "Export";
|
||||||
|
|
||||||
//Context
|
//Context
|
||||||
private val _scope: CoroutineScope = CoroutineScope(Dispatchers.Default);
|
private val _scope: CoroutineScope = CoroutineScope(Dispatchers.Default);
|
||||||
@@ -88,7 +89,7 @@ class ExportingService : Service() {
|
|||||||
}
|
}
|
||||||
fun setupNotificationRequirements() {
|
fun setupNotificationRequirements() {
|
||||||
_notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
|
_notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
|
||||||
_notificationChannel = NotificationChannel(EXPORT_NOTIF_CHANNEL_ID, "Temp", NotificationManager.IMPORTANCE_DEFAULT).apply {
|
_notificationChannel = NotificationChannel(EXPORT_NOTIF_CHANNEL_ID, EXPORT_NOTIF_CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT).apply {
|
||||||
this.enableVibration(false);
|
this.enableVibration(false);
|
||||||
this.setSound(null, null);
|
this.setSound(null, null);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ class MediaPlaybackService : Service() {
|
|||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
Logger.v(TAG, "onDestroy");
|
Logger.v(TAG, "onDestroy");
|
||||||
_instance = null;
|
_instance = null;
|
||||||
MediaControlReceiver.onCloseReceived.emit();
|
MediaControlReceiver.onPauseReceived.emit();
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,7 +153,7 @@ class MediaPlaybackService : Service() {
|
|||||||
|
|
||||||
fun closeMediaSession() {
|
fun closeMediaSession() {
|
||||||
Logger.v(TAG, "closeMediaSession");
|
Logger.v(TAG, "closeMediaSession");
|
||||||
stopForeground(STOP_FOREGROUND_DETACH);
|
stopForeground(STOP_FOREGROUND_REMOVE);
|
||||||
|
|
||||||
val focusRequest = _focusRequest;
|
val focusRequest = _focusRequest;
|
||||||
if (focusRequest != null) {
|
if (focusRequest != null) {
|
||||||
@@ -162,7 +162,9 @@ class MediaPlaybackService : Service() {
|
|||||||
}
|
}
|
||||||
_hasFocus = false;
|
_hasFocus = false;
|
||||||
|
|
||||||
_notificationManager?.cancel(MEDIA_NOTIF_ID);
|
val notifManager = _notificationManager;
|
||||||
|
Logger.i(TAG, "Cancelling playback notification (notifManager: ${notifManager != null})");
|
||||||
|
notifManager?.cancel(MEDIA_NOTIF_ID);
|
||||||
_notif_last_video = null;
|
_notif_last_video = null;
|
||||||
_notif_last_bitmap = null;
|
_notif_last_bitmap = null;
|
||||||
_mediaSession = null;
|
_mediaSession = null;
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import kotlinx.coroutines.withContext
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.util.Random
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
class StateAnnouncement {
|
class StateAnnouncement {
|
||||||
@@ -252,41 +251,6 @@ class StateAnnouncement {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun registerDidYouKnow() {
|
|
||||||
val random = Random();
|
|
||||||
val message: String? = when (random.nextInt(4 * 18 + 1)) {
|
|
||||||
0 -> "You can login to different platforms and unify your content experience. Check it out in the source settings!"
|
|
||||||
1 -> "Importing your playlists and subscriptions from other platforms to Grayjay is quick and easy. Check it out in the source settings!"
|
|
||||||
2 -> "Want to cast to a big screen? Try out FCast (https://fcast.org/)."
|
|
||||||
3 -> "Explore Grayjay's gesture controls. When in full-screen swipe on the left to change brightness, swipe on the right to change volume."
|
|
||||||
4 -> "Explore Grayjay's gesture controls. Swipe up in the center of a video to toggle full-screen."
|
|
||||||
5 -> "Grayjay's multi-platform search lets you find content from various sources."
|
|
||||||
6 -> "Grayjay's multi-platform search filters will unify filters across platforms. If your expected filters are not there, try toggling some platforms off in the search filters."
|
|
||||||
7 -> "You can share playlists with friends on the playlist page and make full-backups in the settings page."
|
|
||||||
8 -> "Discover Grayjay's offline playback feature. Save content for when you're on the go!"
|
|
||||||
9 -> "Paid content from your favorite creators gets seamlessly integrated into your Grayjay feed. Login to a platform to seamlessly see content you paid for."
|
|
||||||
10 -> "Explore Grayjay's plugin features! Login, import playlists, and tweak plugin settings for a tailored experience."
|
|
||||||
11 -> "Directly engage with content by liking, disliking, or leaving comments on the Polycentric network."
|
|
||||||
12 -> "With Grayjay's rotation lock, you can watch videos in your preferred orientation regardless of device settings. Check it out during playback!"
|
|
||||||
13 -> "Grayjay supports background play. Listen to your favorite content even while multitasking!"
|
|
||||||
14 -> "Use Grayjay's quality selection to adjust video resolution. Save data or watch in high definition – it's up to you."
|
|
||||||
15 -> "Customize your Grayjay experience by changing playback speed. Watch content at your own pace."
|
|
||||||
16 -> "Save time by adding videos to your 'Watch Later' list. Perfect for catching up on content during your free time."
|
|
||||||
17 -> "On Grayjay, your playlists, subscriptions, and settings are stored offline for privacy and quick access."
|
|
||||||
18 -> "Explore and engage with live content using Grayjay's live stream feature."
|
|
||||||
else -> null
|
|
||||||
};
|
|
||||||
|
|
||||||
if (message != null) {
|
|
||||||
registerAnnouncement(
|
|
||||||
"did-you-know?",
|
|
||||||
"Did you know?",
|
|
||||||
message,
|
|
||||||
AnnouncementType.SESSION_RECURRING
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun registerDefaultHandlerAnnouncement() {
|
fun registerDefaultHandlerAnnouncement() {
|
||||||
registerAnnouncement(
|
registerAnnouncement(
|
||||||
"default-url-handler",
|
"default-url-handler",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import android.net.NetworkRequest
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.DocumentsContract
|
import android.provider.DocumentsContract
|
||||||
import android.util.DisplayMetrics
|
import android.util.DisplayMetrics
|
||||||
|
import android.util.Log
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
@@ -38,9 +39,9 @@ import com.futo.platformplayer.receivers.AudioNoisyReceiver
|
|||||||
import com.futo.platformplayer.services.DownloadService
|
import com.futo.platformplayer.services.DownloadService
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
import com.futo.platformplayer.stores.v2.ManagedStore
|
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||||
|
import com.futo.platformplayer.views.ToastView
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.time.OffsetDateTime
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import kotlin.system.measureTimeMillis
|
import kotlin.system.measureTimeMillis
|
||||||
@@ -380,13 +381,15 @@ class StateApp {
|
|||||||
|
|
||||||
Logger.i(TAG, "MainApp Starting: Initializing [Polycentric]");
|
Logger.i(TAG, "MainApp Starting: Initializing [Polycentric]");
|
||||||
StatePolycentric.instance.load(context);
|
StatePolycentric.instance.load(context);
|
||||||
Logger.i(TAG, "MainApp Starting: Initializing [Saved]");
|
|
||||||
StateSaved.instance.load();
|
|
||||||
|
|
||||||
Logger.i(TAG, "MainApp Starting: Initializing [Connectivity]");
|
Logger.i(TAG, "MainApp Starting: Initializing [Connectivity]");
|
||||||
displayMetrics = context.resources.displayMetrics;
|
displayMetrics = context.resources.displayMetrics;
|
||||||
ensureConnectivityManager(context);
|
ensureConnectivityManager(context);
|
||||||
|
|
||||||
|
Logger.i(TAG, "MainApp Starting: Cleaning up unused downloads");
|
||||||
|
StateDownloads.instance.cleanupDownloads();
|
||||||
|
|
||||||
|
|
||||||
Logger.i(TAG, "MainApp Starting: Initializing [Telemetry]");
|
Logger.i(TAG, "MainApp Starting: Initializing [Telemetry]");
|
||||||
if (!BuildConfig.DEBUG) {
|
if (!BuildConfig.DEBUG) {
|
||||||
StateTelemetry.instance.initialize();
|
StateTelemetry.instance.initialize();
|
||||||
@@ -423,8 +426,6 @@ class StateApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StateAnnouncement.instance.registerAnnouncement("fa4647d3-36fa-4c8c-832d-85b00fc72dca", "Disclaimer", "This is an early alpha build of the application, expect bugs and unfinished features.", AnnouncementType.DELETABLE, OffsetDateTime.now())
|
|
||||||
|
|
||||||
if(SettingsDev.instance.developerMode && SettingsDev.instance.devServerSettings.devServerOnBoot)
|
if(SettingsDev.instance.developerMode && SettingsDev.instance.devServerSettings.devServerOnBoot)
|
||||||
StateDeveloper.instance.runServer();
|
StateDeveloper.instance.runServer();
|
||||||
|
|
||||||
@@ -460,7 +461,9 @@ class StateApp {
|
|||||||
|
|
||||||
//Foreground download
|
//Foreground download
|
||||||
autoUpdateEnabled -> {
|
autoUpdateEnabled -> {
|
||||||
StateUpdate.instance.checkForUpdates(context, false);
|
scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
|
StateUpdate.instance.checkForUpdates(context, false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
@@ -471,7 +474,11 @@ class StateApp {
|
|||||||
Logger.i(TAG, "MainApp Started: Initialize [Noisy]");
|
Logger.i(TAG, "MainApp Started: Initialize [Noisy]");
|
||||||
_receiverBecomingNoisy?.let {
|
_receiverBecomingNoisy?.let {
|
||||||
_receiverBecomingNoisy = null;
|
_receiverBecomingNoisy = null;
|
||||||
context.unregisterReceiver(it);
|
try {
|
||||||
|
context.unregisterReceiver(it);
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.e(TAG, "Failed to unregister receiver.", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_receiverBecomingNoisy = AudioNoisyReceiver();
|
_receiverBecomingNoisy = AudioNoisyReceiver();
|
||||||
context.registerReceiver(_receiverBecomingNoisy, IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY));
|
context.registerReceiver(_receiverBecomingNoisy, IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY));
|
||||||
@@ -551,13 +558,31 @@ class StateApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StateAnnouncement.instance.registerDefaultHandlerAnnouncement();
|
StateAnnouncement.instance.registerDefaultHandlerAnnouncement();
|
||||||
StateAnnouncement.instance.registerDidYouKnow();
|
|
||||||
Logger.i(TAG, "MainApp Started: Finished");
|
Logger.i(TAG, "MainApp Started: Finished");
|
||||||
|
|
||||||
StatePlaylists.instance.toMigrateCheck();
|
StatePlaylists.instance.toMigrateCheck();
|
||||||
|
|
||||||
if(StateHistory.instance.shouldMigrateLegacyHistory())
|
if(StateHistory.instance.shouldMigrateLegacyHistory())
|
||||||
StateHistory.instance.migrateLegacyHistory();
|
StateHistory.instance.migrateLegacyHistory();
|
||||||
|
|
||||||
|
StateAnnouncement.instance.deleteAnnouncement("plugin-update")
|
||||||
|
|
||||||
|
scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
|
val updateAvailable = StatePlatform.instance.checkForUpdates()
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
if (updateAvailable.isNotEmpty()) {
|
||||||
|
UIDialogs.appToast(
|
||||||
|
ToastView.Toast(updateAvailable
|
||||||
|
.map { " - " + it.name }
|
||||||
|
.joinToString("\n"),
|
||||||
|
true,
|
||||||
|
null,
|
||||||
|
"Plugin updates available"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun mainAppStartedWithExternalFiles(context: Context) {
|
fun mainAppStartedWithExternalFiles(context: Context) {
|
||||||
@@ -619,7 +644,11 @@ class StateApp {
|
|||||||
Logger.i(TAG, "App ended");
|
Logger.i(TAG, "App ended");
|
||||||
_receiverBecomingNoisy?.let {
|
_receiverBecomingNoisy?.let {
|
||||||
_receiverBecomingNoisy = null;
|
_receiverBecomingNoisy = null;
|
||||||
context.unregisterReceiver(it);
|
try {
|
||||||
|
context.unregisterReceiver(it);
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.e(TAG, "Failed to unregister receiver.", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.i(TAG, "Unregistered network callback on connectivityManager.")
|
Logger.i(TAG, "Unregistered network callback on connectivityManager.")
|
||||||
|
|||||||
@@ -49,13 +49,17 @@ class StateCache {
|
|||||||
Logger.i(TAG, "Subscriptions CachePager get subscriptions");
|
Logger.i(TAG, "Subscriptions CachePager get subscriptions");
|
||||||
val subs = StateSubscriptions.instance.getSubscriptions();
|
val subs = StateSubscriptions.instance.getSubscriptions();
|
||||||
Logger.i(TAG, "Subscriptions CachePager polycentric urls");
|
Logger.i(TAG, "Subscriptions CachePager polycentric urls");
|
||||||
val allUrls = subs.map {
|
val allUrls = subs
|
||||||
|
.map {
|
||||||
val otherUrls = PolycentricCache.instance.getCachedProfile(it.channel.url)?.profile?.ownedClaims?.mapNotNull { c -> c.claim.resolveChannelUrl() } ?: listOf();
|
val otherUrls = PolycentricCache.instance.getCachedProfile(it.channel.url)?.profile?.ownedClaims?.mapNotNull { c -> c.claim.resolveChannelUrl() } ?: listOf();
|
||||||
if(!otherUrls.contains(it.channel.url))
|
if(!otherUrls.contains(it.channel.url))
|
||||||
return@map listOf(listOf(it.channel.url), otherUrls).flatten();
|
return@map listOf(listOf(it.channel.url), otherUrls).flatten();
|
||||||
else
|
else
|
||||||
return@map otherUrls;
|
return@map otherUrls;
|
||||||
}.flatten().distinct();
|
}
|
||||||
|
.flatten()
|
||||||
|
.distinct()
|
||||||
|
.filter { StatePlatform.instance.hasEnabledChannelClient(it) };
|
||||||
|
|
||||||
Logger.i(TAG, "Subscriptions CachePager get pagers");
|
Logger.i(TAG, "Subscriptions CachePager get pagers");
|
||||||
val pagers: List<IPager<IPlatformContent>>;
|
val pagers: List<IPager<IPlatformContent>>;
|
||||||
|
|||||||
@@ -352,7 +352,10 @@ class StateDownloads {
|
|||||||
|
|
||||||
fun cleanupDownloads(): Pair<Int, Long> {
|
fun cleanupDownloads(): Pair<Int, Long> {
|
||||||
val expected = getDownloadedVideos();
|
val expected = getDownloadedVideos();
|
||||||
val validFiles = HashSet(expected.flatMap { e -> e.videoSource.map { it.filePath } + e.audioSource.map { it.filePath } });
|
val validFiles = HashSet(expected.flatMap { e ->
|
||||||
|
e.videoSource.map { it.filePath } +
|
||||||
|
e.audioSource.map { it.filePath } +
|
||||||
|
e.subtitlesSources.map { it.filePath }});
|
||||||
|
|
||||||
var totalDeleted: Long = 0;
|
var totalDeleted: Long = 0;
|
||||||
var totalDeletedCount = 0;
|
var totalDeletedCount = 0;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.futo.platformplayer.states
|
package com.futo.platformplayer.states
|
||||||
|
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
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.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
@@ -49,6 +50,9 @@ class StateHistory {
|
|||||||
fun getHistoryPosition(url: String): Long {
|
fun getHistoryPosition(url: String): Long {
|
||||||
return historyIndex[url]?.position ?: 0;
|
return historyIndex[url]?.position ?: 0;
|
||||||
}
|
}
|
||||||
|
fun isHistoryWatched(url: String, duration: Long): Boolean {
|
||||||
|
return getHistoryPosition(url) > duration * 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fun updateHistoryPosition(liveObj: IPlatformVideo, index: DBHistory.Index, updateExisting: Boolean, position: Long = -1L): Long {
|
fun updateHistoryPosition(liveObj: IPlatformVideo, index: DBHistory.Index, updateExisting: Boolean, position: Long = -1L): Long {
|
||||||
@@ -92,14 +96,20 @@ class StateHistory {
|
|||||||
}
|
}
|
||||||
fun getHistoryByVideo(video: IPlatformVideo, create: Boolean = false, watchDate: OffsetDateTime? = null): DBHistory.Index? {
|
fun getHistoryByVideo(video: IPlatformVideo, create: Boolean = false, watchDate: OffsetDateTime? = null): DBHistory.Index? {
|
||||||
val existing = historyIndex[video.url];
|
val existing = historyIndex[video.url];
|
||||||
if(existing != null)
|
var result: DBHistory.Index? = null;
|
||||||
return _historyDBStore.get(existing.id!!);
|
if(existing != null) {
|
||||||
|
result = _historyDBStore.getOrNull(existing.id!!);
|
||||||
|
if(result == null)
|
||||||
|
UIDialogs.toast("History item null?\nNo history tracking..");
|
||||||
|
}
|
||||||
else if(create) {
|
else if(create) {
|
||||||
val newHistItem = HistoryVideo(SerializedPlatformVideo.fromVideo(video), 0, watchDate ?: OffsetDateTime.now());
|
val newHistItem = HistoryVideo(SerializedPlatformVideo.fromVideo(video), 0, watchDate ?: OffsetDateTime.now());
|
||||||
val id = _historyDBStore.insert(newHistItem);
|
val id = _historyDBStore.insert(newHistItem);
|
||||||
return _historyDBStore.get(id);
|
result = _historyDBStore.getOrNull(id);
|
||||||
|
if(result == null)
|
||||||
|
UIDialogs.toast("History creation failed?\nNo history tracking..");
|
||||||
}
|
}
|
||||||
return null;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeHistory(url: String) {
|
fun removeHistory(url: String) {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import androidx.collection.LruCache
|
|||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.api.media.IPlatformClient
|
import com.futo.platformplayer.api.media.IPlatformClient
|
||||||
import com.futo.platformplayer.api.media.IPluginSourced
|
import com.futo.platformplayer.api.media.IPluginSourced
|
||||||
import com.futo.platformplayer.api.media.PlatformMultiClientPool
|
import com.futo.platformplayer.api.media.PlatformMultiClientPool
|
||||||
@@ -45,6 +46,7 @@ import com.futo.platformplayer.models.ImageVariable
|
|||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
import com.futo.platformplayer.stores.StringArrayStorage
|
import com.futo.platformplayer.stores.StringArrayStorage
|
||||||
import com.futo.platformplayer.stores.StringStorage
|
import com.futo.platformplayer.stores.StringStorage
|
||||||
|
import com.futo.platformplayer.views.ToastView
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Deferred
|
import kotlinx.coroutines.Deferred
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -78,6 +80,7 @@ class StatePlatform {
|
|||||||
private val _clientsLock = Object();
|
private val _clientsLock = Object();
|
||||||
private val _availableClients : ArrayList<IPlatformClient> = ArrayList();
|
private val _availableClients : ArrayList<IPlatformClient> = ArrayList();
|
||||||
private val _enabledClients : ArrayList<IPlatformClient> = ArrayList();
|
private val _enabledClients : ArrayList<IPlatformClient> = ArrayList();
|
||||||
|
private var _updatesAvailableMap: HashSet<String> = hashSetOf();
|
||||||
|
|
||||||
//ClientPools are used to isolate plugin usage of certain components from others
|
//ClientPools are used to isolate plugin usage of certain components from others
|
||||||
//This prevents for example a background task like subscriptions from blocking a user from opening a video
|
//This prevents for example a background task like subscriptions from blocking a user from opening a video
|
||||||
@@ -92,11 +95,6 @@ class StatePlatform {
|
|||||||
private val _liveEventClientPool = PlatformMultiClientPool("LiveEvents", 1); //Used exclusively for live events
|
private val _liveEventClientPool = PlatformMultiClientPool("LiveEvents", 1); //Used exclusively for live events
|
||||||
|
|
||||||
|
|
||||||
private val _primaryClientPersistent = FragmentedStorage.get<StringStorage>("primaryClient");
|
|
||||||
private var _primaryClientObj : IPlatformClient? = null;
|
|
||||||
val primaryClient : IPlatformClient get() = _primaryClientObj ?: throw IllegalStateException("PlatformState not yet initialized");
|
|
||||||
|
|
||||||
|
|
||||||
private val _icons : HashMap<String, ImageVariable> = HashMap();
|
private val _icons : HashMap<String, ImageVariable> = HashMap();
|
||||||
|
|
||||||
val hasClients: Boolean get() = _availableClients.size > 0;
|
val hasClients: Boolean get() = _availableClients.size > 0;
|
||||||
@@ -169,8 +167,13 @@ class StatePlatform {
|
|||||||
var enabled: Array<String>;
|
var enabled: Array<String>;
|
||||||
synchronized(_clientsLock) {
|
synchronized(_clientsLock) {
|
||||||
for(e in _enabledClients) {
|
for(e in _enabledClients) {
|
||||||
e.disable();
|
try {
|
||||||
onSourceDisabled.emit(e);
|
e.disable();
|
||||||
|
onSourceDisabled.emit(e);
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
UIDialogs.appToast(ToastView.Toast("If this happens often, please inform the developers on Github", false, null, "Plugin [${e.name}] failed to disable"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_enabledClients.clear();
|
_enabledClients.clear();
|
||||||
@@ -205,20 +208,6 @@ class StatePlatform {
|
|||||||
.filter { id -> _availableClients.any { it.id == id } }
|
.filter { id -> _availableClients.any { it.id == id } }
|
||||||
.toTypedArray();
|
.toTypedArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
val primary = _primaryClientPersistent.value;
|
|
||||||
if(primary.isEmpty() || primary == StateDeveloper.DEV_ID) {
|
|
||||||
selectPrimaryClient(enabled.firstOrNull() ?: _availableClients.first().id);
|
|
||||||
} else if(!_availableClients.any { it.id == primary }) {
|
|
||||||
selectPrimaryClient(_availableClients.firstOrNull()?.id!!);
|
|
||||||
} else {
|
|
||||||
selectPrimaryClient(primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!enabled.any { it == primaryClient.id }) {
|
|
||||||
enabled = enabled.concat(primaryClient.id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
selectClients(*enabled);
|
selectClients(*enabled);
|
||||||
};
|
};
|
||||||
@@ -321,8 +310,6 @@ class StatePlatform {
|
|||||||
newClient.initialize();
|
newClient.initialize();
|
||||||
_enabledClients.add(newClient);
|
_enabledClients.add(newClient);
|
||||||
}
|
}
|
||||||
if (_primaryClientObj == client)
|
|
||||||
_primaryClientObj = newClient;
|
|
||||||
|
|
||||||
_availableClients.removeIf { it.id == id };
|
_availableClients.removeIf { it.id == id };
|
||||||
_availableClients.add(newClient);
|
_availableClients.add(newClient);
|
||||||
@@ -331,6 +318,11 @@ class StatePlatform {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
suspend fun enableClient(ids: List<String>) {
|
||||||
|
val currentClients = getEnabledClients().map { it.id };
|
||||||
|
selectClients(*(currentClients + ids).distinct().toTypedArray());
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Selects the enabled clients, meaning all clients that data is actively requested from.
|
* Selects the enabled clients, meaning all clients that data is actively requested from.
|
||||||
* If a client is disabled, NO requests are made to said client
|
* If a client is disabled, NO requests are made to said client
|
||||||
@@ -363,17 +355,6 @@ class StatePlatform {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Selects the primary client, meaning the first target for requests.
|
|
||||||
* At the moment, since multi-client requests are not yet implemented, this is the goto client.
|
|
||||||
*/
|
|
||||||
fun selectPrimaryClient(id: String) {
|
|
||||||
synchronized(_clientsLock) {
|
|
||||||
_primaryClientObj = getClient(id);
|
|
||||||
_primaryClientPersistent.setAndSave(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getHome(): IPager<IPlatformContent> {
|
fun getHome(): IPager<IPlatformContent> {
|
||||||
Logger.i(TAG, "Platform - getHome");
|
Logger.i(TAG, "Platform - getHome");
|
||||||
var clientIdsOngoing = mutableListOf<String>();
|
var clientIdsOngoing = mutableListOf<String>();
|
||||||
@@ -446,14 +427,12 @@ class StatePlatform {
|
|||||||
toAwait.map { PlaceholderPager(5, { PlatformContentPlaceholder(it.first.id) }) });
|
toAwait.map { PlaceholderPager(5, { PlatformContentPlaceholder(it.first.id) }) });
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getHomePrimary(): IPager<IPlatformContent> {
|
|
||||||
return primaryClient.getHome();
|
|
||||||
}
|
|
||||||
|
|
||||||
//Search
|
//Search
|
||||||
fun searchSuggestions(query: String): Array<String> {
|
fun searchSuggestions(query: String): Array<String> {
|
||||||
Logger.i(TAG, "Platform - searchSuggestions");
|
Logger.i(TAG, "Platform - searchSuggestions");
|
||||||
return primaryClient.searchSuggestions(query);
|
//TODO: hasSearchSuggestions
|
||||||
|
return getEnabledClients().firstOrNull()?.searchSuggestions(query) ?: arrayOf();
|
||||||
}
|
}
|
||||||
|
|
||||||
fun search(query: String, type: String? = null, sort: String? = null, filters: Map<String, List<String>> = mapOf(), clientIds: List<String>? = null): IPager<IPlatformContent> {
|
fun search(query: String, type: String? = null, sort: String? = null, filters: Map<String, List<String>> = mapOf(), clientIds: List<String>? = null): IPager<IPlatformContent> {
|
||||||
@@ -839,6 +818,7 @@ class StatePlatform {
|
|||||||
return urls;
|
return urls;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun hasEnabledPlaylistClient(url: String) : Boolean = getEnabledClients().any { it.isPlaylistUrl(url) };
|
||||||
fun getPlaylistClientOrNull(url: String): IPlatformClient? = getEnabledClients().find { it.isPlaylistUrl(url) }
|
fun getPlaylistClientOrNull(url: String): IPlatformClient? = getEnabledClients().find { it.isPlaylistUrl(url) }
|
||||||
fun getPlaylistClient(url: String): IPlatformClient = getEnabledClients().find { it.isPlaylistUrl(url) }
|
fun getPlaylistClient(url: String): IPlatformClient = getEnabledClients().find { it.isPlaylistUrl(url) }
|
||||||
?: throw NoPlatformClientException("No client enabled that supports this playlist url (${url})");
|
?: throw NoPlatformClientException("No client enabled that supports this playlist url (${url})");
|
||||||
@@ -884,7 +864,6 @@ class StatePlatform {
|
|||||||
synchronized(_clientsLock) {
|
synchronized(_clientsLock) {
|
||||||
val enabledExisting = _enabledClients.filter { it is DevJSClient };
|
val enabledExisting = _enabledClients.filter { it is DevJSClient };
|
||||||
val isEnabled = !enabledExisting.isEmpty()
|
val isEnabled = !enabledExisting.isEmpty()
|
||||||
val isPrimary = _primaryClientObj is DevJSClient;
|
|
||||||
|
|
||||||
for (enabled in enabledExisting) {
|
for (enabled in enabledExisting) {
|
||||||
enabled.disable();
|
enabled.disable();
|
||||||
@@ -899,11 +878,7 @@ class StatePlatform {
|
|||||||
devId = newClient.devID;
|
devId = newClient.devID;
|
||||||
try {
|
try {
|
||||||
StateDeveloper.instance.initializeDev(devId!!);
|
StateDeveloper.instance.initializeDev(devId!!);
|
||||||
if (isPrimary) {
|
if (isEnabled) {
|
||||||
_primaryClientObj = newClient;
|
|
||||||
_enabledClients.add(0, newClient);
|
|
||||||
newClient.initialize();
|
|
||||||
} else if (isEnabled) {
|
|
||||||
_enabledClients.add(newClient);
|
_enabledClients.add(newClient);
|
||||||
newClient.initialize();
|
newClient.initialize();
|
||||||
}
|
}
|
||||||
@@ -932,6 +907,67 @@ class StatePlatform {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun hasUpdateAvailable(c: SourcePluginConfig): Boolean {
|
||||||
|
val updatesAvailableMap = _updatesAvailableMap
|
||||||
|
synchronized(updatesAvailableMap) {
|
||||||
|
return updatesAvailableMap.contains(c.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun checkForUpdates(): List<SourcePluginConfig> = withContext(Dispatchers.IO) {
|
||||||
|
var configs = mutableListOf<SourcePluginConfig>()
|
||||||
|
val updatesAvailableFor = hashSetOf<String>()
|
||||||
|
for (availableClient in getAvailableClients().filter { it is JSClient && it.descriptor.appSettings.checkForUpdates }) {
|
||||||
|
if (availableClient !is JSClient) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checkForUpdates(availableClient.config)) {
|
||||||
|
configs.add(availableClient.config);
|
||||||
|
updatesAvailableFor.add(availableClient.config.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_updatesAvailableMap = updatesAvailableFor
|
||||||
|
return@withContext configs;
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearUpdateAvailable(c: SourcePluginConfig) {
|
||||||
|
val updatesAvailableMap = _updatesAvailableMap
|
||||||
|
synchronized(updatesAvailableMap) {
|
||||||
|
updatesAvailableMap.remove(c.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun checkForUpdates(c: SourcePluginConfig): Boolean = withContext(Dispatchers.IO) {
|
||||||
|
val sourceUrl = c.sourceUrl ?: return@withContext false;
|
||||||
|
|
||||||
|
Logger.i(TAG, "Check for source updates '${c.name}'.");
|
||||||
|
try {
|
||||||
|
val client = ManagedHttpClient();
|
||||||
|
val response = client.get(sourceUrl);
|
||||||
|
Logger.i(TAG, "Downloading source config '$sourceUrl'.");
|
||||||
|
|
||||||
|
if (!response.isOk || response.body == null) {
|
||||||
|
return@withContext false;
|
||||||
|
}
|
||||||
|
|
||||||
|
val configJson = response.body.string();
|
||||||
|
Logger.i(TAG, "Downloaded source config ($sourceUrl):\n${configJson}");
|
||||||
|
|
||||||
|
val config = SourcePluginConfig.fromJson(configJson);
|
||||||
|
if (config.version <= c.version) {
|
||||||
|
return@withContext false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.i(TAG, "Update is available (config.version=${config.version}, source.config.version=${c.version}).");
|
||||||
|
return@withContext true;
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to check for updates.", e);
|
||||||
|
return@withContext false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private var _instance : StatePlatform? = null;
|
private var _instance : StatePlatform? = null;
|
||||||
val instance : StatePlatform
|
val instance : StatePlatform
|
||||||
|
|||||||
@@ -361,6 +361,12 @@ class StatePlayer {
|
|||||||
if (queueShuffle) {
|
if (queueShuffle) {
|
||||||
removeFromShuffledQueue(video);
|
removeFromShuffledQueue(video);
|
||||||
}
|
}
|
||||||
|
if(currentVideo != null) {
|
||||||
|
val newPos = _queue.indexOfFirst { it.url == currentVideo?.url };
|
||||||
|
if(newPos >= 0)
|
||||||
|
_queuePosition = newPos;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onQueueChanged.emit(shouldSwapCurrentItem);
|
onQueueChanged.emit(shouldSwapCurrentItem);
|
||||||
@@ -407,6 +413,12 @@ class StatePlayer {
|
|||||||
if(_queue.size == 1) {
|
if(_queue.size == 1) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if(_queue.size <= _queuePosition && currentVideo != null) {
|
||||||
|
//Out of sync position
|
||||||
|
val newPos = _queue.indexOfFirst { it.url == currentVideo?.url }
|
||||||
|
if(newPos != -1)
|
||||||
|
_queuePosition = newPos;
|
||||||
|
}
|
||||||
|
|
||||||
val shuffledQueue = _queueShuffled;
|
val shuffledQueue = _queueShuffled;
|
||||||
val queue = if (queueShuffle && shuffledQueue != null) {
|
val queue = if (queueShuffle && shuffledQueue != null) {
|
||||||
@@ -421,6 +433,8 @@ class StatePlayer {
|
|||||||
}
|
}
|
||||||
//Standard Behavior
|
//Standard Behavior
|
||||||
if(_queuePosition - 1 >= 0) {
|
if(_queuePosition - 1 >= 0) {
|
||||||
|
if(queue.size <= _queuePosition)
|
||||||
|
return null;
|
||||||
return queue[_queuePosition - 1];
|
return queue[_queuePosition - 1];
|
||||||
}
|
}
|
||||||
//Repeat Behavior (End of queue)
|
//Repeat Behavior (End of queue)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import com.futo.platformplayer.exceptions.ReconstructionException
|
|||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.Playlist
|
import com.futo.platformplayer.models.Playlist
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
|
import com.futo.platformplayer.stores.StringArrayStorage
|
||||||
import com.futo.platformplayer.stores.v2.ManagedStore
|
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||||
import com.futo.platformplayer.stores.v2.ReconstructStore
|
import com.futo.platformplayer.stores.v2.ReconstructStore
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
@@ -35,6 +36,8 @@ class StatePlaylists {
|
|||||||
= SerializedPlatformVideo.fromVideo(StatePlatform.instance.getContentDetails(backup).await() as IPlatformVideoDetails);
|
= SerializedPlatformVideo.fromVideo(StatePlatform.instance.getContentDetails(backup).await() as IPlatformVideoDetails);
|
||||||
})
|
})
|
||||||
.load();
|
.load();
|
||||||
|
private val _watchlistOrderStore = FragmentedStorage.get<StringArrayStorage>("watchListOrder"); //Temporary workaround to add order..
|
||||||
|
|
||||||
val playlistStore = FragmentedStorage.storeJson<Playlist>("playlists")
|
val playlistStore = FragmentedStorage.storeJson<Playlist>("playlists")
|
||||||
.withRestore(PlaylistBackup())
|
.withRestore(PlaylistBackup())
|
||||||
.load();
|
.load();
|
||||||
@@ -48,26 +51,32 @@ class StatePlaylists {
|
|||||||
}
|
}
|
||||||
fun getWatchLater() : List<SerializedPlatformVideo> {
|
fun getWatchLater() : List<SerializedPlatformVideo> {
|
||||||
synchronized(_watchlistStore) {
|
synchronized(_watchlistStore) {
|
||||||
return _watchlistStore.getItems();
|
val order = _watchlistOrderStore.getAllValues();
|
||||||
|
return _watchlistStore.getItems().sortedBy { order.indexOf(it.url) };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun updateWatchLater(updated: List<SerializedPlatformVideo>) {
|
fun updateWatchLater(updated: List<SerializedPlatformVideo>) {
|
||||||
synchronized(_watchlistStore) {
|
synchronized(_watchlistStore) {
|
||||||
_watchlistStore.deleteAll();
|
_watchlistStore.deleteAll();
|
||||||
_watchlistStore.saveAllAsync(updated);
|
_watchlistStore.saveAllAsync(updated);
|
||||||
|
_watchlistOrderStore.set(*updated.map { it.url }.toTypedArray());
|
||||||
|
_watchlistOrderStore.save();
|
||||||
}
|
}
|
||||||
onWatchLaterChanged.emit();
|
onWatchLaterChanged.emit();
|
||||||
}
|
}
|
||||||
fun removeFromWatchLater(video: SerializedPlatformVideo) {
|
fun removeFromWatchLater(video: SerializedPlatformVideo) {
|
||||||
synchronized(_watchlistStore) {
|
synchronized(_watchlistStore) {
|
||||||
_watchlistStore.delete(video);
|
_watchlistStore.delete(video);
|
||||||
|
_watchlistOrderStore.set(*_watchlistOrderStore.values.filter { it != video.url }.toTypedArray());
|
||||||
|
_watchlistOrderStore.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
onWatchLaterChanged.emit();
|
onWatchLaterChanged.emit();
|
||||||
}
|
}
|
||||||
fun addToWatchLater(video: SerializedPlatformVideo) {
|
fun addToWatchLater(video: SerializedPlatformVideo) {
|
||||||
synchronized(_watchlistStore) {
|
synchronized(_watchlistStore) {
|
||||||
_watchlistStore.saveAsync(video);
|
_watchlistStore.saveAsync(video);
|
||||||
|
_watchlistOrderStore.set(*(listOf(video.url) + _watchlistOrderStore.values) .toTypedArray());
|
||||||
|
_watchlistOrderStore.save();
|
||||||
}
|
}
|
||||||
onWatchLaterChanged.emit();
|
onWatchLaterChanged.emit();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ 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.SourcePluginDescriptor
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginDescriptor
|
||||||
import com.futo.platformplayer.developer.DeveloperEndpoints
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.ImageVariable
|
import com.futo.platformplayer.models.ImageVariable
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
@@ -161,6 +160,13 @@ class StatePlugins {
|
|||||||
val configJson = StateAssets.readAsset(context, assetConfigPath) ?: return null;
|
val configJson = StateAssets.readAsset(context, assetConfigPath) ?: return null;
|
||||||
return SourcePluginConfig.fromJson(configJson, "");
|
return SourcePluginConfig.fromJson(configJson, "");
|
||||||
}
|
}
|
||||||
|
fun getEmbeddedPluginConfigFromID(context: Context, pluginId: String): SourcePluginConfig? {
|
||||||
|
val embedded = getEmbeddedSources(context);
|
||||||
|
if(!embedded.containsKey(pluginId))
|
||||||
|
return null;
|
||||||
|
return getEmbeddedPluginConfig(context, embedded[pluginId]!!);
|
||||||
|
}
|
||||||
|
|
||||||
fun installEmbeddedPlugin(context: Context, assetConfigPath: String, id: String? = null): Boolean {
|
fun installEmbeddedPlugin(context: Context, assetConfigPath: String, id: String? = null): Boolean {
|
||||||
try {
|
try {
|
||||||
val configJson = StateAssets.readAsset(context, assetConfigPath) ?:
|
val configJson = StateAssets.readAsset(context, assetConfigPath) ?:
|
||||||
@@ -467,7 +473,6 @@ class StatePlugins {
|
|||||||
_plugins.save(descriptor);
|
_plugins.save(descriptor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
private data class PluginConfig(
|
private data class PluginConfig(
|
||||||
val SOURCES_EMBEDDED: Map<String, String>,
|
val SOURCES_EMBEDDED: Map<String, String>,
|
||||||
|
|||||||
@@ -23,11 +23,25 @@ import com.futo.platformplayer.dp
|
|||||||
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
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||||
|
import com.futo.platformplayer.polycentric.PolycentricStorage
|
||||||
import com.futo.platformplayer.resolveChannelUrl
|
import com.futo.platformplayer.resolveChannelUrl
|
||||||
import com.futo.platformplayer.selectBestImage
|
import com.futo.platformplayer.selectBestImage
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
import com.futo.platformplayer.stores.StringStorage
|
import com.futo.platformplayer.stores.StringStorage
|
||||||
import com.futo.polycentric.core.*
|
import com.futo.polycentric.core.ApiMethods
|
||||||
|
import com.futo.polycentric.core.ClaimType
|
||||||
|
import com.futo.polycentric.core.ContentType
|
||||||
|
import com.futo.polycentric.core.Opinion
|
||||||
|
import com.futo.polycentric.core.ProcessHandle
|
||||||
|
import com.futo.polycentric.core.PublicKey
|
||||||
|
import com.futo.polycentric.core.SignedEvent
|
||||||
|
import com.futo.polycentric.core.SqlLiteDbHelper
|
||||||
|
import com.futo.polycentric.core.Store
|
||||||
|
import com.futo.polycentric.core.SystemState
|
||||||
|
import com.futo.polycentric.core.base64ToByteArray
|
||||||
|
import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl
|
||||||
|
import com.futo.polycentric.core.toBase64
|
||||||
|
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||||
import com.google.protobuf.ByteString
|
import com.google.protobuf.ByteString
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Deferred
|
import kotlinx.coroutines.Deferred
|
||||||
@@ -35,10 +49,10 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import userpackage.Protocol
|
import userpackage.Protocol
|
||||||
|
import userpackage.Protocol.Reference
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.time.ZoneOffset
|
import java.time.ZoneOffset
|
||||||
import kotlin.Exception
|
|
||||||
|
|
||||||
class StatePolycentric {
|
class StatePolycentric {
|
||||||
private data class LikeDislikeEntry(val unixMilliseconds: Long, val hasLiked: Boolean, val hasDisliked: Boolean);
|
private data class LikeDislikeEntry(val unixMilliseconds: Long, val hasLiked: Boolean, val hasDisliked: Boolean);
|
||||||
@@ -54,28 +68,40 @@ class StatePolycentric {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
for (i in 0 .. 1) {
|
||||||
val db = SqlLiteDbHelper(context);
|
try {
|
||||||
Store.initializeSqlLiteStore(db);
|
val db = SqlLiteDbHelper(context);
|
||||||
|
Store.initializeSqlLiteStore(db);
|
||||||
|
|
||||||
val activeProcessHandleString = _activeProcessHandle.value;
|
val activeProcessHandleString = _activeProcessHandle.value;
|
||||||
if (activeProcessHandleString.isNotEmpty()) {
|
if (activeProcessHandleString.isNotEmpty()) {
|
||||||
try {
|
try {
|
||||||
val system = PublicKey.fromProto(Protocol.PublicKey.parseFrom(activeProcessHandleString.base64ToByteArray()));
|
val system = PublicKey.fromProto(Protocol.PublicKey.parseFrom(activeProcessHandleString.base64ToByteArray()));
|
||||||
setProcessHandle(Store.instance.getProcessSecret(system)?.toProcessHandle());
|
setProcessHandle(Store.instance.getProcessSecret(system)?.toProcessHandle());
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
db.upgradeOldSecrets(db.writableDatabase);
|
db.upgradeOldSecrets(db.writableDatabase);
|
||||||
|
|
||||||
val system = PublicKey.fromProto(Protocol.PublicKey.parseFrom(activeProcessHandleString.base64ToByteArray()));
|
val system = PublicKey.fromProto(Protocol.PublicKey.parseFrom(activeProcessHandleString.base64ToByteArray()));
|
||||||
setProcessHandle(Store.instance.getProcessSecret(system)?.toProcessHandle());
|
setProcessHandle(Store.instance.getProcessSecret(system)?.toProcessHandle());
|
||||||
|
|
||||||
|
Log.i(TAG, "Failed to initialize Polycentric.", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getProcessHandles()
|
||||||
|
|
||||||
|
break;
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
if (i == 0) {
|
||||||
|
Logger.i(TAG, "Clearing Polycentric database due to corruption");
|
||||||
|
val db = SqlLiteDbHelper(context);
|
||||||
|
db.recreate()
|
||||||
|
} else {
|
||||||
|
_transientEnabled = false
|
||||||
|
UIDialogs.showGeneralErrorDialog(context, "Failed to initialize Polycentric.", e);
|
||||||
Log.i(TAG, "Failed to initialize Polycentric.", e)
|
Log.i(TAG, "Failed to initialize Polycentric.", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
|
||||||
_transientEnabled = false
|
|
||||||
UIDialogs.showGeneralErrorDialog(context, "Failed to initialize Polycentric.", e);
|
|
||||||
Log.i(TAG, "Failed to initialize Polycentric.", e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +116,32 @@ class StatePolycentric {
|
|||||||
return listOf()
|
return listOf()
|
||||||
}
|
}
|
||||||
|
|
||||||
return Store.instance.getProcessSecrets().map { it.toProcessHandle(); };
|
val storeProcessSecrets = Store.instance.getProcessSecrets().toMutableList()
|
||||||
|
val processSecrets = PolycentricStorage.instance.getProcessSecrets()
|
||||||
|
|
||||||
|
for (processSecret in processSecrets)
|
||||||
|
{
|
||||||
|
if (!storeProcessSecrets.contains(processSecret)) {
|
||||||
|
try {
|
||||||
|
Store.instance.addProcessSecret(processSecret)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to backfill process secret.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (processSecret in storeProcessSecrets)
|
||||||
|
{
|
||||||
|
if (!processSecrets.contains(processSecret)) {
|
||||||
|
try {
|
||||||
|
PolycentricStorage.instance.addProcessSecret(processSecret)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to backfill process secret.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (storeProcessSecrets + processSecrets).distinct().map { it.toProcessHandle() }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setProcessHandle(processHandle: ProcessHandle?) {
|
fun setProcessHandle(processHandle: ProcessHandle?) {
|
||||||
@@ -128,21 +179,21 @@ class StatePolycentric {
|
|||||||
_likeDislikeMap[ref.toByteArray().toBase64()] = LikeDislikeEntry(System.currentTimeMillis(), hasLiked, hasDisliked);
|
_likeDislikeMap[ref.toByteArray().toBase64()] = LikeDislikeEntry(System.currentTimeMillis(), hasLiked, hasDisliked);
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hasDisliked(ref: Protocol.Reference): Boolean {
|
fun hasDisliked(data: ByteArray): Boolean {
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
val entry = _likeDislikeMap[ref.toByteArray().toBase64()] ?: return false;
|
val entry = _likeDislikeMap[data.toBase64()] ?: return false;
|
||||||
return entry.hasDisliked;
|
return entry.hasDisliked;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hasLiked(ref: Protocol.Reference): Boolean {
|
fun hasLiked(data: ByteArray): Boolean {
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
val entry = _likeDislikeMap[ref.toByteArray().toBase64()] ?: return false;
|
val entry = _likeDislikeMap[data.toBase64()] ?: return false;
|
||||||
return entry.hasLiked;
|
return entry.hasLiked;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,7 +326,8 @@ class StatePolycentric {
|
|||||||
rating = RatingLikeDislikes(0, 0),
|
rating = RatingLikeDislikes(0, 0),
|
||||||
date = if (ev.unixMilliseconds != null) Instant.ofEpochMilli(ev.unixMilliseconds!!).atOffset(ZoneOffset.UTC) else OffsetDateTime.MIN,
|
date = if (ev.unixMilliseconds != null) Instant.ofEpochMilli(ev.unixMilliseconds!!).atOffset(ZoneOffset.UTC) else OffsetDateTime.MIN,
|
||||||
replyCount = 0,
|
replyCount = 0,
|
||||||
eventPointer = se.toPointer()
|
eventPointer = se.toPointer(),
|
||||||
|
parentReference = se.event.references.getOrNull(0)
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,7 +368,78 @@ class StatePolycentric {
|
|||||||
return LikesDislikesReplies(likes, dislikes, replyCount)
|
return LikesDislikesReplies(likes, dislikes, replyCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getCommentPager(contextUrl: String, reference: Protocol.Reference): IPager<IPlatformComment> {
|
suspend fun getComment(contextUrl: String, reference: Reference): PolycentricPlatformComment {
|
||||||
|
ensureEnabled()
|
||||||
|
|
||||||
|
if (reference.referenceType != 2L) {
|
||||||
|
throw Exception("Not a pointer")
|
||||||
|
}
|
||||||
|
|
||||||
|
val pointer = Protocol.Pointer.parseFrom(reference.reference)
|
||||||
|
val events = ApiMethods.getEvents(PolycentricCache.SERVER, pointer.system, Protocol.RangesForSystem.newBuilder()
|
||||||
|
.addRangesForProcesses(Protocol.RangesForProcess.newBuilder()
|
||||||
|
.setProcess(pointer.process)
|
||||||
|
.addRanges(Protocol.Range.newBuilder()
|
||||||
|
.setLow(pointer.logicalClock)
|
||||||
|
.setHigh(pointer.logicalClock)
|
||||||
|
.build())
|
||||||
|
.build())
|
||||||
|
.build())
|
||||||
|
|
||||||
|
val sev = SignedEvent.fromProto(events.getEvents(0))
|
||||||
|
val ev = sev.event
|
||||||
|
|
||||||
|
if (ev.contentType != ContentType.POST.value) {
|
||||||
|
throw Exception("This is not a comment")
|
||||||
|
}
|
||||||
|
|
||||||
|
val post = Protocol.Post.parseFrom(ev.content);
|
||||||
|
val systemLinkUrl = ev.system.systemToURLInfoSystemLinkUrl(listOf(PolycentricCache.SERVER));
|
||||||
|
val dp_25 = 25.dp(StateApp.instance.context.resources)
|
||||||
|
|
||||||
|
val profileEvents = ApiMethods.getQueryLatest(
|
||||||
|
PolycentricCache.SERVER,
|
||||||
|
ev.system.toProto(),
|
||||||
|
listOf(
|
||||||
|
ContentType.AVATAR.value,
|
||||||
|
ContentType.USERNAME.value
|
||||||
|
)
|
||||||
|
).eventsList.map { e -> SignedEvent.fromProto(e) }.groupBy { e -> e.event.contentType }
|
||||||
|
.map { (_, events) -> events.maxBy { x -> x.event.unixMilliseconds ?: 0 } };
|
||||||
|
|
||||||
|
val nameEvent = profileEvents.firstOrNull { e -> e.event.contentType == ContentType.USERNAME.value };
|
||||||
|
val avatarEvent = profileEvents.firstOrNull { e -> e.event.contentType == ContentType.AVATAR.value };
|
||||||
|
val imageBundle = if (avatarEvent != null) {
|
||||||
|
val lwwElementValue = avatarEvent.event.lwwElement?.value;
|
||||||
|
if (lwwElementValue != null) {
|
||||||
|
Protocol.ImageBundle.parseFrom(lwwElementValue)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
val ldr = getLikesDislikesReplies(reference)
|
||||||
|
return PolycentricPlatformComment(
|
||||||
|
contextUrl = contextUrl,
|
||||||
|
author = PlatformAuthorLink(
|
||||||
|
id = PlatformID("polycentric", systemLinkUrl, null, ClaimType.POLYCENTRIC.value.toInt()),
|
||||||
|
name = nameEvent?.event?.lwwElement?.value?.decodeToString() ?: "Unknown",
|
||||||
|
url = systemLinkUrl,
|
||||||
|
thumbnail = imageBundle?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(ev.system.toProto(), img.process, listOf(PolycentricCache.SERVER)) },
|
||||||
|
subscribers = null
|
||||||
|
),
|
||||||
|
msg = if (post.content.count() > PolycentricPlatformComment.MAX_COMMENT_SIZE) post.content.substring(0, PolycentricPlatformComment.MAX_COMMENT_SIZE) else post.content,
|
||||||
|
rating = RatingLikeDislikes(ldr.likes, ldr.dislikes),
|
||||||
|
date = if (ev.unixMilliseconds != null) Instant.ofEpochMilli(ev.unixMilliseconds!!).atOffset(ZoneOffset.UTC) else OffsetDateTime.MIN,
|
||||||
|
replyCount = ldr.replyCount.toInt(),
|
||||||
|
eventPointer = sev.toPointer(),
|
||||||
|
parentReference = sev.event.references.getOrNull(0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getCommentPager(contextUrl: String, reference: Protocol.Reference, extraByteReferences: List<ByteArray>? = null): IPager<IPlatformComment> {
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
return EmptyPager()
|
return EmptyPager()
|
||||||
}
|
}
|
||||||
@@ -338,7 +461,8 @@ class StatePolycentric {
|
|||||||
Protocol.QueryReferencesRequestCountReferences.newBuilder()
|
Protocol.QueryReferencesRequestCountReferences.newBuilder()
|
||||||
.setFromType(ContentType.POST.value)
|
.setFromType(ContentType.POST.value)
|
||||||
.build())
|
.build())
|
||||||
.build()
|
.build(),
|
||||||
|
extraByteReferences = extraByteReferences
|
||||||
);
|
);
|
||||||
|
|
||||||
val results = mapQueryReferences(contextUrl, response);
|
val results = mapQueryReferences(contextUrl, response);
|
||||||
@@ -407,7 +531,8 @@ class StatePolycentric {
|
|||||||
ContentType.AVATAR.value,
|
ContentType.AVATAR.value,
|
||||||
ContentType.USERNAME.value
|
ContentType.USERNAME.value
|
||||||
)
|
)
|
||||||
).eventsList.map { e -> SignedEvent.fromProto(e) };
|
).eventsList.map { e -> SignedEvent.fromProto(e) }.groupBy { e -> e.event.contentType }
|
||||||
|
.map { (_, events) -> events.maxBy { x -> x.event.unixMilliseconds ?: 0 } };
|
||||||
|
|
||||||
val nameEvent = profileEvents.firstOrNull { e -> e.event.contentType == ContentType.USERNAME.value };
|
val nameEvent = profileEvents.firstOrNull { e -> e.event.contentType == ContentType.USERNAME.value };
|
||||||
val avatarEvent = profileEvents.firstOrNull { e -> e.event.contentType == ContentType.AVATAR.value };
|
val avatarEvent = profileEvents.firstOrNull { e -> e.event.contentType == ContentType.AVATAR.value };
|
||||||
@@ -439,7 +564,8 @@ class StatePolycentric {
|
|||||||
rating = RatingLikeDislikes(likes, dislikes),
|
rating = RatingLikeDislikes(likes, dislikes),
|
||||||
date = if (unixMilliseconds != null) Instant.ofEpochMilli(unixMilliseconds).atOffset(ZoneOffset.UTC) else OffsetDateTime.MIN,
|
date = if (unixMilliseconds != null) Instant.ofEpochMilli(unixMilliseconds).atOffset(ZoneOffset.UTC) else OffsetDateTime.MIN,
|
||||||
replyCount = replies.toInt(),
|
replyCount = replies.toInt(),
|
||||||
eventPointer = sev.toPointer()
|
eventPointer = sev.toPointer(),
|
||||||
|
parentReference = sev.event.references.getOrNull(0)
|
||||||
);
|
);
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
return@mapNotNull null;
|
return@mapNotNull null;
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
package com.futo.platformplayer.states
|
|
||||||
|
|
||||||
import com.futo.platformplayer.api.media.Serializer
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
|
||||||
import com.futo.platformplayer.stores.StringStorage
|
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
|
|
||||||
@kotlinx.serialization.Serializable
|
|
||||||
data class VideoToOpen(val url: String, val timeSeconds: Long);
|
|
||||||
|
|
||||||
class StateSaved {
|
|
||||||
var videoToOpen: VideoToOpen? = null;
|
|
||||||
|
|
||||||
private val _videoToOpen = FragmentedStorage.get<StringStorage>("videoToOpen")
|
|
||||||
|
|
||||||
fun load() {
|
|
||||||
val videoToOpenString = _videoToOpen.value;
|
|
||||||
if (videoToOpenString.isNotEmpty()) {
|
|
||||||
try {
|
|
||||||
val v = Serializer.json.decodeFromString<VideoToOpen>(videoToOpenString);
|
|
||||||
videoToOpen = v;
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.w(TAG, "Failed to load video to open", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.i(TAG, "loaded videoToOpen=$videoToOpen");
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setVideoToOpenNonBlocking(v: VideoToOpen? = null) {
|
|
||||||
Logger.i(TAG, "set videoToOpen=$v");
|
|
||||||
|
|
||||||
videoToOpen = v;
|
|
||||||
_videoToOpen.setAndSave(if (v != null) Serializer.json.encodeToString(v) else "");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun setVideoToOpenBlocking(v: VideoToOpen? = null) {
|
|
||||||
Logger.i(TAG, "set videoToOpen=$v");
|
|
||||||
|
|
||||||
videoToOpen = v;
|
|
||||||
_videoToOpen.setAndSaveBlocking(if (v != null) Serializer.json.encodeToString(v) else "");
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val TAG = "StateSaved"
|
|
||||||
|
|
||||||
val instance: StateSaved = StateSaved()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +1,19 @@
|
|||||||
package com.futo.platformplayer.states
|
package com.futo.platformplayer.states
|
||||||
|
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
|
||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
|
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
|
||||||
import com.futo.platformplayer.api.media.structures.*
|
import com.futo.platformplayer.api.media.structures.*
|
||||||
import com.futo.platformplayer.api.media.structures.ReusablePager.Companion.asReusable
|
import com.futo.platformplayer.api.media.structures.ReusablePager.Companion.asReusable
|
||||||
import com.futo.platformplayer.constructs.Event0
|
|
||||||
import com.futo.platformplayer.constructs.Event1
|
|
||||||
import com.futo.platformplayer.constructs.Event2
|
import com.futo.platformplayer.constructs.Event2
|
||||||
import com.futo.platformplayer.engine.exceptions.PluginException
|
import com.futo.platformplayer.constructs.Event3
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
import com.futo.platformplayer.functional.CentralizedFeed
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptCriticalException
|
|
||||||
import com.futo.platformplayer.exceptions.ChannelException
|
|
||||||
import com.futo.platformplayer.findNonRuntimeException
|
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
|
||||||
import com.futo.platformplayer.getNowDiffDays
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.Subscription
|
import com.futo.platformplayer.models.Subscription
|
||||||
|
import com.futo.platformplayer.models.SubscriptionGroup
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||||
import com.futo.platformplayer.resolveChannelUrl
|
import com.futo.platformplayer.resolveChannelUrl
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
@@ -32,15 +24,10 @@ import com.futo.platformplayer.subscription.SubscriptionFetchAlgorithm
|
|||||||
import com.futo.platformplayer.subscription.SubscriptionFetchAlgorithms
|
import com.futo.platformplayer.subscription.SubscriptionFetchAlgorithms
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.util.concurrent.ExecutionException
|
|
||||||
import java.util.concurrent.ForkJoinPool
|
import java.util.concurrent.ForkJoinPool
|
||||||
import java.util.concurrent.ForkJoinTask
|
|
||||||
import kotlin.collections.ArrayList
|
|
||||||
import kotlin.coroutines.resumeWithException
|
import kotlin.coroutines.resumeWithException
|
||||||
import kotlin.coroutines.suspendCoroutine
|
import kotlin.coroutines.suspendCoroutine
|
||||||
import kotlin.streams.asSequence
|
import kotlin.streams.asSequence
|
||||||
import kotlin.streams.toList
|
|
||||||
import kotlin.system.measureTimeMillis
|
|
||||||
|
|
||||||
/***
|
/***
|
||||||
* Used to maintain subscriptions
|
* Used to maintain subscriptions
|
||||||
@@ -54,28 +41,26 @@ class StateSubscriptions {
|
|||||||
override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder): Subscription =
|
override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder): Subscription =
|
||||||
Subscription(SerializedChannel.fromChannel(StatePlatform.instance.getChannelLive(backup, false)));
|
Subscription(SerializedChannel.fromChannel(StatePlatform.instance.getChannelLive(backup, false)));
|
||||||
}).load();
|
}).load();
|
||||||
|
private val _subscriptionOthers = FragmentedStorage.storeJson<Subscription>("subscriptions_others")
|
||||||
|
.withUnique { it.channel.url }
|
||||||
|
.load();
|
||||||
private val _subscriptionsPool = ForkJoinPool(Settings.instance.subscriptions.getSubscriptionsConcurrency());
|
private val _subscriptionsPool = ForkJoinPool(Settings.instance.subscriptions.getSubscriptionsConcurrency());
|
||||||
private val _legacySubscriptions = FragmentedStorage.get<SubscriptionStorage>();
|
private val _legacySubscriptions = FragmentedStorage.get<SubscriptionStorage>();
|
||||||
|
|
||||||
|
|
||||||
private var _globalSubscriptionsLock = Object();
|
|
||||||
private var _globalSubscriptionFeed: ReusablePager<IPlatformContent>? = null;
|
|
||||||
var isGlobalUpdating: Boolean = false
|
|
||||||
private set;
|
|
||||||
var globalSubscriptionExceptions: List<Throwable> = listOf()
|
|
||||||
private set;
|
|
||||||
|
|
||||||
private val _algorithmSubscriptions = SubscriptionFetchAlgorithms.SMART;
|
private val _algorithmSubscriptions = SubscriptionFetchAlgorithms.SMART;
|
||||||
|
|
||||||
private var _lastGlobalSubscriptionProgress: Int = 0;
|
val global: CentralizedFeed = CentralizedFeed();
|
||||||
private var _lastGlobalSubscriptionTotal: Int = 0;
|
val feeds: HashMap<String, CentralizedFeed> = hashMapOf();
|
||||||
val onGlobalSubscriptionsUpdateProgress = Event2<Int, Int>();
|
val onFeedProgress = Event3<String?, Int, Int>();
|
||||||
val onGlobalSubscriptionsUpdated = Event0();
|
|
||||||
val onGlobalSubscriptionsUpdatedOnce = Event1<Throwable?>();
|
|
||||||
val onGlobalSubscriptionsException = Event1<List<Throwable>>();
|
|
||||||
|
|
||||||
val onSubscriptionsChanged = Event2<List<Subscription>, Boolean>();
|
val onSubscriptionsChanged = Event2<List<Subscription>, Boolean>();
|
||||||
|
|
||||||
|
init {
|
||||||
|
global.onUpdateProgress.subscribe { progress, total ->
|
||||||
|
onFeedProgress.emit(null, progress, total);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun getOldestUpdateTime(): OffsetDateTime {
|
fun getOldestUpdateTime(): OffsetDateTime {
|
||||||
val subs = getSubscriptions();
|
val subs = getSubscriptions();
|
||||||
if(subs.size == 0)
|
if(subs.size == 0)
|
||||||
@@ -83,75 +68,98 @@ class StateSubscriptions {
|
|||||||
else
|
else
|
||||||
return subs.minOf { it.lastVideoUpdate };
|
return subs.minOf { it.lastVideoUpdate };
|
||||||
}
|
}
|
||||||
fun getGlobalSubscriptionProgress(): Pair<Int, Int> {
|
|
||||||
return Pair(_lastGlobalSubscriptionProgress, _lastGlobalSubscriptionTotal);
|
fun getFeed(id: String? = null, createIfNew: Boolean = false): CentralizedFeed? {
|
||||||
|
if(id == null)
|
||||||
|
return global;
|
||||||
|
else {
|
||||||
|
return synchronized(feeds) {
|
||||||
|
var f = feeds[id];
|
||||||
|
if(f == null && createIfNew) {
|
||||||
|
f = CentralizedFeed();
|
||||||
|
f.onUpdateProgress.subscribe { progress, total ->
|
||||||
|
onFeedProgress.emit(id, progress, total)
|
||||||
|
};
|
||||||
|
feeds[id] = f;
|
||||||
|
}
|
||||||
|
return@synchronized f;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fun updateSubscriptionFeed(scope: CoroutineScope, onlyIfNull: Boolean = false, onProgress: ((Int, Int)->Unit)? = null) {
|
|
||||||
|
fun getGlobalSubscriptionProgress(id: String? = null): Pair<Int, Int> {
|
||||||
|
val feed = getFeed(id, false) ?: return Pair(0, 0);
|
||||||
|
return Pair(feed.lastProgress, feed.lastTotal);
|
||||||
|
}
|
||||||
|
fun updateSubscriptionFeed(scope: CoroutineScope, onlyIfNull: Boolean = false, onProgress: ((Int, Int)->Unit)? = null, group: SubscriptionGroup? = null) {
|
||||||
|
val feed = getFeed(group?.id, true) ?: return;
|
||||||
Logger.v(TAG, "updateSubscriptionFeed");
|
Logger.v(TAG, "updateSubscriptionFeed");
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
synchronized(_globalSubscriptionsLock) {
|
synchronized(feed.lock) {
|
||||||
if (isGlobalUpdating || (onlyIfNull && _globalSubscriptionFeed != null)) {
|
if (feed.isGlobalUpdating || (onlyIfNull && feed.feed != null)) {
|
||||||
Logger.i(TAG, "Already updating subscriptions or not required")
|
Logger.i(TAG, "Already updating subscriptions or not required")
|
||||||
return@launch;
|
return@launch;
|
||||||
}
|
}
|
||||||
isGlobalUpdating = true;
|
feed.isGlobalUpdating = true;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
val subsResult = getSubscriptionsFeedWithExceptions(true, true, scope, { progress, total ->
|
val subsResult = getSubscriptionsFeedWithExceptions(true, true, scope, { progress, total ->
|
||||||
_lastGlobalSubscriptionProgress = progress;
|
feed.lastProgress = progress;
|
||||||
_lastGlobalSubscriptionTotal = total;
|
feed.lastTotal = total;
|
||||||
onGlobalSubscriptionsUpdateProgress.emit(progress, total);
|
feed.onUpdateProgress.emit(progress, total);
|
||||||
onProgress?.invoke(progress, total);
|
onProgress?.invoke(progress, total);
|
||||||
});
|
}, null, group);
|
||||||
if (subsResult.second.any()) {
|
if (subsResult.second.any()) {
|
||||||
globalSubscriptionExceptions = subsResult.second;
|
feed.exceptions = subsResult.second;
|
||||||
onGlobalSubscriptionsException.emit(subsResult.second);
|
feed.onException.emit(subsResult.second);
|
||||||
}
|
}
|
||||||
_globalSubscriptionFeed = subsResult.first.asReusable();
|
feed.feed = subsResult.first.asReusable();
|
||||||
synchronized(_globalSubscriptionsLock) {
|
synchronized(feed.lock) {
|
||||||
onGlobalSubscriptionsUpdated.emit();
|
feed.onUpdated.emit();
|
||||||
onGlobalSubscriptionsUpdatedOnce.emit(null);
|
feed.onUpdatedOnce.emit(null);
|
||||||
onGlobalSubscriptionsUpdatedOnce.clear();
|
feed.onUpdatedOnce.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (e: Throwable) {
|
catch (e: Throwable) {
|
||||||
synchronized(_globalSubscriptionsLock) {
|
synchronized(feed.lock) {
|
||||||
onGlobalSubscriptionsUpdatedOnce.emit(e);
|
feed.onUpdatedOnce.emit(e);
|
||||||
onGlobalSubscriptionsUpdatedOnce.clear();
|
feed.onUpdatedOnce.clear();
|
||||||
}
|
}
|
||||||
Logger.e(TAG, "Failed to update subscription feed.", e);
|
Logger.e(TAG, "Failed to update subscription feed.", e);
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
isGlobalUpdating = false;
|
feed.isGlobalUpdating = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
fun clearSubscriptionFeed() {
|
fun clearSubscriptionFeed(id: String? = null) {
|
||||||
synchronized(_globalSubscriptionsLock) {
|
val feed = getFeed(id) ?: return;
|
||||||
_globalSubscriptionFeed = null;
|
synchronized(feed.lock) {
|
||||||
|
feed.feed = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var loadIndex = 0;
|
private var loadIndex = 0;
|
||||||
suspend fun getGlobalSubscriptionFeed(scope: CoroutineScope, updated: Boolean): IPager<IPlatformContent> {
|
suspend fun getGlobalSubscriptionFeed(scope: CoroutineScope, updated: Boolean, group: SubscriptionGroup? = null): IPager<IPlatformContent> {
|
||||||
|
val feed = getFeed(group?.id, true) ?: return EmptyPager();
|
||||||
//Get Subscriptions only if null
|
//Get Subscriptions only if null
|
||||||
updateSubscriptionFeed(scope, !updated);
|
updateSubscriptionFeed(scope, !updated, null, group);
|
||||||
|
|
||||||
val evRef = Object();
|
val evRef = Object();
|
||||||
val result = suspendCoroutine {
|
val result = suspendCoroutine {
|
||||||
synchronized(_globalSubscriptionsLock) {
|
synchronized(feed.lock) {
|
||||||
if (_globalSubscriptionFeed != null && !updated) {
|
if (feed.feed != null && !updated) {
|
||||||
Logger.i(TAG, "Subscriptions got feed preloaded");
|
Logger.i(TAG, "Subscriptions got feed preloaded");
|
||||||
it.resumeWith(Result.success(_globalSubscriptionFeed!!.getWindow()));
|
it.resumeWith(Result.success(feed.feed!!.getWindow()));
|
||||||
} else {
|
} else {
|
||||||
val loadIndex = loadIndex++;
|
val loadIndex = loadIndex++;
|
||||||
Logger.i(TAG, "[${loadIndex}] Starting await update");
|
Logger.i(TAG, "[${loadIndex}] Starting await update");
|
||||||
onGlobalSubscriptionsUpdatedOnce.subscribe(evRef) {ex ->
|
feed.onUpdatedOnce.subscribe(evRef) { ex ->
|
||||||
Logger.i(TAG, "[${loadIndex}] Subscriptions got feed after update");
|
Logger.i(TAG, "[${loadIndex}] Subscriptions got feed after update");
|
||||||
if(ex != null)
|
if(ex != null)
|
||||||
it.resumeWithException(ex);
|
it.resumeWithException(ex);
|
||||||
else if (_globalSubscriptionFeed != null)
|
else if (feed.feed != null)
|
||||||
it.resumeWith(Result.success(_globalSubscriptionFeed!!.getWindow()));
|
it.resumeWith(Result.success(feed.feed!!.getWindow()));
|
||||||
else
|
else
|
||||||
it.resumeWithException(IllegalStateException("No subscription pager after change? Illegal null set on global subscriptions"))
|
it.resumeWithException(IllegalStateException("No subscription pager after change? Illegal null set on global subscriptions"))
|
||||||
}
|
}
|
||||||
@@ -176,12 +184,35 @@ class StateSubscriptions {
|
|||||||
return _subscriptions.findItem { it.channel.url == url || it.channel.urlAlternatives.contains(url) };
|
return _subscriptions.findItem { it.channel.url == url || it.channel.urlAlternatives.contains(url) };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fun getSubscriptionOther(url: String) : Subscription? {
|
||||||
|
synchronized(_subscriptionOthers) {
|
||||||
|
return _subscriptionOthers.findItem { it.isChannel(url)};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun getSubscriptionOtherOrCreate(url: String) : Subscription {
|
||||||
|
synchronized(_subscriptionOthers) {
|
||||||
|
val sub = getSubscriptionOther(url);
|
||||||
|
if(sub == null) {
|
||||||
|
val newSub = Subscription(SerializedChannel(PlatformID.NONE, url, null, null, 0, null, url, mapOf()));
|
||||||
|
newSub.isOther = true;
|
||||||
|
_subscriptions.save(newSub);
|
||||||
|
return newSub;
|
||||||
|
}
|
||||||
|
else return sub;
|
||||||
|
}
|
||||||
|
}
|
||||||
fun saveSubscription(sub: Subscription) {
|
fun saveSubscription(sub: Subscription) {
|
||||||
_subscriptions.save(sub, false, true);
|
_subscriptions.save(sub, false, true);
|
||||||
}
|
}
|
||||||
fun saveSubscriptionAsync(sub: Subscription) {
|
fun saveSubscriptionAsync(sub: Subscription) {
|
||||||
_subscriptions.saveAsync(sub, false, true);
|
_subscriptions.saveAsync(sub, false, true);
|
||||||
}
|
}
|
||||||
|
fun saveSubscriptionOther(sub: Subscription) {
|
||||||
|
_subscriptionOthers.save(sub, false, true);
|
||||||
|
}
|
||||||
|
fun saveSubscriptionOtherAsync(sub: Subscription) {
|
||||||
|
_subscriptionOthers.saveAsync(sub, false, true);
|
||||||
|
}
|
||||||
fun getSubscriptionCount(): Int {
|
fun getSubscriptionCount(): Int {
|
||||||
synchronized(_subscriptions) {
|
synchronized(_subscriptions) {
|
||||||
return _subscriptions.getItems().size;
|
return _subscriptions.getItems().size;
|
||||||
@@ -239,12 +270,19 @@ class StateSubscriptions {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSubscriptionRequestCount(): Map<JSClient, Int> {
|
fun getSubscriptionRequestCount(subGroup: SubscriptionGroup? = null): Map<JSClient, Int> {
|
||||||
|
val subs = getSubscriptions();
|
||||||
|
val emulatedSubs = subGroup?.let {
|
||||||
|
it.urls.map {url ->
|
||||||
|
subs.find { it.channel.url == url }
|
||||||
|
?: getSubscriptionOtherOrCreate(url);
|
||||||
|
};
|
||||||
|
} ?: subs;
|
||||||
return SubscriptionFetchAlgorithm.getAlgorithm(_algorithmSubscriptions, StateApp.instance.scope)
|
return SubscriptionFetchAlgorithm.getAlgorithm(_algorithmSubscriptions, StateApp.instance.scope)
|
||||||
.countRequests(getSubscriptions().associateWith { StatePolycentric.instance.getChannelUrls(it.channel.url, it.channel.id, true) });
|
.countRequests(emulatedSubs.associateWith { StatePolycentric.instance.getChannelUrls(it.channel.url, it.channel.id, true) });
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSubscriptionsFeedWithExceptions(allowFailure: Boolean = false, withCacheFallback: Boolean = false, cacheScope: CoroutineScope, onProgress: ((Int, Int)->Unit)? = null, onNewCacheHit: ((Subscription, IPlatformContent)->Unit)? = null): Pair<IPager<IPlatformContent>, List<Throwable>> {
|
fun getSubscriptionsFeedWithExceptions(allowFailure: Boolean = false, withCacheFallback: Boolean = false, cacheScope: CoroutineScope, onProgress: ((Int, Int)->Unit)? = null, onNewCacheHit: ((Subscription, IPlatformContent)->Unit)? = null, subGroup: SubscriptionGroup? = null): Pair<IPager<IPlatformContent>, List<Throwable>> {
|
||||||
val algo = SubscriptionFetchAlgorithm.getAlgorithm(_algorithmSubscriptions, cacheScope, allowFailure, withCacheFallback, _subscriptionsPool);
|
val algo = SubscriptionFetchAlgorithm.getAlgorithm(_algorithmSubscriptions, cacheScope, allowFailure, withCacheFallback, _subscriptionsPool);
|
||||||
if(onNewCacheHit != null)
|
if(onNewCacheHit != null)
|
||||||
algo.onNewCacheHit.subscribe(onNewCacheHit)
|
algo.onNewCacheHit.subscribe(onNewCacheHit)
|
||||||
@@ -253,10 +291,19 @@ class StateSubscriptions {
|
|||||||
onProgress?.invoke(progress, total);
|
onProgress?.invoke(progress, total);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val subs = getSubscriptions();
|
||||||
|
val emulatedSubs = subGroup?.let {
|
||||||
|
it.urls.map {url ->
|
||||||
|
subs.find { it.channel.url == url }
|
||||||
|
?: getSubscriptionOtherOrCreate(url);
|
||||||
|
};
|
||||||
|
} ?: subs;
|
||||||
|
|
||||||
|
|
||||||
val usePolycentric = true;
|
val usePolycentric = true;
|
||||||
val lock = Object();
|
val lock = Object();
|
||||||
var polycentricBudget: Int = 10;
|
var polycentricBudget: Int = 10;
|
||||||
val subUrls = getSubscriptions().parallelStream().map {
|
val subUrls = emulatedSubs.parallelStream().map {
|
||||||
if(usePolycentric) {
|
if(usePolycentric) {
|
||||||
val result = StatePolycentric.instance.getChannelUrlsWithUpdateResult(it.channel.url, it.channel.id, polycentricBudget <= 0, true);
|
val result = StatePolycentric.instance.getChannelUrlsWithUpdateResult(it.channel.url, it.channel.id, polycentricBudget <= 0, true);
|
||||||
if(result.first) {
|
if(result.first) {
|
||||||
|
|||||||
@@ -2,15 +2,15 @@ package com.futo.platformplayer.states
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Environment
|
import com.futo.platformplayer.BuildConfig
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
|
import com.futo.platformplayer.copyToOutputStream
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileNotFoundException
|
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
|
|
||||||
@@ -155,47 +155,45 @@ class StateUpdate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun checkForUpdates(context: Context, showUpToDateToast: Boolean) {
|
suspend fun checkForUpdates(context: Context, showUpToDateToast: Boolean, hideExceptionButtons: Boolean = false) = withContext(Dispatchers.IO) {
|
||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
try {
|
||||||
try {
|
val client = ManagedHttpClient();
|
||||||
val client = ManagedHttpClient();
|
val latestVersion = downloadVersionCode(client);
|
||||||
val latestVersion = downloadVersionCode(client);
|
|
||||||
|
|
||||||
if (latestVersion != null) {
|
if (latestVersion != null) {
|
||||||
val currentVersion = BuildConfig.VERSION_CODE;
|
val currentVersion = BuildConfig.VERSION_CODE;
|
||||||
Logger.i(TAG, "Current version ${currentVersion} latest version ${latestVersion}.");
|
Logger.i(TAG, "Current version ${currentVersion} latest version ${latestVersion}.");
|
||||||
|
|
||||||
if (latestVersion > currentVersion) {
|
if (latestVersion > currentVersion) {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
try {
|
try {
|
||||||
UIDialogs.showUpdateAvailableDialog(context, latestVersion);
|
UIDialogs.showUpdateAvailableDialog(context, latestVersion, hideExceptionButtons);
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
UIDialogs.toast(context, "Failed to show update dialog");
|
UIDialogs.toast(context, "Failed to show update dialog");
|
||||||
Logger.w(TAG, "Error occurred in update dialog.");
|
Logger.w(TAG, "Error occurred in update dialog.");
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (showUpToDateToast) {
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
UIDialogs.toast(context, "Already on latest version");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Logger.w(TAG, "Failed to retrieve version from version URL.");
|
if (showUpToDateToast) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
withContext(Dispatchers.Main) {
|
UIDialogs.toast(context, "Already on latest version");
|
||||||
UIDialogs.toast(context, "Failed to retrieve version");
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} else {
|
||||||
Logger.w(TAG, "Failed to check for updates.", e);
|
Logger.w(TAG, "Failed to retrieve version from version URL.");
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
UIDialogs.toast(context, "Failed to check for updates");
|
UIDialogs.toast(context, "Failed to retrieve version");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
} catch (e: Throwable) {
|
||||||
|
Logger.w(TAG, "Failed to check for updates.", e);
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
UIDialogs.toast(context, "Failed to check for updates");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun downloadApkToFile(client: ManagedHttpClient, destinationFile: File, isCancelled: (() -> Boolean)? = null) {
|
private fun downloadApkToFile(client: ManagedHttpClient, destinationFile: File, isCancelled: (() -> Boolean)? = null) {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import com.futo.platformplayer.states.StateApp
|
|||||||
import com.futo.platformplayer.stores.v2.JsonStoreSerializer
|
import com.futo.platformplayer.stores.v2.JsonStoreSerializer
|
||||||
import com.futo.platformplayer.stores.v2.StoreSerializer
|
import com.futo.platformplayer.stores.v2.StoreSerializer
|
||||||
import kotlinx.serialization.KSerializer
|
import kotlinx.serialization.KSerializer
|
||||||
|
import java.lang.IllegalArgumentException
|
||||||
import java.lang.reflect.Field
|
import java.lang.reflect.Field
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import java.util.concurrent.ConcurrentMap
|
import java.util.concurrent.ConcurrentMap
|
||||||
@@ -209,7 +210,9 @@ class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA
|
|||||||
|
|
||||||
fun getObject(id: Long) = get(id).obj!!;
|
fun getObject(id: Long) = get(id).obj!!;
|
||||||
fun get(id: Long): I {
|
fun get(id: Long): I {
|
||||||
return deserializeIndex(dbDaoBase.get(_sqlGet(id)));
|
val result = dbDaoBase.getNullable(_sqlGet(id))
|
||||||
|
?: throw IllegalArgumentException("DB [${name}] has no entry with id ${id}");
|
||||||
|
return deserializeIndex(result);
|
||||||
}
|
}
|
||||||
fun getOrNull(id: Long): I? {
|
fun getOrNull(id: Long): I? {
|
||||||
val result = dbDaoBase.getNullable(_sqlGet(id));
|
val result = dbDaoBase.getNullable(_sqlGet(id));
|
||||||
|
|||||||
+4
-4
@@ -13,6 +13,7 @@ import com.futo.platformplayer.api.media.structures.MultiChronoContentPager
|
|||||||
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.engine.exceptions.ScriptCriticalException
|
import com.futo.platformplayer.engine.exceptions.ScriptCriticalException
|
||||||
|
import com.futo.platformplayer.engine.exceptions.ScriptException
|
||||||
import com.futo.platformplayer.exceptions.ChannelException
|
import com.futo.platformplayer.exceptions.ChannelException
|
||||||
import com.futo.platformplayer.findNonRuntimeException
|
import com.futo.platformplayer.findNonRuntimeException
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionsFeedFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionsFeedFragment
|
||||||
@@ -55,7 +56,7 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
|||||||
val clientCacheCount = clientTasks.value.size - clientTaskCount;
|
val clientCacheCount = clientTasks.value.size - clientTaskCount;
|
||||||
val limit = clientTasks.key.getSubscriptionRateLimit();
|
val limit = clientTasks.key.getSubscriptionRateLimit();
|
||||||
if(clientCacheCount > 0 && clientTaskCount > 0 && limit != null && clientTaskCount >= limit && StateApp.instance.contextOrNull?.let { it is MainActivity && it.isFragmentActive<SubscriptionsFeedFragment>() } == true) {
|
if(clientCacheCount > 0 && clientTaskCount > 0 && limit != null && clientTaskCount >= limit && StateApp.instance.contextOrNull?.let { it is MainActivity && it.isFragmentActive<SubscriptionsFeedFragment>() } == true) {
|
||||||
UIDialogs.toast("[${clientTasks.key.name}] only updating ${clientTaskCount} most urgent channels (rqs). (${clientCacheCount} cached)");
|
UIDialogs.appToast("[${clientTasks.key.name}] only updating ${clientTaskCount} most urgent channels (rqs). (${clientCacheCount} cached)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +70,6 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
|||||||
val cachedChannels = mutableListOf<String>()
|
val cachedChannels = mutableListOf<String>()
|
||||||
val forkTasks = executeSubscriptionTasks(tasks, failedPlugins, cachedChannels);
|
val forkTasks = executeSubscriptionTasks(tasks, failedPlugins, cachedChannels);
|
||||||
|
|
||||||
|
|
||||||
val taskResults = arrayListOf<SubscriptionTaskResult>();
|
val taskResults = arrayListOf<SubscriptionTaskResult>();
|
||||||
val timeTotal = measureTimeMillis {
|
val timeTotal = measureTimeMillis {
|
||||||
for(task in forkTasks) {
|
for(task in forkTasks) {
|
||||||
@@ -126,7 +126,7 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
|||||||
val pager = MultiChronoContentPager(groupedPagers, allowFailure, 15);
|
val pager = MultiChronoContentPager(groupedPagers, allowFailure, 15);
|
||||||
pager.initialize();
|
pager.initialize();
|
||||||
|
|
||||||
return Result(DedupContentPager(pager), exs);
|
return Result(DedupContentPager(pager, StatePlatform.instance.getEnabledClients().map { it.id }), exs);
|
||||||
}
|
}
|
||||||
|
|
||||||
fun executeSubscriptionTasks(tasks: List<SubscriptionTask>, failedPlugins: MutableList<String>, cachedChannels: MutableList<String>): List<ForkJoinTask<SubscriptionTaskResult>> {
|
fun executeSubscriptionTasks(tasks: List<SubscriptionTask>, failedPlugins: MutableList<String>, cachedChannels: MutableList<String>): List<ForkJoinTask<SubscriptionTaskResult>> {
|
||||||
@@ -200,7 +200,7 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
|||||||
else {
|
else {
|
||||||
Logger.i(StateSubscriptions.TAG, "Channel ${task.sub.channel.name} failed, substituting with cache");
|
Logger.i(StateSubscriptions.TAG, "Channel ${task.sub.channel.name} failed, substituting with cache");
|
||||||
pager = StateCache.instance.getChannelCachePager(task.sub.channel.url);
|
pager = StateCache.instance.getChannelCachePager(task.sub.channel.url);
|
||||||
taskEx = ex;
|
taskEx = channelEx;
|
||||||
return@submit SubscriptionTaskResult(task, pager, taskEx);
|
return@submit SubscriptionTaskResult(task, pager, taskEx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,456 @@
|
|||||||
|
package com.futo.platformplayer.views
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.Path
|
||||||
|
import android.graphics.PointF
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.View
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
class IdenticonView(context: Context, attrs: AttributeSet) : View(context, attrs) {
|
||||||
|
var hashString: String = "default"
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
hash = md5(value)
|
||||||
|
iconGenerator = null
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
private var hash = ByteArray(16)
|
||||||
|
private var iconGenerator: IconGenerator? = null
|
||||||
|
private val path = Path()
|
||||||
|
|
||||||
|
override fun onDraw(canvas: Canvas) {
|
||||||
|
super.onDraw(canvas)
|
||||||
|
|
||||||
|
val radius = (width.coerceAtMost(height) / 2).toFloat()
|
||||||
|
val clipPath = path.apply {
|
||||||
|
reset()
|
||||||
|
addCircle(width / 2f, height / 2f, radius, Path.Direction.CW)
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.clipPath(clipPath)
|
||||||
|
|
||||||
|
if (iconGenerator == null) {
|
||||||
|
iconGenerator = IconGenerator(min(height, width).toFloat(), hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
iconGenerator?.render(canvas)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun md5(input: String): ByteArray {
|
||||||
|
val md = MessageDigest.getInstance("MD5")
|
||||||
|
return md.digest(input.toByteArray(Charsets.UTF_8))
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Shape {
|
||||||
|
fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint)
|
||||||
|
}
|
||||||
|
|
||||||
|
class CutCorner : Shape {
|
||||||
|
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
|
||||||
|
val k = size * 0.42f
|
||||||
|
val path = Path().apply {
|
||||||
|
moveTo(0f, 0f)
|
||||||
|
lineTo(size, 0f)
|
||||||
|
lineTo(size, size - k * 2)
|
||||||
|
lineTo(size - k, size)
|
||||||
|
lineTo(0f, size)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
canvas.drawPath(path, paint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SideTriangle : Shape {
|
||||||
|
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
|
||||||
|
val w = size / 2
|
||||||
|
val h = size * 0.8f
|
||||||
|
val path = Path().apply {
|
||||||
|
moveTo(size - w, 0f)
|
||||||
|
lineTo(size, h)
|
||||||
|
lineTo(size - w, h)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
canvas.drawPath(path, paint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MiddleSquare : Shape {
|
||||||
|
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
|
||||||
|
val s = size / 3
|
||||||
|
canvas.drawRect(s, s, size - s, size - s, paint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CornerSquare : Shape {
|
||||||
|
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
|
||||||
|
val inner = size * 0.1f
|
||||||
|
val outer = max(1f, size * 0.25f)
|
||||||
|
canvas.drawRect(outer, outer, size - inner - outer, size - inner - outer, paint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OffCenterCircle : Shape {
|
||||||
|
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
|
||||||
|
val m = size * 0.15f
|
||||||
|
val s = size * 0.5f
|
||||||
|
canvas.drawCircle(size - s - m, size - s - m, s / 2, paint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NegativeTriangle : Shape {
|
||||||
|
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
|
||||||
|
val inner = size * 0.1f
|
||||||
|
val outer = inner * 4
|
||||||
|
val path = Path().apply {
|
||||||
|
addRect(0f, 0f, size, size, Path.Direction.CW)
|
||||||
|
moveTo(outer, outer)
|
||||||
|
lineTo(size - inner, outer)
|
||||||
|
lineTo(outer + (size - outer - inner) / 2, size - inner)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
canvas.drawPath(path, paint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CutSquare : Shape {
|
||||||
|
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
|
||||||
|
val path = Path().apply {
|
||||||
|
moveTo(0f, 0f)
|
||||||
|
lineTo(size, 0f)
|
||||||
|
lineTo(size, size * 0.7f)
|
||||||
|
lineTo(size * 0.4f, size * 0.4f)
|
||||||
|
lineTo(size * 0.7f, size)
|
||||||
|
lineTo(0f, size)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
canvas.drawPath(path, paint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CornerPlusTriangle : Shape {
|
||||||
|
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
|
||||||
|
val halfSize = size / 2
|
||||||
|
canvas.drawRect(0f, 0f, size, halfSize, paint)
|
||||||
|
canvas.drawRect(0f, halfSize, halfSize, size, paint)
|
||||||
|
val path = Path().apply {
|
||||||
|
moveTo(halfSize, halfSize)
|
||||||
|
lineTo(size, halfSize)
|
||||||
|
lineTo(halfSize, size)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
canvas.drawPath(path, paint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NegativeSquare : Shape {
|
||||||
|
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
|
||||||
|
val inner = size * 0.14f
|
||||||
|
val outer = size * 0.35f
|
||||||
|
val path = Path().apply {
|
||||||
|
addRect(0f, 0f, size, size, Path.Direction.CW)
|
||||||
|
addRect(outer, outer, size - outer - inner, size - outer - inner, Path.Direction.CCW)
|
||||||
|
}
|
||||||
|
canvas.drawPath(path, paint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NegativeCircle : Shape {
|
||||||
|
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
|
||||||
|
val inner = size * 0.12f
|
||||||
|
val outer = inner * 3
|
||||||
|
val path = Path().apply {
|
||||||
|
addRect(0f, 0f, size, size, Path.Direction.CW)
|
||||||
|
addCircle(outer, outer, (size - inner - outer) / 2, Path.Direction.CCW)
|
||||||
|
}
|
||||||
|
canvas.drawPath(path, paint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NegativeRhombus : Shape {
|
||||||
|
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
|
||||||
|
val m = size * 0.25f
|
||||||
|
val path = Path().apply {
|
||||||
|
addRect(0f, 0f, size, size, Path.Direction.CW)
|
||||||
|
moveTo(m, size / 2)
|
||||||
|
lineTo(size / 2, m)
|
||||||
|
lineTo(size - m, size / 2)
|
||||||
|
lineTo(size / 2, size - m)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
canvas.drawPath(path, paint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConditionalCircle : Shape {
|
||||||
|
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
|
||||||
|
if (index == 0) {
|
||||||
|
val m = size * 0.4f
|
||||||
|
val s = size * 1.2f
|
||||||
|
canvas.drawCircle(m, m, s / 2, paint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HalfTriangle : Shape {
|
||||||
|
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
|
||||||
|
val path = Path().apply {
|
||||||
|
moveTo(size / 2, size / 2)
|
||||||
|
lineTo(size, size / 2)
|
||||||
|
lineTo(size / 2, size)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
canvas.drawPath(path, paint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Triangle(val corner: Int = 0) : Shape {
|
||||||
|
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
|
||||||
|
val path = Path().apply {
|
||||||
|
when (corner) {
|
||||||
|
0 -> {
|
||||||
|
moveTo(0f, 0f)
|
||||||
|
lineTo(size, 0f)
|
||||||
|
lineTo(0f, size)
|
||||||
|
}
|
||||||
|
1 -> {
|
||||||
|
moveTo(size, 0f)
|
||||||
|
lineTo(size, size)
|
||||||
|
lineTo(0f, size)
|
||||||
|
}
|
||||||
|
2 -> {
|
||||||
|
moveTo(0f, 0f)
|
||||||
|
lineTo(size, 0f)
|
||||||
|
lineTo(size, size)
|
||||||
|
}
|
||||||
|
3 -> {
|
||||||
|
moveTo(0f, 0f)
|
||||||
|
lineTo(0f, size)
|
||||||
|
lineTo(size, size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
canvas.drawPath(path, paint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BottomHalfTriangle : Shape {
|
||||||
|
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
|
||||||
|
val path = Path().apply {
|
||||||
|
moveTo(0f, size / 2)
|
||||||
|
lineTo(size, size / 2)
|
||||||
|
lineTo(size / 2, size)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
canvas.drawPath(path, paint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Rhombus : Shape {
|
||||||
|
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
|
||||||
|
val path = Path().apply {
|
||||||
|
moveTo(size / 2, 0f)
|
||||||
|
lineTo(size, size / 2)
|
||||||
|
lineTo(size / 2, size)
|
||||||
|
lineTo(0f, size / 2)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
canvas.drawPath(path, paint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Circle : Shape {
|
||||||
|
override fun draw(canvas: Canvas, size: Float, index: Int, paint: Paint) {
|
||||||
|
val m = size / 6
|
||||||
|
canvas.drawCircle(m, m, size / 2 - m, paint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class IconGenerator(private val size: Float, private val hash: ByteArray) {
|
||||||
|
private val digits: ByteArray
|
||||||
|
private var selectedColors = arrayOf<Paint>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
digits = ByteArray(max(12, hash.size * 2))
|
||||||
|
var index = 0
|
||||||
|
for (byte in hash) {
|
||||||
|
if (index >= digits.size) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
digits[index] = ((byte.toInt() shr 4) and 0x0f).toByte()
|
||||||
|
digits[index + 1] = (byte.toInt() and 0x0f).toByte()
|
||||||
|
index += 2
|
||||||
|
}
|
||||||
|
selectColors()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun selectColors() {
|
||||||
|
val value = hash.copyOfRange(hash.size - 4, hash.size).fold(0) { acc, byte ->
|
||||||
|
(acc shl 8) or (byte.toInt() and 0xFF)
|
||||||
|
} and 0x0FFFFFFF
|
||||||
|
val colorTheme = ColorTheme(hue = value.toFloat() / 0x0FFFFFFF)
|
||||||
|
|
||||||
|
val selectedColorIndices = mutableListOf<Int>()
|
||||||
|
for (i in 0 until 3) {
|
||||||
|
val index = (digits[8 + i].toInt() % colorTheme.colors.size)
|
||||||
|
selectedColorIndices.add(colorTheme.validateIndex(index, selectedColorIndices))
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedColors = selectedColorIndices.map { index ->
|
||||||
|
Paint().apply {
|
||||||
|
color = colorTheme.colors[index]
|
||||||
|
style = Paint.Style.FILL
|
||||||
|
}
|
||||||
|
}.toTypedArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun renderBitmap(): Bitmap {
|
||||||
|
val bitmap = Bitmap.createBitmap(size.toInt(), size.toInt(), Bitmap.Config.ARGB_8888)
|
||||||
|
val canvas = Canvas(bitmap)
|
||||||
|
render(canvas)
|
||||||
|
return bitmap
|
||||||
|
}
|
||||||
|
|
||||||
|
fun render(canvas: Canvas) {
|
||||||
|
canvas.drawColor(Color.WHITE)
|
||||||
|
|
||||||
|
renderShape(canvas, 0, outerShapes, 2, 3, arrayOf(
|
||||||
|
PointF(1f, 0f),
|
||||||
|
PointF(2f, 0f),
|
||||||
|
PointF(2f, 3f),
|
||||||
|
PointF(1f, 3f),
|
||||||
|
PointF(0f, 1f),
|
||||||
|
PointF(3f, 1f),
|
||||||
|
PointF(3f, 2f),
|
||||||
|
PointF(0f, 2f),
|
||||||
|
))
|
||||||
|
renderShape(canvas, 1, outerShapes, 4, 5, arrayOf(
|
||||||
|
PointF(0f, 0f),
|
||||||
|
PointF(3f, 0f),
|
||||||
|
PointF(3f, 3f),
|
||||||
|
PointF(0f, 3f),
|
||||||
|
))
|
||||||
|
renderShape(canvas, 2, centerShapes, 1, null, arrayOf(
|
||||||
|
PointF(1f, 1f),
|
||||||
|
PointF(2f, 1f),
|
||||||
|
PointF(2f, 2f),
|
||||||
|
PointF(1f, 2f),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderShape(
|
||||||
|
canvas: Canvas,
|
||||||
|
colorIndex: Int,
|
||||||
|
shapes: Array<Shape>,
|
||||||
|
index: Int,
|
||||||
|
rotationIndex: Int?,
|
||||||
|
positions: Array<PointF>
|
||||||
|
) {
|
||||||
|
val cellSize = size / 4
|
||||||
|
var r = rotationIndex?.let { digits[it].toInt() } ?: 0
|
||||||
|
val shape = shapes[digits[index].toInt() % shapes.size]
|
||||||
|
|
||||||
|
val paint = Paint().apply {
|
||||||
|
color = selectedColors[colorIndex % selectedColors.size].color
|
||||||
|
style = Paint.Style.FILL
|
||||||
|
}
|
||||||
|
|
||||||
|
for ((idx, position) in positions.withIndex()) {
|
||||||
|
canvas.save()
|
||||||
|
canvas.translate(position.x * cellSize, position.y * cellSize)
|
||||||
|
canvas.translate(cellSize / 2, cellSize / 2)
|
||||||
|
canvas.rotate((r % 4) * 90f)
|
||||||
|
canvas.translate(-cellSize / 2, -cellSize / 2)
|
||||||
|
|
||||||
|
shape.draw(canvas, cellSize, idx, paint)
|
||||||
|
canvas.restore()
|
||||||
|
r++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ColorTheme(val hue: Float, val saturation: Float = 0.5f) {
|
||||||
|
val colors: List<Int>
|
||||||
|
|
||||||
|
init {
|
||||||
|
colors = listOf(
|
||||||
|
// Dark gray
|
||||||
|
grayscaleColor(0f),
|
||||||
|
// Mid color
|
||||||
|
hslColor(hue, saturation, colorLightness(0.5f)),
|
||||||
|
// Light gray
|
||||||
|
grayscaleColor(1f),
|
||||||
|
// Light color
|
||||||
|
hslColor(hue, saturation, colorLightness(1f)),
|
||||||
|
// Dark color
|
||||||
|
hslColor(hue, saturation, colorLightness(0f))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun validateIndex(index: Int, selected: List<Int>): Int {
|
||||||
|
return if (isDuplicate(index, listOf(0, 4), selected) || isDuplicate(index, listOf(2, 3), selected)) {
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isDuplicate(index: Int, values: List<Int>, selected: List<Int>): Boolean {
|
||||||
|
if (!values.contains(index)) return false
|
||||||
|
return values.any { selected.contains(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun colorLightness(value: Float): Float = lightness(value, 0.4f, 0.8f)
|
||||||
|
|
||||||
|
private fun grayscaleLightness(value: Float): Float = lightness(value, 0.3f, 0.9f)
|
||||||
|
|
||||||
|
private fun lightness(value: Float, min: Float, max: Float): Float {
|
||||||
|
val lightness = min + value * (max - min)
|
||||||
|
return minOf(1f, maxOf(0f, lightness))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun grayscaleColor(lightness: Float): Int {
|
||||||
|
return Color.HSVToColor(floatArrayOf(0f, 0f, lightness))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hslColor(hue: Float, saturation: Float, lightness: Float): Int {
|
||||||
|
return Color.HSVToColor(floatArrayOf(hue, saturation, lightness))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val centerShapes = arrayOf(
|
||||||
|
CutCorner(),
|
||||||
|
SideTriangle(),
|
||||||
|
MiddleSquare(),
|
||||||
|
CornerSquare(),
|
||||||
|
OffCenterCircle(),
|
||||||
|
NegativeTriangle(),
|
||||||
|
CutSquare(),
|
||||||
|
HalfTriangle(),
|
||||||
|
CornerPlusTriangle(),
|
||||||
|
CutSquare(),
|
||||||
|
NegativeCircle(),
|
||||||
|
HalfTriangle(),
|
||||||
|
NegativeRhombus(),
|
||||||
|
ConditionalCircle()
|
||||||
|
)
|
||||||
|
|
||||||
|
val outerShapes = arrayOf(
|
||||||
|
Triangle(),
|
||||||
|
BottomHalfTriangle(),
|
||||||
|
Rhombus(),
|
||||||
|
Circle(),
|
||||||
|
)
|
||||||
|
|
||||||
|
private const val TAG = "IdenticonView"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,11 +8,13 @@ import android.view.View
|
|||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.futo.platformplayer.HorizontalSpaceItemDecoration
|
import com.futo.platformplayer.HorizontalSpaceItemDecoration
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.constructs.Event0
|
import com.futo.platformplayer.constructs.Event0
|
||||||
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||||
@@ -61,6 +63,7 @@ class MonetizationView : LinearLayout {
|
|||||||
|
|
||||||
val onSupportTap = Event0();
|
val onSupportTap = Event0();
|
||||||
val onStoreTap = Event0();
|
val onStoreTap = Event0();
|
||||||
|
val onUrlTap = Event1<String>();
|
||||||
|
|
||||||
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
|
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
|
||||||
inflate(context, R.layout.view_monetization, this);
|
inflate(context, R.layout.view_monetization, this);
|
||||||
@@ -70,10 +73,12 @@ class MonetizationView : LinearLayout {
|
|||||||
_membershipPlatform = findViewById(R.id.membership_platform);
|
_membershipPlatform = findViewById(R.id.membership_platform);
|
||||||
_buttonMembership.setOnClickListener {
|
_buttonMembership.setOnClickListener {
|
||||||
_membershipUrl?.let {
|
_membershipUrl?.let {
|
||||||
|
/*
|
||||||
val uri = Uri.parse(it);
|
val uri = Uri.parse(it);
|
||||||
val intent = Intent(Intent.ACTION_VIEW);
|
val intent = Intent(Intent.ACTION_VIEW);
|
||||||
intent.data = uri;
|
intent.data = uri;
|
||||||
context.startActivity(intent);
|
context.startActivity(intent);*/
|
||||||
|
onUrlTap.emit(it);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,9 +134,18 @@ class MonetizationView : LinearLayout {
|
|||||||
_buttonStore.visibility = View.GONE;
|
_buttonStore.visibility = View.GONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(profile.systemState.donationDestinations.isNotEmpty() ||
|
||||||
|
profile.systemState.membershipUrls.isNotEmpty() ||
|
||||||
|
profile.systemState.store.isNotEmpty() ||
|
||||||
|
profile.systemState.promotion.isNotEmpty())
|
||||||
|
_buttonSupport.isVisible = true;
|
||||||
|
else
|
||||||
|
_buttonSupport.isVisible = false;
|
||||||
|
|
||||||
_root.visibility = View.VISIBLE;
|
_root.visibility = View.VISIBLE;
|
||||||
} else {
|
} else {
|
||||||
_root.visibility = View.GONE;
|
_root.visibility = View.GONE;
|
||||||
|
_buttonSupport.isVisible = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
setMerchandise(null);
|
setMerchandise(null);
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ 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.core.view.isVisible
|
||||||
|
import androidx.core.view.size
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
||||||
@@ -33,6 +35,13 @@ class SupportView : LinearLayout {
|
|||||||
private var _textNoSupportOptionsSet: TextView
|
private var _textNoSupportOptionsSet: TextView
|
||||||
private var _polycentricProfile: PolycentricProfile? = null
|
private var _polycentricProfile: PolycentricProfile? = null
|
||||||
|
|
||||||
|
val hasSupportItems: Boolean get() {
|
||||||
|
return (_layoutPromotions.isVisible && _buttonPromotion.isVisible) ||
|
||||||
|
(_layoutMemberships.isVisible && _layoutMembershipEntries.isVisible && _layoutMembershipEntries.size > 0) ||
|
||||||
|
(_layoutDonation.isVisible && _layoutDonationEntries.isVisible && _layoutDonationEntries.size > 0) ||
|
||||||
|
_buttonStore.isVisible;
|
||||||
|
};
|
||||||
|
|
||||||
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
|
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
|
||||||
inflate(context, R.layout.view_support, this);
|
inflate(context, R.layout.view_support, this);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package com.futo.platformplayer.views
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.dp
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
|
||||||
|
class ToastView : LinearLayout {
|
||||||
|
private val root: LinearLayout;
|
||||||
|
private val title: TextView;
|
||||||
|
private val text: TextView;
|
||||||
|
init {
|
||||||
|
inflate(context, R.layout.toast, this);
|
||||||
|
root = findViewById(R.id.root);
|
||||||
|
title = findViewById(R.id.title);
|
||||||
|
text = findViewById(R.id.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
|
||||||
|
setToast(ToastView.Toast("", false))
|
||||||
|
root.visibility = GONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hide(animate: Boolean, onFinished: (()->Unit)? = null) {
|
||||||
|
Logger.i("MainActivity", "Hiding toast");
|
||||||
|
if(!animate) {
|
||||||
|
root.visibility = GONE;
|
||||||
|
alpha = 0f;
|
||||||
|
onFinished?.invoke();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
animate()
|
||||||
|
.alpha(0f)
|
||||||
|
.setDuration(700)
|
||||||
|
.translationY(20.dp(context.resources).toFloat())
|
||||||
|
.withEndAction { root.visibility = GONE; onFinished?.invoke(); }
|
||||||
|
.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun show(animate: Boolean) {
|
||||||
|
Logger.i("MainActivity", "Showing toast");
|
||||||
|
if(!animate) {
|
||||||
|
root.visibility = VISIBLE;
|
||||||
|
alpha = 1f;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
alpha = 0f;
|
||||||
|
root.visibility = VISIBLE;
|
||||||
|
translationY = 20.dp(context.resources).toFloat();
|
||||||
|
animate()
|
||||||
|
.alpha(1f)
|
||||||
|
.setDuration(700)
|
||||||
|
.translationY(0f)
|
||||||
|
.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun setToast(toast: Toast) {
|
||||||
|
if(toast.title.isNullOrEmpty())
|
||||||
|
title.isVisible = false;
|
||||||
|
else {
|
||||||
|
title.text = toast.title;
|
||||||
|
title.isVisible = true;
|
||||||
|
}
|
||||||
|
text.text = toast.msg;
|
||||||
|
if(toast.color != null)
|
||||||
|
text.setTextColor(toast.color);
|
||||||
|
else
|
||||||
|
text.setTextColor(Color.WHITE);
|
||||||
|
}
|
||||||
|
fun setToastAnimated(toast: Toast) {
|
||||||
|
hide(true) {
|
||||||
|
setToast(toast);
|
||||||
|
show(true);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class Toast(
|
||||||
|
val msg: String,
|
||||||
|
val long: Boolean,
|
||||||
|
val color: Int? = null,
|
||||||
|
val title: String? = null
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -9,15 +9,21 @@ import android.widget.LinearLayout
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||||
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
|
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
|
||||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes
|
import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes
|
||||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.fixHtmlLinks
|
||||||
|
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.setPlatformPlayerLinkMovementMethod
|
||||||
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.toHumanNowDiffString
|
||||||
|
import com.futo.platformplayer.toHumanNumber
|
||||||
import com.futo.platformplayer.views.others.CreatorThumbnail
|
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||||
import com.futo.platformplayer.views.pills.PillButton
|
import com.futo.platformplayer.views.pills.PillButton
|
||||||
import com.futo.platformplayer.views.pills.PillRatingLikesDislikes
|
import com.futo.platformplayer.views.pills.PillRatingLikesDislikes
|
||||||
@@ -104,7 +110,8 @@ class CommentViewHolder : ViewHolder {
|
|||||||
|
|
||||||
fun bind(comment: IPlatformComment, readonly: Boolean) {
|
fun bind(comment: IPlatformComment, readonly: Boolean) {
|
||||||
_creatorThumbnail.setThumbnail(comment.author.thumbnail, false);
|
_creatorThumbnail.setThumbnail(comment.author.thumbnail, false);
|
||||||
_creatorThumbnail.setHarborAvailable(comment is PolycentricPlatformComment,false);
|
val polycentricComment = if (comment is PolycentricPlatformComment) comment else null
|
||||||
|
_creatorThumbnail.setHarborAvailable(polycentricComment != null,false, polycentricComment?.eventPointer?.system?.toProto());
|
||||||
_textAuthor.text = comment.author.name;
|
_textAuthor.text = comment.author.name;
|
||||||
|
|
||||||
val date = comment.date;
|
val date = comment.date;
|
||||||
@@ -161,8 +168,8 @@ class CommentViewHolder : ViewHolder {
|
|||||||
_pillRatingLikesDislikes.visibility = View.VISIBLE;
|
_pillRatingLikesDislikes.visibility = View.VISIBLE;
|
||||||
|
|
||||||
if (comment is PolycentricPlatformComment) {
|
if (comment is PolycentricPlatformComment) {
|
||||||
val hasLiked = StatePolycentric.instance.hasLiked(comment.reference);
|
val hasLiked = StatePolycentric.instance.hasLiked(comment.reference.toByteArray());
|
||||||
val hasDisliked = StatePolycentric.instance.hasDisliked(comment.reference);
|
val hasDisliked = StatePolycentric.instance.hasDisliked(comment.reference.toByteArray());
|
||||||
_pillRatingLikesDislikes.setRating(comment.rating, hasLiked, hasDisliked);
|
_pillRatingLikesDislikes.setRating(comment.rating, hasLiked, hasDisliked);
|
||||||
} else {
|
} else {
|
||||||
_pillRatingLikesDislikes.setRating(comment.rating);
|
_pillRatingLikesDislikes.setRating(comment.rating);
|
||||||
|
|||||||
+10
-3
@@ -55,6 +55,7 @@ class CommentWithReferenceViewHolder : ViewHolder {
|
|||||||
|
|
||||||
var onRepliesClick = Event1<IPlatformComment>();
|
var onRepliesClick = Event1<IPlatformComment>();
|
||||||
var onDelete = Event1<IPlatformComment>();
|
var onDelete = Event1<IPlatformComment>();
|
||||||
|
var onClick = Event1<IPlatformComment>();
|
||||||
var comment: IPlatformComment? = null
|
var comment: IPlatformComment? = null
|
||||||
private set;
|
private set;
|
||||||
|
|
||||||
@@ -108,6 +109,11 @@ class CommentWithReferenceViewHolder : ViewHolder {
|
|||||||
onDelete.emit(c);
|
onDelete.emit(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_layoutComment.setOnClickListener {
|
||||||
|
val c = comment ?: return@setOnClickListener;
|
||||||
|
onClick.emit(c);
|
||||||
|
}
|
||||||
|
|
||||||
_textBody.setPlatformPlayerLinkMovementMethod(viewGroup.context);
|
_textBody.setPlatformPlayerLinkMovementMethod(viewGroup.context);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,7 +132,8 @@ class CommentWithReferenceViewHolder : ViewHolder {
|
|||||||
_taskGetLiveComment.cancel()
|
_taskGetLiveComment.cancel()
|
||||||
|
|
||||||
_creatorThumbnail.setThumbnail(comment.author.thumbnail, false);
|
_creatorThumbnail.setThumbnail(comment.author.thumbnail, false);
|
||||||
_creatorThumbnail.setHarborAvailable(comment is PolycentricPlatformComment,false);
|
val polycentricComment = if (comment is PolycentricPlatformComment) comment else null
|
||||||
|
_creatorThumbnail.setHarborAvailable(polycentricComment != null,false, polycentricComment?.eventPointer?.system?.toProto());
|
||||||
_textAuthor.text = comment.author.name;
|
_textAuthor.text = comment.author.name;
|
||||||
|
|
||||||
val date = comment.date;
|
val date = comment.date;
|
||||||
@@ -168,8 +175,8 @@ class CommentWithReferenceViewHolder : ViewHolder {
|
|||||||
if (likesDislikesReplies != null) {
|
if (likesDislikesReplies != null) {
|
||||||
Log.i(TAG, "updateLikesDislikesReplies set")
|
Log.i(TAG, "updateLikesDislikesReplies set")
|
||||||
|
|
||||||
val hasLiked = StatePolycentric.instance.hasLiked(c.reference);
|
val hasLiked = StatePolycentric.instance.hasLiked(c.reference.toByteArray());
|
||||||
val hasDisliked = StatePolycentric.instance.hasDisliked(c.reference);
|
val hasDisliked = StatePolycentric.instance.hasDisliked(c.reference.toByteArray());
|
||||||
_pillRatingLikesDislikes.setRating(RatingLikeDislikes(likesDislikesReplies.likes, likesDislikesReplies.dislikes), hasLiked, hasDisliked);
|
_pillRatingLikesDislikes.setRating(RatingLikeDislikes(likesDislikesReplies.likes, likesDislikesReplies.dislikes), hasLiked, hasDisliked);
|
||||||
|
|
||||||
_buttonReplies.setLoading(false)
|
_buttonReplies.setLoading(false)
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import android.view.ViewGroup
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.casting.CastingDevice
|
import com.futo.platformplayer.casting.CastingDevice
|
||||||
import com.futo.platformplayer.casting.StateCasting
|
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
|
||||||
class DeviceAdapter : RecyclerView.Adapter<DeviceViewHolder> {
|
class DeviceAdapter : RecyclerView.Adapter<DeviceViewHolder> {
|
||||||
@@ -13,6 +12,7 @@ class DeviceAdapter : RecyclerView.Adapter<DeviceViewHolder> {
|
|||||||
private val _isRememberedDevice: Boolean;
|
private val _isRememberedDevice: Boolean;
|
||||||
|
|
||||||
var onRemove = Event1<CastingDevice>();
|
var onRemove = Event1<CastingDevice>();
|
||||||
|
var onConnect = Event1<CastingDevice>();
|
||||||
|
|
||||||
constructor(devices: ArrayList<CastingDevice>, isRememberedDevice: Boolean) : super() {
|
constructor(devices: ArrayList<CastingDevice>, isRememberedDevice: Boolean) : super() {
|
||||||
_devices = devices;
|
_devices = devices;
|
||||||
@@ -26,6 +26,7 @@ class DeviceAdapter : RecyclerView.Adapter<DeviceViewHolder> {
|
|||||||
val holder = DeviceViewHolder(view);
|
val holder = DeviceViewHolder(view);
|
||||||
holder.setIsRememberedDevice(_isRememberedDevice);
|
holder.setIsRememberedDevice(_isRememberedDevice);
|
||||||
holder.onRemove.subscribe { d -> onRemove.emit(d); };
|
holder.onRemove.subscribe { d -> onRemove.emit(d); };
|
||||||
|
holder.onConnect.subscribe { d -> onConnect.emit(d); }
|
||||||
return holder;
|
return holder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ class DeviceViewHolder : ViewHolder {
|
|||||||
private set
|
private set
|
||||||
|
|
||||||
var onRemove = Event1<CastingDevice>();
|
var onRemove = Event1<CastingDevice>();
|
||||||
|
val onConnect = Event1<CastingDevice>();
|
||||||
|
|
||||||
constructor(view: View) : super(view) {
|
constructor(view: View) : super(view) {
|
||||||
_imageDevice = view.findViewById(R.id.image_device);
|
_imageDevice = view.findViewById(R.id.image_device);
|
||||||
@@ -56,7 +57,7 @@ class DeviceViewHolder : ViewHolder {
|
|||||||
val dev = device ?: return@setOnClickListener;
|
val dev = device ?: return@setOnClickListener;
|
||||||
StateCasting.instance.activeDevice?.stopCasting();
|
StateCasting.instance.activeDevice?.stopCasting();
|
||||||
StateCasting.instance.connectDevice(dev);
|
StateCasting.instance.connectDevice(dev);
|
||||||
updateButton();
|
onConnect.emit(dev);
|
||||||
};
|
};
|
||||||
|
|
||||||
_buttonRemove.setOnClickListener {
|
_buttonRemove.setOnClickListener {
|
||||||
@@ -64,6 +65,10 @@ class DeviceViewHolder : ViewHolder {
|
|||||||
onRemove.emit(dev);
|
onRemove.emit(dev);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
StateCasting.instance.onActiveDeviceConnectionStateChanged.subscribe(this) { _, _ ->
|
||||||
|
updateButton();
|
||||||
|
}
|
||||||
|
|
||||||
setIsRememberedDevice(false);
|
setIsRememberedDevice(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
package com.futo.platformplayer.views.adapters
|
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.futo.platformplayer.R
|
|
||||||
import com.futo.platformplayer.api.media.IPlatformClient
|
|
||||||
import com.futo.platformplayer.constructs.Event1
|
|
||||||
|
|
||||||
class DisabledSourceAdapter : RecyclerView.Adapter<DisabledSourceViewHolder> {
|
|
||||||
private val _sources: MutableList<IPlatformClient>;
|
|
||||||
|
|
||||||
var onClick = Event1<IPlatformClient>();
|
|
||||||
var onAdd = Event1<IPlatformClient>();
|
|
||||||
|
|
||||||
constructor(sources: MutableList<IPlatformClient>) : super() {
|
|
||||||
_sources = sources;
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount() = _sources.size
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): DisabledSourceViewHolder {
|
|
||||||
val holder = DisabledSourceViewHolder(viewGroup);
|
|
||||||
holder.onAdd.subscribe {
|
|
||||||
val source = holder.source;
|
|
||||||
if (source != null) {
|
|
||||||
onAdd.emit(source);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
holder.onClick.subscribe {
|
|
||||||
val source = holder.source;
|
|
||||||
if (source != null) {
|
|
||||||
onClick.emit(source);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return holder;
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(viewHolder: DisabledSourceViewHolder, position: Int) {
|
|
||||||
viewHolder.bind(_sources[position])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +1,15 @@
|
|||||||
package com.futo.platformplayer.views.adapters
|
package com.futo.platformplayer.views.adapters
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.ViewGroup
|
|
||||||
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.recyclerview.widget.RecyclerView.ViewHolder
|
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.api.media.IPlatformClient
|
import com.futo.platformplayer.api.media.IPlatformClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
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.states.StatePlatform
|
||||||
|
|
||||||
class DisabledSourceView : LinearLayout {
|
class DisabledSourceView : LinearLayout {
|
||||||
private val _root: LinearLayout;
|
private val _root: LinearLayout;
|
||||||
@@ -38,7 +36,16 @@ class DisabledSourceView : LinearLayout {
|
|||||||
client.icon?.setImageView(_imageSource);
|
client.icon?.setImageView(_imageSource);
|
||||||
|
|
||||||
_textSource.text = client.name;
|
_textSource.text = client.name;
|
||||||
_textSourceSubtitle.text = context.getString(R.string.tap_to_open);
|
|
||||||
|
if (client is JSClient && StatePlatform.instance.hasUpdateAvailable(client.config)) {
|
||||||
|
_textSourceSubtitle.text = context.getString(R.string.update_available_exclamation)
|
||||||
|
_textSourceSubtitle.setTextColor(context.getColor(R.color.light_blue_400))
|
||||||
|
_textSourceSubtitle.typeface = resources.getFont(R.font.inter_regular)
|
||||||
|
} else {
|
||||||
|
_textSourceSubtitle.text = context.getString(R.string.tap_to_open)
|
||||||
|
_textSourceSubtitle.setTextColor(context.getColor(R.color.gray_ac))
|
||||||
|
_textSourceSubtitle.typeface = resources.getFont(R.font.inter_extra_light)
|
||||||
|
}
|
||||||
|
|
||||||
_buttonAdd.setOnClickListener { onAdd.emit(source) }
|
_buttonAdd.setOnClickListener { onAdd.emit(source) }
|
||||||
_root.setOnClickListener { onClick.emit(); };
|
_root.setOnClickListener { onClick.emit(); };
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
package com.futo.platformplayer.views.adapters
|
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.ImageView
|
|
||||||
import android.widget.LinearLayout
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
|
||||||
import com.futo.platformplayer.R
|
|
||||||
import com.futo.platformplayer.api.media.IPlatformClient
|
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
|
||||||
import com.futo.platformplayer.constructs.Event0
|
|
||||||
|
|
||||||
class DisabledSourceViewHolder : ViewHolder {
|
|
||||||
private val _imageSource: ImageView;
|
|
||||||
private val _textSource: TextView;
|
|
||||||
private val _textSourceSubtitle: TextView;
|
|
||||||
|
|
||||||
private val _buttonAdd: LinearLayout;
|
|
||||||
|
|
||||||
var onClick = Event0();
|
|
||||||
var onAdd = Event0();
|
|
||||||
var source: IPlatformClient? = null
|
|
||||||
private set
|
|
||||||
|
|
||||||
constructor(viewGroup: ViewGroup) : super(LayoutInflater.from(viewGroup.context).inflate(R.layout.list_source_disabled, viewGroup, false)) {
|
|
||||||
_imageSource = itemView.findViewById(R.id.image_source);
|
|
||||||
_textSource = itemView.findViewById(R.id.text_source);
|
|
||||||
_textSourceSubtitle = itemView.findViewById(R.id.text_source_subtitle);
|
|
||||||
_buttonAdd = itemView.findViewById(R.id.button_add);
|
|
||||||
|
|
||||||
val root = itemView.findViewById<LinearLayout>(R.id.root);
|
|
||||||
_buttonAdd.setOnClickListener { onAdd.emit() }
|
|
||||||
root.setOnClickListener { onClick.emit(); };
|
|
||||||
}
|
|
||||||
|
|
||||||
fun bind(client: IPlatformClient) {
|
|
||||||
client.icon?.setImageView(_imageSource);
|
|
||||||
|
|
||||||
_textSource.text = client.name;
|
|
||||||
_textSourceSubtitle.text = itemView.context.getString(R.string.tap_to_open);
|
|
||||||
source = client;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+14
-2
@@ -10,7 +10,9 @@ import androidx.recyclerview.widget.ItemTouchHelper
|
|||||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.api.media.IPlatformClient
|
import com.futo.platformplayer.api.media.IPlatformClient
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
|
|
||||||
class EnabledSourceViewHolder : ViewHolder {
|
class EnabledSourceViewHolder : ViewHolder {
|
||||||
private val _imageSource: ImageView;
|
private val _imageSource: ImageView;
|
||||||
@@ -57,8 +59,18 @@ class EnabledSourceViewHolder : ViewHolder {
|
|||||||
fun bind(client: IPlatformClient) {
|
fun bind(client: IPlatformClient) {
|
||||||
client.icon?.setImageView(_imageSource);
|
client.icon?.setImageView(_imageSource);
|
||||||
|
|
||||||
_textSource.text = client.name;
|
_textSource.text = client.name
|
||||||
_textSourceSubtitle.text = itemView.context.getString(R.string.tap_to_open);
|
|
||||||
|
if (client is JSClient && StatePlatform.instance.hasUpdateAvailable(client.config)) {
|
||||||
|
_textSourceSubtitle.text = itemView.context.getString(R.string.update_available_exclamation)
|
||||||
|
_textSourceSubtitle.setTextColor(itemView.context.getColor(R.color.light_blue_400))
|
||||||
|
_textSourceSubtitle.typeface = itemView.resources.getFont(R.font.inter_regular)
|
||||||
|
} else {
|
||||||
|
_textSourceSubtitle.text = itemView.context.getString(R.string.tap_to_open)
|
||||||
|
_textSourceSubtitle.setTextColor(itemView.context.getColor(R.color.gray_ac))
|
||||||
|
_textSourceSubtitle.typeface = itemView.resources.getFont(R.font.inter_extra_light)
|
||||||
|
}
|
||||||
|
|
||||||
source = client
|
source = client
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,7 +7,7 @@ import android.widget.ImageView
|
|||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.api.media.PlatformID
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
@@ -18,8 +18,8 @@ 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.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.views.others.CreatorThumbnail
|
|
||||||
import com.futo.platformplayer.views.FeedStyle
|
import com.futo.platformplayer.views.FeedStyle
|
||||||
|
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||||
import com.futo.platformplayer.views.platform.PlatformIndicator
|
import com.futo.platformplayer.views.platform.PlatformIndicator
|
||||||
|
|
||||||
|
|
||||||
@@ -149,7 +149,8 @@ open class PlaylistView : LinearLayout {
|
|||||||
_neopassAnimator?.cancel();
|
_neopassAnimator?.cancel();
|
||||||
_neopassAnimator = null;
|
_neopassAnimator = null;
|
||||||
|
|
||||||
val harborAvailable = claims != null && !claims.ownedClaims.isNullOrEmpty();
|
val firstClaim = claims?.ownedClaims?.firstOrNull();
|
||||||
|
val harborAvailable = firstClaim != null
|
||||||
if (harborAvailable) {
|
if (harborAvailable) {
|
||||||
_imageNeopassChannel?.visibility = View.VISIBLE
|
_imageNeopassChannel?.visibility = View.VISIBLE
|
||||||
if (animate) {
|
if (animate) {
|
||||||
@@ -160,7 +161,7 @@ open class PlaylistView : LinearLayout {
|
|||||||
_imageNeopassChannel?.visibility = View.GONE
|
_imageNeopassChannel?.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
_creatorThumbnail?.setHarborAvailable(harborAvailable, animate)
|
_creatorThumbnail?.setHarborAvailable(harborAvailable, animate, firstClaim?.system?.toProto())
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user