Compare commits

...

27 Commits

Author SHA1 Message Date
Koen J aac19aef86 Long press moved to layout comment. 2025-11-06 14:26:11 +01:00
Koen J 33efc5c21d Upgraded all dependencies and changed double tap to long press on comment text. 2025-11-06 14:25:27 +01:00
Koen J fc7001c295 Added double click to copy button on comments. 2025-11-06 11:29:24 +01:00
Koen J 9b68394f70 Added setting for persisting subtitles across multiple videos when the same language exists. 2025-11-05 15:02:33 +01:00
Koen J e2ef8c2593 Shorts player keep screen on interaction. 2025-11-05 12:18:47 +01:00
Koen J 551bfe44ac Loader game visible now allows going into pip automatically. 2025-11-05 09:04:01 +01:00
Koen J 6fbfa98ad3 Made the resume more persistent and not visible when loader game is visible. 2025-11-03 17:58:03 +01:00
Koen 7d19c2357c Merge branch 'aw/polycentric-moderation' into 'master'
Polycentric Moderation

See merge request videostreaming/grayjay!151
2025-10-16 11:13:27 +00:00
austin 64030a038c Polycentric Moderation 2025-10-16 11:13:27 +00:00
Koen 9d9ad52535 Merge branch 'marcus/exp-casting-device-pinning-fix' into 'master'
casting(experimental): ignore devices that are unsupported or fails to parse

See merge request videostreaming/grayjay!150
2025-10-14 07:10:45 +00:00
Marcus Hanestad b10cf6a323 casting(experimental): ignore devices that are unsupported or fails to parse 2025-10-09 09:41:32 +02:00
Koen J 4407e82d8a Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-10-08 16:39:14 +02:00
Koen J 3113dc53a6 fix: Session not authorized showing when it shouldn't 2025-10-08 16:38:10 +02:00
Koen bd25276720 Merge branch 'zvonimir-dev' into 'master'
fix: Session not authorized showing when it shouldn't

See merge request videostreaming/grayjay!149
2025-10-08 13:28:38 +00:00
z2rec 29d3a9986e fix: Session not authorized showing when it shouldn't 2025-10-08 15:27:34 +02:00
Koen J bce93b8e0f Changed intent codes for pip to match with intent codes from notification. 2025-10-07 16:21:37 +02:00
Koen 9a950958f9 Merge branch 'possible-ui-fixes' into 'master'
Possible ui fixes

See merge request videostreaming/grayjay!148
2025-10-06 17:40:35 +00:00
Kelvin b6676e7763 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-10-06 19:14:27 +02:00
Kelvin 35fe093e5c Convert promise cancel exceptions to conventional exceptions 2025-10-06 19:13:52 +02:00
Koen J 7cad4fbe07 Fixed crash in TextView drag drop. 2025-10-06 12:58:10 +02:00
Koen J 240772790d Possible fixes for other activities. 2025-10-06 11:51:04 +02:00
Koen J d659ecc518 Possible fixes for DownloadService issues. 2025-10-06 11:00:47 +02:00
Koen J 7d8bb20b71 Possible fixes for DownloadService issues. 2025-10-06 11:00:36 +02:00
Koen J 1cf5f776d5 Trial 1 2025-10-03 19:22:46 +02:00
Koen J 137ba85538 Sync pairing will now always happen in parallel for direct and relayed and reduced amount of occupied threads. 2025-10-03 14:11:48 +02:00
Kelvin 642d218c54 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-09-29 18:45:56 +02:00
Kelvin 26b5470200 Fix crash fix on async promise handling 2025-09-29 18:45:42 +02:00
70 changed files with 1447 additions and 557 deletions
+42 -44
View File
@@ -1,8 +1,8 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.21'
id 'org.ajoberstar.grgit' version '5.2.2'
id 'org.jetbrains.kotlin.plugin.serialization' version '2.2.21'
id 'org.ajoberstar.grgit' version '5.3.3'
id 'com.google.protobuf'
id 'kotlin-parcelize'
id 'com.google.devtools.ksp'
@@ -97,7 +97,7 @@ android {
defaultConfig {
minSdk 28
targetSdk 35
targetSdk 36
versionCode gitVersionCode
versionName gitVersionName
@@ -155,82 +155,80 @@ android {
dependencies {
//implementation 'com.google.dagger:dagger:2.48'
implementation 'androidx.test:monitor:1.7.2'
implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.test:monitor:1.8.0'
implementation 'com.google.android.material:material:1.13.0'
//annotationProcessor 'com.google.dagger:dagger-compiler:2.48'
//Core
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.11.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.core:core-ktx:1.17.0'
implementation 'androidx.appcompat:appcompat:1.7.1'
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
implementation 'androidx.documentfile:documentfile:1.1.0'
//Images
annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'
implementation 'com.github.bumptech.glide:glide:4.16.0'
annotationProcessor 'com.github.bumptech.glide:compiler:5.0.5'
implementation 'com.github.bumptech.glide:glide:5.0.5'
//Async
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2"
//HTTP
implementation "com.squareup.okhttp3:okhttp:4.11.0"
implementation "com.squareup.okhttp3:okhttp:5.3.0"
//JSON
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2" //Used for structured json
implementation 'com.google.code.gson:gson:2.10.1' //Used for complex/anonymous cases like during development conversions (eg. V8RemoteObject)
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0" //Used for structured json
implementation 'com.google.code.gson:gson:2.13.2' //Used for complex/anonymous cases like during development conversions (eg. V8RemoteObject)
//JS
implementation("com.caoccao.javet:javet-android:3.0.2")
//implementation 'com.caoccao.javet:javet-v8-android:4.1.4' //Change after extensive testing the freezing edge cases are solved.
implementation 'com.caoccao.javet:javet-v8-android:5.0.1'
//Exoplayer
implementation 'androidx.media3:media3-exoplayer:1.2.1'
implementation 'androidx.media3:media3-exoplayer-dash:1.2.1'
implementation 'androidx.media3:media3-ui:1.2.1'
implementation 'androidx.media3:media3-exoplayer-hls:1.2.1'
implementation 'androidx.media3:media3-exoplayer-rtsp:1.2.1'
implementation 'androidx.media3:media3-exoplayer-smoothstreaming:1.2.1'
implementation 'androidx.media3:media3-transformer:1.2.1'
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.6'
implementation 'androidx.navigation:navigation-ui-ktx:2.7.6'
implementation 'androidx.media:media:1.7.0'
implementation 'androidx.media3:media3-exoplayer:1.8.0'
implementation 'androidx.media3:media3-exoplayer-dash:1.8.0'
implementation 'androidx.media3:media3-ui:1.8.0'
implementation 'androidx.media3:media3-exoplayer-hls:1.8.0'
implementation 'androidx.media3:media3-exoplayer-rtsp:1.8.0'
implementation 'androidx.media3:media3-exoplayer-smoothstreaming:1.8.0'
implementation 'androidx.media3:media3-transformer:1.8.0'
implementation 'androidx.navigation:navigation-fragment-ktx:2.9.6'
implementation 'androidx.navigation:navigation-ui-ktx:2.9.6'
implementation 'androidx.media:media:1.7.1'
//Other
implementation 'org.jsoup:jsoup:1.15.3'
implementation 'org.jsoup:jsoup:1.21.2'
implementation 'com.google.android.flexbox:flexbox:3.0.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation fileTree(dir: 'aar', include: ['*.aar'])
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.google.zxing:core:3.4.1'
implementation 'com.google.zxing:core:3.5.3'
implementation 'com.journeyapps:zxing-android-embedded:4.3.0'
implementation 'com.caverock:androidsvg-aar:1.4'
//Protobuf
implementation 'com.google.protobuf:protobuf-javalite:3.25.1'
implementation 'com.google.protobuf:protobuf-javalite:4.33.0'
implementation 'com.polycentric.core:app:1.0'
implementation 'com.futo.futopay:app:1.0'
implementation 'androidx.work:work-runtime-ktx:2.9.0'
implementation 'androidx.concurrent:concurrent-futures-ktx:1.1.0'
implementation 'androidx.work:work-runtime-ktx:2.11.0'
implementation 'androidx.concurrent:concurrent-futures-ktx:1.3.0'
//Database
implementation("androidx.room:room-runtime:2.6.1")
annotationProcessor("androidx.room:room-compiler:2.6.1")
ksp("androidx.room:room-compiler:2.6.1")
implementation("androidx.room:room-ktx:2.6.1")
implementation("androidx.room:room-runtime:2.8.3")
ksp("androidx.room:room-compiler:2.8.3")
implementation("androidx.room:room-ktx:2.8.3")
//Payment
implementation 'com.stripe:stripe-android:20.35.1'
implementation 'com.stripe:stripe-android:22.0.0'
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
testImplementation "org.jetbrains.kotlin:kotlin-test:1.8.22"
testImplementation "org.xmlunit:xmlunit-core:2.9.1"
testImplementation "org.mockito:mockito-core:5.4.0"
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2'
testImplementation "org.jetbrains.kotlin:kotlin-test:2.0.21"
testImplementation "org.xmlunit:xmlunit-core:2.11.0"
testImplementation "org.mockito:mockito-core:5.20.0"
androidTestImplementation 'androidx.test.ext:junit:1.3.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0'
//Rust casting SDK
implementation('org.futo.gitlab.videostreaming.fcast-sdk-jitpack:sender-sdk-minimal:0.3.1') {
+24 -20
View File
@@ -153,30 +153,30 @@
</activity>
<activity
android:name=".activities.TestActivity"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
<activity
android:name=".activities.SettingsActivity"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
<activity
android:name=".activities.DeveloperActivity"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
<activity
android:name=".activities.ExceptionActivity"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
<activity
android:name=".activities.CaptchaActivity"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
<activity
android:name=".activities.LoginActivity"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
<activity
android:name=".activities.AddSourceActivity"
android:exported="true"
android:theme="@style/Theme.FutoVideo.NoActionBar">
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
@@ -189,54 +189,58 @@
<activity
android:name=".activities.AddSourceOptionsActivity"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
<activity
android:name=".activities.PolycentricHomeActivity"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
<activity
android:name=".activities.PolycentricBackupActivity"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
<activity
android:name=".activities.PolycentricCreateProfileActivity"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
<activity
android:name=".activities.PolycentricProfileActivity"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
<activity
android:name=".activities.PolycentricWhyActivity"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
<activity
android:name=".activities.PolycentricImportProfileActivity"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
<activity
android:name=".activities.ManageTabsActivity"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
<activity
android:name=".activities.QRCaptureActivity"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
<activity
android:name=".activities.FCastGuideActivity"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
<activity
android:name=".activities.SyncHomeActivity"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
<activity
android:name=".activities.SyncPairActivity"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
<activity
android:name=".activities.SyncShowPairingCodeActivity"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
<activity
android:name=".activities.PolycentricModerationActivity"
android:exported="false"
android:screenOrientation="portrait" />
</application>
</manifest>
@@ -216,10 +216,9 @@ private fun ByteArray.toInetAddress(): InetAddress {
return InetAddress.getByAddress(this);
}
fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int): Socket? {
fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int, timeoutMs: Int = 10_000): Socket? {
ensureNotMainThread()
val timeout = 10000
val addresses = if(!Settings.instance.casting.allowIpv6) attemptAddresses.filterIsInstance<Inet4Address>() else attemptAddresses;
if(addresses.isEmpty())
throw IllegalStateException("No valid addresses found (ipv6: ${(if(Settings.instance.casting.allowIpv6) "enabled" else "disabled")})");
@@ -232,7 +231,7 @@ fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int): Socket?
val socket = Socket()
try {
return socket.apply { this.connect(InetSocketAddress(addresses[0], port), timeout) }
return socket.apply { this.connect(InetSocketAddress(addresses[0], port), timeoutMs) }
} catch (e: Throwable) {
Log.i("getConnectedSocket", "Failed to connect to: ${addresses[0]}", e)
socket.close()
@@ -263,7 +262,7 @@ fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int): Socket?
}
}
socket.connect(InetSocketAddress(address, port), timeout);
socket.connect(InetSocketAddress(address, port), timeoutMs);
synchronized(syncObject) {
if (connectedSocket == null) {
@@ -7,11 +7,13 @@ 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.V8ValuePromise
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.engine.exceptions.ScriptExecutionException
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
@@ -21,7 +23,6 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.selects.SelectClause0
import kotlinx.coroutines.selects.SelectClause1
import java.util.concurrent.CancellationException
import java.util.concurrent.CountDownLatch
import kotlin.coroutines.AbstractCoroutineContextElement
import kotlin.coroutines.CoroutineContext
@@ -268,12 +269,25 @@ fun <T: V8Value> V8ValuePromise.toV8ValueAsync(plugin: V8Plugin): V8Deferred<T>
underlyingDef.complete(p0 as T);
}
override fun onRejected(p0: V8Value?) {
plugin.resolvePromise(promise);
underlyingDef.completeExceptionally(p0?.toException(plugin.config) ?: NotImplementedError("onRejected promise not implemented.."));
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?) {
plugin.resolvePromise(promise);
underlyingDef.completeExceptionally(p0?.toException(plugin.config) ?: NotImplementedError("onCatch promise not implemented.."));
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);
}
}
});
}
@@ -287,13 +301,16 @@ fun <T: V8Value> V8ValuePromise.toV8ValueAsync(plugin: V8Plugin): V8Deferred<T>
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 Exception("Promise Failed: " + pluginType + msg);
return Throwable("Promise Failed: " + pluginType + msg);
*/
}
else if(p0 is V8ValueString)
return Exception("Promise Failed:" + p0.value);
return Throwable("Promise Failed:" + p0.value);
else
return NotImplementedError("onCatch promise not implemented..");
}
@@ -358,4 +375,16 @@ fun V8ValueObject.invokeV8VoidAsync(method: String, vararg obj: Any?): V8Deferre
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;
}
}
@@ -0,0 +1,118 @@
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)
}
}
}
@@ -408,6 +408,10 @@ class Settings : FragmentedStorageFileJson() {
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)
var preferOriginalAudio: Boolean = true;
@@ -107,10 +107,9 @@ class AddSourceActivity : AppCompatActivity() {
onNewIntent(intent);
}
override fun onNewIntent(intent: Intent?) {
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
var url = intent?.dataString;
var url = intent.dataString;
if(url == 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));
@@ -16,7 +16,6 @@ import android.os.StrictMode.VmPolicy
import android.util.Log
import android.util.TypedValue
import android.view.View
import android.view.WindowManager
import android.widget.FrameLayout
import android.widget.ImageView
import androidx.activity.result.ActivityResult
@@ -36,6 +35,7 @@ import androidx.lifecycle.withStateAtLeast
import androidx.media3.common.util.UnstableApi
import com.futo.platformplayer.BuildConfig
import com.futo.platformplayer.R
import com.futo.platformplayer.RootInsetsController
import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.http.ManagedHttpClient
@@ -199,6 +199,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
private var _privateModeEnabled = false
private var _pictureInPictureEnabled = false
private var _isFullscreen = false
private lateinit var _rootInsetsController: RootInsetsController
private val _urlQrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
@@ -284,9 +285,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
}
setContentView(R.layout.activity_main);
setNavigationBarColorAndIcons();
if (Settings.instance.playback.allowVideoToGoUnderCutout)
window.attributes.layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
runBlocking {
try {
@@ -301,6 +299,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
FragmentedStorage.get<Settings>();
rootView = findViewById(R.id.rootView);
_rootInsetsController = RootInsetsController.attach(this, rootView)
_rootInsetsController.setLightSystemBarAppearance(lightStatus = false, lightNav = false)
_fragContainerTopBar = findViewById(R.id.fragment_top_bar);
_fragContainerMain = findViewById(R.id.fragment_main);
_fragContainerBotBar = findViewById(R.id.fragment_bottom_bar);
@@ -411,6 +412,11 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
Logger.i(TAG, "onFullscreenChanged ${it}");
_isFullscreen = it
updatePrivateModeVisibility()
if (it) {
_rootInsetsController.enterFullscreen(allowCutoutShortEdges = Settings.instance.playback.allowVideoToGoUnderCutout)
} else {
_rootInsetsController.exitFullscreen()
}
}
_fragVideoDetail.onMinimize.subscribe {
@@ -639,6 +645,11 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
private var _qrCodeLoadingDialog: AlertDialog? = null
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
_rootInsetsController.onConfigurationChanged()
}
fun showUrlQrCodeScanner() {
try {
_qrCodeLoadingDialog = UIDialogs.showDialog(this, R.drawable.ic_loader_animated, true,
@@ -697,17 +708,13 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_wasStopped = true;
}
override fun onNewIntent(intent: Intent?) {
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent);
handleIntent(intent);
}
private fun handleIntent(intent: Intent?) {
if (intent == null)
return;
private fun handleIntent(intent: Intent) {
Logger.i(TAG, "handleIntent started by " + intent.action);
var targetData: String? = null;
when (intent.action) {
@@ -0,0 +1,147 @@
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 _editName: EditText;
private lateinit var _buttonExport: BigButton;
private lateinit var _buttonOpenHarborProfile: BigButton;
private lateinit var _buttonModeration: BigButton;
private lateinit var _buttonLogout: BigButton;
private lateinit var _buttonDelete: BigButton;
private lateinit var _username: String;
@@ -71,7 +71,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
_imagePolycentric = findViewById(R.id.image_polycentric);
_editName = findViewById(R.id.edit_profile_name);
_buttonExport = findViewById(R.id.button_export);
_buttonOpenHarborProfile = findViewById(R.id.button_open_harbor_profile);
_buttonModeration = findViewById(R.id.button_moderation);
_buttonLogout = findViewById(R.id.button_logout);
_buttonDelete = findViewById(R.id.button_delete);
_loaderOverlay = findViewById(R.id.loader_overlay);
@@ -99,15 +99,9 @@ class PolycentricProfileActivity : AppCompatActivity() {
startActivity(Intent(this, PolycentricBackupActivity::class.java));
};
_buttonOpenHarborProfile.onClick.subscribe {
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)))
}
}
_buttonModeration.onClick.subscribe {
startActivity(Intent(this, PolycentricModerationActivity::class.java));
};
_buttonLogout.onClick.subscribe {
StatePolycentric.instance.setProcessHandle(null);
@@ -110,7 +110,19 @@ class SyncPairActivity : AppCompatActivity() {
lifecycleScope.launch(Dispatchers.IO) {
try {
StateSync.instance.syncService?.connect(deviceInfo) { complete, message ->
var wasCompleted = false
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) {
if (complete != null) {
if (complete) {
@@ -54,14 +54,16 @@ interface IPlatformChannelContent : IPlatformContent {
val subscribers: Long?
}
open class JSChannelContent : JSContent, IPlatformChannelContent {
override val contentType: ContentType get() = ContentType.CHANNEL
override val thumbnail: String?
override val subscribers: Long?
open class JSChannelContent(
config: SourcePluginConfig,
obj: V8ValueObject
) : JSContent(config, obj), IPlatformChannelContent {
constructor(config: SourcePluginConfig, obj: V8ValueObject) : super(config, obj) {
val contextName = "Channel";
thumbnail = obj.getOrDefault<String>(config, "thumbnail", contextName, null)
subscribers = if(obj.has("subscribers")) obj.getOrThrow(config,"subscribers", contextName) else null
}
}
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()
}
@@ -6,25 +6,15 @@ import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.structures.IPager
import java.time.OffsetDateTime
open class PlatformComment : IPlatformComment {
override val contextUrl: String;
override val author: PlatformAuthorLink;
override val message: String;
override val rating: IRating;
override val date: OffsetDateTime;
open class PlatformComment(
override val contextUrl: String,
override val author: PlatformAuthorLink,
override val message: String,
override val rating: IRating,
override val date: OffsetDateTime,
override val replyCount: Int? = null
) : IPlatformComment {
override val replyCount: Int?;
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();
}
}
override fun getReplies(client: IPlatformClient): IPager<IPlatformComment> =
NoCommentsPager()
}
@@ -41,6 +41,7 @@ class HLSVariantSubtitleUrlSource(
override val format: String,
) : ISubtitleSource {
override val hasFetch: Boolean = false
override val language: String? = null
override fun getSubtitles(): String? {
return null
@@ -9,13 +9,15 @@ class LocalSubtitleSource : ISubtitleSource {
override val name: String;
override val url: String?;
override val format: String?;
override val language: String?
override val hasFetch: Boolean get() = false;
val filePath: String;
constructor(name: String, format: String?, filePath: String) {
constructor(name: String, language: String?, format: String?, filePath: String) {
this.name = name;
this.format = format;
this.language = language
this.filePath = filePath;
this.url = Uri.fromFile(File(filePath)).toString();
}
@@ -32,6 +34,7 @@ class LocalSubtitleSource : ISubtitleSource {
fun fromSource(source: SubtitleRawSource, path: String): LocalSubtitleSource {
return LocalSubtitleSource(
source.name,
source.language,
source.format,
path
);
@@ -6,6 +6,7 @@ import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
@kotlinx.serialization.Serializable
class SubtitleRawSource(
override val name: String,
override val language: String?,
override val format: String?,
val _subtitles: String,
override val url: String? = null,
@@ -7,6 +7,7 @@ interface ISubtitleSource {
val url: String?;
val format: String?;
val hasFetch: Boolean;
val language: String?
fun getSubtitles(): String?;
@@ -103,7 +103,7 @@ open class JSClient : IPlatformClient {
override val id: String get() = config.id;
override val name: String get() = config.name;
override val icon: ImageVariable;
override val icon: ImageVariable get() = StatePlatform.instance.getPlatformIcon(config.id) ?: ImageVariable(config.absoluteIconUrl, null, null)
override var capabilities: PlatformClientCapabilities = PlatformClientCapabilities();
private var _busyAction = "";
@@ -147,7 +147,6 @@ open class JSClient : IPlatformClient {
constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String? = null) {
this._context = context;
this.config = descriptor.config;
icon = StatePlatform.instance.getPlatformIcon(config.id) ?: ImageVariable(config.absoluteIconUrl, null, null);
this.descriptor = descriptor;
_injectedSaveState = saveState;
_auth = descriptor.getAuth();
@@ -178,7 +177,6 @@ open class JSClient : IPlatformClient {
constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String?, script: String, withoutCredentials: Boolean = false) {
this._context = context;
this.config = descriptor.config;
icon = StatePlatform.instance.getPlatformIcon(config.id) ?: ImageVariable(config.absoluteIconUrl, null, null);
this.descriptor = descriptor;
_injectedSaveState = saveState;
if(!withoutCredentials)
@@ -23,17 +23,22 @@ import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.getOrThrowNullableList
import com.futo.platformplayer.states.StateDeveloper
open class JSArticle : JSContent, IPlatformArticle, IPluginSourced {
final override val contentType: ContentType get() = ContentType.ARTICLE;
open class JSArticle(
config: SourcePluginConfig,
obj: V8ValueObject
) : JSContent(config, obj), IPlatformArticle, IPluginSourced {
override val summary: String;
override val thumbnails: Thumbnails?;
final override val contentType: ContentType = ContentType.ARTICLE
constructor(config: SourcePluginConfig, obj: V8ValueObject): super(config, obj) {
val contextName = "PlatformArticle";
override val summary: String =
obj.getOrDefault<String>(config, "summary", "PlatformArticle", "") ?: ""
summary = _content.getOrDefault(config, "summary", contextName, "") ?: "";
thumbnails = Thumbnails.fromV8(config, _content.getOrThrow(config, "thumbnails", contextName));
}
}
override val thumbnails: Thumbnails? =
if (obj.has("thumbnails"))
Thumbnails.fromV8(
config,
obj.getOrThrow<V8ValueObject>(config, "thumbnails", "PlatformArticle")
)
else
null
}
@@ -24,36 +24,37 @@ import com.futo.platformplayer.getOrThrowNullableList
import com.futo.platformplayer.invokeV8
import com.futo.platformplayer.states.StateDeveloper
open class JSArticleDetails : JSContent, IPlatformArticleDetails, IPluginSourced, IPlatformContentDetails {
final override val contentType: ContentType get() = ContentType.ARTICLE;
open class JSArticleDetails(
private val client: JSClient,
obj: V8ValueObject
) : JSContent(client.config, obj), IPlatformArticleDetails, IPluginSourced, IPlatformContentDetails {
private val _hasGetComments: Boolean;
private val _hasGetContentRecommendations: Boolean;
final override val contentType: ContentType = ContentType.ARTICLE
override val rating: IRating;
private val _hasGetComments: Boolean = _content.has("getComments")
private val _hasGetContentRecommendations: Boolean = _content.has("getContentRecommendations")
override val summary: String;
override val thumbnails: Thumbnails?;
override val segments: List<IJSArticleSegment>;
override val rating: IRating =
obj.getOrDefault<V8ValueObject>(client.config, "rating", "PlatformArticle", null)
?.let { IRating.fromV8(client.config, it, "PlatformArticle") }
?: RatingLikes(0)
constructor(client: JSClient, obj: V8ValueObject): super(client.config, obj) {
val contextName = "PlatformArticle";
override val summary: String =
_content.getOrThrow(client.config, "summary", "PlatformArticle")
rating = obj.getOrDefault<V8ValueObject>(client.config, "rating", contextName, null)?.let { IRating.fromV8(client.config, it, contextName) } ?: RatingLikes(0);
summary = _content.getOrThrow(client.config, "summary", contextName);
if(_content.has("thumbnails"))
thumbnails = Thumbnails.fromV8(client.config, _content.getOrThrow(client.config, "thumbnails", contextName));
override val thumbnails: Thumbnails? =
if (_content.has("thumbnails"))
Thumbnails.fromV8(
client.config,
_content.getOrThrow(client.config, "thumbnails", "PlatformArticle")
)
else
thumbnails = null;
null
segments = (obj.getOrThrowNullableList<V8ValueObject>(client.config, "segments", contextName)
?.map { fromV8Segment(client, it) }
?.filterNotNull() ?: listOf());
_hasGetComments = _content.has("getComments");
_hasGetContentRecommendations = _content.has("getContentRecommendations");
}
override val segments: List<IJSArticleSegment> =
obj.getOrThrowNullableList<V8ValueObject>(client.config, "segments", "PlatformArticle")
?.mapNotNull { fromV8Segment(client, it) }
?: emptyList()
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? {
if(!_hasGetComments || _content.isClosed)
@@ -16,51 +16,49 @@ import java.time.LocalDateTime
import java.time.OffsetDateTime
import java.time.ZoneOffset
open class JSContent : IPlatformContent, IPluginSourced {
protected val _pluginConfig: SourcePluginConfig;
protected val _content : V8ValueObject;
open class JSContent(
protected val _pluginConfig: SourcePluginConfig,
protected val _content: V8ValueObject
) : IPlatformContent, IPluginSourced {
protected val _hasGetDetails: Boolean;
override val contentType: ContentType = ContentType.UNKNOWN
override val contentType: ContentType get() = ContentType.UNKNOWN;
protected val _hasGetDetails: Boolean = _content.has("getDetails")
override val id: PlatformID;
override val name: String;
override val author: PlatformAuthorLink;
override val datetime: OffsetDateTime?;
override val id: PlatformID =
PlatformID.fromV8(_pluginConfig, _content.getOrThrow(_pluginConfig, "id", CTX))
override val url: String;
override val shareUrl: String;
override val name: String =
HtmlCompat.fromHtml(
_content.getOrThrow<String>(_pluginConfig, "name", CTX).decodeUnicode(),
HtmlCompat.FROM_HTML_MODE_LEGACY
).toString()
override val sourceConfig: SourcePluginConfig get() = _pluginConfig;
override val author: PlatformAuthorLink =
_content.getOrDefault<V8ValueObject>(_pluginConfig, "author", CTX, null)
?.let { PlatformAuthorLink.fromV8(_pluginConfig, it) }
?: PlatformAuthorLink.UNKNOWN
constructor(config: SourcePluginConfig, obj: V8ValueObject) {
_pluginConfig = config;
_content = obj;
private val _epoch: Long? =
_content.getOrDefault<Long>(_pluginConfig, "datetime", CTX, null)?.toLong()
val contextName = "PlatformContent";
override val datetime: OffsetDateTime? =
_epoch?.takeIf { it != 0L }?.let {
OffsetDateTime.of(LocalDateTime.ofEpochSecond(it, 0, ZoneOffset.UTC), ZoneOffset.UTC)
}
id = PlatformID.fromV8(_pluginConfig, _content.getOrThrow(config, "id", contextName));
name = HtmlCompat.fromHtml(_content.getOrThrow<String>(config, "name", contextName).decodeUnicode(), HtmlCompat.FROM_HTML_MODE_LEGACY).toString();
override val url: String =
_content.getOrThrow<String>(_pluginConfig, "url", CTX)
val authorObj = _content.getOrDefault<V8ValueObject>(config, "author", contextName, null);
if(authorObj != null)
author = PlatformAuthorLink.fromV8(_pluginConfig, authorObj);
else
author = PlatformAuthorLink.UNKNOWN;
override val shareUrl: String =
_content.getOrDefault<String>(_pluginConfig, "shareUrl", CTX, null) ?: url
val datetimeInt = _content.getOrDefault<Int>(config, "datetime", contextName, null)?.toLong();
if(datetimeInt == null || 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;
override val sourceConfig: SourcePluginConfig
get() = _pluginConfig
_hasGetDetails = _content.has("getDetails");
fun getUnderlyingObject(): V8ValueObject? = _content
companion object {
private const val CTX = "PlatformContent"
}
fun getUnderlyingObject(): V8ValueObject? {
return _content;
}
}
}
@@ -6,14 +6,16 @@ import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.getOrDefault
open class JSPlaylist : JSContent, IPlatformPlaylist {
override val contentType: ContentType get() = ContentType.PLAYLIST;
override val thumbnail: String?;
override val videoCount: Int;
open class JSPlaylist(
config: SourcePluginConfig,
obj: V8ValueObject
) : JSContent(config, obj), IPlatformPlaylist {
constructor(config: SourcePluginConfig, obj: V8ValueObject) : super(config, obj) {
val contextName = "Playlist";
thumbnail = obj.getOrDefault(config, "thumbnail", contextName, null);
videoCount = obj.getOrDefault(config, "videoCount", contextName, -1)!!;
}
}
override val contentType: ContentType = ContentType.PLAYLIST
override val thumbnail: String? =
_content.getOrDefault<String>(_pluginConfig, "thumbnail", "Playlist", null)
override val videoCount: Int =
_content.getOrDefault<Int>(_pluginConfig, "videoCount", "Playlist", null)?.toInt() ?: -1
}
@@ -22,6 +22,7 @@ class JSSubtitleSource : ISubtitleSource {
override val name: String;
override val url: String?;
override val format: String?;
override val language: String?
override val hasFetch: Boolean;
constructor(config: SourcePluginConfig, v8Value: V8ValueObject) {
@@ -29,6 +30,7 @@ class JSSubtitleSource : ISubtitleSource {
val context = "JSSubtitles";
name = v8Value.getOrThrow(config, "name", context, false);
language = v8Value.getOrThrow(config, "language", context, false);
url = v8Value.getOrThrow(config, "url", context, true);
format = v8Value.getOrThrow(config, "format", context, true);
hasFetch = v8Value.has("getSubtitles");
@@ -8,43 +8,44 @@ import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
open class JSAudioUrlSource : IAudioUrlSource, JSSource {
override val name: String;
override val bitrate : Int;
override val container : String;
override val codec: String;
private val url : String;
open class JSAudioUrlSource(
plugin: JSClient,
obj: V8ValueObject
) : JSSource(TYPE_AUDIOURL, plugin, obj), IAudioUrlSource {
override val language: String;
private val ctx = "AudioUrlSource"
private val cfg = plugin.config
override val duration: Long?;
override val bitrate: Int =
_obj.getOrThrow<Int>(cfg, "bitrate", ctx)
override var priority: Boolean = false;
override val container: String =
_obj.getOrThrow<String>(cfg, "container", ctx)
override var original: Boolean = false;
override val codec: String =
_obj.getOrThrow<String>(cfg, "codec", ctx)
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_AUDIOURL, plugin, obj) {
val contextName = "AudioUrlSource";
val config = plugin.config;
private val url: String =
_obj.getOrThrow<String>(cfg, "url", ctx)
bitrate = _obj.getOrThrow(config, "bitrate", contextName);
container = _obj.getOrThrow(config, "container", contextName);
codec = _obj.getOrThrow(config, "codec", contextName);
url = _obj.getOrThrow(config, "url", contextName);
language = _obj.getOrThrow(config, "language", contextName);
duration = _obj.getOrDefault(config, "duration", contextName, null);
override val language: String =
_obj.getOrThrow<String>(cfg, "language", ctx)
name = _obj.getOrDefault(config, "name", contextName, "${container} ${bitrate}") ?: "${container} ${bitrate}";
override val duration: Long? =
_obj.getOrDefault<Long>(cfg, "duration", ctx, null)?.toLong()
priority = if(_obj.has("priority")) obj.getOrThrow(config, "priority", contextName) else false;
original = if(_obj.has("original")) obj.getOrThrow(config, "original", contextName) else false;
}
override val name: String =
_obj.getOrDefault<String>(cfg, "name", ctx, null)
?: "$container $bitrate"
override fun getAudioUrl() : String {
return url;
}
override var priority: Boolean =
if (_obj.has("priority")) _obj.getOrThrow<Boolean>(cfg, "priority", ctx) else false
override fun toString(): String {
return "(name=$name, container=$container, bitrate=$bitrate, codec=$codec, url=$url, language=$language, duration=$duration)";
}
}
override var original: Boolean =
if (_obj.has("original")) _obj.getOrThrow<Boolean>(cfg, "original", ctx) else false
override fun getAudioUrl(): String = url
override fun toString(): String =
"(name=$name, container=$container, bitrate=$bitrate, codec=$codec, url=$url, language=$language, duration=$duration)"
}
@@ -31,42 +31,52 @@ interface IJSDashManifestRawSource {
fun generateAsync(scope: CoroutineScope): Deferred<String?>;
fun generate(): String?;
}
open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSource, IStreamMetaDataSource {
override val container : String;
override val name : String;
override val width: Int;
override val height: Int;
override val codec: String;
override val bitrate: Int?;
override val duration: Long;
override val priority: Boolean;
open class JSDashManifestRawSource(
plugin: JSClient,
obj: V8ValueObject
) : JSSource(TYPE_DASH_RAW, plugin, obj), IVideoSource, IJSDashManifestRawSource, IStreamMetaDataSource {
val url: String?;
override var manifest: String?;
private val ctx = "DashRawSource"
private val cfg = plugin.config
override val hasGenerate: Boolean;
val canMerge: Boolean;
override val container: String =
_obj.getOrDefault<String>(cfg, "container", ctx, null) ?: "application/dash+xml"
override var streamMetaData: StreamMetaData? = null;
override val name: String =
_obj.getOrThrow<String>(cfg, "name", ctx)
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH_RAW, plugin, obj) {
val contextName = "DashRawSource";
val config = plugin.config;
name = _obj.getOrThrow(config, "name", contextName);
url = _obj.getOrThrow(config, "url", contextName);
container = _obj.getOrDefault<String>(config, "container", contextName, null) ?: "application/dash+xml";
manifest = _obj.getOrDefault<String>(config, "manifest", contextName, null);
width = _obj.getOrDefault(config, "width", contextName, 0) ?: 0;
height = _obj.getOrDefault(config, "height", contextName, 0) ?: 0;
codec = _obj.getOrDefault(config, "codec", contextName, "") ?: "";
bitrate = _obj.getOrDefault(config, "bitrate", contextName, 0) ?: 0;
duration = _obj.getOrDefault(config, "duration", contextName, 0) ?: 0;
priority = _obj.getOrDefault(config, "priority", contextName, false) ?: false;
canMerge = _obj.getOrDefault(config, "canMerge", contextName, false) ?: false;
hasGenerate = _obj.has("generate");
}
override val width: Int =
_obj.getOrDefault<Int>(cfg, "width", ctx, null)?.toInt() ?: 0
private var _pregenerate: V8Deferred<String?>? = null;
override val height: Int =
_obj.getOrDefault<Int>(cfg, "height", ctx, null)?.toInt() ?: 0
override val codec: String =
_obj.getOrDefault<String>(cfg, "codec", ctx, "") ?: ""
override val bitrate: Int? =
_obj.getOrDefault<Int>(cfg, "bitrate", ctx, null)?.toInt()
override val duration: Long =
_obj.getOrDefault<Long>(cfg, "duration", ctx, 0)?.toLong() ?: 0L
override val priority: Boolean =
_obj.getOrDefault<Boolean>(cfg, "priority", ctx, false) ?: false
val url: String? =
_obj.getOrDefault<String>(cfg, "url", ctx, null)
override var manifest: String? =
_obj.getOrDefault<String>(cfg, "manifest", ctx, null)
override val hasGenerate: Boolean = _obj.has("generate")
val canMerge: Boolean =
_obj.getOrDefault<Boolean>(cfg, "canMerge", ctx, false) ?: false
override var streamMetaData: StreamMetaData? = null
private var _pregenerate: V8Deferred<String?>? = null
fun pregenerateAsync(scope: CoroutineScope): V8Deferred<String?>? {
_pregenerate = generateAsync(scope);
return _pregenerate;
@@ -5,42 +5,47 @@ import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrNull
import com.futo.platformplayer.getOrThrow
open class JSVideoUrlSource : IVideoUrlSource, JSSource {
override val width : Int;
override val height : Int;
override val container : String;
override val codec: String;
override val name : String;
override val bitrate : Int;
override val duration: Long;
private val url : String;
open class JSVideoUrlSource(
plugin: JSClient,
obj: V8ValueObject
) : JSSource(TYPE_VIDEOURL, plugin, obj), IVideoUrlSource {
override var priority: Boolean = false;
private val ctx = "JSVideoUrlSource"
private val cfg = plugin.config
constructor(plugin: JSClient, obj: V8ValueObject): super(TYPE_VIDEOURL, plugin, obj) {
val contextName = "JSVideoUrlSource";
val config = plugin.config;
override val width: Int =
_obj.getOrThrow<Int>(cfg, "width", ctx)
width = _obj.getOrThrow(config, "width", contextName);
height = _obj.getOrThrow(config, "height", contextName);
container = _obj.getOrThrow(config, "container", contextName);
codec = _obj.getOrThrow(config, "codec", contextName);
name = _obj.getOrThrow(config, "name", contextName);
bitrate = _obj.getOrThrow(config, "bitrate", contextName);
duration = _obj.getOrThrow<Int>(config, "duration", contextName).toLong();
url = _obj.getOrThrow(config, "url", contextName);
override val height: Int =
_obj.getOrThrow<Int>(cfg, "height", ctx)
priority = obj.getOrNull(config, "priority", contextName) ?: false;
}
override val container: String =
_obj.getOrThrow<String>(cfg, "container", ctx)
override fun getVideoUrl() : String {
return url;
}
override val codec: String =
_obj.getOrThrow<String>(cfg, "codec", ctx)
override fun toString(): String {
return "(width=$width, height=$height, container=$container, codec=$codec, name=$name, bitrate=$bitrate, duration=$duration, url=$url)"
}
}
override val name: String =
_obj.getOrThrow<String>(cfg, "name", ctx)
override val bitrate: Int =
_obj.getOrThrow<Int>(cfg, "bitrate", ctx)
override val duration: Long =
_obj.getOrThrow<Long>(cfg, "duration", ctx)
private val url: String =
_obj.getOrThrow<String>(cfg, "url", ctx)
override var priority: Boolean =
_obj.getOrDefault<Boolean>(cfg, "priority", ctx, false) ?: false
override fun getVideoUrl(): String = url
override fun toString(): String =
"(width=$width, height=$height, container=$container, codec=$codec, name=$name, bitrate=$bitrate, duration=$duration, url=$url)"
}
@@ -33,6 +33,7 @@ import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManif
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawAudioSource
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawSource
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
import com.futo.platformplayer.awaitCancelConverted
import com.futo.platformplayer.builders.DashBuilder
import com.futo.platformplayer.models.CastingDeviceInfo
import com.futo.platformplayer.constructs.Event1
@@ -89,8 +90,7 @@ abstract class StateCasting {
abstract fun start(context: Context)
abstract fun stop()
@Throws
abstract fun deviceFromInfo(deviceInfo: CastingDeviceInfo): CastingDevice
abstract fun deviceFromInfo(deviceInfo: CastingDeviceInfo): CastingDevice?
abstract fun startUpdateTimeJob(
onTimeJobTimeChanged_s: Event1<Long>, setTime: (Long) -> Unit
): Job?
@@ -1183,7 +1183,7 @@ abstract class StateCasting {
onLoading?.invoke(true)
}
}
deferred.await()
deferred.awaitCancelConverted()
} finally {
if (castId == _castId.get()) {
withContext(Dispatchers.Main) {
@@ -1287,9 +1287,11 @@ abstract class StateCasting {
return listOf()
}
fun addRememberedDevice(deviceInfo: CastingDeviceInfo): CastingDeviceInfo {
val device = deviceFromInfo(deviceInfo);
return addRememberedDevice(device);
fun addRememberedDevice(deviceInfo: CastingDeviceInfo): CastingDeviceInfo? {
return when (val device = deviceFromInfo(deviceInfo)) {
null -> null
else -> addRememberedDevice(device)
}
}
fun addRememberedDevice(device: CastingDevice): CastingDeviceInfo {
@@ -1298,7 +1300,7 @@ abstract class StateCasting {
}
fun getRememberedCastingDevices(): List<CastingDevice> {
return _storage.getDevices().map { deviceFromInfo(it) }
return _storage.getDevices().map { deviceFromInfo(it) }.filterNotNull()
}
fun getRememberedCastingDeviceNames(): List<String> {
@@ -151,21 +151,25 @@ class StateCastingExp : StateCasting() {
setTime: (Long) -> Unit
): Job? = null
override fun deviceFromInfo(deviceInfo: CastingDeviceInfo): CastingDeviceExp {
val rsAddrs =
deviceInfo.addresses.map { org.fcast.sender_sdk.tryIpAddrFromStr(it) } // Throws!
val rsDeviceInfo = RsDeviceInfo(
name = deviceInfo.name,
protocol = when (deviceInfo.type) {
com.futo.platformplayer.casting.CastProtocolType.CHROMECAST -> ProtocolType.CHROMECAST
com.futo.platformplayer.casting.CastProtocolType.FCAST -> ProtocolType.F_CAST
else -> throw IllegalArgumentException()
},
addresses = rsAddrs,
port = deviceInfo.port.toUShort(),
)
override fun deviceFromInfo(deviceInfo: CastingDeviceInfo): CastingDeviceExp? {
try {
val rsAddrs =
deviceInfo.addresses.map { org.fcast.sender_sdk.tryIpAddrFromStr(it) }
val rsDeviceInfo = RsDeviceInfo(
name = deviceInfo.name,
protocol = when (deviceInfo.type) {
com.futo.platformplayer.casting.CastProtocolType.CHROMECAST -> ProtocolType.CHROMECAST
com.futo.platformplayer.casting.CastProtocolType.FCAST -> ProtocolType.F_CAST
else -> throw IllegalArgumentException()
},
addresses = rsAddrs,
port = deviceInfo.port.toUShort(),
)
return CastingDeviceExp(_context.createDeviceFromInfo(rsDeviceInfo))
return CastingDeviceExp(_context.createDeviceFromInfo(rsDeviceInfo))
} catch (_: Throwable) {
return null
}
}
companion object {
@@ -55,7 +55,9 @@ class StateCastingLegacy : StateCasting() {
)
)
connectDevice(deviceFromInfo(foundInfo))
if (foundInfo != null) {
connectDevice(deviceFromInfo(foundInfo))
}
}
override fun onStop() {
@@ -4,6 +4,8 @@ import android.content.Context
import com.caoccao.javet.exceptions.JavetCompilationException
import com.caoccao.javet.exceptions.JavetException
import com.caoccao.javet.exceptions.JavetExecutionException
import com.caoccao.javet.interfaces.IJavetEntityError
import com.caoccao.javet.interfaces.IJavetEntityMap
import com.caoccao.javet.interop.V8Host
import com.caoccao.javet.interop.V8Runtime
import com.caoccao.javet.values.V8Value
@@ -18,6 +20,7 @@ import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.engine.exceptions.NoInternetException
import com.futo.platformplayer.engine.exceptions.PluginEngineStoppedException
import com.futo.platformplayer.engine.exceptions.PluginException
import com.futo.platformplayer.engine.exceptions.ScriptAgeException
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
import com.futo.platformplayer.engine.exceptions.ScriptCompilationException
@@ -36,6 +39,7 @@ import com.futo.platformplayer.engine.packages.PackageHttp
import com.futo.platformplayer.engine.packages.PackageJSDOM
import com.futo.platformplayer.engine.packages.PackageUtilities
import com.futo.platformplayer.engine.packages.V8Package
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateAssets
@@ -407,6 +411,12 @@ class V8Plugin {
return _runtimeMap.getOrDefault(runtime, null);
}
private fun ctxString(ctx: Any?, key: String): String? = when (ctx) {
is Map<*, *> -> ctx[key]?.toString()
is V8ValueObject -> if (ctx.has(key)) ctx.getString(key) else null
else -> null
}
fun <T: Any?> catchScriptErrors(config: IV8PluginConfig, context: String, code: String? = null, handle: ()->T): T {
var codeStripped = code;
if(codeStripped != null) { //TODO: Improve code stripped
@@ -440,37 +450,6 @@ class V8Plugin {
throw ScriptCompilationException(config, "Compilation: [${context}]: ${scriptEx.message}\n(${scriptEx.scriptingError.lineNumber})[${scriptEx.scriptingError.startColumn}-${scriptEx.scriptingError.endColumn}]: ${scriptEx.scriptingError.sourceLine}", null, codeStripped);
}
catch(executeEx: JavetExecutionException) {
val obj = executeEx.scriptingError?.context
if(obj != null && obj.containsKey("plugin_type") == true) {
val pluginType = obj["plugin_type"].toString();
//Captcha
if (pluginType == "CaptchaRequiredException") {
throw ScriptCaptchaRequiredException(config,
obj["url"]?.toString(),
obj["body"]?.toString(),
executeEx, executeEx.scriptingError?.stack, codeStripped);
}
//Reload Required
if (pluginType == "ReloadRequiredException") {
throw ScriptReloadRequiredException(config,
obj["msg"]?.toString(),
obj["reloadData"]?.toString(),
executeEx, executeEx.scriptingError?.stack, codeStripped);
}
//Others
throwExceptionFromV8(
config,
pluginType,
(extractJSExceptionMessage(executeEx) ?: ""),
executeEx,
executeEx.scriptingError?.stack,
codeStripped
);
}
/* //Required for newer V8 versions
if(executeEx.scriptingError?.context is IJavetEntityError) {
val obj = executeEx.scriptingError?.context as IJavetEntityError
if(obj.context.containsKey("plugin_type") == true) {
@@ -504,7 +483,6 @@ class V8Plugin {
}
}
*/
throw ScriptExecutionException(config, extractJSExceptionMessage(executeEx) ?: "", null, executeEx.scriptingError?.stack, codeStripped);
}
catch(ex: Exception) {
@@ -513,18 +491,29 @@ class V8Plugin {
}
private fun throwExceptionFromV8(config: IV8PluginConfig, pluginType: String, msg: String, innerEx: Exception? = null, stack: String? = null, code: String? = null) {
throw getExceptionFromPlugin(config, pluginType, msg, innerEx, stack, code);
}
fun getExceptionFromPlugin(config: IV8PluginConfig, obj: V8ValueObject, innerEx: Exception? = null, stack: String? = null, code: String? = null, prefix: String? = null): PluginException {
val pluginType = obj.getOrDefault(config, "plugin_type", "Exception Handling", "")?.let { if(!it.isNullOrBlank()) it + "" else "" } ?: "";
var msg = obj.getOrDefault<String?>(config, "msg", "Exception Handling", null)
?: obj.getOrDefault(config, "message", "Exception Handling", "");
if(!prefix.isNullOrBlank())
msg = prefix + msg;
return getExceptionFromPlugin(config, pluginType, msg ?: "Unknown exception", innerEx, stack, code);
}
fun getExceptionFromPlugin(config: IV8PluginConfig, pluginType: String, msg: String, innerEx: Exception? = null, stack: String? = null, code: String? = null): PluginException {
when(pluginType) {
"ScriptException" -> throw ScriptException(config, msg, innerEx, stack, code);
"CriticalException" -> throw ScriptCriticalException(config, msg, innerEx, stack, code);
"AgeException" -> throw ScriptAgeException(config, msg, innerEx, stack, code);
"UnavailableException" -> throw ScriptUnavailableException(config, msg, innerEx, stack, code);
"ScriptLoginRequiredException" -> throw ScriptLoginRequiredException(config, msg, innerEx, stack, code);
"ScriptExecutionException" -> throw ScriptExecutionException(config, msg, innerEx, stack, code);
"ScriptCompilationException" -> throw ScriptCompilationException(config, msg, innerEx, code);
"ScriptImplementationException" -> throw ScriptImplementationException(config, msg, innerEx, null, code);
"ScriptTimeoutException" -> throw ScriptTimeoutException(config, msg, innerEx);
"NoInternetException" -> throw NoInternetException(config, msg, innerEx, stack, code);
else -> throw ScriptExecutionException(config, msg, innerEx, stack, code);
"ScriptException" -> return ScriptException(config, msg, innerEx, stack, code);
"CriticalException" -> return ScriptCriticalException(config, msg, innerEx, stack, code);
"AgeException" -> return ScriptAgeException(config, msg, innerEx, stack, code);
"UnavailableException" -> return ScriptUnavailableException(config, msg, innerEx, stack, code);
"ScriptLoginRequiredException" -> return ScriptLoginRequiredException(config, msg, innerEx, stack, code);
"ScriptExecutionException" -> return ScriptExecutionException(config, msg, innerEx, stack, code);
"ScriptCompilationException" -> return ScriptCompilationException(config, msg, innerEx, code);
"ScriptImplementationException" -> return ScriptImplementationException(config, msg, innerEx, null, code);
"ScriptTimeoutException" -> return ScriptTimeoutException(config, msg, innerEx);
"NoInternetException" -> return NoInternetException(config, msg, innerEx, stack, code);
else -> return ScriptExecutionException(config, msg, innerEx, stack, code);
}
}
@@ -254,7 +254,7 @@ class PackageHttp: V8Package {
//TODO: This object is currently re-wrapped each modification, this is due to an issue passing the same object back and forth, should be fixed in future.
@V8Convert(mode = V8ConversionMode.AllowOnly, proxyMode = V8ProxyMode.Class)
class BatchBuilder(private val _package: PackageHttp, existingRequests: MutableList<Pair<PackageHttpClient, RequestDescriptor>> = mutableListOf()): V8BindObject() {
class BatchBuilder(@Transient private val _package: PackageHttp, existingRequests: MutableList<Pair<PackageHttpClient, RequestDescriptor>> = mutableListOf()): V8BindObject() {
@Transient
private val _reqs = existingRequests;
@@ -5,6 +5,7 @@ import android.content.Intent
import android.graphics.drawable.Animatable
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.WindowManager
import android.view.animation.AccelerateInterpolator
import android.view.animation.OvershootInterpolator
import android.widget.FrameLayout
@@ -214,6 +215,16 @@ class ShortView : FrameLayout {
}
}
player.onPlayChanged.subscribe {
if (it) {
Logger.i(TAG, "Keep screen on set because isPlaying")
fragment.activity?.window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
} else {
Logger.i(TAG, "Keep screen on cleared because not isPlaying")
fragment.activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
}
onPlayingToggled.subscribe { playing ->
if (playing) {
playPauseIcon.setImageResource(R.drawable.ic_play)
@@ -7,6 +7,7 @@ import android.view.LayoutInflater
import android.view.SoundEffectConstants
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout
@@ -309,6 +310,12 @@ class ShortsFragment : MainFragment() {
customViewAdapter?.previousShownView?.stop()
}
override fun onDestroyMainView() {
super.onDestroyMainView()
Logger.i(TAG, "Keep screen on cleared because onDestroyMainView fragment")
activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
companion object {
private const val TAG = "ShortsFragment"
@@ -244,6 +244,7 @@ class VideoDetailView : ConstraintLayout {
private val _buttonSubscribe: SubscribeButton;
private val _buttonPins: RoundButtonGroup;
private var _loaderGameVisible = false
//private val _buttonMore: RoundButton;
var preventPictureInPicture: Boolean = false
@@ -261,7 +262,6 @@ class VideoDetailView : ConstraintLayout {
private val _textSkip: TextView;
private val _textResume: TextView;
private val _layoutResume: LinearLayout;
private var _jobHideResume: Job? = null;
private val _layoutPlayerContainer: TouchInterceptFrameLayout;
private val _layoutChangeBottomSection: LinearLayout;
@@ -336,7 +336,7 @@ class VideoDetailView : ConstraintLayout {
!StateCasting.instance.isCasting &&
Settings.instance.playback.isBackgroundPictureInPicture() &&
!isAudioOnlyUserAction &&
isPlaying
(isPlaying || _loaderGameVisible)
val onShouldEnterPictureInPictureChanged = Event0();
@@ -357,6 +357,7 @@ class VideoDetailView : ConstraintLayout {
Pair(-5 * 60, 30), //around 5 minutes, try every 30 seconds
Pair(0, 10) //around live, try every 10 seconds
);
private var _subtitleLanguage: String? = null
@androidx.annotation.OptIn(UnstableApi::class)
constructor(context: Context, attrs : AttributeSet? = null) : super(context, attrs) {
@@ -548,6 +549,16 @@ class VideoDetailView : ConstraintLayout {
_buttonMore = buttonMore;
updateMoreButtons();
val handleLoaderGameVisibilityChanged = { b: Boolean ->
_loaderGameVisible = b
fragment.lifecycleScope.launch(Dispatchers.Main) {
onShouldEnterPictureInPictureChanged.emit()
}
updateResumeVisibilityFor(lastPositionMilliseconds)
}
_player.loaderGameVisibilityChanged.subscribe(handleLoaderGameVisibilityChanged)
_cast.loaderGameVisibilityChanged.subscribe(handleLoaderGameVisibilityChanged)
_channelButton.setOnClickListener {
if (video is TutorialFragment.TutorialVideo) {
return@setOnClickListener
@@ -872,11 +883,6 @@ class VideoDetailView : ConstraintLayout {
_layoutResume.setOnClickListener {
handleSeek(_historicalPosition * 1000);
val job = _jobHideResume;
_jobHideResume = null;
job?.cancel();
_layoutResume.visibility = View.GONE;
};
@@ -1255,10 +1261,6 @@ class VideoDetailView : ConstraintLayout {
MediaControlReceiver.onCloseReceived.remove(this);
MediaControlReceiver.onBackgroundReceived.remove(this);
MediaControlReceiver.onSeekToReceived.remove(this);
val job = _jobHideResume;
_jobHideResume = null;
job?.cancel();
}
//Video Setters
@@ -1780,26 +1782,7 @@ class VideoDetailView : ConstraintLayout {
TAG,
"Historical position: $_historicalPosition, last position: $lastPositionMilliseconds"
);
if (_historicalPosition > 60 && video.duration - _historicalPosition > 5 && Math.abs(
_historicalPosition - lastPositionMilliseconds / 1000
) > 5.0
) {
_layoutResume.visibility = View.VISIBLE;
_textResume.text = "Resume at ${_historicalPosition.toHumanTime(false)}";
_jobHideResume = fragment.lifecycleScope.launch(Dispatchers.Main) {
try {
delay(8000);
_layoutResume.visibility = View.GONE;
_textResume.text = "";
} catch (e: Throwable) {
Logger.e(TAG, "Failed to set resume changes.", e);
}
}
} else {
_layoutResume.visibility = View.GONE;
_textResume.text = "";
}
updateResumeVisibilityFor(lastPositionMilliseconds)
}
}
}
@@ -1845,6 +1828,35 @@ class VideoDetailView : ConstraintLayout {
_taskLoadRecommendations.run(videoDetail.url)
}
}
private fun shouldShowResume(positionMs: Long): Boolean {
if (_loaderGameVisible) return false
val v = video ?: return false
val resumeS = _historicalPosition
val durS = v.duration
if (_overlay_loading.visibility == View.VISIBLE) return false
if (resumeS <= 60) return false
if (durS - resumeS <= 5) return false
val posMs = positionMs
val resumeMs = resumeS * 1000
val durMs = durS * 1000L
val inFirstFewSeconds = posMs < 8_000
val notYetReachedResume = (resumeMs - posMs) > 5_000
return inFirstFewSeconds && notYetReachedResume && durMs > 0
}
private fun updateResumeVisibilityFor(positionMs: Long) {
val visible = shouldShowResume(positionMs)
if (visible) {
_layoutResume.visibility = View.VISIBLE
_textResume.text = "Resume at ${_historicalPosition.toHumanTime(false)}"
} else {
_layoutResume.visibility = View.GONE
_textResume.text = ""
}
}
fun loadVODChat(video: IPlatformVideoDetails) {
_liveChat?.stop();
_container_content_liveChat.cancel();
@@ -1964,7 +1976,7 @@ class VideoDetailView : ConstraintLayout {
try {
val videoSource = _lastVideoSource ?: _player.getPreferredVideoSource(video, Settings.instance.playback.getCurrentPreferredQualityPixelCount());
val audioSource = _lastAudioSource ?: _player.getPreferredAudioSource(video, Settings.instance.playback.getPrimaryLanguage(context));
val subtitleSource = _lastSubtitleSource ?: (if(video is VideoLocal) video.subtitlesSources.firstOrNull() else null);
val subtitleSource = _lastSubtitleSource ?: (if (Settings.instance.playback.stickySubtitles) _player.getPreferredSubtitleSource(video, _subtitleLanguage) else null) ?: (if(video is VideoLocal) video.subtitlesSources.firstOrNull() else null);
Logger.i(TAG, "loadCurrentVideo(videoSource=$videoSource, audioSource=$audioSource, subtitleSource=$subtitleSource, resumePositionMs=$resumePositionMs)")
if(videoSource == null && audioSource == null) {
@@ -2648,6 +2660,7 @@ class VideoDetailView : ConstraintLayout {
}
}
_lastSubtitleSource = toSet;
_subtitleLanguage = toSet?.language
}
private fun handleUnavailableVideo(msg: String? = null) {
@@ -2805,6 +2818,8 @@ class VideoDetailView : ConstraintLayout {
_overlay_loading.visibility = View.GONE;
(_overlay_loading_spinner.drawable as Animatable?)?.stop()
}
updateResumeVisibilityFor(lastPositionMilliseconds)
}
//UI Actions
@@ -3038,9 +3053,9 @@ class VideoDetailView : ConstraintLayout {
}
val playpauseAction = if(_player.playing)
RemoteAction(Icon.createWithResource(context, R.drawable.ic_pause_notif), context.getString(R.string.pause), context.getString(R.string.pauses_the_video), MediaControlReceiver.getPauseIntent(context, 5));
RemoteAction(Icon.createWithResource(context, R.drawable.ic_pause_notif), context.getString(R.string.pause), context.getString(R.string.pauses_the_video), MediaControlReceiver.getPauseIntent(context, 2));
else
RemoteAction(Icon.createWithResource(context, R.drawable.ic_play_notif), context.getString(R.string.play), context.getString(R.string.resumes_the_video), MediaControlReceiver.getPlayIntent(context, 6));
RemoteAction(Icon.createWithResource(context, R.drawable.ic_play_notif), context.getString(R.string.play), context.getString(R.string.resumes_the_video), MediaControlReceiver.getPlayIntent(context, 1));
val toBackgroundAction = RemoteAction(Icon.createWithResource(context, R.drawable.ic_screen_share), context.getString(R.string.background), context.getString(R.string.background_switch_audio), MediaControlReceiver.getToBackgroundIntent(context, 7));
@@ -3095,6 +3110,8 @@ class VideoDetailView : ConstraintLayout {
handleSeek(55000);
}
}
updateResumeVisibilityFor(positionMilliseconds)
}
private fun updateTracker(positionMs: Long, isPlaying: Boolean, forceUpdate: Boolean = false) {
@@ -19,6 +19,8 @@ import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSour
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
import com.futo.platformplayer.api.media.models.streams.sources.IWidevineSource
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSAudioUrlRangeSource
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawAudioSource
@@ -134,6 +136,62 @@ class VideoHelper {
return bestSource;
}
fun selectBestSubtitleSource(sources: Iterable<ISubtitleSource>, preferredLanguage: String?): ISubtitleSource? {
if (preferredLanguage.isNullOrBlank()) return null
val prefTag = normalizeTag(preferredLanguage)
val prefPrimary = primarySubtag(prefTag) ?: return null
var best: ISubtitleSource? = null
var bestKey: Quad<Int, Int, String, String>? = null
for (src in sources) {
val raw = src.language ?: continue
val tag = normalizeTag(raw)
val primary = primarySubtag(tag) ?: continue
val score = when {
tag.equals(prefTag, ignoreCase = true) -> 0
primary.equals(prefPrimary, ignoreCase = true) && findRegion(tag) == null -> 1
primary.equals(prefPrimary, ignoreCase = true) -> 2
else -> 3
}
if (score >= 3) continue
val key = Quad(score, src.name.length, tag.lowercase(), src.name)
if (bestKey == null || key < bestKey!!) {
bestKey = key
best = src
}
}
return best
}
private fun normalizeTag(tag: String): String = tag.trim().replace('_', '-')
private fun primarySubtag(tag: String): String? = tag.split('-').firstOrNull { it.isNotBlank() }?.lowercase()
private fun findRegion(tag: String): String? {
val parts = tag.split('-').drop(1) // skip primary language
for (p in parts) {
val isAlpha2 = p.length == 2 && p[0].isLetter() && p[1].isLetter()
val isNumeric3 = p.length == 3 && p.all { it.isDigit() }
if (isAlpha2 || isNumeric3) return p.uppercase()
}
return null
}
private data class Quad<A : Comparable<A>, B : Comparable<B>, C : Comparable<C>, D : Comparable<D>>(
val a: A, val b: B, val c: C, val d: D
) : Comparable<Quad<A, B, C, D>> {
override fun compareTo(other: Quad<A, B, C, D>): Int =
when {
a != other.a -> a.compareTo(other.a)
b != other.b -> b.compareTo(other.b)
c != other.c -> c.compareTo(other.c)
else -> d.compareTo(other.d)
}
}
@OptIn(UnstableApi::class)
fun convertItagSourceToChunkedDashSource(videoSource: JSVideoUrlRangeSource) : Pair<MediaSource, String> {
val urlToUse = videoSource.getVideoUrl();
@@ -0,0 +1,80 @@
package com.futo.platformplayer.polycentric
import android.content.Context
import android.content.SharedPreferences
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import org.json.JSONObject
class ModerationsManager private constructor(context: Context) {
private val prefs: SharedPreferences = context.getSharedPreferences("polycentric_moderation", Context.MODE_PRIVATE)
private val _moderationLevels = MutableLiveData<Map<String, Int>>()
val moderationLevels: LiveData<Map<String, Int>> = _moderationLevels
init {
loadModerationLevels()
}
private fun loadModerationLevels() {
val levels = mutableMapOf<String, Int>()
levels["hate"] = prefs.getInt("offensive_level", 2)
levels["sexual"] = prefs.getInt("explicit_level", 1)
levels["violence"] = prefs.getInt("violence_level", 1)
_moderationLevels.value = levels
}
fun setModerationLevel(category: String, level: Int) {
when (category) {
"hate" -> prefs.edit().putInt("offensive_level", level).apply()
"sexual" -> prefs.edit().putInt("explicit_level", level).apply()
"violence" -> prefs.edit().putInt("violence_level", level).apply()
}
val currentMap = _moderationLevels.value?.toMutableMap() ?: mutableMapOf()
currentMap[category] = level
_moderationLevels.value = currentMap
}
fun getModerationLevelsJson(): String {
val json = JSONObject()
moderationLevels.value?.forEach { (key, value) ->
json.put(key, value)
}
return json.toString()
}
fun shouldFilter(category: String, contentLevel: Int): Boolean {
val userLevel = when (category) {
"hate" -> prefs.getInt("offensive_level", 2)
"sexual" -> prefs.getInt("explicit_level", 1)
"violence" -> prefs.getInt("violence_level", 1)
else -> 3
}
return contentLevel > userLevel
}
fun getCurrentModerationLevels(): Map<String, Int>? {
return moderationLevels.value
}
companion object {
@Volatile
private var instance: ModerationsManager? = null
fun initialize(context: Context) {
if (instance == null) {
synchronized(this) {
if (instance == null) {
instance = ModerationsManager(context.applicationContext)
}
}
}
}
fun getInstance(): ModerationsManager {
return instance ?: throw IllegalStateException("ModerationsManager not initialized")
}
}
}
@@ -66,10 +66,9 @@ class DownloadService : Service() {
return START_NOT_STICKY;
if(!FragmentedStorage.isInitialized) {
Logger.i(TAG, "Attempted to start DownloadService without initialized files");
stopSelf()
closeDownloadSession();
return START_NOT_STICKY;
Logger.i(TAG, "Attempted to start DownloadService without initialized files")
closeDownloadSession()
return START_NOT_STICKY
}
_started = true;
}
@@ -107,12 +106,19 @@ class DownloadService : Service() {
return START_STICKY;
}
fun setupNotificationRequirements() {
_notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
_notificationChannel = NotificationChannel(DOWNLOAD_NOTIF_CHANNEL_ID, DOWNLOAD_NOTIF_CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT).apply {
this.enableVibration(false);
this.setSound(null, null);
};
_notificationManager!!.createNotificationChannel(_notificationChannel!!);
_notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (_notificationChannel == null) {
_notificationChannel = NotificationChannel(
DOWNLOAD_NOTIF_CHANNEL_ID,
DOWNLOAD_NOTIF_CHANNEL_NAME,
NotificationManager.IMPORTANCE_LOW
).apply {
enableVibration(false)
setSound(null, null)
setShowBadge(false)
}
}
_notificationManager?.createNotificationChannel(_notificationChannel!!)
}
override fun onCreate() {
@@ -293,21 +299,28 @@ class DownloadService : Service() {
val notif = builder.build();
notif.flags = notif.flags or NotificationCompat.FLAG_ONGOING_EVENT or NotificationCompat.FLAG_NO_CLEAR;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(DOWNLOAD_NOTIF_ID, notif, FOREGROUND_SERVICE_TYPE_DATA_SYNC);
if (_isForeground) {
_notificationManager?.notify(DOWNLOAD_NOTIF_ID, notif)
} else {
startForeground(DOWNLOAD_NOTIF_ID, notif);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
startForeground(DOWNLOAD_NOTIF_ID, notif, FOREGROUND_SERVICE_TYPE_DATA_SYNC)
else
startForeground(DOWNLOAD_NOTIF_ID, notif)
_isForeground = true
}
}
fun closeDownloadSession() {
Logger.i(TAG, "closeDownloadSession");
stopForeground(STOP_FOREGROUND_REMOVE);
_notificationManager?.cancel(DOWNLOAD_NOTIF_ID);
stopService();
_started = false;
super.stopSelf();
Logger.i(TAG, "closeDownloadSession")
if (_isForeground) {
stopForeground(STOP_FOREGROUND_REMOVE)
_isForeground = false
}
_notificationManager?.cancel(DOWNLOAD_NOTIF_ID)
_started = false
super.stopSelf()
}
override fun onDestroy() {
Logger.i(TAG, "onDestroy");
_instance = null;
@@ -49,6 +49,8 @@ import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.v2.ManagedStore
import com.futo.platformplayer.views.ToastView
import com.futo.polycentric.core.ApiMethods
import com.futo.polycentric.core.toBase64Url
import com.futo.platformplayer.polycentric.ModerationsManager
import kotlinx.coroutines.*
import java.io.File
import java.util.*
@@ -385,6 +387,29 @@ class StateApp {
_cacheDirectory?.let { ApiMethods.initCache(it) };
}
Logger.i(TAG, "MainApp Starting: Initializing [ModerationsManager]");
ModerationsManager.initialize(context);
Logger.i(TAG, "MainApp Starting: Setting [ModerationLevelProvider]");
ApiMethods.setModerationLevelProvider {
try {
ModerationsManager.getInstance().getCurrentModerationLevels()
} catch (e: IllegalStateException) {
Logger.e(TAG, "Failed to get moderation levels from manager", e);
null
}
}
Logger.i(TAG, "MainApp Starting: Setting [ModerationExemptSystemProvider]");
ApiMethods.setModerationExemptSystemProvider {
try {
StatePolycentric.instance.processHandle?.system?.toProto()?.toByteArray()?.toBase64Url()
} catch (e: Throwable) {
Logger.e(TAG, "Failed to get moderation exempt system from manager", e);
null
}
}
val logFile = File(context.filesDir, "log.txt");
if (Settings.instance.logging.logLevel > LogLevel.NONE.value) {
val fileLogConsumer = FileLogConsumer(logFile, LogLevel.fromInt(Settings.instance.logging.logLevel), false);
@@ -439,7 +439,7 @@ class StateDownloads {
} else {
throw NotImplementedError("Unsuported scheme");
}
return if (subtitles != null) SubtitleRawSource(subtitle.name, subtitle.format, subtitles!!) else null;
return if (subtitles != null) SubtitleRawSource(subtitle.name, subtitle.language, subtitle.format, subtitles!!) else null;
}
fun cleanupDownloads(): Pair<Int, Long> {
@@ -28,6 +28,7 @@ import com.futo.platformplayer.selectBestImage
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.StringStorage
import com.futo.polycentric.core.ApiMethods
import com.futo.polycentric.core.ensureServerAndBackfill
import com.futo.polycentric.core.ClaimType
import com.futo.polycentric.core.ContentType
import com.futo.polycentric.core.Opinion
@@ -46,8 +47,10 @@ import com.google.protobuf.ByteString
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import userpackage.Protocol
import userpackage.Protocol.Reference
@@ -67,6 +70,8 @@ class StatePolycentric {
private val _commentPool = ForkJoinPool(2);
private val _commentPoolDispatcher = _commentPool.asCoroutineDispatcher();
private val _backgroundJob = SupervisorJob()
private val _backgroundScope = CoroutineScope(_backgroundJob + Dispatchers.IO)
fun load(context: Context) {
if (!enabled) {
@@ -173,6 +178,15 @@ class StatePolycentric {
}
_likeDislikeMap = newMap
// Ensure current server is registered & synced
_backgroundScope.launch {
try {
processHandle.ensureServerAndBackfill()
} catch (e: Throwable) {
Logger.w(TAG, "Failed to ensure server and backfill: "+e.message)
}
}
} else {
_activeProcessHandle.setAndSave("");
_likeDislikeMap = hashMapOf()
@@ -559,6 +573,11 @@ class StatePolycentric {
};
}
fun cleanup() {
_backgroundJob.cancel()
_commentPool.shutdown()
}
companion object {
private const val TAG = "StatePolycentric";
@@ -6,6 +6,7 @@ import android.net.nsd.NsdServiceInfo
import android.os.Build
import android.util.Log
import com.futo.platformplayer.Settings
import com.futo.platformplayer.ensureNotMainThread
import com.futo.platformplayer.generateReadablePassword
import com.futo.platformplayer.getConnectedSocket
import com.futo.platformplayer.logging.Logger
@@ -17,14 +18,23 @@ import com.futo.polycentric.core.base64UrlToByteArray
import com.futo.polycentric.core.toBase64
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.selects.select
import kotlinx.coroutines.withContext
import java.net.InetAddress
import java.net.InetSocketAddress
import java.net.ServerSocket
import java.net.Socket
import java.nio.ByteBuffer
import java.nio.channels.ClosedChannelException
import java.nio.channels.ServerSocketChannel
import java.nio.channels.SocketChannel
import java.util.Base64
import java.util.Locale
import kotlin.math.min
@@ -64,11 +74,7 @@ class SyncService(
private val database: ISyncDatabaseProvider,
private val settings: SyncServiceSettings = SyncServiceSettings()
) {
private var _serverSocket: ServerSocket? = null
private var _thread: Thread? = null
private var _connectThread: Thread? = null
private var _mdnsThread: Thread? = null
@Volatile private var _started = false
private var _serverSocket: ServerSocketChannel? = null
private val _sessions: MutableMap<String, SyncSession> = mutableMapOf()
private val _lastConnectTimesMdns: MutableMap<String, Long> = mutableMapOf()
private val _lastConnectTimesIp: MutableMap<String, Long> = mutableMapOf()
@@ -82,10 +88,10 @@ class SyncService(
private val _pairingCode: String? = generateReadablePassword(8)
val pairingCode: String? get() = _pairingCode
private var _relaySession: SyncSocketSession? = null
private var _threadRelay: Thread? = null
private val _remotePendingStatusUpdate = mutableMapOf<String, (complete: Boolean?, message: String) -> Unit>()
private val _remotePendingStatusUpdateRelayed = mutableMapOf<String, (complete: Boolean?, message: String) -> Unit>()
private val _remotePendingStatusUpdateDirect = mutableMapOf<String, (complete: Boolean?, message: String) -> Unit>()
private var _nsdManager: NsdManager? = null
private var _scope: CoroutineScope? = null
@Volatile private var _scope: CoroutineScope? = null
private val _mdnsCache = mutableMapOf<String, SyncDeviceInfo>()
private var _discoveryListener: NsdManager.DiscoveryListener = object : NsdManager.DiscoveryListener {
override fun onDiscoveryStarted(regType: String) {
@@ -216,11 +222,12 @@ class SyncService(
var authorizePrompt: ((String, (Boolean) -> Unit) -> Unit)? = null
fun start(context: Context) {
if (_started) {
Logger.i(TAG, "Already started.")
if (_scope != null) {
Log.i(TAG, "Already started.")
return
}
_started = true
Log.i(TAG, "Start SyncService.")
_scope = CoroutineScope(Dispatchers.IO)
try {
@@ -294,27 +301,30 @@ class SyncService(
private fun startListener() {
serverSocketFailedToStart = false
serverSocketStarted = false
_thread = Thread {
_scope?.launch(Dispatchers.IO) {
try {
val serverSocket = ServerSocket(settings.listenerPort)
val serverSocket = ServerSocketChannel.open()
serverSocket.socket().bind(InetSocketAddress("0.0.0.0", settings.listenerPort))
_serverSocket = serverSocket
serverSocketStarted = true
Log.i(TAG, "Running on port ${settings.listenerPort} (TCP)")
serverSocketStarted = true
while (_started) {
while (isActive) {
val socket = serverSocket.accept()
val session = createSocketSession(socket, true)
//TODO: Switch to SocketChannel?
val session = createSocketSession(socket.socket(), true)
session.startAsResponder()
}
serverSocketStarted = false
} catch (e: ClosedChannelException) {
// normal shutdown
} catch (e: Throwable) {
Logger.e(TAG, "Failed to bind server socket to port ${settings.listenerPort}", e)
Log.e(TAG, "Failed to bind server socket to port ${settings.listenerPort}", e)
serverSocketFailedToStart = true
} finally {
serverSocketStarted = false
}
}.apply { start() }
}
}
private fun startMdnsRetryLoop() {
@@ -322,43 +332,44 @@ class SyncService(
discoverServices(serviceName, NsdManager.PROTOCOL_DNS_SD, _discoveryListener)
}
_mdnsThread = Thread {
while (_started) {
_scope?.launch(Dispatchers.IO) {
while (isActive) {
try {
val now = System.currentTimeMillis()
synchronized(_mdnsCache) {
for ((pkey, info) in _mdnsCache) {
if (!database.isAuthorized(pkey) || getLinkType(pkey) == LinkType.Direct) continue
val pairs = synchronized (_mdnsCache) { _mdnsCache.toList() }
for ((pkey, info) in pairs) {
if (!database.isAuthorized(pkey) || getLinkType(pkey) == LinkType.Direct) continue
val last = synchronized(_lastConnectTimesMdns) {
_lastConnectTimesMdns[pkey] ?: 0L
}
if (now - last > 30_000L) {
val last = synchronized(_lastConnectTimesMdns) {
_lastConnectTimesMdns[pkey] ?: 0L
}
if (now - last > 30_000L) {
synchronized(_lastConnectTimesMdns) {
_lastConnectTimesMdns[pkey] = now
try {
Logger.i(TAG, "MDNS-retry: connecting to $pkey")
connect(info)
} catch (ex: Throwable) {
Logger.w(TAG, "MDNS retry failed for $pkey", ex)
}
}
try {
Log.i(TAG, "MDNS-retry: connecting to $pkey")
connect(info)
if (!isActive) break
} catch (ex: Throwable) {
Log.w(TAG, "MDNS retry failed for $pkey", ex)
}
}
}
} catch (ex: Throwable) {
Logger.e(TAG, "Error in MDNS retry loop", ex)
Log.e(TAG, "Error in MDNS retry loop", ex)
}
Thread.sleep(5000)
delay(5000)
}
}.apply { start() }
}
}
private fun startConnectLastLoop() {
_connectThread = Thread {
_scope?.launch(Dispatchers.IO) {
Log.i(TAG, "Running auto reconnector")
while (_started) {
val authorizedDevices = database.getAllAuthorizedDevices() ?: arrayOf()
while (isActive) {
val authorizedDevices = database.getAllAuthorizedDevices()?.toList() ?: listOf()
val addressesToConnect = authorizedDevices.mapNotNull {
val connectedDirectly = getLinkType(it) == LinkType.Direct
if (connectedDirectly) {
@@ -382,26 +393,26 @@ class SyncService(
_lastConnectTimesIp[connectPair.first] = now
}
Logger.i(TAG, "Attempting to connect to authorized device by last known IP '${connectPair.first}' with pkey=${connectPair.first}")
Log.i(TAG, "Attempting to connect to authorized device by last known IP '${connectPair.first}' with pkey=${connectPair.first}")
connect(arrayOf(connectPair.second), settings.listenerPort, connectPair.first, null)
}
} catch (e: Throwable) {
Logger.i(TAG, "Failed to connect to " + connectPair.first, e)
Log.i(TAG, "Failed to connect to " + connectPair.first, e)
}
}
Thread.sleep(5000)
delay(5000)
}
}.apply { start() }
}
}
private fun startRelayLoop() {
relayConnected = false
_threadRelay = Thread {
_scope?.launch(Dispatchers.IO) {
try {
var backoffs: Array<Long> = arrayOf(1000, 5000, 10000, 20000)
var backoffIndex = 0;
while (_started) {
while (isActive) {
try {
Log.i(TAG, "Starting relay session...")
relayConnected = false
@@ -465,7 +476,7 @@ class SyncService(
Thread {
try {
while (_started && !socketClosed) {
while (isActive && !socketClosed) {
val unconnectedAuthorizedDevices =
database.getAllAuthorizedDevices()
?.filter {
@@ -503,27 +514,14 @@ class SyncService(
connectionInfo.ipv4Addresses
.filter { it != connectionInfo.remoteIp }
if (getLinkType(targetKey) != LinkType.Direct && connectionInfo.allowLocalDirect && Settings.instance.synchronization.connectLocalDirectThroughRelay) {
Thread {
launch(Dispatchers.IO) {
try {
Log.v(
TAG,
"Attempting to connect directly, locally to '$targetKey'."
)
connect(
potentialLocalAddresses.map { it }
.toTypedArray(),
settings.listenerPort,
targetKey,
null
)
Log.v(TAG, "Attempting to connect directly, locally to '$targetKey'.")
connect(potentialLocalAddresses.map { it }.toTypedArray(), settings.listenerPort, targetKey, null)
} catch (e: Throwable) {
Log.e(
TAG,
"Failed to start direct connection using connection info with $targetKey.",
e
)
Log.e(TAG, "Failed to start direct connection using connection info with $targetKey.", e)
}
}.start()
}
}
if (connectionInfo.allowRemoteDirect) {
@@ -587,7 +585,7 @@ class SyncService(
} catch (ex: Throwable) {
Log.i(TAG, "Unhandled exception in relay loop.", ex)
}
}.apply { start() }
}
}
private fun createSocketSession(socket: Socket, isResponder: Boolean): SyncSocketSession {
@@ -699,14 +697,21 @@ class SyncService(
return _pairingCode == pairingCode
}
private fun sendRemotePendingStatusUpdate(remotePublicKey: String, complete: Boolean, message: String) {
synchronized(_remotePendingStatusUpdateDirect) {
_remotePendingStatusUpdateDirect.remove(remotePublicKey)?.invoke(complete, message)
}
synchronized(_remotePendingStatusUpdateRelayed) {
_remotePendingStatusUpdateRelayed.remove(remotePublicKey)?.invoke(complete, message)
}
}
private fun createNewSyncSession(rpk: String, remoteDeviceName: String?): SyncSession {
val remotePublicKey = rpk.base64ToByteArray().toBase64()
return SyncSession(
remotePublicKey,
onAuthorized = { it, isNewlyAuthorized, isNewSession ->
synchronized(_remotePendingStatusUpdate) {
_remotePendingStatusUpdate.remove(remotePublicKey)?.invoke(true, "Authorized")
}
sendRemotePendingStatusUpdate(remotePublicKey, true, "Authorized")
if (isNewSession) {
it.remoteDeviceName?.let { remoteDeviceName ->
@@ -719,10 +724,7 @@ class SyncService(
onAuthorized?.invoke(it, isNewlyAuthorized, isNewSession)
},
onUnauthorized = {
synchronized(_remotePendingStatusUpdate) {
_remotePendingStatusUpdate.remove(remotePublicKey)?.invoke(false, "Unauthorized")
}
sendRemotePendingStatusUpdate(remotePublicKey, false, "Unauthorized")
onUnauthorized?.invoke(it)
},
onConnectedChanged = { it, connected ->
@@ -733,9 +735,7 @@ class SyncService(
Logger.i(TAG, "$remotePublicKey closed")
removeSession(it.remotePublicKey)
synchronized(_remotePendingStatusUpdate) {
_remotePendingStatusUpdate.remove(remotePublicKey)?.invoke(false, "Connection closed")
}
sendRemotePendingStatusUpdate(remotePublicKey, false, "Connection closed")
onClose?.invoke(it)
},
@@ -757,42 +757,67 @@ class SyncService(
fun getAllAuthorizedDevices(): Array<String>? = database.getAllAuthorizedDevices()
fun removeAuthorizedDevice(publicKey: String) = database.removeAuthorizedDevice(publicKey)
fun connect(deviceInfo: SyncDeviceInfo, onStatusUpdate: ((complete: Boolean?, message: String) -> Unit)? = null) {
try {
connect(deviceInfo.addresses, deviceInfo.port, deviceInfo.publicKey, deviceInfo.pairingCode, onStatusUpdate)
} catch (e: Throwable) {
Logger.e(TAG, "Failed to connect directly", e)
val relaySession = _relaySession
if (relaySession != null && Settings.instance.synchronization.pairThroughRelay) {
onStatusUpdate?.invoke(null, "Connecting via relay...")
runBlocking {
if (onStatusUpdate != null) {
synchronized(_remotePendingStatusUpdate) {
_remotePendingStatusUpdate[deviceInfo.publicKey.base64ToByteArray().toBase64()] = onStatusUpdate
}
}
relaySession.startRelayedChannel(deviceInfo.publicKey.base64ToByteArray().toBase64(), appId, deviceInfo.pairingCode)
suspend fun connect(deviceInfo: SyncDeviceInfo, alsoTryRelayed: Boolean = false, timeout_ms: Int = 10_000, onStatusUpdate: ((complete: Boolean?, message: String) -> Unit)? = null) {
val rs = _relaySession
val startTime = System.currentTimeMillis()
if (alsoTryRelayed && rs != null && settings.relayPairAllowed) {
onStatusUpdate?.invoke(null, "Connecting via relay...")
if (onStatusUpdate != null) {
synchronized(_remotePendingStatusUpdateRelayed) {
_remotePendingStatusUpdateRelayed[deviceInfo.publicKey.base64ToByteArray().toBase64()] = onStatusUpdate
}
} else {
throw e
}
//TODO: Do not try relayed channel here only for pairing mode?
rs.startRelayedChannel(deviceInfo.publicKey.base64ToByteArray().toBase64(), appId, deviceInfo.pairingCode)
}
try {
connect(deviceInfo.addresses, deviceInfo.port, deviceInfo.publicKey, deviceInfo.pairingCode, onStatusUpdate, timeout_ms)
} catch (e: Throwable) {
Log.e(TAG, "Failed to connect directly", e)
val waitTime_ms = timeout_ms - (System.currentTimeMillis() - startTime)
if (waitTime_ms > 0)
delay(waitTime_ms)
onStatusUpdate?.invoke(false, "Failed to connect.")
synchronized(_remotePendingStatusUpdateRelayed) {
_remotePendingStatusUpdateRelayed.remove(deviceInfo.publicKey.base64ToByteArray().toBase64())
}
}
}
fun connect(addresses: Array<String>, port: Int, publicKey: String, pairingCode: String?, onStatusUpdate: ((complete: Boolean?, message: String) -> Unit)? = null): SyncSocketSession {
suspend fun connect(addresses: Array<String>, port: Int, publicKey: String, pairingCode: String?, onStatusUpdate: ((complete: Boolean?, message: String) -> Unit)? = null, timeout_ms: Int = 10_000): SyncSocketSession {
val startTime_ms = System.currentTimeMillis()
Log.i(TAG, "Connecting directly (timeout_ms = ${timeout_ms})...")
onStatusUpdate?.invoke(null, "Connecting directly...")
val socket = getConnectedSocket(addresses.map { InetAddress.getByName(it) }, port) ?: throw Exception("Failed to connect")
val socket = getConnectedSocket(addresses.map { InetAddress.getByName(it) }, port, timeout_ms) ?: throw Exception("Failed to connect")
onStatusUpdate?.invoke(null, "Handshaking...")
val session = createSocketSession(socket, false)
if (onStatusUpdate != null) {
synchronized(_remotePendingStatusUpdate) {
_remotePendingStatusUpdate[publicKey.base64ToByteArray().toBase64()] = onStatusUpdate
synchronized(_remotePendingStatusUpdateDirect) {
_remotePendingStatusUpdateDirect[publicKey.base64ToByteArray().toBase64()] = onStatusUpdate
}
}
session.startAsInitiator(publicKey, appId, pairingCode)
while ((System.currentTimeMillis() - startTime_ms) < timeout_ms && !session.isAuthorized) {
delay(100)
}
if (!session.isAuthorized) {
Log.i(TAG, "Session is not authorized after timeout, cancelling connection.")
session.stop()
onStatusUpdate?.invoke(false, "Session not authorized.")
synchronized(_remotePendingStatusUpdateDirect) {
_remotePendingStatusUpdateDirect.remove(publicKey.base64ToByteArray().toBase64())
}
}
return session
}
@@ -811,6 +836,8 @@ class SyncService(
synchronized(_sessions) {
_sessions.clear()
}
_remotePendingStatusUpdateDirect.clear()
_remotePendingStatusUpdateRelayed.clear()
}
private fun getDeviceName(): String {
@@ -56,6 +56,7 @@ class SyncSocketSession {
private var _remotePublicKey: String? = null
val remotePublicKey: String? get() = _remotePublicKey
private var _started: Boolean = false
val started get() = _started
private val _localKeyPair: DHState
private var _thread: Thread? = null
private var _localPublicKey: String
@@ -1,6 +1,11 @@
package com.futo.platformplayer.views.adapters
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.view.GestureDetector
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
@@ -11,6 +16,7 @@ import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.api.media.models.comments.LazyComment
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
@@ -97,6 +103,15 @@ class CommentViewHolder : ViewHolder {
StatePolycentric.instance.updateLikeMap(c.reference, args.hasLiked, args.hasDisliked)
};
_layoutComment.setOnLongClickListener {
val clipboard = viewGroup.context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val text = comment?.message.orEmpty()
val clip = ClipData.newPlainText("Comment", text)
clipboard.setPrimaryClip(clip)
UIDialogs.toast(viewGroup.context, "Copied", false)
true
}
_creatorThumbnail.onClick.subscribe {
val c = comment ?: return@subscribe;
onAuthorClick.emit(c);
@@ -120,7 +135,7 @@ class CommentViewHolder : ViewHolder {
onDelete.emit(c);
}
_textBody.setPlatformPlayerLinkMovementMethod(viewGroup.context);
_textBody.setPlatformPlayerLinkMovementMethod(viewGroup.context)
}
fun bind(comment: IPlatformComment, readonly: Boolean) {
@@ -0,0 +1,25 @@
package com.futo.platformplayer.views.behavior
import android.content.Context
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatTextView
import com.futo.platformplayer.logging.Logger
class SafeTextView : AppCompatTextView {
constructor(context: Context) : super(context) {}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {}
override fun performLongClick(): Boolean {
try {
return super.performLongClick()
} catch (e: IllegalStateException) {
Logger.w(TAG, "Swallowed exception", e)
return false
}
}
companion object {
private const val TAG = "SafeTextView"
}
}
@@ -68,6 +68,7 @@ class CastView : ConstraintLayout {
val onPrevious = Event0();
val onNext = Event0();
val onTimeJobTimeChanged_s = Event1<Long>()
val loaderGameVisibilityChanged = Event1<Boolean>();
@OptIn(UnstableApi::class)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
@@ -90,6 +91,7 @@ class CastView : ConstraintLayout {
_gestureControlView = findViewById(R.id.gesture_control);
_loaderGame = findViewById(R.id.loader_overlay)
_loaderGame.visibility = View.GONE
loaderGameVisibilityChanged.emit(false)
_gestureControlView.fullScreenGestureEnabled = false
_gestureControlView.setupTouchArea();
@@ -319,15 +321,18 @@ class CastView : ConstraintLayout {
if (isLoading) {
_loaderGame.visibility = View.VISIBLE
_loaderGame.startLoader()
loaderGameVisibilityChanged.emit(true)
} else {
_loaderGame.visibility = View.GONE
_loaderGame.stopAndResetLoader()
loaderGameVisibilityChanged.emit(false)
}
}
fun setLoading(expectedDurationMs: Int) {
_loaderGame.visibility = View.VISIBLE
_loaderGame.startLoader(expectedDurationMs.toLong())
loaderGameVisibilityChanged.emit(true)
}
companion object {
@@ -60,6 +60,10 @@ class FutoShortPlayer(context: Context, attrs: AttributeSet? = null) :
if (events.containsAny(Player.EVENT_PLAYBACK_STATE_CHANGED)) {
onPlaybackStateChanged.emit(player.playbackState)
}
if (events.containsAny(Player.EVENT_IS_PLAYING_CHANGED)) {
onPlayChanged.emit(player.isPlaying)
}
}
}
@@ -164,6 +164,8 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
private val _loaderGame: TargetTapLoaderView
val loaderGameVisibilityChanged = Event1<Boolean>();
@OptIn(UnstableApi::class)
constructor(context: Context, attrs: AttributeSet? = null) : super(PLAYER_STATE_NAME, context, attrs) {
LayoutInflater.from(context).inflate(R.layout.video_view, this, true);
@@ -206,6 +208,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
_loaderGame = findViewById(R.id.loader_overlay)
_loaderGame.visibility = View.GONE
loaderGameVisibilityChanged.emit(false)
_control_chapter.setOnClickListener {
_currentChapter?.let {
@@ -894,15 +897,18 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
if (isLoading) {
_loaderGame.visibility = View.VISIBLE
_loaderGame.startLoader()
loaderGameVisibilityChanged.emit(true)
} else {
_loaderGame.visibility = View.GONE
_loaderGame.stopAndResetLoader()
loaderGameVisibilityChanged.emit(false)
}
}
override fun setLoading(expectedDurationMs: Int) {
_loaderGame.visibility = View.VISIBLE
_loaderGame.startLoader(expectedDurationMs.toLong())
loaderGameVisibilityChanged.emit(true)
}
override fun switchToVideoMode() {
@@ -69,6 +69,7 @@ import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalAud
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.awaitCancelConverted
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.TaskHandler
@@ -643,7 +644,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
setLoading(true)
}
}
val generated = generatedDef.await();
val generated = generatedDef.awaitCancelConverted();
if (_swapIdVideo.get() != swapId) {
return@launch
}
@@ -807,7 +808,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
setLoading(true)
}
}
val generated = generatedDef.await();
val generated = generatedDef.awaitCancelConverted();
if (_swapIdAudio.get() != swapId) {
return@launch
}
@@ -913,6 +914,10 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
return VideoHelper.selectBestAudioSource(video.video, PREFERED_AUDIO_CONTAINERS, preferredLanguage);
}
fun getPreferredSubtitleSource(video: IPlatformVideoDetails, preferredLanguage: String?): ISubtitleSource? {
return VideoHelper.selectBestSubtitleSource(video.subtitles, preferredLanguage);
}
@OptIn(UnstableApi::class)
private fun loadSelectedSources(play: Boolean, resume: Boolean): Boolean {
val sourceVideo = if(!isAudioMode || _lastAudioMediaSource == null) _lastVideoMediaSource else null;
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/colorPrimary" />
<corners android:radius="18dp" />
</shape>
@@ -15,7 +15,6 @@
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingTop="20dp"
android:paddingBottom="15dp">
<ImageButton
@@ -19,7 +19,6 @@
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingTop="20dp"
android:paddingBottom="15dp">
<ImageButton
-1
View File
@@ -15,7 +15,6 @@
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingTop="20dp"
android:paddingBottom="15dp">
<ImageButton
@@ -18,7 +18,6 @@
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingTop="20dp"
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:paddingBottom="15dp">
@@ -14,7 +14,6 @@
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingTop="20dp"
android:paddingBottom="15dp">
<ImageButton
@@ -64,7 +64,6 @@
android:id="@+id/layout_buttons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="20dp"
android:paddingBottom="20dp"
android:orientation="vertical">
@@ -0,0 +1,228 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
android:padding="16dp">
<LinearLayout
android:id="@+id/header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintTop_toTopOf="parent">
<ImageButton
android:id="@+id/button_back"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/back"
android:padding="12dp"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_back_thin_white_16dp" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:text="Moderation Settings"
android:textColor="?attr/colorOnBackground"
android:textSize="18sp"
android:textStyle="bold" />
</LinearLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/header">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/text_moderation_blurb"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:text="A lower slider value hides more content. Posts or comments with a tag level ABOVE your selected value will be hidden. (Level 0 = most strict, Level 3 = allow everything)"
android:textColor="?attr/colorOnSurface"
android:textSize="14sp" />
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardBackgroundColor="?attr/colorSurface"
app:cardCornerRadius="16dp"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Moderation Levels"
android:textColor="?attr/colorOnSurface"
android:textSize="18sp"
android:textStyle="bold" />
<!-- Offensive Content -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Offensive Content"
android:textColor="?attr/colorOnSurface"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/text_offensive_desc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="Description"
android:textColor="?attr/colorOnSurface"
android:textSize="14sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<SeekBar
android:id="@+id/seekbar_offensive"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:max="3"
android:progress="2" />
<TextView
android:id="@+id/text_offensive_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:background="@drawable/background_slider_value"
android:gravity="center"
android:minWidth="36dp"
android:padding="8dp"
android:text="2"
android:textColor="@android:color/white"
android:textStyle="bold" />
</LinearLayout>
<!-- Explicit Content -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="Explicit Content"
android:textColor="?attr/colorOnSurface"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/text_explicit_desc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="Description"
android:textColor="?attr/colorOnSurface"
android:textSize="14sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<SeekBar
android:id="@+id/seekbar_explicit"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:max="3"
android:progress="1" />
<TextView
android:id="@+id/text_explicit_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:background="@drawable/background_slider_value"
android:gravity="center"
android:minWidth="36dp"
android:padding="8dp"
android:text="1"
android:textColor="@android:color/white"
android:textStyle="bold" />
</LinearLayout>
<!-- Violence -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="Violence"
android:textColor="?attr/colorOnSurface"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/text_violence_desc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="Description"
android:textColor="?attr/colorOnSurface"
android:textSize="14sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<SeekBar
android:id="@+id/seekbar_violence"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:max="3"
android:progress="1" />
<TextView
android:id="@+id/text_violence_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:background="@drawable/background_slider_value"
android:gravity="center"
android:minWidth="36dp"
android:padding="8dp"
android:text="1"
android:textColor="@android:color/white"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -73,7 +73,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/inter_regular"
android:text="Further customize your profile, make platform claims, and other creator-specific features in the Harbor app."
android:text="Further customize your profile, make platform claims, and other creator-specific features in the Polycentric app."
android:textSize="12dp"
android:linksClickable="true"
android:paddingLeft="20dp"
@@ -90,7 +90,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/inter_regular"
android:text="https://harbor.social"
android:text="https://polycentric.io"
android:textSize="12dp"
android:linksClickable="true"
android:paddingLeft="20dp"
@@ -107,7 +107,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/inter_regular"
android:text="After you've installed Harbor you can export this profile to Harbor using the Export button."
android:text="After you've installed Polycentric you can export this profile to Polycentric using the Export button."
android:textSize="12dp"
android:linksClickable="true"
android:paddingLeft="20dp"
@@ -133,13 +133,13 @@
app:layout_constraintBottom_toBottomOf="parent">
<com.futo.platformplayer.views.buttons.BigButton
android:id="@+id/button_open_harbor_profile"
android:id="@+id/button_moderation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:buttonText="Harbor Profile"
app:buttonSubText="See your Harbor profile in a browser"
app:buttonIcon="@drawable/ic_export"
android:layout_marginTop="8dp" />
app:buttonSubText="Set moderation settings for polycentric comments"
android:layout_marginTop="8dp"
app:buttonIcon="@drawable/ic_settings"
app:buttonText="Moderation Settings" />
<com.futo.platformplayer.views.buttons.BigButton
android:id="@+id/button_export"
@@ -17,7 +17,6 @@
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingTop="20dp"
android:paddingBottom="15dp">
<ImageButton
@@ -5,7 +5,6 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:fitsSystemWindows="false"
android:background="@drawable/bottom_menu_border"
android:id="@+id/root"
android:clickable="true">
@@ -5,7 +5,6 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:fitsSystemWindows="false"
android:background="@drawable/bottom_menu_border"
android:id="@+id/root"
android:clickable="true">
@@ -5,7 +5,6 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:fitsSystemWindows="false"
android:background="@drawable/bottom_menu_border"
android:id="@+id/videodetail_root"
android:clickable="true">
@@ -5,7 +5,6 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:fitsSystemWindows="false"
android:background="@drawable/bottom_menu_border"
android:id="@+id/root"
android:clickable="true">
@@ -26,7 +26,7 @@
app:layout_constraintTop_toBottomOf="@id/topbar"
app:layout_constraintBottom_toBottomOf="parent">
<TextView
<com.futo.platformplayer.views.behavior.SafeTextView
android:id="@+id/text_description"
android:layout_width="match_parent"
android:layout_height="match_parent"
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.FutoVideo.NoActionBarFitsSystem" parent="Theme.FutoVideo.NoActionBar">
<item name="android:fitsSystemWindows">true</item>
<item name="android:enforceStatusBarContrast">false</item>
<item name="android:enforceNavigationBarContrast">false</item>
</style>
</resources>
+2
View File
@@ -505,6 +505,8 @@
<string name="preferred_preview_quality">Preferred Preview Quality</string>
<string name="preferred_preview_quality_description">Default quality while previewing a video in a feed</string>
<string name="primary_language">Primary Language</string>
<string name="sticky_subtitles">Persist Subtitles</string>
<string name="sticky_subtitles_description">Once a subtitle language is selected, search for a best match once a new video is played</string>
<string name="prefer_original_audio">Prefer Original Audio</string>
<string name="prefer_original_audio_description">Use original audio instead of preferred language when it is known</string>
<string name="default_comment_section">Default Comment Section</string>
+12 -1
View File
@@ -42,7 +42,8 @@
<item name="colorSecondaryVariant">@color/gray_30</item>
<item name="colorOnSecondary">@color/black</item>
<item name="android:statusBarColor">@color/black</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="imageButtonStyle">@style/Theme.FutoVideo.ImageButton</item>
<item name="editTextStyle">@style/Theme.FutoVideo.EditText</item>
@@ -91,6 +92,16 @@
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
<item name="android:windowBackground">@color/black</item>
<item name="android:statusBarColor">@color/black</item>
<item name="android:navigationBarColor">@color/black</item>
<item name="android:navigationBarDividerColor">@color/transparent</item>
<item name="android:windowLightStatusBar">false</item>
<item name="android:windowLightNavigationBar">false</item>
</style>
<style name="Theme.FutoVideo.NoActionBarFitsSystem" parent="Theme.FutoVideo.NoActionBar">
<item name="android:fitsSystemWindows">true</item>
</style>
<style name="Theme.FutoVideo.TextAppearance.TabLayout" parent="@android:style/TextAppearance.Medium">
+5 -5
View File
@@ -1,8 +1,8 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id 'com.android.application' version '8.5.2' apply false
id 'com.android.library' version '8.5.2' apply false
id 'org.jetbrains.kotlin.android' version '1.9.0' apply false
id 'com.google.protobuf' version '0.9.4' apply false
id 'com.google.devtools.ksp' version '1.9.0-1.0.13' apply false
id 'com.android.application' version '8.13.0' apply false
id 'com.android.library' version '8.13.0' apply false
id 'org.jetbrains.kotlin.android' version '2.2.21' apply false
id 'com.google.protobuf' version '0.9.5' apply false
id 'com.google.devtools.ksp' version '2.2.21-2.0.4' apply false
}
+1 -1
View File
@@ -1,6 +1,6 @@
#Fri Nov 11 13:25:09 CET 2022
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME