mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-28 18:43:02 +02:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6fa97a21f6 | |||
| b71d1d3cd6 | |||
| 3ab38c7e4e | |||
| 1abcc92a3b | |||
| 25ab7aff92 | |||
| d8a0781d10 | |||
| 42040e5f3d | |||
| f00e71522f | |||
| 8916fd35ab | |||
| cfb030f59b | |||
| 71f626e8e0 | |||
| 6f4d7fd6b9 | |||
| f207ef9954 | |||
| 1ad3b7ae8f | |||
| 217d738dd1 | |||
| 799dad8875 | |||
| dc91987e88 | |||
| 8f5d90a1c8 | |||
| 6e58107a5e | |||
| 79f478e421 | |||
| a20ebd49a4 | |||
| 31f0109438 | |||
| bbd9ba0a0a | |||
| bc67f4c486 | |||
| c862b60c71 | |||
| 1524687f75 | |||
| cb74e82fa1 | |||
| 4b3e89d0af | |||
| 31a34e4583 | |||
| 19b96c2ea1 | |||
| 545ece59ad | |||
| c93fc664f6 | |||
| eb8e9270e0 | |||
| 7aea4a3f9f | |||
| 76ce60d679 | |||
| 24dbb543ae | |||
| 79a2c6484c | |||
| 3aa873f82b | |||
| 49f3fa9cb8 | |||
| 49367c6c75 | |||
| e1cb3308df | |||
| eb48d6c494 |
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ea10d3c5562c9f449a4e89e9c3dfcf881ed79a952f3409bc005bcc62c2cf4b81
|
||||
size 65512557
|
||||
@@ -153,30 +153,30 @@
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".activities.TestActivity"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.SettingsActivity"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.DeveloperActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.ExceptionActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.CaptchaActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.LoginActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.AddSourceActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem">
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
@@ -189,54 +189,62 @@
|
||||
<activity
|
||||
android:name=".activities.AddSourceOptionsActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.PolycentricHomeActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.PolycentricBackupActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.PolycentricCreateProfileActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.PolycentricProfileActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.PolycentricWhyActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.PolycentricImportProfileActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.ManageTabsActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.QRCaptureActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.FCastGuideActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.SyncHomeActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.SyncPairActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.SyncShowPairingCodeActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBarFitsSystem" />
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.PolycentricModerationActivity"
|
||||
android:exported="false"
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name=".activities.QRCodeFullscreenActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -216,9 +216,10 @@ private fun ByteArray.toInetAddress(): InetAddress {
|
||||
return InetAddress.getByAddress(this);
|
||||
}
|
||||
|
||||
fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int, timeoutMs: Int = 10_000): Socket? {
|
||||
fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int): Socket? {
|
||||
ensureNotMainThread()
|
||||
|
||||
val timeout = 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")})");
|
||||
@@ -231,7 +232,7 @@ fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int, timeoutMs
|
||||
val socket = Socket()
|
||||
|
||||
try {
|
||||
return socket.apply { this.connect(InetSocketAddress(addresses[0], port), timeoutMs) }
|
||||
return socket.apply { this.connect(InetSocketAddress(addresses[0], port), timeout) }
|
||||
} catch (e: Throwable) {
|
||||
Log.i("getConnectedSocket", "Failed to connect to: ${addresses[0]}", e)
|
||||
socket.close()
|
||||
@@ -262,7 +263,7 @@ fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int, timeoutMs
|
||||
}
|
||||
}
|
||||
|
||||
socket.connect(InetSocketAddress(address, port), timeoutMs);
|
||||
socket.connect(InetSocketAddress(address, port), timeout);
|
||||
|
||||
synchronized(syncObject) {
|
||||
if (connectedSocket == null) {
|
||||
|
||||
@@ -7,7 +7,6 @@ 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
|
||||
@@ -301,13 +300,10 @@ 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 Throwable("Promise Failed: " + pluginType + msg);
|
||||
*/
|
||||
}
|
||||
else if(p0 is V8ValueString)
|
||||
return Throwable("Promise Failed:" + p0.value);
|
||||
@@ -375,16 +371,4 @@ 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,80 @@
|
||||
package com.futo.platformplayer
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
package com.futo.platformplayer
|
||||
|
||||
import android.app.Activity
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.view.ViewGroup
|
||||
import android.view.Window
|
||||
import android.view.WindowManager
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsCompat.Type
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.core.view.doOnAttach
|
||||
import androidx.core.view.updatePadding
|
||||
import kotlin.math.max
|
||||
|
||||
class RootInsetsController private constructor(
|
||||
private val activity: Activity,
|
||||
private val window: Window,
|
||||
private val root: ViewGroup
|
||||
) {
|
||||
private val controller by lazy { WindowInsetsControllerCompat(window, root) }
|
||||
|
||||
private val basePaddingLeft = root.paddingLeft
|
||||
private val basePaddingTop = root.paddingTop
|
||||
private val basePaddingRight = root.paddingRight
|
||||
private val basePaddingBottom = root.paddingBottom
|
||||
|
||||
private var currentInsets: WindowInsetsCompat = WindowInsetsCompat.CONSUMED
|
||||
private var fullscreen = false
|
||||
|
||||
init {
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
window.statusBarColor = Color.TRANSPARENT
|
||||
window.navigationBarColor = Color.TRANSPARENT
|
||||
controller.systemBarsBehavior =
|
||||
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(root) { _, insets ->
|
||||
currentInsets = insets
|
||||
applyPadding()
|
||||
insets
|
||||
}
|
||||
|
||||
root.doOnAttach { ViewCompat.requestApplyInsets(root) }
|
||||
}
|
||||
|
||||
private fun effectiveInsets(): Insets {
|
||||
if (fullscreen) return Insets.NONE
|
||||
|
||||
val sys = currentInsets.getInsets(Type.systemBars())
|
||||
val cut = currentInsets.getInsetsIgnoringVisibility(Type.displayCutout())
|
||||
val portrait = activity.resources.configuration.orientation == android.content.res.Configuration.ORIENTATION_PORTRAIT
|
||||
|
||||
val top = if (portrait) max(sys.top, cut.top) else sys.top
|
||||
return Insets.of(sys.left, top, sys.right, sys.bottom)
|
||||
}
|
||||
|
||||
|
||||
private fun applyPadding() {
|
||||
val e = effectiveInsets()
|
||||
root.updatePadding(
|
||||
left = basePaddingLeft + e.left,
|
||||
top = basePaddingTop + e.top,
|
||||
right = basePaddingRight + e.right,
|
||||
bottom = basePaddingBottom + e.bottom
|
||||
)
|
||||
}
|
||||
|
||||
private fun forceRelayoutAndInsets() {
|
||||
root.post {
|
||||
ViewCompat.requestApplyInsets(root)
|
||||
applyPadding()
|
||||
root.post {
|
||||
ViewCompat.requestApplyInsets(root)
|
||||
applyPadding()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun enterFullscreen(allowCutoutShortEdges: Boolean = true) {
|
||||
fullscreen = true
|
||||
if (allowCutoutShortEdges) {
|
||||
window.attributes = window.attributes.apply {
|
||||
layoutInDisplayCutoutMode =
|
||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
||||
}
|
||||
}
|
||||
controller.hide(Type.systemBars())
|
||||
forceRelayoutAndInsets()
|
||||
}
|
||||
|
||||
fun exitFullscreen() {
|
||||
fullscreen = false
|
||||
window.attributes = window.attributes.apply {
|
||||
layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
|
||||
}
|
||||
controller.show(Type.systemBars())
|
||||
forceRelayoutAndInsets()
|
||||
}
|
||||
|
||||
fun onConfigurationChanged() {
|
||||
forceRelayoutAndInsets()
|
||||
}
|
||||
|
||||
fun setLightSystemBarAppearance(lightStatus: Boolean, lightNav: Boolean) {
|
||||
controller.isAppearanceLightStatusBars = lightStatus
|
||||
controller.isAppearanceLightNavigationBars = lightNav
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun attach(activity: Activity, root: ViewGroup): RootInsetsController {
|
||||
return RootInsetsController(activity, activity.window, root)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ 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
|
||||
@@ -35,7 +36,6 @@ 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,7 +199,6 @@ 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)
|
||||
@@ -285,6 +284,9 @@ 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 {
|
||||
@@ -299,9 +301,6 @@ 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);
|
||||
@@ -412,11 +411,6 @@ 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 {
|
||||
@@ -645,11 +639,6 @@ 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,
|
||||
@@ -718,14 +707,21 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
return;
|
||||
Logger.i(TAG, "handleIntent started by " + intent.action);
|
||||
|
||||
|
||||
var targetData: String? = null;
|
||||
|
||||
when (intent.action) {
|
||||
Intent.ACTION_SEND -> {
|
||||
targetData = intent.getStringExtra(Intent.EXTRA_STREAM)
|
||||
?: intent.getStringExtra(Intent.EXTRA_TEXT);
|
||||
Logger.i(TAG, "Share Received: " + targetData);
|
||||
val streamExtra = intent.getStringExtra(Intent.EXTRA_STREAM);
|
||||
val textExtra = intent.getStringExtra(Intent.EXTRA_TEXT);
|
||||
val streamParcelable = intent.getParcelableExtra<android.net.Uri>(Intent.EXTRA_STREAM);
|
||||
|
||||
// Try to get the actual file content
|
||||
targetData = when {
|
||||
streamParcelable != null -> streamParcelable.toString()
|
||||
streamExtra != null -> streamExtra
|
||||
textExtra != null -> textExtra
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
Intent.ACTION_VIEW -> {
|
||||
@@ -1013,6 +1009,14 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
|
||||
fun handleUnknownText(text: String): Boolean {
|
||||
try {
|
||||
// Check for Polycentric profile data
|
||||
if (text.startsWith("polycentric://")) {
|
||||
startActivity(Intent(this, PolycentricImportProfileActivity::class.java).apply {
|
||||
putExtra("url", text.trim())
|
||||
})
|
||||
return true;
|
||||
}
|
||||
|
||||
if (text.startsWith("@/Subscription") || text.startsWith("Subscriptions")) {
|
||||
val lines = text.split("\n").map { it.trim() }.drop(1).filter { it.isNotEmpty() };
|
||||
navigate(_fragImportSubscriptions, lines);
|
||||
|
||||
@@ -13,15 +13,18 @@ import android.view.View
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateApp.Companion.withContext
|
||||
import com.futo.platformplayer.states.StatePolycentric
|
||||
import com.futo.platformplayer.views.buttons.BigButton
|
||||
import com.futo.platformplayer.activities.QRCodeFullscreenActivity
|
||||
import com.futo.polycentric.core.ContentType
|
||||
import com.futo.polycentric.core.SignedEvent
|
||||
import com.futo.polycentric.core.StorageTypeCRDTItem
|
||||
@@ -29,8 +32,10 @@ import com.futo.polycentric.core.StorageTypeCRDTSetItem
|
||||
import com.futo.polycentric.core.Store
|
||||
import com.futo.polycentric.core.toBase64Url
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.EncodeHintType
|
||||
import com.google.zxing.MultiFormatWriter
|
||||
import com.google.zxing.common.BitMatrix
|
||||
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -41,11 +46,27 @@ import userpackage.Protocol.URLInfo
|
||||
class PolycentricBackupActivity : AppCompatActivity() {
|
||||
private lateinit var _buttonShare: BigButton;
|
||||
private lateinit var _buttonCopy: BigButton;
|
||||
private lateinit var _buttonExportFile: BigButton;
|
||||
private lateinit var _imageQR: ImageView;
|
||||
private lateinit var _exportBundle: String;
|
||||
private lateinit var _textQR: TextView;
|
||||
private lateinit var _textQRHint: TextView;
|
||||
private lateinit var _loader: View
|
||||
|
||||
private val _createDocumentLauncher = registerForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { uri ->
|
||||
uri?.let { fileUri ->
|
||||
try {
|
||||
contentResolver.openOutputStream(fileUri)?.use { outputStream ->
|
||||
outputStream.write(_exportBundle.toByteArray())
|
||||
}
|
||||
UIDialogs.toast(this, getString(R.string.profile_saved_successfully))
|
||||
} catch (e: Exception) {
|
||||
Logger.e(TAG, "Failed to write to document", e)
|
||||
UIDialogs.toast(this, "Failed to save profile: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun attachBaseContext(newBase: Context?) {
|
||||
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
||||
}
|
||||
@@ -57,8 +78,10 @@ class PolycentricBackupActivity : AppCompatActivity() {
|
||||
|
||||
_buttonShare = findViewById(R.id.button_share)
|
||||
_buttonCopy = findViewById(R.id.button_copy)
|
||||
_buttonExportFile = findViewById(R.id.button_export_file)
|
||||
_imageQR = findViewById(R.id.image_qr)
|
||||
_textQR = findViewById(R.id.text_qr)
|
||||
_textQRHint = findViewById(R.id.text_qr_hint)
|
||||
_loader = findViewById(R.id.progress_loader)
|
||||
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
|
||||
finish();
|
||||
@@ -66,14 +89,23 @@ class PolycentricBackupActivity : AppCompatActivity() {
|
||||
|
||||
_imageQR.visibility = View.INVISIBLE
|
||||
_textQR.visibility = View.INVISIBLE
|
||||
_textQRHint.visibility = View.INVISIBLE
|
||||
_loader.visibility = View.VISIBLE
|
||||
_buttonShare.visibility = View.INVISIBLE
|
||||
_buttonCopy.visibility = View.INVISIBLE
|
||||
_buttonExportFile.visibility = View.INVISIBLE
|
||||
|
||||
lifecycleScope.launch {
|
||||
val bundle = withContext(Dispatchers.IO) { createExportBundle() }
|
||||
_exportBundle = bundle
|
||||
Logger.i(TAG, "Export bundle created, length: ${bundle.length}")
|
||||
|
||||
try {
|
||||
val pair = withContext(Dispatchers.IO) {
|
||||
val bundle = createExportBundle()
|
||||
if (!isContentSuitableForQRCode(bundle)) {
|
||||
throw Exception("Data too big for QR code generation")
|
||||
}
|
||||
|
||||
val dimension = TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP, 200f, resources.displayMetrics
|
||||
).toInt()
|
||||
@@ -81,18 +113,35 @@ class PolycentricBackupActivity : AppCompatActivity() {
|
||||
Pair(bundle, qr)
|
||||
}
|
||||
|
||||
_exportBundle = pair.first
|
||||
_imageQR.setImageBitmap(pair.second)
|
||||
_imageQR.visibility = View.VISIBLE
|
||||
_textQR.visibility = View.VISIBLE
|
||||
_textQRHint.visibility = View.VISIBLE
|
||||
_buttonShare.visibility = View.VISIBLE
|
||||
_buttonCopy.visibility = View.VISIBLE
|
||||
|
||||
_imageQR.setOnClickListener {
|
||||
val intent = QRCodeFullscreenActivity.createIntent(this@PolycentricBackupActivity, _exportBundle)
|
||||
startActivity(intent)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.e(TAG, getString(R.string.failed_to_generate_qr_code), e)
|
||||
val byteSize = bundle.toByteArray(Charsets.UTF_8).size
|
||||
Logger.e(TAG, "QR code generation failed. Bundle length: ${bundle.length} chars, ${byteSize} bytes, Error: ${e.message}", e)
|
||||
|
||||
if (e.message?.contains("Data too big") == true) {
|
||||
_textQR.text = getString(R.string.qr_code_too_large_use_file_export)
|
||||
_buttonExportFile.visibility = View.VISIBLE
|
||||
} else {
|
||||
_textQR.text = getString(R.string.failed_to_generate_qr_code)
|
||||
}
|
||||
|
||||
_textQR.visibility = View.VISIBLE
|
||||
_textQRHint.visibility = View.INVISIBLE
|
||||
_buttonShare.visibility = View.VISIBLE
|
||||
_buttonCopy.visibility = View.VISIBLE
|
||||
|
||||
// Hide QR image since generation failed
|
||||
_imageQR.visibility = View.INVISIBLE
|
||||
_textQR.visibility = View.INVISIBLE
|
||||
_buttonShare.visibility = View.INVISIBLE
|
||||
_buttonCopy.visibility = View.INVISIBLE
|
||||
} finally {
|
||||
_loader.visibility = View.GONE
|
||||
}
|
||||
@@ -108,11 +157,29 @@ class PolycentricBackupActivity : AppCompatActivity() {
|
||||
val clip = ClipData.newPlainText(getString(R.string.copied_text), _exportBundle);
|
||||
clipboard.setPrimaryClip(clip);
|
||||
};
|
||||
|
||||
_buttonExportFile.onClick.subscribe {
|
||||
val fileName = "polycentric_profile_${System.currentTimeMillis()}.txt"
|
||||
_createDocumentLauncher.launch(fileName)
|
||||
};
|
||||
}
|
||||
|
||||
private fun isContentSuitableForQRCode(content: String): Boolean {
|
||||
val bytes = content.toByteArray(Charsets.UTF_8)
|
||||
return bytes.size <= 2300 // QR Code Version 40 with Error Correction Level M can hold ~2331 bytes
|
||||
}
|
||||
|
||||
private fun generateQRCode(content: String, width: Int, height: Int): Bitmap {
|
||||
val bitMatrix = MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height);
|
||||
return bitMatrixToBitmap(bitMatrix);
|
||||
if (!isContentSuitableForQRCode(content)) {
|
||||
throw Exception("Data too big for QR code generation")
|
||||
}
|
||||
|
||||
val hints = java.util.EnumMap<EncodeHintType, Any>(EncodeHintType::class.java)
|
||||
hints[EncodeHintType.ERROR_CORRECTION] = ErrorCorrectionLevel.M
|
||||
hints[EncodeHintType.MARGIN] = 1
|
||||
|
||||
val bitMatrix = MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height, hints)
|
||||
return bitMatrixToBitmap(bitMatrix)
|
||||
}
|
||||
|
||||
private fun bitMatrixToBitmap(matrix: BitMatrix): Bitmap {
|
||||
@@ -203,9 +270,11 @@ class PolycentricBackupActivity : AppCompatActivity() {
|
||||
.setBody(exportBundle.toByteString())
|
||||
.build();
|
||||
|
||||
return "polycentric://" + urlInfo.toByteArray().toBase64Url()
|
||||
val data = urlInfo.toByteArray()
|
||||
return "polycentric://" + data.toBase64Url()
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
private const val TAG = "PolycentricBackupActivity";
|
||||
}
|
||||
|
||||
+57
@@ -34,6 +34,7 @@ import userpackage.Protocol.ExportBundle
|
||||
class PolycentricImportProfileActivity : AppCompatActivity() {
|
||||
private lateinit var _buttonHelp: ImageButton;
|
||||
private lateinit var _buttonScanProfile: LinearLayout;
|
||||
private lateinit var _buttonImportFile: LinearLayout;
|
||||
private lateinit var _buttonImportProfile: LinearLayout;
|
||||
private lateinit var _editProfile: EditText;
|
||||
private lateinit var _loaderOverlay: LoaderOverlay;
|
||||
@@ -48,6 +49,56 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private val _filePickerLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
|
||||
uri?.let { fileUri ->
|
||||
try {
|
||||
// Check file size before reading
|
||||
val fileSize = contentResolver.openFileDescriptor(fileUri, "r")?.statSize ?: 0
|
||||
val maxFileSize = 10 * 1024 * 1024 // 10MB limit
|
||||
|
||||
if (fileSize > maxFileSize) {
|
||||
UIDialogs.toast(this, "File too large. Maximum size is 10MB.")
|
||||
return@let
|
||||
}
|
||||
|
||||
if (fileSize == 0L) {
|
||||
UIDialogs.toast(this, "Selected file is empty.")
|
||||
return@let
|
||||
}
|
||||
|
||||
val content = contentResolver.openInputStream(fileUri)?.bufferedReader()?.readText()
|
||||
content?.let { fileContent ->
|
||||
val trimmedContent = fileContent.trim()
|
||||
|
||||
// Check if content is empty after trimming
|
||||
if (trimmedContent.isEmpty()) {
|
||||
UIDialogs.toast(this, "Selected file contains no data.")
|
||||
return@let
|
||||
}
|
||||
|
||||
// Check if content looks like a valid polycentric URL
|
||||
if (!trimmedContent.startsWith("polycentric://")) {
|
||||
UIDialogs.toast(this, "Selected file does not contain a valid polycentric profile URL.")
|
||||
return@let
|
||||
}
|
||||
|
||||
import(trimmedContent)
|
||||
} ?: run {
|
||||
UIDialogs.toast(this, "Could not read file content.")
|
||||
}
|
||||
} catch (e: SecurityException) {
|
||||
Logger.e(TAG, "Security exception reading file", e)
|
||||
UIDialogs.toast(this, "Permission denied to read file.")
|
||||
} catch (e: OutOfMemoryError) {
|
||||
Logger.e(TAG, "Out of memory reading file", e)
|
||||
UIDialogs.toast(this, "File too large to process.")
|
||||
} catch (e: Exception) {
|
||||
Logger.e(TAG, "Failed to read file", e)
|
||||
UIDialogs.toast(this, "Failed to read file: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun attachBaseContext(newBase: Context?) {
|
||||
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
||||
}
|
||||
@@ -59,6 +110,7 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
||||
|
||||
_buttonHelp = findViewById(R.id.button_help);
|
||||
_buttonScanProfile = findViewById(R.id.button_scan_profile);
|
||||
_buttonImportFile = findViewById(R.id.button_import_file);
|
||||
_buttonImportProfile = findViewById(R.id.button_import_profile);
|
||||
_loaderOverlay = findViewById(R.id.loader_overlay);
|
||||
_editProfile = findViewById(R.id.edit_profile);
|
||||
@@ -82,6 +134,10 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
||||
_qrCodeResultLauncher.launch(integrator.createScanIntent())
|
||||
};
|
||||
|
||||
_buttonImportFile.setOnClickListener {
|
||||
_filePickerLauncher.launch("text/plain")
|
||||
};
|
||||
|
||||
_buttonImportProfile.setOnClickListener {
|
||||
if (_editProfile.text.isEmpty()) {
|
||||
UIDialogs.toast(this, getString(R.string.text_field_does_not_contain_any_data));
|
||||
@@ -109,6 +165,7 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
||||
try {
|
||||
val data = url.substring("polycentric://".length).base64UrlToByteArray();
|
||||
val urlInfo = Protocol.URLInfo.parseFrom(data);
|
||||
|
||||
if (urlInfo.urlType != 3L) {
|
||||
throw Exception("Expected urlInfo struct of type ExportBundle")
|
||||
}
|
||||
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
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.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()
|
||||
|
||||
try {
|
||||
_moderationsManager = ModerationsManager.getInstance()
|
||||
} catch (e: IllegalStateException) {
|
||||
// ModerationsManager not initialized, finish activity
|
||||
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() {
|
||||
if (isFinishing || isDestroyed) return
|
||||
|
||||
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) {
|
||||
if (isFinishing || isDestroyed) return
|
||||
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) {
|
||||
if (isFinishing || isDestroyed) return
|
||||
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) {
|
||||
if (isFinishing || isDestroyed) return
|
||||
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>) {
|
||||
if (isFinishing || isDestroyed) return
|
||||
|
||||
val progress = seekBar?.progress ?: 0
|
||||
if (progress in descriptions.indices) {
|
||||
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);
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
package com.futo.platformplayer.activities
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.EncodeHintType
|
||||
import com.google.zxing.MultiFormatWriter
|
||||
import com.google.zxing.common.BitMatrix
|
||||
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
|
||||
|
||||
class QRCodeFullscreenActivity : AppCompatActivity() {
|
||||
companion object {
|
||||
private const val EXTRA_QR_TEXT = "qr_text"
|
||||
|
||||
fun createIntent(context: Context, qrText: String): android.content.Intent {
|
||||
return android.content.Intent(context, QRCodeFullscreenActivity::class.java).apply {
|
||||
putExtra(EXTRA_QR_TEXT, qrText)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun attachBaseContext(newBase: Context?) {
|
||||
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_qr_code_fullscreen)
|
||||
setNavigationBarColorAndIcons()
|
||||
|
||||
val qrText = intent.getStringExtra(EXTRA_QR_TEXT)
|
||||
|
||||
val imageQR = findViewById<ImageView>(R.id.image_qr_fullscreen)
|
||||
val buttonBack = findViewById<ImageButton>(R.id.button_back_fullscreen)
|
||||
val buttonClose = findViewById<ImageButton>(R.id.button_close_fullscreen)
|
||||
|
||||
// Generate QR code bitmap from text
|
||||
qrText?.let { text ->
|
||||
try {
|
||||
if (!isContentSuitableForQRCode(text)) {
|
||||
throw Exception("Data too big for QR code generation")
|
||||
}
|
||||
|
||||
val dimension = TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP, 300f, resources.displayMetrics
|
||||
).toInt()
|
||||
val qrBitmap = generateQRCode(text, dimension, dimension)
|
||||
imageQR.setImageBitmap(qrBitmap)
|
||||
} catch (e: Exception) {
|
||||
// If QR generation fails, show error or fallback
|
||||
imageQR.setImageResource(R.drawable.ic_qr)
|
||||
}
|
||||
}
|
||||
|
||||
buttonBack.setOnClickListener {
|
||||
finish()
|
||||
}
|
||||
|
||||
buttonClose.setOnClickListener {
|
||||
finish()
|
||||
}
|
||||
|
||||
imageQR.setOnClickListener {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
private fun isContentSuitableForQRCode(content: String): Boolean {
|
||||
val bytes = content.toByteArray(Charsets.UTF_8)
|
||||
return bytes.size <= 2300 // QR Code Version 40 with Error Correction Level M can hold ~2331 bytes
|
||||
}
|
||||
|
||||
private fun generateQRCode(content: String, width: Int, height: Int): Bitmap {
|
||||
if (!isContentSuitableForQRCode(content)) {
|
||||
throw Exception("Data too big for QR code generation")
|
||||
}
|
||||
|
||||
val hints = java.util.EnumMap<EncodeHintType, Any>(EncodeHintType::class.java)
|
||||
hints[EncodeHintType.ERROR_CORRECTION] = ErrorCorrectionLevel.M
|
||||
hints[EncodeHintType.MARGIN] = 1
|
||||
|
||||
val bitMatrix = MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height, hints)
|
||||
return bitMatrixToBitmap(bitMatrix)
|
||||
}
|
||||
|
||||
private fun bitMatrixToBitmap(matrix: BitMatrix): Bitmap {
|
||||
val width = matrix.width
|
||||
val height = matrix.height
|
||||
val bmp = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565)
|
||||
|
||||
for (x in 0 until width) {
|
||||
for (y in 0 until height) {
|
||||
bmp.setPixel(x, y, if (matrix[x, y]) Color.BLACK else Color.WHITE)
|
||||
}
|
||||
}
|
||||
return bmp
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import com.futo.platformplayer.*
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
import com.futo.platformplayer.views.LoaderView
|
||||
import com.futo.platformplayer.views.fields.FieldForm
|
||||
import com.futo.platformplayer.views.fields.ReadOnlyTextField
|
||||
@@ -114,6 +115,11 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
|
||||
|
||||
var isFirstLoad = true;
|
||||
fun reloadSettings() {
|
||||
// Ensure files are initialized before accessing Settings
|
||||
if (!FragmentedStorage.isInitialized) {
|
||||
FragmentedStorage.initialize(filesDir);
|
||||
}
|
||||
|
||||
val firstLoad = isFirstLoad;
|
||||
isFirstLoad = false;
|
||||
_form.setSearchVisible(false);
|
||||
@@ -149,6 +155,11 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
|
||||
}
|
||||
|
||||
fun updateDevMode() {
|
||||
// Ensure files are initialized before accessing SettingsDev
|
||||
if (!FragmentedStorage.isInitialized) {
|
||||
FragmentedStorage.initialize(filesDir);
|
||||
}
|
||||
|
||||
if(SettingsDev.instance.developerMode)
|
||||
_devSets.visibility = View.VISIBLE;
|
||||
else
|
||||
|
||||
@@ -110,7 +110,7 @@ class SyncPairActivity : AppCompatActivity() {
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
StateSync.instance.syncService?.connect(deviceInfo, true) { complete, message ->
|
||||
StateSync.instance.syncService?.connect(deviceInfo) { complete, message ->
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
if (complete != null) {
|
||||
if (complete) {
|
||||
|
||||
@@ -33,7 +33,6 @@ 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
|
||||
@@ -1184,7 +1183,7 @@ abstract class StateCasting {
|
||||
onLoading?.invoke(true)
|
||||
}
|
||||
}
|
||||
deferred.awaitCancelConverted()
|
||||
deferred.await()
|
||||
} finally {
|
||||
if (castId == _castId.get()) {
|
||||
withContext(Dispatchers.Main) {
|
||||
|
||||
@@ -18,7 +18,6 @@ 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
|
||||
@@ -37,7 +36,6 @@ 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
|
||||
@@ -515,29 +513,18 @@ 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" -> 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);
|
||||
"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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(@Transient private val _package: PackageHttp, existingRequests: MutableList<Pair<PackageHttpClient, RequestDescriptor>> = mutableListOf()): V8BindObject() {
|
||||
class BatchBuilder(private val _package: PackageHttp, existingRequests: MutableList<Pair<PackageHttpClient, RequestDescriptor>> = mutableListOf()): V8BindObject() {
|
||||
@Transient
|
||||
private val _reqs = existingRequests;
|
||||
|
||||
|
||||
+3
-2
@@ -21,6 +21,7 @@ import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.activities.PolycentricHomeActivity
|
||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
|
||||
import com.futo.platformplayer.api.media.models.comments.LazyComment
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
@@ -170,8 +171,8 @@ class CommentsFragment : MainFragment() {
|
||||
return@showConfirmationDialog
|
||||
}
|
||||
|
||||
val index = _comments.indexOf(comment)
|
||||
if (index != -1) {
|
||||
val index = _comments.indexOfFirst { it == comment || (it is LazyComment && it.getUnderlyingComment() == comment) }
|
||||
if (index >= 0) {
|
||||
_comments.removeAt(index)
|
||||
_adapterComments.notifyItemRemoved(_adapterComments.childToParentPosition(index))
|
||||
|
||||
|
||||
+2
-2
@@ -3038,9 +3038,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, 2));
|
||||
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));
|
||||
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, 1));
|
||||
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));
|
||||
|
||||
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));
|
||||
|
||||
|
||||
@@ -66,9 +66,10 @@ class DownloadService : Service() {
|
||||
return START_NOT_STICKY;
|
||||
|
||||
if(!FragmentedStorage.isInitialized) {
|
||||
Logger.i(TAG, "Attempted to start DownloadService without initialized files")
|
||||
closeDownloadSession()
|
||||
return START_NOT_STICKY
|
||||
Logger.i(TAG, "Attempted to start DownloadService without initialized files");
|
||||
stopSelf()
|
||||
closeDownloadSession();
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
_started = true;
|
||||
}
|
||||
@@ -106,19 +107,12 @@ class DownloadService : Service() {
|
||||
return START_STICKY;
|
||||
}
|
||||
fun setupNotificationRequirements() {
|
||||
_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!!)
|
||||
_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!!);
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
@@ -299,28 +293,21 @@ class DownloadService : Service() {
|
||||
val notif = builder.build();
|
||||
notif.flags = notif.flags or NotificationCompat.FLAG_ONGOING_EVENT or NotificationCompat.FLAG_NO_CLEAR;
|
||||
|
||||
if (_isForeground) {
|
||||
_notificationManager?.notify(DOWNLOAD_NOTIF_ID, notif)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
startForeground(DOWNLOAD_NOTIF_ID, notif, FOREGROUND_SERVICE_TYPE_DATA_SYNC);
|
||||
} else {
|
||||
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
|
||||
startForeground(DOWNLOAD_NOTIF_ID, notif);
|
||||
}
|
||||
}
|
||||
|
||||
fun closeDownloadSession() {
|
||||
Logger.i(TAG, "closeDownloadSession")
|
||||
if (_isForeground) {
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
_isForeground = false
|
||||
}
|
||||
_notificationManager?.cancel(DOWNLOAD_NOTIF_ID)
|
||||
_started = false
|
||||
super.stopSelf()
|
||||
Logger.i(TAG, "closeDownloadSession");
|
||||
stopForeground(STOP_FOREGROUND_REMOVE);
|
||||
_notificationManager?.cancel(DOWNLOAD_NOTIF_ID);
|
||||
stopService();
|
||||
_started = false;
|
||||
super.stopSelf();
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
Logger.i(TAG, "onDestroy");
|
||||
_instance = null;
|
||||
|
||||
@@ -49,6 +49,7 @@ 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 kotlinx.coroutines.*
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
@@ -385,6 +386,26 @@ 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) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
ApiMethods.setModerationExemptSystemProvider {
|
||||
try {
|
||||
StatePolycentric.instance.processHandle?.system?.toProto()?.toByteArray()?.toBase64Url()
|
||||
} catch (e: Throwable) {
|
||||
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);
|
||||
|
||||
@@ -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 (runs in background)
|
||||
_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,7 +6,6 @@ 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
|
||||
@@ -18,23 +17,14 @@ 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
|
||||
@@ -74,7 +64,11 @@ class SyncService(
|
||||
private val database: ISyncDatabaseProvider,
|
||||
private val settings: SyncServiceSettings = SyncServiceSettings()
|
||||
) {
|
||||
private var _serverSocket: ServerSocketChannel? = null
|
||||
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 val _sessions: MutableMap<String, SyncSession> = mutableMapOf()
|
||||
private val _lastConnectTimesMdns: MutableMap<String, Long> = mutableMapOf()
|
||||
private val _lastConnectTimesIp: MutableMap<String, Long> = mutableMapOf()
|
||||
@@ -88,10 +82,10 @@ class SyncService(
|
||||
private val _pairingCode: String? = generateReadablePassword(8)
|
||||
val pairingCode: String? get() = _pairingCode
|
||||
private var _relaySession: SyncSocketSession? = null
|
||||
private val _remotePendingStatusUpdateRelayed = mutableMapOf<String, (complete: Boolean?, message: String) -> Unit>()
|
||||
private val _remotePendingStatusUpdateDirect = mutableMapOf<String, (complete: Boolean?, message: String) -> Unit>()
|
||||
private var _threadRelay: Thread? = null
|
||||
private val _remotePendingStatusUpdate = mutableMapOf<String, (complete: Boolean?, message: String) -> Unit>()
|
||||
private var _nsdManager: NsdManager? = null
|
||||
@Volatile private var _scope: CoroutineScope? = null
|
||||
private var _scope: CoroutineScope? = null
|
||||
private val _mdnsCache = mutableMapOf<String, SyncDeviceInfo>()
|
||||
private var _discoveryListener: NsdManager.DiscoveryListener = object : NsdManager.DiscoveryListener {
|
||||
override fun onDiscoveryStarted(regType: String) {
|
||||
@@ -222,12 +216,11 @@ class SyncService(
|
||||
var authorizePrompt: ((String, (Boolean) -> Unit) -> Unit)? = null
|
||||
|
||||
fun start(context: Context) {
|
||||
if (_scope != null) {
|
||||
Log.i(TAG, "Already started.")
|
||||
if (_started) {
|
||||
Logger.i(TAG, "Already started.")
|
||||
return
|
||||
}
|
||||
|
||||
Log.i(TAG, "Start SyncService.")
|
||||
_started = true
|
||||
_scope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
try {
|
||||
@@ -301,30 +294,27 @@ class SyncService(
|
||||
private fun startListener() {
|
||||
serverSocketFailedToStart = false
|
||||
serverSocketStarted = false
|
||||
_scope?.launch(Dispatchers.IO) {
|
||||
_thread = Thread {
|
||||
try {
|
||||
val serverSocket = ServerSocketChannel.open()
|
||||
serverSocket.socket().bind(InetSocketAddress("0.0.0.0", settings.listenerPort))
|
||||
val serverSocket = ServerSocket(settings.listenerPort)
|
||||
_serverSocket = serverSocket
|
||||
|
||||
Log.i(TAG, "Running on port ${settings.listenerPort} (TCP)")
|
||||
serverSocketStarted = true
|
||||
Log.i(TAG, "Running on port ${settings.listenerPort} (TCP)")
|
||||
|
||||
while (isActive) {
|
||||
while (_started) {
|
||||
val socket = serverSocket.accept()
|
||||
//TODO: Switch to SocketChannel?
|
||||
val session = createSocketSession(socket.socket(), true)
|
||||
val session = createSocketSession(socket, true)
|
||||
session.startAsResponder()
|
||||
}
|
||||
} catch (e: ClosedChannelException) {
|
||||
// normal shutdown
|
||||
|
||||
serverSocketStarted = false
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, "Failed to bind server socket to port ${settings.listenerPort}", e)
|
||||
Logger.e(TAG, "Failed to bind server socket to port ${settings.listenerPort}", e)
|
||||
serverSocketFailedToStart = true
|
||||
} finally {
|
||||
serverSocketStarted = false
|
||||
}
|
||||
}
|
||||
}.apply { start() }
|
||||
}
|
||||
|
||||
private fun startMdnsRetryLoop() {
|
||||
@@ -332,44 +322,43 @@ class SyncService(
|
||||
discoverServices(serviceName, NsdManager.PROTOCOL_DNS_SD, _discoveryListener)
|
||||
}
|
||||
|
||||
_scope?.launch(Dispatchers.IO) {
|
||||
while (isActive) {
|
||||
_mdnsThread = Thread {
|
||||
while (_started) {
|
||||
try {
|
||||
val now = System.currentTimeMillis()
|
||||
val pairs = synchronized (_mdnsCache) { _mdnsCache.toList() }
|
||||
for ((pkey, info) in pairs) {
|
||||
if (!database.isAuthorized(pkey) || getLinkType(pkey) == LinkType.Direct) continue
|
||||
synchronized(_mdnsCache) {
|
||||
for ((pkey, info) in _mdnsCache) {
|
||||
if (!database.isAuthorized(pkey) || getLinkType(pkey) == LinkType.Direct) continue
|
||||
|
||||
val last = synchronized(_lastConnectTimesMdns) {
|
||||
_lastConnectTimesMdns[pkey] ?: 0L
|
||||
}
|
||||
if (now - last > 30_000L) {
|
||||
synchronized(_lastConnectTimesMdns) {
|
||||
_lastConnectTimesMdns[pkey] = now
|
||||
val last = synchronized(_lastConnectTimesMdns) {
|
||||
_lastConnectTimesMdns[pkey] ?: 0L
|
||||
}
|
||||
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)
|
||||
if (now - last > 30_000L) {
|
||||
_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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (ex: Throwable) {
|
||||
Log.e(TAG, "Error in MDNS retry loop", ex)
|
||||
Logger.e(TAG, "Error in MDNS retry loop", ex)
|
||||
}
|
||||
delay(5000)
|
||||
Thread.sleep(5000)
|
||||
}
|
||||
}
|
||||
}.apply { start() }
|
||||
}
|
||||
|
||||
|
||||
private fun startConnectLastLoop() {
|
||||
_scope?.launch(Dispatchers.IO) {
|
||||
_connectThread = Thread {
|
||||
Log.i(TAG, "Running auto reconnector")
|
||||
|
||||
while (isActive) {
|
||||
val authorizedDevices = database.getAllAuthorizedDevices()?.toList() ?: listOf()
|
||||
while (_started) {
|
||||
val authorizedDevices = database.getAllAuthorizedDevices() ?: arrayOf()
|
||||
val addressesToConnect = authorizedDevices.mapNotNull {
|
||||
val connectedDirectly = getLinkType(it) == LinkType.Direct
|
||||
if (connectedDirectly) {
|
||||
@@ -393,26 +382,26 @@ class SyncService(
|
||||
_lastConnectTimesIp[connectPair.first] = now
|
||||
}
|
||||
|
||||
Log.i(TAG, "Attempting to connect to authorized device by last known IP '${connectPair.first}' with pkey=${connectPair.first}")
|
||||
Logger.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) {
|
||||
Log.i(TAG, "Failed to connect to " + connectPair.first, e)
|
||||
Logger.i(TAG, "Failed to connect to " + connectPair.first, e)
|
||||
}
|
||||
}
|
||||
delay(5000)
|
||||
Thread.sleep(5000)
|
||||
}
|
||||
}
|
||||
}.apply { start() }
|
||||
}
|
||||
|
||||
private fun startRelayLoop() {
|
||||
relayConnected = false
|
||||
_scope?.launch(Dispatchers.IO) {
|
||||
_threadRelay = Thread {
|
||||
try {
|
||||
var backoffs: Array<Long> = arrayOf(1000, 5000, 10000, 20000)
|
||||
var backoffIndex = 0;
|
||||
|
||||
while (isActive) {
|
||||
while (_started) {
|
||||
try {
|
||||
Log.i(TAG, "Starting relay session...")
|
||||
relayConnected = false
|
||||
@@ -476,7 +465,7 @@ class SyncService(
|
||||
|
||||
Thread {
|
||||
try {
|
||||
while (isActive && !socketClosed) {
|
||||
while (_started && !socketClosed) {
|
||||
val unconnectedAuthorizedDevices =
|
||||
database.getAllAuthorizedDevices()
|
||||
?.filter {
|
||||
@@ -514,14 +503,27 @@ class SyncService(
|
||||
connectionInfo.ipv4Addresses
|
||||
.filter { it != connectionInfo.remoteIp }
|
||||
if (getLinkType(targetKey) != LinkType.Direct && connectionInfo.allowLocalDirect && Settings.instance.synchronization.connectLocalDirectThroughRelay) {
|
||||
launch(Dispatchers.IO) {
|
||||
Thread {
|
||||
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) {
|
||||
@@ -585,7 +587,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 {
|
||||
@@ -697,21 +699,14 @@ 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 ->
|
||||
sendRemotePendingStatusUpdate(remotePublicKey, true, "Authorized")
|
||||
synchronized(_remotePendingStatusUpdate) {
|
||||
_remotePendingStatusUpdate.remove(remotePublicKey)?.invoke(true, "Authorized")
|
||||
}
|
||||
|
||||
if (isNewSession) {
|
||||
it.remoteDeviceName?.let { remoteDeviceName ->
|
||||
@@ -724,7 +719,10 @@ class SyncService(
|
||||
onAuthorized?.invoke(it, isNewlyAuthorized, isNewSession)
|
||||
},
|
||||
onUnauthorized = {
|
||||
sendRemotePendingStatusUpdate(remotePublicKey, false, "Unauthorized")
|
||||
synchronized(_remotePendingStatusUpdate) {
|
||||
_remotePendingStatusUpdate.remove(remotePublicKey)?.invoke(false, "Unauthorized")
|
||||
}
|
||||
|
||||
onUnauthorized?.invoke(it)
|
||||
},
|
||||
onConnectedChanged = { it, connected ->
|
||||
@@ -735,7 +733,9 @@ class SyncService(
|
||||
Logger.i(TAG, "$remotePublicKey closed")
|
||||
|
||||
removeSession(it.remotePublicKey)
|
||||
sendRemotePendingStatusUpdate(remotePublicKey, false, "Connection closed")
|
||||
synchronized(_remotePendingStatusUpdate) {
|
||||
_remotePendingStatusUpdate.remove(remotePublicKey)?.invoke(false, "Connection closed")
|
||||
}
|
||||
|
||||
onClose?.invoke(it)
|
||||
},
|
||||
@@ -757,67 +757,42 @@ class SyncService(
|
||||
fun getAllAuthorizedDevices(): Array<String>? = database.getAllAuthorizedDevices()
|
||||
fun removeAuthorizedDevice(publicKey: String) = database.removeAuthorizedDevice(publicKey)
|
||||
|
||||
|
||||
suspend fun connect(deviceInfo: SyncDeviceInfo, alsoTryRelayed: Boolean = false, timeout_ms: Int = 5_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
|
||||
}
|
||||
}
|
||||
//TODO: Do not try relayed channel here only for pairing mode?
|
||||
rs.startRelayedChannel(deviceInfo.publicKey.base64ToByteArray().toBase64(), appId, deviceInfo.pairingCode)
|
||||
}
|
||||
|
||||
fun connect(deviceInfo: SyncDeviceInfo, onStatusUpdate: ((complete: Boolean?, message: String) -> Unit)? = null) {
|
||||
try {
|
||||
connect(deviceInfo.addresses, deviceInfo.port, deviceInfo.publicKey, deviceInfo.pairingCode, onStatusUpdate, timeout_ms)
|
||||
connect(deviceInfo.addresses, deviceInfo.port, deviceInfo.publicKey, deviceInfo.pairingCode, onStatusUpdate)
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, "Failed to connect directly", e)
|
||||
Logger.e(TAG, "Failed to connect directly", e)
|
||||
val relaySession = _relaySession
|
||||
if (relaySession != null && Settings.instance.synchronization.pairThroughRelay) {
|
||||
onStatusUpdate?.invoke(null, "Connecting via relay...")
|
||||
|
||||
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())
|
||||
runBlocking {
|
||||
if (onStatusUpdate != null) {
|
||||
synchronized(_remotePendingStatusUpdate) {
|
||||
_remotePendingStatusUpdate[deviceInfo.publicKey.base64ToByteArray().toBase64()] = onStatusUpdate
|
||||
}
|
||||
}
|
||||
relaySession.startRelayedChannel(deviceInfo.publicKey.base64ToByteArray().toBase64(), appId, deviceInfo.pairingCode)
|
||||
}
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
fun connect(addresses: Array<String>, port: Int, publicKey: String, pairingCode: String?, onStatusUpdate: ((complete: Boolean?, message: String) -> Unit)? = null): SyncSocketSession {
|
||||
onStatusUpdate?.invoke(null, "Connecting directly...")
|
||||
val socket = getConnectedSocket(addresses.map { InetAddress.getByName(it) }, port, timeout_ms) ?: throw Exception("Failed to connect")
|
||||
val socket = getConnectedSocket(addresses.map { InetAddress.getByName(it) }, port) ?: throw Exception("Failed to connect")
|
||||
onStatusUpdate?.invoke(null, "Handshaking...")
|
||||
|
||||
val session = createSocketSession(socket, false)
|
||||
if (onStatusUpdate != null) {
|
||||
synchronized(_remotePendingStatusUpdateDirect) {
|
||||
_remotePendingStatusUpdateDirect[publicKey.base64ToByteArray().toBase64()] = onStatusUpdate
|
||||
synchronized(_remotePendingStatusUpdate) {
|
||||
_remotePendingStatusUpdate[publicKey.base64ToByteArray().toBase64()] = onStatusUpdate
|
||||
}
|
||||
}
|
||||
|
||||
session.startAsInitiator(publicKey, appId, pairingCode)
|
||||
|
||||
while (timeout_ms - (startTime_ms - System.currentTimeMillis()) > 0 && !session.isAuthorized && session.started) {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -836,8 +811,6 @@ class SyncService(
|
||||
synchronized(_sessions) {
|
||||
_sessions.clear()
|
||||
}
|
||||
_remotePendingStatusUpdateDirect.clear()
|
||||
_remotePendingStatusUpdateRelayed.clear()
|
||||
}
|
||||
|
||||
private fun getDeviceName(): String {
|
||||
|
||||
@@ -56,7 +56,6 @@ 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,25 +0,0 @@
|
||||
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"
|
||||
}
|
||||
}
|
||||
@@ -161,8 +161,8 @@ class CommentsList : ConstraintLayout {
|
||||
return@showConfirmationDialog
|
||||
}
|
||||
|
||||
val index = _comments.indexOf(comment)
|
||||
if (index != -1) {
|
||||
val index = _comments.indexOfFirst { it == comment || (it is LazyComment && it.getUnderlyingComment() == comment) }
|
||||
if (index >= 0) {
|
||||
_comments.removeAt(index)
|
||||
_adapterComments.notifyItemRemoved(_adapterComments.childToParentPosition(index))
|
||||
|
||||
|
||||
@@ -69,7 +69,6 @@ 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
|
||||
@@ -644,7 +643,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
setLoading(true)
|
||||
}
|
||||
}
|
||||
val generated = generatedDef.awaitCancelConverted();
|
||||
val generated = generatedDef.await();
|
||||
if (_swapIdVideo.get() != swapId) {
|
||||
return@launch
|
||||
}
|
||||
@@ -808,7 +807,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
setLoading(true)
|
||||
}
|
||||
}
|
||||
val generated = generatedDef.awaitCancelConverted();
|
||||
val generated = generatedDef.await();
|
||||
if (_swapIdAudio.get() != swapId) {
|
||||
return@launch
|
||||
}
|
||||
|
||||
@@ -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,6 +15,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingTop="20dp"
|
||||
android:paddingBottom="15dp">
|
||||
|
||||
<ImageButton
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingTop="20dp"
|
||||
android:paddingBottom="15dp">
|
||||
|
||||
<ImageButton
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingTop="20dp"
|
||||
android:paddingBottom="15dp">
|
||||
|
||||
<ImageButton
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
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,6 +14,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingTop="20dp"
|
||||
android:paddingBottom="15dp">
|
||||
|
||||
<ImageButton
|
||||
|
||||
@@ -29,6 +29,8 @@
|
||||
android:id="@+id/image_qr"
|
||||
android:layout_width="200dp"
|
||||
android:layout_height="200dp"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:padding="8dp"
|
||||
app:srcCompat="@drawable/ic_qr"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
@@ -37,15 +39,31 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_qr"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/scan_to_import"
|
||||
android:fontFamily="@font/inter_light"
|
||||
android:textSize="32dp"
|
||||
android:textAlignment="center"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/image_qr"
|
||||
app:layout_constraintLeft_toLeftOf="@id/image_qr"
|
||||
app:layout_constraintRight_toRightOf="@id/image_qr" />
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_qr_hint"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/tap_qr_code_for_fullscreen"
|
||||
android:fontFamily="@font/inter_light"
|
||||
android:textSize="14dp"
|
||||
android:textColor="@color/gray_400"
|
||||
android:layout_marginTop="8dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_qr"
|
||||
app:layout_constraintLeft_toLeftOf="@id/text_qr"
|
||||
app:layout_constraintRight_toRightOf="@id/text_qr" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_buttons"
|
||||
@@ -55,7 +73,7 @@
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:layout_marginTop="30dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_qr"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_qr_hint"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent">
|
||||
|
||||
@@ -75,6 +93,15 @@
|
||||
app:buttonSubText="@string/copy_your_identity_to_clipboard"
|
||||
app:buttonIcon="@drawable/ic_copy"
|
||||
android:layout_marginTop="8dp" />
|
||||
|
||||
<com.futo.platformplayer.views.buttons.BigButton
|
||||
android:id="@+id/button_export_file"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:buttonText="@string/export_to_file"
|
||||
app:buttonSubText="@string/save_profile_to_file_for_sharing"
|
||||
app:buttonIcon="@drawable/ic_download"
|
||||
android:layout_marginTop="8dp" />
|
||||
</LinearLayout>
|
||||
|
||||
<ProgressBar
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
android:id="@+id/layout_buttons"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="20dp"
|
||||
android:paddingBottom="20dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
|
||||
@@ -47,6 +47,28 @@
|
||||
android:text="@string/scan_qr" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/button_import_file"
|
||||
android:layout_width="140dp"
|
||||
android:layout_height="40dp"
|
||||
android:background="@drawable/background_button_primary_round"
|
||||
android:gravity="center"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/button_scan_profile">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="@font/inter_light"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16dp"
|
||||
android:text="@string/import_from_file" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_or"
|
||||
android:layout_width="wrap_content"
|
||||
@@ -55,7 +77,7 @@
|
||||
android:fontFamily="@font/inter_light"
|
||||
android:textSize="28dp"
|
||||
android:layout_marginTop="30dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/button_scan_profile"
|
||||
app:layout_constraintTop_toBottomOf="@id/button_import_file"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent" />
|
||||
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
<?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">
|
||||
|
||||
<!-- BEGIN BLURB -->
|
||||
<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" />
|
||||
<!-- END BLURB -->
|
||||
|
||||
<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/bg_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/bg_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/bg_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"
|
||||
@@ -168,6 +168,7 @@
|
||||
app:buttonIcon="@drawable/ic_trash"
|
||||
android:layout_marginTop="8dp"
|
||||
app:buttonBackground="@drawable/background_big_button_red"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.futo.platformplayer.views.overlays.LoaderOverlay
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
<?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"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/black">
|
||||
|
||||
<!-- Top navigation bar -->
|
||||
<ImageButton
|
||||
android:id="@+id/button_back_fullscreen"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
android:contentDescription="@string/cd_button_back"
|
||||
android:padding="10dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
app:srcCompat="@drawable/ic_back_thin_white_16dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginStart="16dp" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_close_fullscreen"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
android:contentDescription="@string/cd_button_close"
|
||||
android:padding="10dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
app:srcCompat="@drawable/ic_close"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp" />
|
||||
|
||||
<!-- Full screen QR code -->
|
||||
<ImageView
|
||||
android:id="@+id/image_qr_fullscreen"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:padding="40dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/button_back_fullscreen"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginBottom="20dp"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginEnd="20dp" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -17,6 +17,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingTop="20dp"
|
||||
android:paddingBottom="15dp">
|
||||
|
||||
<ImageButton
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
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,6 +5,7 @@
|
||||
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,6 +5,7 @@
|
||||
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,6 +5,7 @@
|
||||
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">
|
||||
|
||||
<com.futo.platformplayer.views.behavior.SafeTextView
|
||||
<TextView
|
||||
android:id="@+id/text_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
||||
@@ -404,6 +404,8 @@
|
||||
<string name="unknown_reconstruction_type">Unbekannter Rekonstruktionstyp</string>
|
||||
<string name="failed_to_parse_newpipe_subscriptions">Fehler beim Parsen von NewPipe-Abonnements</string>
|
||||
<string name="failed_to_generate_qr_code">Fehler beim Generieren des QR-Codes</string>
|
||||
<string name="qr_code_too_large_use_text_below">QR-Code zu groß. Verwenden Sie den Text unten, um Ihr Profil zu teilen.</string>
|
||||
<string name="tap_qr_code_for_fullscreen">Tippen Sie auf den QR-Code für Vollbildansicht</string>
|
||||
<string name="share_text">Text teilen</string>
|
||||
<string name="copied_text">Text kopiert</string>
|
||||
<string name="must_be_at_least_3_characters_long">Muss mindestens 3 Zeichen lang sein.</string>
|
||||
|
||||
@@ -381,6 +381,8 @@
|
||||
<string name="unknown_reconstruction_type">Tipo de reconstrucción desconocido</string>
|
||||
<string name="failed_to_parse_newpipe_subscriptions">Error al analizar las suscripciones de NewPipe</string>
|
||||
<string name="failed_to_generate_qr_code">Error al generar el código QR</string>
|
||||
<string name="qr_code_too_large_use_text_below">Código QR demasiado grande. Use el texto de abajo para compartir su perfil.</string>
|
||||
<string name="tap_qr_code_for_fullscreen">Toca el código QR para vista completa</string>
|
||||
<string name="share_text">Compartir texto</string>
|
||||
<string name="copied_text">Texto copiado</string>
|
||||
<string name="must_be_at_least_3_characters_long">Debe tener al menos 3 caracteres de longitud.</string>
|
||||
|
||||
@@ -420,6 +420,8 @@
|
||||
<string name="unknown_reconstruction_type">Type de reconstruction inconnu</string>
|
||||
<string name="failed_to_parse_newpipe_subscriptions">Échec de l\'analyse des abonnements NewPipe</string>
|
||||
<string name="failed_to_generate_qr_code">Échec de la génération du code QR</string>
|
||||
<string name="qr_code_too_large_use_text_below">Code QR trop volumineux. Utilisez le texte ci-dessous pour partager votre profil.</string>
|
||||
<string name="tap_qr_code_for_fullscreen">Appuyez sur le code QR pour la vue plein écran</string>
|
||||
<string name="share_text">Partager le texte</string>
|
||||
<string name="copied_text">Texte copié</string>
|
||||
<string name="must_be_at_least_3_characters_long">Doit comporter au moins 3 caractères.</string>
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
<?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>
|
||||
@@ -653,6 +653,18 @@
|
||||
<string name="failed_to_parse_text_file">Failed to parse text file</string>
|
||||
<string name="failed_to_parse_newpipe_subscriptions">Failed to parse NewPipe Subscriptions</string>
|
||||
<string name="failed_to_generate_qr_code">Failed to generate QR code</string>
|
||||
<string name="qr_code_too_large_use_file_export">QR code too large. Use the file export button below to share your profile.</string>
|
||||
<string name="tap_qr_code_for_fullscreen">Tap QR code for fullscreen view</string>
|
||||
<string name="export_to_file">Export to File</string>
|
||||
<string name="save_profile_to_file_for_sharing">Save profile to file for sharing</string>
|
||||
<string name="import_from_file">Import from File</string>
|
||||
<string name="export_profile">Export Profile</string>
|
||||
<string name="choose_export_option">Choose export option</string>
|
||||
<string name="save_to_device">Save to Device</string>
|
||||
<string name="share_profile">Share Profile</string>
|
||||
<string name="profile_saved_to_downloads">Profile saved to Downloads</string>
|
||||
<string name="failed_to_save_profile">Failed to save profile</string>
|
||||
<string name="authority">com.futo.platformplayer.fileprovider</string>
|
||||
<string name="share_text">Share Text</string>
|
||||
<string name="copied_text">Copied Text</string>
|
||||
<string name="must_be_at_least_3_characters_long">Must be at least 3 characters long.</string>
|
||||
|
||||
@@ -42,8 +42,7 @@
|
||||
<item name="colorSecondaryVariant">@color/gray_30</item>
|
||||
<item name="colorOnSecondary">@color/black</item>
|
||||
|
||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||
<item name="android:statusBarColor">@color/black</item>
|
||||
|
||||
<item name="imageButtonStyle">@style/Theme.FutoVideo.ImageButton</item>
|
||||
<item name="editTextStyle">@style/Theme.FutoVideo.EditText</item>
|
||||
@@ -92,16 +91,6 @@
|
||||
<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">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<files-path name="shares" path="shares/"/>
|
||||
<files-path name="files" path="." />
|
||||
<external-path name="external_files" path="." />
|
||||
</paths>
|
||||
Submodule app/src/stable/assets/sources/apple-podcasts updated: 8cff240ca7...089987f007
Submodule app/src/stable/assets/sources/bilibili updated: f636e9713d...1222638042
Submodule app/src/stable/assets/sources/dailymotion updated: 850eb8122d...d115430011
Submodule app/src/stable/assets/sources/kick updated: 4ff0b02700...b7173f1538
Submodule app/src/stable/assets/sources/odysee updated: 89ad7e9a4b...6ea9fa7e4c
Submodule app/src/stable/assets/sources/patreon updated: 6880b30b71...b811f8bdfb
Submodule app/src/stable/assets/sources/peertube updated: 21dcf4bef5...56bff39123
Submodule app/src/stable/assets/sources/rumble updated: 3a7087ccb0...401274b1ec
Submodule app/src/stable/assets/sources/soundcloud updated: 49db9e3e15...048acef152
Submodule app/src/stable/assets/sources/spotify updated: 0b50c2e61b...214ac1dfcc
Submodule app/src/stable/assets/sources/twitch updated: 8de3ab18f5...08346f9177
Submodule app/src/stable/assets/sources/youtube updated: 07101fbc34...48d98c1f0c
Submodule app/src/unstable/assets/sources/apple-podcasts updated: 8cff240ca7...089987f007
Submodule app/src/unstable/assets/sources/bilibili updated: f636e9713d...1222638042
Submodule app/src/unstable/assets/sources/dailymotion updated: 850eb8122d...d115430011
Submodule app/src/unstable/assets/sources/kick updated: 4ff0b02700...b7173f1538
Submodule app/src/unstable/assets/sources/odysee updated: 89ad7e9a4b...6ea9fa7e4c
Submodule app/src/unstable/assets/sources/patreon updated: 6880b30b71...b811f8bdfb
Submodule app/src/unstable/assets/sources/peertube updated: 21dcf4bef5...56bff39123
Submodule app/src/unstable/assets/sources/rumble updated: 3a7087ccb0...401274b1ec
Submodule app/src/unstable/assets/sources/soundcloud updated: 49db9e3e15...048acef152
Submodule app/src/unstable/assets/sources/spotify updated: 0b50c2e61b...214ac1dfcc
Submodule app/src/unstable/assets/sources/twitch updated: 8de3ab18f5...08346f9177
Submodule app/src/unstable/assets/sources/youtube updated: 07101fbc34...48d98c1f0c
+3
-1
@@ -6,7 +6,9 @@
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
org.gradle.jvmargs=-Xmx4096m -Dfile.encoding=UTF-8
|
||||
# Kotlin daemon memory settings
|
||||
kotlin.daemon.jvmargs=-Xmx4096m
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
|
||||
Reference in New Issue
Block a user