Compare commits

..

8 Commits

Author SHA1 Message Date
Koen J b025e8a30f Rename to direct and relayed. 2025-04-14 10:38:17 +02:00
Koen J 5b2f8b8617 Finished remote sync. 2025-04-11 16:31:07 +02:00
Koen J 955ba23b0d Fixed various implementation bugs. 2025-04-10 10:55:27 +02:00
Koen J 1ae9f0ea26 Fix bugs. 2025-04-09 15:41:42 +02:00
Koen J 97381739dd Added tests and fixes. 2025-04-09 14:01:36 +02:00
Koen J 79a932b4ca Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay into remote-sync 2025-04-09 09:50:18 +02:00
Koen J 436846ce1f Merge 2025-04-08 16:53:44 +02:00
Koen J ccb1bed4a8 Remote sync implementation 2025-04-08 16:45:02 +02:00
570 changed files with 7165 additions and 31860 deletions
-6
View File
@@ -1,6 +0,0 @@
aar/* filter=lfs diff=lfs merge=lfs -text
app/aar/* filter=lfs diff=lfs merge=lfs -text
app/src/main/jniLibs/arm64-v8a filter=lfs diff=lfs merge=lfs -text
app/src/main/jniLibs/armeabi-v7a filter=lfs diff=lfs merge=lfs -text
app/src/main/jniLibs/x86 filter=lfs diff=lfs merge=lfs -text
app/src/main/jniLibs/x86_64 filter=lfs diff=lfs merge=lfs -text
@@ -1,9 +1,6 @@
name: Bug Report name: Bug Report
description: Let us know about an unexpected error, a crash, or an incorrect behavior. description: Let us know about an unexpected error, a crash, or an incorrect behavior.
labels: ["Bug", "Android"] labels: ["Bug"]
title: "Bug: "
type: bug
projects: ["futo-org/19"]
body: body:
- type: markdown - type: markdown
attributes: attributes:
@@ -21,33 +18,11 @@ body:
* if you've found out a particular series of UI interactions can introduce buggy behavior, please label those steps 1-n with markdown * if you've found out a particular series of UI interactions can introduce buggy behavior, please label those steps 1-n with markdown
- type: textarea - type: textarea
id: reproduction-steps id: what-happened
attributes: attributes:
label: Reproduction steps label: What happened?
description: Please provide us with the steps to reproduce the issue if possible. This step makes a big difference if we are going to be able to fix it so be as precise as possible. description: What did you expect to happen?
placeholder: | placeholder: Tell us what you see!
0. Play a YouTube video
1. Press on Download button
2. Select quality 1440p
3. Grayjay crashes when attempting to download
validations:
required: true
- type: textarea
id: actual-result
attributes:
label: Actual result
description: What happend?
placeholder: Tell us what you saw!
validations:
required: true
- type: textarea
id: expected-result
attributes:
label: Expected result
description: What was suppose to happen?
placeholder: Tell us what you expected to happen!
validations: validations:
required: true required: true
@@ -56,7 +31,7 @@ body:
attributes: attributes:
label: Grayjay Version label: Grayjay Version
description: In the application, select More > Settings, scroll to the bottom and locate the value next to "Version Name". description: In the application, select More > Settings, scroll to the bottom and locate the value next to "Version Name".
placeholder: "311" placeholder: "242"
validations: validations:
required: true required: true
@@ -67,23 +42,19 @@ body:
multiple: true multiple: true
options: options:
- "All" - "All"
- "Apple Podcasts" - "Youtube"
- "Odysee"
- "Rumble"
- "Kick"
- "Twitch"
- "PeerTube"
- "Patreon"
- "Nebula"
- "BiliBili (CN)" - "BiliBili (CN)"
- "Bitchute" - "Bitchute"
- "Crunchyroll"
- "CuriosityStream"
- "Dailymotion"
- "Kick"
- "Nebula"
- "Odysee"
- "Patreon"
- "PeerTube"
- "Rumble"
- "SoundCloud" - "SoundCloud"
- "Spotify" - "Dailymotion"
- "TedTalks" - "Apple Podcasts"
- "Twitch"
- "YouTube"
- "Other" - "Other"
validations: validations:
required: true required: true
@@ -95,30 +66,6 @@ body:
description: In the application, select Sources > [the broken plugin], write down the value under "Version". description: In the application, select Sources > [the broken plugin], write down the value under "Version".
placeholder: "12" placeholder: "12"
- type: input
id: android-version
attributes:
label: Which android version are you using?
placeholder: "Android 15"
validations:
required: true
- type: input
id: phone-model
attributes:
label: Which device are you using?
placeholder: "Google Pixel 9"
validations:
required: true
- type: input
id: os-version
attributes:
label: Which operating system are you using?
placeholder: "GrapheneOS/CalyxOS/Tizen/HyperOS 2/..."
validations:
required: true
- type: checkboxes - type: checkboxes
id: login id: login
attributes: attributes:
@@ -139,28 +86,9 @@ body:
validations: validations:
required: true required: true
- type: textarea
id: grayjay-references
attributes:
label: References
description: |
Are there any other GitHub issues, whether open or closed, that are related to the problem you've described above? If so, please create a list below that mentions each of them. For example:
```
- #10
```
placeholder:
value:
validations:
required: false
- type: textarea - type: textarea
id: logs id: logs
attributes: attributes:
label: Relevant log output label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
render: shell render: shell
- type: markdown
attributes:
value: |
**Note:** If the submit button is disabled and you have filled out all required fields, please check that you did not forget a **Title** for the issue.
@@ -1,16 +1,13 @@
name: Documentation Issue name: Documentation Issue
description: Report an issue or suggest a change in the documentation. description: Report an issue or suggest a change in the documentation.
labels: ["Documentation"] labels: ["Documentation"]
title: "Documentation: "
type: task
projects: ["futo-org/19"]
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
# Thank you for opening a documentation change request. # Thank you for opening a documentation change request.
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay android application. Use the `Documentation` issue type to report problems with the documentation in our code repositories, inside the application, or on [https://grayjay.app](https://grayjay.app) The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay Android Application. Use the `Documentation` issue type to report problems with the documentation in our code repositories, inside the application, or on [https://grayjay.app](https://grayjay.app)
Technical writers monitor this issue type, so report Grayjay bugs or feature requests with the `Bug report` or `Feature Request` issue types instead to get engineering attention. Technical writers monitor this issue type, so report Grayjay bugs or feature requests with the `Bug report` or `Feature Request` issue types instead to get engineering attention.
For general usage questions, please see: [The Official FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay) For general usage questions, please see: [The Official FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)
@@ -1,16 +1,13 @@
name: Feature Request name: Feature Request
description: Suggest a new feature or other enhancement. description: Suggest a new feature or other enhancement.
labels: ["Enhancement", "Android"] labels: ["Enhancement"]
title: "Feature request: "
type: feature
projects: ["futo-org/19"]
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
# Thank you for opening a feature request. # Thank you for opening a feature request.
The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues and feature requests relating to the Grayjay android application The [grayjay-android](https://github.com/futo-org/grayjay-android) issue tracker is reserved for issues relating to the Grayjay Android Application
For discussion related to enhancements, please see: [The FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay) For discussion related to enhancements, please see: [The FUTO Grayjay Zulip Channel](https://chat.futo.org/#narrow/stream/46-Grayjay)
+6 -18
View File
@@ -64,6 +64,12 @@
[submodule "app/src/stable/assets/sources/bilibili"] [submodule "app/src/stable/assets/sources/bilibili"]
path = app/src/stable/assets/sources/bilibili path = app/src/stable/assets/sources/bilibili
url = ../plugins/bilibili.git url = ../plugins/bilibili.git
[submodule "app/src/stable/assets/sources/spotify"]
path = app/src/stable/assets/sources/spotify
url = ../plugins/spotify.git
[submodule "app/src/unstable/assets/sources/spotify"]
path = app/src/unstable/assets/sources/spotify
url = ../plugins/spotify.git
[submodule "app/src/stable/assets/sources/bitchute"] [submodule "app/src/stable/assets/sources/bitchute"]
path = app/src/stable/assets/sources/bitchute path = app/src/stable/assets/sources/bitchute
url = ../plugins/bitchute.git url = ../plugins/bitchute.git
@@ -88,21 +94,3 @@
[submodule "app/src/unstable/assets/sources/tedtalks"] [submodule "app/src/unstable/assets/sources/tedtalks"]
path = app/src/unstable/assets/sources/tedtalks path = app/src/unstable/assets/sources/tedtalks
url = ../plugins/tedtalks.git url = ../plugins/tedtalks.git
[submodule "app/src/stable/assets/sources/curiositystream"]
path = app/src/stable/assets/sources/curiositystream
url = ../plugins/curiositystream.git
[submodule "app/src/unstable/assets/sources/curiositystream"]
path = app/src/unstable/assets/sources/curiositystream
url = ../plugins/curiositystream.git
[submodule "app/src/unstable/assets/sources/crunchyroll"]
path = app/src/unstable/assets/sources/crunchyroll
url = ../plugins/crunchyroll.git
[submodule "app/src/stable/assets/sources/crunchyroll"]
path = app/src/stable/assets/sources/crunchyroll
url = ../plugins/crunchyroll.git
[submodule "app/src/stable/assets/sources/mixcloud"]
path = app/src/stable/assets/sources/mixcloud
url = ../plugins/mixcloud.git
[submodule "app/src/unstable/assets/sources/mixcloud"]
path = app/src/unstable/assets/sources/mixcloud
url = ../plugins/mixcloud.git
-3
View File
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:22c06ca0d1a5808b2fc0a12227d5915b3126bc0b9b1305cf6bab855f2ec6fcbb
size 36133152
+46 -54
View File
@@ -1,8 +1,8 @@
plugins { plugins {
id 'com.android.application' id 'com.android.application'
id 'org.jetbrains.kotlin.android' id 'org.jetbrains.kotlin.android'
id 'org.jetbrains.kotlin.plugin.serialization' version '2.2.21' id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.21'
id 'org.ajoberstar.grgit' version '5.3.3' id 'org.ajoberstar.grgit' version '5.2.2'
id 'com.google.protobuf' id 'com.google.protobuf'
id 'kotlin-parcelize' id 'kotlin-parcelize'
id 'com.google.devtools.ksp' id 'com.google.devtools.ksp'
@@ -39,7 +39,7 @@ protobuf {
android { android {
namespace 'com.futo.platformplayer' namespace 'com.futo.platformplayer'
compileSdk 36 compileSdk 34
flavorDimensions "buildType" flavorDimensions "buildType"
productFlavors { productFlavors {
stable { stable {
@@ -97,7 +97,7 @@ android {
defaultConfig { defaultConfig {
minSdk 28 minSdk 28
targetSdk 36 targetSdk 34
versionCode gitVersionCode versionCode gitVersionCode
versionName gitVersionName versionName gitVersionName
@@ -146,7 +146,6 @@ android {
} }
sourceSets { sourceSets {
main { main {
jniLibs.srcDirs = ['src/main/jniLibs']
assets { assets {
srcDirs 'src/main/assets', 'src/tests/assets', 'src/test/assets' srcDirs 'src/main/assets', 'src/tests/assets', 'src/test/assets'
} }
@@ -155,85 +154,78 @@ android {
} }
dependencies { dependencies {
//implementation 'com.google.dagger:dagger:2.48' implementation 'com.google.dagger:dagger:2.48'
implementation 'androidx.test:monitor:1.8.0' implementation 'androidx.test:monitor:1.7.2'
implementation 'com.google.android.material:material:1.13.0' annotationProcessor 'com.google.dagger:dagger-compiler:2.48'
//annotationProcessor 'com.google.dagger:dagger-compiler:2.48'
//Core //Core
implementation 'androidx.core:core-ktx:1.17.0' implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.appcompat:appcompat:1.7.1' implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.constraintlayout:constraintlayout:2.2.1' implementation 'com.google.android.material:material:1.11.0'
implementation 'androidx.documentfile:documentfile:1.1.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
//Images //Images
annotationProcessor 'com.github.bumptech.glide:compiler:5.0.5' annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'
implementation 'com.github.bumptech.glide:glide:5.0.5' implementation 'com.github.bumptech.glide:glide:4.16.0'
//Async //Async
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
//HTTP //HTTP
implementation "com.squareup.okhttp3:okhttp:5.3.0" implementation "com.squareup.okhttp3:okhttp:4.11.0"
//JSON //JSON
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0" //Used for structured json implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2" //Used for structured json
implementation 'com.google.code.gson:gson:2.13.2' //Used for complex/anonymous cases like during development conversions (eg. V8RemoteObject) implementation 'com.google.code.gson:gson:2.10.1' //Used for complex/anonymous cases like during development conversions (eg. V8RemoteObject)
//JS //JS
implementation 'com.caoccao.javet:javet-v8-android:4.1.5' implementation("com.caoccao.javet:javet-android:3.0.2")
//Exoplayer //Exoplayer
implementation 'androidx.media3:media3-exoplayer:1.8.0' implementation 'androidx.media3:media3-exoplayer:1.2.1'
implementation 'androidx.media3:media3-exoplayer-dash:1.8.0' implementation 'androidx.media3:media3-exoplayer-dash:1.2.1'
implementation 'androidx.media3:media3-ui:1.8.0' implementation 'androidx.media3:media3-ui:1.2.1'
implementation 'androidx.media3:media3-exoplayer-hls:1.8.0' implementation 'androidx.media3:media3-exoplayer-hls:1.2.1'
implementation 'androidx.media3:media3-exoplayer-rtsp:1.8.0' implementation 'androidx.media3:media3-exoplayer-rtsp:1.2.1'
implementation 'androidx.media3:media3-exoplayer-smoothstreaming:1.8.0' implementation 'androidx.media3:media3-exoplayer-smoothstreaming:1.2.1'
implementation 'androidx.media3:media3-transformer:1.8.0' implementation 'androidx.media3:media3-transformer:1.2.1'
implementation 'androidx.navigation:navigation-fragment-ktx:2.9.6' implementation 'androidx.navigation:navigation-fragment-ktx:2.7.6'
implementation 'androidx.navigation:navigation-ui-ktx:2.9.6' implementation 'androidx.navigation:navigation-ui-ktx:2.7.6'
implementation 'androidx.media:media:1.7.1' implementation 'androidx.media:media:1.7.0'
//Other //Other
implementation 'org.jsoup:jsoup:1.21.2' implementation 'org.jsoup:jsoup:1.15.3'
implementation 'com.google.android.flexbox:flexbox:3.0.0' implementation 'com.google.android.flexbox:flexbox:3.0.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation fileTree(dir: 'aar', include: ['*.aar']) implementation 'com.arthenica:ffmpeg-kit-full:6.0-2.LTS'
implementation 'com.arthenica:smart-exception-java:0.2.1' implementation 'org.jetbrains.kotlin:kotlin-reflect:1.9.0'
implementation 'org.jetbrains.kotlin:kotlin-reflect:2.2.0'
implementation 'com.github.dhaval2404:imagepicker:2.1' implementation 'com.github.dhaval2404:imagepicker:2.1'
implementation 'com.google.zxing:core:3.5.3' implementation 'com.google.zxing:core:3.4.1'
implementation 'com.journeyapps:zxing-android-embedded:4.3.0' implementation 'com.journeyapps:zxing-android-embedded:4.3.0'
implementation 'com.caverock:androidsvg-aar:1.4' implementation 'com.caverock:androidsvg-aar:1.4'
//Protobuf //Protobuf
implementation 'com.google.protobuf:protobuf-javalite:4.33.0' implementation 'com.google.protobuf:protobuf-javalite:3.25.1'
implementation 'com.polycentric.core:app:1.0' implementation 'com.polycentric.core:app:1.0'
implementation 'com.futo.futopay:app:1.0' implementation 'com.futo.futopay:app:1.0'
implementation 'androidx.work:work-runtime-ktx:2.11.0' implementation 'androidx.work:work-runtime-ktx:2.9.0'
implementation 'androidx.concurrent:concurrent-futures-ktx:1.3.0' implementation 'androidx.concurrent:concurrent-futures-ktx:1.1.0'
//Database //Database
implementation("androidx.room:room-runtime:2.8.3") implementation("androidx.room:room-runtime:2.6.1")
ksp("androidx.room:room-compiler:2.8.3") annotationProcessor("androidx.room:room-compiler:2.6.1")
implementation("androidx.room:room-ktx:2.8.3") ksp("androidx.room:room-compiler:2.6.1")
implementation("androidx.room:room-ktx:2.6.1")
//Payment //Payment
implementation 'com.stripe:stripe-android:22.0.0' implementation 'com.stripe:stripe-android:20.35.1'
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2' testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
testImplementation "org.jetbrains.kotlin:kotlin-test:2.0.21" testImplementation "org.jetbrains.kotlin:kotlin-test:1.8.22"
testImplementation "org.xmlunit:xmlunit-core:2.11.0" testImplementation "org.xmlunit:xmlunit-core:2.9.1"
testImplementation "org.mockito:mockito-core:5.20.0" testImplementation "org.mockito:mockito-core:5.4.0"
androidTestImplementation 'androidx.test.ext:junit:1.3.0' androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
//Rust casting SDK
implementation('org.futo.gitlab.videostreaming.fcast-sdk-jitpack:sender-sdk-minimal:0.3.1') {
// Polycentricandroid includes this
exclude group: 'net.java.dev.jna'
}
} }
@@ -1,38 +0,0 @@
package com.futo.platformplayer
import android.graphics.Color
import org.junit.Assert.assertEquals
import org.junit.Test
import toAndroidColor
class CSSColorTests {
@Test
fun test1() {
val androidHex = "#80336699"
val androidColorInt = Color.parseColor(androidHex)
val cssHex = "#33669980"
val cssColor = CSSColor.parseColor(cssHex)
assertEquals(
"CSSColor($cssHex).toAndroidColor() should equal Color.parseColor($androidHex)",
androidColorInt,
cssColor.toAndroidColor(),
)
}
@Test
fun test2() {
val androidHex = "#123ABC"
val androidColorInt = Color.parseColor(androidHex)
val cssHex = "#123ABCFF"
val cssColor = CSSColor.parseColor(cssHex)
assertEquals(
"CSSColor($cssHex).toAndroidColor() should equal Color.parseColor($androidHex)",
androidColorInt,
cssColor.toAndroidColor()
)
}
}
@@ -3,21 +3,19 @@ package com.futo.platformplayer
import com.futo.platformplayer.noise.protocol.Noise import com.futo.platformplayer.noise.protocol.Noise
import com.futo.platformplayer.sync.internal.* import com.futo.platformplayer.sync.internal.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.selects.select
import org.junit.Assert.* import org.junit.Assert.*
import org.junit.Test import org.junit.Test
import java.net.Socket import java.net.Socket
import java.nio.ByteBuffer import java.nio.ByteBuffer
import kotlin.random.Random import kotlin.random.Random
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
/*
class SyncServerTests { class SyncServerTests {
//private val relayHost = "relay.grayjay.app" //private val relayHost = "relay.grayjay.app"
//private val relayKey = "xGbHRzDOvE6plRbQaFgSen82eijF+gxS0yeUaeEErkw=" //private val relayKey = "xGbHRzDOvE6plRbQaFgSen82eijF+gxS0yeUaeEErkw="
private val relayKey = "XlUaSpIlRaCg0TGzZ7JYmPupgUHDqTZXUUBco2K7ejw=" private val relayKey = "XlUaSpIlRaCg0TGzZ7JYmPupgUHDqTZXUUBco2K7ejw="
private val relayHost = "192.168.1.138" private val relayHost = "192.168.1.175"
private val relayPort = 9000 private val relayPort = 9000
/** Creates a client connected to the live relay server. */ /** Creates a client connected to the live relay server. */
@@ -25,8 +23,7 @@ class SyncServerTests {
onHandshakeComplete: ((SyncSocketSession) -> Unit)? = null, onHandshakeComplete: ((SyncSocketSession) -> Unit)? = null,
onData: ((SyncSocketSession, UByte, UByte, ByteBuffer) -> Unit)? = null, onData: ((SyncSocketSession, UByte, UByte, ByteBuffer) -> Unit)? = null,
onNewChannel: ((SyncSocketSession, ChannelRelayed) -> Unit)? = null, onNewChannel: ((SyncSocketSession, ChannelRelayed) -> Unit)? = null,
isHandshakeAllowed: ((LinkType, SyncSocketSession, String, String?, UInt) -> Boolean)? = null, isHandshakeAllowed: ((SyncSocketSession, String, String?) -> Boolean)? = null
onException: ((Throwable) -> Unit)? = null
): SyncSocketSession = withContext(Dispatchers.IO) { ): SyncSocketSession = withContext(Dispatchers.IO) {
val p = Noise.createDH("25519") val p = Noise.createDH("25519")
p.generateKeyPair() p.generateKeyPair()
@@ -46,14 +43,10 @@ class SyncServerTests {
}, },
onData = onData ?: { _, _, _, _ -> }, onData = onData ?: { _, _, _, _ -> },
onNewChannel = onNewChannel ?: { _, _ -> }, onNewChannel = onNewChannel ?: { _, _ -> },
isHandshakeAllowed = isHandshakeAllowed ?: { _, _, _, _, _ -> true } isHandshakeAllowed = isHandshakeAllowed ?: { _, _, _ -> true }
) )
socketSession.authorizable = AlwaysAuthorized() socketSession.authorizable = AlwaysAuthorized()
try { socketSession.startAsInitiator(relayKey)
socketSession.startAsInitiator(relayKey)
} catch (e: Throwable) {
onException?.invoke(e)
}
withTimeout(5000.milliseconds) { tcs.await() } withTimeout(5000.milliseconds) { tcs.await() }
return@withContext socketSession return@withContext socketSession
} }
@@ -266,73 +259,8 @@ class SyncServerTests {
clientA.stop() clientA.stop()
clientB.stop() clientB.stop()
} }
@Test
fun relayedTransport_WithValidAppId_Success() = runBlocking {
// Arrange: Set up clients
val allowedAppId = 1234u
val tcsB = CompletableDeferred<ChannelRelayed>()
// Client B requires appId 1234
val clientB = createClient(
onNewChannel = { _, c -> tcsB.complete(c) },
isHandshakeAllowed = { linkType, _, _, _, appId -> linkType == LinkType.Relayed && appId == allowedAppId }
)
val clientA = createClient()
// Act: Start relayed channel with valid appId
val channelTask = async { clientA.startRelayedChannel(clientB.localPublicKey, appId = allowedAppId) }
val channelB = withTimeout(5.seconds) { tcsB.await() }
withTimeout(5.seconds) { channelTask.await() }
// Assert: Channel is established
assertNotNull("Channel should be created on target with valid appId", channelB)
// Clean up
clientA.stop()
clientB.stop()
}
@Test
fun relayedTransport_WithInvalidAppId_Fails() = runBlocking {
// Arrange: Set up clients
val allowedAppId = 1234u
val invalidAppId = 5678u
val tcsB = CompletableDeferred<ChannelRelayed>()
// Client B requires appId 1234
val clientB = createClient(
onNewChannel = { _, c -> tcsB.complete(c) },
isHandshakeAllowed = { linkType, _, _, _, appId -> linkType == LinkType.Relayed && appId == allowedAppId },
onException = { }
)
val clientA = createClient()
// Act & Assert: Attempt with invalid appId should fail
try {
withTimeout(5.seconds) {
clientA.startRelayedChannel(clientB.localPublicKey, appId = invalidAppId)
}
fail("Starting relayed channel with invalid appId should fail")
} catch (e: Throwable) {
// Expected: The channel creation should time out or fail
}
// Ensure no channel was created on client B
val completedTask = select {
tcsB.onAwait { "channel" }
async { delay(1.seconds); "timeout" }.onAwait { "timeout" }
}
assertEquals("No channel should be created with invalid appId", "timeout", completedTask)
// Clean up
clientA.stop()
clientB.stop()
}
} }
class AlwaysAuthorized : IAuthorizable { class AlwaysAuthorized : IAuthorizable {
override val isAuthorized: Boolean get() = true override val isAuthorized: Boolean get() = true
}*/ }
@@ -1,512 +0,0 @@
package com.futo.platformplayer
import com.futo.platformplayer.noise.protocol.DHState
import com.futo.platformplayer.noise.protocol.Noise
import com.futo.platformplayer.sync.internal.*
import kotlinx.coroutines.*
import org.junit.Assert.*
import org.junit.Test
import java.io.PipedInputStream
import java.io.PipedOutputStream
import java.nio.ByteBuffer
import kotlin.random.Random
import java.io.InputStream
import java.io.OutputStream
import kotlin.time.Duration.Companion.seconds
/*
data class PipeStreams(
val initiatorInput: LittleEndianDataInputStream,
val initiatorOutput: LittleEndianDataOutputStream,
val responderInput: LittleEndianDataInputStream,
val responderOutput: LittleEndianDataOutputStream
)
typealias OnHandshakeComplete = (SyncSocketSession) -> Unit
typealias IsHandshakeAllowed = (LinkType, SyncSocketSession, String, String?, UInt) -> Boolean
typealias OnClose = (SyncSocketSession) -> Unit
typealias OnData = (SyncSocketSession, UByte, UByte, ByteBuffer) -> Unit
class SyncSocketTests {
private fun createPipeStreams(): PipeStreams {
val initiatorOutput = PipedOutputStream()
val responderOutput = PipedOutputStream()
val responderInput = PipedInputStream(initiatorOutput)
val initiatorInput = PipedInputStream(responderOutput)
return PipeStreams(
LittleEndianDataInputStream(initiatorInput), LittleEndianDataOutputStream(initiatorOutput),
LittleEndianDataInputStream(responderInput), LittleEndianDataOutputStream(responderOutput)
)
}
fun generateKeyPair(): DHState {
val p = Noise.createDH("25519")
p.generateKeyPair()
return p
}
private fun createSessions(
initiatorInput: LittleEndianDataInputStream,
initiatorOutput: LittleEndianDataOutputStream,
responderInput: LittleEndianDataInputStream,
responderOutput: LittleEndianDataOutputStream,
initiatorKeyPair: DHState,
responderKeyPair: DHState,
onInitiatorHandshakeComplete: OnHandshakeComplete,
onResponderHandshakeComplete: OnHandshakeComplete,
onInitiatorClose: OnClose? = null,
onResponderClose: OnClose? = null,
onClose: OnClose? = null,
isHandshakeAllowed: IsHandshakeAllowed? = null,
onDataA: OnData? = null,
onDataB: OnData? = null
): Pair<SyncSocketSession, SyncSocketSession> {
val initiatorSession = SyncSocketSession(
"", initiatorKeyPair, initiatorInput, initiatorOutput,
onClose = {
onClose?.invoke(it)
onInitiatorClose?.invoke(it)
},
onHandshakeComplete = onInitiatorHandshakeComplete,
onData = onDataA,
isHandshakeAllowed = isHandshakeAllowed
)
val responderSession = SyncSocketSession(
"", responderKeyPair, responderInput, responderOutput,
onClose = {
onClose?.invoke(it)
onResponderClose?.invoke(it)
},
onHandshakeComplete = onResponderHandshakeComplete,
onData = onDataB,
isHandshakeAllowed = isHandshakeAllowed
)
return Pair(initiatorSession, responderSession)
}
@Test
fun handshake_WithValidPairingCode_Succeeds(): Unit = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val validPairingCode = "secret"
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
isHandshakeAllowed = { _, _, _, pairingCode, _ -> pairingCode == validPairingCode }
)
initiatorSession.startAsInitiator(responderSession.localPublicKey, pairingCode = validPairingCode)
responderSession.startAsResponder()
withTimeout(5.seconds) {
handshakeInitiatorCompleted.await()
handshakeResponderCompleted.await()
}
}
@Test
fun handshake_WithInvalidPairingCode_Fails() = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val validPairingCode = "secret"
val invalidPairingCode = "wrong"
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val initiatorClosed = CompletableDeferred<Boolean>()
val responderClosed = CompletableDeferred<Boolean>()
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
onInitiatorClose = {
initiatorClosed.complete(true)
},
onResponderClose = {
responderClosed.complete(true)
},
isHandshakeAllowed = { _, _, _, pairingCode, _ -> pairingCode == validPairingCode }
)
initiatorSession.startAsInitiator(responderSession.localPublicKey, pairingCode = invalidPairingCode)
responderSession.startAsResponder()
withTimeout(100.seconds) {
initiatorClosed.await()
responderClosed.await()
}
assertFalse(handshakeInitiatorCompleted.isCompleted)
assertFalse(handshakeResponderCompleted.isCompleted)
}
@Test
fun handshake_WithoutPairingCodeWhenRequired_Fails() = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val validPairingCode = "secret"
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val initiatorClosed = CompletableDeferred<Boolean>()
val responderClosed = CompletableDeferred<Boolean>()
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
onInitiatorClose = {
initiatorClosed.complete(true)
},
onResponderClose = {
responderClosed.complete(true)
},
isHandshakeAllowed = { _, _, _, pairingCode, _ -> pairingCode == validPairingCode }
)
initiatorSession.startAsInitiator(responderSession.localPublicKey) // No pairing code
responderSession.startAsResponder()
withTimeout(5.seconds) {
initiatorClosed.await()
responderClosed.await()
}
assertFalse(handshakeInitiatorCompleted.isCompleted)
assertFalse(handshakeResponderCompleted.isCompleted)
}
@Test
fun handshake_WithPairingCodeWhenNotRequired_Succeeds(): Unit = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val pairingCode = "unnecessary"
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
isHandshakeAllowed = { _, _, _, _, _ -> true } // Always allow
)
initiatorSession.startAsInitiator(responderSession.localPublicKey, pairingCode = pairingCode)
responderSession.startAsResponder()
withTimeout(10.seconds) {
handshakeInitiatorCompleted.await()
handshakeResponderCompleted.await()
}
}
@Test
fun sendAndReceive_SmallDataPacket_Succeeds() = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val tcsDataReceived = CompletableDeferred<ByteArray>()
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
onDataB = { _, opcode, subOpcode, data ->
if (opcode == Opcode.DATA.value && subOpcode == 0u.toUByte()) {
val b = ByteArray(data.remaining())
data.get(b)
tcsDataReceived.complete(b)
}
}
)
initiatorSession.startAsInitiator(responderSession.localPublicKey)
responderSession.startAsResponder()
withTimeout(10.seconds) {
handshakeInitiatorCompleted.await()
handshakeResponderCompleted.await()
}
// Ensure both sessions are authorized
initiatorSession.authorizable = Authorized()
responderSession.authorizable = Authorized()
val smallData = byteArrayOf(1, 2, 3)
initiatorSession.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(smallData))
val receivedData = withTimeout(10.seconds) { tcsDataReceived.await() }
assertArrayEquals(smallData, receivedData)
}
@Test
fun sendAndReceive_ExactlyMaximumPacketSize_Succeeds() = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val tcsDataReceived = CompletableDeferred<ByteArray>()
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
onDataB = { _, opcode, subOpcode, data ->
if (opcode == Opcode.DATA.value && subOpcode == 0u.toUByte()) {
val b = ByteArray(data.remaining())
data.get(b)
tcsDataReceived.complete(b)
}
}
)
initiatorSession.startAsInitiator(responderSession.localPublicKey)
responderSession.startAsResponder()
withTimeout(10.seconds) {
handshakeInitiatorCompleted.await()
handshakeResponderCompleted.await()
}
// Ensure both sessions are authorized
initiatorSession.authorizable = Authorized()
responderSession.authorizable = Authorized()
val maxData = ByteArray(SyncSocketSession.MAXIMUM_PACKET_SIZE - SyncSocketSession.HEADER_SIZE).apply { Random.nextBytes(this) }
initiatorSession.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(maxData))
val receivedData = withTimeout(10.seconds) { tcsDataReceived.await() }
assertArrayEquals(maxData, receivedData)
}
@Test
fun stream_LargeData_Succeeds() = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val tcsDataReceived = CompletableDeferred<ByteArray>()
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
onDataB = { _, opcode, subOpcode, data ->
if (opcode == Opcode.DATA.value && subOpcode == 0u.toUByte()) {
val b = ByteArray(data.remaining())
data.get(b)
tcsDataReceived.complete(b)
}
}
)
initiatorSession.startAsInitiator(responderSession.localPublicKey)
responderSession.startAsResponder()
withTimeout(10.seconds) {
handshakeInitiatorCompleted.await()
handshakeResponderCompleted.await()
}
// Ensure both sessions are authorized
initiatorSession.authorizable = Authorized()
responderSession.authorizable = Authorized()
val largeData = ByteArray(2 * (SyncSocketSession.MAXIMUM_PACKET_SIZE - SyncSocketSession.HEADER_SIZE)).apply { Random.nextBytes(this) }
initiatorSession.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(largeData))
val receivedData = withTimeout(10.seconds) { tcsDataReceived.await() }
assertArrayEquals(largeData, receivedData)
}
@Test
fun authorizedSession_CanSendData() = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val tcsDataReceived = CompletableDeferred<ByteArray>()
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
onDataB = { _, opcode, subOpcode, data ->
if (opcode == Opcode.DATA.value && subOpcode == 0u.toUByte()) {
val b = ByteArray(data.remaining())
data.get(b)
tcsDataReceived.complete(b)
}
}
)
initiatorSession.startAsInitiator(responderSession.localPublicKey)
responderSession.startAsResponder()
withTimeout(10.seconds) {
handshakeInitiatorCompleted.await()
handshakeResponderCompleted.await()
}
// Authorize both sessions
initiatorSession.authorizable = Authorized()
responderSession.authorizable = Authorized()
val data = byteArrayOf(1, 2, 3)
initiatorSession.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(data))
val receivedData = withTimeout(10.seconds) { tcsDataReceived.await() }
assertArrayEquals(data, receivedData)
}
@Test
fun unauthorizedSession_CannotSendData() = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val tcsDataReceived = CompletableDeferred<ByteArray>()
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
onDataB = { _, _, _, _ -> }
)
initiatorSession.startAsInitiator(responderSession.localPublicKey)
responderSession.startAsResponder()
withTimeout(10.seconds) {
handshakeInitiatorCompleted.await()
handshakeResponderCompleted.await()
}
// Authorize initiator but not responder
initiatorSession.authorizable = Authorized()
responderSession.authorizable = Unauthorized()
val data = byteArrayOf(1, 2, 3)
initiatorSession.send(Opcode.DATA.value, 0u, ByteBuffer.wrap(data))
delay(1.seconds)
assertFalse(tcsDataReceived.isCompleted)
}
@Test
fun directHandshake_WithValidAppId_Succeeds() = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val allowedAppId = 1234u
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val responderIsHandshakeAllowed = { linkType: LinkType, _: SyncSocketSession, _: String, _: String?, appId: UInt ->
linkType == LinkType.Direct && appId == allowedAppId
}
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
isHandshakeAllowed = responderIsHandshakeAllowed
)
initiatorSession.startAsInitiator(responderSession.localPublicKey, appId = allowedAppId)
responderSession.startAsResponder()
withTimeout(5.seconds) {
handshakeInitiatorCompleted.await()
handshakeResponderCompleted.await()
}
assertNotNull(initiatorSession.remotePublicKey)
assertNotNull(responderSession.remotePublicKey)
}
@Test
fun directHandshake_WithInvalidAppId_Fails() = runBlocking {
val (initiatorInput, initiatorOutput, responderInput, responderOutput) = createPipeStreams()
val initiatorKeyPair = generateKeyPair()
val responderKeyPair = generateKeyPair()
val allowedAppId = 1234u
val invalidAppId = 5678u
val handshakeInitiatorCompleted = CompletableDeferred<Boolean>()
val handshakeResponderCompleted = CompletableDeferred<Boolean>()
val initiatorClosed = CompletableDeferred<Boolean>()
val responderClosed = CompletableDeferred<Boolean>()
val responderIsHandshakeAllowed = { linkType: LinkType, _: SyncSocketSession, _: String, _: String?, appId: UInt ->
linkType == LinkType.Direct && appId == allowedAppId
}
val (initiatorSession, responderSession) = createSessions(
initiatorInput, initiatorOutput, responderInput, responderOutput,
initiatorKeyPair, responderKeyPair,
{ handshakeInitiatorCompleted.complete(true) },
{ handshakeResponderCompleted.complete(true) },
onInitiatorClose = {
initiatorClosed.complete(true)
},
onResponderClose = {
responderClosed.complete(true)
},
isHandshakeAllowed = responderIsHandshakeAllowed
)
initiatorSession.startAsInitiator(responderSession.localPublicKey, appId = invalidAppId)
responderSession.startAsResponder()
withTimeout(5.seconds) {
initiatorClosed.await()
responderClosed.await()
}
assertFalse(handshakeInitiatorCompleted.isCompleted)
assertFalse(handshakeResponderCompleted.isCompleted)
}
}
class Authorized : IAuthorizable {
override val isAuthorized: Boolean = true
}
class Unauthorized : IAuthorizable {
override val isAuthorized: Boolean = false
}*/
+21 -51
View File
@@ -16,9 +16,6 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
<uses-permission android:name="android.permission.WRITE_SETTINGS" tools:ignore="ProtectedPermissions"/> <uses-permission android:name="android.permission.WRITE_SETTINGS" tools:ignore="ProtectedPermissions"/>
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<application <application
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
@@ -29,8 +26,6 @@
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.FutoVideo" android:theme="@style/Theme.FutoVideo"
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true"
tools:replace="android:enableOnBackInvokedCallback"
android:enableOnBackInvokedCallback="false"
tools:targetApi="31" tools:targetApi="31"
android:largeHeap="true"> android:largeHeap="true">
<provider <provider
@@ -60,10 +55,9 @@
<activity <activity
android:name=".activities.MainActivity" android:name=".activities.MainActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout|uiMode" android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
android:exported="true" android:exported="true"
android:theme="@style/Theme.FutoVideo.NoActionBar" android:theme="@style/Theme.FutoVideo.NoActionBar"
android:windowSoftInputMode="adjustPan"
android:launchMode="singleInstance" android:launchMode="singleInstance"
android:resizeableActivity="true" android:resizeableActivity="true"
android:supportsPictureInPicture="true"> android:supportsPictureInPicture="true">
@@ -159,30 +153,30 @@
</activity> </activity>
<activity <activity
android:name=".activities.TestActivity" android:name=".activities.TestActivity"
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity <activity
android:name=".activities.SettingsActivity" android:name=".activities.SettingsActivity"
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity <activity
android:name=".activities.DeveloperActivity" android:name=".activities.DeveloperActivity"
android:screenOrientation="sensorPortrait" android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity <activity
android:name=".activities.ExceptionActivity" android:name=".activities.ExceptionActivity"
android:screenOrientation="sensorPortrait" android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity <activity
android:name=".activities.CaptchaActivity" android:name=".activities.CaptchaActivity"
android:screenOrientation="sensorPortrait" android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity <activity
android:name=".activities.LoginActivity" android:name=".activities.LoginActivity"
android:screenOrientation="sensorPortrait" android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity <activity
android:name=".activities.AddSourceActivity" android:name=".activities.AddSourceActivity"
android:exported="true" android:exported="true"
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem"> android:theme="@style/Theme.FutoVideo.NoActionBar">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
@@ -195,78 +189,54 @@
<activity <activity
android:name=".activities.AddSourceOptionsActivity" android:name=".activities.AddSourceOptionsActivity"
android:screenOrientation="sensorPortrait" android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity <activity
android:name=".activities.PolycentricHomeActivity" android:name=".activities.PolycentricHomeActivity"
android:screenOrientation="sensorPortrait" android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity <activity
android:name=".activities.PolycentricBackupActivity" android:name=".activities.PolycentricBackupActivity"
android:screenOrientation="sensorPortrait" android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity <activity
android:name=".activities.PolycentricCreateProfileActivity" android:name=".activities.PolycentricCreateProfileActivity"
android:screenOrientation="sensorPortrait" android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity <activity
android:name=".activities.PolycentricProfileActivity" android:name=".activities.PolycentricProfileActivity"
android:screenOrientation="sensorPortrait" android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity <activity
android:name=".activities.PolycentricWhyActivity" android:name=".activities.PolycentricWhyActivity"
android:screenOrientation="sensorPortrait" android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity <activity
android:name=".activities.PolycentricImportProfileActivity" android:name=".activities.PolycentricImportProfileActivity"
android:screenOrientation="sensorPortrait" android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity <activity
android:name=".activities.ManageTabsActivity" android:name=".activities.ManageTabsActivity"
android:screenOrientation="sensorPortrait" android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity <activity
android:name=".activities.QRCaptureActivity" android:name=".activities.QRCaptureActivity"
android:screenOrientation="sensorPortrait" android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity <activity
android:name=".activities.FCastGuideActivity" android:name=".activities.FCastGuideActivity"
android:screenOrientation="sensorPortrait" android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity <activity
android:name=".activities.SyncHomeActivity" android:name=".activities.SyncHomeActivity"
android:screenOrientation="sensorPortrait" android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity <activity
android:name=".activities.SyncPairActivity" android:name=".activities.SyncPairActivity"
android:screenOrientation="sensorPortrait" android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity <activity
android:name=".activities.SyncShowPairingCodeActivity" android:name=".activities.SyncShowPairingCodeActivity"
android:screenOrientation="sensorPortrait" android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
<activity
android:name=".activities.PolycentricModerationActivity"
android:exported="false"
android:screenOrientation="portrait" />
<activity
android:name=".activities.QRCodeFullscreenActivity"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
<service
android:name=".UpdateDownloadService"
android:exported="false"
android:foregroundServiceType="dataSync" />
<receiver
android:name=".UpdateActionReceiver"
android:exported="false" />
<activity
android:name=".activities.InstallUpdateActivity"
android:exported="false"
android:theme="@style/Theme.App.TransparentNoUi"
android:excludeFromRecents="true"
android:finishOnTaskLaunch="true" />
</application> </application>
</manifest> </manifest>
+2 -25
View File
@@ -1022,38 +1022,15 @@
return x.value return x.value
}); });
let settingsToUse = __DEV_SETTINGS ?? {};
if (true) {
const settings = this.Plugin?.currentPlugin?.settings;
if (settings) {
for (let setting of settings) {
if (typeof settingsToUse[setting.variable] == "undefined") {
switch (setting?.type?.toLowerCase()) {
case "boolean":
settingsToUse[setting.variable] = setting.default === 'true';
break;
case "dropdown":
let dropDownIndex = parseInt(setting.default);
if (dropDownIndex) {
settingsToUse[setting.variable] = setting.options[dropDownIndex];
}
break;
}
}
}
}
}
if(name == "enable") { if(name == "enable") {
if(parameterVals.length > 0) if(parameterVals.length > 0)
parameterVals[0] = this.Plugin.currentPlugin; parameterVals[0] = this.Plugin.currentPlugin;
else else
parameterVals.push(this.Plugin.currentPlugin); parameterVals.push(this.Plugin.currentPlugin);
if(parameterVals.length > 1) if(parameterVals.length > 1)
parameterVals[1] = settingsToUse; parameterVals[1] = __DEV_SETTINGS;
else else
parameterVals.push(settingsToUse); parameterVals.push(__DEV_SETTINGS);
} }
const func = source[name]; const func = source[name];
+9 -62
View File
@@ -32,8 +32,7 @@ let Type = {
Text: { Text: {
RAW: 0, RAW: 0,
HTML: 1, HTML: 1,
MARKUP: 2, MARKUP: 2
CODE: 3
}, },
Chapter: { Chapter: {
NORMAL: 0, NORMAL: 0,
@@ -67,7 +66,6 @@ class ScriptException extends Error {
super(arguments[0]); super(arguments[0]);
this.plugin_type = "ScriptException"; this.plugin_type = "ScriptException";
this.message = arguments[0]; this.message = arguments[0];
this.msg = arguments[0];
} }
else { else {
super(msg); super(msg);
@@ -104,12 +102,6 @@ class UnavailableException extends ScriptException {
super("UnavailableException", msg); super("UnavailableException", msg);
} }
} }
class ReloadRequiredException extends ScriptException {
constructor(msg, reloadData) {
super("ReloadRequiredException", msg);
this.reloadData = reloadData;
}
}
class AgeException extends ScriptException { class AgeException extends ScriptException {
constructor(msg) { constructor(msg) {
super("AgeException", msg); super("AgeException", msg);
@@ -252,9 +244,6 @@ class PlatformVideo extends PlatformContent {
this.duration = obj.duration ?? -1; //Long this.duration = obj.duration ?? -1; //Long
this.viewCount = obj.viewCount ?? -1; //Long this.viewCount = obj.viewCount ?? -1; //Long
this.playbackTime = obj.playbackTime ?? -1;
this.playbackDate = obj.playbackDate ?? undefined;
this.isLive = obj.isLive ?? false; //Boolean this.isLive = obj.isLive ?? false; //Boolean
this.isShort = !!obj.isShort ?? false; this.isShort = !!obj.isShort ?? false;
} }
@@ -302,39 +291,15 @@ class PlatformPostDetails extends PlatformPost {
} }
} }
class PlatformWeb extends PlatformContent { class PlatformArticleDetails extends PlatformContent {
constructor(obj) {
super(obj, 7);
obj = obj ?? {};
this.plugin_type = "PlatformWeb";
}
}
class PlatformWebDetails extends PlatformWeb {
constructor(obj) {
super(obj, 7);
obj = obj ?? {};
this.plugin_type = "PlatformWebDetails";
this.html = obj.html;
}
}
class PlatformArticle extends PlatformContent {
constructor(obj) {
super(obj, 3);
obj = obj ?? {};
this.plugin_type = "PlatformArticle";
this.rating = obj.rating ?? new RatingLikes(-1);
this.summary = obj.summary ?? "";
this.thumbnails = obj.thumbnails ?? new Thumbnails([]);
}
}
class PlatformArticleDetails extends PlatformArticle {
constructor(obj) { constructor(obj) {
super(obj, 3); super(obj, 3);
obj = obj ?? {}; obj = obj ?? {};
this.plugin_type = "PlatformArticleDetails"; this.plugin_type = "PlatformArticleDetails";
this.rating = obj.rating ?? new RatingLikes(-1); this.rating = obj.rating ?? new RatingLikes(-1);
this.summary = obj.summary ?? "";
this.segments = obj.segments ?? []; this.segments = obj.segments ?? [];
this.thumbnails = obj.thumbnails ?? new Thumbnails([]);
} }
} }
class ArticleSegment { class ArticleSegment {
@@ -350,17 +315,9 @@ class ArticleTextSegment extends ArticleSegment {
} }
} }
class ArticleImagesSegment extends ArticleSegment { class ArticleImagesSegment extends ArticleSegment {
constructor(images, caption) { constructor(images) {
super(2); super(2);
this.images = images; this.images = images;
this.caption = caption;
}
}
class ArticleHeaderSegment extends ArticleSegment {
constructor(content, level) {
super(3);
this.level = level;
this.content = content;
} }
} }
class ArticleNestedSegment extends ArticleSegment { class ArticleNestedSegment extends ArticleSegment {
@@ -468,20 +425,14 @@ class AudioUrlWidevineSource extends AudioUrlSource {
this.getLicenseRequestExecutor = () => { this.getLicenseRequestExecutor = () => {
return { return {
executeRequest: (url, _headers, _method, license_request_data) => { executeRequest: (url, _headers, _method, license_request_data) => {
const response = http.POST( return http.POST(
url, url,
license_request_data, license_request_data,
{ Authorization: `Bearer ${obj.bearerToken}` }, { Authorization: `Bearer ${obj.bearerToken}` },
false, false,
true true
); ).body
}
if (!response.body) {
throw new ScriptException("Unable to acquire license key");
}
return response.body;
}
} }
} }
} }
@@ -644,8 +595,6 @@ class PlatformComment {
this.date = obj.date ?? 0; this.date = obj.date ?? 0;
this.replyCount = obj.replyCount ?? 0; this.replyCount = obj.replyCount ?? 0;
this.context = obj.context ?? {}; this.context = obj.context ?? {};
if(obj.getReplies)
this.getReplies = obj.getReplies;
} }
} }
@@ -717,12 +666,11 @@ class LiveEventViewCount extends LiveEvent {
} }
} }
class LiveEventRaid extends LiveEvent { class LiveEventRaid extends LiveEvent {
constructor(targetUrl, targetName, targetThumbnail, isOutgoing) { constructor(targetUrl, targetName, targetThumbnail) {
super(100); super(100);
this.targetUrl = targetUrl; this.targetUrl = targetUrl;
this.targetName = targetName; this.targetName = targetName;
this.targetThumbnail = targetThumbnail; this.targetThumbnail = targetThumbnail;
this.isOutgoing = isOutgoing ?? true;
} }
} }
@@ -795,7 +743,6 @@ let plugin = {
//To override by plugin //To override by plugin
const source = { const source = {
getHome() { return new ContentPager([], false, {}); }, getHome() { return new ContentPager([], false, {}); },
getShorts() { return new VideoPager([], false, {}); },
enable(config){ }, enable(config){ },
disable() {}, disable() {},
@@ -1,43 +0,0 @@
package com.futo.platformplayer
import android.content.Context
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.net.HttpURLConnection
import java.net.URL
object AppCaUpdater {
private const val CA_URL = "https://curl.se/ca/cacert.pem"
private const val CACHE_FILENAME = "curl-ca-bundle.pem"
private const val MAX_AGE_DAYS = 30
suspend fun ensureCaBundle(context: Context): File = withContext(Dispatchers.IO) {
val file = File(context.noBackupFilesDir, CACHE_FILENAME)
val needsUpdate = !file.exists() || isOlderThanDays(file, MAX_AGE_DAYS)
if (needsUpdate) {
downloadToFile(CA_URL, file)
}
return@withContext file
}
private fun isOlderThanDays(file: File, days: Int): Boolean {
val ageMs = System.currentTimeMillis() - file.lastModified()
return ageMs > days * 24L * 60L * 60L * 1000L
}
private fun downloadToFile(urlStr: String, dest: File) {
val conn = (URL(urlStr).openConnection() as HttpURLConnection).apply {
connectTimeout = 15000
readTimeout = 15000
instanceFollowRedirects = true
}
conn.inputStream.use { input ->
dest.parentFile?.mkdirs()
dest.outputStream().use { output ->
input.copyTo(output)
}
}
conn.disconnect()
}
}
@@ -1,319 +0,0 @@
import kotlin.math.*
class CSSColor(r: Float, g: Float, b: Float, a: Float = 1f) {
init {
require(r in 0f..1f && g in 0f..1f && b in 0f..1f && a in 0f..1f) {
"RGBA channels must be in [0,1]"
}
}
// -- RGB(A) channels stored 01 --
var r: Float = r.coerceIn(0f, 1f)
set(v) { field = v.coerceIn(0f, 1f); _hslDirty = true }
var g: Float = g.coerceIn(0f, 1f)
set(v) { field = v.coerceIn(0f, 1f); _hslDirty = true }
var b: Float = b.coerceIn(0f, 1f)
set(v) { field = v.coerceIn(0f, 1f); _hslDirty = true }
var a: Float = a.coerceIn(0f, 1f)
set(v) { field = v.coerceIn(0f, 1f) }
// -- Int views of RGBA 0255 --
var red: Int
get() = (r * 255).roundToInt()
set(v) { r = (v.coerceIn(0, 255) / 255f) }
var green: Int
get() = (g * 255).roundToInt()
set(v) { g = (v.coerceIn(0, 255) / 255f) }
var blue: Int
get() = (b * 255).roundToInt()
set(v) { b = (v.coerceIn(0, 255) / 255f) }
var alpha: Int
get() = (a * 255).roundToInt()
set(v) { a = (v.coerceIn(0, 255) / 255f) }
// -- HSLA storage & lazy recompute flags --
private var _h: Float = 0f
private var _s: Float = 0f
private var _l: Float = 0f
private var _hslDirty = true
/** Hue [0...360) */
var hue: Float
get() { computeHslIfNeeded(); return _h }
set(v) { setHsl(v, saturation, lightness) }
/** Saturation [0...1] */
var saturation: Float
get() { computeHslIfNeeded(); return _s }
set(v) { setHsl(hue, v, lightness) }
/** Lightness [0...1] */
var lightness: Float
get() { computeHslIfNeeded(); return _l }
set(v) { setHsl(hue, saturation, v) }
private fun computeHslIfNeeded() {
if (!_hslDirty) return
val max = max(max(r, g), b)
val min = min(min(r, g), b)
val d = max - min
_l = (max + min) / 2f
_s = if (d == 0f) 0f else d / (1f - abs(2f * _l - 1f))
_h = when {
d == 0f -> 0f
max == r -> ((g - b) / d % 6f) * 60f
max == g -> (((b - r) / d) + 2f) * 60f
else -> (((r - g) / d) + 4f) * 60f
}.let { if (it < 0f) it + 360f else it }
_hslDirty = false
}
/**
* Set all three HSL channels at once.
* Hue in degrees [0...360), s/l [0...1].
*/
fun setHsl(h: Float, s: Float, l: Float) {
val hh = ((h % 360f) + 360f) % 360f
val cc = (1f - abs(2f * l - 1f)) * s
val x = cc * (1f - abs((hh / 60f) % 2f - 1f))
val m = l - cc / 2f
val (rp, gp, bp) = when {
hh < 60f -> Triple(cc, x, 0f)
hh < 120f -> Triple(x, cc, 0f)
hh < 180f -> Triple(0f, cc, x)
hh < 240f -> Triple(0f, x, cc)
hh < 300f -> Triple(x, 0f, cc)
else -> Triple(cc, 0f, x)
}
r = rp + m; g = gp + m; b = bp + m
_h = hh; _s = s; _l = l; _hslDirty = false
}
/** Return 0xRRGGBBAA int */
fun toRgbaInt(): Int {
val ai = (a * 255).roundToInt() and 0xFF
val ri = (r * 255).roundToInt() and 0xFF
val gi = (g * 255).roundToInt() and 0xFF
val bi = (b * 255).roundToInt() and 0xFF
return (ri shl 24) or (gi shl 16) or (bi shl 8) or ai
}
/** Return 0xAARRGGBB int */
fun toArgbInt(): Int {
val ai = (a * 255).roundToInt() and 0xFF
val ri = (r * 255).roundToInt() and 0xFF
val gi = (g * 255).roundToInt() and 0xFF
val bi = (b * 255).roundToInt() and 0xFF
return (ai shl 24) or (ri shl 16) or (gi shl 8) or bi
}
// — Convenience modifiers (chainable) —
/** Lighten by fraction [0...1] */
fun lighten(fraction: Float): CSSColor = apply {
lightness = (lightness + fraction).coerceIn(0f, 1f)
}
/** Darken by fraction [0...1] */
fun darken(fraction: Float): CSSColor = apply {
lightness = (lightness - fraction).coerceIn(0f, 1f)
}
/** Increase saturation by fraction [0...1] */
fun saturate(fraction: Float): CSSColor = apply {
saturation = (saturation + fraction).coerceIn(0f, 1f)
}
/** Decrease saturation by fraction [0...1] */
fun desaturate(fraction: Float): CSSColor = apply {
saturation = (saturation - fraction).coerceIn(0f, 1f)
}
/** Rotate hue by degrees (can be negative) */
fun rotateHue(degrees: Float): CSSColor = apply {
hue = (hue + degrees) % 360f
}
companion object {
/** Create from Android 0xAARRGGBB */
@JvmStatic fun fromArgb(color: Int): CSSColor {
val a = ((color ushr 24) and 0xFF) / 255f
val r = ((color ushr 16) and 0xFF) / 255f
val g = ((color ushr 8) and 0xFF) / 255f
val b = ( color and 0xFF) / 255f
return CSSColor(r, g, b, a)
}
/** Create from Android 0xRRGGBBAA */
@JvmStatic fun fromRgba(color: Int): CSSColor {
val r = ((color ushr 24) and 0xFF) / 255f
val g = ((color ushr 16) and 0xFF) / 255f
val b = ((color ushr 8) and 0xFF) / 255f
val a = ( color and 0xFF) / 255f
return CSSColor(r, g, b, a)
}
@JvmStatic fun fromAndroidColor(color: Int): CSSColor {
return fromArgb(color)
}
private val NAMED_HEX = mapOf(
"aliceblue" to "F0F8FF", "antiquewhite" to "FAEBD7", "aqua" to "00FFFF",
"aquamarine" to "7FFFD4", "azure" to "F0FFFF", "beige" to "F5F5DC",
"bisque" to "FFE4C4", "black" to "000000", "blanchedalmond" to "FFEBCD",
"blue" to "0000FF", "blueviolet" to "8A2BE2", "brown" to "A52A2A",
"burlywood" to "DEB887", "cadetblue" to "5F9EA0", "chartreuse" to "7FFF00",
"chocolate" to "D2691E", "coral" to "FF7F50", "cornflowerblue" to "6495ED",
"cornsilk" to "FFF8DC", "crimson" to "DC143C", "cyan" to "00FFFF",
"darkblue" to "00008B", "darkcyan" to "008B8B", "darkgoldenrod" to "B8860B",
"darkgray" to "A9A9A9", "darkgreen" to "006400", "darkgrey" to "A9A9A9",
"darkkhaki" to "BDB76B", "darkmagenta" to "8B008B", "darkolivegreen" to "556B2F",
"darkorange" to "FF8C00", "darkorchid" to "9932CC", "darkred" to "8B0000",
"darksalmon" to "E9967A", "darkseagreen" to "8FBC8F", "darkslateblue" to "483D8B",
"darkslategray" to "2F4F4F", "darkslategrey" to "2F4F4F", "darkturquoise" to "00CED1",
"darkviolet" to "9400D3", "deeppink" to "FF1493", "deepskyblue" to "00BFFF",
"dimgray" to "696969", "dimgrey" to "696969", "dodgerblue" to "1E90FF",
"firebrick" to "B22222", "floralwhite" to "FFFAF0", "forestgreen" to "228B22",
"fuchsia" to "FF00FF", "gainsboro" to "DCDCDC", "ghostwhite" to "F8F8FF",
"gold" to "FFD700", "goldenrod" to "DAA520", "gray" to "808080",
"green" to "008000", "greenyellow" to "ADFF2F", "grey" to "808080",
"honeydew" to "F0FFF0", "hotpink" to "FF69B4", "indianred" to "CD5C5C",
"indigo" to "4B0082", "ivory" to "FFFFF0", "khaki" to "F0E68C",
"lavender" to "E6E6FA", "lavenderblush" to "FFF0F5", "lawngreen" to "7CFC00",
"lemonchiffon" to "FFFACD", "lightblue" to "ADD8E6", "lightcoral" to "F08080",
"lightcyan" to "E0FFFF", "lightgoldenrodyellow" to "FAFAD2", "lightgray" to "D3D3D3",
"lightgreen" to "90EE90", "lightgrey" to "D3D3D3", "lightpink" to "FFB6C1",
"lightsalmon" to "FFA07A", "lightseagreen" to "20B2AA", "lightskyblue" to "87CEFA",
"lightslategray" to "778899", "lightslategrey" to "778899", "lightsteelblue" to "B0C4DE",
"lightyellow" to "FFFFE0", "lime" to "00FF00", "limegreen" to "32CD32",
"linen" to "FAF0E6", "magenta" to "FF00FF", "maroon" to "800000",
"mediumaquamarine" to "66CDAA", "mediumblue" to "0000CD", "mediumorchid" to "BA55D3",
"mediumpurple" to "9370DB", "mediumseagreen" to "3CB371", "mediumslateblue" to "7B68EE",
"mediumspringgreen" to "00FA9A", "mediumturquoise" to "48D1CC", "mediumvioletred" to "C71585",
"midnightblue" to "191970", "mintcream" to "F5FFFA", "mistyrose" to "FFE4E1",
"moccasin" to "FFE4B5", "navajowhite" to "FFDEAD", "navy" to "000080",
"oldlace" to "FDF5E6", "olive" to "808000", "olivedrab" to "6B8E23",
"orange" to "FFA500", "orangered" to "FF4500", "orchid" to "DA70D6",
"palegoldenrod" to "EEE8AA", "palegreen" to "98FB98", "paleturquoise" to "AFEEEE",
"palevioletred" to "DB7093", "papayawhip" to "FFEFD5", "peachpuff" to "FFDAB9",
"peru" to "CD853F", "pink" to "FFC0CB", "plum" to "DDA0DD",
"powderblue" to "B0E0E6", "purple" to "800080", "rebeccapurple" to "663399",
"red" to "FF0000", "rosybrown" to "BC8F8F", "royalblue" to "4169E1",
"saddlebrown" to "8B4513", "salmon" to "FA8072", "sandybrown" to "F4A460",
"seagreen" to "2E8B57", "seashell" to "FFF5EE", "sienna" to "A0522D",
"silver" to "C0C0C0", "skyblue" to "87CEEB", "slateblue" to "6A5ACD",
"slategray" to "708090", "slategrey" to "708090", "snow" to "FFFAFA",
"springgreen" to "00FF7F", "steelblue" to "4682B4", "tan" to "D2B48C",
"teal" to "008080", "thistle" to "D8BFD8", "tomato" to "FF6347",
"turquoise" to "40E0D0", "violet" to "EE82EE", "wheat" to "F5DEB3",
"white" to "FFFFFF", "whitesmoke" to "F5F5F5", "yellow" to "FFFF00",
"yellowgreen" to "9ACD32"
)
private val NAMED: Map<String, Int> = NAMED_HEX
.mapValues { (_, hexRgb) ->
// parse hexRgb ("RRGGBB") to Int, then OR in 0xFF000000 for full opacity
val rgb = hexRgb.toInt(16)
(rgb shl 8) or 0xFF
} + ("transparent" to 0x00000000)
private val HEX_REGEX = Regex("^#([0-9a-fA-F]{3,8})$", RegexOption.IGNORE_CASE)
private val RGB_REGEX = Regex("^rgba?\\(([^)]+)\\)\$", RegexOption.IGNORE_CASE)
private val HSL_REGEX = Regex("^hsla?\\(([^)]+)\\)\$", RegexOption.IGNORE_CASE)
@JvmStatic
fun parseColor(s: String): CSSColor {
val str = s.trim()
// named
NAMED[str.lowercase()]?.let { return it.RGBAtoCSSColor() }
// hex
HEX_REGEX.matchEntire(str)?.groupValues?.get(1)?.let { part ->
return parseHexPart(part)
}
// rgb/rgba
RGB_REGEX.matchEntire(str)?.groupValues?.get(1)?.let {
return parseRgbParts(it.split(',').map(String::trim))
}
// hsl/hsla
HSL_REGEX.matchEntire(str)?.groupValues?.get(1)?.let {
return parseHslParts(it.split(',').map(String::trim))
}
error("Cannot parse color: \"$s\"")
}
private fun parseHexPart(p: String): CSSColor {
// expand shorthand like "RGB" or "RGBA" to full 8-chars "RRGGBBAA"
val hex = when (p.length) {
3 -> p.map { "$it$it" }.joinToString("") + "FF"
4 -> p.map { "$it$it" }.joinToString("")
6 -> p + "FF"
8 -> p
else -> error("Invalid hex color: #$p")
}
val parsed = hex.toLong(16).toInt()
val alpha = (parsed and 0xFF) shl 24
val rgbOnly = (parsed ushr 8) and 0x00FFFFFF
val argb = alpha or rgbOnly
return fromArgb(argb)
}
private fun parseRgbParts(parts: List<String>): CSSColor {
require(parts.size == 3 || parts.size == 4) { "rgb/rgba needs 3 or 4 parts" }
// r/g/b: "128" → 128/255, "50%" → 0.5
fun channel(ch: String): Float =
if (ch.endsWith("%")) ch.removeSuffix("%").toFloat() / 100f
else ch.toFloat().coerceIn(0f, 255f) / 255f
// alpha: "0.5" → 0.5, "50%" → 0.5
fun alpha(a: String): Float =
if (a.endsWith("%")) a.removeSuffix("%").toFloat() / 100f
else a.toFloat().coerceIn(0f, 1f)
val r = channel(parts[0])
val g = channel(parts[1])
val b = channel(parts[2])
val a = if (parts.size == 4) alpha(parts[3]) else 1f
return CSSColor(r, g, b, a)
}
private fun parseHslParts(parts: List<String>): CSSColor {
require(parts.size == 3 || parts.size == 4) { "hsl/hsla needs 3 or 4 parts" }
fun hueOf(h: String): Float = when {
h.endsWith("deg") -> h.removeSuffix("deg").toFloat()
h.endsWith("grad") -> h.removeSuffix("grad").toFloat() * 0.9f
h.endsWith("rad") -> h.removeSuffix("rad").toFloat() * (180f / PI.toFloat())
h.endsWith("turn") -> h.removeSuffix("turn").toFloat() * 360f
else -> h.toFloat()
}
// for s and l you only ever see percentages
fun pct(p: String): Float =
p.removeSuffix("%").toFloat().coerceIn(0f, 100f) / 100f
// alpha: "0.5" → 0.5, "50%" → 0.5
fun alpha(a: String): Float =
if (a.endsWith("%")) pct(a)
else a.toFloat().coerceIn(0f, 1f)
val h = hueOf(parts[0])
val s = pct(parts[1])
val l = pct(parts[2])
val a = if (parts.size == 4) alpha(parts[3]) else 1f
return CSSColor(0f, 0f, 0f, a).apply { setHsl(h, s, l) }
}
}
}
fun Int.RGBAtoCSSColor(): CSSColor = CSSColor.fromRgba(this)
fun Int.ARGBtoCSSColor(): CSSColor = CSSColor.fromArgb(this)
fun CSSColor.toAndroidColor(): Int = toArgbInt()
@@ -14,6 +14,7 @@ import java.text.DecimalFormat
import java.time.OffsetDateTime import java.time.OffsetDateTime
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.roundToInt
import kotlin.math.roundToLong import kotlin.math.roundToLong
@@ -375,19 +376,14 @@ private val slds = hashSetOf(".com.ac", ".net.ac", ".gov.ac", ".org.ac", ".mil.a
fun String.matchesDomain(queryDomain: String): Boolean { fun String.matchesDomain(queryDomain: String): Boolean {
if(queryDomain.startsWith(".")) { if(queryDomain.startsWith(".")) {
val parts = this.lowercase().split(".");
val queryParts = queryDomain.lowercase().trimStart("."[0]).split("."); val parts = queryDomain.lowercase().split(".");
if(queryParts.size < 2) if(parts.size < 3)
throw IllegalStateException("Illegal use of wildcards on First-Level-Domain (" + queryDomain + ")"); throw IllegalStateException("Illegal use of wildcards on First-Level-Domain (" + queryDomain + ")");
else { if(parts.size >= 3){
val possibleDomain = "." + queryParts.joinToString("."); val isSLD = slds.contains("." + parts[parts.size - 2] + "." + parts[parts.size - 1]);
if(slds.contains(possibleDomain)) if(isSLD && parts.size <= 3)
throw IllegalStateException("Illegal use of wildcards on Second-Level-Domain (" + queryDomain + ")"); throw IllegalStateException("Illegal use of wildcards on Second-Level-Domain (" + queryDomain + ")");
/*
val isSLD = slds.contains("." + queryParts[queryParts.size - 2] + "." + queryParts[queryParts.size - 1]);
if(isSLD && queryParts.size <= 3)
throw IllegalStateException("Illegal use of wildcards on Second-Level-Domain (" + queryDomain + ")");
*/
} }
//TODO: Should be safe, but double verify if can't be exploited //TODO: Should be safe, but double verify if can't be exploited
@@ -399,11 +395,9 @@ fun String.matchesDomain(queryDomain: String): Boolean {
fun String.getSubdomainWildcardQuery(): String { fun String.getSubdomainWildcardQuery(): String {
val domainParts = this.split("."); val domainParts = this.split(".");
var wildcardDomain = if(domainParts.size > 2) val sldParts = "." + domainParts[domainParts.size - 2].lowercase() + "." + domainParts[domainParts.size - 1].lowercase();
"." + domainParts.drop(1).joinToString(".") if(slds.contains(sldParts))
return "." + domainParts.drop(domainParts.size - 3).joinToString(".");
else else
"." + domainParts.joinToString("."); return "." + domainParts.drop(domainParts.size - 2).joinToString(".");
if(slds.contains(wildcardDomain.lowercase()))
"." + domainParts.joinToString(".");
return wildcardDomain;
} }
@@ -216,8 +216,9 @@ private fun ByteArray.toInetAddress(): InetAddress {
return InetAddress.getByAddress(this); return InetAddress.getByAddress(this);
} }
fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int, timeoutMs: Int = 10_000): Socket? { fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int): Socket? {
ensureNotMainThread() val timeout = 2000
val addresses = if(!Settings.instance.casting.allowIpv6) attemptAddresses.filterIsInstance<Inet4Address>() else attemptAddresses; val addresses = if(!Settings.instance.casting.allowIpv6) attemptAddresses.filterIsInstance<Inet4Address>() else attemptAddresses;
if(addresses.isEmpty()) if(addresses.isEmpty())
@@ -231,7 +232,7 @@ fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int, timeoutMs
val socket = Socket() val socket = Socket()
try { try {
return socket.apply { this.connect(InetSocketAddress(addresses[0], port), timeoutMs) } return socket.apply { this.connect(InetSocketAddress(addresses[0], port), timeout) }
} catch (e: Throwable) { } catch (e: Throwable) {
Log.i("getConnectedSocket", "Failed to connect to: ${addresses[0]}", e) Log.i("getConnectedSocket", "Failed to connect to: ${addresses[0]}", e)
socket.close() socket.close()
@@ -240,11 +241,8 @@ fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int, timeoutMs
return null; return null;
} }
val sortedAddresses: List<InetAddress> = addresses
.sortedBy { addr -> addressScore(addr) }
val sockets: ArrayList<Socket> = arrayListOf(); val sockets: ArrayList<Socket> = arrayListOf();
for (i in sortedAddresses.indices) { for (i in addresses.indices) {
sockets.add(Socket()); sockets.add(Socket());
} }
@@ -252,7 +250,7 @@ fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int, timeoutMs
var connectedSocket: Socket? = null; var connectedSocket: Socket? = null;
val threads: ArrayList<Thread> = arrayListOf(); val threads: ArrayList<Thread> = arrayListOf();
for (i in 0 until sockets.size) { for (i in 0 until sockets.size) {
val address = sortedAddresses[i]; val address = addresses[i];
val socket = sockets[i]; val socket = sockets[i];
val thread = Thread { val thread = Thread {
try { try {
@@ -262,7 +260,7 @@ fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int, timeoutMs
} }
} }
socket.connect(InetSocketAddress(address, port), timeoutMs); socket.connect(InetSocketAddress(address, port), timeout);
synchronized(syncObject) { synchronized(syncObject) {
if (connectedSocket == null) { if (connectedSocket == null) {
@@ -7,9 +7,6 @@ import java.net.InetAddress
import java.net.URI import java.net.URI
import java.net.URISyntaxException import java.net.URISyntaxException
import java.net.URLEncoder import java.net.URLEncoder
import java.time.Instant
import java.time.OffsetDateTime
import java.time.ZoneOffset
//Syntax sugaring //Syntax sugaring
inline fun <reified T> Any.assume(): T?{ inline fun <reified T> Any.assume(): T?{
@@ -36,37 +33,13 @@ fun Boolean?.toYesNo(): String {
fun InetAddress?.toUrlAddress(): String { fun InetAddress?.toUrlAddress(): String {
return when (this) { return when (this) {
is Inet6Address -> { is Inet6Address -> {
val hostAddr = this.hostAddress ?: throw Exception("Invalid address: hostAddress is null") "[${hostAddress}]"
val index = hostAddr.indexOf('%')
if (index != -1) {
val addrPart = hostAddr.substring(0, index)
val scopeId = hostAddr.substring(index + 1)
"[${addrPart}%25${scopeId}]" // %25 is URL-encoded '%'
} else {
"[$hostAddr]"
}
} }
is Inet4Address -> { is Inet4Address -> {
this.hostAddress ?: throw Exception("Invalid address: hostAddress is null") hostAddress
} }
else -> { else -> {
throw Exception("Invalid address type") throw Exception("Invalid address type")
} }
} }
}
fun Long?.sToOffsetDateTimeUTC(): OffsetDateTime {
if (this == null || this < 0)
return OffsetDateTime.MIN
if(this > 4070912400)
return OffsetDateTime.MAX;
return OffsetDateTime.ofInstant(Instant.ofEpochSecond(this), ZoneOffset.UTC)
}
fun Long?.msToOffsetDateTimeUTC(): OffsetDateTime {
if (this == null || this < 0)
return OffsetDateTime.MIN
if(this > 4070912400)
return OffsetDateTime.MAX;
return OffsetDateTime.ofInstant(Instant.ofEpochMilli(this), ZoneOffset.UTC)
} }
@@ -2,32 +2,10 @@ package com.futo.platformplayer
import com.caoccao.javet.values.V8Value import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.primitive.* import com.caoccao.javet.values.primitive.*
import com.caoccao.javet.values.reference.IV8ValuePromise
import com.caoccao.javet.values.reference.V8ValueArray import com.caoccao.javet.values.reference.V8ValueArray
import com.caoccao.javet.values.reference.V8ValueError
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.caoccao.javet.values.reference.V8ValuePromise
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.engine.exceptions.ScriptExecutionException
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
import com.futo.platformplayer.logging.Logger
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.selects.SelectClause0
import kotlinx.coroutines.selects.SelectClause1
import java.util.concurrent.CountDownLatch
import kotlin.coroutines.AbstractCoroutineContextElement
import kotlin.coroutines.CoroutineContext
import kotlin.reflect.jvm.internal.impl.load.kotlin.JvmType
//V8 //V8
@@ -46,10 +24,6 @@ fun <R> V8Value?.orDefault(default: R, handler: (V8Value)->R): R {
return handler(this); return handler(this);
} }
inline fun V8Value.getSourcePlugin(): V8Plugin? {
return V8Plugin.getPluginFromRuntime(this.v8Runtime);
}
inline fun <reified T> V8Value.expectOrThrow(config: IV8PluginConfig, contextName: String): T { inline fun <reified T> V8Value.expectOrThrow(config: IV8PluginConfig, contextName: String): T {
if(this !is T) if(this !is T)
throw ScriptImplementationException(config, "Expected ${contextName} to be of type ${T::class.simpleName}, but found ${this::class.simpleName}"); throw ScriptImplementationException(config, "Expected ${contextName} to be of type ${T::class.simpleName}, but found ${this::class.simpleName}");
@@ -115,29 +89,7 @@ inline fun <reified T> V8ValueArray.expectV8Variants(config: IV8PluginConfig, co
.map { kv-> kv.second.orNull { it.expectV8Variant<T>(config, contextName + "[${kv.first}]", ) } as T }; .map { kv-> kv.second.orNull { it.expectV8Variant<T>(config, contextName + "[${kv.first}]", ) } as T };
} }
inline fun V8Plugin.ensureIsBusy() {
this.let {
if (!it.isThreadAlreadyBusy()) {
//throw IllegalStateException("Tried to access V8Plugin without busy");
val stacktrace = Thread.currentThread().stackTrace;
Logger.w("Extensions_V8",
"V8 USE OUTSIDE BUSY: " + stacktrace.drop(3)?.firstOrNull().toString() +
", " + stacktrace.drop(4)?.firstOrNull().toString() +
", " + stacktrace.drop(5)?.firstOrNull()?.toString() +
", " + stacktrace.drop(6)?.firstOrNull()?.toString()
);
}
}
}
inline fun V8Value.ensureIsBusy() {
this?.getSourcePlugin()?.let {
it.ensureIsBusy();
}
}
inline fun <reified T> V8Value.expectV8Variant(config: IV8PluginConfig, contextName: String): T { inline fun <reified T> V8Value.expectV8Variant(config: IV8PluginConfig, contextName: String): T {
if(false)
ensureIsBusy();
return when(T::class) { return when(T::class) {
String::class -> this.expectOrThrow<V8ValueString>(config, contextName).value as T; String::class -> this.expectOrThrow<V8ValueString>(config, contextName).value as T;
Int::class -> { Int::class -> {
@@ -194,209 +146,4 @@ fun V8ObjectToHashMap(obj: V8ValueObject?): HashMap<String, String> {
for(prop in obj.ownPropertyNames.keys.map { obj.ownPropertyNames.get<V8Value>(it).toString() }) for(prop in obj.ownPropertyNames.keys.map { obj.ownPropertyNames.get<V8Value>(it).toString() })
map.put(prop, obj.getString(prop)); map.put(prop, obj.getString(prop));
return map; return map;
}
fun <T: V8Value> V8ValuePromise.toV8ValueBlocking(plugin: V8Plugin): T {
val latch = CountDownLatch(1);
var promiseResult: T? = null;
var promiseException: Throwable? = null;
plugin.busy {
this.register(object: IV8ValuePromise.IListener {
override fun onFulfilled(p0: V8Value?) {
if(p0 is V8ValueError)
promiseException = ScriptExecutionException(plugin.config, p0.message);
else {
if(p0 is V8ValueObject)
p0.setWeak();
promiseResult = p0 as T;
}
latch.countDown();
}
override fun onRejected(p0: V8Value?) {
promiseException = p0?.toException(plugin.config);
latch.countDown();
}
override fun onCatch(p0: V8Value?) {
promiseException = p0?.toException(plugin.config);
latch.countDown();
}
});
}
plugin.registerPromise(this) {
promiseException = CancellationException("Cancelled by system");
latch.countDown();
}
//Logger.i("V8", "V8ValueBlocking started (Busy) [" + blockCount + "]" + Thread.currentThread().stackTrace.drop(3)?.firstOrNull()?.toString() + ", " + Thread.currentThread().stackTrace.drop(4)?.firstOrNull()?.toString()+ ", " + Thread.currentThread().stackTrace.drop(5)?.firstOrNull()?.toString());
if(!promise.isPending) {
try {
Logger.i("V8", "V8Promise resolved synchronously");
if(promise.isFulfilled)
promiseResult = promise.getResult<T>();
else
promiseException = promise.getResult<V8Value>().toException(plugin.config);
}
catch(ex: Throwable) {
promiseException = ex;
}
}
else {
plugin.unbusy {
latch.await();
}
}
if(promiseException != null)
throw promiseException!!;
return promiseResult!!;
}
fun <T: V8Value> V8ValuePromise.toV8ValueAsync(plugin: V8Plugin): V8Deferred<T> {
val underlyingDef = CompletableDeferred<T>();
val def = if(this.has("estDuration"))
V8Deferred(underlyingDef,
this.getOrDefault(plugin.config, "estDuration", "toV8ValueAsync", -1) ?: -1);
else
V8Deferred<T>(underlyingDef);
if(def.estDuration > 0)
Logger.i("V8", "Promise with duration: [${def.estDuration}]");
val promise = this;
plugin.busy {
this.register(object: IV8ValuePromise.IListener {
override fun onFulfilled(p0: V8Value?) {
plugin.resolvePromise(promise);
underlyingDef.complete(p0 as T);
}
override fun onRejected(p0: V8Value?) {
try {
plugin.resolvePromise(promise);
val exceptionFound = p0?.toException(plugin.config) ?: NotImplementedError("onRejected promise not implemented..");
Logger.i("V8", "Promise rejected, setting exception");
underlyingDef.completeExceptionally(CancellationException(exceptionFound.message, exceptionFound));
}
catch(ex: Throwable) {
Logger.e("V8", "Rejection handling failed?" , ex);
}
}
override fun onCatch(p0: V8Value?) {
try {
plugin.resolvePromise(promise);
val exceptionFound = p0?.toException(plugin.config) ?: NotImplementedError("onCatch promise not implemented..");
underlyingDef.completeExceptionally(CancellationException(exceptionFound.message, exceptionFound));
}
catch(ex: Throwable) {
Logger.e("V8", "Catching handling failed?" , ex);
}
}
});
}
plugin.registerPromise(promise) {
if(def.isActive)
def.cancel("Cancelled by system");
}
return def;
}
fun V8Value.toException(config: IV8PluginConfig): Throwable {
val p0 = this;
if(p0 is V8ValueObject) {
return V8Plugin.getExceptionFromPlugin(config, p0, null, null, null, "P:");
/*
val pluginType = p0.getOrDefault(config, "plugin_type", "Promise Exception", "")?.let { if(!it.isNullOrBlank()) it + "" else "" }
val msg = p0.getOrDefault<String?>(config, "msg", "Promise Exception", null)
?: p0.getOrDefault(config, "message", "Promise Exception", "");
return Throwable("Promise Failed: " + pluginType + msg);
*/
}
else if(p0 is V8ValueString)
return Throwable("Promise Failed:" + p0.value);
else
return NotImplementedError("onCatch promise not implemented..");
}
class V8Deferred<T>(val deferred: Deferred<T>, val estDuration: Int = -1): Deferred<T> by deferred {
fun <R> convert(conversion: (result: T)->R): V8Deferred<R>{
val newDef = CompletableDeferred<R>()
this.invokeOnCompletion {
if(it != null)
newDef.completeExceptionally(it);
else
newDef.complete(conversion(this@V8Deferred.getCompleted()));
}
return V8Deferred<R>(newDef, estDuration);
}
companion object {
fun <T, R> merge(scope: CoroutineScope, defs: List<V8Deferred<T>>, conversion: (result: List<T>)->R): V8Deferred<R> {
var amount = -1;
for(def in defs)
amount = Math.max(amount, def.estDuration);
val def = scope.async {
val results = defs.map { it.await() };
return@async conversion(results);
}
return V8Deferred(def, amount);
}
}
}
fun <T: V8Value> V8ValueObject.invokeV8(method: String, vararg obj: Any?): T {
var result = this.invoke<V8Value>(method, *obj);
if(result is V8ValuePromise) {
return result.toV8ValueBlocking(this.getSourcePlugin()!!);
}
return result as T;
}
fun <T: V8Value> V8ValueObject.invokeV8Async(method: String, vararg obj: Any?): V8Deferred<T> {
var result = this.invoke<V8Value>(method, *obj);
if(result is V8ValuePromise) {
return result.toV8ValueAsync(this.getSourcePlugin()!!);
}
return V8Deferred(CompletableDeferred(result as T));
}
fun V8ValueObject.invokeV8Void(method: String, vararg obj: Any?): V8Value {
var result = this.invoke<V8Value>(method, *obj);
if(result is V8ValuePromise) {
return result.toV8ValueBlocking(this.getSourcePlugin()!!);
}
return result;
}
fun V8ValueObject.invokeV8VoidAsync(method: String, vararg obj: Any?): V8Deferred<V8Value> {
var result = this.invoke<V8Value>(method, *obj);
if(result is V8ValuePromise) {
val result = result.toV8ValueAsync<V8Value>(this.getSourcePlugin()!!);
return result;
}
return V8Deferred(CompletableDeferred(result));
}
suspend fun <T> Deferred<T>.awaitCancelConverted(): T {
try {
return this.await();
}
catch(ex: CancellationException) {
if(ex.cause != null) {
throw ex.cause!!;
}
throw ex;
}
}
fun <T> IPager<T>.toList(): List<T> {
val list = this.getResults().toMutableList();
while(this.hasMorePages()) {
this.nextPage();
list.addAll(this.getResults());
}
return list.toList();
} }
@@ -1,118 +0,0 @@
package com.futo.platformplayer
import android.app.Activity
import android.graphics.Color
import android.os.Build
import android.view.ViewGroup
import android.view.Window
import android.view.WindowManager
import androidx.core.graphics.Insets
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsCompat.Type
import androidx.core.view.WindowInsetsControllerCompat
import androidx.core.view.doOnAttach
import androidx.core.view.updatePadding
import kotlin.math.max
class RootInsetsController private constructor(
private val activity: Activity,
private val window: Window,
private val root: ViewGroup
) {
private val controller by lazy { WindowInsetsControllerCompat(window, root) }
private val basePaddingLeft = root.paddingLeft
private val basePaddingTop = root.paddingTop
private val basePaddingRight = root.paddingRight
private val basePaddingBottom = root.paddingBottom
private var currentInsets: WindowInsetsCompat = WindowInsetsCompat.CONSUMED
private var fullscreen = false
init {
WindowCompat.setDecorFitsSystemWindows(window, false)
window.statusBarColor = Color.TRANSPARENT
window.navigationBarColor = Color.TRANSPARENT
controller.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
ViewCompat.setOnApplyWindowInsetsListener(root) { _, insets ->
currentInsets = insets
applyPadding()
insets
}
root.doOnAttach { ViewCompat.requestApplyInsets(root) }
}
private fun effectiveInsets(): Insets {
if (fullscreen) return Insets.NONE
val sys = currentInsets.getInsets(Type.systemBars())
val cut = currentInsets.getInsetsIgnoringVisibility(Type.displayCutout())
val portrait = activity.resources.configuration.orientation == android.content.res.Configuration.ORIENTATION_PORTRAIT
val top = if (portrait) max(sys.top, cut.top) else sys.top
return Insets.of(sys.left, top, sys.right, sys.bottom)
}
private fun applyPadding() {
val e = effectiveInsets()
root.updatePadding(
left = basePaddingLeft + e.left,
top = basePaddingTop + e.top,
right = basePaddingRight + e.right,
bottom = basePaddingBottom + e.bottom
)
}
private fun forceRelayoutAndInsets() {
root.post {
ViewCompat.requestApplyInsets(root)
applyPadding()
root.post {
ViewCompat.requestApplyInsets(root)
applyPadding()
}
}
}
fun enterFullscreen(allowCutoutShortEdges: Boolean = true) {
fullscreen = true
if (allowCutoutShortEdges) {
window.attributes = window.attributes.apply {
layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
}
}
controller.hide(Type.systemBars())
forceRelayoutAndInsets()
}
fun exitFullscreen() {
fullscreen = false
window.attributes = window.attributes.apply {
layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
}
controller.show(Type.systemBars())
forceRelayoutAndInsets()
}
fun onConfigurationChanged() {
forceRelayoutAndInsets()
}
fun setLightSystemBarAppearance(lightStatus: Boolean, lightNav: Boolean) {
controller.isAppearanceLightStatusBars = lightStatus
controller.isAppearanceLightNavigationBars = lightNav
}
companion object {
fun attach(activity: Activity, root: ViewGroup): RootInsetsController {
return RootInsetsController(activity, activity.window, root)
}
}
}
@@ -10,11 +10,11 @@ import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.activities.ManageTabsActivity import com.futo.platformplayer.activities.ManageTabsActivity
import com.futo.platformplayer.activities.PolycentricHomeActivity import com.futo.platformplayer.activities.PolycentricHomeActivity
import com.futo.platformplayer.activities.PolycentricProfileActivity import com.futo.platformplayer.activities.PolycentricProfileActivity
import com.futo.platformplayer.activities.SettingsActivity
import com.futo.platformplayer.activities.SyncHomeActivity import com.futo.platformplayer.activities.SyncHomeActivity
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
import com.futo.platformplayer.fragment.mainactivity.main.SettingsFragment
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.serializers.FlexibleBooleanSerializer import com.futo.platformplayer.serializers.FlexibleBooleanSerializer
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
@@ -25,23 +25,21 @@ import com.futo.platformplayer.states.StateCache
import com.futo.platformplayer.states.StateMeta import com.futo.platformplayer.states.StateMeta
import com.futo.platformplayer.states.StatePayment import com.futo.platformplayer.states.StatePayment
import com.futo.platformplayer.states.StatePolycentric import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.states.StateSync
import com.futo.platformplayer.states.StateUpdate import com.futo.platformplayer.states.StateUpdate
import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.FragmentedStorageFileJson import com.futo.platformplayer.stores.FragmentedStorageFileJson
import com.futo.platformplayer.views.FeedStyle import com.futo.platformplayer.views.FeedStyle
import com.futo.platformplayer.views.fields.AdvancedField
import com.futo.platformplayer.views.fields.DropdownFieldOptionsId import com.futo.platformplayer.views.fields.DropdownFieldOptionsId
import com.futo.platformplayer.views.fields.FieldForm import com.futo.platformplayer.views.fields.FieldForm
import com.futo.platformplayer.views.fields.FormField import com.futo.platformplayer.views.fields.FormField
import com.futo.platformplayer.views.fields.FormFieldButton import com.futo.platformplayer.views.fields.FormFieldButton
import com.futo.platformplayer.views.fields.FormFieldWarning
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient import kotlinx.serialization.Transient
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.io.File import java.io.File
import java.time.OffsetDateTime import java.time.OffsetDateTime
@@ -63,7 +61,7 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.sync_grayjay, FieldForm.BUTTON, R.string.sync_grayjay_description, -8) @FormField(R.string.sync_grayjay, FieldForm.BUTTON, R.string.sync_grayjay_description, -8)
@FormFieldButton(R.drawable.ic_update) @FormFieldButton(R.drawable.ic_update)
fun syncGrayjay() { fun syncGrayjay() {
StateApp?.instance?.activity?.let { SettingsActivity.getActivity()?.let {
it.startActivity(Intent(it, SyncHomeActivity::class.java)) it.startActivity(Intent(it, SyncHomeActivity::class.java))
} }
} }
@@ -72,7 +70,7 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.manage_polycentric_identity, FieldForm.BUTTON, R.string.manage_your_polycentric_identity, -7) @FormField(R.string.manage_polycentric_identity, FieldForm.BUTTON, R.string.manage_your_polycentric_identity, -7)
@FormFieldButton(R.drawable.ic_person) @FormFieldButton(R.drawable.ic_person)
fun managePolycentricIdentity() { fun managePolycentricIdentity() {
StateApp?.instance?.activity?.let { SettingsActivity.getActivity()?.let {
if (StatePolycentric.instance.enabled) { if (StatePolycentric.instance.enabled) {
if (StatePolycentric.instance.processHandle != null) { if (StatePolycentric.instance.processHandle != null) {
it.startActivity(Intent(it, PolycentricProfileActivity::class.java)); it.startActivity(Intent(it, PolycentricProfileActivity::class.java));
@@ -90,7 +88,7 @@ class Settings : FragmentedStorageFileJson() {
fun openFAQ() { fun openFAQ() {
try { try {
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(Settings.URL_FAQ)) val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(Settings.URL_FAQ))
StateApp?.instance?.activity?.startActivity(browserIntent); SettingsActivity.getActivity()?.startActivity(browserIntent);
} catch (e: Throwable) { } catch (e: Throwable) {
//Ignored //Ignored
} }
@@ -100,7 +98,7 @@ class Settings : FragmentedStorageFileJson() {
fun openIssues() { fun openIssues() {
try { try {
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/futo-org/grayjay-android/issues")) val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/futo-org/grayjay-android/issues"))
StateApp?.instance?.activity?.startActivity(browserIntent); SettingsActivity.getActivity()?.startActivity(browserIntent);
} catch (e: Throwable) { } catch (e: Throwable) {
//Ignored //Ignored
} }
@@ -131,7 +129,7 @@ class Settings : FragmentedStorageFileJson() {
@FormFieldButton(R.drawable.ic_tabs) @FormFieldButton(R.drawable.ic_tabs)
fun manageTabs() { fun manageTabs() {
try { try {
StateApp?.instance?.activity?.let { SettingsActivity.getActivity()?.let {
it.startActivity(Intent(it, ManageTabsActivity::class.java)); it.startActivity(Intent(it, ManageTabsActivity::class.java));
} }
} catch (e: Throwable) { } catch (e: Throwable) {
@@ -144,7 +142,7 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.import_data, FieldForm.BUTTON, R.string.import_data_description, -3) @FormField(R.string.import_data, FieldForm.BUTTON, R.string.import_data_description, -3)
@FormFieldButton(R.drawable.ic_move_up) @FormFieldButton(R.drawable.ic_move_up)
fun import() { fun import() {
val act = StateApp.instance.activity ?: return; val act = SettingsActivity.getActivity() ?: return;
val intent = MainActivity.getImportOptionsIntent(act); val intent = MainActivity.getImportOptionsIntent(act);
act.startActivity(intent); act.startActivity(intent);
} }
@@ -153,7 +151,7 @@ class Settings : FragmentedStorageFileJson() {
@FormFieldButton(R.drawable.ic_link) @FormFieldButton(R.drawable.ic_link)
fun manageLinks() { fun manageLinks() {
try { try {
StateApp.instance.activity?.let { UIDialogs.showUrlHandlingPrompt(it) } SettingsActivity.getActivity()?.let { UIDialogs.showUrlHandlingPrompt(it) }
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.e(TAG, "Failed to show url handling prompt", e) Logger.e(TAG, "Failed to show url handling prompt", e)
} }
@@ -162,7 +160,7 @@ class Settings : FragmentedStorageFileJson() {
/*@FormField(R.string.disable_battery_optimization, FieldForm.BUTTON, R.string.click_to_go_to_battery_optimization_settings_disabling_battery_optimization_will_prevent_the_os_from_killing_media_sessions, -1) /*@FormField(R.string.disable_battery_optimization, FieldForm.BUTTON, R.string.click_to_go_to_battery_optimization_settings_disabling_battery_optimization_will_prevent_the_os_from_killing_media_sessions, -1)
@FormFieldButton(R.drawable.battery_full_24px) @FormFieldButton(R.drawable.battery_full_24px)
fun ignoreBatteryOptimization() { fun ignoreBatteryOptimization() {
StateApp.instance.activity?.let { SettingsActivity.getActivity()?.let {
val intent = Intent() val intent = Intent()
val packageName = it.packageName val packageName = it.packageName
val pm = it.getSystemService(POWER_SERVICE) as PowerManager; val pm = it.getSystemService(POWER_SERVICE) as PowerManager;
@@ -177,10 +175,6 @@ class Settings : FragmentedStorageFileJson() {
} }
}*/ }*/
@FormField(R.string.advanced_settings, FieldForm.TOGGLE, R.string.advanced_settings_description, -1, "advancedSettings")
var advancedSettings: Boolean = false;
@FormField(R.string.language, "group", -1, 0) @FormField(R.string.language, "group", -1, 0)
var language = LanguageSettings(); var language = LanguageSettings();
@Serializable @Serializable
@@ -202,8 +196,6 @@ class Settings : FragmentedStorageFileJson() {
8 -> "zh"; 8 -> "zh";
9 -> "ru"; 9 -> "ru";
10 -> "ar"; 10 -> "ar";
11 -> "it";
12 -> "tr";
else -> null else -> null
} }
} }
@@ -229,11 +221,10 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.show_home_filters_plugin_names, FieldForm.TOGGLE, R.string.show_home_filters_plugin_names_description, 5) @FormField(R.string.show_home_filters_plugin_names, FieldForm.TOGGLE, R.string.show_home_filters_plugin_names_description, 5)
var showHomeFiltersPluginNames: Boolean = false; var showHomeFiltersPluginNames: Boolean = false;
@AdvancedField
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6) @FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
var previewFeedItems: Boolean = true; var previewFeedItems: Boolean = true;
@AdvancedField
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6) @FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
var progressBar: Boolean = true; var progressBar: Boolean = true;
@@ -243,7 +234,7 @@ class Settings : FragmentedStorageFileJson() {
fun clearHidden() { fun clearHidden() {
StateMeta.instance.removeAllHiddenCreators(); StateMeta.instance.removeAllHiddenCreators();
StateMeta.instance.removeAllHiddenVideos(); StateMeta.instance.removeAllHiddenVideos();
StateApp.instance.activity?.let { SettingsActivity.getActivity()?.let {
UIDialogs.toast(it, "Creators and videos should show up again"); UIDialogs.toast(it, "Creators and videos should show up again");
} }
} }
@@ -262,11 +253,9 @@ class Settings : FragmentedStorageFileJson() {
@DropdownFieldOptionsId(R.array.feed_style) @DropdownFieldOptionsId(R.array.feed_style)
var searchFeedStyle: Int = 1; var searchFeedStyle: Int = 1;
@AdvancedField
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 5) @FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 5)
var previewFeedItems: Boolean = true; var previewFeedItems: Boolean = true;
@AdvancedField
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6) @FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
var progressBar: Boolean = true; var progressBar: Boolean = true;
@@ -288,7 +277,6 @@ class Settings : FragmentedStorageFileJson() {
@Serializable @Serializable
class ChannelSettings { class ChannelSettings {
@AdvancedField
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6) @FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
var progressBar: Boolean = true; var progressBar: Boolean = true;
} }
@@ -314,20 +302,16 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.use_subscription_exchange, FieldForm.TOGGLE, R.string.use_subscription_exchange_description, 6) @FormField(R.string.use_subscription_exchange, FieldForm.TOGGLE, R.string.use_subscription_exchange_description, 6)
var useSubscriptionExchange: Boolean = false; var useSubscriptionExchange: Boolean = false;
@AdvancedField
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6) @FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
var previewFeedItems: Boolean = true; var previewFeedItems: Boolean = true;
@AdvancedField
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 7) @FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 7)
var progressBar: Boolean = true; var progressBar: Boolean = true;
@AdvancedField
@FormField(R.string.fetch_on_app_boot, FieldForm.TOGGLE, R.string.shortly_after_opening_the_app_start_fetching_subscriptions, 8) @FormField(R.string.fetch_on_app_boot, FieldForm.TOGGLE, R.string.shortly_after_opening_the_app_start_fetching_subscriptions, 8)
@Serializable(with = FlexibleBooleanSerializer::class) @Serializable(with = FlexibleBooleanSerializer::class)
var fetchOnAppBoot: Boolean = true; var fetchOnAppBoot: Boolean = true;
@AdvancedField
@FormField(R.string.fetch_on_tab_opened, FieldForm.TOGGLE, R.string.fetch_on_tab_opened_description, 9) @FormField(R.string.fetch_on_tab_opened, FieldForm.TOGGLE, R.string.fetch_on_tab_opened_description, 9)
var fetchOnTabOpen: Boolean = true; var fetchOnTabOpen: Boolean = true;
@@ -358,24 +342,21 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.show_watch_metrics, FieldForm.TOGGLE, R.string.show_watch_metrics_description, 12) @FormField(R.string.show_watch_metrics, FieldForm.TOGGLE, R.string.show_watch_metrics_description, 12)
var showWatchMetrics: Boolean = false; var showWatchMetrics: Boolean = false;
@AdvancedField
@FormField(R.string.track_playtime_locally, FieldForm.TOGGLE, R.string.track_playtime_locally_description, 13) @FormField(R.string.track_playtime_locally, FieldForm.TOGGLE, R.string.track_playtime_locally_description, 13)
var allowPlaytimeTracking: Boolean = true; var allowPlaytimeTracking: Boolean = true;
@AdvancedField
@FormField(R.string.always_reload_from_cache, FieldForm.TOGGLE, R.string.always_reload_from_cache_description, 14) @FormField(R.string.always_reload_from_cache, FieldForm.TOGGLE, R.string.always_reload_from_cache_description, 14)
var alwaysReloadFromCache: Boolean = false; var alwaysReloadFromCache: Boolean = false;
@AdvancedField
@FormField(R.string.peek_channel_contents, FieldForm.TOGGLE, R.string.peek_channel_contents_description, 15) @FormField(R.string.peek_channel_contents, FieldForm.TOGGLE, R.string.peek_channel_contents_description, 15)
var peekChannelContents: Boolean = false; var peekChannelContents: Boolean = false;
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON, R.string.clear_channel_cache_description, 16) @FormField(R.string.clear_channel_cache, FieldForm.BUTTON, R.string.clear_channel_cache_description, 16)
fun clearChannelCache() { fun clearChannelCache() {
UIDialogs.toast(StateApp.instance.activity!!, "Started clearing.."); UIDialogs.toast(SettingsActivity.getActivity()!!, "Started clearing..");
StateCache.instance.clear(); StateCache.instance.clear();
UIDialogs.toast(StateApp.instance.activity!!, "Finished clearing"); UIDialogs.toast(SettingsActivity.getActivity()!!, "Finished clearing");
} }
} }
@@ -407,10 +388,6 @@ class Settings : FragmentedStorageFileJson() {
else -> null else -> null
} }
} }
@FormField(R.string.sticky_subtitles, FieldForm.TOGGLE, R.string.sticky_subtitles_description, -1)
var stickySubtitles: Boolean = true;
@FormField(R.string.prefer_original_audio, FieldForm.TOGGLE, R.string.prefer_original_audio_description, -1) @FormField(R.string.prefer_original_audio, FieldForm.TOGGLE, R.string.prefer_original_audio_description, -1)
var preferOriginalAudio: Boolean = true; var preferOriginalAudio: Boolean = true;
@@ -429,9 +406,6 @@ class Settings : FragmentedStorageFileJson() {
6 -> 1.75f; 6 -> 1.75f;
7 -> 2.0f; 7 -> 2.0f;
8 -> 2.25f; 8 -> 2.25f;
9 -> 2.5f;
10 -> 2.75f;
11 -> 3.0f;
else -> 1.0f; else -> 1.0f;
}; };
@@ -451,11 +425,9 @@ class Settings : FragmentedStorageFileJson() {
var preferredPreviewQuality: Int = 5; var preferredPreviewQuality: Int = 5;
fun getPreferredPreviewQualityPixelCount(): Int = preferedQualityToPixels(preferredPreviewQuality); fun getPreferredPreviewQualityPixelCount(): Int = preferedQualityToPixels(preferredPreviewQuality);
@AdvancedField
@FormField(R.string.simplify_sources, FieldForm.TOGGLE, R.string.simplify_sources_description, 4) @FormField(R.string.simplify_sources, FieldForm.TOGGLE, R.string.simplify_sources_description, 4)
var simplifySources: Boolean = true; var simplifySources: Boolean = true;
@AdvancedField
@FormField(R.string.always_allow_reverse_landscape_auto_rotate, FieldForm.TOGGLE, R.string.always_allow_reverse_landscape_auto_rotate_description, 5) @FormField(R.string.always_allow_reverse_landscape_auto_rotate, FieldForm.TOGGLE, R.string.always_allow_reverse_landscape_auto_rotate_description, 5)
var alwaysAllowReverseLandscapeAutoRotate: Boolean = true var alwaysAllowReverseLandscapeAutoRotate: Boolean = true
@@ -466,7 +438,6 @@ class Settings : FragmentedStorageFileJson() {
fun isBackgroundContinue() = backgroundPlay == 1; fun isBackgroundContinue() = backgroundPlay == 1;
fun isBackgroundPictureInPicture() = backgroundPlay == 2; fun isBackgroundPictureInPicture() = backgroundPlay == 2;
@AdvancedField
@FormField(R.string.resume_after_preview, FieldForm.DROPDOWN, R.string.when_watching_a_video_in_preview_mode_resume_at_the_position_when_opening_the_video_code, 7) @FormField(R.string.resume_after_preview, FieldForm.DROPDOWN, R.string.when_watching_a_video_in_preview_mode_resume_at_the_position_when_opening_the_video_code, 7)
@DropdownFieldOptionsId(R.array.resume_after_preview) @DropdownFieldOptionsId(R.array.resume_after_preview)
var resumeAfterPreview: Int = 1; var resumeAfterPreview: Int = 1;
@@ -493,10 +464,14 @@ class Settings : FragmentedStorageFileJson() {
}; };
} }
@AdvancedField
@FormField(R.string.live_chat_webview, FieldForm.TOGGLE, R.string.use_the_live_chat_web_window_when_available_over_native_implementation, 9) @FormField(R.string.live_chat_webview, FieldForm.TOGGLE, R.string.use_the_live_chat_web_window_when_available_over_native_implementation, 9)
var useLiveChatWindow: Boolean = true; var useLiveChatWindow: Boolean = true;
@FormField(R.string.background_switch_audio, FieldForm.TOGGLE, R.string.background_switch_audio_description, 10)
var backgroundSwitchToAudio: Boolean = true;
@FormField(R.string.restart_after_audio_focus_loss, FieldForm.DROPDOWN, R.string.restart_playback_when_gaining_audio_focus_after_a_loss, 11) @FormField(R.string.restart_after_audio_focus_loss, FieldForm.DROPDOWN, R.string.restart_playback_when_gaining_audio_focus_after_a_loss, 11)
@DropdownFieldOptionsId(R.array.restart_playback_after_loss) @DropdownFieldOptionsId(R.array.restart_playback_after_loss)
var restartPlaybackAfterLoss: Int = 1; var restartPlaybackAfterLoss: Int = 1;
@@ -522,107 +497,8 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.autoplay, FieldForm.TOGGLE, R.string.autoplay, 21) @FormField(R.string.autoplay, FieldForm.TOGGLE, R.string.autoplay, 21)
var autoplay: Boolean = false; var autoplay: Boolean = false;
@AdvancedField
@FormField(R.string.delete_watchlist_on_finish, FieldForm.TOGGLE, R.string.delete_watchlist_on_finish_description, 22) @FormField(R.string.delete_watchlist_on_finish, FieldForm.TOGGLE, R.string.delete_watchlist_on_finish_description, 22)
var deleteFromWatchLaterAuto: Boolean = true; var deleteFromWatchLaterAuto: Boolean = true;
@FormField(R.string.seek_offset, FieldForm.DROPDOWN, R.string.seek_offset_description, 23)
@DropdownFieldOptionsId(R.array.seek_offset_duration)
var seekOffset: Int = 2;
fun getSeekOffset(): Long {
return when(seekOffset) {
0 -> 3_000L;
1 -> 5_000L;
2 -> 10_000L;
3 -> 20_000L;
4 -> 30_000L;
5 -> 60_000L;
else -> 10_000L;
}
}
@FormField(R.string.min_playback_speed, FieldForm.DROPDOWN, R.string.min_playback_speed_description, 25)
@DropdownFieldOptionsId(R.array.min_playback_speed)
var minimumPlaybackSpeed: Int = 0;
@FormField(R.string.max_playback_speed, FieldForm.DROPDOWN, R.string.max_playback_speed_description, 26)
@DropdownFieldOptionsId(R.array.max_playback_speed)
var maximumPlaybackSpeed: Int = 2;
@FormField(R.string.step_playback_speed, FieldForm.DROPDOWN, R.string.step_playback_speed_description, 26)
@DropdownFieldOptionsId(R.array.step_playback_speed)
var stepPlaybackSpeed: Int = 1;
fun getPlaybackSpeedStep(): Double {
return when(stepPlaybackSpeed) {
0 -> 0.05
1 -> 0.1
2 -> 0.25
else -> 0.1;
}
}
fun getPlaybackSpeeds(): List<Double> {
val playbackSpeeds = mutableListOf<Double>();
playbackSpeeds.add(1.0);
val minSpeed = when(minimumPlaybackSpeed) {
0 -> 0.25
1 -> 0.5
2 -> 1.0
else -> 0.25
}
val maxSpeed = when(maximumPlaybackSpeed) {
0 -> 2.0
1 -> 2.25
2 -> 3.0
3 -> 4.0
4 -> 5.0
else -> 2.25;
}
var testSpeed = 1.0;
while(testSpeed > minSpeed) {
val nextSpeed = (testSpeed - 0.25) as Double;
testSpeed = Math.max(nextSpeed, minSpeed);
playbackSpeeds.add(testSpeed);
}
testSpeed = 1.0;
while(testSpeed < maxSpeed) {
val nextSpeed = (testSpeed + if(testSpeed < 2) 0.25 else 1.0) as Double;
testSpeed = Math.min(nextSpeed, maxSpeed);
playbackSpeeds.add(testSpeed);
}
playbackSpeeds.sort();
return playbackSpeeds;
}
@FormField(R.string.hold_playback_speed, FieldForm.DROPDOWN, R.string.hold_playback_speed_description, 27)
@DropdownFieldOptionsId(R.array.hold_playback_speeds)
var holdPlaybackSpeed: Int = 4;
fun getHoldPlaybackSpeed(): Double {
return when(holdPlaybackSpeed) {
0 -> 1.0
1 -> 1.25
2 -> 1.5
3 -> 1.75
4 -> 2.0
5 -> 2.25
6 -> 2.5
7 -> 2.75
8 -> 3.0
else -> 2.0
}
}
@AdvancedField
@FormField(R.string.shorts_pregenerate, FieldForm.TOGGLE, R.string.shorts_pregenerate_description, 28)
var shortsPregenerate: Boolean = false;
@AdvancedField
@FormField(R.string.shorts_fit_video, FieldForm.TOGGLE, R.string.shorts_fit_video_description, 29)
@FormFieldWarning(R.string.shorts_fit_video_warning)
var shortsFitVideo: Boolean = false;
} }
@FormField(R.string.comments, "group", R.string.comments_description, 6) @FormField(R.string.comments, "group", R.string.comments_description, 6)
@@ -638,7 +514,6 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.default_recommendations, FieldForm.TOGGLE, R.string.default_recommendations_description, 0) @FormField(R.string.default_recommendations, FieldForm.TOGGLE, R.string.default_recommendations_description, 0)
var recommendationsDefault: Boolean = false; var recommendationsDefault: Boolean = false;
@AdvancedField
@FormField(R.string.hide_recommendations, FieldForm.TOGGLE, R.string.hide_recommendations_description, 0) @FormField(R.string.hide_recommendations, FieldForm.TOGGLE, R.string.hide_recommendations_description, 0)
var hideRecommendations: Boolean = false; var hideRecommendations: Boolean = false;
@@ -675,12 +550,10 @@ class Settings : FragmentedStorageFileJson() {
var preferredAudioQuality: Int = 1; var preferredAudioQuality: Int = 1;
fun isHighBitrateDefault(): Boolean = preferredAudioQuality > 0; fun isHighBitrateDefault(): Boolean = preferredAudioQuality > 0;
@AdvancedField
@FormField(R.string.byte_range_download, FieldForm.TOGGLE, R.string.attempt_to_utilize_byte_ranges, 4) @FormField(R.string.byte_range_download, FieldForm.TOGGLE, R.string.attempt_to_utilize_byte_ranges, 4)
@Serializable(with = FlexibleBooleanSerializer::class) @Serializable(with = FlexibleBooleanSerializer::class)
var byteRangeDownload: Boolean = true; var byteRangeDownload: Boolean = true;
@AdvancedField
@FormField(R.string.byte_range_concurrency, FieldForm.DROPDOWN, R.string.number_of_concurrent_threads_to_multiply_download_speeds_from_throttled_sources, 5) @FormField(R.string.byte_range_concurrency, FieldForm.DROPDOWN, R.string.number_of_concurrent_threads_to_multiply_download_speeds_from_throttled_sources, 5)
@DropdownFieldOptionsId(R.array.thread_count) @DropdownFieldOptionsId(R.array.thread_count)
var byteRangeConcurrency: Int = 3; var byteRangeConcurrency: Int = 3;
@@ -710,25 +583,14 @@ class Settings : FragmentedStorageFileJson() {
@Serializable(with = FlexibleBooleanSerializer::class) @Serializable(with = FlexibleBooleanSerializer::class)
var keepScreenOn: Boolean = true; var keepScreenOn: Boolean = true;
@AdvancedField
@FormField(R.string.always_proxy_requests, FieldForm.TOGGLE, R.string.always_proxy_requests_description, 3) @FormField(R.string.always_proxy_requests, FieldForm.TOGGLE, R.string.always_proxy_requests_description, 3)
@Serializable(with = FlexibleBooleanSerializer::class) @Serializable(with = FlexibleBooleanSerializer::class)
var alwaysProxyRequests: Boolean = false; var alwaysProxyRequests: Boolean = false;
@AdvancedField
@FormField(R.string.allow_ipv6, FieldForm.TOGGLE, R.string.allow_ipv6_description, 4) @FormField(R.string.allow_ipv6, FieldForm.TOGGLE, R.string.allow_ipv6_description, 4)
@Serializable(with = FlexibleBooleanSerializer::class) @Serializable(with = FlexibleBooleanSerializer::class)
var allowIpv6: Boolean = true; var allowIpv6: Boolean = false;
@AdvancedField
@FormField(R.string.allow_ipv4, FieldForm.TOGGLE, R.string.allow_ipv4_description, 5)
@Serializable(with = FlexibleBooleanSerializer::class)
var allowLinkLocalIpv4: Boolean = false;
@AdvancedField
@FormField(R.string.experimental_cast, FieldForm.TOGGLE, R.string.experimental_cast_description, 6)
@Serializable(with = FlexibleBooleanSerializer::class)
var experimentalCasting: Boolean = true
/*TODO: Should we have a different casting quality? /*TODO: Should we have a different casting quality?
@FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3) @FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3)
@@ -762,7 +624,7 @@ class Settings : FragmentedStorageFileJson() {
try { try {
if (!Logger.submitLogs()) { if (!Logger.submitLogs()) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
StateApp.instance.activity?.let { UIDialogs.toast(it, it.getString(R.string.please_enable_logging_to_submit_logs)) } SettingsActivity.getActivity()?.let { UIDialogs.toast(it, it.getString(R.string.please_enable_logging_to_submit_logs)) }
} }
} }
} catch (e: Throwable) { } catch (e: Throwable) {
@@ -779,7 +641,7 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.reset_announcements, FieldForm.BUTTON, R.string.reset_hidden_announcements, 1) @FormField(R.string.reset_announcements, FieldForm.BUTTON, R.string.reset_hidden_announcements, 1)
fun resetAnnouncements() { fun resetAnnouncements() {
StateAnnouncement.instance.resetAnnouncements(); StateAnnouncement.instance.resetAnnouncements();
StateApp.instance.activity?.let { UIDialogs.toast(it, it.getString(R.string.announcements_reset)); }; SettingsActivity.getActivity()?.let { UIDialogs.toast(it, it.getString(R.string.announcements_reset)); };
} }
} }
@@ -797,11 +659,9 @@ class Settings : FragmentedStorageFileJson() {
@Serializable @Serializable
class Plugins { class Plugins {
@AdvancedField
@FormField(R.string.check_disabled_plugin_updates, FieldForm.TOGGLE, R.string.check_disabled_plugin_updates_description, -1) @FormField(R.string.check_disabled_plugin_updates, FieldForm.TOGGLE, R.string.check_disabled_plugin_updates_description, -1)
var checkDisabledPluginsForUpdates: Boolean = false; var checkDisabledPluginsForUpdates: Boolean = false;
@AdvancedField
@FormField(R.string.clear_cookies_on_logout, FieldForm.TOGGLE, R.string.clears_cookies_when_you_log_out, 0) @FormField(R.string.clear_cookies_on_logout, FieldForm.TOGGLE, R.string.clears_cookies_when_you_log_out, 0)
var clearCookiesOnLogout: Boolean = true; var clearCookiesOnLogout: Boolean = true;
@@ -847,13 +707,13 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.change_external_general_directory, FieldForm.BUTTON, R.string.change_the_external_directory_for_general_files, 3) @FormField(R.string.change_external_general_directory, FieldForm.BUTTON, R.string.change_the_external_directory_for_general_files, 3)
fun changeStorageGeneral() { fun changeStorageGeneral() {
StateApp.instance.activity?.let { SettingsActivity.getActivity()?.let {
StateApp.instance.changeExternalGeneralDirectory(it); StateApp.instance.changeExternalGeneralDirectory(it);
} }
} }
@FormField(R.string.change_external_downloads_directory, FieldForm.BUTTON, R.string.change_the_external_storage_for_download_files, 4) @FormField(R.string.change_external_downloads_directory, FieldForm.BUTTON, R.string.change_the_external_storage_for_download_files, 4)
fun changeStorageDownload() { fun changeStorageDownload() {
StateApp.instance.activity?.let { SettingsActivity.getActivity()?.let {
StateApp.instance.changeExternalDownloadDirectory(it); StateApp.instance.changeExternalDownloadDirectory(it);
} }
} }
@@ -862,7 +722,7 @@ class Settings : FragmentedStorageFileJson() {
fun clearStorageDownload() { fun clearStorageDownload() {
Settings.instance.storage.storage_download = null; Settings.instance.storage.storage_download = null;
Settings.instance.save(); Settings.instance.save();
StateApp.instance.activity?.let { UIDialogs.toast(it, "Cleared download storage directory") }; SettingsActivity.getActivity()?.let { UIDialogs.toast(it, "Cleared download storage directory") };
} }
} }
@@ -899,13 +759,13 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.manual_check, FieldForm.BUTTON, R.string.manually_check_for_updates, 3) @FormField(R.string.manual_check, FieldForm.BUTTON, R.string.manually_check_for_updates, 3)
fun manualCheck() { fun manualCheck() {
if (!BuildConfig.IS_PLAYSTORE_BUILD) { if (!BuildConfig.IS_PLAYSTORE_BUILD) {
StateApp.instance.activity?.let { SettingsActivity.getActivity()?.let {
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
StateUpdate.instance.checkForUpdates(it, true) StateUpdate.instance.checkForUpdates(it, true)
} }
} }
} else { } else {
StateApp.instance.activity?.let { SettingsActivity.getActivity()?.let {
try { try {
it.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=${it.packageName}"))) it.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=${it.packageName}")))
} catch (e: ActivityNotFoundException) { } catch (e: ActivityNotFoundException) {
@@ -917,7 +777,7 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.view_changelog, FieldForm.BUTTON, R.string.review_the_current_and_past_changelogs, 4) @FormField(R.string.view_changelog, FieldForm.BUTTON, R.string.review_the_current_and_past_changelogs, 4)
fun viewChangelog() { fun viewChangelog() {
StateApp.instance.activity?.let { SettingsActivity.getActivity()?.let {
UIDialogs.toast(it.getString(R.string.retrieving_changelog)); UIDialogs.toast(it.getString(R.string.retrieving_changelog));
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
@@ -957,7 +817,7 @@ class Settings : FragmentedStorageFileJson() {
class Backup { class Backup {
@Serializable(with = OffsetDateTimeSerializer::class) @Serializable(with = OffsetDateTimeSerializer::class)
var lastAutoBackupTime: OffsetDateTime = OffsetDateTime.MIN; var lastAutoBackupTime: OffsetDateTime = OffsetDateTime.MIN;
var didAskAutoBackup: Boolean = true; var didAskAutoBackup: Boolean = false;
var autoBackupPassword: String? = null; var autoBackupPassword: String? = null;
fun shouldAutomaticBackup() = autoBackupPassword != null; fun shouldAutomaticBackup() = autoBackupPassword != null;
@@ -966,13 +826,13 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.set_automatic_backup, FieldForm.BUTTON, R.string.configure_daily_backup_in_case_of_catastrophic_failure, 1) @FormField(R.string.set_automatic_backup, FieldForm.BUTTON, R.string.configure_daily_backup_in_case_of_catastrophic_failure, 1)
fun configureAutomaticBackup() { fun configureAutomaticBackup() {
UIDialogs.showAutomaticBackupDialog(StateApp.instance.activity!!, autoBackupPassword != null) { UIDialogs.showAutomaticBackupDialog(SettingsActivity.getActivity()!!, autoBackupPassword != null) {
SettingsFragment.currentView?.reloadSettings(); SettingsActivity.getActivity()?.reloadSettings();
}; };
} }
@FormField(R.string.restore_automatic_backup, FieldForm.BUTTON, R.string.restore_a_previous_automatic_backup, 2) @FormField(R.string.restore_automatic_backup, FieldForm.BUTTON, R.string.restore_a_previous_automatic_backup, 2)
fun restoreAutomaticBackup() { fun restoreAutomaticBackup() {
val activity = StateApp.instance.activity!! val activity = SettingsActivity.getActivity()!!
if(!StateBackup.hasAutomaticBackup()) if(!StateBackup.hasAutomaticBackup())
UIDialogs.toast(activity, activity.getString(R.string.you_don_t_have_any_automatic_backups), false); UIDialogs.toast(activity, activity.getString(R.string.you_don_t_have_any_automatic_backups), false);
@@ -983,9 +843,8 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.export_data, FieldForm.BUTTON, R.string.creates_a_zip_file_with_your_data_which_can_be_imported_by_opening_it_with_grayjay, 3) @FormField(R.string.export_data, FieldForm.BUTTON, R.string.creates_a_zip_file_with_your_data_which_can_be_imported_by_opening_it_with_grayjay, 3)
fun export() { fun export() {
val activity = StateApp.instance.activity ?: return; val activity = SettingsActivity.getActivity() ?: return;
val fragView = SettingsFragment.currentView ?: return; UISlideOverlays.showOverlay(activity.overlay, "Select export type", null, {},
UISlideOverlays.showOverlay(fragView.overlay, "Select export type", null, {},
SlideUpMenuItem(activity, R.drawable.ic_share, "Share", "", tag = null, call = { SlideUpMenuItem(activity, R.drawable.ic_share, "Share", "", tag = null, call = {
StateBackup.shareExternalBackup(); StateBackup.shareExternalBackup();
}), }),
@@ -1001,32 +860,16 @@ class Settings : FragmentedStorageFileJson() {
@Serializable @Serializable
class Payment { class Payment {
@FormField(R.string.payment_status, FieldForm.READONLYTEXT, -1, 1) @FormField(R.string.payment_status, FieldForm.READONLYTEXT, -1, 1)
val paymentStatus: String get() = StateApp.instance.activity?.let { if (StatePayment.instance.hasPaid) it.getString(R.string.paid) else it.getString(R.string.not_paid); } ?: "Unknown"; val paymentStatus: String get() = SettingsActivity.getActivity()?.let { if (StatePayment.instance.hasPaid) it.getString(R.string.paid) else it.getString(R.string.not_paid); } ?: "Unknown";
@FormField(R.string.license_status, FieldForm.BUTTON, R.string.view_license_status, 2) @FormField(R.string.clear_payment, FieldForm.BUTTON, R.string.deletes_license_keys_from_app, 2)
fun viewLicenseStatus() {
StateApp.instance.activity?.let {
try {
if (StatePayment.instance.hasPaid) {
val paymentKey = StatePayment.instance.getPaymentKey()
UIDialogs.showDialogOk(it, R.drawable.ic_paid, "License activated\n" + paymentKey.first)
} else {
UIDialogs.showDialogOk(it, R.drawable.ic_paid, "No license activated")
}
} catch (e: Throwable) {
Logger.e(TAG, "Failed to show license status dialog", e)
}
}
}
@FormField(R.string.clear_payment, FieldForm.BUTTON, R.string.deletes_license_keys_from_app, 3)
fun clearPayment() { fun clearPayment() {
StateApp.instance.activity?.let { context -> SettingsActivity.getActivity()?.let { context ->
UIDialogs.showConfirmationDialog(context, "Are you sure you want to delete your license?", { UIDialogs.showConfirmationDialog(context, "Are you sure you want to delete your license?", {
StatePayment.instance.clearLicenses(); StatePayment.instance.clearLicenses();
StateApp.instance.activity?.let { SettingsActivity.getActivity()?.let {
UIDialogs.toast(it, it.getString(R.string.licenses_cleared_might_require_app_restart)); UIDialogs.toast(it, it.getString(R.string.licenses_cleared_might_require_app_restart));
SettingsFragment.currentView?.reloadSettings(); it.reloadSettings();
} }
}) })
} }
@@ -1037,23 +880,16 @@ class Settings : FragmentedStorageFileJson() {
var other = Other(); var other = Other();
@Serializable @Serializable
class Other { class Other {
@AdvancedField
@FormField(R.string.playlist_delete_confirmation, FieldForm.TOGGLE, R.string.playlist_delete_confirmation_description, 2) @FormField(R.string.playlist_delete_confirmation, FieldForm.TOGGLE, R.string.playlist_delete_confirmation_description, 2)
var playlistDeleteConfirmation: Boolean = true; var playlistDeleteConfirmation: Boolean = true;
@AdvancedField
@FormField(R.string.playlist_allow_dups, FieldForm.TOGGLE, R.string.playlist_allow_dups_description, 3) @FormField(R.string.playlist_allow_dups, FieldForm.TOGGLE, R.string.playlist_allow_dups_description, 3)
var playlistAllowDups: Boolean = true; var playlistAllowDups: Boolean = true;
@FormField(R.string.watch_later_add_start, FieldForm.TOGGLE, R.string.watch_later_add_start_description, 4) @FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 4)
var watchLaterAddStart: Boolean = true;
@FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 5)
var polycentricEnabled: Boolean = true; var polycentricEnabled: Boolean = true;
@FormField(R.string.polycentric_local_cache, FieldForm.TOGGLE, R.string.polycentric_local_cache_description, 7) @FormField(R.string.polycentric_local_cache, FieldForm.TOGGLE, R.string.polycentric_local_cache_description, 5)
var polycentricLocalCache: Boolean = true; var polycentricLocalCache: Boolean = true;
var showPrivacyModeDialog: Boolean = true;
} }
@FormField(R.string.gesture_controls, FieldForm.GROUP, -1, 19) @FormField(R.string.gesture_controls, FieldForm.GROUP, -1, 19)
@@ -1090,7 +926,7 @@ class Settings : FragmentedStorageFileJson() {
@Serializable @Serializable
class Synchronization { class Synchronization {
@FormField(R.string.enabled, FieldForm.TOGGLE, R.string.enabled_description, 1) @FormField(R.string.enabled, FieldForm.TOGGLE, R.string.enabled_description, 1)
var enabled: Boolean = false; var enabled: Boolean = true;
@FormField(R.string.broadcast, FieldForm.TOGGLE, R.string.broadcast_description, 1) @FormField(R.string.broadcast, FieldForm.TOGGLE, R.string.broadcast_description, 1)
var broadcast: Boolean = false; var broadcast: Boolean = false;
@@ -1100,54 +936,6 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.connect_last, FieldForm.TOGGLE, R.string.connect_last_description, 3) @FormField(R.string.connect_last, FieldForm.TOGGLE, R.string.connect_last_description, 3)
var connectLast: Boolean = true; var connectLast: Boolean = true;
@FormField(R.string.discover_through_relay, FieldForm.TOGGLE, R.string.discover_through_relay_description, 3)
var discoverThroughRelay: Boolean = true;
@FormField(R.string.pair_through_relay, FieldForm.TOGGLE, R.string.pair_through_relay_description, 3)
var pairThroughRelay: Boolean = true;
@FormField(R.string.connect_through_relay, FieldForm.TOGGLE, R.string.connect_through_relay_description, 3)
var connectThroughRelay: Boolean = true;
@FormField(R.string.connect_local_direct_through_relay, FieldForm.TOGGLE, R.string.connect_local_direct_through_relay_description, 3)
var connectLocalDirectThroughRelay: Boolean = true;
@FormField(R.string.local_connections, FieldForm.TOGGLE, R.string.local_connections_description, 3)
var localConnections: Boolean = true;
var syncServerUrl: String? = null;
@FormField(R.string.relay_server, FieldForm.READONLYTEXT, -1, 6)
val syncServer: String get() = if(syncServerUrl?.isBlank() == true) StateSync.RELAY_SERVER else syncServerUrl ?: StateSync.RELAY_SERVER;
@AdvancedField
@FormField(R.string.configure_sync_server, FieldForm.BUTTON, R.string.configure_sync_server_description, 7)
fun configureSyncServer() {
StateApp.instance.activity?.let { context ->
UIDialogs.showDialog(context, R.drawable.device_sync, false,
"Enter the url to your relay server",
"Using your own relay server requires a proper setup with portforwarding.\nUse at your own risk.",
null,
syncServerUrl ?: "",
"YourRelayServerDomain.com", 0,
UIDialogs.Action("Cancel", {}),
UIDialogs.Action("Reset", {
syncServerUrl = null;
instance.save();
SettingsFragment.currentView?.reloadSettings();
UIDialogs.toast("Sync server changes require a restart");
}, UIDialogs.ActionStyle.ACCENT),
UIDialogs.Action.withInput("Configure", {
syncServerUrl = it?.text
instance.save();
SettingsFragment.currentView?.reloadSettings();
UIDialogs.toast("Sync server changes require a restart");
}, UIDialogs.ActionStyle.PRIMARY),
)
}
}
} }
@FormField(R.string.info, FieldForm.GROUP, -1, 21) @FormField(R.string.info, FieldForm.GROUP, -1, 21)
@@ -1216,4 +1004,4 @@ class Settings : FragmentedStorageFileJson() {
} }
} }
//endregion //endregion
} }
@@ -8,7 +8,9 @@ import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager import androidx.work.WorkManager
import com.caoccao.javet.values.primitive.V8ValueInteger import com.caoccao.javet.values.primitive.V8ValueInteger
import com.caoccao.javet.values.primitive.V8ValueString import com.caoccao.javet.values.primitive.V8ValueString
import com.futo.platformplayer.activities.DeveloperActivity
import com.futo.platformplayer.activities.MainActivity import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.activities.SettingsActivity
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.models.contents.IPlatformContent import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.video.IPlatformVideo import com.futo.platformplayer.api.media.models.video.IPlatformVideo
@@ -18,8 +20,6 @@ import com.futo.platformplayer.api.media.platforms.js.SourcePluginDescriptor
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.background.BackgroundWorker import com.futo.platformplayer.background.BackgroundWorker
import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.fragment.mainactivity.main.DeveloperFragment
import com.futo.platformplayer.fragment.mainactivity.main.SettingsFragment
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.serializers.FlexibleBooleanSerializer import com.futo.platformplayer.serializers.FlexibleBooleanSerializer
import com.futo.platformplayer.states.StateAnnouncement import com.futo.platformplayer.states.StateAnnouncement
@@ -97,10 +97,10 @@ class SettingsDev : FragmentedStorageFileJson() {
fun subscriptionsCache5000() { fun subscriptionsCache5000() {
Logger.i("SettingsDev", "Started caching 5000 sub items"); Logger.i("SettingsDev", "Started caching 5000 sub items");
UIDialogs.toast( UIDialogs.toast(
StateApp.instance.activity!!, SettingsActivity.getActivity()!!,
"Started caching 5000 sub items" "Started caching 5000 sub items"
); );
val button = DeveloperFragment.currentView?.getField("subscription_cache_button"); val button = DeveloperActivity.getActivity()?.getField("subscription_cache_button");
if(button is ButtonField) if(button is ButtonField)
button.setButtonEnabled(false); button.setButtonEnabled(false);
StateApp.instance.scope.launch(Dispatchers.IO) { StateApp.instance.scope.launch(Dispatchers.IO) {
@@ -121,7 +121,7 @@ class SettingsDev : FragmentedStorageFileJson() {
val diff = System.currentTimeMillis() - lastToast; val diff = System.currentTimeMillis() - lastToast;
lastToast = System.currentTimeMillis(); lastToast = System.currentTimeMillis();
UIDialogs.toast( UIDialogs.toast(
StateApp.instance.activity!!, SettingsActivity.getActivity()!!,
"Page: ${page}, Total: ${total}, Speed: ${diff}ms" "Page: ${page}, Total: ${total}, Speed: ${diff}ms"
); );
} }
@@ -130,7 +130,7 @@ class SettingsDev : FragmentedStorageFileJson() {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
UIDialogs.toast( UIDialogs.toast(
StateApp.instance.activity!!, SettingsActivity.getActivity()!!,
"FINISHED Page: ${page}, Total: ${total}" "FINISHED Page: ${page}, Total: ${total}"
); );
} }
@@ -152,10 +152,10 @@ class SettingsDev : FragmentedStorageFileJson() {
fun historyCache100() { fun historyCache100() {
Logger.i("SettingsDev", "Started caching 100 history items (from home)"); Logger.i("SettingsDev", "Started caching 100 history items (from home)");
UIDialogs.toast( UIDialogs.toast(
StateApp.instance.activity!!, SettingsActivity.getActivity()!!,
"Started caching 100 history items (from home)" "Started caching 100 history items (from home)"
); );
val button = DeveloperFragment.currentView?.getField("history_cache_button"); val button = DeveloperActivity.getActivity()?.getField("history_cache_button");
if(button is ButtonField) if(button is ButtonField)
button.setButtonEnabled(false); button.setButtonEnabled(false);
StateApp.instance.scope.launch(Dispatchers.IO) { StateApp.instance.scope.launch(Dispatchers.IO) {
@@ -186,7 +186,7 @@ class SettingsDev : FragmentedStorageFileJson() {
val diff = System.currentTimeMillis() - lastToast; val diff = System.currentTimeMillis() - lastToast;
lastToast = System.currentTimeMillis(); lastToast = System.currentTimeMillis();
UIDialogs.toast( UIDialogs.toast(
StateApp.instance.activity!!, SettingsActivity.getActivity()!!,
"Page: ${page}, Total: ${total}, Speed: ${diff}ms" "Page: ${page}, Total: ${total}, Speed: ${diff}ms"
); );
} }
@@ -195,7 +195,7 @@ class SettingsDev : FragmentedStorageFileJson() {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
UIDialogs.toast( UIDialogs.toast(
StateApp.instance.activity!!, SettingsActivity.getActivity()!!,
"FINISHED Page: ${page}, Total: ${total}" "FINISHED Page: ${page}, Total: ${total}"
); );
} }
@@ -235,9 +235,9 @@ class SettingsDev : FragmentedStorageFileJson() {
@FormField(R.string.test_background_worker, FieldForm.BUTTON, @FormField(R.string.test_background_worker, FieldForm.BUTTON,
R.string.test_background_worker_description, 4) R.string.test_background_worker_description, 4)
fun triggerBackgroundUpdate() { fun triggerBackgroundUpdate() {
val act = StateApp.instance.activity!!; val act = SettingsActivity.getActivity()!!;
try { try {
UIDialogs.toast(StateApp.instance.activity!!, "Starting test background worker"); UIDialogs.toast(SettingsActivity.getActivity()!!, "Starting test background worker");
val wm = WorkManager.getInstance(act); val wm = WorkManager.getInstance(act);
val req = OneTimeWorkRequestBuilder<BackgroundWorker>() val req = OneTimeWorkRequestBuilder<BackgroundWorker>()
@@ -251,9 +251,9 @@ class SettingsDev : FragmentedStorageFileJson() {
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON, @FormField(R.string.clear_channel_cache, FieldForm.BUTTON,
R.string.test_background_worker_description, 4) R.string.test_background_worker_description, 4)
fun clearChannelContentCache() { fun clearChannelContentCache() {
UIDialogs.toast(StateApp.instance.activity!!, "Clearing cache"); UIDialogs.toast(SettingsActivity.getActivity()!!, "Clearing cache");
StateCache.instance.clearToday(); StateCache.instance.clearToday();
UIDialogs.toast(StateApp.instance.activity!!, "Cleared"); UIDialogs.toast(SettingsActivity.getActivity()!!, "Cleared");
} }
@@ -113,8 +113,8 @@ class UIDialogs {
currentDialog.code, currentDialog.code,
currentDialog.defaultCloseAction, currentDialog.defaultCloseAction,
*currentDialog.actions.map { *currentDialog.actions.map {
return@map Action.withInput(it.text, { str -> return@map Action(it.text, {
it.invokeAction(str); it.action();
multiShowDialog(context, dialogDescriptor.drop(1), finally); multiShowDialog(context, dialogDescriptor.drop(1), finally);
}, it.style); }, it.style);
}.toTypedArray()); }.toTypedArray());
@@ -203,9 +203,7 @@ class UIDialogs {
fun showDialog(context: Context, icon: Int, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action): AlertDialog { fun showDialog(context: Context, icon: Int, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action): AlertDialog {
return showDialog(context, icon, false, text, textDetails, code, defaultCloseAction, *actions); return showDialog(context, icon, false, text, textDetails, code, defaultCloseAction, *actions);
} }
fun showDialog(context: Context, icon: Int, animated: Boolean, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action): AlertDialog fun showDialog(context: Context, icon: Int, animated: Boolean, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action): AlertDialog {
= showDialog(context, icon, animated, text, textDetails, code, null, null, defaultCloseAction, *actions);
fun showDialog(context: Context, icon: Int, animated: Boolean, text: String, textDetails: String? = null, code: String? = null, input: String?, placeholder: String?, defaultCloseAction: Int, vararg actions: Action): AlertDialog {
val builder = AlertDialog.Builder(context); val builder = AlertDialog.Builder(context);
val view = LayoutInflater.from(context).inflate(R.layout.dialog_multi_button, null); val view = LayoutInflater.from(context).inflate(R.layout.dialog_multi_button, null);
builder.setView(view); builder.setView(view);
@@ -228,16 +226,6 @@ class UIDialogs {
this.text = textDetails; this.text = textDetails;
} }
}; };
var inputView = view.findViewById<TextView>(R.id.dialog_text_input);
inputView.apply {
if (input == null && placeholder == null) this.visibility = View.GONE;
else {
this.text = input ?: "";
this.hint = placeholder ?: "";
this.visibility = View.VISIBLE;
this.textAlignment = View.TEXT_ALIGNMENT_VIEW_START
}
};
view.findViewById<TextView>(R.id.dialog_text_code).apply { view.findViewById<TextView>(R.id.dialog_text_code).apply {
if (code == null) this.visibility = View.GONE; if (code == null) this.visibility = View.GONE;
else { else {
@@ -262,7 +250,7 @@ class UIDialogs {
buttonView.textSize = 14f; buttonView.textSize = 14f;
buttonView.typeface = resources.getFont(R.font.inter_regular); buttonView.typeface = resources.getFont(R.font.inter_regular);
buttonView.text = act.text; buttonView.text = act.text;
buttonView.setOnClickListener { act.invokeAction(DialogResult(inputView?.text?.toString())); dialog.dismiss(); }; buttonView.setOnClickListener { act.action(); dialog.dismiss(); };
when(act.style) { when(act.style) {
ActionStyle.PRIMARY -> buttonView.setBackgroundResource(R.drawable.background_button_primary); ActionStyle.PRIMARY -> buttonView.setBackgroundResource(R.drawable.background_button_primary);
ActionStyle.ACCENT -> buttonView.setBackgroundResource(R.drawable.background_button_accent); ActionStyle.ACCENT -> buttonView.setBackgroundResource(R.drawable.background_button_accent);
@@ -287,7 +275,7 @@ class UIDialogs {
}; };
dialog.setOnCancelListener { dialog.setOnCancelListener {
if(defaultCloseAction >= 0 && defaultCloseAction < actions.size) if(defaultCloseAction >= 0 && defaultCloseAction < actions.size)
actions[defaultCloseAction].invokeAction(DialogResult(inputView?.text?.toString())); actions[defaultCloseAction].action();
} }
dialog.setOnDismissListener { dialog.setOnDismissListener {
registerDialogClosed(dialog); registerDialogClosed(dialog);
@@ -331,11 +319,7 @@ class UIDialogs {
closeAction?.invoke() closeAction?.invoke()
}, UIDialogs.ActionStyle.NONE), }, UIDialogs.ActionStyle.NONE),
UIDialogs.Action(context.getString(R.string.retry), { UIDialogs.Action(context.getString(R.string.retry), {
try { retryAction?.invoke();
retryAction?.invoke();
} catch (e: Throwable) {
Logger.e(TAG, "Unhandled exception retrying", e)
}
}, UIDialogs.ActionStyle.PRIMARY) }, UIDialogs.ActionStyle.PRIMARY)
); );
else else
@@ -349,11 +333,7 @@ class UIDialogs {
closeAction?.invoke() closeAction?.invoke()
}, UIDialogs.ActionStyle.NONE), }, UIDialogs.ActionStyle.NONE),
UIDialogs.Action(context.getString(R.string.retry), { UIDialogs.Action(context.getString(R.string.retry), {
try { retryAction?.invoke();
retryAction?.invoke();
} catch (e: Throwable) {
Logger.e(TAG, "Unhandled exception retrying", e)
}
}, UIDialogs.ActionStyle.PRIMARY) }, UIDialogs.ActionStyle.PRIMARY)
); );
} }
@@ -370,19 +350,17 @@ class UIDialogs {
} }
fun showConfirmationDialog(context: Context, text: String, action: () -> Unit, cancelAction: (() -> Unit)? = null, dismissAction: (() -> Unit)? = null): AlertDialog { fun showConfirmationDialog(context: Context, text: String, action: () -> Unit, cancelAction: (() -> Unit)? = null) {
val confirmButtonAction = Action(context.getString(R.string.confirm), action, ActionStyle.PRIMARY) val confirmButtonAction = Action(context.getString(R.string.confirm), action, ActionStyle.PRIMARY)
val cancelButtonAction = Action(context.getString(R.string.cancel), cancelAction ?: {}, ActionStyle.ACCENT) val cancelButtonAction = Action(context.getString(R.string.cancel), cancelAction ?: {}, ActionStyle.ACCENT)
return showDialog(context, R.drawable.ic_error, text, null, null, 0, cancelButtonAction, confirmButtonAction).apply { showDialog(context, R.drawable.ic_error, text, null, null, 0, cancelButtonAction, confirmButtonAction)
setOnDismissListener { dismissAction?.invoke() }
}
} }
fun showConfirmationDialog(context: Context, text: String, action: () -> Unit, cancelAction: (() -> Unit)? = null, dismissAction: (() -> Unit)? = null, doNotAskAgainAction: (() -> Unit)? = null): AlertDialog { fun showConfirmationDialog(context: Context, text: String, action: () -> Unit, cancelAction: (() -> Unit)? = null, doNotAskAgainAction: (() -> Unit)? = null) {
val confirmButtonAction = Action(context.getString(R.string.confirm), action, ActionStyle.PRIMARY) val confirmButtonAction = Action(context.getString(R.string.confirm), action, ActionStyle.PRIMARY)
val cancelButtonAction = Action(context.getString(R.string.cancel), cancelAction ?: {}, ActionStyle.ACCENT) val cancelButtonAction = Action(context.getString(R.string.cancel), cancelAction ?: {}, ActionStyle.ACCENT)
val doNotAskAgain = Action(context.getString(R.string.do_not_ask_again), doNotAskAgainAction ?: {}, ActionStyle.NONE) val doNotAskAgain = Action(context.getString(R.string.do_not_ask_again), doNotAskAgainAction ?: {}, ActionStyle.NONE)
return showDialog(context, R.drawable.ic_error, text, null, null, 0, doNotAskAgain, cancelButtonAction, confirmButtonAction) showDialog(context, R.drawable.ic_error, text, null, null, 0, doNotAskAgain, cancelButtonAction, confirmButtonAction)
} }
fun showUpdateAvailableDialog(context: Context, lastVersion: Int, hideExceptionButtons: Boolean = false) { fun showUpdateAvailableDialog(context: Context, lastVersion: Int, hideExceptionButtons: Boolean = false) {
@@ -405,6 +383,13 @@ class UIDialogs {
dialog.setMaxVersion(lastVersion); dialog.setMaxVersion(lastVersion);
} }
fun showInstallDownloadedUpdateDialog(context: Context, apkFile: File) {
val dialog = AutoUpdateDialog(context);
registerDialogOpened(dialog);
dialog.setOnDismissListener { registerDialogClosed(dialog) };
dialog.showPredownloaded(apkFile);
}
fun showMigrateDialog(context: Context, store: ManagedStore<*>, onConcluded: ()->Unit) { fun showMigrateDialog(context: Context, store: ManagedStore<*>, onConcluded: ()->Unit) {
if(!store.hasMissingReconstructions()) if(!store.hasMissingReconstructions())
onConcluded(); onConcluded();
@@ -431,7 +416,7 @@ class UIDialogs {
} }
fun showCastingDialog(context: Context, ownerActivity: Activity? = null) { fun showCastingDialog(context: Context) {
val d = StateCasting.instance.activeDevice; val d = StateCasting.instance.activeDevice;
if (d != null) { if (d != null) {
val dialog = ConnectedCastingDialog(context); val dialog = ConnectedCastingDialog(context);
@@ -439,7 +424,6 @@ class UIDialogs {
dialog.setOwnerActivity(context) dialog.setOwnerActivity(context)
} }
registerDialogOpened(dialog); registerDialogOpened(dialog);
ownerActivity?.let { dialog.setOwnerActivity(it) }
dialog.setOnDismissListener { registerDialogClosed(dialog) }; dialog.setOnDismissListener { registerDialogClosed(dialog) };
dialog.show(); dialog.show();
} else { } else {
@@ -452,24 +436,21 @@ class UIDialogs {
if (c is Activity) { if (c is Activity) {
dialog.setOwnerActivity(c); dialog.setOwnerActivity(c);
} }
ownerActivity?.let { dialog.setOwnerActivity(it) }
dialog.setOnDismissListener { registerDialogClosed(dialog) }; dialog.setOnDismissListener { registerDialogClosed(dialog) };
dialog.show(); dialog.show();
} }
} }
fun showCastingTutorialDialog(context: Context, ownerActivity: Activity? = null) { fun showCastingTutorialDialog(context: Context) {
val dialog = CastingHelpDialog(context); val dialog = CastingHelpDialog(context);
registerDialogOpened(dialog); registerDialogOpened(dialog);
ownerActivity?.let { dialog.setOwnerActivity(it) }
dialog.setOnDismissListener { registerDialogClosed(dialog) }; dialog.setOnDismissListener { registerDialogClosed(dialog) };
dialog.show(); dialog.show();
} }
fun showCastingAddDialog(context: Context, ownerActivity: Activity? = null) { fun showCastingAddDialog(context: Context) {
val dialog = CastingAddDialog(context); val dialog = CastingAddDialog(context);
registerDialogOpened(dialog); registerDialogOpened(dialog);
ownerActivity?.let { dialog.setOwnerActivity(it) }
dialog.setOnDismissListener { registerDialogClosed(dialog) }; dialog.setOnDismissListener { registerDialogClosed(dialog) };
dialog.show(); dialog.show();
} }
@@ -542,36 +523,17 @@ class UIDialogs {
} }
class Action { class Action {
val text: String; val text: String;
val action: ((DialogResult?)->Unit); val action: ()->Unit;
val style: ActionStyle; val style: ActionStyle;
var center: Boolean; var center: Boolean;
constructor(text: String, action: ()->Unit, style: ActionStyle = ActionStyle.NONE, center: Boolean = false) { constructor(text: String, action: ()->Unit, style: ActionStyle = ActionStyle.NONE, center: Boolean = false) {
this.text = text;
this.action = { action() };
this.style = style;
this.center = center;
}
protected constructor(text: String, action: (DialogResult?)->Unit, style: ActionStyle = ActionStyle.NONE, center: Boolean = false) {
this.text = text; this.text = text;
this.action = action; this.action = action;
this.style = style; this.style = style;
this.center = center; this.center = center;
} }
fun invokeAction(input: DialogResult? = null) {
this.action(input);
}
companion object {
fun withInput(text: String, action: (DialogResult?)->Unit, style: ActionStyle = ActionStyle.NONE, center: Boolean = false): Action {
return Action(text, action, style, center);
}
}
} }
class DialogResult(
val text: String?
);
enum class ActionStyle { enum class ActionStyle {
NONE, NONE,
PRIMARY, PRIMARY,
@@ -4,16 +4,11 @@ import android.app.NotificationManager
import android.content.ContentResolver import android.content.ContentResolver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.hls.playlist.DefaultHlsPlaylistParserFactory
import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist
import androidx.media3.exoplayer.hls.playlist.HlsMultivariantPlaylist
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.activities.MainActivity import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.activities.SettingsActivity
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.models.ResultCapabilities import com.futo.platformplayer.api.media.models.ResultCapabilities
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
@@ -42,9 +37,6 @@ import com.futo.platformplayer.models.Playlist
import com.futo.platformplayer.models.Subscription import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.models.SubscriptionGroup import com.futo.platformplayer.models.SubscriptionGroup
import com.futo.platformplayer.parsers.HLS import com.futo.platformplayer.parsers.HLS
import com.futo.platformplayer.parsers.HLS.MediaRendition
import com.futo.platformplayer.parsers.HLS.StreamInfo
import com.futo.platformplayer.parsers.HLS.VariantPlaylistReference
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateDownloads import com.futo.platformplayer.states.StateDownloads
import com.futo.platformplayer.states.StateHistory import com.futo.platformplayer.states.StateHistory
@@ -71,9 +63,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.ByteArrayInputStream
import androidx.core.net.toUri
import com.futo.platformplayer.fragment.mainactivity.main.SettingsFragment
class UISlideOverlays { class UISlideOverlays {
companion object { companion object {
@@ -129,163 +118,115 @@ class UISlideOverlays {
val originalVideo = subscription.doFetchVideos; val originalVideo = subscription.doFetchVideos;
val originalPosts = subscription.doFetchPosts; val originalPosts = subscription.doFetchPosts;
val menu = SlideUpMenuOverlay( val menu = SlideUpMenuOverlay(container.context, container, "Subscription Settings", null, true, listOf());
container.context,
container,
"Subscription Settings",
null,
true,
listOf()
);
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { StateApp.instance.scopeOrNull?.launch(Dispatchers.IO){
try { val plugin = StatePlatform.instance.getChannelClient(subscription.channel.url);
val plugin = StatePlatform.instance.getChannelClient(subscription.channel.url); val capabilities = plugin.getChannelCapabilities();
val capabilities = plugin.getChannelCapabilities();
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
items.addAll( items.addAll(listOf(
listOf( SlideUpMenuItem(
SlideUpMenuItem( container.context,
container.context, R.drawable.ic_notifications,
R.drawable.ic_notifications, "Notifications",
"Notifications", "",
"", tag = "notifications",
tag = "notifications", call = {
call = { subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications;
subscription.doNotifications = },
menu?.selectOption(null, "notifications", true, true) invokeParent = false
?: subscription.doNotifications; ),
}, if(StateSubscriptionGroups.instance.getSubscriptionGroups().isNotEmpty())
invokeParent = false SlideUpMenuGroup(container.context, "Subscription Groups",
), "You can select which groups this subscription is part of.",
if (StateSubscriptionGroups.instance.getSubscriptionGroups() -1, listOf()) else null,
.isNotEmpty() if(StateSubscriptionGroups.instance.getSubscriptionGroups().isNotEmpty())
) SlideUpMenuRecycler(container.context, "as") {
SlideUpMenuGroup( val groups = ArrayList<SubscriptionGroup>(StateSubscriptionGroups.instance.getSubscriptionGroups()
container.context, "Subscription Groups", .map { SubscriptionGroup.Selectable(it, it.urls.contains(subscription.channel.url)) }
"You can select which groups this subscription is part of.", .sortedBy { !it.selected });
-1, listOf() var adapter: AnyAdapterView<SubscriptionGroup, SubscriptionGroupBarViewHolder>? = null;
) else null, adapter = it.asAny(groups, RecyclerView.HORIZONTAL) {
if (StateSubscriptionGroups.instance.getSubscriptionGroups() it.onClick.subscribe {
.isNotEmpty() if(it is SubscriptionGroup.Selectable) {
) val actualGroup = StateSubscriptionGroups.instance.getSubscriptionGroup(it.id)
SlideUpMenuRecycler(container.context, "as") { ?: return@subscribe;
val groups = groups.clear();
ArrayList<SubscriptionGroup>( if(it.selected)
StateSubscriptionGroups.instance.getSubscriptionGroups() actualGroup.urls.remove(subscription.channel.url);
.map { else
SubscriptionGroup.Selectable( actualGroup.urls.add(subscription.channel.url);
it,
it.urls.contains(subscription.channel.url)
)
}
.sortedBy { !it.selected });
var adapter: AnyAdapterView<SubscriptionGroup, SubscriptionGroupBarViewHolder>? =
null;
adapter = it.asAny(groups, RecyclerView.HORIZONTAL) {
it.onClick.subscribe {
if (it is SubscriptionGroup.Selectable) {
val actualGroup =
StateSubscriptionGroups.instance.getSubscriptionGroup(
it.id
)
?: return@subscribe;
groups.clear();
if (it.selected)
actualGroup.urls.remove(subscription.channel.url);
else
actualGroup.urls.add(subscription.channel.url);
StateSubscriptionGroups.instance.updateSubscriptionGroup( StateSubscriptionGroups.instance.updateSubscriptionGroup(actualGroup);
actualGroup groups.addAll(StateSubscriptionGroups.instance.getSubscriptionGroups()
); .map { SubscriptionGroup.Selectable(it, it.urls.contains(subscription.channel.url)) }
groups.addAll( .sortedBy { !it.selected });
StateSubscriptionGroups.instance.getSubscriptionGroups() adapter?.notifyContentChanged();
.map { }
SubscriptionGroup.Selectable( }
it, };
it.urls.contains(subscription.channel.url) return@SlideUpMenuRecycler adapter;
) } else null,
} SlideUpMenuGroup(container.context, "Fetch Settings",
.sortedBy { !it.selected }); "Depending on the platform you might not need to enable a type for it to be available.",
adapter?.notifyContentChanged(); -1, listOf()),
} if(capabilities.hasType(ResultCapabilities.TYPE_LIVE)) SlideUpMenuItem(
} container.context,
}; R.drawable.ic_live_tv,
return@SlideUpMenuRecycler adapter; "Livestreams",
} else null, "Check for livestreams",
SlideUpMenuGroup( tag = "fetchLive",
container.context, "Fetch Settings", call = {
"Depending on the platform you might not need to enable a type for it to be available.", subscription.doFetchLive = menu?.selectOption(null, "fetchLive", true, true) ?: subscription.doFetchLive;
-1, listOf() },
), invokeParent = false
if (capabilities.hasType(ResultCapabilities.TYPE_LIVE)) SlideUpMenuItem( ) else null,
container.context, if(capabilities.hasType(ResultCapabilities.TYPE_STREAMS)) SlideUpMenuItem(
R.drawable.ic_live_tv, container.context,
"Livestreams", R.drawable.ic_play,
"Check for livestreams", "Streams",
tag = "fetchLive", "Check for streams",
call = { tag = "fetchStreams",
subscription.doFetchLive = call = {
menu?.selectOption(null, "fetchLive", true, true) subscription.doFetchStreams = menu?.selectOption(null, "fetchStreams", true, true) ?: subscription.doFetchStreams;
?: subscription.doFetchLive; },
}, invokeParent = false
invokeParent = false ) else null,
) else null, if(capabilities.hasType(ResultCapabilities.TYPE_VIDEOS))
if (capabilities.hasType(ResultCapabilities.TYPE_STREAMS)) SlideUpMenuItem( SlideUpMenuItem(
container.context, container.context,
R.drawable.ic_play, R.drawable.ic_play,
"Streams", "Videos",
"Check for streams", "Check for videos",
tag = "fetchStreams", tag = "fetchVideos",
call = { call = {
subscription.doFetchStreams = subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos;
menu?.selectOption(null, "fetchStreams", true, true) },
?: subscription.doFetchStreams; invokeParent = false
}, ) else if(capabilities.hasType(ResultCapabilities.TYPE_MIXED) || capabilities.types.isEmpty())
invokeParent = false SlideUpMenuItem(
) else null, container.context,
if (capabilities.hasType(ResultCapabilities.TYPE_VIDEOS)) R.drawable.ic_play,
SlideUpMenuItem( "Content",
container.context, "Check for content",
R.drawable.ic_play, tag = "fetchVideos",
"Videos", call = {
"Check for videos", subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos;
tag = "fetchVideos", },
call = { invokeParent = false
subscription.doFetchVideos = ) else null,
menu?.selectOption(null, "fetchVideos", true, true) if(capabilities.hasType(ResultCapabilities.TYPE_POSTS)) SlideUpMenuItem(
?: subscription.doFetchVideos; container.context,
}, R.drawable.ic_chat,
invokeParent = false "Posts",
) else if (capabilities.hasType(ResultCapabilities.TYPE_MIXED) || capabilities.types.isEmpty()) "Check for posts",
SlideUpMenuItem( tag = "fetchPosts",
container.context, call = {
R.drawable.ic_play, subscription.doFetchPosts = menu?.selectOption(null, "fetchPosts", true, true) ?: subscription.doFetchPosts;
"Content", },
"Check for content", invokeParent = false
tag = "fetchVideos", ) else null/*,,
call = {
subscription.doFetchVideos =
menu?.selectOption(null, "fetchVideos", true, true)
?: subscription.doFetchVideos;
},
invokeParent = false
) else null,
if (capabilities.hasType(ResultCapabilities.TYPE_POSTS)) SlideUpMenuItem(
container.context,
R.drawable.ic_chat,
"Posts",
"Check for posts",
tag = "fetchPosts",
call = {
subscription.doFetchPosts =
menu?.selectOption(null, "fetchPosts", true, true)
?: subscription.doFetchPosts;
},
invokeParent = false
) else null/*,,
SlideUpMenuGroup(container.context, "Actions", SlideUpMenuGroup(container.context, "Actions",
"Various things you can do with this subscription", "Various things you can do with this subscription",
@@ -293,76 +234,61 @@ class UISlideOverlays {
SlideUpMenuItem(container.context, R.drawable.ic_list, "Add to Group", "", "btnAddToGroup", { SlideUpMenuItem(container.context, R.drawable.ic_list, "Add to Group", "", "btnAddToGroup", {
showCreateSubscriptionGroup(container, subscription.channel); showCreateSubscriptionGroup(container, subscription.channel);
}, false)*/ }, false)*/
).filterNotNull() ).filterNotNull());
);
menu.setItems(items); menu.setItems(items);
if (subscription.doNotifications) if(subscription.doNotifications)
menu.selectOption(null, "notifications", true, true); menu.selectOption(null, "notifications", true, true);
if (subscription.doFetchLive) if(subscription.doFetchLive)
menu.selectOption(null, "fetchLive", true, true); menu.selectOption(null, "fetchLive", true, true);
if (subscription.doFetchStreams) if(subscription.doFetchStreams)
menu.selectOption(null, "fetchStreams", true, true); menu.selectOption(null, "fetchStreams", true, true);
if (subscription.doFetchVideos) if(subscription.doFetchVideos)
menu.selectOption(null, "fetchVideos", true, true); menu.selectOption(null, "fetchVideos", true, true);
if (subscription.doFetchPosts) if(subscription.doFetchPosts)
menu.selectOption(null, "fetchPosts", true, true); menu.selectOption(null, "fetchPosts", true, true);
menu.onOK.subscribe { menu.onOK.subscribe {
subscription.save(); subscription.save();
menu.hide(true); menu.hide(true);
if (subscription.doNotifications && !originalNotif) { if(subscription.doNotifications && !originalNotif) {
val mainContext = StateApp.instance.contextOrNull; val mainContext = StateApp.instance.contextOrNull;
if (Settings.instance.subscriptions.subscriptionsBackgroundUpdateInterval == 0) { if(Settings.instance.subscriptions.subscriptionsBackgroundUpdateInterval == 0) {
UIDialogs.toast( UIDialogs.toast(container.context, "Enable 'Background Update' in settings for notifications to work");
container.context,
"Enable 'Background Update' in settings for notifications to work"
);
if (mainContext is MainActivity) { if(mainContext is MainActivity) {
UIDialogs.showDialog( UIDialogs.showDialog(mainContext, R.drawable.ic_settings, "Background Updating Required",
mainContext, "You need to set a Background Updating interval for notifications", null, 0,
R.drawable.ic_settings, UIDialogs.Action("Cancel", {}),
"Background Updating Required", UIDialogs.Action("Configure", {
"You need to set a Background Updating interval for notifications", val intent = Intent(mainContext, SettingsActivity::class.java);
null, intent.putExtra("query", mainContext.getString(R.string.background_update));
0, mainContext.startActivity(intent);
UIDialogs.Action("Cancel", {}), }, UIDialogs.ActionStyle.PRIMARY));
UIDialogs.Action("Configure", { }
StateApp.instance.activity?.let { return@subscribe;
it.navigate(it.getFragment<SettingsFragment>(), mainContext.getString(R.string.background_update)) }
} else if(!(mainContext?.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).areNotificationsEnabled()) {
}, UIDialogs.ActionStyle.PRIMARY) UIDialogs.toast(container.context, "Android notifications are disabled");
); if(mainContext is MainActivity) {
} mainContext.requestNotificationPermissions("Notifications are required for subscription updating and notifications to work");
return@subscribe;
} else if (!(mainContext?.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).areNotificationsEnabled()) {
UIDialogs.toast(
container.context,
"Android notifications are disabled"
);
if (mainContext is MainActivity) {
mainContext.requestNotificationPermissions("Notifications are required for subscription updating and notifications to work");
}
} }
} }
}; }
menu.onCancel.subscribe { };
subscription.doNotifications = originalNotif; menu.onCancel.subscribe {
subscription.doFetchLive = originalLive; subscription.doNotifications = originalNotif;
subscription.doFetchStreams = originalStream; subscription.doFetchLive = originalLive;
subscription.doFetchVideos = originalVideo; subscription.doFetchStreams = originalStream;
subscription.doFetchPosts = originalPosts; subscription.doFetchVideos = originalVideo;
}; subscription.doFetchPosts = originalPosts;
};
menu.setOk("Save"); menu.setOk("Save");
menu.show(); menu.show();
}
} catch (e: Throwable) {
Logger.e(TAG, "Failed to show subscription overlay.", e)
} }
} }
@@ -373,7 +299,6 @@ class UISlideOverlays {
} }
@OptIn(UnstableApi::class)
fun showHlsPicker(video: IPlatformVideoDetails, source: Any, sourceUrl: String, container: ViewGroup): SlideUpMenuOverlay { fun showHlsPicker(video: IPlatformVideoDetails, source: Any, sourceUrl: String, container: ViewGroup): SlideUpMenuOverlay {
val items = arrayListOf<View>(LoaderView(container.context)) val items = arrayListOf<View>(LoaderView(container.context))
val slideUpMenuOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.download_video), null, true, items) val slideUpMenuOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.download_video), null, true, items)
@@ -385,8 +310,6 @@ class UISlideOverlays {
val masterPlaylistContent = masterPlaylistResponse.body?.string() val masterPlaylistContent = masterPlaylistResponse.body?.string()
?: throw Exception("Master playlist content is empty") ?: throw Exception("Master playlist content is empty")
val resolvedPlaylistUrl = masterPlaylistResponse.url
val videoButtons = arrayListOf<SlideUpMenuItem>() val videoButtons = arrayListOf<SlideUpMenuItem>()
val audioButtons = arrayListOf<SlideUpMenuItem>() val audioButtons = arrayListOf<SlideUpMenuItem>()
//TODO: Implement subtitles //TODO: Implement subtitles
@@ -399,103 +322,55 @@ class UISlideOverlays {
val masterPlaylist: HLS.MasterPlaylist val masterPlaylist: HLS.MasterPlaylist
try { try {
val inputStream = ByteArrayInputStream(masterPlaylistContent.toByteArray()) masterPlaylist = HLS.parseMasterPlaylist(masterPlaylistContent, sourceUrl)
val playlist = DefaultHlsPlaylistParserFactory().createPlaylistParser()
.parse(sourceUrl.toUri(), inputStream)
if (playlist is HlsMediaPlaylist) { masterPlaylist.getAudioSources().forEach { it ->
if (source is IHLSManifestAudioSource) {
val variant = HLS.mediaRenditionToVariant(MediaRendition("AUDIO", playlist.baseUri, "Single Playlist", null, null, null, null, null))!!
val estSize = VideoHelper.estimateSourceSize(variant); val estSize = VideoHelper.estimateSourceSize(it);
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else ""; val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
audioButtons.add(SlideUpMenuItem( audioButtons.add(SlideUpMenuItem(
container.context, container.context,
R.drawable.ic_music, R.drawable.ic_music,
variant.name, it.name,
listOf(variant.language, variant.codec).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "), listOf(it.language, it.codec).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "),
(prefix + variant.codec).trim(), (prefix + it.codec).trim(),
tag = variant, tag = it,
call = { call = {
selectedAudioVariant = variant selectedAudioVariant = it
slideUpMenuOverlay.selectOption(audioButtons, variant) slideUpMenuOverlay.selectOption(audioButtons, it)
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
},
invokeParent = false
))
} else {
val variant = HLS.variantReferenceToVariant(VariantPlaylistReference(playlist.baseUri, StreamInfo(null, null, null, null, null, null, null, null, null)))
val estSize = VideoHelper.estimateSourceSize(variant);
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
videoButtons.add(SlideUpMenuItem(
container.context,
R.drawable.ic_movie,
variant.name,
"${variant.width}x${variant.height}",
(prefix + variant.codec).trim(),
tag = variant,
call = {
selectedVideoVariant = variant
slideUpMenuOverlay.selectOption(videoButtons, variant)
if (audioButtons.isEmpty()){
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
}
},
invokeParent = false
))
}
} else if (playlist is HlsMultivariantPlaylist) {
masterPlaylist = HLS.parseMasterPlaylist(masterPlaylistContent, resolvedPlaylistUrl)
masterPlaylist.getAudioSources().forEach { it ->
val estSize = VideoHelper.estimateSourceSize(it);
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
audioButtons.add(SlideUpMenuItem(
container.context,
R.drawable.ic_music,
it.name,
listOf(it.language, it.codec).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "),
(prefix + it.codec).trim(),
tag = it,
call = {
selectedAudioVariant = it
slideUpMenuOverlay.selectOption(audioButtons, it)
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
},
invokeParent = false
))
}
/*masterPlaylist.getSubtitleSources().forEach { it ->
subtitleButtons.add(SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, listOf(it.format).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "), it, {
selectedSubtitleVariant = it
slideUpMenuOverlay.selectOption(subtitleButtons, it)
slideUpMenuOverlay.setOk(container.context.getString(R.string.download)) slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
}, false)) },
}*/ invokeParent = false
))
}
masterPlaylist.getVideoSources().forEach { /*masterPlaylist.getSubtitleSources().forEach { it ->
val estSize = VideoHelper.estimateSourceSize(it); subtitleButtons.add(SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, listOf(it.format).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "), it, {
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else ""; selectedSubtitleVariant = it
videoButtons.add(SlideUpMenuItem( slideUpMenuOverlay.selectOption(subtitleButtons, it)
container.context, slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
R.drawable.ic_movie, }, false))
it.name, }*/
"${it.width}x${it.height}",
(prefix + it.codec).trim(), masterPlaylist.getVideoSources().forEach {
tag = it, val estSize = VideoHelper.estimateSourceSize(it);
call = { val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
selectedVideoVariant = it videoButtons.add(SlideUpMenuItem(
slideUpMenuOverlay.selectOption(videoButtons, it) container.context,
if (audioButtons.isEmpty()){ R.drawable.ic_movie,
slideUpMenuOverlay.setOk(container.context.getString(R.string.download)) it.name,
} "${it.width}x${it.height}",
}, (prefix + it.codec).trim(),
invokeParent = false tag = it,
)) call = {
} selectedVideoVariant = it
slideUpMenuOverlay.selectOption(videoButtons, it)
if (audioButtons.isEmpty()){
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
}
},
invokeParent = false
))
} }
val newItems = arrayListOf<View>() val newItems = arrayListOf<View>()
@@ -523,11 +398,11 @@ class UISlideOverlays {
if (masterPlaylistContent.lines().any { it.startsWith("#EXTINF:") }) { if (masterPlaylistContent.lines().any { it.startsWith("#EXTINF:") }) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
if (source is IHLSManifestSource) { if (source is IHLSManifestSource) {
StateDownloads.instance.download(video, HLSVariantVideoUrlSource("variant", 0, 0, "application/vnd.apple.mpegurl", "", null, 0, false, resolvedPlaylistUrl), null, null) StateDownloads.instance.download(video, HLSVariantVideoUrlSource("variant", 0, 0, "application/vnd.apple.mpegurl", "", null, 0, false, sourceUrl), null, null)
UIDialogs.toast(container.context, "Variant video HLS playlist download started") UIDialogs.toast(container.context, "Variant video HLS playlist download started")
slideUpMenuOverlay.hide() slideUpMenuOverlay.hide()
} else if (source is IHLSManifestAudioSource) { } else if (source is IHLSManifestAudioSource) {
StateDownloads.instance.download(video, null, HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, false, resolvedPlaylistUrl), null) StateDownloads.instance.download(video, null, HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, false, sourceUrl), null)
UIDialogs.toast(container.context, "Variant audio HLS playlist download started") UIDialogs.toast(container.context, "Variant audio HLS playlist download started")
slideUpMenuOverlay.hide() slideUpMenuOverlay.hide()
} else { } else {
@@ -809,10 +684,6 @@ class UISlideOverlays {
} }
} }
} }
if(!Settings.instance.downloads.shouldDownload()) {
UIDialogs.appToast("Download will start when you're back on wifi.\n" +
"(You can change this in settings)", true);
}
} }
}; };
return menu.apply { show() }; return menu.apply { show() };
@@ -1109,30 +980,26 @@ class UISlideOverlays {
+ actions).filterNotNull() + actions).filterNotNull()
)); ));
items.add( items.add(
SlideUpMenuGroup( SlideUpMenuGroup(container.context, container.context.getString(R.string.add_to), "addto",
container.context, container.context.getString(R.string.add_to), "addto", SlideUpMenuItem(container.context,
SlideUpMenuItem(
container.context,
R.drawable.ic_queue_add, R.drawable.ic_queue_add,
container.context.getString(R.string.add_to_queue), container.context.getString(R.string.add_to_queue),
"${queue.size} " + container.context.getString(R.string.videos), "${queue.size} " + container.context.getString(R.string.videos),
tag = "queue", tag = "queue",
call = { StatePlayer.instance.addToQueue(video); }), call = { StatePlayer.instance.addToQueue(video); }),
SlideUpMenuItem( SlideUpMenuItem(container.context,
container.context,
R.drawable.ic_watchlist_add, R.drawable.ic_watchlist_add,
"${container.context.getString(R.string.add_to)} " + StatePlayer.TYPE_WATCHLATER + "", "${container.context.getString(R.string.add_to)} " + StatePlayer.TYPE_WATCHLATER + "",
"${watchLater.size} " + container.context.getString(R.string.videos), "${watchLater.size} " + container.context.getString(R.string.videos),
tag = "watch later", tag = "watch later",
call = { StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true); }), call = { StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true); }),
SlideUpMenuItem( SlideUpMenuItem(container.context,
container.context,
R.drawable.ic_history, R.drawable.ic_history,
container.context.getString(R.string.add_to_history), container.context.getString(R.string.add_to_history),
"Mark as watched", "Mark as watched",
tag = "history", tag = "history",
call = { StateHistory.instance.markAsWatched(video); }), call = { StateHistory.instance.markAsWatched(video); }),
)); ));
val playlistItems = arrayListOf<SlideUpMenuItem>(); val playlistItems = arrayListOf<SlideUpMenuItem>();
playlistItems.add(SlideUpMenuItem( playlistItems.add(SlideUpMenuItem(
@@ -1196,17 +1063,14 @@ class UISlideOverlays {
val queue = StatePlayer.instance.getQueue(); val queue = StatePlayer.instance.getQueue();
val watchLater = StatePlaylists.instance.getWatchLater(); val watchLater = StatePlaylists.instance.getWatchLater();
items.add( items.add(
SlideUpMenuGroup( SlideUpMenuGroup(container.context, container.context.getString(R.string.other), "other",
container.context, container.context.getString(R.string.other), "other", SlideUpMenuItem(container.context,
SlideUpMenuItem(
container.context,
R.drawable.ic_queue_add, R.drawable.ic_queue_add,
container.context.getString(R.string.queue), container.context.getString(R.string.queue),
"${queue.size} " + container.context.getString(R.string.videos), "${queue.size} " + container.context.getString(R.string.videos),
tag = "queue", tag = "queue",
call = { StatePlayer.instance.addToQueue(video); }), call = { StatePlayer.instance.addToQueue(video); }),
SlideUpMenuItem( SlideUpMenuItem(container.context,
container.context,
R.drawable.ic_watchlist_add, R.drawable.ic_watchlist_add,
StatePlayer.TYPE_WATCHLATER, StatePlayer.TYPE_WATCHLATER,
"${watchLater.size} " + container.context.getString(R.string.videos), "${watchLater.size} " + container.context.getString(R.string.videos),
@@ -1214,10 +1078,8 @@ class UISlideOverlays {
call = { call = {
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true)) if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true))
UIDialogs.appToast("Added to watch later", false); UIDialogs.appToast("Added to watch later", false);
else
UIDialogs.toast(container.context.getString(R.string.already_in_watch_later))
}), }),
) )
); );
val playlistItems = arrayListOf<SlideUpMenuItem>(); val playlistItems = arrayListOf<SlideUpMenuItem>();
@@ -1255,8 +1117,8 @@ class UISlideOverlays {
return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.add_to), null, true, items).apply { show() }; return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.add_to), null, true, items).apply { show() };
} }
fun showFiltersOverlay(lifecycleScope: CoroutineScope, container: ViewGroup, enabledClientsIds: List<String>, filterValues: HashMap<String, List<String>>): SlideUpMenuFilters { fun showFiltersOverlay(lifecycleScope: CoroutineScope, container: ViewGroup, enabledClientsIds: List<String>, filterValues: HashMap<String, List<String>>, isChannelSearch: Boolean = false): SlideUpMenuFilters {
val overlay = SlideUpMenuFilters(lifecycleScope, container, enabledClientsIds, filterValues); val overlay = SlideUpMenuFilters(lifecycleScope, container, enabledClientsIds, filterValues, isChannelSearch);
overlay.show(); overlay.show();
return overlay; return overlay;
} }
@@ -1,63 +0,0 @@
package com.futo.platformplayer
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.dialogs.AutoUpdateDialog
import com.futo.platformplayer.states.StateApp
import java.io.File
class UpdateActionReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
UpdateNotificationManager.ACTION_UPDATE_YES -> handleUpdateYes(context, intent)
UpdateNotificationManager.ACTION_UPDATE_NO -> handleUpdateNo(context)
UpdateNotificationManager.ACTION_UPDATE_NEVER -> handleUpdateNever(context)
UpdateNotificationManager.ACTION_DOWNLOAD_CANCEL -> handleDownloadCancel(context, intent)
}
}
private fun handleUpdateYes(context: Context, intent: Intent) {
AutoUpdateDialog.currentDialog?.dismiss()
val version = intent.getIntExtra(UpdateNotificationManager.EXTRA_VERSION, 0)
if (version == 0) {
return
}
NotificationManagerCompat.from(context).cancel(UpdateNotificationManager.NOTIF_ID_AVAILABLE)
val serviceIntent = Intent(context, UpdateDownloadService::class.java).apply {
putExtra(UpdateDownloadService.EXTRA_VERSION, version)
}
ContextCompat.startForegroundService(context, serviceIntent)
}
private fun handleUpdateNo(context: Context) {
AutoUpdateDialog.currentDialog?.dismiss()
NotificationManagerCompat.from(context).cancel(UpdateNotificationManager.NOTIF_ID_AVAILABLE)
}
private fun handleUpdateNever(context: Context) {
AutoUpdateDialog.currentDialog?.dismiss()
Settings.instance.autoUpdate.check = 1
Settings.instance.save()
UpdateNotificationManager.cancelAll(context)
}
private fun handleDownloadCancel(context: Context, intent: Intent) {
val version = intent.getIntExtra(UpdateNotificationManager.EXTRA_VERSION, 0)
val cancelIntent = Intent(context, UpdateDownloadService::class.java).apply {
putExtra(UpdateDownloadService.EXTRA_CANCEL, true)
putExtra(UpdateDownloadService.EXTRA_VERSION, version)
}
ContextCompat.startForegroundService(context, cancelIntent)
NotificationManagerCompat.from(context).cancel(UpdateNotificationManager.NOTIF_ID_DOWNLOADING)
}
}
@@ -1,64 +0,0 @@
package com.futo.platformplayer
import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateUpdate
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class UpdateCheckWorker(appContext: Context, workerParams: WorkerParameters) : CoroutineWorker(appContext, workerParams) {
override suspend fun doWork(): Result {
if (!Settings.instance.autoUpdate.isAutoUpdateEnabled()) {
Logger.i(TAG, "Auto-update disabled, skipping worker run")
return Result.success()
}
return withContext(Dispatchers.IO) {
try {
val client = ManagedHttpClient()
val latestVersion = StateUpdate.Companion.instance.downloadVersionCode(client)
if (latestVersion == null) {
Logger.w(TAG, "Failed to fetch latest version in worker")
return@withContext Result.retry()
}
val currentVersion = BuildConfig.VERSION_CODE
Logger.i(TAG, "Worker check: current=$currentVersion, latest=$latestVersion")
if (latestVersion <= currentVersion) {
return@withContext Result.success()
}
UpdateNotificationManager.showUpdateAvailableNotification(applicationContext, latestVersion)
if (StateApp.instance.isMainActive) {
withContext(Dispatchers.Main) {
StateApp.withContext { ctx ->
try {
UIDialogs.showUpdateAvailableDialog(ctx, latestVersion, false)
} catch (t: Throwable) {
Logger.w(TAG, "Failed to show in-app update dialog from worker", t)
}
}
}
}
Result.success()
} catch (t: Throwable) {
Logger.w(TAG, "Exception in UpdateCheckWorker", t)
Result.retry()
}
}
}
companion object {
private const val TAG = "UpdateCheckWorker"
const val UNIQUE_WORK_NAME = "updateCheck"
}
}
@@ -1,241 +0,0 @@
package com.futo.platformplayer
import android.app.Dialog
import android.app.Service
import android.content.Intent
import android.os.IBinder
import com.futo.platformplayer.UIDialogs.ActionStyle
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateUpdate
import kotlinx.coroutines.*
import java.io.File
import java.io.FileOutputStream
import java.net.HttpURLConnection
import java.net.URL
class UpdateDownloadService : Service() {
companion object {
private const val TAG = "UpdateDownloadService"
const val EXTRA_VERSION = "version"
const val EXTRA_CANCEL = "cancel"
private const val MAX_RETRIES = 5
private const val INITIAL_BACKOFF_MS = 5_000L
private const val BUFFER_SIZE = 8 * 1024
var updateDownloadedDialog: Dialog? = null
}
private val job = SupervisorJob()
private val scope = CoroutineScope(Dispatchers.IO + job)
@Volatile
private var isDownloading: Boolean = false
@Volatile
private var cancelRequested: Boolean = false
override fun onBind(intent: Intent?): IBinder? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent == null) {
stopSelf()
return START_NOT_STICKY
}
if (intent.getBooleanExtra(EXTRA_CANCEL, false)) {
cancelRequested = true
Logger.i(TAG, "Download cancel requested")
stopForeground(Service.STOP_FOREGROUND_REMOVE)
stopSelf()
return START_NOT_STICKY
}
val version = intent.getIntExtra(EXTRA_VERSION, 0)
if (version == 0) {
stopSelf()
return START_NOT_STICKY
}
if (isDownloading) {
Logger.i(TAG, "Download already in progress, ignoring new start")
return START_STICKY
}
isDownloading = true
cancelRequested = false
val notification = UpdateNotificationManager.buildDownloadProgressNotification(this, version, 0, true)
startForeground(UpdateNotificationManager.NOTIF_ID_DOWNLOADING, notification)
scope.launch {
downloadApk(version)
}
return START_STICKY
}
override fun onDestroy() {
super.onDestroy()
job.cancel()
}
private suspend fun downloadApk(version: Int) {
val apkFile = StateUpdate.getApkFile(this, version)
val partialFile = StateUpdate.getPartialApkFile(this, version)
try {
if (apkFile.exists() && apkFile.length() > 0L) {
Logger.i(TAG, "APK already downloaded at ${apkFile.absolutePath}")
onDownloadComplete(version, apkFile)
return
}
var backoffMs = INITIAL_BACKOFF_MS
for (attempt in 0 until MAX_RETRIES) {
if (cancelRequested) {
Logger.i(TAG, "Download cancelled before attempt ${attempt + 1}")
break
}
try {
performDownload(StateUpdate.APK_URL, partialFile, version)
if (!cancelRequested) {
if (apkFile.exists()) {
apkFile.delete()
}
if (!partialFile.renameTo(apkFile)) {
throw IllegalStateException("Failed to rename partial APK file")
}
onDownloadComplete(version, apkFile)
}
break
} catch (t: Throwable) {
if (cancelRequested) {
Logger.i(TAG, "Download cancelled by user", t)
break
}
if (attempt == MAX_RETRIES - 1) {
Logger.e(TAG, "Download failed after ${attempt + 1} attempts", t)
UpdateNotificationManager.showDownloadFailedNotification(this, version, t)
break
} else {
Logger.w(TAG, "Download attempt ${attempt + 1} failed, retrying in ${backoffMs / 1000}s", t)
delay(backoffMs)
backoffMs *= 2
}
}
}
} finally {
isDownloading = false
cancelRequested = false
stopForeground(Service.STOP_FOREGROUND_REMOVE)
stopSelf()
}
}
private fun performDownload(url: String, partialFile: File, version: Int) {
var startOffset = if (partialFile.exists()) partialFile.length() else 0L
Logger.i(TAG, "Starting download. url=$url, existingBytes=$startOffset")
var connection: HttpURLConnection? = null
try {
connection = (URL(url).openConnection() as HttpURLConnection).apply {
connectTimeout = 15_000
readTimeout = 30_000
if (startOffset > 0L) {
setRequestProperty("Range", "bytes=$startOffset-")
}
}
connection.connect()
val responseCode = connection.responseCode
if (responseCode == HttpURLConnection.HTTP_OK && startOffset > 0L) {
Logger.w(TAG, "Server ignored Range header, restarting download from scratch")
partialFile.delete()
startOffset = 0L
} else if (responseCode != HttpURLConnection.HTTP_OK &&
responseCode != HttpURLConnection.HTTP_PARTIAL) {
throw IllegalStateException("Unexpected HTTP response code $responseCode")
}
val contentLength = connection.contentLengthLong
val totalBytes = if (contentLength > 0L) startOffset + contentLength else -1L
val buffer = ByteArray(BUFFER_SIZE)
var downloaded = 0L
var lastProgress = -1
connection.inputStream.use { input ->
FileOutputStream(partialFile, startOffset > 0L).use { output ->
while (!cancelRequested) {
val read = input.read(buffer)
if (read == -1) {
break
}
output.write(buffer, 0, read)
downloaded += read
if (totalBytes > 0L) {
val progress = (((startOffset + downloaded) * 100L) / totalBytes).toInt()
if (progress != lastProgress) {
lastProgress = progress
val safeProgress = when {
progress < 0 -> 0
progress > 100 -> 100
else -> progress
}
UpdateNotificationManager.updateDownloadProgress(this, version, safeProgress, false)
}
} else {
UpdateNotificationManager.updateDownloadProgress(this, version, 0, true)
}
}
output.flush()
}
}
if (cancelRequested) {
throw CancellationException("Download cancelled")
}
if (totalBytes > 0L && startOffset + downloaded < totalBytes) {
throw IllegalStateException("Download incomplete: expected=$totalBytes, got=${startOffset + downloaded}")
}
} finally {
connection?.disconnect()
}
}
private fun onDownloadComplete(version: Int, apkFile: File) {
Logger.i(TAG, "Download complete for version=$version, file=${apkFile.absolutePath}")
UpdateNotificationManager.showDownloadCompleteNotification(this, version, apkFile)
if (StateApp.instance.isMainActive) {
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
StateApp.withContext { ctx ->
try {
updateDownloadedDialog = UIDialogs.showDialog(ctx, R.drawable.foreground,
"Update downloaded",
"Would you like to install it now?", null, 0,
UIDialogs.Action("Cancel", {
updateDownloadedDialog = null
}, ActionStyle.NONE, true),
UIDialogs.Action("Install", {
UpdateNotificationManager.cancelAll(ctx)
UpdateInstaller.startInstall(ctx, apkFile)
}, ActionStyle.PRIMARY, true));
} catch (t: Throwable) {
Logger.w(TAG, "Failed to show in-app update downloaded dialog", t)
updateDownloadedDialog = null
}
}
}
}
}
}
@@ -1,104 +0,0 @@
package com.futo.platformplayer
import android.annotation.SuppressLint
import android.app.PendingIntent.FLAG_MUTABLE
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.app.PendingIntent.getBroadcast
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInstaller
import android.graphics.drawable.Animatable
import android.provider.Settings
import android.view.View
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.receivers.InstallReceiver
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.io.InputStream
import androidx.core.net.toUri
import com.futo.platformplayer.dialogs.AutoUpdateDialog
import com.futo.platformplayer.states.StateApp
object UpdateInstaller {
private const val TAG = "UpdateInstaller"
@SuppressLint("RequestInstallPackagesPolicy")
fun startInstall(context: Context, apkFile: File) {
if (!apkFile.exists()) {
Logger.w(TAG, "APK file does not exist: ${apkFile.absolutePath}")
UIDialogs.toast(context, "Update file missing")
return
}
if (BuildConfig.IS_PLAYSTORE_BUILD) {
UIDialogs.toast(context, "Updates are managed by the Play Store")
return
}
try {
val pm = context.packageManager
if (!pm.canRequestPackageInstalls()) {
UIDialogs.toast(context, "Allow this app to install updates, then try again")
val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES).apply {
data = "package:${context.packageName}".toUri()
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
context.startActivity(intent)
return
}
} catch (t: Throwable) {
Logger.e(TAG, "Failed to check unknown sources permission", t)
}
GlobalScope.launch(Dispatchers.IO) {
var inputStream: InputStream? = null
var session: PackageInstaller.Session? = null
try {
val packageInstaller: PackageInstaller = context.packageManager.packageInstaller
val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
val sessionId = packageInstaller.createSession(params)
session = packageInstaller.openSession(sessionId)
inputStream = apkFile.inputStream()
val dataLength = apkFile.length()
session.openWrite("package", 0, dataLength).use { sessionStream ->
inputStream.copyToOutputStream(dataLength, sessionStream) { _ -> }
session.fsync(sessionStream)
}
val intent = Intent(context, InstallReceiver::class.java)
val pendingIntent = getBroadcast(context, 0, intent, FLAG_MUTABLE or FLAG_UPDATE_CURRENT)
val statusReceiver = pendingIntent.intentSender
InstallReceiver.onReceiveResult.subscribe(this) { message ->
InstallReceiver.onReceiveResult.clear();
onReceiveResult(context, message);
};
Logger.i(TAG, "Committing install session for ${apkFile.absolutePath}")
session.commit(statusReceiver)
} catch (e: Throwable) {
Logger.w(TAG, "Exception while installing update", e)
session?.abandon()
withContext(Dispatchers.Main) {
UIDialogs.toast(context, "Failed to install update: ${e.message}")
}
} finally {
session?.close()
inputStream?.close()
}
}
}
private fun onReceiveResult(context: Context, result: String?) {
InstallReceiver.onReceiveResult.remove(this);
UIDialogs.showGeneralErrorDialog(context, "Install failed due to:\n" + result);
}
}
@@ -1,174 +0,0 @@
package com.futo.platformplayer
import android.Manifest
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.PendingIntent.FLAG_MUTABLE
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.app.PendingIntent.getBroadcast
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import com.futo.platformplayer.activities.InstallUpdateActivity
import java.io.File
object UpdateNotificationManager {
private const val CHANNEL_ID = "app_updates"
private const val CHANNEL_NAME = "App updates"
private const val CHANNEL_DESCRIPTION = "Notifications about new app versions"
const val ACTION_UPDATE_YES = "com.futo.platformplayer.UPDATE_YES"
const val ACTION_UPDATE_NO = "com.futo.platformplayer.UPDATE_NO"
const val ACTION_UPDATE_NEVER = "com.futo.platformplayer.UPDATE_NEVER"
const val ACTION_DOWNLOAD_CANCEL = "com.futo.platformplayer.UPDATE_CANCEL"
const val ACTION_INSTALL_NOW = "com.futo.platformplayer.UPDATE_INSTALL"
private const val REQUEST_CODE_INSTALL = 1001
const val EXTRA_VERSION = "version"
const val EXTRA_APK_PATH = "apk_path"
const val NOTIF_ID_AVAILABLE = 2001
const val NOTIF_ID_DOWNLOADING = 2002
const val NOTIF_ID_READY = 2003
fun ensureChannel(context: Context) {
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (manager.getNotificationChannel(CHANNEL_ID) == null) {
val channel = NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT).apply {
description = CHANNEL_DESCRIPTION
enableVibration(false)
enableLights(false)
setSound(null, null)
}
manager.createNotificationChannel(channel)
}
}
fun showUpdateAvailableNotification(context: Context, version: Int) {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
return
}
ensureChannel(context)
val yesIntent = Intent(context, UpdateActionReceiver::class.java).apply {
action = ACTION_UPDATE_YES
putExtra(EXTRA_VERSION, version)
}
val yesPendingIntent = getBroadcast(context, 0, yesIntent, FLAG_MUTABLE or FLAG_UPDATE_CURRENT)
val noIntent = Intent(context, UpdateActionReceiver::class.java).apply {
action = ACTION_UPDATE_NO
putExtra(EXTRA_VERSION, version)
}
val noPendingIntent = getBroadcast(context, 1, noIntent, FLAG_MUTABLE or FLAG_UPDATE_CURRENT)
val neverIntent = Intent(context, UpdateActionReceiver::class.java).apply {
action = ACTION_UPDATE_NEVER
putExtra(EXTRA_VERSION, version)
}
val neverPendingIntent = getBroadcast(context, 2, neverIntent, FLAG_MUTABLE or FLAG_UPDATE_CURRENT)
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.foreground)
.setContentTitle("Update available")
.setContentText("A new version ($version) is available.")
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setAutoCancel(true)
.setSilent(true)
.addAction(0, "Never", neverPendingIntent)
.addAction(0, "Not now", noPendingIntent)
.addAction(0, "Download", yesPendingIntent)
NotificationManagerCompat.from(context).notify(NOTIF_ID_AVAILABLE, builder.build())
}
fun buildDownloadProgressNotification(context: Context, version: Int, progress: Int, indeterminate: Boolean): Notification {
ensureChannel(context)
val cancelIntent = Intent(context, UpdateActionReceiver::class.java).apply {
action = ACTION_DOWNLOAD_CANCEL
putExtra(EXTRA_VERSION, version)
}
val cancelPendingIntent = getBroadcast(
context,
3,
cancelIntent,
FLAG_MUTABLE or FLAG_UPDATE_CURRENT
)
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.foreground)
.setContentTitle("Downloading update")
.setContentText("Downloading version $version")
.setPriority(NotificationCompat.PRIORITY_LOW)
.setOngoing(true)
.setSilent(true)
.addAction(0, "Cancel", cancelPendingIntent)
if (indeterminate) {
builder.setProgress(0, 0, true)
} else {
builder.setProgress(100, progress, false)
}
return builder.build()
}
fun updateDownloadProgress(context: Context, version: Int, progress: Int, indeterminate: Boolean) {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
return
}
val notification = buildDownloadProgressNotification(context, version, progress, indeterminate)
NotificationManagerCompat.from(context).notify(NOTIF_ID_DOWNLOADING, notification)
}
fun showDownloadCompleteNotification(context: Context, version: Int, apkFile: File) {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
return
}
ensureChannel(context)
val installIntent = InstallUpdateActivity.createIntent(context, version, apkFile.absolutePath)
val installPendingIntent = PendingIntent.getActivity(context, REQUEST_CODE_INSTALL, installIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.foreground)
.setContentTitle("Update downloaded")
.setContentText("Tap to install version $version.")
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setAutoCancel(true)
.setSilent(true)
.addAction(0, "Install", installPendingIntent)
NotificationManagerCompat.from(context).notify(NOTIF_ID_READY, builder.build())
}
fun showDownloadFailedNotification(context: Context, version: Int, error: Throwable?) {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
return
}
ensureChannel(context)
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.foreground)
.setContentTitle("Failed to download update")
.setContentText(error?.message ?: "Unknown error")
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setAutoCancel(true)
.setSilent(true)
NotificationManagerCompat.from(context).notify(NOTIF_ID_READY, builder.build())
}
fun cancelAll(context: Context) {
NotificationManagerCompat.from(context).cancel(NOTIF_ID_AVAILABLE)
NotificationManagerCompat.from(context).cancel(NOTIF_ID_DOWNLOADING)
NotificationManagerCompat.from(context).cancel(NOTIF_ID_READY)
}
}
@@ -5,6 +5,8 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.res.Resources import android.content.res.Resources
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.icu.util.Output
import android.os.Build import android.os.Build
import android.os.Looper import android.os.Looper
import android.os.OperationCanceledException import android.os.OperationCanceledException
@@ -29,12 +31,6 @@ import java.io.ByteArrayOutputStream
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
import java.net.Inet4Address
import java.net.Inet6Address
import java.net.InetAddress
import java.net.InterfaceAddress
import java.net.NetworkInterface
import java.net.SocketException
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.security.SecureRandom import java.security.SecureRandom
import java.time.OffsetDateTime import java.time.OffsetDateTime
@@ -42,9 +38,6 @@ import java.util.*
import java.util.concurrent.ThreadLocalRandom import java.util.concurrent.ThreadLocalRandom
import java.util.zip.GZIPInputStream import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream import java.util.zip.GZIPOutputStream
import androidx.core.graphics.scale
import com.bumptech.glide.RequestBuilder
import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
private val _allowedCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz "; private val _allowedCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz ";
fun getRandomString(sizeOfRandomString: Int): String { fun getRandomString(sizeOfRandomString: Int): String {
@@ -76,14 +69,7 @@ fun warnIfMainThread(context: String) {
} }
fun ensureNotMainThread() { fun ensureNotMainThread() {
val isMainLooper = try { if (Looper.myLooper() == Looper.getMainLooper()) {
Looper.myLooper() == Looper.getMainLooper()
} catch (e: Throwable) {
//Ignore, for unit tests where its not mocked
false
}
if (isMainLooper) {
Logger.e("Utility", "Throwing exception because a function that should not be called on main thread, is called on main thread") Logger.e("Utility", "Throwing exception because a function that should not be called on main thread, is called on main thread")
throw IllegalStateException("Cannot run on main thread") throw IllegalStateException("Cannot run on main thread")
} }
@@ -102,7 +88,7 @@ fun String.isHexColor(): Boolean {
fun IPlatformClient.fromPool(pool: PlatformMultiClientPool) = pool.getClientPooled(this); fun IPlatformClient.fromPool(pool: PlatformMultiClientPool) = pool.getClientPooled(this);
fun IPlatformVideo.withTimestamp(sec: Long) = PlatformVideoWithTime(this, sec); fun IPlatformVideo.withTimestamp(sec: Long) = PlatformVideoWithTime(this, sec);
fun DocumentFile.getInputStream(context: Context) = context.contentResolver.openInputStream(this.uri); fun DocumentFile.getInputStream(context: Context) = context.contentResolver.openInputStream(this.uri);
fun DocumentFile.getOutputStream(context: Context) = context.contentResolver.openOutputStream(this.uri); fun DocumentFile.getOutputStream(context: Context) = context.contentResolver.openOutputStream(this.uri);
@@ -115,6 +101,23 @@ fun DocumentFile.writeBytes(context: Context, byteArray: ByteArray) = context.co
it.flush(); it.flush();
}; };
fun loadBitmap(url: String): Bitmap {
try {
val client = ManagedHttpClient();
val response = client.get(url);
if (response.isOk && response.body != null) {
val bitmapStream = response.body.byteStream();
val bitmap = BitmapFactory.decodeStream(bitmapStream);
return bitmap;
} else {
throw Exception("Failed to find data at URL.");
}
} catch (e: Throwable) {
Logger.w("Utility", "Exception thrown while downloading bitmap.", e);
throw e;
}
}
fun TextView.setPlatformPlayerLinkMovementMethod(context: Context) { fun TextView.setPlatformPlayerLinkMovementMethod(context: Context) {
this.movementMethod = PlatformLinkMovementMethod(context); this.movementMethod = PlatformLinkMovementMethod(context);
} }
@@ -269,7 +272,7 @@ fun <T> findNewIndex(originalArr: List<T>, newArr: List<T>, item: T): Int{
} }
} }
if(newIndex < 0) if(newIndex < 0)
return newArr.size; return originalArr.size;
else else
return newIndex; return newIndex;
} }
@@ -321,132 +324,4 @@ fun ByteArray.fromGzip(): ByteArray {
} }
} }
return outputStream.toByteArray() return outputStream.toByteArray()
}
fun findCandidateAddresses(): List<InetAddress> {
val candidates = NetworkInterface.getNetworkInterfaces()
.toList()
.asSequence()
.filter(::isUsableInterface)
.flatMap { nif ->
nif.interfaceAddresses
.asSequence()
.mapNotNull { ia ->
ia.address.takeIf(::isUsableAddress)?.let { addr ->
nif to ia
}
}
}
.toList()
return candidates
.sortedWith(
compareBy<Pair<NetworkInterface, InterfaceAddress>>(
{ addressScore(it.second.address) },
{ interfaceScore(it.first) },
{ -it.second.networkPrefixLength.toInt() },
{ -it.first.mtu }
)
).map { it.second.address }
}
fun findPreferredAddress(): InetAddress? {
val candidates = NetworkInterface.getNetworkInterfaces()
.toList()
.asSequence()
.filter(::isUsableInterface)
.flatMap { nif ->
nif.interfaceAddresses
.asSequence()
.mapNotNull { ia ->
ia.address.takeIf(::isUsableAddress)?.let { addr ->
nif to ia
}
}
}
.toList()
return candidates
.minWithOrNull(
compareBy<Pair<NetworkInterface, InterfaceAddress>>(
{ addressScore(it.second.address) },
{ interfaceScore(it.first) },
{ -it.second.networkPrefixLength.toInt() },
{ -it.first.mtu }
)
)?.second?.address
}
private fun isUsableInterface(nif: NetworkInterface): Boolean {
val name = nif.name.lowercase()
return try {
// must be up, not loopback/virtual/PtP, have a MAC, not Docker/tun/etc.
nif.isUp
&& !nif.isLoopback
&& !nif.isPointToPoint
&& !nif.isVirtual
&& !name.startsWith("docker")
&& !name.startsWith("veth")
&& !name.startsWith("br-")
&& !name.startsWith("virbr")
&& !name.startsWith("vmnet")
&& !name.startsWith("tun")
&& !name.startsWith("tap")
} catch (e: SocketException) {
false
}
}
private fun isUsableAddress(addr: InetAddress): Boolean {
return when {
addr.isAnyLocalAddress -> false // 0.0.0.0 / ::
addr.isLoopbackAddress -> false
addr.isLinkLocalAddress -> false // 169.254.x.x or fe80::/10
addr.isMulticastAddress -> false
else -> true
}
}
private fun interfaceScore(nif: NetworkInterface): Int {
val name = nif.name.lowercase()
return when {
name.matches(Regex("^(eth|enp|eno|ens|em)\\d+")) -> 0
name.startsWith("eth") || name.contains("ethernet") -> 0
name.matches(Regex("^(wlan|wlp)\\d+")) -> 1
name.contains("wi-fi") || name.contains("wifi") -> 1
else -> 2
}
}
fun addressScore(addr: InetAddress): Int {
return when (addr) {
is Inet4Address -> {
val octets = addr.address.map { it.toInt() and 0xFF }
when {
octets[0] == 10 -> 0 // 10/8
octets[0] == 192 && octets[1] == 168 -> 0 // 192.168/16
octets[0] == 172 && octets[1] in 16..31 -> 0 // 172.1631/12
else -> 1 // public IPv4
}
}
is Inet6Address -> {
// ULA (fc00::/7) vs global vs others
val b0 = addr.address[0].toInt() and 0xFF
when {
(b0 and 0xFE) == 0xFC -> 2 // ULA
(b0 and 0xE0) == 0x20 -> 3 // global
else -> 4
}
}
else -> Int.MAX_VALUE
}
}
fun <T> Enumeration<T>.toList(): List<T> = Collections.list(this)
fun <T> RequestBuilder<T>.withMaxSizePx(maxSizePx: Int = 1920): RequestBuilder<T> {
return this;
//.downsample(DownsampleStrategy.AT_MOST)
//.override(maxSizePx, maxSizePx)
//.centerInside()
} }
@@ -107,9 +107,10 @@ class AddSourceActivity : AppCompatActivity() {
onNewIntent(intent); onNewIntent(intent);
} }
override fun onNewIntent(intent: Intent) { override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent) super.onNewIntent(intent)
var url = intent.dataString; var url = intent?.dataString;
if(url == null) if(url == null)
UIDialogs.showDialog(this, R.drawable.ic_error, getString(R.string.no_valid_url_provided), null, null, UIDialogs.showDialog(this, R.drawable.ic_error, getString(R.string.no_valid_url_provided), null, null,
0, UIDialogs.Action(getString(R.string.ok), { finish() }, UIDialogs.ActionStyle.PRIMARY)); 0, UIDialogs.Action(getString(R.string.ok), { finish() }, UIDialogs.ActionStyle.PRIMARY));
@@ -0,0 +1,58 @@
package com.futo.platformplayer.activities
import android.annotation.SuppressLint
import android.os.Bundle
import android.widget.ImageButton
import androidx.appcompat.app.AppCompatActivity
import com.futo.platformplayer.*
import com.futo.platformplayer.views.fields.FieldForm
import com.futo.platformplayer.views.fields.IField
class DeveloperActivity : AppCompatActivity() {
private lateinit var _form: FieldForm;
private lateinit var _buttonBack: ImageButton;
fun getField(id: String): IField? {
return _form.findField(id);
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState);
DeveloperActivity._lastActivity = this;
setContentView(R.layout.activity_dev);
setNavigationBarColorAndIcons();
_buttonBack = findViewById(R.id.button_back);
_form = findViewById(R.id.settings_form);
_form.fromObject(SettingsDev.instance);
_form.onChanged.subscribe { _, _ ->
_form.setObjectValues();
SettingsDev.instance.save();
};
_buttonBack.setOnClickListener {
finish();
}
}
override fun finish() {
super.finish()
overridePendingTransition(R.anim.slide_lighten, R.anim.slide_out_up)
}
companion object {
//TODO: Temporary for solving Settings issues
@SuppressLint("StaticFieldLeak")
private var _lastActivity: DeveloperActivity? = null;
fun getActivity(): DeveloperActivity? {
val act = _lastActivity;
if(act != null)
return act;
return null;
}
}
}
@@ -1,47 +0,0 @@
package com.futo.platformplayer.activities
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.UpdateInstaller
import com.futo.platformplayer.UpdateNotificationManager
import com.futo.platformplayer.logging.Logger
import java.io.File
class InstallUpdateActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val version = intent.getIntExtra(UpdateNotificationManager.EXTRA_VERSION, 0)
val apkPath = intent.getStringExtra(UpdateNotificationManager.EXTRA_APK_PATH)
if (version == 0 || apkPath.isNullOrEmpty()) {
Logger.w("InstallUpdateActivity", "Missing version or apkPath")
finish()
return
}
val apkFile = File(apkPath)
if (!apkFile.exists()) {
Logger.w("InstallUpdateActivity", "APK file does not exist: $apkPath")
UIDialogs.Companion.toast(this, "Update file missing")
finish()
return
}
UpdateInstaller.startInstall(this, apkFile)
finish()
}
companion object {
fun createIntent(context: Context, version: Int, apkPath: String): Intent =
Intent(context, InstallUpdateActivity::class.java).apply {
putExtra(UpdateNotificationManager.EXTRA_VERSION, version)
putExtra(UpdateNotificationManager.EXTRA_APK_PATH, apkPath)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
}
}
@@ -15,7 +15,6 @@ import com.futo.platformplayer.api.media.platforms.js.SourceAuth
import com.futo.platformplayer.api.media.platforms.js.SourcePluginAuthConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginAuthConfig
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.matchesDomain
import com.futo.platformplayer.others.LoginWebViewClient import com.futo.platformplayer.others.LoginWebViewClient
import com.futo.platformplayer.setNavigationBarColorAndIcons import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
@@ -75,26 +74,9 @@ class LoginActivity : AppCompatActivity() {
finish(); finish();
}; };
var isFirstLoad = true; var isFirstLoad = true;
val loginWarnings = authConfig.loginWarnings?.toMutableList() ?: mutableListOf<SourcePluginAuthConfig.Warning>();
val uiMods = authConfig.uiMods?.toMutableList() ?: mutableListOf<SourcePluginAuthConfig.UIMod>();
var currentScale = 100;
var currentDesktop = false;
webViewClient.onPageLoaded.subscribe { view, url -> webViewClient.onPageLoaded.subscribe { view, url ->
_textUrl.setText(url ?: ""); _textUrl.setText(url ?: "");
if(loginWarnings.size > 0 && url != null) {
synchronized(loginWarnings) {
val warning = loginWarnings.find { url.matches(it.getRegex()) };
if(warning != null) {
if(warning.once == true)
loginWarnings.remove(warning);
UIDialogs.showDialog(this@LoginActivity, R.drawable.ic_warning_yellow, warning.text ?: "", warning.details ?: "", null, 0,
UIDialogs.Action("Understood", {
}, UIDialogs.ActionStyle.PRIMARY));
}
}
}
if(!isFirstLoad) if(!isFirstLoad)
return@subscribe; return@subscribe;
isFirstLoad = false; isFirstLoad = false;
@@ -104,35 +86,6 @@ class LoginActivity : AppCompatActivity() {
//TODO: Find most reliable way to wait for page js to finish //TODO: Find most reliable way to wait for page js to finish
view?.evaluateJavascript("setTimeout(()=> document.querySelector(\"${authConfig.loginButton}\")?.click(), 1000)", {}); view?.evaluateJavascript("setTimeout(()=> document.querySelector(\"${authConfig.loginButton}\")?.click(), 1000)", {});
} }
/*
var specifiedScale = false;
var specifiedDesktop = false;
if(uiMods.size > 0 && url != null) {
synchronized(uiMods) {
val uimod = uiMods.find { url.matches(it.getRegex()) };
if(uimod != null) {
if(uimod.scale != null) {
currentScale =(uimod.scale * 100).toInt();
_webView.setInitialScale(currentScale);
specifiedScale = true;
}
if(uimod.desktop != null && uimod.desktop) {
_webView.settings.useWideViewPort = true;
specifiedDesktop = true;
}
}
}
}
if(!specifiedScale && currentScale != 100) {
currentScale = (100).toInt();
_webView.setInitialScale(currentScale);
}
if(!specifiedDesktop && currentDesktop) {
_webView.settings.useWideViewPort = false;
currentDesktop = false;
}
*/
} }
_webView.settings.domStorageEnabled = true; _webView.settings.domStorageEnabled = true;
@@ -1,28 +1,26 @@
package com.futo.platformplayer.activities package com.futo.platformplayer.activities
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.AlertDialog import android.content.ComponentName
import android.app.UiModeManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.Color import android.media.AudioManager
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.StrictMode import android.os.StrictMode
import android.os.StrictMode.VmPolicy import android.os.StrictMode.VmPolicy
import android.util.Log import android.util.Log
import android.util.TypedValue import android.util.TypedValue
import android.view.View import android.view.View
import android.view.WindowManager
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.ImageView import android.widget.ImageView
import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.motion.widget.MotionLayout import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
@@ -32,20 +30,15 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentContainerView import androidx.fragment.app.FragmentContainerView
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.withStateAtLeast
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import com.futo.platformplayer.BuildConfig import com.futo.platformplayer.BuildConfig
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.RootInsetsController
import com.futo.platformplayer.Settings import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.models.video.LocalVideoDetails
import com.futo.platformplayer.casting.StateCasting import com.futo.platformplayer.casting.StateCasting
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.dp
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
import com.futo.platformplayer.fragment.mainactivity.main.ArticleDetailFragment
import com.futo.platformplayer.fragment.mainactivity.main.BrowserFragment import com.futo.platformplayer.fragment.mainactivity.main.BrowserFragment
import com.futo.platformplayer.fragment.mainactivity.main.BuyFragment import com.futo.platformplayer.fragment.mainactivity.main.BuyFragment
import com.futo.platformplayer.fragment.mainactivity.main.ChannelFragment import com.futo.platformplayer.fragment.mainactivity.main.ChannelFragment
@@ -53,29 +46,17 @@ import com.futo.platformplayer.fragment.mainactivity.main.CommentsFragment
import com.futo.platformplayer.fragment.mainactivity.main.ContentSearchResultsFragment import com.futo.platformplayer.fragment.mainactivity.main.ContentSearchResultsFragment
import com.futo.platformplayer.fragment.mainactivity.main.CreatorSearchResultsFragment import com.futo.platformplayer.fragment.mainactivity.main.CreatorSearchResultsFragment
import com.futo.platformplayer.fragment.mainactivity.main.CreatorsFragment import com.futo.platformplayer.fragment.mainactivity.main.CreatorsFragment
import com.futo.platformplayer.fragment.mainactivity.main.DeveloperFragment
import com.futo.platformplayer.fragment.mainactivity.main.DownloadsFragment import com.futo.platformplayer.fragment.mainactivity.main.DownloadsFragment
import com.futo.platformplayer.fragment.mainactivity.main.HistoryFragment import com.futo.platformplayer.fragment.mainactivity.main.HistoryFragment
import com.futo.platformplayer.fragment.mainactivity.main.HomeFragment import com.futo.platformplayer.fragment.mainactivity.main.HomeFragment
import com.futo.platformplayer.fragment.mainactivity.main.ImportPlaylistsFragment import com.futo.platformplayer.fragment.mainactivity.main.ImportPlaylistsFragment
import com.futo.platformplayer.fragment.mainactivity.main.ImportSubscriptionsFragment import com.futo.platformplayer.fragment.mainactivity.main.ImportSubscriptionsFragment
import com.futo.platformplayer.fragment.mainactivity.main.LibraryAlbumFragment
import com.futo.platformplayer.fragment.mainactivity.main.LibraryAlbumsFragment
import com.futo.platformplayer.fragment.mainactivity.main.LibraryArtistFragment
import com.futo.platformplayer.fragment.mainactivity.main.LibraryArtistsFragment
import com.futo.platformplayer.fragment.mainactivity.main.LibraryFilesFragment
import com.futo.platformplayer.fragment.mainactivity.main.LibraryFragment
import com.futo.platformplayer.fragment.mainactivity.main.LibrarySearchFragment
import com.futo.platformplayer.fragment.mainactivity.main.LibraryVideosFragment
import com.futo.platformplayer.fragment.mainactivity.main.LoginFragment
import com.futo.platformplayer.fragment.mainactivity.main.MainFragment import com.futo.platformplayer.fragment.mainactivity.main.MainFragment
import com.futo.platformplayer.fragment.mainactivity.main.PlaylistFragment import com.futo.platformplayer.fragment.mainactivity.main.PlaylistFragment
import com.futo.platformplayer.fragment.mainactivity.main.PlaylistSearchResultsFragment import com.futo.platformplayer.fragment.mainactivity.main.PlaylistSearchResultsFragment
import com.futo.platformplayer.fragment.mainactivity.main.PlaylistsFragment import com.futo.platformplayer.fragment.mainactivity.main.PlaylistsFragment
import com.futo.platformplayer.fragment.mainactivity.main.PostDetailFragment import com.futo.platformplayer.fragment.mainactivity.main.PostDetailFragment
import com.futo.platformplayer.fragment.mainactivity.main.RemotePlaylistFragment import com.futo.platformplayer.fragment.mainactivity.main.RemotePlaylistFragment
import com.futo.platformplayer.fragment.mainactivity.main.SettingsFragment
import com.futo.platformplayer.fragment.mainactivity.main.ShortsFragment
import com.futo.platformplayer.fragment.mainactivity.main.SourceDetailFragment import com.futo.platformplayer.fragment.mainactivity.main.SourceDetailFragment
import com.futo.platformplayer.fragment.mainactivity.main.SourcesFragment import com.futo.platformplayer.fragment.mainactivity.main.SourcesFragment
import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionGroupFragment import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionGroupFragment
@@ -84,11 +65,8 @@ import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionsFeedFragm
import com.futo.platformplayer.fragment.mainactivity.main.SuggestionsFragment import com.futo.platformplayer.fragment.mainactivity.main.SuggestionsFragment
import com.futo.platformplayer.fragment.mainactivity.main.TutorialFragment import com.futo.platformplayer.fragment.mainactivity.main.TutorialFragment
import com.futo.platformplayer.fragment.mainactivity.main.VideoDetailFragment import com.futo.platformplayer.fragment.mainactivity.main.VideoDetailFragment
import com.futo.platformplayer.fragment.mainactivity.main.VideoDetailFragment.State
import com.futo.platformplayer.fragment.mainactivity.main.WatchLaterFragment import com.futo.platformplayer.fragment.mainactivity.main.WatchLaterFragment
import com.futo.platformplayer.fragment.mainactivity.main.WebDetailFragment
import com.futo.platformplayer.fragment.mainactivity.topbar.AddTopBarFragment import com.futo.platformplayer.fragment.mainactivity.topbar.AddTopBarFragment
import com.futo.platformplayer.fragment.mainactivity.topbar.FilesTopBarFragment
import com.futo.platformplayer.fragment.mainactivity.topbar.GeneralTopBarFragment import com.futo.platformplayer.fragment.mainactivity.topbar.GeneralTopBarFragment
import com.futo.platformplayer.fragment.mainactivity.topbar.ImportTopBarFragment import com.futo.platformplayer.fragment.mainactivity.topbar.ImportTopBarFragment
import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFragment import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFragment
@@ -96,6 +74,7 @@ import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.ImportCache import com.futo.platformplayer.models.ImportCache
import com.futo.platformplayer.models.UrlVideoWithTime import com.futo.platformplayer.models.UrlVideoWithTime
import com.futo.platformplayer.receivers.MediaButtonReceiver
import com.futo.platformplayer.setNavigationBarColorAndIcons import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateBackup import com.futo.platformplayer.states.StateBackup
@@ -128,7 +107,7 @@ import java.io.PrintWriter
import java.io.StringWriter import java.io.StringWriter
import java.lang.reflect.InvocationTargetException import java.lang.reflect.InvocationTargetException
import java.util.LinkedList import java.util.LinkedList
import java.util.UUID import java.util.Queue
import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.ConcurrentLinkedQueue
@@ -160,7 +139,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
lateinit var _fragTopBarNavigation: NavigationTopBarFragment; lateinit var _fragTopBarNavigation: NavigationTopBarFragment;
lateinit var _fragTopBarImport: ImportTopBarFragment; lateinit var _fragTopBarImport: ImportTopBarFragment;
lateinit var _fragTopBarAdd: AddTopBarFragment; lateinit var _fragTopBarAdd: AddTopBarFragment;
lateinit var _fragTopBarFiles: FilesTopBarFragment;
//Frags BotBar //Frags BotBar
lateinit var _fragBotBarMenu: MenuBottomBarFragment; lateinit var _fragBotBarMenu: MenuBottomBarFragment;
@@ -168,8 +146,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
//Frags Main //Frags Main
lateinit var _fragMainHome: HomeFragment; lateinit var _fragMainHome: HomeFragment;
lateinit var _fragPostDetail: PostDetailFragment; lateinit var _fragPostDetail: PostDetailFragment;
lateinit var _fragArticleDetail: ArticleDetailFragment;
lateinit var _fragWebDetail: WebDetailFragment;
lateinit var _fragMainVideoSearchResults: ContentSearchResultsFragment; lateinit var _fragMainVideoSearchResults: ContentSearchResultsFragment;
lateinit var _fragMainCreatorSearchResults: CreatorSearchResultsFragment; lateinit var _fragMainCreatorSearchResults: CreatorSearchResultsFragment;
lateinit var _fragMainPlaylistSearchResults: PlaylistSearchResultsFragment; lateinit var _fragMainPlaylistSearchResults: PlaylistSearchResultsFragment;
@@ -185,7 +161,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
lateinit var _fragMainRemotePlaylist: RemotePlaylistFragment; lateinit var _fragMainRemotePlaylist: RemotePlaylistFragment;
lateinit var _fragWatchlist: WatchLaterFragment; lateinit var _fragWatchlist: WatchLaterFragment;
lateinit var _fragHistory: HistoryFragment; lateinit var _fragHistory: HistoryFragment;
lateinit var _fragShorts: ShortsFragment;
lateinit var _fragSourceDetail: SourceDetailFragment; lateinit var _fragSourceDetail: SourceDetailFragment;
lateinit var _fragDownloads: DownloadsFragment; lateinit var _fragDownloads: DownloadsFragment;
lateinit var _fragImportSubscriptions: ImportSubscriptionsFragment; lateinit var _fragImportSubscriptions: ImportSubscriptionsFragment;
@@ -193,17 +168,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
lateinit var _fragBuy: BuyFragment; lateinit var _fragBuy: BuyFragment;
lateinit var _fragSubGroup: SubscriptionGroupFragment; lateinit var _fragSubGroup: SubscriptionGroupFragment;
lateinit var _fragSubGroupList: SubscriptionGroupListFragment; lateinit var _fragSubGroupList: SubscriptionGroupListFragment;
lateinit var _fragLibrary: LibraryFragment;
lateinit var _fragLibraryAlbums: LibraryAlbumsFragment;
lateinit var _fragLibraryAlbum: LibraryAlbumFragment;
lateinit var _fragLibraryArtists: LibraryArtistsFragment;
lateinit var _fragLibraryArtist: LibraryArtistFragment;
lateinit var _fragLibraryVideos: LibraryVideosFragment;
lateinit var _fragLibrarySearch: LibrarySearchFragment;
lateinit var _fragLibraryFiles: LibraryFilesFragment;
lateinit var _fragSettings: SettingsFragment;
lateinit var _fragDeveloper: DeveloperFragment;
lateinit var _fragLogin: LoginFragment;
lateinit var _fragBrowser: BrowserFragment; lateinit var _fragBrowser: BrowserFragment;
@@ -211,8 +175,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
lateinit var _fragVideoDetail: VideoDetailFragment; lateinit var _fragVideoDetail: VideoDetailFragment;
//State //State
private val _queue: LinkedList<Pair<MainFragment, Any?>> = LinkedList(); private val _queue: Queue<Pair<MainFragment, Any?>> = LinkedList();
var fragCurrent: MainFragment? = null; private set; lateinit var fragCurrent: MainFragment private set;
private var _parameterCurrent: Any? = null; private var _parameterCurrent: Any? = null;
var fragBeforeOverlay: MainFragment? = null; private set; var fragBeforeOverlay: MainFragment? = null; private set;
@@ -221,10 +185,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
private var _isVisible = true; private var _isVisible = true;
private var _wasStopped = false; private var _wasStopped = false;
private var _privateModeEnabled = false
private var _pictureInPictureEnabled = false
private var _isFullscreen = false
private lateinit var _rootInsetsController: RootInsetsController
private val _urlQrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> private val _urlQrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data) val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
@@ -236,7 +196,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
} }
try { try {
lifecycleScope.launch { runBlocking {
handleUrlAll(content) handleUrlAll(content)
} }
} catch (e: Throwable) { } catch (e: Throwable) {
@@ -246,8 +206,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
} }
} }
val mainId = UUID.randomUUID().toString().substring(0, 5)
constructor() : super() { constructor() : super() {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
StrictMode.setVmPolicy( StrictMode.setVmPolicy(
@@ -299,29 +257,19 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
@UnstableApi @UnstableApi
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
Logger.w(TAG, "MainActivity Starting [$mainId]"); Logger.i(TAG, "MainActivity Starting");
StateApp.instance.setGlobalContext(this, lifecycleScope);
StateApp.instance.setGlobalContext(this, lifecycleScope, mainId);
StateApp.instance.mainAppStarting(this); StateApp.instance.mainAppStarting(this);
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val uiMode = getSystemService(UiModeManager::class.java)
uiMode.setApplicationNightMode(UiModeManager.MODE_NIGHT_YES)
}
setContentView(R.layout.activity_main); setContentView(R.layout.activity_main);
setNavigationBarColorAndIcons(); setNavigationBarColorAndIcons();
if (Settings.instance.playback.allowVideoToGoUnderCutout)
window.attributes.layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
runBlocking { runBlocking {
try { StatePlatform.instance.updateAvailableClients(this@MainActivity);
StatePlatform.instance.updateAvailableClients(this@MainActivity);
} catch (e: Throwable) {
Logger.e(TAG, "Unhandled exception in updateAvailableClients", e)
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
window.isNavigationBarContrastEnforced = false
} }
//Preload common files to memory //Preload common files to memory
@@ -329,9 +277,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
FragmentedStorage.get<Settings>(); FragmentedStorage.get<Settings>();
rootView = findViewById(R.id.rootView); rootView = findViewById(R.id.rootView);
_rootInsetsController = RootInsetsController.attach(this, rootView)
_rootInsetsController.setLightSystemBarAppearance(lightStatus = false, lightNav = false)
_fragContainerTopBar = findViewById(R.id.fragment_top_bar); _fragContainerTopBar = findViewById(R.id.fragment_top_bar);
_fragContainerMain = findViewById(R.id.fragment_main); _fragContainerMain = findViewById(R.id.fragment_main);
_fragContainerBotBar = findViewById(R.id.fragment_bottom_bar); _fragContainerBotBar = findViewById(R.id.fragment_bottom_bar);
@@ -348,7 +293,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_fragTopBarNavigation = NavigationTopBarFragment.newInstance(); _fragTopBarNavigation = NavigationTopBarFragment.newInstance();
_fragTopBarImport = ImportTopBarFragment.newInstance(); _fragTopBarImport = ImportTopBarFragment.newInstance();
_fragTopBarAdd = AddTopBarFragment.newInstance(); _fragTopBarAdd = AddTopBarFragment.newInstance();
_fragTopBarFiles = FilesTopBarFragment.newInstance();
//BotBars //BotBars
_fragBotBarMenu = MenuBottomBarFragment.newInstance(); _fragBotBarMenu = MenuBottomBarFragment.newInstance();
@@ -369,11 +313,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_fragMainPlaylist = PlaylistFragment.newInstance(); _fragMainPlaylist = PlaylistFragment.newInstance();
_fragMainRemotePlaylist = RemotePlaylistFragment.newInstance(); _fragMainRemotePlaylist = RemotePlaylistFragment.newInstance();
_fragPostDetail = PostDetailFragment.newInstance(); _fragPostDetail = PostDetailFragment.newInstance();
_fragArticleDetail = ArticleDetailFragment.newInstance();
_fragWebDetail = WebDetailFragment.newInstance();
_fragWatchlist = WatchLaterFragment.newInstance(); _fragWatchlist = WatchLaterFragment.newInstance();
_fragHistory = HistoryFragment.newInstance(); _fragHistory = HistoryFragment.newInstance();
_fragShorts = ShortsFragment.newInstance();
_fragSourceDetail = SourceDetailFragment.newInstance(); _fragSourceDetail = SourceDetailFragment.newInstance();
_fragDownloads = DownloadsFragment(); _fragDownloads = DownloadsFragment();
_fragImportSubscriptions = ImportSubscriptionsFragment.newInstance(); _fragImportSubscriptions = ImportSubscriptionsFragment.newInstance();
@@ -381,17 +322,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_fragBuy = BuyFragment.newInstance(); _fragBuy = BuyFragment.newInstance();
_fragSubGroup = SubscriptionGroupFragment.newInstance(); _fragSubGroup = SubscriptionGroupFragment.newInstance();
_fragSubGroupList = SubscriptionGroupListFragment.newInstance(); _fragSubGroupList = SubscriptionGroupListFragment.newInstance();
_fragLibrary = LibraryFragment.newInstance();
_fragLibraryAlbums = LibraryAlbumsFragment.newInstance();
_fragLibraryAlbum = LibraryAlbumFragment.newInstance();
_fragLibraryArtists = LibraryArtistsFragment.newInstance();
_fragLibraryArtist = LibraryArtistFragment.newInstance();
_fragLibraryVideos = LibraryVideosFragment.newInstance();
_fragLibraryFiles = LibraryFilesFragment.newInstance();
_fragLibrarySearch = LibrarySearchFragment.newInstance();
_fragSettings = SettingsFragment.newInstance();
_fragDeveloper = DeveloperFragment.newInstance();
_fragLogin = LoginFragment.newInstance();
_fragBrowser = BrowserFragment.newInstance(); _fragBrowser = BrowserFragment.newInstance();
@@ -410,17 +340,12 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
updateSegmentPaddings(); updateSegmentPaddings();
}; };
_fragVideoDetail.onTransitioning.subscribe { _fragVideoDetail.onTransitioning.subscribe {
if (it || _fragVideoDetail.state != VideoDetailFragment.State.MINIMIZED) { if (it || _fragVideoDetail.state != VideoDetailFragment.State.MINIMIZED)
Logger.i(TAG, "onTransition Setting elevation higher");
_fragContainerOverlay.elevation = _fragContainerOverlay.elevation =
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 15f, resources.displayMetrics); TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 15f, resources.displayMetrics);
} else
else {
Logger.i(TAG, "onTransition Setting elevation lower");
_fragContainerOverlay.elevation = _fragContainerOverlay.elevation =
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics); TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics);
}
} }
_fragVideoDetail.onCloseEvent.subscribe { _fragVideoDetail.onCloseEvent.subscribe {
@@ -429,18 +354,22 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_fragMainSubscriptionsFeed.setPreviewsEnabled(true); _fragMainSubscriptionsFeed.setPreviewsEnabled(true);
_fragContainerVideoDetail.visibility = View.INVISIBLE; _fragContainerVideoDetail.visibility = View.INVISIBLE;
updateSegmentPaddings(); updateSegmentPaddings();
updatePrivateModeVisibility()
}; };
_buttonIncognito = findViewById(R.id.incognito_button); _buttonIncognito = findViewById(R.id.incognito_button);
updatePrivateModeVisibility() _buttonIncognito.elevation = -99f;
_buttonIncognito.alpha = 0f;
StateApp.instance.privateModeChanged.subscribe { StateApp.instance.privateModeChanged.subscribe {
//Messing with visibility causes some issues with layout ordering? //Messing with visibility causes some issues with layout ordering?
_privateModeEnabled = it if (it) {
updatePrivateModeVisibility() _buttonIncognito.elevation = 99f;
_buttonIncognito.alpha = 1f;
} else {
_buttonIncognito.elevation = -99f;
_buttonIncognito.alpha = 0f;
}
} }
_buttonIncognito.setOnClickListener { _buttonIncognito.setOnClickListener {
if (!StateApp.instance.privateMode) if (!StateApp.instance.privateMode)
return@setOnClickListener; return@setOnClickListener;
@@ -457,23 +386,21 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
}; };
_fragVideoDetail.onFullscreenChanged.subscribe { _fragVideoDetail.onFullscreenChanged.subscribe {
Logger.i(TAG, "onFullscreenChanged ${it}"); Logger.i(TAG, "onFullscreenChanged ${it}");
_isFullscreen = it
updatePrivateModeVisibility()
if (it) { if (it) {
_rootInsetsController.enterFullscreen(allowCutoutShortEdges = Settings.instance.playback.allowVideoToGoUnderCutout) _buttonIncognito.elevation = -99f;
_buttonIncognito.alpha = 0f;
} else { } else {
_rootInsetsController.exitFullscreen() if (StateApp.instance.privateMode) {
_buttonIncognito.elevation = 99f;
_buttonIncognito.alpha = 1f;
} else {
_buttonIncognito.elevation = -99f;
_buttonIncognito.alpha = 0f;
}
} }
} }
_fragVideoDetail.onMinimize.subscribe {
updatePrivateModeVisibility()
}
_fragVideoDetail.onMaximized.subscribe {
updatePrivateModeVisibility()
}
StatePlayer.instance.also { StatePlayer.instance.also {
it.onQueueChanged.subscribe { shouldSwapCurrentItem -> it.onQueueChanged.subscribe { shouldSwapCurrentItem ->
if (!shouldSwapCurrentItem) { if (!shouldSwapCurrentItem) {
@@ -519,8 +446,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_fragMainPlaylist.topBar = _fragTopBarNavigation; _fragMainPlaylist.topBar = _fragTopBarNavigation;
_fragMainRemotePlaylist.topBar = _fragTopBarNavigation; _fragMainRemotePlaylist.topBar = _fragTopBarNavigation;
_fragPostDetail.topBar = _fragTopBarNavigation; _fragPostDetail.topBar = _fragTopBarNavigation;
_fragArticleDetail.topBar = _fragTopBarNavigation;
_fragWebDetail.topBar = _fragTopBarNavigation;
_fragWatchlist.topBar = _fragTopBarNavigation; _fragWatchlist.topBar = _fragTopBarNavigation;
_fragHistory.topBar = _fragTopBarNavigation; _fragHistory.topBar = _fragTopBarNavigation;
_fragSourceDetail.topBar = _fragTopBarNavigation; _fragSourceDetail.topBar = _fragTopBarNavigation;
@@ -528,16 +453,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_fragImportSubscriptions.topBar = _fragTopBarImport; _fragImportSubscriptions.topBar = _fragTopBarImport;
_fragImportPlaylists.topBar = _fragTopBarImport; _fragImportPlaylists.topBar = _fragTopBarImport;
_fragSubGroupList.topBar = _fragTopBarAdd; _fragSubGroupList.topBar = _fragTopBarAdd;
_fragLibrary.topBar = _fragTopBarGeneral;
_fragLibraryAlbums.topBar = _fragTopBarNavigation;
_fragLibraryAlbum.topBar = _fragTopBarNavigation;
_fragLibraryArtists.topBar = _fragTopBarNavigation;
_fragLibraryArtist.topBar = _fragTopBarNavigation;
_fragLibraryVideos.topBar = _fragTopBarNavigation;
_fragLibraryFiles.topBar = _fragTopBarFiles;
_fragLibrarySearch.topBar = _fragTopBarSearch;
_fragSettings.topBar = _fragTopBarNavigation;
_fragDeveloper.topBar = _fragTopBarNavigation;
_fragBrowser.topBar = _fragTopBarNavigation; _fragBrowser.topBar = _fragTopBarNavigation;
@@ -563,7 +478,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
defaultTab.action(_fragBotBarMenu); defaultTab.action(_fragBotBarMenu);
StateSubscriptions.instance; StateSubscriptions.instance;
fragCurrent?.onShown(null, false); fragCurrent.onShown(null, false);
//Other stuff //Other stuff
rootView.progress = 0f; rootView.progress = 0f;
@@ -618,10 +533,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
sharedPreferences.edit().putBoolean("IsFirstBoot", false).apply() sharedPreferences.edit().putBoolean("IsFirstBoot", false).apply()
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && Settings.instance.autoUpdate.isAutoUpdateEnabled()) {
requestNotificationPermissions("Grayjay uses notifications to inform you when a new app update is available.");
}
val submissionStatus = FragmentedStorage.get<StringStorage>("subscriptionSubmissionStatus") val submissionStatus = FragmentedStorage.get<StringStorage>("subscriptionSubmissionStatus")
val numSubscriptions = StateSubscriptions.instance.getSubscriptionCount() val numSubscriptions = StateSubscriptions.instance.getSubscriptionCount()
@@ -679,8 +590,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
}, UIDialogs.ActionStyle.PRIMARY) }, UIDialogs.ActionStyle.PRIMARY)
) )
} }
//startActivity(Intent(this, TestActivity::class.java))
} }
/* /*
@@ -704,23 +613,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
UIDialogs.toast(this, "No external file permissions\nExporting and auto backups will not work"); UIDialogs.toast(this, "No external file permissions\nExporting and auto backups will not work");
}*/ }*/
private var _qrCodeLoadingDialog: AlertDialog? = null
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
_rootInsetsController.onConfigurationChanged()
}
fun showUrlQrCodeScanner() { fun showUrlQrCodeScanner() {
try { try {
_qrCodeLoadingDialog = UIDialogs.showDialog(this, R.drawable.ic_loader_animated, true,
"Launching QR scanner",
"Make sure your camera is enabled", null, -2,
UIDialogs.Action("Close", {
_qrCodeLoadingDialog?.dismiss()
_qrCodeLoadingDialog = null
}));
val integrator = IntentIntegrator(this) val integrator = IntentIntegrator(this)
integrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE) integrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
integrator.setPrompt(getString(R.string.scan_a_qr_code)) integrator.setPrompt(getString(R.string.scan_a_qr_code))
@@ -736,46 +630,35 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
} }
} }
@OptIn(UnstableApi::class)
private fun updatePrivateModeVisibility() {
if (_privateModeEnabled && (_fragVideoDetail.state == State.CLOSED || !_pictureInPictureEnabled && !_isFullscreen)) {
_buttonIncognito.elevation = 99f;
_buttonIncognito.alpha = 1f;
_buttonIncognito.translationY = if (_fragVideoDetail.state == State.MINIMIZED) -60.dp(resources).toFloat() else 0f
} else {
_buttonIncognito.elevation = -99f;
_buttonIncognito.alpha = 0f;
}
}
override fun onResume() { override fun onResume() {
super.onResume(); super.onResume();
Logger.w(TAG, "onResume [$mainId]") Logger.v(TAG, "onResume")
_isVisible = true; _isVisible = true;
} }
override fun onPause() { override fun onPause() {
super.onPause(); super.onPause();
Logger.w(TAG, "onPause [$mainId]") Logger.v(TAG, "onPause")
_isVisible = false; _isVisible = false;
_qrCodeLoadingDialog?.dismiss()
_qrCodeLoadingDialog = null
} }
override fun onStop() { override fun onStop() {
super.onStop() super.onStop()
Logger.w(TAG, "onStop [$mainId]"); Logger.v(TAG, "_wasStopped = true");
_wasStopped = true; _wasStopped = true;
} }
override fun onNewIntent(intent: Intent) { override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent); super.onNewIntent(intent);
handleIntent(intent); handleIntent(intent);
} }
private fun handleIntent(intent: Intent) { private fun handleIntent(intent: Intent?) {
if (intent == null)
return;
Logger.i(TAG, "handleIntent started by " + intent.action); Logger.i(TAG, "handleIntent started by " + intent.action);
var targetData: String? = null; var targetData: String? = null;
when (intent.action) { when (intent.action) {
@@ -795,7 +678,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
"VIDEO" -> { "VIDEO" -> {
val url = intent.getStringExtra("VIDEO"); val url = intent.getStringExtra("VIDEO");
navigateWhenReady(_fragVideoDetail, url); navigate(_fragVideoDetail, url);
} }
"IMPORT_OPTIONS" -> { "IMPORT_OPTIONS" -> {
@@ -813,11 +696,11 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
"Sources" -> { "Sources" -> {
runBlocking { runBlocking {
StatePlatform.instance.updateAvailableClients(this@MainActivity, true) //Ideally this is not needed.. StatePlatform.instance.updateAvailableClients(this@MainActivity, true) //Ideally this is not needed..
navigateWhenReady(_fragMainSources); navigate(_fragMainSources);
} }
}; };
"BROWSE_PLUGINS" -> { "BROWSE_PLUGINS" -> {
navigateWhenReady(_fragBrowser, BrowserFragment.NavigateOptions("https://plugins.grayjay.app/phone.html", mapOf( navigate(_fragBrowser, BrowserFragment.NavigateOptions("https://plugins.grayjay.app/phone.html", mapOf(
Pair("grayjay") { req -> Pair("grayjay") { req ->
StateApp.instance.contextOrNull?.let { StateApp.instance.contextOrNull?.let {
if (it is MainActivity) { if (it is MainActivity) {
@@ -835,12 +718,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
try { try {
if (targetData != null) { if (targetData != null) {
lifecycleScope.launch(Dispatchers.Main) { runBlocking {
try { handleUrlAll(targetData)
handleUrlAll(targetData, intent)
} catch (e: Throwable) {
Logger.e(TAG, "Unhandled exception in handleUrlAll", e)
}
} }
} }
} catch (ex: Throwable) { } catch (ex: Throwable) {
@@ -848,9 +727,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
} }
} }
suspend fun handleUrlAll(url: String, openIntent: Intent? = null) { suspend fun handleUrlAll(url: String) {
val uri = Uri.parse(url) val uri = Uri.parse(url)
val intent = openIntent ?: this.intent;
when (uri.scheme) { when (uri.scheme) {
"grayjay" -> { "grayjay" -> {
if (url.startsWith("grayjay://license/")) { if (url.startsWith("grayjay://license/")) {
@@ -869,19 +747,19 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
startActivity(intent); startActivity(intent);
} else if (url.startsWith("grayjay://video/")) { } else if (url.startsWith("grayjay://video/")) {
val videoUrl = url.substring("grayjay://video/".length); val videoUrl = url.substring("grayjay://video/".length);
navigateWhenReady(_fragVideoDetail, videoUrl); navigate(_fragVideoDetail, videoUrl);
} else if (url.startsWith("grayjay://channel/")) { } else if (url.startsWith("grayjay://channel/")) {
val channelUrl = url.substring("grayjay://channel/".length); val channelUrl = url.substring("grayjay://channel/".length);
navigateWhenReady(_fragMainChannel, channelUrl); navigate(_fragMainChannel, channelUrl);
} }
} }
"content" -> { "content" -> {
if (!handleContent(url, intent?.type)) { if (!handleContent(url, intent.type)) {
UIDialogs.showSingleButtonDialog( UIDialogs.showSingleButtonDialog(
this, this,
R.drawable.ic_play, R.drawable.ic_play,
getString(R.string.unknown_content_format) + " [${url}]\n[${intent?.type}]", getString(R.string.unknown_content_format) + " [${url}]\n[${intent.type}]",
"Ok", "Ok",
{ }); { });
} }
@@ -938,29 +816,29 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
Logger.i(TAG, "handleUrl(url=$url) on IO"); Logger.i(TAG, "handleUrl(url=$url) on IO");
if (StatePlatform.instance.hasEnabledContentClient(url)) { if (StatePlatform.instance.hasEnabledVideoClient(url)) {
Logger.i(TAG, "handleUrl(url=$url) found video client"); Logger.i(TAG, "handleUrl(url=$url) found video client");
withContext(Dispatchers.Main) { lifecycleScope.launch(Dispatchers.Main) {
if (position > 0) if (position > 0)
navigateWhenReady(_fragVideoDetail, UrlVideoWithTime(url, position.toLong(), true)); navigate(_fragVideoDetail, UrlVideoWithTime(url, position.toLong(), true));
else else
navigateWhenReady(_fragVideoDetail, url); navigate(_fragVideoDetail, url);
_fragVideoDetail.maximizeVideoDetail(true); _fragVideoDetail.maximizeVideoDetail(true);
} }
return@withContext true; return@withContext true;
} else if (StatePlatform.instance.hasEnabledChannelClient(url)) { } else if (StatePlatform.instance.hasEnabledChannelClient(url)) {
Logger.i(TAG, "handleUrl(url=$url) found channel client"); Logger.i(TAG, "handleUrl(url=$url) found channel client");
withContext(Dispatchers.Main) { lifecycleScope.launch(Dispatchers.Main) {
navigateWhenReady(_fragMainChannel, url); navigate(_fragMainChannel, url);
delay(100); delay(100);
_fragVideoDetail.minimizeVideoDetail(); _fragVideoDetail.minimizeVideoDetail();
}; };
return@withContext true; return@withContext true;
} else if (StatePlatform.instance.hasEnabledPlaylistClient(url)) { } else if (StatePlatform.instance.hasEnabledPlaylistClient(url)) {
Logger.i(TAG, "handleUrl(url=$url) found playlist client"); Logger.i(TAG, "handleUrl(url=$url) found playlist client");
withContext(Dispatchers.Main) { lifecycleScope.launch(Dispatchers.Main) {
navigateWhenReady(_fragMainRemotePlaylist, url); navigate(_fragMainRemotePlaylist, url);
delay(100); delay(100);
_fragVideoDetail.minimizeVideoDetail(); _fragVideoDetail.minimizeVideoDetail();
}; };
@@ -1002,12 +880,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
} else if (file.lowercase().endsWith(".txt") || mime == "text/plain") { } else if (file.lowercase().endsWith(".txt") || mime == "text/plain") {
return handleUnknownText(String(data)); return handleUnknownText(String(data));
} }
else if (mime?.let { it.startsWith("video/") || it.startsWith("audio/") } ?: false) {
val mediaItem = LocalVideoDetails.fromContent(file, mime);
navigateWhenReady(_fragVideoDetail, mediaItem);
return true;
}
return false; return false;
} }
@@ -1122,7 +994,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
Logger.i(TAG, "handleFCast"); Logger.i(TAG, "handleFCast");
try { try {
StateCasting.instance.handleUrl(url) StateCasting.instance.handleUrl(this, url)
return true; return true;
} catch (e: Throwable) { } catch (e: Throwable) {
Log.e(TAG, "Failed to parse FCast URL '${url}'.", e) Log.e(TAG, "Failed to parse FCast URL '${url}'.", e)
@@ -1154,7 +1026,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
if (_fragVideoDetail.state == VideoDetailFragment.State.MAXIMIZED && _fragVideoDetail.onBackPressed()) if (_fragVideoDetail.state == VideoDetailFragment.State.MAXIMIZED && _fragVideoDetail.onBackPressed())
return; return;
if (!(fragCurrent?.onBackPressed() ?: true)) if (!fragCurrent.onBackPressed())
closeSegment(); closeSegment();
} }
@@ -1178,38 +1050,18 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
Logger.v(TAG, "onPictureInPictureModeChanged isInPictureInPictureMode=$isInPictureInPictureMode isStop=$isStop") Logger.v(TAG, "onPictureInPictureModeChanged isInPictureInPictureMode=$isInPictureInPictureMode isStop=$isStop")
_fragVideoDetail.onPictureInPictureModeChanged(isInPictureInPictureMode, isStop, newConfig); _fragVideoDetail.onPictureInPictureModeChanged(isInPictureInPictureMode, isStop, newConfig);
Logger.v(TAG, "onPictureInPictureModeChanged Ready"); Logger.v(TAG, "onPictureInPictureModeChanged Ready");
_pictureInPictureEnabled = isInPictureInPictureMode
updatePrivateModeVisibility()
} }
override fun onDestroy() { override fun onDestroy() {
super.onDestroy(); super.onDestroy();
Logger.w(TAG, "onDestroy [$mainId]") Logger.v(TAG, "onDestroy")
StateApp.instance.mainAppDestroyed(this, mainId); StateApp.instance.mainAppDestroyed(this);
} }
inline fun <reified T> isFragmentActive(): Boolean { inline fun <reified T> isFragmentActive(): Boolean {
return fragCurrent is T; return fragCurrent is T;
} }
fun navigateWhenReady(segment: MainFragment, parameter: Any? = null, withHistory: Boolean = true, isBack: Boolean = false) {
if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
navigate(segment, parameter, withHistory, isBack)
} else {
lifecycleScope.launch {
lifecycle.withStateAtLeast(Lifecycle.State.RESUMED) {
navigate(segment, parameter, withHistory, isBack)
}
}
}
}
inline fun <reified T : Fragment> navigate(parameter: Any? = null, withHistory: Boolean = true, isBack: Boolean = false) {
val segment = getFragment<T>();
navigate(segment as MainFragment, parameter, withHistory, isBack);
}
/** /**
* Navigate takes a MainFragment, and makes them the current main visible view * Navigate takes a MainFragment, and makes them the current main visible view
* A parameter can be provided which becomes available in the onShow of said fragment * A parameter can be provided which becomes available in the onShow of said fragment
@@ -1232,27 +1084,27 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
return; return;
} }
fragCurrent?.onHide(); fragCurrent.onHide();
if (segment.isMainView) { if (segment.isMainView) {
var transaction = supportFragmentManager.beginTransaction(); var transaction = supportFragmentManager.beginTransaction();
if (segment.topBar != null) { if (segment.topBar != null) {
if (segment.topBar != fragCurrent?.topBar) { if (segment.topBar != fragCurrent.topBar) {
transaction = transaction transaction = transaction
.show(segment.topBar as Fragment) .show(segment.topBar as Fragment)
.replace(R.id.fragment_top_bar, segment.topBar as Fragment); .replace(R.id.fragment_top_bar, segment.topBar as Fragment);
fragCurrent?.topBar?.onHide(); fragCurrent.topBar?.onHide();
} }
} else if (fragCurrent?.topBar != null) } else if (fragCurrent.topBar != null)
transaction.hide(fragCurrent?.topBar as Fragment); transaction.hide(fragCurrent.topBar as Fragment);
transaction = transaction.replace(R.id.fragment_main, segment); transaction = transaction.replace(R.id.fragment_main, segment);
if (segment.hasBottomBar) { if (segment.hasBottomBar) {
if (!(fragCurrent?.hasBottomBar ?: false)) if (!fragCurrent.hasBottomBar)
transaction = transaction.show(_fragBotBarMenu); transaction = transaction.show(_fragBotBarMenu);
} else { } else {
if (fragCurrent?.hasBottomBar ?: false) if (fragCurrent.hasBottomBar)
transaction = transaction.hide(_fragBotBarMenu); transaction = transaction.hide(_fragBotBarMenu);
} }
transaction.commitNow(); transaction.commitNow();
@@ -1265,12 +1117,13 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
} }
} }
if (fragCurrent?.isHistory ?: false && withHistory && _queue.lastOrNull() != fragCurrent) if (fragCurrent.isHistory && withHistory && _queue.lastOrNull() != fragCurrent)
_queue.add(Pair(fragCurrent!!, _parameterCurrent)); _queue.add(Pair(fragCurrent, _parameterCurrent));
if (segment.isOverlay && !(fragCurrent?.isOverlay ?: false) && withHistory)// && fragCurrent.isHistory) if (segment.isOverlay && !fragCurrent.isOverlay && withHistory)// && fragCurrent.isHistory)
fragBeforeOverlay = fragCurrent; fragBeforeOverlay = fragCurrent;
fragCurrent = segment; fragCurrent = segment;
_parameterCurrent = parameter; _parameterCurrent = parameter;
} }
@@ -1301,21 +1154,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
if (_fragVideoDetail.state == VideoDetailFragment.State.CLOSED) { if (_fragVideoDetail.state == VideoDetailFragment.State.CLOSED) {
finish(); finish();
} else { } else {
//UIDialogs.toast("Grayjay continues in background because of an open video.")
if(Settings.instance.playback.isBackgroundPictureInPicture()) {
try {
_fragVideoDetail._viewDetail?.startPictureInPicture();
_fragVideoDetail?.forcePictureInPicture();
} catch (ex: Throwable) {
} //Fail silently
}
else
moveTaskToBack(false);
/*
UIDialogs.showConfirmationDialog(this, "There is a video playing, are you sure you want to exit the app?", { UIDialogs.showConfirmationDialog(this, "There is a video playing, are you sure you want to exit the app?", {
finish(); finish();
}) })
*/
} }
} }
} }
@@ -1334,7 +1175,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
VideoDetailFragment::class -> _fragVideoDetail as T; VideoDetailFragment::class -> _fragVideoDetail as T;
MenuBottomBarFragment::class -> _fragBotBarMenu as T; MenuBottomBarFragment::class -> _fragBotBarMenu as T;
GeneralTopBarFragment::class -> _fragTopBarGeneral as T; GeneralTopBarFragment::class -> _fragTopBarGeneral as T;
FilesTopBarFragment::class -> _fragTopBarFiles as T;
SearchTopBarFragment::class -> _fragTopBarSearch as T; SearchTopBarFragment::class -> _fragTopBarSearch as T;
CreatorsFragment::class -> _fragMainSubscriptions as T; CreatorsFragment::class -> _fragMainSubscriptions as T;
CommentsFragment::class -> _fragMainComments as T; CommentsFragment::class -> _fragMainComments as T;
@@ -1346,11 +1186,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
PlaylistFragment::class -> _fragMainPlaylist as T; PlaylistFragment::class -> _fragMainPlaylist as T;
RemotePlaylistFragment::class -> _fragMainRemotePlaylist as T; RemotePlaylistFragment::class -> _fragMainRemotePlaylist as T;
PostDetailFragment::class -> _fragPostDetail as T; PostDetailFragment::class -> _fragPostDetail as T;
ArticleDetailFragment::class -> _fragArticleDetail as T;
WebDetailFragment::class -> _fragWebDetail as T;
WatchLaterFragment::class -> _fragWatchlist as T; WatchLaterFragment::class -> _fragWatchlist as T;
HistoryFragment::class -> _fragHistory as T; HistoryFragment::class -> _fragHistory as T;
ShortsFragment::class -> _fragShorts as T;
SourceDetailFragment::class -> _fragSourceDetail as T; SourceDetailFragment::class -> _fragSourceDetail as T;
DownloadsFragment::class -> _fragDownloads as T; DownloadsFragment::class -> _fragDownloads as T;
ImportSubscriptionsFragment::class -> _fragImportSubscriptions as T; ImportSubscriptionsFragment::class -> _fragImportSubscriptions as T;
@@ -1359,17 +1196,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
BuyFragment::class -> _fragBuy as T; BuyFragment::class -> _fragBuy as T;
SubscriptionGroupFragment::class -> _fragSubGroup as T; SubscriptionGroupFragment::class -> _fragSubGroup as T;
SubscriptionGroupListFragment::class -> _fragSubGroupList as T; SubscriptionGroupListFragment::class -> _fragSubGroupList as T;
LibraryFragment::class -> _fragLibrary as T;
LibraryAlbumsFragment::class -> _fragLibraryAlbums as T;
LibraryAlbumFragment::class -> _fragLibraryAlbum as T;
LibraryArtistsFragment::class -> _fragLibraryArtists as T;
LibraryArtistFragment::class -> _fragLibraryArtist as T;
LibraryVideosFragment::class -> _fragLibraryVideos as T;
LibraryFilesFragment::class -> _fragLibraryFiles as T;
LibrarySearchFragment::class -> _fragLibrarySearch as T;
SettingsFragment:: class -> _fragSettings as T;
DeveloperFragment::class -> _fragDeveloper as T;
LoginFragment::class -> _fragLogin as T;
else -> throw IllegalArgumentException("Fragment type ${T::class.java.name} is not available in MainActivity"); else -> throw IllegalArgumentException("Fragment type ${T::class.java.name} is not available in MainActivity");
} }
} }
@@ -1377,7 +1203,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
private fun updateSegmentPaddings() { private fun updateSegmentPaddings() {
var paddingBottom = 0f; var paddingBottom = 0f;
if (fragCurrent?.hasBottomBar ?: false) if (fragCurrent.hasBottomBar)
paddingBottom += HEIGHT_MENU_DP; paddingBottom += HEIGHT_MENU_DP;
_fragContainerOverlay.setPadding( _fragContainerOverlay.setPadding(
@@ -1394,23 +1220,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
); );
} }
var _callbackPermissionAudio: ((Boolean)->Unit)? = null;
var _callbackPermissionVideo: ((Boolean)->Unit)? = null;
val permissionReqAudio = registerForActivityResult(ActivityResultContracts.RequestPermission(), { isGranted ->
_callbackPermissionAudio?.invoke(isGranted);
});
val permissionReqVideo = registerForActivityResult(ActivityResultContracts.RequestPermission(), { isGranted ->
_callbackPermissionVideo?.invoke(isGranted);
});
fun requestPermissionAudio(cb: ((Boolean)->Unit)? = null) {
_callbackPermissionAudio = cb;
permissionReqAudio.launch(android.Manifest.permission.READ_MEDIA_AUDIO);
}
fun requestPermissionVideo(cb: ((Boolean)->Unit)? = null) {
_callbackPermissionVideo = cb;
permissionReqVideo.launch(android.Manifest.permission.READ_MEDIA_VIDEO);
}
val notifPermission = "android.permission.POST_NOTIFICATIONS"; val notifPermission = "android.permission.POST_NOTIFICATIONS";
val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean -> val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
@@ -13,18 +13,13 @@ import android.view.View
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.setNavigationBarColorAndIcons import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateApp.Companion.withContext
import com.futo.platformplayer.states.StatePolycentric import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.views.buttons.BigButton import com.futo.platformplayer.views.buttons.BigButton
import com.futo.platformplayer.activities.QRCodeFullscreenActivity
import com.futo.polycentric.core.ContentType import com.futo.polycentric.core.ContentType
import com.futo.polycentric.core.SignedEvent import com.futo.polycentric.core.SignedEvent
import com.futo.polycentric.core.StorageTypeCRDTItem import com.futo.polycentric.core.StorageTypeCRDTItem
@@ -32,13 +27,8 @@ import com.futo.polycentric.core.StorageTypeCRDTSetItem
import com.futo.polycentric.core.Store import com.futo.polycentric.core.Store
import com.futo.polycentric.core.toBase64Url import com.futo.polycentric.core.toBase64Url
import com.google.zxing.BarcodeFormat import com.google.zxing.BarcodeFormat
import com.google.zxing.EncodeHintType
import com.google.zxing.MultiFormatWriter import com.google.zxing.MultiFormatWriter
import com.google.zxing.common.BitMatrix import com.google.zxing.common.BitMatrix
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import userpackage.Protocol import userpackage.Protocol
import userpackage.Protocol.ExportBundle import userpackage.Protocol.ExportBundle
import userpackage.Protocol.URLInfo import userpackage.Protocol.URLInfo
@@ -46,26 +36,9 @@ import userpackage.Protocol.URLInfo
class PolycentricBackupActivity : AppCompatActivity() { class PolycentricBackupActivity : AppCompatActivity() {
private lateinit var _buttonShare: BigButton; private lateinit var _buttonShare: BigButton;
private lateinit var _buttonCopy: BigButton; private lateinit var _buttonCopy: BigButton;
private lateinit var _buttonExportFile: BigButton;
private lateinit var _imageQR: ImageView; private lateinit var _imageQR: ImageView;
private lateinit var _exportBundle: String; private lateinit var _exportBundle: String;
private lateinit var _textQR: TextView; private lateinit var _textQR: TextView;
private lateinit var _textQRHint: TextView;
private lateinit var _loader: View
private val _createDocumentLauncher = registerForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { uri ->
uri?.let { fileUri ->
try {
contentResolver.openOutputStream(fileUri)?.use { outputStream ->
outputStream.write(_exportBundle.toByteArray())
}
UIDialogs.toast(this, getString(R.string.profile_saved_successfully))
} catch (e: Exception) {
Logger.e(TAG, "Failed to write to document", e)
UIDialogs.toast(this, "Failed to save profile: ${e.message}")
}
}
}
override fun attachBaseContext(newBase: Context?) { override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase)) super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
@@ -76,75 +49,24 @@ class PolycentricBackupActivity : AppCompatActivity() {
setContentView(R.layout.activity_polycentric_backup); setContentView(R.layout.activity_polycentric_backup);
setNavigationBarColorAndIcons(); setNavigationBarColorAndIcons();
_buttonShare = findViewById(R.id.button_share) _buttonShare = findViewById(R.id.button_share);
_buttonCopy = findViewById(R.id.button_copy) _buttonCopy = findViewById(R.id.button_copy);
_buttonExportFile = findViewById(R.id.button_export_file) _imageQR = findViewById(R.id.image_qr);
_imageQR = findViewById(R.id.image_qr) _textQR = findViewById(R.id.text_qr);
_textQR = findViewById(R.id.text_qr)
_textQRHint = findViewById(R.id.text_qr_hint)
_loader = findViewById(R.id.progress_loader)
findViewById<ImageButton>(R.id.button_back).setOnClickListener { findViewById<ImageButton>(R.id.button_back).setOnClickListener {
finish(); finish();
}; };
_imageQR.visibility = View.INVISIBLE _exportBundle = createExportBundle();
_textQR.visibility = View.INVISIBLE
_textQRHint.visibility = View.INVISIBLE
_loader.visibility = View.VISIBLE
_buttonShare.visibility = View.INVISIBLE
_buttonCopy.visibility = View.INVISIBLE
_buttonExportFile.visibility = View.INVISIBLE
lifecycleScope.launch { try {
val bundle = withContext(Dispatchers.IO) { createExportBundle() } val dimension = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 200f, resources.displayMetrics).toInt();
_exportBundle = bundle val qrCodeBitmap = generateQRCode(_exportBundle, dimension, dimension);
Logger.i(TAG, "Export bundle created, length: ${bundle.length}") _imageQR.setImageBitmap(qrCodeBitmap);
} catch (e: Exception) {
try { Logger.e(TAG, getString(R.string.failed_to_generate_qr_code), e);
val pair = withContext(Dispatchers.IO) { _imageQR.visibility = View.INVISIBLE;
if (!isContentSuitableForQRCode(bundle)) { _textQR.visibility = View.INVISIBLE;
throw Exception("Data too big for QR code generation")
}
val dimension = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 200f, resources.displayMetrics
).toInt()
val qr = generateQRCode(bundle, dimension, dimension)
Pair(bundle, qr)
}
_imageQR.setImageBitmap(pair.second)
_imageQR.visibility = View.VISIBLE
_textQR.visibility = View.VISIBLE
_textQRHint.visibility = View.VISIBLE
_buttonShare.visibility = View.VISIBLE
_buttonCopy.visibility = View.VISIBLE
_imageQR.setOnClickListener {
val intent = QRCodeFullscreenActivity.createIntent(this@PolycentricBackupActivity, _exportBundle)
startActivity(intent)
}
} catch (e: Exception) {
val byteSize = bundle.toByteArray(Charsets.UTF_8).size
Logger.e(TAG, "QR code generation failed. Bundle length: ${bundle.length} chars, ${byteSize} bytes, Error: ${e.message}", e)
if (e.message?.contains("Data too big") == true) {
_textQR.text = getString(R.string.qr_code_too_large_use_file_export)
_buttonExportFile.visibility = View.VISIBLE
} else {
_textQR.text = getString(R.string.failed_to_generate_qr_code)
}
_textQR.visibility = View.VISIBLE
_textQRHint.visibility = View.INVISIBLE
_buttonShare.visibility = View.VISIBLE
_buttonCopy.visibility = View.VISIBLE
// Hide QR image since generation failed
_imageQR.visibility = View.INVISIBLE
} finally {
_loader.visibility = View.GONE
}
} }
_buttonShare.onClick.subscribe { _buttonShare.onClick.subscribe {
@@ -157,29 +79,11 @@ class PolycentricBackupActivity : AppCompatActivity() {
val clip = ClipData.newPlainText(getString(R.string.copied_text), _exportBundle); val clip = ClipData.newPlainText(getString(R.string.copied_text), _exportBundle);
clipboard.setPrimaryClip(clip); clipboard.setPrimaryClip(clip);
}; };
_buttonExportFile.onClick.subscribe {
val fileName = "polycentric_profile_${System.currentTimeMillis()}.txt"
_createDocumentLauncher.launch(fileName)
};
}
private fun isContentSuitableForQRCode(content: String): Boolean {
val bytes = content.toByteArray(Charsets.UTF_8)
return bytes.size <= 2300 // QR Code Version 40 with Error Correction Level M can hold ~2331 bytes
} }
private fun generateQRCode(content: String, width: Int, height: Int): Bitmap { private fun generateQRCode(content: String, width: Int, height: Int): Bitmap {
if (!isContentSuitableForQRCode(content)) { val bitMatrix = MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height);
throw Exception("Data too big for QR code generation") return bitMatrixToBitmap(bitMatrix);
}
val hints = java.util.EnumMap<EncodeHintType, Any>(EncodeHintType::class.java)
hints[EncodeHintType.ERROR_CORRECTION] = ErrorCorrectionLevel.M
hints[EncodeHintType.MARGIN] = 1
val bitMatrix = MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height, hints)
return bitMatrixToBitmap(bitMatrix)
} }
private fun bitMatrixToBitmap(matrix: BitMatrix): Bitmap { private fun bitMatrixToBitmap(matrix: BitMatrix): Bitmap {
@@ -270,8 +174,7 @@ class PolycentricBackupActivity : AppCompatActivity() {
.setBody(exportBundle.toByteString()) .setBody(exportBundle.toByteString())
.build(); .build();
val data = urlInfo.toByteArray() return "polycentric://" + urlInfo.toByteArray().toBase64Url()
return "polycentric://" + data.toBase64Url()
} }
companion object { companion object {
@@ -32,166 +32,100 @@ import userpackage.Protocol
import userpackage.Protocol.ExportBundle import userpackage.Protocol.ExportBundle
class PolycentricImportProfileActivity : AppCompatActivity() { class PolycentricImportProfileActivity : AppCompatActivity() {
private lateinit var _buttonHelp: ImageButton private lateinit var _buttonHelp: ImageButton;
private lateinit var _buttonScanProfile: LinearLayout private lateinit var _buttonScanProfile: LinearLayout;
private lateinit var _buttonImportFile: LinearLayout private lateinit var _buttonImportProfile: LinearLayout;
private lateinit var _buttonImportProfile: LinearLayout private lateinit var _editProfile: EditText;
private lateinit var _editProfile: EditText private lateinit var _loaderOverlay: LoaderOverlay;
private lateinit var _loaderOverlay: LoaderOverlay
private val _qrCodeResultLauncher = private val _qrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
val scanResult = scanResult?.let {
IntentIntegrator.parseActivityResult(result.resultCode, result.data) if (it.contents != null) {
scanResult?.let { val scannedUrl = it.contents
if (it.contents != null) { import(scannedUrl)
val scannedUrl = it.contents
import(scannedUrl)
}
}
}
private val _filePickerLauncher =
registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
uri?.let { fileUri ->
try {
// Check file size before reading
val fileSize =
contentResolver.openFileDescriptor(fileUri, "r")?.statSize ?: 0
val maxFileSize = 10 * 1024 * 1024 // 10MB limit
if (fileSize > maxFileSize) {
UIDialogs.toast(this, "File too large. Maximum size is 10MB.")
return@let
}
if (fileSize == 0L) {
UIDialogs.toast(this, "Selected file is empty.")
return@let
}
val content =
contentResolver
.openInputStream(fileUri)
?.bufferedReader()
?.readText()
content?.let { fileContent ->
val trimmedContent = fileContent.trim()
// Check if content is empty after trimming
if (trimmedContent.isEmpty()) {
UIDialogs.toast(this, "Selected file contains no data.")
return@let
}
// Check if content looks like a valid polycentric URL
if (!trimmedContent.startsWith("polycentric://")) {
UIDialogs.toast(
this,
"Selected file does not contain a valid polycentric profile URL."
)
return@let
}
import(trimmedContent)
}
?: run { UIDialogs.toast(this, "Could not read file content.") }
} catch (e: SecurityException) {
Logger.e(TAG, "Security exception reading file", e)
UIDialogs.toast(this, "Permission denied to read file.")
} catch (e: OutOfMemoryError) {
Logger.e(TAG, "Out of memory reading file", e)
UIDialogs.toast(this, "File too large to process.")
} catch (e: Exception) {
Logger.e(TAG, "Failed to read file", e)
UIDialogs.toast(this, "Failed to read file: ${e.message}")
}
}
} }
}
}
override fun attachBaseContext(newBase: Context?) { override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase)) super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState);
setContentView(R.layout.activity_polycentric_import_profile) setContentView(R.layout.activity_polycentric_import_profile);
setNavigationBarColorAndIcons() setNavigationBarColorAndIcons();
_buttonHelp = findViewById(R.id.button_help) _buttonHelp = findViewById(R.id.button_help);
_buttonScanProfile = findViewById(R.id.button_scan_profile) _buttonScanProfile = findViewById(R.id.button_scan_profile);
_buttonImportFile = findViewById(R.id.button_import_file) _buttonImportProfile = findViewById(R.id.button_import_profile);
_buttonImportProfile = findViewById(R.id.button_import_profile) _loaderOverlay = findViewById(R.id.loader_overlay);
_loaderOverlay = findViewById(R.id.loader_overlay) _editProfile = findViewById(R.id.edit_profile);
_editProfile = findViewById(R.id.edit_profile) findViewById<ImageButton>(R.id.button_back).setOnClickListener {
findViewById<ImageButton>(R.id.button_back).setOnClickListener { finish() } finish();
};
_buttonHelp.setOnClickListener { _buttonHelp.setOnClickListener {
startActivity(Intent(this, PolycentricWhyActivity::class.java)) startActivity(Intent(this, PolycentricWhyActivity::class.java));
} };
_buttonScanProfile.setOnClickListener { _buttonScanProfile.setOnClickListener {
val integrator = IntentIntegrator(this) val integrator = IntentIntegrator(this)
integrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE) integrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
integrator.setPrompt(getString(R.string.scan_a_qr_code)) integrator.setPrompt(getString(R.string.scan_a_qr_code))
integrator.setOrientationLocked(true) integrator.setOrientationLocked(true);
integrator.setCameraId(0) integrator.setCameraId(0)
integrator.setBeepEnabled(false) integrator.setBeepEnabled(false)
integrator.setBarcodeImageEnabled(true) integrator.setBarcodeImageEnabled(true)
integrator.setCaptureActivity(QRCaptureActivity::class.java) integrator.setCaptureActivity(QRCaptureActivity::class.java);
_qrCodeResultLauncher.launch(integrator.createScanIntent()) _qrCodeResultLauncher.launch(integrator.createScanIntent())
} };
_buttonImportFile.setOnClickListener { _filePickerLauncher.launch("text/plain") }
_buttonImportProfile.setOnClickListener { _buttonImportProfile.setOnClickListener {
if (_editProfile.text.isEmpty()) { if (_editProfile.text.isEmpty()) {
UIDialogs.toast(this, getString(R.string.text_field_does_not_contain_any_data)) UIDialogs.toast(this, getString(R.string.text_field_does_not_contain_any_data));
return@setOnClickListener return@setOnClickListener;
} }
import(_editProfile.text.toString()) import(_editProfile.text.toString());
} };
val url = intent.getStringExtra("url") val url = intent.getStringExtra("url");
if (url != null) { if (url != null) {
import(url) import(url);
} }
} }
private fun import(url: String) { private fun import(url: String) {
if (!url.startsWith("polycentric://")) { if (!url.startsWith("polycentric://")) {
UIDialogs.toast(this, getString(R.string.not_a_valid_url)) UIDialogs.toast(this, getString(R.string.not_a_valid_url));
return return;
} }
_loaderOverlay.show() _loaderOverlay.show()
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
try { try {
val data = url.substring("polycentric://".length).base64UrlToByteArray() val data = url.substring("polycentric://".length).base64UrlToByteArray();
val urlInfo = Protocol.URLInfo.parseFrom(data) val urlInfo = Protocol.URLInfo.parseFrom(data);
if (urlInfo.urlType != 3L) { if (urlInfo.urlType != 3L) {
throw Exception("Expected urlInfo struct of type ExportBundle") throw Exception("Expected urlInfo struct of type ExportBundle")
} }
val exportBundle = ExportBundle.parseFrom(urlInfo.body) val exportBundle = ExportBundle.parseFrom(urlInfo.body);
val keyPair = KeyPair.fromProto(exportBundle.keyPair) val keyPair = KeyPair.fromProto(exportBundle.keyPair);
val existingProcessSecret = Store.instance.getProcessSecret(keyPair.publicKey) val existingProcessSecret = Store.instance.getProcessSecret(keyPair.publicKey);
if (existingProcessSecret != null) { if (existingProcessSecret != null) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
UIDialogs.toast( UIDialogs.toast(this@PolycentricImportProfileActivity, getString(R.string.this_profile_is_already_imported));
this@PolycentricImportProfileActivity,
getString(R.string.this_profile_is_already_imported)
)
} }
return@launch return@launch;
} }
val processSecret = ProcessSecret(keyPair, Process.random()) val processSecret = ProcessSecret(keyPair, Process.random());
Store.instance.addProcessSecret(processSecret) Store.instance.addProcessSecret(processSecret);
try { try {
PolycentricStorage.instance.addProcessSecret(processSecret) PolycentricStorage.instance.addProcessSecret(processSecret)
@@ -199,43 +133,37 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
Logger.e(TAG, "Failed to save process secret to secret storage.", e) Logger.e(TAG, "Failed to save process secret to secret storage.", e)
} }
val processHandle = processSecret.toProcessHandle() val processHandle = processSecret.toProcessHandle();
for (e in exportBundle.events.eventsList) { for (e in exportBundle.events.eventsList) {
try { try {
val se = SignedEvent.fromProto(e) val se = SignedEvent.fromProto(e);
Store.instance.putSignedEvent(se) Store.instance.putSignedEvent(se);
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.w(TAG, "Ignored invalid event", e) Logger.w(TAG, "Ignored invalid event", e);
} }
} }
StatePolycentric.instance.setProcessHandle(processHandle) StatePolycentric.instance.setProcessHandle(processHandle);
processHandle.fullyBackfillClient(ApiMethods.SERVER) processHandle.fullyBackfillClient(ApiMethods.SERVER);
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
startActivity( startActivity(Intent(this@PolycentricImportProfileActivity, PolycentricProfileActivity::class.java));
Intent( finish();
this@PolycentricImportProfileActivity,
PolycentricProfileActivity::class.java
)
)
finish()
} }
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.w(TAG, "Failed to import profile", e) Logger.w(TAG, "Failed to import profile", e);
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
UIDialogs.toast( UIDialogs.toast(this@PolycentricImportProfileActivity, getString(R.string.failed_to_import_profile) + " '${e.message}'");
this@PolycentricImportProfileActivity,
getString(R.string.failed_to_import_profile) + " '${e.message}'"
)
} }
} finally { } finally {
withContext(Dispatchers.Main) { _loaderOverlay.hide() } withContext(Dispatchers.Main) {
_loaderOverlay.hide();
}
} }
} }
} }
companion object { companion object {
private const val TAG = "PolycentricImportProfileActivity" private const val TAG = "PolycentricImportProfileActivity";
} }
} }
@@ -1,147 +0,0 @@
package com.futo.platformplayer.activities
import android.content.Context
import android.os.Bundle
import android.widget.ImageButton
import android.widget.SeekBar
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import com.futo.platformplayer.polycentric.ModerationsManager
import com.futo.platformplayer.R
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.setNavigationBarColorAndIcons
class PolycentricModerationActivity : AppCompatActivity() {
private lateinit var _seekbarOffensive: SeekBar
private lateinit var _seekbarExplicit: SeekBar
private lateinit var _seekbarViolence: SeekBar
private lateinit var _textOffensiveDesc: TextView
private lateinit var _textExplicitDesc: TextView
private lateinit var _textViolenceDesc: TextView
private lateinit var _textOffensiveValue: TextView
private lateinit var _textExplicitValue: TextView
private lateinit var _textViolenceValue: TextView
private lateinit var _moderationsManager: ModerationsManager
override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_polycentric_moderation)
setNavigationBarColorAndIcons()
_moderationsManager = ModerationsManager.getInstance()
try {
_moderationsManager = ModerationsManager.getInstance()
} catch (e: IllegalStateException) {
finish()
return
}
_seekbarOffensive = findViewById(R.id.seekbar_offensive)
_seekbarExplicit = findViewById(R.id.seekbar_explicit)
_seekbarViolence = findViewById(R.id.seekbar_violence)
_textOffensiveDesc = findViewById(R.id.text_offensive_desc)
_textExplicitDesc = findViewById(R.id.text_explicit_desc)
_textViolenceDesc = findViewById(R.id.text_violence_desc)
_textOffensiveValue = findViewById(R.id.text_offensive_value)
_textExplicitValue = findViewById(R.id.text_explicit_value)
_textViolenceValue = findViewById(R.id.text_violence_value)
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
finish()
}
loadSettings()
setupListeners()
}
private fun loadSettings() {
val levels = _moderationsManager.moderationLevels.value ?: mapOf()
val offensiveLevel = levels["hate"] ?: 2
val explicitLevel = levels["sexual"] ?: 1
val violenceLevel = levels["violence"] ?: 1
_seekbarOffensive.progress = offensiveLevel
_seekbarExplicit.progress = explicitLevel
_seekbarViolence.progress = violenceLevel
updateDescriptionText(_seekbarOffensive, _textOffensiveDesc, _textOffensiveValue, getOffensiveDescriptions())
updateDescriptionText(_seekbarExplicit, _textExplicitDesc, _textExplicitValue, getExplicitDescriptions())
updateDescriptionText(_seekbarViolence, _textViolenceDesc, _textViolenceValue, getViolenceDescriptions())
}
private fun setupListeners() {
_seekbarOffensive.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
updateDescriptionText(seekBar, _textOffensiveDesc, _textOffensiveValue, getOffensiveDescriptions())
if (fromUser) {
_moderationsManager.setModerationLevel("hate", progress)
}
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {}
override fun onStopTrackingTouch(seekBar: SeekBar?) {}
})
_seekbarExplicit.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
updateDescriptionText(seekBar, _textExplicitDesc, _textExplicitValue, getExplicitDescriptions())
if (fromUser) {
_moderationsManager.setModerationLevel("sexual", progress)
}
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {}
override fun onStopTrackingTouch(seekBar: SeekBar?) {}
})
_seekbarViolence.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
updateDescriptionText(seekBar, _textViolenceDesc, _textViolenceValue, getViolenceDescriptions())
if (fromUser) {
_moderationsManager.setModerationLevel("violence", progress)
}
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {}
override fun onStopTrackingTouch(seekBar: SeekBar?) {}
})
}
private fun updateDescriptionText(seekBar: SeekBar?, textDesc: TextView, textValue: TextView, descriptions: Array<String>) {
val progress = seekBar?.progress ?: 0
textDesc.text = descriptions[progress]
textValue.text = progress.toString()
}
private fun getOffensiveDescriptions(): Array<String> {
return arrayOf(
"Neutral, general terms, no bias or hate.",
"Mildly sensitive, factual.",
"Potentially offensive content",
"Offensive content"
)
}
private fun getExplicitDescriptions(): Array<String> {
return arrayOf(
"No explicit content",
"Mildly suggestive, factual or educational",
"Moderate sexual content, non-graphic",
"Explicit sexual content"
)
}
private fun getViolenceDescriptions(): Array<String> {
return arrayOf(
"Non-violent",
"Mild violence, factual or contextual",
"Moderate violence, some graphic content.",
"Graphic violence"
)
}
}
@@ -49,7 +49,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
private lateinit var _buttonHelp: ImageButton; private lateinit var _buttonHelp: ImageButton;
private lateinit var _editName: EditText; private lateinit var _editName: EditText;
private lateinit var _buttonExport: BigButton; private lateinit var _buttonExport: BigButton;
private lateinit var _buttonModeration: BigButton; private lateinit var _buttonOpenHarborProfile: BigButton;
private lateinit var _buttonLogout: BigButton; private lateinit var _buttonLogout: BigButton;
private lateinit var _buttonDelete: BigButton; private lateinit var _buttonDelete: BigButton;
private lateinit var _username: String; private lateinit var _username: String;
@@ -71,7 +71,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
_imagePolycentric = findViewById(R.id.image_polycentric); _imagePolycentric = findViewById(R.id.image_polycentric);
_editName = findViewById(R.id.edit_profile_name); _editName = findViewById(R.id.edit_profile_name);
_buttonExport = findViewById(R.id.button_export); _buttonExport = findViewById(R.id.button_export);
_buttonModeration = findViewById(R.id.button_moderation); _buttonOpenHarborProfile = findViewById(R.id.button_open_harbor_profile);
_buttonLogout = findViewById(R.id.button_logout); _buttonLogout = findViewById(R.id.button_logout);
_buttonDelete = findViewById(R.id.button_delete); _buttonDelete = findViewById(R.id.button_delete);
_loaderOverlay = findViewById(R.id.loader_overlay); _loaderOverlay = findViewById(R.id.loader_overlay);
@@ -99,9 +99,15 @@ class PolycentricProfileActivity : AppCompatActivity() {
startActivity(Intent(this, PolycentricBackupActivity::class.java)); startActivity(Intent(this, PolycentricBackupActivity::class.java));
}; };
_buttonModeration.onClick.subscribe { _buttonOpenHarborProfile.onClick.subscribe {
startActivity(Intent(this, PolycentricModerationActivity::class.java)); val processHandle = StatePolycentric.instance.processHandle!!;
}; processHandle?.let {
val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(it.system));
val url = it.system.systemToURLInfoSystemLinkUrl(systemState.servers.asIterable());
val navUrl = "https://harbor.social/" + url.substring("polycentric://".length)
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(navUrl)))
}
}
_buttonLogout.onClick.subscribe { _buttonLogout.onClick.subscribe {
StatePolycentric.instance.setProcessHandle(null); StatePolycentric.instance.setProcessHandle(null);
@@ -1,109 +0,0 @@
package com.futo.platformplayer.activities
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Color
import android.os.Bundle
import android.util.TypedValue
import android.view.View
import android.widget.ImageButton
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity
import com.futo.platformplayer.R
import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp
import com.google.zxing.BarcodeFormat
import com.google.zxing.EncodeHintType
import com.google.zxing.MultiFormatWriter
import com.google.zxing.common.BitMatrix
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
class QRCodeFullscreenActivity : AppCompatActivity() {
companion object {
private const val EXTRA_QR_TEXT = "qr_text"
fun createIntent(context: Context, qrText: String): android.content.Intent {
return android.content.Intent(context, QRCodeFullscreenActivity::class.java).apply {
putExtra(EXTRA_QR_TEXT, qrText)
}
}
}
override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_qr_code_fullscreen)
setNavigationBarColorAndIcons()
val qrText = intent.getStringExtra(EXTRA_QR_TEXT)
val imageQR = findViewById<ImageView>(R.id.image_qr_fullscreen)
val buttonBack = findViewById<ImageButton>(R.id.button_back_fullscreen)
val buttonClose = findViewById<ImageButton>(R.id.button_close_fullscreen)
// Generate QR code bitmap from text
qrText?.let { text ->
try {
if (!isContentSuitableForQRCode(text)) {
throw Exception("Data too big for QR code generation")
}
val dimension = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 300f, resources.displayMetrics
).toInt()
val qrBitmap = generateQRCode(text, dimension, dimension)
imageQR.setImageBitmap(qrBitmap)
} catch (e: Exception) {
// If QR generation fails, show error or fallback
imageQR.setImageResource(R.drawable.ic_qr)
}
}
buttonBack.setOnClickListener {
finish()
}
buttonClose.setOnClickListener {
finish()
}
imageQR.setOnClickListener {
finish()
}
}
private fun isContentSuitableForQRCode(content: String): Boolean {
val bytes = content.toByteArray(Charsets.UTF_8)
return bytes.size <= 2300 // QR Code Version 40 with Error Correction Level M can hold ~2331 bytes
}
private fun generateQRCode(content: String, width: Int, height: Int): Bitmap {
if (!isContentSuitableForQRCode(content)) {
throw Exception("Data too big for QR code generation")
}
val hints = java.util.EnumMap<EncodeHintType, Any>(EncodeHintType::class.java)
hints[EncodeHintType.ERROR_CORRECTION] = ErrorCorrectionLevel.M
hints[EncodeHintType.MARGIN] = 1
val bitMatrix = MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height, hints)
return bitMatrixToBitmap(bitMatrix)
}
private fun bitMatrixToBitmap(matrix: BitMatrix): Bitmap {
val width = matrix.width
val height = matrix.height
val bmp = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565)
for (x in 0 until width) {
for (y in 0 until height) {
bmp.setPixel(x, y, if (matrix[x, y]) Color.BLACK else Color.WHITE)
}
}
return bmp
}
}
@@ -0,0 +1,208 @@
package com.futo.platformplayer.activities
import android.annotation.SuppressLint
import android.app.NotificationManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
import android.view.View
import android.widget.FrameLayout
import android.widget.ImageButton
import android.widget.LinearLayout
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.*
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.views.LoaderView
import com.futo.platformplayer.views.fields.FieldForm
import com.futo.platformplayer.views.fields.ReadOnlyTextField
import com.google.android.material.button.MaterialButton
class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
private lateinit var _form: FieldForm;
private lateinit var _buttonBack: ImageButton;
private lateinit var _loaderView: LoaderView;
private lateinit var _devSets: LinearLayout;
private lateinit var _buttonDev: MaterialButton;
private var _isFinished = false;
lateinit var overlay: FrameLayout;
val notifPermission = "android.permission.POST_NOTIFICATIONS";
val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
if (isGranted)
UIDialogs.toast(this, "Notification permission granted");
else
UIDialogs.toast(this, "Notification permission denied");
}
override fun attachBaseContext(newBase: Context?) {
Logger.i("SettingsActivity", "SettingsActivity.attachBaseContext")
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_settings);
setNavigationBarColorAndIcons();
_form = findViewById(R.id.settings_form);
_buttonBack = findViewById(R.id.button_back);
_buttonDev = findViewById(R.id.button_dev);
_devSets = findViewById(R.id.dev_settings);
_loaderView = findViewById(R.id.loader);
overlay = findViewById(R.id.overlay_container);
_form.onChanged.subscribe { field, _ ->
Logger.i("SettingsActivity", "Setting [${field.field?.name}] changed, saving");
_form.setObjectValues();
Settings.instance.save();
if(field.descriptor?.id == "app_language") {
Logger.i("SettingsActivity", "App language change detected, propogating to shared preferences");
StateApp.instance.setLocaleSetting(this, Settings.instance.language.getAppLanguageLocaleString());
}
if(field.descriptor?.id == "background_update") {
Logger.i("SettingsActivity", "Detected change in background work ${field.value}");
if(Settings.instance.subscriptions.subscriptionsBackgroundUpdateInterval > 0) {
val notifManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
if(!notifManager.areNotificationsEnabled()) {
UIDialogs.toast(this, "Notifications aren't enabled");
when {
ContextCompat.checkSelfPermission(this, notifPermission) == PackageManager.PERMISSION_GRANTED -> {
}
ActivityCompat.shouldShowRequestPermissionRationale(this, notifPermission) -> {
UIDialogs.showDialog(this, R.drawable.ic_notifications, "Notifications Required",
"Notifications need to be enabled for background updating to function", null, 0,
UIDialogs.Action("Cancel", {}),
UIDialogs.Action("Enable", {
requestPermissionLauncher.launch(notifPermission);
}, UIDialogs.ActionStyle.PRIMARY));
}
else -> {
requestPermissionLauncher.launch(notifPermission);
}
}
}
}
}
};
_buttonBack.setOnClickListener {
finish();
}
_buttonDev.setOnClickListener {
startActivity(Intent(this, DeveloperActivity::class.java));
}
_lastActivity = this;
reloadSettings();
}
var isFirstLoad = true;
fun reloadSettings() {
val firstLoad = isFirstLoad;
isFirstLoad = false;
_form.setSearchVisible(false);
_loaderView.start();
_form.fromObject(lifecycleScope, Settings.instance) {
_loaderView.stop();
_form.setSearchVisible(true);
var devCounter = 0;
_form.findField("code")?.assume<ReadOnlyTextField>()?.setOnClickListener {
devCounter++;
if(devCounter > 5) {
devCounter = 0;
SettingsDev.instance.developerMode = true;
SettingsDev.instance.save();
updateDevMode();
UIDialogs.toast(this, getString(R.string.you_are_now_in_developer_mode));
}
};
if(firstLoad) {
val query = intent.getStringExtra("query");
if(!query.isNullOrEmpty()) {
_form.setSearchQuery(query);
}
}
};
}
override fun onResume() {
super.onResume()
updateDevMode();
}
fun updateDevMode() {
if(SettingsDev.instance.developerMode)
_devSets.visibility = View.VISIBLE;
else
_devSets.visibility = View.GONE;
}
override fun finish() {
super.finish()
_isFinished = true;
if(_lastActivity == this)
_lastActivity = null;
overridePendingTransition(R.anim.slide_lighten, R.anim.slide_out_up)
}
private var resultLauncherMap = mutableMapOf<Int, (ActivityResult)->Unit>();
private var requestCode: Int? = -1;
private val resultLauncher: ActivityResultLauncher<Intent> = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()) {
result: ActivityResult ->
val handler = synchronized(resultLauncherMap) {
resultLauncherMap.remove(requestCode);
}
if(handler != null)
handler(result);
};
override fun launchForResult(intent: Intent, code: Int, handler: (ActivityResult)->Unit) {
synchronized(resultLauncherMap) {
resultLauncherMap[code] = handler;
}
requestCode = code;
resultLauncher.launch(intent);
}
override fun onDestroy() {
super.onDestroy()
settingsActivityClosed.emit()
}
companion object {
//TODO: Temporary for solving Settings issues
@SuppressLint("StaticFieldLeak")
private var _lastActivity: SettingsActivity? = null;
val settingsActivityClosed = Event0()
fun getActivity(): SettingsActivity? {
val act = _lastActivity;
if(act != null && !act._isFinished)
return act;
return null;
}
}
}
@@ -9,8 +9,6 @@ import android.widget.LinearLayout
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.setNavigationBarColorAndIcons import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateSync import com.futo.platformplayer.states.StateSync
@@ -31,16 +29,6 @@ class SyncHomeActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
if (StateApp.instance.contextOrNull == null) {
Logger.w(TAG, "No main activity, restarting main.")
val intent = Intent(this, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
startActivity(intent)
finish()
return
}
setContentView(R.layout.activity_sync_home) setContentView(R.layout.activity_sync_home)
setNavigationBarColorAndIcons() setNavigationBarColorAndIcons()
@@ -66,6 +54,7 @@ class SyncHomeActivity : AppCompatActivity() {
val view = _viewMap[publicKey] val view = _viewMap[publicKey]
if (!session.isAuthorized) { if (!session.isAuthorized) {
if (view != null) { if (view != null) {
_layoutDevices.removeView(view)
_viewMap.remove(publicKey) _viewMap.remove(publicKey)
} }
return@launch return@launch
@@ -100,20 +89,6 @@ class SyncHomeActivity : AppCompatActivity() {
updateEmptyVisibility() updateEmptyVisibility()
} }
} }
StateSync.instance.confirmStarted(this, onStarted = {
if (StateSync.instance.syncService?.serverSocketFailedToStart == true) {
UIDialogs.toast(this, "Server socket failed to start, is the port in use?", true)
}
if (StateSync.instance.syncService?.relayConnected == false) {
UIDialogs.toast(this, "Not connected to relay, remote connections will work.", false)
}
if (StateSync.instance.syncService?.serverSocketStarted == false) {
UIDialogs.toast(this, "Listener not started, local connections will not work.", false)
}
}, onNotStarted = {
finish()
})
} }
override fun onDestroy() { override fun onDestroy() {
@@ -125,12 +100,11 @@ class SyncHomeActivity : AppCompatActivity() {
private fun updateDeviceView(syncDeviceView: SyncDeviceView, publicKey: String, session: SyncSession?): SyncDeviceView { private fun updateDeviceView(syncDeviceView: SyncDeviceView, publicKey: String, session: SyncSession?): SyncDeviceView {
val connected = session?.connected ?: false val connected = session?.connected ?: false
val authorized = session?.isAuthorized ?: false
syncDeviceView.setLinkType(session?.linkType ?: LinkType.None) syncDeviceView.setLinkType(session?.linkType ?: LinkType.None)
.setName(session?.displayName ?: StateSync.instance.getCachedName(publicKey) ?: publicKey) .setName(session?.displayName ?: StateSync.instance.getCachedName(publicKey) ?: publicKey)
//TODO: also display public key? //TODO: also display public key?
.setStatus(if (connected && authorized) "Connected" else "Disconnected or unauthorized") .setStatus(if (connected) "Connected" else "Disconnected")
return syncDeviceView return syncDeviceView
} }
@@ -83,7 +83,6 @@ class SyncPairActivity : AppCompatActivity() {
_layoutPairingSuccess.setOnClickListener { _layoutPairingSuccess.setOnClickListener {
_layoutPairingSuccess.visibility = View.GONE _layoutPairingSuccess.visibility = View.GONE
finish()
} }
_layoutPairingError.setOnClickListener { _layoutPairingError.setOnClickListener {
_layoutPairingError.visibility = View.GONE _layoutPairingError.visibility = View.GONE
@@ -110,29 +109,11 @@ class SyncPairActivity : AppCompatActivity() {
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
try { try {
var wasCompleted = false StateSync.instance.connect(deviceInfo) { complete, message ->
StateSync.instance.syncService?.connect(deviceInfo, true) { complete, message ->
if (wasCompleted) {
Logger.i(TAG, "onStatusUpdate(complete = ${complete}, message = '${message} ignored because wasCompleted')")
return@connect
}
if (complete == true) {
wasCompleted = true
}
Logger.i(TAG, "onStatusUpdate(complete = ${complete}, message = '${message}')")
lifecycleScope.launch(Dispatchers.Main) { lifecycleScope.launch(Dispatchers.Main) {
if (complete != null) { if (complete != null && complete) {
if (complete) { _layoutPairingSuccess.visibility = View.VISIBLE
_layoutPairingSuccess.visibility = View.VISIBLE _layoutPairing.visibility = View.GONE
_layoutPairing.visibility = View.GONE
} else {
_textError.text = message
_layoutPairingError.visibility = View.VISIBLE
_layoutPairing.visibility = View.GONE
}
} else { } else {
_textPairingStatus.text = message _textPairingStatus.text = message
} }
@@ -156,6 +137,8 @@ class SyncPairActivity : AppCompatActivity() {
_textError.text = e.message _textError.text = e.message
_layoutPairing.visibility = View.GONE _layoutPairing.visibility = View.GONE
Logger.e(TAG, "Failed to pair", e) Logger.e(TAG, "Failed to pair", e)
} finally {
_layoutPairing.visibility = View.GONE
} }
} }
@@ -67,18 +67,11 @@ class SyncShowPairingCodeActivity : AppCompatActivity() {
} }
val ips = getIPs() val ips = getIPs()
val publicKey = StateSync.instance.syncService?.publicKey val selfDeviceInfo = SyncDeviceInfo(StateSync.instance.publicKey!!, ips.toTypedArray(), StateSync.PORT, StateSync.instance.pairingCode)
val pairingCode = StateSync.instance.syncService?.pairingCode val json = Json.encodeToString(selfDeviceInfo)
if (publicKey == null || pairingCode == null) { val base64 = Base64.encodeToString(json.toByteArray(), Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP)
setCode("Public key or pairing code was not known, is sync enabled?") val url = "grayjay://sync/${base64}"
} else { setCode(url)
val selfDeviceInfo = SyncDeviceInfo(publicKey, ips.toTypedArray(), StateSync.PORT, pairingCode)
val json = Json.encodeToString(selfDeviceInfo)
val base64 = Base64.encodeToString(json.toByteArray(), Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP)
val url = "grayjay://sync/${base64}"
setCode(url)
}
} }
fun setCode(code: String?) { fun setCode(code: String?) {
@@ -2,24 +2,12 @@ package com.futo.platformplayer.activities
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.views.TargetTapLoaderView
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class TestActivity : AppCompatActivity() { class TestActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test); setContentView(R.layout.activity_test);
val view = findViewById<TargetTapLoaderView>(R.id.test_view)
view.startLoader(10000)
lifecycleScope.launch {
delay(5000)
view.startLoader()
}
} }
companion object { companion object {
@@ -90,7 +90,6 @@ open class ManagedHttpClient {
} }
fun tryHead(url: String): Map<String, String>? { fun tryHead(url: String): Map<String, String>? {
ensureNotMainThread()
try { try {
val result = head(url); val result = head(url);
if(result.isOk) if(result.isOk)
@@ -105,7 +104,7 @@ open class ManagedHttpClient {
} }
fun socket(url: String, headers: MutableMap<String, String> = HashMap(), listener: SocketListener): Socket { fun socket(url: String, headers: MutableMap<String, String> = HashMap(), listener: SocketListener): Socket {
ensureNotMainThread()
val requestBuilder: okhttp3.Request.Builder = okhttp3.Request.Builder() val requestBuilder: okhttp3.Request.Builder = okhttp3.Request.Builder()
.url(url); .url(url);
if(user_agent.isNotEmpty() && !headers.any { it.key.lowercase() == "user-agent" }) if(user_agent.isNotEmpty() && !headers.any { it.key.lowercase() == "user-agent" })
@@ -301,7 +300,6 @@ open class ManagedHttpClient {
} }
fun send(msg: String) { fun send(msg: String) {
ensureNotMainThread()
socket.send(msg); socket.send(msg);
} }
@@ -1,318 +0,0 @@
package com.futo.platformplayer.api.http.server.handlers
import android.content.ContentResolver
import android.content.Context
import android.net.Uri
import android.provider.MediaStore
import android.provider.OpenableColumns
import com.futo.platformplayer.api.http.server.HttpContext
import com.futo.platformplayer.api.http.server.HttpHeaders
import com.futo.platformplayer.logging.Logger
import java.io.FileNotFoundException
import java.io.InputStream
import java.io.OutputStream
import java.text.SimpleDateFormat
import java.util.*
class HttpContentUriHandler(
method: String,
path: String,
private val contentResolver: ContentResolver,
private val uri: Uri,
private val explicitContentType: String? = null
) : HttpHandler(method, path) {
override fun handle(httpContext: HttpContext) {
val resolver = contentResolver
val requestHeaders = httpContext.headers
val responseHeaders = this.headers.clone()
val meta = try {
queryMetadata(resolver, uri)
} catch (e: Exception) {
Logger.e(TAG, "Failed to query metadata for $uri", e)
httpContext.respondCode(404, responseHeaders)
return
}
val contentType = explicitContentType
?: resolver.getType(uri)
?: "application/octet-stream"
responseHeaders["Content-Type"] = contentType
meta.lastModifiedMillis?.let { lastModified ->
responseHeaders["Last-Modified"] = httpDateFormat.format(Date(lastModified))
val ifModifiedSinceHeader = requestHeaders["If-Modified-Since"]
if (ifModifiedSinceHeader != null) {
val ifModifiedSince = try {
httpDateFormat.parse(ifModifiedSinceHeader)
} catch (_: Exception) {
null
}
if (ifModifiedSince != null && lastModified <= ifModifiedSince.time) {
httpContext.respondCode(304, responseHeaders)
return
}
}
}
val safeName = (meta.displayName ?: "content.bin").replace("\"", "\\\"")
responseHeaders["Content-Disposition"] = "attachment; filename=\"$safeName\""
val length = meta.size
if (length == null) {
Logger.i(TAG, "Streaming $uri with unknown length; Range not supported")
responseHeaders.remove("Content-Length")
responseHeaders.remove("Content-Range")
responseHeaders.remove("Accept-Ranges")
stream(
httpContext = httpContext,
resolver = resolver,
uri = uri,
statusCode = 200,
headers = responseHeaders,
start = null,
length = null
)
return
}
responseHeaders["Accept-Ranges"] = "bytes"
val rangeHeader = requestHeaders["Range"]
if (rangeHeader.isNullOrBlank()) {
responseHeaders["Content-Length"] = length.toString()
Logger.i(TAG, "Sending full content for $uri, length=$length")
stream(
httpContext = httpContext,
resolver = resolver,
uri = uri,
statusCode = 200,
headers = responseHeaders,
start = 0L,
length = length
)
return
}
val range = parseRange(rangeHeader, length)
if (range == null) {
Logger.w(TAG, "Invalid Range '$rangeHeader' for $uri (length=$length)")
responseHeaders["Content-Range"] = "bytes */$length"
httpContext.respondCode(416, responseHeaders)
return
}
val start = range.first
val endInclusive = range.last
val bytesToSend = endInclusive - start + 1
responseHeaders["Content-Range"] = "bytes $start-$endInclusive/$length"
responseHeaders["Content-Length"] = bytesToSend.toString()
Logger.i(TAG, "Sending range $start-$endInclusive (length=$bytesToSend) of $length for $uri")
stream(
httpContext = httpContext,
resolver = resolver,
uri = uri,
statusCode = 206,
headers = responseHeaders,
start = start,
length = bytesToSend
)
}
data class ContentMeta(
val displayName: String?,
val size: Long?,
val lastModifiedMillis: Long?
)
private fun queryMetadata(resolver: ContentResolver, uri: Uri): ContentMeta {
var displayName: String? = null
var size: Long? = null
var lastModifiedMillis: Long? = null
resolver.query(uri, null, null, null, null)?.use { cursor ->
if (cursor.moveToFirst()) {
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
if (nameIndex != -1 && !cursor.isNull(nameIndex)) {
displayName = cursor.getString(nameIndex)
}
val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
if (sizeIndex != -1 && !cursor.isNull(sizeIndex)) {
val s = cursor.getLong(sizeIndex)
if (s >= 0) size = s // -1 means unknown
}
val dateModifiedIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED)
if (dateModifiedIndex != -1 && !cursor.isNull(dateModifiedIndex)) {
val seconds = cursor.getLong(dateModifiedIndex)
if (seconds > 0) {
lastModifiedMillis = seconds * 1000L
}
}
if (lastModifiedMillis == null) {
val dateAddedIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DATE_ADDED)
if (dateAddedIndex != -1 && !cursor.isNull(dateAddedIndex)) {
val seconds = cursor.getLong(dateAddedIndex)
if (seconds > 0) {
lastModifiedMillis = seconds * 1000L
}
}
}
}
}
if (displayName == null) {
displayName = uri.lastPathSegment
}
if (size == null) {
try {
resolver.openAssetFileDescriptor(uri, "r")?.use { afd ->
val assetLen = afd.length
if (assetLen >= 0) {
size = assetLen
}
}
} catch (_: Exception) { }
}
return ContentMeta(
displayName = displayName,
size = size,
lastModifiedMillis = lastModifiedMillis
)
}
private fun parseRange(header: String, totalLength: Long): LongRange? {
if (totalLength <= 0L) return null
val prefix = "bytes="
if (!header.startsWith(prefix, ignoreCase = true)) return null
val spec = header.substring(prefix.length).trim()
if (spec.isEmpty()) return null
if (spec.contains(",")) return null
val dashIndex = spec.indexOf('-')
if (dashIndex < 0) return null
val startPart = spec.substring(0, dashIndex).trim()
val endPart = spec.substring(dashIndex + 1).trim()
return when {
startPart.isNotEmpty() -> {
val start = startPart.toLongOrNull() ?: return null
if (start < 0 || start >= totalLength) return null
val end = if (endPart.isNotEmpty()) {
val rawEnd = endPart.toLongOrNull() ?: return null
if (rawEnd < start) return null
rawEnd.coerceAtMost(totalLength - 1)
} else {
totalLength - 1
}
start..end
}
endPart.isNotEmpty() -> {
val suffixLen = endPart.toLongOrNull() ?: return null
if (suffixLen <= 0L) return null
if (suffixLen >= totalLength) {
0L..(totalLength - 1)
} else {
val start = totalLength - suffixLen
val end = totalLength - 1
start..end
}
}
else -> null
}
}
private fun stream(httpContext: HttpContext, resolver: ContentResolver, uri: Uri, statusCode: Int, headers: HttpHeaders, start: Long?, length: Long?) {
try {
val input = resolver.openInputStream(uri)
if (input == null) {
Logger.w(TAG, "Content not found: $uri")
httpContext.respondCode(404, headers)
return
}
input.use { inputStream ->
httpContext.respond(statusCode, headers) { outputStream ->
try {
val offset = start ?: 0L
if (offset > 0L) {
skipFully(inputStream, offset)
}
copyStream(inputStream, outputStream, length)
outputStream.flush()
} catch (e: Exception) {
Logger.e(TAG, "Error while streaming $uri (start=$start, length=$length)", e)
}
}
}
} catch (e: FileNotFoundException) {
Logger.w(TAG, "Content not found: $uri", e)
httpContext.respondCode(404, headers)
} catch (e: Exception) {
Logger.e(TAG, "Failed to open stream for $uri", e)
httpContext.respondCode(500, headers)
}
}
private fun copyStream(input: InputStream, output: OutputStream, limit: Long?) {
val buffer = ByteArray(8192)
if (limit == null) {
while (true) {
val read = input.read(buffer)
if (read < 0) break
output.write(buffer, 0, read)
}
} else {
var remaining = limit
while (remaining > 0L) {
val toRead = remaining.coerceAtMost(buffer.size.toLong()).toInt()
val read = input.read(buffer, 0, toRead)
if (read < 0) break
output.write(buffer, 0, read)
remaining -= read.toLong()
}
}
}
private fun skipFully(input: InputStream, bytesToSkip: Long) {
var remaining = bytesToSkip
while (remaining > 0L) {
val skipped = input.skip(remaining)
if (skipped <= 0L) {
val b = input.read()
if (b == -1) break
remaining -= 1L
} else {
remaining -= skipped
}
}
}
companion object {
private const val TAG = "HttpContentUriHandler"
private val httpDateFormat = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.US).apply {
timeZone = TimeZone.getTimeZone("GMT")
}
}
}
@@ -5,7 +5,6 @@ import android.util.Log
import com.futo.platformplayer.api.http.server.HttpContext import com.futo.platformplayer.api.http.server.HttpContext
import com.futo.platformplayer.api.http.server.HttpHeaders import com.futo.platformplayer.api.http.server.HttpHeaders
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.models.modifier.IRequest
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.parsers.HttpResponseParser import com.futo.platformplayer.parsers.HttpResponseParser
import com.futo.platformplayer.readLine import com.futo.platformplayer.readLine
@@ -28,7 +27,6 @@ class HttpProxyHandler(method: String, path: String, val targetUrl: String, priv
private var _injectReferer = false; private var _injectReferer = false;
private val _client = ManagedHttpClient(); private val _client = ManagedHttpClient();
private var _requestModifier: ((String, Map<String, String>) -> IRequest)? = null;
override fun handle(context: HttpContext) { override fun handle(context: HttpContext) {
if (useTcp) { if (useTcp) {
@@ -45,33 +43,21 @@ class HttpProxyHandler(method: String, path: String, val targetUrl: String, priv
for (injectHeader in _injectRequestHeader) for (injectHeader in _injectRequestHeader)
proxyHeaders[injectHeader.first] = injectHeader.second; proxyHeaders[injectHeader.first] = injectHeader.second;
val req = _requestModifier?.invoke(targetUrl, proxyHeaders) val parsed = Uri.parse(targetUrl);
var url = targetUrl
if (req != null) {
req.url?.let {
url = it
}
req.headers.let {
proxyHeaders.clear()
proxyHeaders.putAll(it)
}
}
val parsed = Uri.parse(url);
if(_injectHost) if(_injectHost)
proxyHeaders.put("Host", parsed.host!!); proxyHeaders.put("Host", parsed.host!!);
if(_injectReferer) if(_injectReferer)
proxyHeaders.put("Referer", url); proxyHeaders.put("Referer", targetUrl);
val useMethod = if (method == "inherit") context.method else method; val useMethod = if (method == "inherit") context.method else method;
Logger.i(TAG, "handleWithOkHttp Proxied Request ${useMethod}: ${url}"); Logger.i(TAG, "handleWithOkHttp Proxied Request ${useMethod}: ${targetUrl}");
Logger.i(TAG, "handleWithOkHttp Headers:" + proxyHeaders.map { "${it.key}: ${it.value}" }.joinToString("\n")); Logger.i(TAG, "handleWithOkHttp Headers:" + proxyHeaders.map { "${it.key}: ${it.value}" }.joinToString("\n"));
val resp = when (useMethod) { val resp = when (useMethod) {
"GET" -> _client.get(url, proxyHeaders); "GET" -> _client.get(targetUrl, proxyHeaders);
"POST" -> _client.post(url, content ?: "", proxyHeaders); "POST" -> _client.post(targetUrl, content ?: "", proxyHeaders);
"HEAD" -> _client.head(url, proxyHeaders) "HEAD" -> _client.head(targetUrl, proxyHeaders)
else -> _client.requestMethod(useMethod, url, proxyHeaders); else -> _client.requestMethod(useMethod, targetUrl, proxyHeaders);
}; };
Logger.i(TAG, "Proxied Response [${resp.code}]"); Logger.i(TAG, "Proxied Response [${resp.code}]");
@@ -105,23 +91,11 @@ class HttpProxyHandler(method: String, path: String, val targetUrl: String, priv
for (injectHeader in _injectRequestHeader) for (injectHeader in _injectRequestHeader)
proxyHeaders[injectHeader.first] = injectHeader.second; proxyHeaders[injectHeader.first] = injectHeader.second;
val req = _requestModifier?.invoke(targetUrl, proxyHeaders) val parsed = Uri.parse(targetUrl);
var url = targetUrl
if (req != null) {
req.url?.let {
url = it
}
req.headers.let {
proxyHeaders.clear()
proxyHeaders.putAll(it)
}
}
val parsed = Uri.parse(url);
if(_injectHost) if(_injectHost)
proxyHeaders.put("Host", parsed.host!!); proxyHeaders.put("Host", parsed.host!!);
if(_injectReferer) if(_injectReferer)
proxyHeaders.put("Referer", url); proxyHeaders.put("Referer", targetUrl);
val useMethod = if (method == "inherit") context.method else method; val useMethod = if (method == "inherit") context.method else method;
Logger.i(TAG, "handleWithTcp Proxied Request ${useMethod}: ${parsed}"); Logger.i(TAG, "handleWithTcp Proxied Request ${useMethod}: ${parsed}");
@@ -268,10 +242,6 @@ class HttpProxyHandler(method: String, path: String, val targetUrl: String, priv
_ignoreRequestHeaders.add("referer"); _ignoreRequestHeaders.add("referer");
return this; return this;
} }
fun withRequestModifier(modifier: (String, Map<String, String>) -> IRequest) : HttpProxyHandler {
_requestModifier = modifier;
return this;
}
companion object { companion object {
private const val TAG = "HttpProxyHandler" private const val TAG = "HttpProxyHandler"
@@ -1,6 +1,5 @@
package com.futo.platformplayer.api.media package com.futo.platformplayer.api.media
import com.futo.platformplayer.api.media.models.IPlatformChannelContent
import com.futo.platformplayer.api.media.models.PlatformAuthorLink import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import com.futo.platformplayer.api.media.models.ResultCapabilities import com.futo.platformplayer.api.media.models.ResultCapabilities
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
@@ -13,7 +12,6 @@ import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.models.ImageVariable import com.futo.platformplayer.models.ImageVariable
@@ -37,11 +35,6 @@ interface IPlatformClient {
*/ */
fun getHome(): IPager<IPlatformContent> fun getHome(): IPager<IPlatformContent>
/**
* Gets the shorts feed
*/
fun getShorts(): IPager<IPlatformVideo>
//Search //Search
/** /**
* Gets search suggestion for the provided query string * Gets search suggestion for the provided query string
@@ -73,11 +66,6 @@ interface IPlatformClient {
*/ */
fun searchChannels(query: String): IPager<PlatformAuthorLink>; fun searchChannels(query: String): IPager<PlatformAuthorLink>;
/**
* Searches for channels and returns a content pager
*/
fun searchChannelsAsContent(query: String): IPager<IPlatformContent>;
//Video Pages //Video Pages
/** /**
@@ -182,10 +170,6 @@ interface IPlatformClient {
* Retrieves the subscriptions of the currently logged in user * Retrieves the subscriptions of the currently logged in user
*/ */
fun getUserSubscriptions(): Array<String>; fun getUserSubscriptions(): Array<String>;
/**
* Retrieves the history of the currently logged in user
*/
fun getUserHistory(): IPager<IPlatformContent>;
fun isClaimTypeSupported(claimType: Int): Boolean; fun isClaimTypeSupported(claimType: Int): Boolean;
@@ -11,7 +11,6 @@ import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
import com.futo.platformplayer.api.media.models.live.LiveEventComment import com.futo.platformplayer.api.media.models.live.LiveEventComment
import com.futo.platformplayer.api.media.models.live.LiveEventEmojis import com.futo.platformplayer.api.media.models.live.LiveEventEmojis
import com.futo.platformplayer.api.media.platforms.js.models.JSLiveEventPager import com.futo.platformplayer.api.media.platforms.js.models.JSLiveEventPager
import com.futo.platformplayer.api.media.platforms.js.models.JSVODEventPager
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.constructs.BatchedTaskHandler import com.futo.platformplayer.constructs.BatchedTaskHandler
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
@@ -27,17 +26,12 @@ class LiveChatManager {
private val _emojiCache: EmojiCache = EmojiCache(); private val _emojiCache: EmojiCache = EmojiCache();
private val _pager: IPager<IPlatformLiveEvent>?; private val _pager: IPager<IPlatformLiveEvent>?;
private var _position: Long = 0;
private var _eventsPosition: Long = 0;
private val _history: ArrayList<IPlatformLiveEvent> = arrayListOf(); private val _history: ArrayList<IPlatformLiveEvent> = arrayListOf();
private var _startCounter = 0; private var _startCounter = 0;
private val _followers: HashMap<Any, (List<IPlatformLiveEvent>) -> Unit> = hashMapOf(); private val _followers: HashMap<Any, (List<IPlatformLiveEvent>) -> Unit> = hashMapOf();
val isVOD get() = _pager is JSVODEventPager;
var viewCount: Long = 0 var viewCount: Long = 0
private set; private set;
@@ -45,24 +39,8 @@ class LiveChatManager {
_scope = scope; _scope = scope;
_pager = pager; _pager = pager;
viewCount = initialViewCount; viewCount = initialViewCount;
if(pager is JSVODEventPager) handleEvents(listOf(LiveEventComment("SYSTEM", null, "Live chat is still under construction. While it is mostly functional, the experience still needs to be improved.\n")));
handleEvents(listOf(LiveEventComment("SYSTEM", null, "VOD chat is still under construction. While it is mostly functional, the experience still needs to be improved.\n"))); handleEvents(pager.getResults());
else
handleEvents(listOf(LiveEventComment("SYSTEM", null, "Live chat is still under construction. While it is mostly functional, the experience still needs to be improved.\n")));
if(pager is JSVODEventPager) {
var replayResults = pager.getResults().filter { it.time > _eventsPosition || it is LiveEventEmojis };
//TODO: Remove this once dripfeed is done properly
replayResults = replayResults.filter{ it.time < _eventsPosition + 1500 || it is LiveEventEmojis };
if(replayResults.size > 0) {
_eventsPosition = replayResults.maxOf { it.time };
Logger.i(TAG, "VOD Events last event: " + _eventsPosition);
}
else
_eventsPosition = _eventsPosition + 1500;
}
else
handleEvents(pager.getResults());
} }
fun start() { fun start() {
@@ -74,10 +52,6 @@ class LiveChatManager {
_startCounter++; _startCounter++;
} }
fun setVideoPosition(ms: Long) {
_position = ms;
}
fun getHistory(): List<IPlatformLiveEvent> { fun getHistory(): List<IPlatformLiveEvent> {
synchronized(_history) { synchronized(_history) {
return _history.toList(); return _history.toList();
@@ -111,34 +85,13 @@ class LiveChatManager {
try { try {
while(_startCounter == counter) { while(_startCounter == counter) {
var nextInterval = 1000L; var nextInterval = 1000L;
if(_pager is JSVODEventPager && _eventsPosition > _position) {
delay(500);
continue;
}
try { try {
if(_pager == null || !_pager.hasMorePages()) if(_pager == null || !_pager.hasMorePages())
return@launch; return@launch;
val newEvents = if(_pager is JSVODEventPager) { _pager.nextPage();
val requestPosition = _position; val newEvents = _pager.getResults();
_pager.nextPage(requestPosition.toInt());
var replayResults = _pager.getResults().filter { it.time > requestPosition || it is LiveEventEmojis };
if(replayResults.size > 0) {
_eventsPosition = replayResults.maxOf { it.time };
Logger.i(TAG, "VOD Events last event: " + _eventsPosition);
}
else
_eventsPosition = requestPosition + _pager.nextRequest.coerceAtLeast(800).toLong();
replayResults;
}
else {
_pager.nextPage();
_pager.getResults();
}
if(_pager is JSLiveEventPager) if(_pager is JSLiveEventPager)
nextInterval = _pager.nextRequest.coerceAtLeast(800).toLong(); nextInterval = _pager.nextRequest.coerceAtLeast(800).toLong();
else if(_pager is JSVODEventPager)
nextInterval = _pager.nextRequest.coerceAtLeast(800).toLong();
if(newEvents.size > 0) if(newEvents.size > 0)
Logger.i(TAG, "New Live Events (${newEvents.size}) [${newEvents.map { it.type.name }.joinToString(", ")}]"); Logger.i(TAG, "New Live Events (${newEvents.size}) [${newEvents.map { it.type.name }.joinToString(", ")}]");
@@ -20,8 +20,7 @@ data class PlatformClientCapabilities(
val hasGetContentChapters: Boolean = false, val hasGetContentChapters: Boolean = false,
val hasPeekChannelContents: Boolean = false, val hasPeekChannelContents: Boolean = false,
val hasGetChannelPlaylists: Boolean = false, val hasGetChannelPlaylists: Boolean = false,
val hasGetContentRecommendations: Boolean = false, val hasGetContentRecommendations: Boolean = false
val hasGetUserHistory: Boolean = false
) { ) {
} }
@@ -14,16 +14,14 @@ class PlatformClientPool {
private var _poolCounter = 0; private var _poolCounter = 0;
private val _poolName: String?; private val _poolName: String?;
private val _privatePool: Boolean; private val _privatePool: Boolean;
private val _isolatedInitialization: Boolean
var isDead: Boolean = false var isDead: Boolean = false
private set; private set;
val onDead = Event2<JSClient, PlatformClientPool>(); val onDead = Event2<JSClient, PlatformClientPool>();
constructor(parentClient: IPlatformClient, name: String? = null, privatePool: Boolean = false, isolatedInitialization: Boolean = false) { constructor(parentClient: IPlatformClient, name: String? = null, privatePool: Boolean = false) {
_poolName = name; _poolName = name;
_privatePool = privatePool; _privatePool = privatePool;
_isolatedInitialization = isolatedInitialization
if(parentClient !is JSClient) if(parentClient !is JSClient)
throw IllegalArgumentException("Pooling only supported for JSClients right now"); throw IllegalArgumentException("Pooling only supported for JSClients right now");
Logger.i(TAG, "Pool for ${parentClient.name} was started"); Logger.i(TAG, "Pool for ${parentClient.name} was started");
@@ -34,10 +32,8 @@ class PlatformClientPool {
isDead = true; isDead = true;
onDead.emit(parentClient, this); onDead.emit(parentClient, this);
synchronized(_pool) { for(clientPair in _pool) {
for (clientPair in _pool) { clientPair.key.disable();
clientPair.key.disable();
}
} }
}; };
} }
@@ -57,7 +53,7 @@ class PlatformClientPool {
reserved = _pool.keys.find { !it.isBusy }; reserved = _pool.keys.find { !it.isBusy };
if(reserved == null && _pool.size < capacity) { if(reserved == null && _pool.size < capacity) {
Logger.i(TAG, "Started additional [${_parent.name}] client in pool [${_poolName}] (${_pool.size + 1}/${capacity})"); Logger.i(TAG, "Started additional [${_parent.name}] client in pool [${_poolName}] (${_pool.size + 1}/${capacity})");
reserved = _parent.getCopy(_privatePool, _isolatedInitialization); reserved = _parent.getCopy(_privatePool);
reserved?.onCaptchaException?.subscribe { client, ex -> reserved?.onCaptchaException?.subscribe { client, ex ->
StateApp.instance.handleCaptchaException(client, ex); StateApp.instance.handleCaptchaException(client, ex);
@@ -2,7 +2,6 @@ package com.futo.platformplayer.api.media
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.getOrThrowNullable import com.futo.platformplayer.getOrThrowNullable
@@ -45,7 +44,6 @@ class PlatformID {
val NONE = PlatformID("Unknown", null); val NONE = PlatformID("Unknown", null);
fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformID { fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformID {
value.ensureIsBusy();
val contextName = "PlatformID"; val contextName = "PlatformID";
return PlatformID( return PlatformID(
value.getOrThrow(config, "platform", contextName), value.getOrThrow(config, "platform", contextName),
@@ -7,15 +7,13 @@ class PlatformMultiClientPool {
private var _isFake = false; private var _isFake = false;
private var _privatePool = false; private var _privatePool = false;
private val _isolatedInitialization: Boolean
constructor(name: String, maxCap: Int = -1, isPrivatePool: Boolean = false, isolatedInitialization: Boolean = false) { constructor(name: String, maxCap: Int = -1, isPrivatePool: Boolean = false) {
_name = name; _name = name;
_maxCap = if(maxCap > 0) _maxCap = if(maxCap > 0)
maxCap maxCap
else 99; else 99;
_privatePool = isPrivatePool; _privatePool = isPrivatePool;
_isolatedInitialization = isolatedInitialization
} }
fun getClientPooled(parentClient: IPlatformClient, capacity: Int = _maxCap): IPlatformClient { fun getClientPooled(parentClient: IPlatformClient, capacity: Int = _maxCap): IPlatformClient {
@@ -23,7 +21,7 @@ class PlatformMultiClientPool {
return parentClient; return parentClient;
val pool = synchronized(_clientPools) { val pool = synchronized(_clientPools) {
if(!_clientPools.containsKey(parentClient)) if(!_clientPools.containsKey(parentClient))
_clientPools[parentClient] = PlatformClientPool(parentClient, _name, _privatePool, _isolatedInitialization).apply { _clientPools[parentClient] = PlatformClientPool(parentClient, _name, _privatePool).apply {
this.onDead.subscribe { _, pool -> this.onDead.subscribe { _, pool ->
synchronized(_clientPools) { synchronized(_clientPools) {
if(_clientPools[parentClient] == pool) if(_clientPools[parentClient] == pool)
@@ -2,11 +2,7 @@ package com.futo.platformplayer.api.media.models
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.PlatformID import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.platforms.js.models.JSContent
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
@@ -34,7 +30,6 @@ open class PlatformAuthorLink {
val UNKNOWN = PlatformAuthorLink(PlatformID.NONE, "Unknown", "", null, null); val UNKNOWN = PlatformAuthorLink(PlatformID.NONE, "Unknown", "", null, null);
fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformAuthorLink { fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformAuthorLink {
value.ensureIsBusy();
if(value.has("membershipUrl")) if(value.has("membershipUrl"))
return PlatformAuthorMembershipLink.fromV8(config, value); return PlatformAuthorMembershipLink.fromV8(config, value);
@@ -47,23 +42,4 @@ open class PlatformAuthorLink {
); );
} }
} }
} }
interface IPlatformChannelContent : IPlatformContent {
val thumbnail: String?
val subscribers: Long?
}
open class JSChannelContent(
config: SourcePluginConfig,
obj: V8ValueObject
) : JSContent(config, obj), IPlatformChannelContent {
final override val contentType: ContentType = ContentType.CHANNEL
override val thumbnail: String? =
_content.getOrDefault<String>(_pluginConfig, "thumbnail", "Channel", null)
override val subscribers: Long? =
_content.getOrDefault<Long>(_pluginConfig, "subscribers", "Channel", null)?.toLong()
}
@@ -3,7 +3,6 @@ package com.futo.platformplayer.api.media.models
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.PlatformID import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
@@ -21,7 +20,6 @@ class PlatformAuthorMembershipLink: PlatformAuthorLink {
companion object { companion object {
fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformAuthorMembershipLink { fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformAuthorMembershipLink {
value.ensureIsBusy();
val context = "AuthorMembershipLink" val context = "AuthorMembershipLink"
return PlatformAuthorMembershipLink(PlatformID.fromV8(config, value.getOrThrow(config, "id", context, false)), return PlatformAuthorMembershipLink(PlatformID.fromV8(config, value.getOrThrow(config, "id", context, false)),
value.getOrThrow(config ,"name", context), value.getOrThrow(config ,"name", context),
@@ -5,7 +5,6 @@ import com.caoccao.javet.values.primitive.V8ValueInteger
import com.caoccao.javet.values.reference.V8ValueArray import com.caoccao.javet.values.reference.V8ValueArray
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.expectV8Variant import com.futo.platformplayer.expectV8Variant
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
@@ -47,7 +46,6 @@ class ResultCapabilities(
fun fromV8(config: IV8PluginConfig, value: V8ValueObject): ResultCapabilities { fun fromV8(config: IV8PluginConfig, value: V8ValueObject): ResultCapabilities {
val contextName = "ResultCapabilities"; val contextName = "ResultCapabilities";
value.ensureIsBusy();
return ResultCapabilities( return ResultCapabilities(
value.getOrThrow<V8ValueArray>(config, "types", contextName).toArray().map { it.expectV8Variant(config, "Capabilities.types") }, value.getOrThrow<V8ValueArray>(config, "types", contextName).toArray().map { it.expectV8Variant(config, "Capabilities.types") },
value.getOrThrow<V8ValueArray>(config, "sorts", contextName).toArray().map { it.expectV8Variant(config, "Capabilities.sorts"); }, value.getOrThrow<V8ValueArray>(config, "sorts", contextName).toArray().map { it.expectV8Variant(config, "Capabilities.sorts"); },
@@ -71,7 +69,6 @@ class FilterGroup(
companion object { companion object {
fun fromV8(config: IV8PluginConfig, value: V8ValueObject): FilterGroup { fun fromV8(config: IV8PluginConfig, value: V8ValueObject): FilterGroup {
value.ensureIsBusy();
return FilterGroup( return FilterGroup(
value.getString("name"), value.getString("name"),
value.getOrDefault<V8ValueArray>(config, "filters", "FilterGroup", null) value.getOrDefault<V8ValueArray>(config, "filters", "FilterGroup", null)
@@ -93,7 +90,6 @@ class FilterCapability(
companion object { companion object {
fun fromV8(obj: V8ValueObject): FilterCapability { fun fromV8(obj: V8ValueObject): FilterCapability {
obj.ensureIsBusy();
val value = obj.get("value") as V8Value; val value = obj.get("value") as V8Value;
return FilterCapability( return FilterCapability(
obj.getString("name"), obj.getString("name"),
@@ -4,7 +4,6 @@ import com.caoccao.javet.values.reference.V8ValueArray
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8PluginConfig import com.futo.platformplayer.engine.V8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
@@ -32,7 +31,6 @@ class Thumbnails {
companion object { companion object {
fun fromV8(config: IV8PluginConfig, value: V8ValueObject): Thumbnails { fun fromV8(config: IV8PluginConfig, value: V8ValueObject): Thumbnails {
value.ensureIsBusy();
return Thumbnails((value.getOrThrow<V8ValueArray>(config, "sources", "Thumbnails")) return Thumbnails((value.getOrThrow<V8ValueArray>(config, "sources", "Thumbnails"))
.toArray() .toArray()
.map { Thumbnail.fromV8(config, it as V8ValueObject) } .map { Thumbnail.fromV8(config, it as V8ValueObject) }
@@ -1,9 +0,0 @@
package com.futo.platformplayer.api.media.models.article
import com.futo.platformplayer.api.media.models.Thumbnails
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
interface IPlatformArticle: IPlatformContent {
val summary: String?;
val thumbnails: Thumbnails?;
}
@@ -1,12 +0,0 @@
package com.futo.platformplayer.api.media.models.article
import com.futo.platformplayer.api.media.models.Thumbnails
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.platforms.js.models.IJSArticleSegment
interface IPlatformArticleDetails: IPlatformContent, IPlatformArticle, IPlatformContentDetails {
val segments: List<IJSArticleSegment>;
val rating : IRating;
}
@@ -6,15 +6,25 @@ import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
import java.time.OffsetDateTime import java.time.OffsetDateTime
open class PlatformComment( open class PlatformComment : IPlatformComment {
override val contextUrl: String, override val contextUrl: String;
override val author: PlatformAuthorLink, override val author: PlatformAuthorLink;
override val message: String, override val message: String;
override val rating: IRating, override val rating: IRating;
override val date: OffsetDateTime, override val date: OffsetDateTime;
override val replyCount: Int? = null
) : IPlatformComment {
override fun getReplies(client: IPlatformClient): IPager<IPlatformComment> = override val replyCount: Int?;
NoCommentsPager()
} constructor(contextUrl: String, author: PlatformAuthorLink, msg: String, rating: IRating, date: OffsetDateTime, replyCount: Int? = null) {
this.contextUrl = contextUrl;
this.author = author;
this.message = msg;
this.rating = rating;
this.date = date;
this.replyCount = replyCount;
}
override fun getReplies(client: IPlatformClient): IPager<IPlatformComment> {
return NoCommentsPager();
}
}
@@ -8,12 +8,10 @@ enum class ContentType(val value: Int) {
POST(2), POST(2),
ARTICLE(3), ARTICLE(3),
PLAYLIST(4), PLAYLIST(4),
WEB(7),
URL(9), URL(9),
NESTED_VIDEO(11), NESTED_VIDEO(11),
CHANNEL(60),
LOCKED(70), LOCKED(70),
@@ -2,17 +2,14 @@ package com.futo.platformplayer.api.media.models.live
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
interface IPlatformLiveEvent { interface IPlatformLiveEvent {
val type : LiveEventType; val type : LiveEventType;
var time: Long;
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject, contextName: String = "LiveEvent") : IPlatformLiveEvent { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject, contextName: String = "LiveEvent") : IPlatformLiveEvent {
obj.ensureIsBusy();
val t = LiveEventType.fromInt(obj.getOrThrow<Int>(config, "type", contextName)); val t = LiveEventType.fromInt(obj.getOrThrow<Int>(config, "type", contextName));
return when(t) { return when(t) {
LiveEventType.COMMENT -> LiveEventComment.fromV8(config, obj); LiveEventType.COMMENT -> LiveEventComment.fromV8(config, obj);
@@ -4,7 +4,6 @@ import com.caoccao.javet.values.reference.V8ValueArray
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.ratings.RatingLikes import com.futo.platformplayer.api.media.models.ratings.RatingLikes
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
@@ -18,21 +17,16 @@ class LiveEventComment: IPlatformLiveEvent, ILiveEventChatMessage {
val colorName: String?; val colorName: String?;
val badges: List<String>; val badges: List<String>;
override var time: Long = -1; constructor(name: String, thumbnail: String?, message: String, colorName: String? = null, badges: List<String>? = null) {
constructor(name: String, thumbnail: String?, message: String, colorName: String? = null, badges: List<String>? = null, time: Long = -1) {
this.name = name; this.name = name;
this.message = message; this.message = message;
this.thumbnail = thumbnail; this.thumbnail = thumbnail;
this.colorName = colorName; this.colorName = colorName;
this.badges = badges ?: listOf(); this.badges = badges ?: listOf();
this.time = time;
} }
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventComment { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventComment {
obj.ensureIsBusy();
val contextName = "LiveEventComment" val contextName = "LiveEventComment"
val colorName = obj.getOrDefault<String>(config, "colorName", contextName, null); val colorName = obj.getOrDefault<String>(config, "colorName", contextName, null);
@@ -42,8 +36,7 @@ class LiveEventComment: IPlatformLiveEvent, ILiveEventChatMessage {
obj.getOrThrow(config, "name", contextName), obj.getOrThrow(config, "name", contextName),
obj.getOrThrow(config, "thumbnail", contextName, true), obj.getOrThrow(config, "thumbnail", contextName, true),
obj.getOrThrow(config, "message", contextName), obj.getOrThrow(config, "message", contextName),
colorName, badges, colorName, badges);
obj.getOrDefault(config, "time", contextName, -1) ?: -1);
} }
} }
} }
@@ -3,7 +3,6 @@ package com.futo.platformplayer.api.media.models.live
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.ratings.RatingLikes import com.futo.platformplayer.api.media.models.ratings.RatingLikes
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
@@ -21,8 +20,6 @@ class LiveEventDonation: IPlatformLiveEvent, ILiveEventChatMessage {
var expire: Int = 6000; var expire: Int = 6000;
override var time: Long = -1;
constructor(name: String, thumbnail: String?, message: String, amount: String, expire: Int = 6000, colorDonation: String? = null) { constructor(name: String, thumbnail: String?, message: String, amount: String, expire: Int = 6000, colorDonation: String? = null) {
this.name = name; this.name = name;
@@ -40,7 +37,6 @@ class LiveEventDonation: IPlatformLiveEvent, ILiveEventChatMessage {
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventDonation { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventDonation {
obj.ensureIsBusy();
val contextName = "LiveEventDonation" val contextName = "LiveEventDonation"
return LiveEventDonation( return LiveEventDonation(
obj.getOrThrow(config, "name", contextName), obj.getOrThrow(config, "name", contextName),
@@ -2,7 +2,6 @@ package com.futo.platformplayer.api.media.models.live
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
class LiveEventEmojis: IPlatformLiveEvent { class LiveEventEmojis: IPlatformLiveEvent {
@@ -10,17 +9,15 @@ class LiveEventEmojis: IPlatformLiveEvent {
val emojis: HashMap<String, String>; val emojis: HashMap<String, String>;
override var time: Long = -1;
constructor(emojis: HashMap<String, String>) { constructor(emojis: HashMap<String, String>) {
this.emojis = emojis; this.emojis = emojis;
} }
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventEmojis { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventEmojis {
obj.ensureIsBusy();
val contextName = "LiveEventEmojis" val contextName = "LiveEventEmojis"
return LiveEventEmojis(obj.getOrThrow(config, "emojis", contextName)); return LiveEventEmojis(
obj.getOrThrow(config, "emojis", contextName));
} }
} }
} }
@@ -2,8 +2,6 @@ package com.futo.platformplayer.api.media.models.live
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
class LiveEventRaid: IPlatformLiveEvent { class LiveEventRaid: IPlatformLiveEvent {
@@ -12,26 +10,20 @@ class LiveEventRaid: IPlatformLiveEvent {
val targetName: String; val targetName: String;
val targetThumbnail: String; val targetThumbnail: String;
val targetUrl: String; val targetUrl: String;
val isOutgoing: Boolean;
override var time: Long = -1; constructor(name: String, url: String, thumbnail: String) {
constructor(name: String, url: String, thumbnail: String, isOutgoing: Boolean) {
this.targetName = name; this.targetName = name;
this.targetUrl = url; this.targetUrl = url;
this.targetThumbnail = thumbnail; this.targetThumbnail = thumbnail;
this.isOutgoing = isOutgoing;
} }
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventRaid { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventRaid {
obj.ensureIsBusy();
val contextName = "LiveEventRaid" val contextName = "LiveEventRaid"
return LiveEventRaid( return LiveEventRaid(
obj.getOrThrow(config, "targetName", contextName), obj.getOrThrow(config, "targetName", contextName),
obj.getOrThrow(config, "targetUrl", contextName), obj.getOrThrow(config, "targetUrl", contextName),
obj.getOrThrow(config, "targetThumbnail", contextName), obj.getOrThrow(config, "targetThumbnail", contextName));
obj.getOrDefault<Boolean>(config, "isOutgoing", contextName, true) ?: true);
} }
} }
} }
@@ -2,7 +2,6 @@ package com.futo.platformplayer.api.media.models.live
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
class LiveEventViewCount: IPlatformLiveEvent { class LiveEventViewCount: IPlatformLiveEvent {
@@ -10,15 +9,12 @@ class LiveEventViewCount: IPlatformLiveEvent {
val viewCount: Int; val viewCount: Int;
override var time: Long = -1;
constructor(viewCount: Int) { constructor(viewCount: Int) {
this.viewCount = viewCount; this.viewCount = viewCount;
} }
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventViewCount { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : LiveEventViewCount {
obj.ensureIsBusy();
val contextName = "LiveEventViewCount" val contextName = "LiveEventViewCount"
return LiveEventViewCount( return LiveEventViewCount(
obj.getOrThrow(config, "viewCount", contextName)); obj.getOrThrow(config, "viewCount", contextName));
@@ -5,8 +5,7 @@ import com.futo.platformplayer.api.media.exceptions.UnknownPlatformException
enum class TextType(val value: Int) { enum class TextType(val value: Int) {
RAW(0), RAW(0),
HTML(1), HTML(1),
MARKUP(2), MARKUP(2);
CODE(3);
companion object { companion object {
fun fromInt(value: Int): TextType fun fromInt(value: Int): TextType
@@ -3,7 +3,6 @@ package com.futo.platformplayer.api.media.models.ratings
import com.caoccao.javet.values.V8Value import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.orDefault import com.futo.platformplayer.orDefault
import com.futo.platformplayer.serializers.IRatingSerializer import com.futo.platformplayer.serializers.IRatingSerializer
@@ -14,12 +13,8 @@ interface IRating {
companion object { companion object {
fun fromV8OrDefault(config: IV8PluginConfig, obj: V8Value?, default: IRating): IRating { fun fromV8OrDefault(config: IV8PluginConfig, obj: V8Value?, default: IRating) = obj.orDefault(default) { fromV8(config, it as V8ValueObject) };
obj?.ensureIsBusy();
return obj.orDefault(default) { fromV8(config, it as V8ValueObject) }
};
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject, contextName: String = "Rating") : IRating { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject, contextName: String = "Rating") : IRating {
obj.ensureIsBusy();
val t = RatingType.fromInt(obj.getOrThrow<Int>(config, "type", contextName)); val t = RatingType.fromInt(obj.getOrThrow<Int>(config, "type", contextName));
return when(t) { return when(t) {
RatingType.LIKES -> RatingLikes.fromV8(config, obj); RatingType.LIKES -> RatingLikes.fromV8(config, obj);
@@ -2,7 +2,6 @@ package com.futo.platformplayer.api.media.models.ratings
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
/** /**
@@ -15,7 +14,6 @@ class RatingLikeDislikes(val likes: Long, val dislikes: Long) : IRating {
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : RatingLikeDislikes { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : RatingLikeDislikes {
obj.ensureIsBusy();
return RatingLikeDislikes(obj.getOrThrow(config, "likes", "RatingLikeDislikes"), obj.getOrThrow(config, "dislikes", "RatingLikeDislikes")); return RatingLikeDislikes(obj.getOrThrow(config, "likes", "RatingLikeDislikes"), obj.getOrThrow(config, "dislikes", "RatingLikeDislikes"));
} }
} }
@@ -2,7 +2,6 @@ package com.futo.platformplayer.api.media.models.ratings
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
/** /**
@@ -14,7 +13,6 @@ class RatingLikes(val likes: Long) : IRating {
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : RatingLikes { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : RatingLikes {
obj.ensureIsBusy();
return RatingLikes(obj.getOrThrow(config, "likes", "RatingLikes")); return RatingLikes(obj.getOrThrow(config, "likes", "RatingLikes"));
} }
} }
@@ -2,7 +2,6 @@ package com.futo.platformplayer.api.media.models.ratings
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
/** /**
@@ -14,7 +13,6 @@ class RatingScaler(val value: Float) : IRating {
companion object { companion object {
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : RatingScaler { fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : RatingScaler {
obj.ensureIsBusy()
return RatingScaler(obj.getOrThrow(config, "value", "RatingScaler")); return RatingScaler(obj.getOrThrow(config, "value", "RatingScaler"));
} }
} }
@@ -2,24 +2,10 @@ package com.futo.platformplayer.api.media.models.streams
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalAudioContentSource
import com.futo.platformplayer.downloads.VideoLocal import com.futo.platformplayer.downloads.VideoLocal
class LocalVideoUnMuxedSourceDescriptor : VideoUnMuxedSourceDescriptor { class LocalVideoUnMuxedSourceDescriptor(private val video: VideoLocal) : VideoUnMuxedSourceDescriptor() {
override val videoSources: Array<IVideoSource>; override val videoSources: Array<IVideoSource> get() = video.videoSource.toTypedArray();
override val audioSources: Array<IAudioSource>; override val audioSources: Array<IAudioSource> get() = video.audioSource.toTypedArray();
constructor(video: VideoLocal) {
videoSources = video.videoSource.toTypedArray();
audioSources = video.audioSource.toTypedArray();
}
constructor(audio: LocalAudioContentSource) {
videoSources = arrayOf()
audioSources = arrayOf(audio);
}
constructor(videoSources: Array<IVideoSource>, audioSources: Array<IAudioSource>) {
this.videoSources = videoSources;
this.audioSources = audioSources;
}
} }
@@ -14,8 +14,7 @@ class AudioUrlSource(
override val language: String = Language.UNKNOWN, override val language: String = Language.UNKNOWN,
override val duration: Long? = null, override val duration: Long? = null,
override var priority: Boolean = false, override var priority: Boolean = false,
override var original: Boolean = false, override var original: Boolean = false
var isLocal: Boolean = false
) : IAudioUrlSource, IStreamMetaDataSource{ ) : IAudioUrlSource, IStreamMetaDataSource{
override var streamMetaData: StreamMetaData? = null; override var streamMetaData: StreamMetaData? = null;
@@ -41,7 +41,6 @@ class HLSVariantSubtitleUrlSource(
override val format: String, override val format: String,
) : ISubtitleSource { ) : ISubtitleSource {
override val hasFetch: Boolean = false override val hasFetch: Boolean = false
override val language: String? = null
override fun getSubtitles(): String? { override fun getSubtitles(): String? {
return null return null
@@ -9,15 +9,13 @@ class LocalSubtitleSource : ISubtitleSource {
override val name: String; override val name: String;
override val url: String?; override val url: String?;
override val format: String?; override val format: String?;
override val language: String?
override val hasFetch: Boolean get() = false; override val hasFetch: Boolean get() = false;
val filePath: String; val filePath: String;
constructor(name: String, language: String?, format: String?, filePath: String) { constructor(name: String, format: String?, filePath: String) {
this.name = name; this.name = name;
this.format = format; this.format = format;
this.language = language
this.filePath = filePath; this.filePath = filePath;
this.url = Uri.fromFile(File(filePath)).toString(); this.url = Uri.fromFile(File(filePath)).toString();
} }
@@ -34,7 +32,6 @@ class LocalSubtitleSource : ISubtitleSource {
fun fromSource(source: SubtitleRawSource, path: String): LocalSubtitleSource { fun fromSource(source: SubtitleRawSource, path: String): LocalSubtitleSource {
return LocalSubtitleSource( return LocalSubtitleSource(
source.name, source.name,
source.language,
source.format, source.format,
path path
); );
@@ -6,7 +6,6 @@ import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
@kotlinx.serialization.Serializable @kotlinx.serialization.Serializable
class SubtitleRawSource( class SubtitleRawSource(
override val name: String, override val name: String,
override val language: String?,
override val format: String?, override val format: String?,
val _subtitles: String, val _subtitles: String,
override val url: String? = null, override val url: String? = null,
@@ -14,8 +14,7 @@ open class VideoUrlSource(
override val codec : String = "", override val codec : String = "",
override val bitrate : Int? = 0, override val bitrate : Int? = 0,
override var priority: Boolean = false, override var priority: Boolean = false
var isLocal: Boolean = false
) : IVideoUrlSource, IStreamMetaDataSource { ) : IVideoUrlSource, IStreamMetaDataSource {
override var streamMetaData: StreamMetaData? = null; override var streamMetaData: StreamMetaData? = null;
@@ -7,7 +7,6 @@ interface ISubtitleSource {
val url: String?; val url: String?;
val format: String?; val format: String?;
val hasFetch: Boolean; val hasFetch: Boolean;
val language: String?
fun getSubtitles(): String?; fun getSubtitles(): String?;
@@ -2,7 +2,6 @@ package com.futo.platformplayer.api.media.models.video
import com.futo.platformplayer.api.media.models.Thumbnails import com.futo.platformplayer.api.media.models.Thumbnails
import com.futo.platformplayer.api.media.models.contents.IPlatformContent import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import java.time.OffsetDateTime
/** /**
* A search result representing a video (overview data) * A search result representing a video (overview data)
@@ -13,9 +12,6 @@ interface IPlatformVideo : IPlatformContent {
val duration: Long; val duration: Long;
val viewCount: Long; val viewCount: Long;
val playbackTime: Long;
val playbackDate: OffsetDateTime?;
val isLive : Boolean; val isLive : Boolean;
val isShort: Boolean; val isShort: Boolean;
@@ -1,122 +0,0 @@
package com.futo.platformplayer.api.media.models.video
import android.annotation.SuppressLint
import android.net.Uri
import android.provider.MediaStore
import android.provider.OpenableColumns
import androidx.core.net.toUri
import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.api.media.Serializer
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import com.futo.platformplayer.api.media.models.Thumbnails
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.LocalVideoUnMuxedSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.sources.AudioUrlSource
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
import com.futo.platformplayer.api.media.models.streams.sources.LocalAudioSource
import com.futo.platformplayer.api.media.models.streams.sources.LocalVideoSource
import com.futo.platformplayer.api.media.models.streams.sources.SubtitleRawSource
import com.futo.platformplayer.api.media.models.streams.sources.VideoUrlSource
import com.futo.platformplayer.api.media.platforms.local.models.LocalVideoMuxedSourceDescriptor
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalAudioContentSource
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalVideoContentSource
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalVideoFileSource
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.others.Language
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
import com.futo.platformplayer.states.StateApp
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.time.OffsetDateTime
@kotlinx.serialization.Serializable
open class LocalVideoDetails(
override val id: PlatformID,
override val name: String,
override val thumbnails: Thumbnails,
override val author: PlatformAuthorLink,
override val url: String,
override val duration: Long,
val mimeType: String? = null,
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
override val datetime: OffsetDateTime?
) : IPlatformVideo, IPlatformVideoDetails {
final override val contentType: ContentType get() = ContentType.MEDIA;
override var playbackTime: Long = -1;
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
override var playbackDate: OffsetDateTime? = null;
override val isLive: Boolean get() = false;
override val dash: IDashManifestSource? get() = null;
override val hls: IHLSManifestSource? get() = null;
override val live: IVideoSource? get() = null;
override val shareUrl: String = ""
override val viewCount: Long = -1
override val rating: IRating = RatingLikes(0)
override val description: String = "";
override val video: IVideoSourceDescriptor = (if(mimeType?.startsWith("audio/") ?: false)
(LocalVideoUnMuxedSourceDescriptor(
arrayOf(),
arrayOf(LocalAudioContentSource(url, mimeType ?: "", name, duration))
))
else (LocalVideoMuxedSourceDescriptor(
LocalVideoContentSource(url, mimeType ?: "", name, duration)
))
);
override val preview: ISerializedVideoSourceDescriptor? = null;
override val subtitles: List<SubtitleRawSource> = listOf()
override val isShort: Boolean = false
fun toJson() : String {
return Json.encodeToString(this);
}
fun fromJson(str : String) : SerializedPlatformVideoDetails {
return Serializer.json.decodeFromString<SerializedPlatformVideoDetails>(str);
}
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? = null;
override fun getPlaybackTracker(): IPlaybackTracker? = null;
override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? = null;
companion object {
fun fromFile(name: String, filePath: String, mimeType: String? = null) : LocalVideoDetails {
if(filePath.startsWith("content://"))
return fromContent(filePath, mimeType);
return LocalVideoDetails(PlatformID("FILE", filePath, null, 0, -1),
name, Thumbnails(), PlatformAuthorLink.UNKNOWN, filePath, -1, mimeType, null);
}
fun fromContent(contentUrl: String, mimeType: String? = null) : LocalVideoDetails {
var nameToUse = getFileNameFromContentUrl(contentUrl) ?: "File";
return LocalVideoDetails(PlatformID("FILE", contentUrl, null, 0, -1),
nameToUse, Thumbnails(), PlatformAuthorLink.UNKNOWN, contentUrl, -1, mimeType, null);
}
@SuppressLint("Range")
private fun getFileNameFromContentUrl(url: String): String? {
val cursor = StateApp.instance.context.contentResolver.query(url.toUri(), null, null, null, null);
cursor?.moveToFirst();
val fileName = cursor?.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
cursor?.close();
return fileName;
}
}
}
@@ -3,10 +3,11 @@ package com.futo.platformplayer.api.media.models.video
import com.futo.platformplayer.api.media.PlatformID import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.api.media.Serializer import com.futo.platformplayer.api.media.Serializer
import com.futo.platformplayer.api.media.models.PlatformAuthorLink import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import com.futo.platformplayer.api.media.models.Thumbnail
import com.futo.platformplayer.api.media.models.Thumbnails import com.futo.platformplayer.api.media.models.Thumbnails
import com.futo.platformplayer.api.media.models.contents.ContentType import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
import com.futo.polycentric.core.combineHashCodes
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonNames import kotlinx.serialization.json.JsonNames
@@ -17,7 +18,7 @@ open class SerializedPlatformVideo(
override val contentType: ContentType = ContentType.MEDIA, override val contentType: ContentType = ContentType.MEDIA,
override val id: PlatformID, override val id: PlatformID,
override val name: String, override val name: String,
override val thumbnails: Thumbnails = Thumbnails(), override val thumbnails: Thumbnails,
override val author: PlatformAuthorLink, override val author: PlatformAuthorLink,
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class) @kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
@JsonNames("datetime", "dateTime") @JsonNames("datetime", "dateTime")
@@ -32,10 +33,6 @@ open class SerializedPlatformVideo(
override val isLive: Boolean = false; override val isLive: Boolean = false;
override var playbackTime: Long = -1;
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
override var playbackDate: OffsetDateTime? = null;
override fun toJson() : String { override fun toJson() : String {
return Json.encodeToString(this); return Json.encodeToString(this);
} }
@@ -13,6 +13,7 @@ import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.models.streams.sources.* import com.futo.platformplayer.api.media.models.streams.sources.*
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.time.OffsetDateTime import java.time.OffsetDateTime
@@ -42,10 +43,6 @@ open class SerializedPlatformVideoDetails(
) : IPlatformVideo, IPlatformVideoDetails { ) : IPlatformVideo, IPlatformVideoDetails {
final override val contentType: ContentType get() = ContentType.MEDIA; final override val contentType: ContentType get() = ContentType.MEDIA;
override var playbackTime: Long = -1;
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
override var playbackDate: OffsetDateTime? = null;
override val isLive: Boolean get() = false; override val isLive: Boolean get() = false;
override val dash: IDashManifestSource? get() = null; override val dash: IDashManifestSource? get() = null;
@@ -54,12 +54,8 @@ class DevJSClient : JSClient {
return DevJSClient(context, config, _devScript, _auth, _captcha, devID, descriptor.settings); return DevJSClient(context, config, _devScript, _auth, _captcha, devID, descriptor.settings);
} }
override fun getCopy(privateCopy: Boolean, noSaveState: Boolean): JSClient { override fun getCopy(privateCopy: Boolean): JSClient {
val client = DevJSClient(_context, descriptor, _script, if(!privateCopy) _auth else null, _captcha, if (noSaveState) null else saveState(), devID); return DevJSClient(_context, descriptor, _script, if(!privateCopy) _auth else null, _captcha, saveState(), devID);
client.setReloadData(getReloadData(true));
if (noSaveState)
client.initialize()
return client
} }
override fun initialize() { override fun initialize() {
@@ -10,7 +10,6 @@ import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.IPlatformClient import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.PlatformClientCapabilities import com.futo.platformplayer.api.media.PlatformClientCapabilities
import com.futo.platformplayer.api.media.models.IPlatformChannelContent
import com.futo.platformplayer.api.media.models.PlatformAuthorLink import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import com.futo.platformplayer.api.media.models.ResultCapabilities import com.futo.platformplayer.api.media.models.ResultCapabilities
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
@@ -23,7 +22,6 @@ import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.platforms.js.internal.JSCallDocs import com.futo.platformplayer.api.media.platforms.js.internal.JSCallDocs
import com.futo.platformplayer.api.media.platforms.js.internal.JSDocs import com.futo.platformplayer.api.media.platforms.js.internal.JSDocs
import com.futo.platformplayer.api.media.platforms.js.internal.JSDocsParameter import com.futo.platformplayer.api.media.platforms.js.internal.JSDocsParameter
@@ -33,7 +31,6 @@ import com.futo.platformplayer.api.media.platforms.js.internal.JSParameterDocs
import com.futo.platformplayer.api.media.platforms.js.models.IJSContent import com.futo.platformplayer.api.media.platforms.js.models.IJSContent
import com.futo.platformplayer.api.media.platforms.js.models.IJSContentDetails import com.futo.platformplayer.api.media.platforms.js.models.IJSContentDetails
import com.futo.platformplayer.api.media.platforms.js.models.JSChannel import com.futo.platformplayer.api.media.platforms.js.models.JSChannel
import com.futo.platformplayer.api.media.platforms.js.models.JSChannelContentPager
import com.futo.platformplayer.api.media.platforms.js.models.JSChannelPager import com.futo.platformplayer.api.media.platforms.js.models.JSChannelPager
import com.futo.platformplayer.api.media.platforms.js.models.JSChapter import com.futo.platformplayer.api.media.platforms.js.models.JSChapter
import com.futo.platformplayer.api.media.platforms.js.models.JSComment import com.futo.platformplayer.api.media.platforms.js.models.JSComment
@@ -44,7 +41,6 @@ import com.futo.platformplayer.api.media.platforms.js.models.JSLiveEventPager
import com.futo.platformplayer.api.media.platforms.js.models.JSPlaybackTracker import com.futo.platformplayer.api.media.platforms.js.models.JSPlaybackTracker
import com.futo.platformplayer.api.media.platforms.js.models.JSPlaylistDetails import com.futo.platformplayer.api.media.platforms.js.models.JSPlaylistDetails
import com.futo.platformplayer.api.media.platforms.js.models.JSPlaylistPager import com.futo.platformplayer.api.media.platforms.js.models.JSPlaylistPager
import com.futo.platformplayer.api.media.platforms.js.models.JSVideoPager
import com.futo.platformplayer.api.media.structures.EmptyPager import com.futo.platformplayer.api.media.structures.EmptyPager
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
@@ -61,13 +57,9 @@ import com.futo.platformplayer.states.AnnouncementType
import com.futo.platformplayer.states.StateAnnouncement import com.futo.platformplayer.states.StateAnnouncement
import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StatePlugins import com.futo.platformplayer.states.StatePlugins
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.time.OffsetDateTime import java.time.OffsetDateTime
import java.util.Random
import kotlin.Exception import kotlin.Exception
import kotlin.reflect.full.findAnnotations import kotlin.reflect.full.findAnnotations
import kotlin.reflect.jvm.kotlinFunction import kotlin.reflect.jvm.kotlinFunction
@@ -89,8 +81,6 @@ open class JSClient : IPlatformClient {
private var _channelCapabilities: ResultCapabilities? = null; private var _channelCapabilities: ResultCapabilities? = null;
private var _peekChannelTypes: List<String>? = null; private var _peekChannelTypes: List<String>? = null;
private var _usedReloadData: String? = null;
protected val _script: String; protected val _script: String;
private var _initialized: Boolean = false; private var _initialized: Boolean = false;
@@ -103,17 +93,17 @@ open class JSClient : IPlatformClient {
override val id: String get() = config.id; override val id: String get() = config.id;
override val name: String get() = config.name; override val name: String get() = config.name;
override val icon: ImageVariable get() = StatePlatform.instance.getPlatformIcon(config.id) ?: ImageVariable(config.absoluteIconUrl, null, null) override val icon: ImageVariable;
override var capabilities: PlatformClientCapabilities = PlatformClientCapabilities(); override var capabilities: PlatformClientCapabilities = PlatformClientCapabilities();
private val _busyLock = Object();
private var _busyCounter = 0;
private var _busyAction = ""; private var _busyAction = "";
val isBusy: Boolean get() = _plugin.isBusy; val isBusy: Boolean get() = _busyCounter > 0;
val isBusyAction: String get() { val isBusyAction: String get() {
return _busyAction; return _busyAction;
} }
val declareOnEnable = HashMap<String, String>();
val settings: HashMap<String, String?> get() = descriptor.settings; val settings: HashMap<String, String?> get() = descriptor.settings;
val flags: Array<String>; val flags: Array<String>;
@@ -126,7 +116,6 @@ open class JSClient : IPlatformClient {
val enableInSearch get() = descriptor.appSettings.tabEnabled.enableSearch ?: true val enableInSearch get() = descriptor.appSettings.tabEnabled.enableSearch ?: true
val enableInHome get() = descriptor.appSettings.tabEnabled.enableHome ?: true val enableInHome get() = descriptor.appSettings.tabEnabled.enableHome ?: true
val enableInShorts get() = descriptor.appSettings.tabEnabled.enableShorts ?: true
fun getSubscriptionRateLimit(): Int? { fun getSubscriptionRateLimit(): Int? {
val pluginRateLimit = config.subscriptionRateLimit; val pluginRateLimit = config.subscriptionRateLimit;
@@ -147,14 +136,15 @@ open class JSClient : IPlatformClient {
constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String? = null) { constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String? = null) {
this._context = context; this._context = context;
this.config = descriptor.config; this.config = descriptor.config;
icon = StatePlatform.instance.getPlatformIcon(config.id) ?: ImageVariable(config.absoluteIconUrl, null, null);
this.descriptor = descriptor; this.descriptor = descriptor;
_injectedSaveState = saveState; _injectedSaveState = saveState;
_auth = descriptor.getAuth(); _auth = descriptor.getAuth();
_captcha = descriptor.getCaptchaData(); _captcha = descriptor.getCaptchaData();
flags = descriptor.flags.toTypedArray(); flags = descriptor.flags.toTypedArray();
_httpClient = JSHttpClient(this, null, _captcha, config); _httpClient = JSHttpClient(this, null, _captcha);
_httpClientAuth = JSHttpClient(this, _auth, _captcha, config); _httpClientAuth = JSHttpClient(this, _auth, _captcha);
_plugin = V8Plugin(context, descriptor.config, null, _httpClient, _httpClientAuth); _plugin = V8Plugin(context, descriptor.config, null, _httpClient, _httpClientAuth);
_plugin.withDependency(context, "scripts/polyfil.js"); _plugin.withDependency(context, "scripts/polyfil.js");
_plugin.withDependency(context, "scripts/source.js"); _plugin.withDependency(context, "scripts/source.js");
@@ -177,6 +167,7 @@ open class JSClient : IPlatformClient {
constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String?, script: String, withoutCredentials: Boolean = false) { constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String?, script: String, withoutCredentials: Boolean = false) {
this._context = context; this._context = context;
this.config = descriptor.config; this.config = descriptor.config;
icon = StatePlatform.instance.getPlatformIcon(config.id) ?: ImageVariable(config.absoluteIconUrl, null, null);
this.descriptor = descriptor; this.descriptor = descriptor;
_injectedSaveState = saveState; _injectedSaveState = saveState;
if(!withoutCredentials) if(!withoutCredentials)
@@ -186,8 +177,8 @@ open class JSClient : IPlatformClient {
_captcha = descriptor.getCaptchaData(); _captcha = descriptor.getCaptchaData();
flags = descriptor.flags.toTypedArray(); flags = descriptor.flags.toTypedArray();
_httpClient = JSHttpClient(this, null, _captcha, config); _httpClient = JSHttpClient(this, null, _captcha);
_httpClientAuth = JSHttpClient(this, _auth, _captcha, config); _httpClientAuth = JSHttpClient(this, _auth, _captcha);
_plugin = V8Plugin(context, descriptor.config, script, _httpClient, _httpClientAuth); _plugin = V8Plugin(context, descriptor.config, script, _httpClient, _httpClientAuth);
_plugin.withDependency(context, "scripts/polyfil.js"); _plugin.withDependency(context, "scripts/polyfil.js");
_plugin.withDependency(context, "scripts/source.js"); _plugin.withDependency(context, "scripts/source.js");
@@ -202,12 +193,8 @@ open class JSClient : IPlatformClient {
_plugin.changeAllowDevSubmit(descriptor.appSettings.allowDeveloperSubmit); _plugin.changeAllowDevSubmit(descriptor.appSettings.allowDeveloperSubmit);
} }
open fun getCopy(withoutCredentials: Boolean = false, noSaveState: Boolean = false): JSClient { open fun getCopy(withoutCredentials: Boolean = false): JSClient {
val client = JSClient(_context, descriptor, if (noSaveState) null else saveState(), _script, withoutCredentials); return JSClient(_context, descriptor, saveState(), _script, withoutCredentials);
client.setReloadData(getReloadData(true));
if (noSaveState)
client.initialize()
return client
} }
fun getUnderlyingPlugin(): V8Plugin { fun getUnderlyingPlugin(): V8Plugin {
@@ -221,31 +208,12 @@ open class JSClient : IPlatformClient {
return plugin.httpClientOthers[id]; return plugin.httpClientOthers[id];
} }
fun setReloadData(data: String?) {
if(data == null) {
if(declareOnEnable.containsKey("__reloadData"))
declareOnEnable.remove("__reloadData");
}
else
declareOnEnable.put("__reloadData", data ?: "");
}
fun getReloadData(orLast: Boolean): String? {
if(declareOnEnable.containsKey("__reloadData"))
return declareOnEnable["__reloadData"];
else if(orLast)
return _usedReloadData;
return null;
}
override fun initialize() { override fun initialize() {
if (_initialized) return Logger.i(TAG, "Plugin [${config.name}] initializing");
plugin.start(); plugin.start();
plugin.execute("plugin.config = ${Json.encodeToString(config)}"); plugin.execute("plugin.config = ${Json.encodeToString(config)}");
plugin.execute("plugin.settings = parseSettings(${Json.encodeToString(descriptor.getSettingsWithDefaults())})"); plugin.execute("plugin.settings = parseSettings(${Json.encodeToString(descriptor.getSettingsWithDefaults())})");
descriptor.appSettings.loadDefaults(descriptor.config); descriptor.appSettings.loadDefaults(descriptor.config);
_initialized = true; _initialized = true;
@@ -270,8 +238,7 @@ open class JSClient : IPlatformClient {
hasGetContentChapters = plugin.executeBoolean("!!source.getContentChapters") ?: false, hasGetContentChapters = plugin.executeBoolean("!!source.getContentChapters") ?: false,
hasPeekChannelContents = plugin.executeBoolean("!!source.peekChannelContents") ?: false, hasPeekChannelContents = plugin.executeBoolean("!!source.peekChannelContents") ?: false,
hasGetChannelPlaylists = plugin.executeBoolean("!!source.getChannelPlaylists") ?: false, hasGetChannelPlaylists = plugin.executeBoolean("!!source.getChannelPlaylists") ?: false,
hasGetContentRecommendations = plugin.executeBoolean("!!source.getContentRecommendations") ?: false, hasGetContentRecommendations = plugin.executeBoolean("!!source.getContentRecommendations") ?: false
hasGetUserHistory = plugin.executeBoolean("!!source.getUserHistory") ?: false
); );
try { try {
@@ -286,28 +253,19 @@ open class JSClient : IPlatformClient {
} }
@JSDocs(0, "source.enable()", "Called when the plugin is enabled/started") @JSDocs(0, "source.enable()", "Called when the plugin is enabled/started")
fun enable() = isBusyWith("enable") { fun enable() {
if(!_initialized) if(!_initialized)
initialize(); initialize();
for(toDeclare in declareOnEnable) {
plugin.execute("var ${toDeclare.key} = " + Json.encodeToString(toDeclare.value));
}
plugin.execute("source.enable(${Json.encodeToString(config)}, parseSettings(${Json.encodeToString(descriptor.getSettingsWithDefaults())}), ${Json.encodeToString(_injectedSaveState)})"); plugin.execute("source.enable(${Json.encodeToString(config)}, parseSettings(${Json.encodeToString(descriptor.getSettingsWithDefaults())}), ${Json.encodeToString(_injectedSaveState)})");
if(declareOnEnable.containsKey("__reloadData")) {
Logger.i(TAG, "Plugin [${config.name}] enabled with reload data: ${declareOnEnable["__reloadData"]}");
_usedReloadData = declareOnEnable["__reloadData"];
declareOnEnable.remove("__reloadData");
}
_enabled = true; _enabled = true;
} }
@JSDocs(1, "source.saveState()", "Provide a string that is passed to enable for quicker startup of multiple instances") @JSDocs(1, "source.saveState()", "Provide a string that is passed to enable for quicker startup of multiple instances")
fun saveState(): String? = isBusyWith("saveState") { fun saveState(): String? {
ensureEnabled(); ensureEnabled();
if(!capabilities.hasSaveState) if(!capabilities.hasSaveState)
return@isBusyWith null; return null;
val resp = plugin.executeTyped<V8ValueString>("source.saveState()").value; val resp = plugin.executeTyped<V8ValueString>("source.saveState()").value;
return@isBusyWith resp; return resp;
} }
@JSDocs(1, "source.disable()", "Called before the plugin is disabled/stopped") @JSDocs(1, "source.disable()", "Called before the plugin is disabled/stopped")
@@ -330,13 +288,6 @@ open class JSClient : IPlatformClient {
plugin.executeTyped("source.getHome()")); plugin.executeTyped("source.getHome()"));
} }
@JSDocs(2, "source.getShorts()", "Gets the Shorts feed of the platform")
override fun getShorts(): IPager<IPlatformVideo> = isBusyWith("getShorts") {
ensureEnabled()
return@isBusyWith JSVideoPager(config, this,
plugin.executeTyped("source.getShorts()"))
}
@JSDocs(3, "source.searchSuggestions(query)", "Gets search suggestions for a given query") @JSDocs(3, "source.searchSuggestions(query)", "Gets search suggestions for a given query")
@JSDocsParameter("query", "Query to complete suggestions for") @JSDocsParameter("query", "Query to complete suggestions for")
override fun searchSuggestions(query: String): Array<String> = isBusyWith("searchSuggestions") { override fun searchSuggestions(query: String): Array<String> = isBusyWith("searchSuggestions") {
@@ -355,10 +306,8 @@ open class JSClient : IPlatformClient {
return _searchCapabilities!!; return _searchCapabilities!!;
} }
return busy { _searchCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getSearchCapabilities()"));
_searchCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getSearchCapabilities()")); return _searchCapabilities!!;
return@busy _searchCapabilities!!;
}
} }
catch(ex: Throwable) { catch(ex: Throwable) {
announcePluginUnhandledException("getSearchCapabilities", ex); announcePluginUnhandledException("getSearchCapabilities", ex);
@@ -386,10 +335,8 @@ open class JSClient : IPlatformClient {
if (_searchChannelContentsCapabilities != null) if (_searchChannelContentsCapabilities != null)
return _searchChannelContentsCapabilities!!; return _searchChannelContentsCapabilities!!;
return busy { _searchChannelContentsCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getSearchChannelContentsCapabilities()"));
_searchChannelContentsCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getSearchChannelContentsCapabilities()")); return _searchChannelContentsCapabilities!!;
return@busy _searchChannelContentsCapabilities!!;
}
} }
@JSDocs(5, "source.searchChannelContents(query)", "Searches for videos on the platform") @JSDocs(5, "source.searchChannelContents(query)", "Searches for videos on the platform")
@JSDocsParameter("channelUrl", "Channel url to search") @JSDocsParameter("channelUrl", "Channel url to search")
@@ -414,21 +361,17 @@ open class JSClient : IPlatformClient {
return@isBusyWith JSChannelPager(config, this, return@isBusyWith JSChannelPager(config, this,
plugin.executeTyped("source.searchChannels(${Json.encodeToString(query)})")); plugin.executeTyped("source.searchChannels(${Json.encodeToString(query)})"));
} }
override fun searchChannelsAsContent(query: String): IPager<IPlatformContent> = isBusyWith("searchChannels") {
ensureEnabled();
return@isBusyWith JSChannelContentPager(config, this, plugin.executeTyped("source.searchChannels(${Json.encodeToString(query)})"), );
}
@JSDocs(6, "source.isChannelUrl(url)", "Validates if an channel url is for this platform") @JSDocs(6, "source.isChannelUrl(url)", "Validates if an channel url is for this platform")
@JSDocsParameter("url", "A channel url (May not be your platform)") @JSDocsParameter("url", "A channel url (May not be your platform)")
override fun isChannelUrl(url: String): Boolean = isBusyWith("isChannelUrl") { override fun isChannelUrl(url: String): Boolean {
try { try {
return@isBusyWith plugin.executeTyped<V8ValueBoolean>("source.isChannelUrl(${Json.encodeToString(url)})") return plugin.executeTyped<V8ValueBoolean>("source.isChannelUrl(${Json.encodeToString(url)})")
.value; .value;
} }
catch(ex: Throwable) { catch(ex: Throwable) {
announcePluginUnhandledException("isChannelUrl", ex); announcePluginUnhandledException("isChannelUrl", ex);
return@isBusyWith false; return false;
} }
} }
@JSDocs(7, "source.getChannel(channelUrl)", "Gets a channel by its url") @JSDocs(7, "source.getChannel(channelUrl)", "Gets a channel by its url")
@@ -446,10 +389,9 @@ open class JSClient : IPlatformClient {
if (_channelCapabilities != null) { if (_channelCapabilities != null) {
return _channelCapabilities!!; return _channelCapabilities!!;
} }
return busy {
_channelCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getChannelCapabilities()")); _channelCapabilities = ResultCapabilities.fromV8(config, plugin.executeTyped("source.getChannelCapabilities()"));
return@busy _channelCapabilities!!; return _channelCapabilities!!;
};
} }
catch(ex: Throwable) { catch(ex: Throwable) {
announcePluginUnhandledException("getChannelCapabilities", ex); announcePluginUnhandledException("getChannelCapabilities", ex);
@@ -560,14 +502,14 @@ open class JSClient : IPlatformClient {
@JSDocs(13, "source.isContentDetailsUrl(url)", "Validates if an content url is for this platform") @JSDocs(13, "source.isContentDetailsUrl(url)", "Validates if an content url is for this platform")
@JSDocsParameter("url", "A content url (May not be your platform)") @JSDocsParameter("url", "A content url (May not be your platform)")
override fun isContentDetailsUrl(url: String): Boolean = isBusyWith("isContentDetailsUrl") { override fun isContentDetailsUrl(url: String): Boolean {
try { try {
return@isBusyWith plugin.executeTyped<V8ValueBoolean>("source.isContentDetailsUrl(${Json.encodeToString(url)})") return plugin.executeTyped<V8ValueBoolean>("source.isContentDetailsUrl(${Json.encodeToString(url)})")
.value; .value;
} }
catch(ex: Throwable) { catch(ex: Throwable) {
announcePluginUnhandledException("isContentDetailsUrl", ex); announcePluginUnhandledException("isContentDetailsUrl", ex);
return@isBusyWith false; return false;
} }
} }
@JSDocs(14, "source.getContentDetails(url)", "Gets content details by its url") @JSDocs(14, "source.getContentDetails(url)", "Gets content details by its url")
@@ -599,7 +541,7 @@ open class JSClient : IPlatformClient {
Logger.i(TAG, "JSClient.getPlaybackTracker(${url})"); Logger.i(TAG, "JSClient.getPlaybackTracker(${url})");
val tracker = plugin.executeTyped<V8Value>("source.getPlaybackTracker(${Json.encodeToString(url)})"); val tracker = plugin.executeTyped<V8Value>("source.getPlaybackTracker(${Json.encodeToString(url)})");
if(tracker is V8ValueObject) if(tracker is V8ValueObject)
return@isBusyWith JSPlaybackTracker(this, tracker); return@isBusyWith JSPlaybackTracker(config, tracker);
else else
return@isBusyWith null; return@isBusyWith null;
} }
@@ -641,6 +583,7 @@ open class JSClient : IPlatformClient {
plugin.executeTyped("source.getLiveEvents(${Json.encodeToString(url)})")); plugin.executeTyped("source.getLiveEvents(${Json.encodeToString(url)})"));
} }
@JSDocs(19, "source.getContentRecommendations(url)", "Gets recommendations of a content page") @JSDocs(19, "source.getContentRecommendations(url)", "Gets recommendations of a content page")
@JSDocsParameter("url", "Url of content") @JSDocsParameter("url", "Url of content")
override fun getContentRecommendations(url: String): IPager<IPlatformContent>? = isBusyWith("getContentRecommendations") { override fun getContentRecommendations(url: String): IPager<IPlatformContent>? = isBusyWith("getContentRecommendations") {
@@ -668,19 +611,17 @@ open class JSClient : IPlatformClient {
@JSOptional @JSOptional
@JSDocs(20, "source.isPlaylistUrl(url)", "Validates if a playlist url is for this platform") @JSDocs(20, "source.isPlaylistUrl(url)", "Validates if a playlist url is for this platform")
@JSDocsParameter("url", "Url of playlist") @JSDocsParameter("url", "Url of playlist")
override fun isPlaylistUrl(url: String): Boolean = isBusyWith("isPlaylistUrl") { override fun isPlaylistUrl(url: String): Boolean {
if (!capabilities.hasGetPlaylist) if (!capabilities.hasGetPlaylist)
return@isBusyWith false; return false;
try { try {
return@isBusyWith busy { return plugin.executeTyped<V8ValueBoolean>("source.isPlaylistUrl(${Json.encodeToString(url)})")
return@busy plugin.executeTyped<V8ValueBoolean>("source.isPlaylistUrl(${Json.encodeToString(url)})") .value;
.value;
}
} }
catch(ex: Throwable) { catch(ex: Throwable) {
announcePluginUnhandledException("isPlaylistUrl", ex); announcePluginUnhandledException("isPlaylistUrl", ex);
return@isBusyWith false; return false;
} }
} }
@JSOptional @JSOptional
@@ -711,13 +652,6 @@ open class JSClient : IPlatformClient {
.toTypedArray(); .toTypedArray();
} }
@JSOptional
@JSDocs(23, "source.getUserHistory()", "Gets the history of the current user")
override fun getUserHistory(): IPager<IPlatformContent> {
ensureEnabled();
return JSContentPager(config, this, plugin.executeTyped("source.getUserHistory()"));
}
fun validate() { fun validate() {
try { try {
plugin.start(); plugin.start();
@@ -789,29 +723,19 @@ open class JSClient : IPlatformClient {
return urls; return urls;
} }
fun <T> busy(handle: ()->T): T {
return _plugin.busy {
return@busy handle();
}
}
fun <T> busyBlockingSuspended(handle: suspend ()->T): T {
return _plugin.busy {
return@busy runBlocking {
return@runBlocking handle();
}
}
}
fun <T> isBusyWith(actionName: String, handle: ()->T): T {
//val busyId = kotlin.random.Random.nextInt(9999);
return busy {
try {
_busyAction = actionName;
return@busy handle();
private fun <T> isBusyWith(actionName: String, handle: ()->T): T {
try {
synchronized(_busyLock) {
_busyCounter++;
} }
finally { _busyAction = actionName;
_busyAction = ""; return handle();
}
finally {
_busyAction = "";
synchronized(_busyLock) {
_busyCounter--;
} }
} }
} }
@@ -1,11 +1,6 @@
package com.futo.platformplayer.api.media.platforms.js package com.futo.platformplayer.api.media.platforms.js
import kotlinx.serialization.Contextual @kotlinx.serialization.Serializable
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import java.util.Dictionary
@Serializable
class SourcePluginAuthConfig( class SourcePluginAuthConfig(
val loginUrl: String, val loginUrl: String,
val completionUrl: String? = null, val completionUrl: String? = null,
@@ -16,44 +11,5 @@ class SourcePluginAuthConfig(
val userAgent: String? = null, val userAgent: String? = null,
val loginButton: String? = null, val loginButton: String? = null,
val domainHeadersToFind: Map<String, List<String>>? = null, val domainHeadersToFind: Map<String, List<String>>? = null,
val loginWarning: String? = null, val loginWarning: String? = null
val loginWarnings: List<Warning>? = null, ) { }
val uiMods: List<UIMod>? = null
) {
@Serializable
class Warning(
val url: String,
val text: String?,
val details: String? = null,
val once: Boolean? = true
) {
@Transient
private var _regex: Regex? = null;
fun getRegex(): Regex {
return _regex ?: url.let {
val reg = Regex(it);
_regex = reg;
return reg;
}
}
}
@Serializable
class UIMod(
val url: String,
val scale: Float?,
val desktop: Boolean?
) {
@Contextual
private var _regex: Regex? = null;
fun getRegex(): Regex {
return _regex ?: url.let {
val reg = Regex(it);
_regex = reg;
return reg;
}
}
}
}
@@ -4,7 +4,6 @@ import android.net.Uri
import com.futo.platformplayer.SignatureProvider import com.futo.platformplayer.SignatureProvider
import com.futo.platformplayer.api.media.Serializer import com.futo.platformplayer.api.media.Serializer
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.matchesDomain import com.futo.platformplayer.matchesDomain
import com.futo.platformplayer.states.StatePlugins import com.futo.platformplayer.states.StatePlugins
import kotlinx.serialization.Contextual import kotlinx.serialization.Contextual
@@ -23,7 +22,7 @@ class SourcePluginConfig(
//Script //Script
val repositoryUrl: String? = null, val repositoryUrl: String? = null,
val scriptUrl: String = "", val scriptUrl: String = "",
var version: Int = -1, val version: Int = -1,
val iconUrl: String? = null, val iconUrl: String? = null,
var id: String = UUID.randomUUID().toString(), var id: String = UUID.randomUUID().toString(),
@@ -48,7 +47,6 @@ class SourcePluginConfig(
var subscriptionRateLimit: Int? = null, var subscriptionRateLimit: Int? = null,
var enableInSearch: Boolean = true, var enableInSearch: Boolean = true,
var enableInHome: Boolean = true, var enableInHome: Boolean = true,
var enableInShorts: Boolean = true,
var supportedClaimTypes: List<Int> = listOf(), var supportedClaimTypes: List<Int> = listOf(),
var primaryClaimFieldType: Int? = null, var primaryClaimFieldType: Int? = null,
var developerSubmitUrl: String? = null, var developerSubmitUrl: String? = null,
@@ -170,17 +168,12 @@ class SourcePluginConfig(
} }
fun validate(text: String): Boolean { fun validate(text: String): Boolean {
try { if(scriptPublicKey.isNullOrEmpty())
if (scriptPublicKey.isNullOrEmpty()) throw IllegalStateException("No public key present");
throw IllegalStateException("No public key present"); if(scriptSignature.isNullOrEmpty())
if (scriptSignature.isNullOrEmpty()) throw IllegalStateException("No signature present");
throw IllegalStateException("No signature present");
return SignatureProvider.verify(text, scriptSignature, scriptPublicKey); return SignatureProvider.verify(text, scriptSignature, scriptPublicKey);
} catch (e: Throwable) {
Logger.e(TAG, "Failed to verify due to an unhandled exception", e)
return false
}
} }
fun isUrlAllowed(url: String): Boolean { fun isUrlAllowed(url: String): Boolean {
@@ -211,8 +204,6 @@ class SourcePluginConfig(
obj.sourceUrl = sourceUrl; obj.sourceUrl = sourceUrl;
return obj; return obj;
} }
private val TAG = "SourcePluginConfig"
} }
@kotlinx.serialization.Serializable @kotlinx.serialization.Serializable
@@ -5,16 +5,10 @@ import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.AnnouncementType import com.futo.platformplayer.states.AnnouncementType
import com.futo.platformplayer.states.StateAnnouncement import com.futo.platformplayer.states.StateAnnouncement
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateHistory
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.views.fields.DropdownFieldOptions import com.futo.platformplayer.views.fields.DropdownFieldOptions
import com.futo.platformplayer.views.fields.FieldForm import com.futo.platformplayer.views.fields.FieldForm
import com.futo.platformplayer.views.fields.FormField import com.futo.platformplayer.views.fields.FormField
import com.futo.platformplayer.views.fields.FormFieldButton
import com.futo.platformplayer.views.fields.FormFieldWarning import com.futo.platformplayer.views.fields.FormFieldWarning
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
@@ -109,22 +103,12 @@ class SourcePluginDescriptor {
@FormField(R.string.home, FieldForm.TOGGLE, R.string.show_content_in_home_tab, 1) @FormField(R.string.home, FieldForm.TOGGLE, R.string.show_content_in_home_tab, 1)
var enableHome: Boolean? = null; var enableHome: Boolean? = null;
@FormField(R.string.search, FieldForm.TOGGLE, R.string.show_content_in_search_results, 2) @FormField(R.string.search, FieldForm.TOGGLE, R.string.show_content_in_search_results, 2)
var enableSearch: Boolean? = null; var enableSearch: Boolean? = null;
@FormField(R.string.shorts, FieldForm.TOGGLE, R.string.show_content_in_shorts_tab, 3)
var enableShorts: Boolean? = null;
} }
@FormField(R.string.sync, "group", R.string.sync_desc, 3,"sync") @FormField(R.string.ratelimit, "group", R.string.ratelimit_description, 3)
var sync = Sync();
@Serializable
class Sync {
@FormField(R.string.sync_history, FieldForm.TOGGLE, R.string.sync_history_desc, 1,"syncHistory")
var enableHistorySync: Boolean? = null;
}
@FormField(R.string.ratelimit, "group", R.string.ratelimit_description, 4)
var rateLimit = RateLimit(); var rateLimit = RateLimit();
@Serializable @Serializable
class RateLimit { class RateLimit {
@@ -159,8 +143,6 @@ class SourcePluginDescriptor {
tabEnabled.enableHome = config.enableInHome tabEnabled.enableHome = config.enableInHome
if(tabEnabled.enableSearch == null) if(tabEnabled.enableSearch == null)
tabEnabled.enableSearch = config.enableInSearch tabEnabled.enableSearch = config.enableInSearch
if(tabEnabled.enableShorts == null)
tabEnabled.enableShorts = config.enableInShorts
} }
} }
@@ -23,7 +23,6 @@ import java.util.UUID
class JSHttpClient : ManagedHttpClient { class JSHttpClient : ManagedHttpClient {
private val _jsClient: JSClient?; private val _jsClient: JSClient?;
private val _jsConfig: SourcePluginConfig?; private val _jsConfig: SourcePluginConfig?;
val config get() = _jsConfig
private val _auth: SourceAuth?; private val _auth: SourceAuth?;
private val _captcha: SourceCaptchaData?; private val _captcha: SourceCaptchaData?;
@@ -68,25 +67,6 @@ class JSHttpClient : ManagedHttpClient {
} }
fun resetAuthCookies() {
_currentCookieMap.clear();
if(!_auth?.cookieMap.isNullOrEmpty()) {
for(domainCookies in _auth!!.cookieMap!!)
_currentCookieMap.put(domainCookies.key, HashMap(domainCookies.value));
}
if(!_captcha?.cookieMap.isNullOrEmpty()) {
for(domainCookies in _captcha!!.cookieMap!!) {
if(_currentCookieMap.containsKey(domainCookies.key))
_currentCookieMap[domainCookies.key]?.putAll(domainCookies.value);
else
_currentCookieMap.put(domainCookies.key, HashMap(domainCookies.value));
}
}
}
fun clearOtherCookies() {
_otherCookieMap.clear();
}
override fun clone(): ManagedHttpClient { override fun clone(): ManagedHttpClient {
val newClient = JSHttpClient(_jsClient, _auth); val newClient = JSHttpClient(_jsClient, _auth);
newClient._currentCookieMap = HashMap(_currentCookieMap.toList().associate { Pair(it.first, HashMap(it.second)) }) newClient._currentCookieMap = HashMap(_currentCookieMap.toList().associate { Pair(it.first, HashMap(it.second)) })
@@ -147,7 +127,7 @@ class JSHttpClient : ManagedHttpClient {
} }
if(doApplyCookies) { if(doApplyCookies) {
if (_currentCookieMap.isNotEmpty() || _otherCookieMap.isNotEmpty()) { if (_currentCookieMap.isNotEmpty()) {
val cookiesToApply = hashMapOf<String, String>(); val cookiesToApply = hashMapOf<String, String>();
synchronized(_currentCookieMap) { synchronized(_currentCookieMap) {
for(cookie in _currentCookieMap for(cookie in _currentCookieMap
@@ -155,12 +135,6 @@ class JSHttpClient : ManagedHttpClient {
.flatMap { it.value.toList() }) .flatMap { it.value.toList() })
cookiesToApply[cookie.first] = cookie.second; cookiesToApply[cookie.first] = cookie.second;
}; };
synchronized(_otherCookieMap) {
for(cookie in _otherCookieMap
.filter { domain.matchesDomain(it.key) }
.flatMap { it.value.toList() })
cookiesToApply[cookie.first] = cookie.second;
}
if(cookiesToApply.size > 0) { if(cookiesToApply.size > 0) {
val cookieString = cookiesToApply.map { it.key + "=" + it.value }.joinToString("; "); val cookieString = cookiesToApply.map { it.key + "=" + it.value }.joinToString("; ");
@@ -255,76 +229,6 @@ class JSHttpClient : ManagedHttpClient {
return resp; return resp;
} }
fun processRequest(method: String, responseCode: Int, url: Uri, headers: Map<String, List<String>>) {
if(doUpdateCookies) {
val domain = url.host?.lowercase() ?: return;
val domainParts = domain.split(".");
val defaultCookieDomain =
"." + domainParts.drop(domainParts.size - 2).joinToString(".");
for (header in headers) {
if(header.key.lowercase() == "set-cookie") {
var domainToUse = domain;
val cookie = cookieStringToPair(header.value.first());
var cookieValue = cookie.second;
if (cookie.first.isNotEmpty() && cookie.second.isNotEmpty()) {
val cookieParts = cookie.second.split(";");
if (cookieParts.size == 0)
continue;
cookieValue = cookieParts[0].trim();
val cookieVariables = cookieParts.drop(1).map {
val splitIndex = it.indexOf("=");
if (splitIndex < 0)
return@map Pair(it.trim().lowercase(), "");
return@map Pair<String, String>(
it.substring(0, splitIndex).lowercase().trim(),
it.substring(splitIndex + 1).trim()
);
}.toMap();
domainToUse = if (cookieVariables.containsKey("domain"))
cookieVariables["domain"]!!.lowercase();
else defaultCookieDomain;
//TODO: Make sure this has no negative effect besides apply cookies to root domain
if(!domainToUse.startsWith("."))
domainToUse = ".${domainToUse}";
}
if ((_auth != null || _currentCookieMap.isNotEmpty())) {
val cookieMap = if (_currentCookieMap.containsKey(domainToUse))
_currentCookieMap[domainToUse]!!;
else {
val newMap = hashMapOf<String, String>();
_currentCookieMap[domainToUse] = newMap
newMap;
}
if (cookieMap.containsKey(cookie.first) || doAllowNewCookies)
cookieMap[cookie.first] = cookieValue;
}
else {
val cookieMap = if (_otherCookieMap.containsKey(domainToUse))
_otherCookieMap[domainToUse]!!;
else {
val newMap = hashMapOf<String, String>();
_otherCookieMap[domainToUse] = newMap
newMap;
}
if (cookieMap.containsKey(cookie.first) || doAllowNewCookies)
cookieMap[cookie.first] = cookieValue;
}
}
}
}
if(_jsClient is DevJSClient) {
//val peekBody = resp.peekBody(1000 * 1000).string();
StateDeveloper.instance.addDevHttpExchange(
StateDeveloper.DevHttpExchange(
StateDeveloper.DevHttpRequest(method, url.toString(), mapOf(*headers.map { Pair(it.key, it.value.joinToString(";")) }.toTypedArray()), ""),
StateDeveloper.DevHttpRequest("RESP", url.toString(), mapOf(*headers.map { Pair(it.key, it.value.joinToString(";")) }.toTypedArray()), "", responseCode)
));
}
}
private fun cookieStringToPair(cookie: String): Pair<String, String> { private fun cookieStringToPair(cookie: String): Pair<String, String> {
val cookieKey = cookie.substring(0, cookie.indexOf("=")); val cookieKey = cookie.substring(0, cookie.indexOf("="));
@@ -1,12 +1,10 @@
package com.futo.platformplayer.api.media.platforms.js.models package com.futo.platformplayer.api.media.platforms.js.models
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.JSChannelContent
import com.futo.platformplayer.api.media.models.contents.ContentType import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.contents.IPlatformContent import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
@@ -14,7 +12,6 @@ interface IJSContent: IPlatformContent {
companion object { companion object {
fun fromV8(plugin: JSClient, obj: V8ValueObject): IPlatformContent { fun fromV8(plugin: JSClient, obj: V8ValueObject): IPlatformContent {
obj.ensureIsBusy();
val config = plugin.config; val config = plugin.config;
val type: Int = obj.getOrThrow(config, "contentType", "ContentItem"); val type: Int = obj.getOrThrow(config, "contentType", "ContentItem");
val pluginType: String? = obj.getOrDefault(config, "plugin_type", "ContentItem", null); val pluginType: String? = obj.getOrDefault(config, "plugin_type", "ContentItem", null);
@@ -29,9 +26,6 @@ interface IJSContent: IPlatformContent {
ContentType.NESTED_VIDEO -> JSNestedMediaContent(config, obj); ContentType.NESTED_VIDEO -> JSNestedMediaContent(config, obj);
ContentType.PLAYLIST -> JSPlaylist(config, obj); ContentType.PLAYLIST -> JSPlaylist(config, obj);
ContentType.LOCKED -> JSLockedContent(config, obj); ContentType.LOCKED -> JSLockedContent(config, obj);
ContentType.CHANNEL -> JSChannelContent(config, obj);
ContentType.ARTICLE -> JSArticle(config, obj);
ContentType.WEB -> JSWeb(config, obj);
else -> throw NotImplementedError("Unknown content type ${type}"); else -> throw NotImplementedError("Unknown content type ${type}");
} }
} }
@@ -6,20 +6,17 @@ import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.ensureIsBusy
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
interface IJSContentDetails: IPlatformContent { interface IJSContentDetails: IPlatformContent {
companion object { companion object {
fun fromV8(plugin: JSClient, obj: V8ValueObject): IPlatformContentDetails { fun fromV8(plugin: JSClient, obj: V8ValueObject): IPlatformContentDetails {
obj.ensureIsBusy();
val type: Int = obj.getOrThrow(plugin.config, "contentType", "ContentDetails"); val type: Int = obj.getOrThrow(plugin.config, "contentType", "ContentDetails");
return when(ContentType.fromInt(type)) { return when(ContentType.fromInt(type)) {
ContentType.MEDIA -> JSVideoDetails(plugin, obj); ContentType.MEDIA -> JSVideoDetails(plugin, obj);
ContentType.POST -> JSPostDetails(plugin.config, obj); ContentType.POST -> JSPostDetails(plugin.config, obj);
ContentType.ARTICLE -> JSArticleDetails(plugin, obj); ContentType.ARTICLE -> JSArticleDetails(plugin, obj);
ContentType.WEB -> JSWebDetails(plugin, obj);
else -> throw NotImplementedError("Unknown content type ${type}"); else -> throw NotImplementedError("Unknown content type ${type}");
} }
} }
@@ -1,44 +0,0 @@
package com.futo.platformplayer.api.media.platforms.js.models
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.IPluginSourced
import com.futo.platformplayer.api.media.models.Thumbnails
import com.futo.platformplayer.api.media.models.article.IPlatformArticle
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
import com.futo.platformplayer.api.media.models.post.IPlatformPost
import com.futo.platformplayer.api.media.models.post.TextType
import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.getOrThrowNullableList
import com.futo.platformplayer.states.StateDeveloper
open class JSArticle(
config: SourcePluginConfig,
obj: V8ValueObject
) : JSContent(config, obj), IPlatformArticle, IPluginSourced {
final override val contentType: ContentType = ContentType.ARTICLE
override val summary: String =
obj.getOrDefault<String>(config, "summary", "PlatformArticle", "") ?: ""
override val thumbnails: Thumbnails? =
if (obj.has("thumbnails"))
Thumbnails.fromV8(
config,
obj.getOrThrow<V8ValueObject>(config, "thumbnails", "PlatformArticle")
)
else
null
}
@@ -4,8 +4,6 @@ import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.IPlatformClient import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.IPluginSourced import com.futo.platformplayer.api.media.IPluginSourced
import com.futo.platformplayer.api.media.models.Thumbnails import com.futo.platformplayer.api.media.models.Thumbnails
import com.futo.platformplayer.api.media.models.article.IPlatformArticle
import com.futo.platformplayer.api.media.models.article.IPlatformArticleDetails
import com.futo.platformplayer.api.media.models.comments.IPlatformComment import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.api.media.models.contents.ContentType import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.contents.IPlatformContent import com.futo.platformplayer.api.media.models.contents.IPlatformContent
@@ -21,40 +19,38 @@ import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.getOrThrowNullableList import com.futo.platformplayer.getOrThrowNullableList
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.states.StateDeveloper import com.futo.platformplayer.states.StateDeveloper
open class JSArticleDetails( open class JSArticleDetails : JSContent, IPluginSourced, IPlatformContentDetails {
private val client: JSClient, final override val contentType: ContentType get() = ContentType.ARTICLE;
obj: V8ValueObject
) : JSContent(client.config, obj), IPlatformArticleDetails, IPluginSourced, IPlatformContentDetails {
final override val contentType: ContentType = ContentType.ARTICLE private val _hasGetComments: Boolean;
private val _hasGetContentRecommendations: Boolean;
private val _hasGetComments: Boolean = _content.has("getComments") val rating: IRating;
private val _hasGetContentRecommendations: Boolean = _content.has("getContentRecommendations")
override val rating: IRating = val summary: String;
obj.getOrDefault<V8ValueObject>(client.config, "rating", "PlatformArticle", null) val thumbnails: Thumbnails?;
?.let { IRating.fromV8(client.config, it, "PlatformArticle") } val segments: List<IJSArticleSegment>;
?: RatingLikes(0)
override val summary: String = constructor(client: JSClient, obj: V8ValueObject): super(client.config, obj) {
_content.getOrThrow(client.config, "summary", "PlatformArticle") val contextName = "PlatformPost";
override val thumbnails: Thumbnails? = rating = obj.getOrDefault<V8ValueObject>(client.config, "rating", contextName, null)?.let { IRating.fromV8(client.config, it, contextName) } ?: RatingLikes(0);
if (_content.has("thumbnails")) summary = _content.getOrThrow(client.config, "summary", contextName);
Thumbnails.fromV8( if(_content.has("thumbnails"))
client.config, thumbnails = Thumbnails.fromV8(client.config, _content.getOrThrow(client.config, "thumbnails", contextName));
_content.getOrThrow(client.config, "thumbnails", "PlatformArticle")
)
else else
null thumbnails = null;
override val segments: List<IJSArticleSegment> =
obj.getOrThrowNullableList<V8ValueObject>(client.config, "segments", "PlatformArticle") segments = (obj.getOrThrowNullableList<V8ValueObject>(client.config, "segments", contextName)
?.mapNotNull { fromV8Segment(client, it) } ?.map { fromV8Segment(client, it) }
?: emptyList() ?.filterNotNull() ?: listOf());
_hasGetComments = _content.has("getComments");
_hasGetContentRecommendations = _content.has("getContentRecommendations");
}
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? { override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? {
if(!_hasGetComments || _content.isClosed) if(!_hasGetComments || _content.isClosed)
@@ -87,12 +83,12 @@ open class JSArticleDetails(
} }
private fun getContentRecommendationsJS(client: JSClient): JSContentPager { private fun getContentRecommendationsJS(client: JSClient): JSContentPager {
val contentPager = _content.invokeV8<V8ValueObject>("getContentRecommendations", arrayOf<Any>()); val contentPager = _content.invoke<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
return JSContentPager(_pluginConfig, client, contentPager); return JSContentPager(_pluginConfig, client, contentPager);
} }
private fun getCommentsJS(client: JSClient): JSCommentPager { private fun getCommentsJS(client: JSClient): JSCommentPager {
val commentPager = _content.invokeV8<V8ValueObject>("getComments", arrayOf<Any>()); val commentPager = _content.invoke<V8ValueObject>("getComments", arrayOf<Any>());
return JSCommentPager(_pluginConfig, client, commentPager); return JSCommentPager(_pluginConfig, client, commentPager);
} }
@@ -103,7 +99,6 @@ open class JSArticleDetails(
return when(SegmentType.fromInt(obj.getOrThrow(client.config, "type", "JSArticle.Segment"))) { return when(SegmentType.fromInt(obj.getOrThrow(client.config, "type", "JSArticle.Segment"))) {
SegmentType.TEXT -> JSTextSegment(client, obj); SegmentType.TEXT -> JSTextSegment(client, obj);
SegmentType.IMAGES -> JSImagesSegment(client, obj); SegmentType.IMAGES -> JSImagesSegment(client, obj);
SegmentType.HEADER -> JSHeaderSegment(client, obj);
SegmentType.NESTED -> JSNestedSegment(client, obj); SegmentType.NESTED -> JSNestedSegment(client, obj);
else -> null; else -> null;
} }
@@ -115,7 +110,6 @@ enum class SegmentType(val value: Int) {
UNKNOWN(0), UNKNOWN(0),
TEXT(1), TEXT(1),
IMAGES(2), IMAGES(2),
HEADER(3),
NESTED(9); NESTED(9);
@@ -156,17 +150,6 @@ class JSImagesSegment: IJSArticleSegment {
caption = obj.getOrDefault(client.config, "caption", contextName, "") ?: ""; caption = obj.getOrDefault(client.config, "caption", contextName, "") ?: "";
} }
} }
class JSHeaderSegment: IJSArticleSegment {
override val type = SegmentType.HEADER;
val content: String;
val level: Int;
constructor(client: JSClient, obj: V8ValueObject) {
val contextName = "JSHeaderSegment";
content = obj.getOrDefault(client.config, "content", contextName, "") ?: "";
level = obj.getOrDefault(client.config, "level", contextName, 1) ?: 1;
}
}
class JSNestedSegment: IJSArticleSegment { class JSNestedSegment: IJSArticleSegment {
override val type = SegmentType.NESTED; override val type = SegmentType.NESTED;
val nested: IPlatformContent; val nested: IPlatformContent;
@@ -5,6 +5,7 @@ import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.engine.V8Plugin
class JSChannelPager : JSPager<PlatformAuthorLink>, IPager<PlatformAuthorLink> { class JSChannelPager : JSPager<PlatformAuthorLink>, IPager<PlatformAuthorLink> {
@@ -12,7 +12,6 @@ import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.getOrThrowNullable import com.futo.platformplayer.getOrThrowNullable
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.OffsetDateTime import java.time.OffsetDateTime
@@ -61,7 +60,7 @@ class JSComment : IPlatformComment {
if(!_hasGetReplies) if(!_hasGetReplies)
return null; return null;
val obj = _comment!!.invokeV8<V8ValueObject>("getReplies", arrayOf<Any>()); val obj = _comment!!.invoke<V8ValueObject>("getReplies", arrayOf<Any>());
val plugin = if(client is JSClient) client else throw NotImplementedError("Only implemented for JSClient"); val plugin = if(client is JSClient) client else throw NotImplementedError("Only implemented for JSClient");
return JSCommentPager(_config!!, plugin, obj); return JSCommentPager(_config!!, plugin, obj);
} }
@@ -16,49 +16,51 @@ import java.time.LocalDateTime
import java.time.OffsetDateTime import java.time.OffsetDateTime
import java.time.ZoneOffset import java.time.ZoneOffset
open class JSContent( open class JSContent : IPlatformContent, IPluginSourced {
protected val _pluginConfig: SourcePluginConfig, protected val _pluginConfig: SourcePluginConfig;
protected val _content: V8ValueObject protected val _content : V8ValueObject;
) : IPlatformContent, IPluginSourced {
override val contentType: ContentType = ContentType.UNKNOWN protected val _hasGetDetails: Boolean;
protected val _hasGetDetails: Boolean = _content.has("getDetails") override val contentType: ContentType get() = ContentType.UNKNOWN;
override val id: PlatformID = override val id: PlatformID;
PlatformID.fromV8(_pluginConfig, _content.getOrThrow(_pluginConfig, "id", CTX)) override val name: String;
override val author: PlatformAuthorLink;
override val datetime: OffsetDateTime?;
override val name: String = override val url: String;
HtmlCompat.fromHtml( override val shareUrl: String;
_content.getOrThrow<String>(_pluginConfig, "name", CTX).decodeUnicode(),
HtmlCompat.FROM_HTML_MODE_LEGACY
).toString()
override val author: PlatformAuthorLink = override val sourceConfig: SourcePluginConfig get() = _pluginConfig;
_content.getOrDefault<V8ValueObject>(_pluginConfig, "author", CTX, null)
?.let { PlatformAuthorLink.fromV8(_pluginConfig, it) }
?: PlatformAuthorLink.UNKNOWN
private val _epoch: Long? = constructor(config: SourcePluginConfig, obj: V8ValueObject) {
_content.getOrDefault<Long>(_pluginConfig, "datetime", CTX, null)?.toLong() _pluginConfig = config;
_content = obj;
override val datetime: OffsetDateTime? = val contextName = "PlatformContent";
_epoch?.takeIf { it != 0L }?.let {
OffsetDateTime.of(LocalDateTime.ofEpochSecond(it, 0, ZoneOffset.UTC), ZoneOffset.UTC)
}
override val url: String = id = PlatformID.fromV8(_pluginConfig, _content.getOrThrow(config, "id", contextName));
_content.getOrThrow<String>(_pluginConfig, "url", CTX) name = HtmlCompat.fromHtml(_content.getOrThrow<String>(config, "name", contextName).decodeUnicode(), HtmlCompat.FROM_HTML_MODE_LEGACY).toString();
override val shareUrl: String = val authorObj = _content.getOrDefault<V8ValueObject>(config, "author", contextName, null);
_content.getOrDefault<String>(_pluginConfig, "shareUrl", CTX, null) ?: url if(authorObj != null)
author = PlatformAuthorLink.fromV8(_pluginConfig, authorObj);
else
author = PlatformAuthorLink.UNKNOWN;
override val sourceConfig: SourcePluginConfig val datetimeInt = _content.getOrThrow<Int>(config, "datetime", contextName).toLong();
get() = _pluginConfig if(datetimeInt == 0.toLong())
datetime = null;
else
datetime = OffsetDateTime.of(LocalDateTime.ofEpochSecond(datetimeInt, 0, ZoneOffset.UTC), ZoneOffset.UTC);
url = _content.getOrThrow(config, "url", contextName);
shareUrl = _content.getOrDefault<String>(config, "shareUrl", contextName, null) ?: url;
fun getUnderlyingObject(): V8ValueObject? = _content _hasGetDetails = _content.has("getDetails");
companion object {
private const val CTX = "PlatformContent"
} }
}
fun getUnderlyingObject(): V8ValueObject? {
return _content;
}
}
@@ -2,7 +2,6 @@ package com.futo.platformplayer.api.media.platforms.js.models
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.IPluginSourced import com.futo.platformplayer.api.media.IPluginSourced
import com.futo.platformplayer.api.media.models.JSChannelContent
import com.futo.platformplayer.api.media.models.contents.IPlatformContent import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
@@ -16,14 +15,4 @@ class JSContentPager : JSPager<IPlatformContent>, IPluginSourced {
override fun convertResult(obj: V8ValueObject): IPlatformContent { override fun convertResult(obj: V8ValueObject): IPlatformContent {
return IJSContent.fromV8(plugin, obj); return IJSContent.fromV8(plugin, obj);
} }
}
class JSChannelContentPager : JSPager<IPlatformContent>, IPluginSourced {
override val sourceConfig: SourcePluginConfig get() = config;
constructor(config: SourcePluginConfig, plugin: JSClient, pager: V8ValueObject) : super(config, plugin, pager) {}
override fun convertResult(obj: V8ValueObject): IPlatformContent {
return JSChannelContent(config, obj);
}
} }
@@ -15,7 +15,7 @@ class JSLiveEventPager : JSPager<IPlatformLiveEvent>, IPlatformLiveEventPager {
nextRequest = pager.getOrThrow(config, "nextRequest", "LiveEventPager"); nextRequest = pager.getOrThrow(config, "nextRequest", "LiveEventPager");
} }
override fun nextPage() = plugin.isBusyWith("JSLiveEventPager.nextPage") { override fun nextPage() {
super.nextPage(); super.nextPage();
nextRequest = pager.getOrThrow(config, "nextRequest", "LiveEventPager"); nextRequest = pager.getOrThrow(config, "nextRequest", "LiveEventPager");
} }
@@ -10,7 +10,6 @@ import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrDefault import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.warnIfMainThread import com.futo.platformplayer.warnIfMainThread
abstract class JSPager<T> : IPager<T> { abstract class JSPager<T> : IPager<T> {
@@ -19,8 +18,8 @@ abstract class JSPager<T> : IPager<T> {
protected var pager: V8ValueObject; protected var pager: V8ValueObject;
private var _lastResults: List<T>? = null; private var _lastResults: List<T>? = null;
protected var _resultChanged: Boolean = true; private var _resultChanged: Boolean = true;
protected var _hasMorePages: Boolean = false; private var _hasMorePages: Boolean = false;
//private var _morePagesWasFalse: Boolean = false; //private var _morePagesWasFalse: Boolean = false;
val isAvailable get() = plugin.getUnderlyingPlugin()._runtime?.let { !it.isClosed && !it.isDead } ?: false; val isAvailable get() = plugin.getUnderlyingPlugin()._runtime?.let { !it.isClosed && !it.isDead } ?: false;
@@ -30,9 +29,7 @@ abstract class JSPager<T> : IPager<T> {
this.pager = pager; this.pager = pager;
this.config = config; this.config = config;
plugin.busy { _hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;
_hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;
}
getResults(); getResults();
} }
@@ -41,20 +38,17 @@ abstract class JSPager<T> : IPager<T> {
} }
override fun hasMorePages(): Boolean { override fun hasMorePages(): Boolean {
return _hasMorePages && !pager.isClosed; return _hasMorePages;
} }
override fun nextPage() { override fun nextPage() {
warnIfMainThread("JSPager.nextPage"); warnIfMainThread("JSPager.nextPage");
val pluginV8 = plugin.getUnderlyingPlugin(); pager = plugin.getUnderlyingPlugin().catchScriptErrors("[${plugin.config.name}] JSPager", "pager.nextPage()") {
pluginV8.busy { pager.invoke("nextPage", arrayOf<Any>());
pager = pluginV8.catchScriptErrors("[${plugin.config.name}] JSPager", "pager.nextPage()") { };
pager.invokeV8("nextPage", arrayOf<Any>()); _hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;
}; _resultChanged = true;
_hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;
_resultChanged = true;
}
/* /*
try { try {
} }
@@ -76,18 +70,15 @@ abstract class JSPager<T> : IPager<T> {
return previousResults; return previousResults;
warnIfMainThread("JSPager.getResults"); warnIfMainThread("JSPager.getResults");
val items = pager.getOrThrow<V8ValueArray>(config, "results", "JSPager");
return plugin.getUnderlyingPlugin().busy { if(items.v8Runtime.isDead || items.v8Runtime.isClosed)
val items = pager.getOrThrow<V8ValueArray>(config, "results", "JSPager"); throw IllegalStateException("Runtime closed");
if (items.v8Runtime.isDead || items.v8Runtime.isClosed) val newResults = items.toArray()
throw IllegalStateException("Runtime closed"); .map { convertResult(it as V8ValueObject) }
val newResults = items.toArray() .toList();
.map { convertResult(it as V8ValueObject) } _lastResults = newResults;
.toList(); _resultChanged = false;
_lastResults = newResults; return newResults;
_resultChanged = false;
return@busy newResults;
}
} }
abstract fun convertResult(obj: V8ValueObject): T; abstract fun convertResult(obj: V8ValueObject): T;

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