mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-27 18:25:21 +02:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f25c76687e |
@@ -36,12 +36,6 @@
|
||||
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
|
||||
<receiver android:name=".receivers.MediaButtonReceiver" android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<service android:name=".services.MediaPlaybackService"
|
||||
android:enabled="true"
|
||||
android:foregroundServiceType="mediaPlayback" />
|
||||
@@ -58,7 +52,7 @@
|
||||
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar"
|
||||
android:launchMode="singleInstance"
|
||||
android:launchMode="singleTask"
|
||||
android:resizeableActivity="true"
|
||||
android:supportsPictureInPicture="true">
|
||||
|
||||
|
||||
@@ -367,16 +367,6 @@ class VideoUrlSource {
|
||||
this.requestModifier = obj.requestModifier;
|
||||
}
|
||||
}
|
||||
class VideoUrlWidevineSource extends VideoUrlSource {
|
||||
constructor(obj) {
|
||||
super(obj);
|
||||
this.plugin_type = "VideoUrlWidevineSource";
|
||||
|
||||
this.licenseUri = obj.licenseUri;
|
||||
if(obj.getLicenseRequestExecutor)
|
||||
this.getLicenseRequestExecutor = obj.getLicenseRequestExecutor;
|
||||
}
|
||||
}
|
||||
class VideoUrlRangeSource extends VideoUrlSource {
|
||||
constructor(obj) {
|
||||
super(obj);
|
||||
@@ -409,26 +399,8 @@ class AudioUrlWidevineSource extends AudioUrlSource {
|
||||
super(obj);
|
||||
this.plugin_type = "AudioUrlWidevineSource";
|
||||
|
||||
this.bearerToken = obj.bearerToken;
|
||||
this.licenseUri = obj.licenseUri;
|
||||
if(obj.getLicenseRequestExecutor)
|
||||
this.getLicenseRequestExecutor = obj.getLicenseRequestExecutor;
|
||||
|
||||
// deprecated api conversion
|
||||
if(obj.bearerToken) {
|
||||
this.getLicenseRequestExecutor = () => {
|
||||
return {
|
||||
executeRequest: (url, _headers, _method, license_request_data) => {
|
||||
return http.POST(
|
||||
url,
|
||||
license_request_data,
|
||||
{ Authorization: `Bearer ${obj.bearerToken}` },
|
||||
false,
|
||||
true
|
||||
).body
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
class AudioUrlRangeSource extends AudioUrlSource {
|
||||
@@ -471,16 +443,6 @@ class DashSource {
|
||||
this.requestModifier = obj.requestModifier;
|
||||
}
|
||||
}
|
||||
class DashWidevineSource extends DashSource {
|
||||
constructor(obj) {
|
||||
super(obj);
|
||||
this.plugin_type = "DashWidevineSource";
|
||||
|
||||
this.licenseUri = obj.licenseUri;
|
||||
if(obj.getLicenseRequestExecutor)
|
||||
this.getLicenseRequestExecutor = obj.getLicenseRequestExecutor;
|
||||
}
|
||||
}
|
||||
class DashManifestRawSource {
|
||||
constructor(obj) {
|
||||
obj = obj ?? {};
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.hardware.Sensor
|
||||
import android.hardware.SensorEvent
|
||||
import android.hardware.SensorEventListener
|
||||
import android.hardware.SensorManager
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
class AdvancedOrientationListener(private val activity: Activity, private val lifecycleScope: CoroutineScope) {
|
||||
private val sensorManager: SensorManager = activity.getSystemService(Context.SENSOR_SERVICE) as SensorManager
|
||||
private val accelerometer: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
|
||||
private val magnetometer: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)
|
||||
|
||||
private var lastOrientation: Int = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||
private var lastStableOrientation: Int = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||
private var lastOrientationChangeTime = 0L
|
||||
private val debounceTime = 200L
|
||||
private val stabilityThresholdTime = 800L
|
||||
private var deviceAspectRatio: Float = 1.0f
|
||||
|
||||
private val gravity = FloatArray(3)
|
||||
private val geomagnetic = FloatArray(3)
|
||||
private val rotationMatrix = FloatArray(9)
|
||||
private val orientationAngles = FloatArray(3)
|
||||
|
||||
val onOrientationChanged = Event1<Int>()
|
||||
|
||||
private val sensorListener = object : SensorEventListener {
|
||||
override fun onSensorChanged(event: SensorEvent) {
|
||||
when (event.sensor.type) {
|
||||
Sensor.TYPE_ACCELEROMETER -> {
|
||||
System.arraycopy(event.values, 0, gravity, 0, gravity.size)
|
||||
}
|
||||
Sensor.TYPE_MAGNETIC_FIELD -> {
|
||||
System.arraycopy(event.values, 0, geomagnetic, 0, geomagnetic.size)
|
||||
}
|
||||
}
|
||||
|
||||
if (gravity.isNotEmpty() && geomagnetic.isNotEmpty()) {
|
||||
val success = SensorManager.getRotationMatrix(rotationMatrix, null, gravity, geomagnetic)
|
||||
if (success) {
|
||||
SensorManager.getOrientation(rotationMatrix, orientationAngles)
|
||||
|
||||
val azimuth = Math.toDegrees(orientationAngles[0].toDouble()).toFloat()
|
||||
val pitch = Math.toDegrees(orientationAngles[1].toDouble()).toFloat()
|
||||
val roll = Math.toDegrees(orientationAngles[2].toDouble()).toFloat()
|
||||
|
||||
val newOrientation = when {
|
||||
roll in -155f .. -15f && isWithinThreshold(pitch, 0f, 30.0) -> {
|
||||
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||
}
|
||||
roll in 15f .. 155f && isWithinThreshold(pitch, 0f, 30.0) -> {
|
||||
ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
|
||||
}
|
||||
isWithinThreshold(pitch, -90f, 30.0 * deviceAspectRatio) && roll in -15f .. 15f -> {
|
||||
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
}
|
||||
isWithinThreshold(pitch, 90f, 30.0 * deviceAspectRatio) && roll in -15f .. 15f -> {
|
||||
ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT
|
||||
}
|
||||
else -> lastOrientation
|
||||
}
|
||||
|
||||
//Logger.i("AdvancedOrientationListener", "newOrientation = ${newOrientation}, roll = ${roll}, pitch = ${pitch}, azimuth = ${azimuth}")
|
||||
|
||||
if (newOrientation != lastStableOrientation) {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
if (currentTime - lastOrientationChangeTime > debounceTime) {
|
||||
lastOrientationChangeTime = currentTime
|
||||
lastStableOrientation = newOrientation
|
||||
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
delay(stabilityThresholdTime)
|
||||
if (newOrientation == lastStableOrientation) {
|
||||
lastOrientation = newOrientation
|
||||
onOrientationChanged.emit(newOrientation)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Logger.i(TAG, "Failed to trigger onOrientationChanged", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
|
||||
}
|
||||
|
||||
private fun isWithinThreshold(value: Float, target: Float, threshold: Double): Boolean {
|
||||
return Math.abs(value - target) <= threshold
|
||||
}
|
||||
|
||||
init {
|
||||
sensorManager.registerListener(sensorListener, accelerometer, SensorManager.SENSOR_DELAY_GAME)
|
||||
sensorManager.registerListener(sensorListener, magnetometer, SensorManager.SENSOR_DELAY_GAME)
|
||||
|
||||
val metrics = activity.resources.displayMetrics
|
||||
deviceAspectRatio = (metrics.heightPixels.toFloat() / metrics.widthPixels.toFloat())
|
||||
if (deviceAspectRatio == 0.0f)
|
||||
deviceAspectRatio = 1.0f
|
||||
|
||||
lastOrientation = activity.resources.configuration.orientation
|
||||
}
|
||||
|
||||
fun stopListening() {
|
||||
sensorManager.unregisterListener(sensorListener)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = "AdvancedOrientationListener"
|
||||
}
|
||||
}
|
||||
@@ -412,13 +412,15 @@ class Settings : FragmentedStorageFileJson() {
|
||||
var preferredPreviewQuality: Int = 5;
|
||||
fun getPreferredPreviewQualityPixelCount(): Int = preferedQualityToPixels(preferredPreviewQuality);
|
||||
|
||||
|
||||
@FormField(R.string.simplify_sources, FieldForm.TOGGLE, R.string.simplify_sources_description, 4)
|
||||
var simplifySources: Boolean = true;
|
||||
|
||||
@FormField(R.string.force_enable_auto_rotate_in_full_screen, FieldForm.TOGGLE, R.string.force_enable_auto_rotate_in_full_screen_description, 5)
|
||||
var forceAllowFullScreenRotation: Boolean = false
|
||||
@FormField(R.string.auto_rotate, FieldForm.DROPDOWN, -1, 5)
|
||||
@DropdownFieldOptionsId(R.array.system_enabled_disabled_array)
|
||||
var autoRotate: Int = 2;
|
||||
|
||||
@FormField(R.string.background_behavior, FieldForm.DROPDOWN, -1, 6)
|
||||
@FormField(R.string.background_behavior, FieldForm.DROPDOWN, -1, 7)
|
||||
@DropdownFieldOptionsId(R.array.player_background_behavior)
|
||||
var backgroundPlay: Int = 2;
|
||||
|
||||
@@ -864,9 +866,6 @@ class Settings : FragmentedStorageFileJson() {
|
||||
|
||||
@FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 3)
|
||||
var polycentricEnabled: Boolean = true;
|
||||
|
||||
@FormField(R.string.polycentric_local_cache, FieldForm.TOGGLE, R.string.polycentric_local_cache_description, 4)
|
||||
var polycentricLocalCache: Boolean = true;
|
||||
}
|
||||
|
||||
@FormField(R.string.gesture_controls, FieldForm.GROUP, -1, 19)
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
package com.futo.platformplayer.activities
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.media.AudioManager
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.StrictMode
|
||||
@@ -74,7 +72,6 @@ import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.ImportCache
|
||||
import com.futo.platformplayer.models.UrlVideoWithTime
|
||||
import com.futo.platformplayer.receivers.MediaButtonReceiver
|
||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateBackup
|
||||
@@ -110,7 +107,6 @@ import java.util.LinkedList
|
||||
import java.util.Queue
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
|
||||
|
||||
class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
|
||||
//TODO: Move to dimensions
|
||||
@@ -838,7 +834,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
} else if (StatePlatform.instance.hasEnabledPlaylistClient(url)) {
|
||||
Logger.i(TAG, "handleUrl(url=$url) found playlist client");
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
navigate(_fragMainRemotePlaylist, url);
|
||||
navigate(_fragMainPlaylist, url);
|
||||
delay(100);
|
||||
_fragVideoDetail.minimizeVideoDetail();
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ import java.io.BufferedInputStream
|
||||
import java.io.OutputStream
|
||||
import java.lang.reflect.Field
|
||||
import java.lang.reflect.Method
|
||||
import java.net.BindException
|
||||
import java.net.InetAddress
|
||||
import java.net.NetworkInterface
|
||||
import java.net.ServerSocket
|
||||
@@ -41,20 +42,34 @@ class ManagedHttpServer(private val _requestedPort: Int = 0) {
|
||||
_workerPool = Executors.newCachedThreadPool();
|
||||
|
||||
Thread {
|
||||
var socket: ServerSocket? = null
|
||||
try {
|
||||
val socket = ServerSocket(_requestedPort);
|
||||
socket = ServerSocket(_requestedPort);
|
||||
port = socket.localPort;
|
||||
} catch (e: BindException) {
|
||||
try {
|
||||
Logger.w(TAG, "Failed create socket due to port being in use, attempting to automatically choose port...", e);
|
||||
socket = ServerSocket(0);
|
||||
port = socket.localPort;
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to accept socket.", e);
|
||||
stop();
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to accept socket.", e);
|
||||
stop();
|
||||
}
|
||||
|
||||
try {
|
||||
val stopCount = _stopCount;
|
||||
while (_stopCount == stopCount) {
|
||||
if(_logVerbose)
|
||||
Logger.i(TAG, "Waiting for connection...");
|
||||
val s = socket.accept() ?: continue;
|
||||
val s = socket?.accept() ?: continue;
|
||||
|
||||
try {
|
||||
handleClientRequest(s);
|
||||
}
|
||||
catch(ex : Exception) {
|
||||
} catch(ex : Exception) {
|
||||
Logger.e(TAG, "Client disconnected due to: " + ex.message, ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
package com.futo.platformplayer.api.media.models.comments
|
||||
|
||||
import com.futo.platformplayer.api.media.IPlatformClient
|
||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||
import com.futo.platformplayer.api.media.models.ratings.IRating
|
||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||
import com.futo.platformplayer.api.media.models.ratings.RatingType
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import kotlinx.coroutines.Deferred
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
class LazyComment: IPlatformComment {
|
||||
private var _commentDeferred: Deferred<IPlatformComment>;
|
||||
private var _commentLoaded: IPlatformComment? = null;
|
||||
private var _commentException: Throwable? = null;
|
||||
|
||||
override val contextUrl: String
|
||||
get() = _commentLoaded?.contextUrl ?: "";
|
||||
override val author: PlatformAuthorLink
|
||||
get() = _commentLoaded?.author ?: PlatformAuthorLink.UNKNOWN;
|
||||
override val message: String
|
||||
get() = _commentLoaded?.message ?: "";
|
||||
override val rating: IRating
|
||||
get() = _commentLoaded?.rating ?: RatingLikes(0);
|
||||
override val date: OffsetDateTime?
|
||||
get() = _commentLoaded?.date ?: OffsetDateTime.MIN;
|
||||
override val replyCount: Int?
|
||||
get() = _commentLoaded?.replyCount ?: 0;
|
||||
|
||||
val isAvailable: Boolean get() = _commentLoaded != null;
|
||||
|
||||
private var _uiHandler: ((LazyComment)->Unit)? = null;
|
||||
|
||||
constructor(commentDeferred: Deferred<IPlatformComment>) {
|
||||
_commentDeferred = commentDeferred;
|
||||
_commentDeferred.invokeOnCompletion {
|
||||
if(it == null) {
|
||||
_commentLoaded = commentDeferred.getCompleted();
|
||||
Logger.i("LazyComment", "Resolved comment");
|
||||
}
|
||||
else {
|
||||
_commentException = it;
|
||||
Logger.e("LazyComment", "Resolving comment failed: ${it.message}", it);
|
||||
}
|
||||
|
||||
_uiHandler?.invoke(this);
|
||||
}
|
||||
}
|
||||
|
||||
fun getUnderlyingComment(): IPlatformComment? {
|
||||
return _commentLoaded;
|
||||
}
|
||||
|
||||
fun setUIHandler(handler: (LazyComment)->Unit){
|
||||
_uiHandler = handler;
|
||||
}
|
||||
|
||||
override fun getReplies(client: IPlatformClient): IPager<IPlatformComment>? {
|
||||
return _commentLoaded?.getReplies(client);
|
||||
}
|
||||
|
||||
}
|
||||
+4
-1
@@ -1,3 +1,6 @@
|
||||
package com.futo.platformplayer.api.media.models.streams.sources
|
||||
|
||||
interface IAudioUrlWidevineSource : IAudioUrlSource, IWidevineSource
|
||||
interface IAudioUrlWidevineSource : IAudioUrlSource {
|
||||
val bearerToken: String
|
||||
val licenseUri: String
|
||||
}
|
||||
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
package com.futo.platformplayer.api.media.models.streams.sources
|
||||
|
||||
interface IDashManifestWidevineSource : IWidevineSource {
|
||||
val url: String
|
||||
}
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
package com.futo.platformplayer.api.media.models.streams.sources
|
||||
|
||||
interface IVideoUrlWidevineSource : IVideoUrlSource, IWidevineSource
|
||||
-9
@@ -1,9 +0,0 @@
|
||||
package com.futo.platformplayer.api.media.models.streams.sources
|
||||
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
|
||||
|
||||
interface IWidevineSource {
|
||||
val licenseUri: String
|
||||
val hasLicenseRequestExecutor: Boolean
|
||||
fun getLicenseRequestExecutor(): JSRequestExecutor?
|
||||
}
|
||||
+1
-2
@@ -10,7 +10,6 @@ import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.api.media.structures.ReusablePager
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.models.Playlist
|
||||
import java.util.UUID
|
||||
|
||||
class JSPlaylistDetails: JSPlaylist, IPlatformPlaylistDetails {
|
||||
override val contents: IPager<IPlatformVideo>;
|
||||
@@ -38,6 +37,6 @@ class JSPlaylistDetails: JSPlaylist, IPlatformPlaylistDetails {
|
||||
onProgress?.invoke(videos.size);
|
||||
}
|
||||
|
||||
return Playlist(UUID.randomUUID().toString(), name, videos.map { SerializedPlatformVideo.fromVideo(it)});
|
||||
return Playlist(id.toString(), name, videos.map { SerializedPlatformVideo.fromVideo(it)});
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -42,7 +42,7 @@ class JSRequestExecutor {
|
||||
|
||||
//TODO: Executor properties?
|
||||
@Throws(ScriptException::class)
|
||||
open fun executeRequest(method: String, url: String, body: ByteArray?, headers: Map<String, String>): ByteArray {
|
||||
open fun executeRequest(url: String, headers: Map<String, String>): ByteArray {
|
||||
if (_executor.isClosed)
|
||||
throw IllegalStateException("Executor object is closed");
|
||||
|
||||
@@ -53,7 +53,7 @@ class JSRequestExecutor {
|
||||
"[${_config.name}] JSRequestExecutor",
|
||||
"builder.modifyRequest()"
|
||||
) {
|
||||
_executor.invoke("executeRequest", url, headers, method, body);
|
||||
_executor.invoke("executeRequest", url, headers);
|
||||
} as V8Value;
|
||||
}
|
||||
else V8Plugin.catchScriptErrors<Any>(
|
||||
@@ -61,7 +61,7 @@ class JSRequestExecutor {
|
||||
"[${_config.name}] JSRequestExecutor",
|
||||
"builder.modifyRequest()"
|
||||
) {
|
||||
_executor.invoke("executeRequest", url, headers, method, body);
|
||||
_executor.invoke("executeRequest", url, headers);
|
||||
} as V8Value;
|
||||
|
||||
try {
|
||||
|
||||
+3
-20
@@ -3,39 +3,22 @@ package com.futo.platformplayer.api.media.platforms.js.models.sources
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlWidevineSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
|
||||
import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
class JSAudioUrlWidevineSource : JSAudioUrlSource, IAudioUrlWidevineSource {
|
||||
override val bearerToken: String
|
||||
override val licenseUri: String
|
||||
override val hasLicenseRequestExecutor: Boolean
|
||||
|
||||
@Suppress("ConvertSecondaryConstructorToPrimary")
|
||||
constructor(plugin: JSClient, obj: V8ValueObject) : super(plugin, obj) {
|
||||
val contextName = "JSAudioUrlWidevineSource"
|
||||
val config = plugin.config
|
||||
|
||||
bearerToken = _obj.getOrThrow(config, "bearerToken", contextName)
|
||||
licenseUri = _obj.getOrThrow(config, "licenseUri", contextName)
|
||||
hasLicenseRequestExecutor = obj.has("getLicenseRequestExecutor")
|
||||
}
|
||||
|
||||
override fun getLicenseRequestExecutor(): JSRequestExecutor? {
|
||||
if (!hasLicenseRequestExecutor || _obj.isClosed)
|
||||
return null
|
||||
|
||||
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSAudioUrlWidevineSource", "obj.getLicenseRequestExecutor()") {
|
||||
_obj.invoke("getLicenseRequestExecutor", arrayOf<Any>())
|
||||
}
|
||||
|
||||
if (result !is V8ValueObject)
|
||||
return null
|
||||
|
||||
return JSRequestExecutor(_plugin, result)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
val url = getAudioUrl()
|
||||
return "(name=$name, container=$container, bitrate=$bitrate, codec=$codec, url=$url, language=$language, duration=$duration, hasLicenseRequestExecutor=${hasLicenseRequestExecutor}, licenseUri=$licenseUri)"
|
||||
return "(name=$name, container=$container, bitrate=$bitrate, codec=$codec, url=$url, language=$language, duration=$duration, bearerToken=$bearerToken, licenseUri=$licenseUri)"
|
||||
}
|
||||
}
|
||||
|
||||
-60
@@ -1,60 +0,0 @@
|
||||
package com.futo.platformplayer.api.media.platforms.js.models.sources
|
||||
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestWidevineSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
|
||||
import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.futo.platformplayer.getOrNull
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
class JSDashManifestWidevineSource : IVideoUrlSource, IDashManifestSource,
|
||||
IDashManifestWidevineSource, JSSource {
|
||||
override val width: Int = 0
|
||||
override val height: Int = 0
|
||||
override val container: String = "application/dash+xml"
|
||||
override val codec: String = "Dash"
|
||||
override val name: String
|
||||
override val bitrate: Int? = null
|
||||
override val url: String
|
||||
override val duration: Long
|
||||
|
||||
override var priority: Boolean = false
|
||||
|
||||
override val licenseUri: String
|
||||
override val hasLicenseRequestExecutor: Boolean
|
||||
|
||||
@Suppress("ConvertSecondaryConstructorToPrimary")
|
||||
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH, plugin, obj) {
|
||||
val contextName = "DashWidevineSource"
|
||||
val config = plugin.config
|
||||
name = _obj.getOrThrow(config, "name", contextName)
|
||||
url = _obj.getOrThrow(config, "url", contextName)
|
||||
duration = _obj.getOrThrow(config, "duration", contextName)
|
||||
|
||||
priority = obj.getOrNull(config, "priority", contextName) ?: false
|
||||
|
||||
licenseUri = _obj.getOrThrow(config, "licenseUri", contextName)
|
||||
hasLicenseRequestExecutor = obj.has("getLicenseRequestExecutor")
|
||||
}
|
||||
|
||||
override fun getLicenseRequestExecutor(): JSRequestExecutor? {
|
||||
if (!hasLicenseRequestExecutor || _obj.isClosed)
|
||||
return null
|
||||
|
||||
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSDashManifestWidevineSource", "obj.getLicenseRequestExecutor()") {
|
||||
_obj.invoke("getLicenseRequestExecutor", arrayOf<Any>())
|
||||
}
|
||||
|
||||
if (result !is V8ValueObject)
|
||||
return null
|
||||
|
||||
return JSRequestExecutor(_plugin, result)
|
||||
}
|
||||
|
||||
override fun getVideoUrl(): String {
|
||||
return url
|
||||
}
|
||||
}
|
||||
-4
@@ -98,22 +98,18 @@ abstract class JSSource {
|
||||
const val TYPE_AUDIO_WITH_METADATA = "AudioUrlRangeSource";
|
||||
const val TYPE_VIDEO_WITH_METADATA = "VideoUrlRangeSource";
|
||||
const val TYPE_DASH = "DashSource";
|
||||
const val TYPE_DASH_WIDEVINE = "DashWidevineSource";
|
||||
const val TYPE_DASH_RAW = "DashRawSource";
|
||||
const val TYPE_DASH_RAW_AUDIO = "DashRawAudioSource";
|
||||
const val TYPE_HLS = "HLSSource";
|
||||
const val TYPE_AUDIOURL_WIDEVINE = "AudioUrlWidevineSource"
|
||||
const val TYPE_VIDEOURL_WIDEVINE = "VideoUrlWidevineSource"
|
||||
|
||||
fun fromV8VideoNullable(plugin: JSClient, obj: V8Value?) : IVideoSource? = obj.orNull { fromV8Video(plugin, it as V8ValueObject) };
|
||||
fun fromV8Video(plugin: JSClient, obj: V8ValueObject) : IVideoSource? {
|
||||
val type = obj.getString("plugin_type");
|
||||
return when(type) {
|
||||
TYPE_VIDEOURL -> JSVideoUrlSource(plugin, obj);
|
||||
TYPE_VIDEOURL_WIDEVINE -> JSVideoUrlWidevineSource(plugin, obj);
|
||||
TYPE_VIDEO_WITH_METADATA -> JSVideoUrlRangeSource(plugin, obj);
|
||||
TYPE_HLS -> fromV8HLS(plugin, obj);
|
||||
TYPE_DASH_WIDEVINE -> JSDashManifestWidevineSource(plugin, obj)
|
||||
TYPE_DASH -> fromV8Dash(plugin, obj);
|
||||
TYPE_DASH_RAW -> fromV8DashRaw(plugin, obj);
|
||||
else -> {
|
||||
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
package com.futo.platformplayer.api.media.platforms.js.models.sources
|
||||
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlWidevineSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
|
||||
import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
class JSVideoUrlWidevineSource : JSVideoUrlSource, IVideoUrlWidevineSource {
|
||||
override val licenseUri: String
|
||||
override val hasLicenseRequestExecutor: Boolean
|
||||
|
||||
@Suppress("ConvertSecondaryConstructorToPrimary")
|
||||
constructor(plugin: JSClient, obj: V8ValueObject) : super(plugin, obj) {
|
||||
val contextName = "JSAudioUrlWidevineSource"
|
||||
val config = plugin.config
|
||||
|
||||
licenseUri = _obj.getOrThrow(config, "licenseUri", contextName)
|
||||
hasLicenseRequestExecutor = obj.has("getLicenseRequestExecutor")
|
||||
}
|
||||
|
||||
override fun getLicenseRequestExecutor(): JSRequestExecutor? {
|
||||
if (!hasLicenseRequestExecutor || _obj.isClosed)
|
||||
return null
|
||||
|
||||
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSAudioUrlWidevineSource", "obj.getLicenseRequestExecutor()") {
|
||||
_obj.invoke("getLicenseRequestExecutor", arrayOf<Any>())
|
||||
}
|
||||
|
||||
if (result !is V8ValueObject)
|
||||
return null
|
||||
|
||||
return JSRequestExecutor(_plugin, result)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
val url = getVideoUrl()
|
||||
return "(width=$width, height=$height, container=$container, codec=$codec, name=$name, bitrate=$bitrate, duration=$duration, url=$url, hasLicenseRequestExecutor=$hasLicenseRequestExecutor, licenseUri=$licenseUri)"
|
||||
}
|
||||
}
|
||||
@@ -1245,7 +1245,7 @@ class StateCasting {
|
||||
|
||||
val videoExecutor = _videoExecutor;
|
||||
if (videoExecutor != null) {
|
||||
val data = videoExecutor.executeRequest("GET", originalUrl, null, httpContext.headers)
|
||||
val data = videoExecutor.executeRequest(originalUrl, httpContext.headers)
|
||||
httpContext.respondBytes(200, HttpHeaders().apply {
|
||||
put("Content-Type", mediaType)
|
||||
}, data);
|
||||
@@ -1263,7 +1263,7 @@ class StateCasting {
|
||||
|
||||
val audioExecutor = _audioExecutor;
|
||||
if (audioExecutor != null) {
|
||||
val data = audioExecutor.executeRequest("GET", originalUrl, null, httpContext.headers)
|
||||
val data = audioExecutor.executeRequest(originalUrl, httpContext.headers)
|
||||
httpContext.respondBytes(200, HttpHeaders().apply {
|
||||
put("Content-Type", mediaType)
|
||||
}, data);
|
||||
|
||||
@@ -6,7 +6,6 @@ import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.KeyEvent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.WindowManager
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
@@ -58,21 +57,11 @@ class CommentDialog(context: Context?, val contextUrl: String, val ref: Protocol
|
||||
_editComment = findViewById(R.id.edit_comment);
|
||||
_textCharacterCount = findViewById(R.id.character_count);
|
||||
_textCharacterCountMax = findViewById(R.id.character_count_max);
|
||||
setCanceledOnTouchOutside(false)
|
||||
setOnKeyListener { _, keyCode, event ->
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_UP) {
|
||||
handleCloseAttempt()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
_editComment.addTextChangedListener(object : TextWatcher {
|
||||
override fun afterTextChanged(s: Editable?) = Unit
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, c: Int) {
|
||||
val count = s?.length ?: 0;
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
||||
_textCharacterCount.text = count.toString();
|
||||
|
||||
if (count > PolycentricPlatformComment.MAX_COMMENT_SIZE) {
|
||||
@@ -90,13 +79,10 @@ class CommentDialog(context: Context?, val contextUrl: String, val ref: Protocol
|
||||
_inputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager;
|
||||
|
||||
_buttonCancel.setOnClickListener {
|
||||
handleCloseAttempt()
|
||||
clearFocus();
|
||||
dismiss();
|
||||
};
|
||||
|
||||
setOnCancelListener {
|
||||
handleCloseAttempt()
|
||||
}
|
||||
|
||||
_buttonCreate.setOnClickListener {
|
||||
clearFocus();
|
||||
|
||||
@@ -148,22 +134,6 @@ class CommentDialog(context: Context?, val contextUrl: String, val ref: Protocol
|
||||
focus();
|
||||
}
|
||||
|
||||
private fun handleCloseAttempt() {
|
||||
if (_editComment.text.isEmpty()) {
|
||||
clearFocus()
|
||||
dismiss()
|
||||
} else {
|
||||
UIDialogs.showConfirmationDialog(
|
||||
context,
|
||||
context.resources.getString(R.string.not_empty_close),
|
||||
action = {
|
||||
clearFocus()
|
||||
dismiss()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun focus() {
|
||||
_editComment.requestFocus();
|
||||
window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
|
||||
|
||||
@@ -100,7 +100,6 @@ class VideoDownload {
|
||||
|
||||
var requireVideoSource: Boolean = false;
|
||||
var requireAudioSource: Boolean = false;
|
||||
var requiredCheck: Boolean = false;
|
||||
|
||||
@Contextual
|
||||
@Transient
|
||||
@@ -165,7 +164,7 @@ class VideoDownload {
|
||||
onStateChanged.emit(newState);
|
||||
}
|
||||
|
||||
constructor(video: IPlatformVideo, targetPixelCount: Long? = null, targetBitrate: Long? = null, optionalSources: Boolean = false) {
|
||||
constructor(video: IPlatformVideo, targetPixelCount: Long? = null, targetBitrate: Long? = null) {
|
||||
this.video = SerializedPlatformVideo.fromVideo(video);
|
||||
this.videoSource = null;
|
||||
this.audioSource = null;
|
||||
@@ -176,9 +175,8 @@ class VideoDownload {
|
||||
this.requiresLiveVideoSource = false;
|
||||
this.requiresLiveAudioSource = false;
|
||||
this.targetVideoName = videoSource?.name;
|
||||
this.requireVideoSource = targetPixelCount != null;
|
||||
this.requireVideoSource = targetPixelCount != null
|
||||
this.requireAudioSource = targetBitrate != null; //TODO: May not be a valid check.. can only be determined after live fetch?
|
||||
this.requiredCheck = optionalSources;
|
||||
}
|
||||
constructor(video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: SubtitleRawSource?) {
|
||||
this.video = SerializedPlatformVideo.fromVideo(video);
|
||||
@@ -252,30 +250,6 @@ class VideoDownload {
|
||||
if(original !is IPlatformVideoDetails)
|
||||
throw IllegalStateException("Original content is not media?");
|
||||
|
||||
if(requiredCheck) {
|
||||
if(original.video is VideoUnMuxedSourceDescriptor) {
|
||||
if(requireVideoSource) {
|
||||
if((original.video as VideoUnMuxedSourceDescriptor).audioSources.any() && !original.video.videoSources.any()) {
|
||||
requireVideoSource = false;
|
||||
targetPixelCount = null;
|
||||
}
|
||||
}
|
||||
if(requireAudioSource) {
|
||||
if(!(original.video as VideoUnMuxedSourceDescriptor).audioSources.any() && original.video.videoSources.any()) {
|
||||
requireAudioSource = false;
|
||||
targetBitrate = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
if(requireAudioSource) {
|
||||
requireAudioSource = false;
|
||||
targetBitrate = null;
|
||||
}
|
||||
}
|
||||
requiredCheck = false;
|
||||
}
|
||||
|
||||
if(original.video.hasAnySource() && !original.isDownloadable()) {
|
||||
Logger.i(TAG, "Attempted to download unsupported video [${original.name}]:${original.url}");
|
||||
throw DownloadException("Unsupported video for downloading", false);
|
||||
@@ -689,7 +663,7 @@ class VideoDownload {
|
||||
val url = foundTemplateUrl.replace("\$Number\$", indexCounter.toString());
|
||||
|
||||
val data = if(executor != null)
|
||||
executor.executeRequest("GET", url, null, mapOf());
|
||||
executor.executeRequest(url, mapOf());
|
||||
else {
|
||||
val resp = client.get(url, mutableMapOf());
|
||||
if(!resp.isOk)
|
||||
|
||||
+6
-15
@@ -1,13 +1,12 @@
|
||||
package com.futo.platformplayer.fragment.channel.tab
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
@@ -16,6 +15,7 @@ import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSPager
|
||||
import com.futo.platformplayer.api.media.structures.IAsyncPager
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
@@ -41,11 +41,10 @@ import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||
import com.futo.platformplayer.views.adapters.feedtypes.PreviewContentListAdapter
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.max
|
||||
|
||||
class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
||||
private var _recyclerResults: RecyclerView? = null;
|
||||
private var _glmVideo: GridLayoutManager? = null;
|
||||
private var _llmVideo: LinearLayoutManager? = null;
|
||||
private var _loading = false;
|
||||
private var _pager_parent: IPager<IPlatformContent>? = null;
|
||||
private var _pager: IPager<IPlatformContent>? = null;
|
||||
@@ -119,7 +118,7 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
||||
super.onScrolled(recyclerView, dx, dy);
|
||||
|
||||
val recyclerResults = _recyclerResults ?: return;
|
||||
val llmVideo = _glmVideo ?: return;
|
||||
val llmVideo = _llmVideo ?: return;
|
||||
|
||||
val visibleItemCount = recyclerResults.childCount;
|
||||
val firstVisibleItem = llmVideo.findFirstVisibleItemPosition();
|
||||
@@ -164,10 +163,9 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
||||
this.onLongPress.subscribe(this@ChannelContentsFragment.onLongPress::emit);
|
||||
}
|
||||
|
||||
val numColumns = max((resources.configuration.screenWidthDp.toDouble() / resources.getInteger(R.integer.column_width_dp)).toInt(), 1)
|
||||
_glmVideo = GridLayoutManager(view.context, numColumns);
|
||||
_llmVideo = LinearLayoutManager(view.context);
|
||||
_recyclerResults?.adapter = _adapterResults;
|
||||
_recyclerResults?.layoutManager = _glmVideo;
|
||||
_recyclerResults?.layoutManager = _llmVideo;
|
||||
_recyclerResults?.addOnScrollListener(_scrollListener);
|
||||
|
||||
return view;
|
||||
@@ -183,13 +181,6 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
||||
_nextPageHandler.cancel();
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
|
||||
_glmVideo?.spanCount =
|
||||
max((resources.configuration.screenWidthDp.toDouble() / resources.getInteger(R.integer.column_width_dp)).toInt(), 1)
|
||||
}
|
||||
|
||||
/*
|
||||
private fun setPager(pager: IPager<IPlatformContent>, cache: FeedFragment.ItemCache<IPlatformContent>? = null) {
|
||||
if (_pager_parent != null && _pager_parent is IRefreshPager<*>) {
|
||||
|
||||
+5
-15
@@ -1,13 +1,12 @@
|
||||
package com.futo.platformplayer.fragment.channel.tab
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
@@ -37,11 +36,10 @@ import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||
import com.futo.platformplayer.views.adapters.feedtypes.PreviewContentListAdapter
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.max
|
||||
|
||||
class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
|
||||
private var _recyclerResults: RecyclerView? = null
|
||||
private var _glmPlaylist: GridLayoutManager? = null
|
||||
private var _llmPlaylist: LinearLayoutManager? = null
|
||||
private var _loading = false
|
||||
private var _pagerParent: IPager<IPlatformPlaylist>? = null
|
||||
private var _pager: IPager<IPlatformPlaylist>? = null
|
||||
@@ -111,7 +109,7 @@ class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
|
||||
val recyclerResults = _recyclerResults ?: return
|
||||
val llmPlaylist = _glmPlaylist ?: return
|
||||
val llmPlaylist = _llmPlaylist ?: return
|
||||
|
||||
val visibleItemCount = recyclerResults.childCount
|
||||
val firstVisibleItem = llmPlaylist.findFirstVisibleItemPosition()
|
||||
@@ -160,10 +158,9 @@ class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
|
||||
this.onLongPress.subscribe(this@ChannelPlaylistsFragment.onLongPress::emit)
|
||||
}
|
||||
|
||||
val numColumns = max((resources.configuration.screenWidthDp.toDouble() / resources.getInteger(R.integer.column_width_dp)).toInt(), 1)
|
||||
_glmPlaylist = GridLayoutManager(view.context, numColumns)
|
||||
_llmPlaylist = LinearLayoutManager(view.context)
|
||||
_recyclerResults?.adapter = _adapterResults
|
||||
_recyclerResults?.layoutManager = _glmPlaylist
|
||||
_recyclerResults?.layoutManager = _llmPlaylist
|
||||
_recyclerResults?.addOnScrollListener(_scrollListener)
|
||||
|
||||
return view
|
||||
@@ -179,13 +176,6 @@ class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
|
||||
_nextPageHandler.cancel()
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
|
||||
_glmPlaylist?.spanCount =
|
||||
max((resources.configuration.screenWidthDp.toDouble() / resources.getInteger(R.integer.column_width_dp)).toInt(), 1)
|
||||
}
|
||||
|
||||
private fun setPager(
|
||||
pager: IPager<IPlatformPlaylist>
|
||||
) {
|
||||
|
||||
@@ -90,7 +90,7 @@ class BuyFragment : MainFragment() {
|
||||
try {
|
||||
val currencies = StatePayment.instance.getAvailableCurrencies("grayjay");
|
||||
val prices = StatePayment.instance.getAvailableCurrencyPrices("grayjay");
|
||||
val country = StatePayment.instance.getPaymentCountryFromIP(true)?.let { c -> PaymentConfigurations.COUNTRIES.find { it.id.equals(c, ignoreCase = true) } };
|
||||
val country = StatePayment.instance.getPaymentCountryFromIP()?.let { c -> PaymentConfigurations.COUNTRIES.find { it.id.equals(c, ignoreCase = true) } };
|
||||
val currency = country?.let { c -> PaymentConfigurations.CURRENCIES.find { it.id == c.defaultCurrencyId && (currencies.contains(it.id)) } };
|
||||
|
||||
if(currency != null && prices.containsKey(currency.id)) {
|
||||
|
||||
+1
-2
@@ -33,7 +33,6 @@ import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
|
||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
||||
import com.futo.platformplayer.withTimestamp
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.max
|
||||
|
||||
abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent, IPlatformContent, IPager<IPlatformContent>, ContentPreviewViewHolder> where TFragment : MainFragment {
|
||||
private var _exoPlayer: PlayerManager? = null;
|
||||
@@ -169,7 +168,7 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
||||
val glmResults =
|
||||
GridLayoutManager(
|
||||
context,
|
||||
max((resources.configuration.screenWidthDp.toDouble() / resources.getInteger(R.integer.column_width_dp)).toInt(), 1)
|
||||
(resources.configuration.screenWidthDp / resources.getDimension(R.dimen.landscape_threshold)).toInt() + 1
|
||||
);
|
||||
return glmResults
|
||||
}
|
||||
|
||||
+2
-17
@@ -8,7 +8,6 @@ import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.EditText
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageButton
|
||||
import android.widget.Spinner
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
@@ -26,20 +25,11 @@ class CreatorsFragment : MainFragment() {
|
||||
private var _overlayContainer: FrameLayout? = null;
|
||||
private var _containerSearch: FrameLayout? = null;
|
||||
private var _editSearch: EditText? = null;
|
||||
private var _buttonClearSearch: ImageButton? = null
|
||||
|
||||
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
val view = inflater.inflate(R.layout.fragment_creators, container, false);
|
||||
_containerSearch = view.findViewById(R.id.container_search);
|
||||
val editSearch: EditText = view.findViewById(R.id.edit_search);
|
||||
val buttonClearSearch: ImageButton = view.findViewById(R.id.button_clear_search)
|
||||
_editSearch = editSearch
|
||||
_buttonClearSearch = buttonClearSearch
|
||||
buttonClearSearch.setOnClickListener {
|
||||
editSearch.text.clear()
|
||||
editSearch.requestFocus()
|
||||
_buttonClearSearch?.visibility = View.INVISIBLE;
|
||||
}
|
||||
_editSearch = view.findViewById(R.id.edit_search);
|
||||
|
||||
val adapter = SubscriptionAdapter(inflater, getString(R.string.confirm_delete_subscription));
|
||||
adapter.onClick.subscribe { platformUser -> navigate<ChannelFragment>(platformUser) };
|
||||
@@ -61,12 +51,7 @@ class CreatorsFragment : MainFragment() {
|
||||
_spinnerSortBy = spinnerSortBy;
|
||||
|
||||
_editSearch?.addTextChangedListener {
|
||||
adapter.query = it.toString()
|
||||
if (it?.isEmpty() == true) {
|
||||
_buttonClearSearch?.visibility = View.INVISIBLE
|
||||
} else {
|
||||
_buttonClearSearch?.visibility = View.VISIBLE
|
||||
}
|
||||
adapter.query = it.toString();
|
||||
}
|
||||
|
||||
val recyclerView = view.findViewById<RecyclerView>(R.id.recycler_subscriptions);
|
||||
|
||||
@@ -25,12 +25,10 @@ import com.futo.platformplayer.views.others.ProgressBar
|
||||
import com.futo.platformplayer.views.others.TagsView
|
||||
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||
import com.futo.platformplayer.views.adapters.InsertedViewHolder
|
||||
import com.futo.platformplayer.views.announcements.AnnouncementView
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.time.OffsetDateTime
|
||||
import kotlin.math.max
|
||||
|
||||
abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : LinearLayout where TPager : IPager<TResult>, TViewHolder : RecyclerView.ViewHolder, TFragment : MainFragment {
|
||||
protected val _recyclerResults: RecyclerView;
|
||||
@@ -39,7 +37,6 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
||||
private val _progressBar: ProgressBar;
|
||||
private val _spinnerSortBy: Spinner;
|
||||
private val _containerSortBy: LinearLayout;
|
||||
private val _announcementView: AnnouncementView;
|
||||
private val _tagsView: TagsView;
|
||||
private val _textCentered: TextView;
|
||||
private val _emptyPagerContainer: FrameLayout;
|
||||
@@ -76,7 +73,6 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
||||
_textCentered = findViewById(R.id.text_centered);
|
||||
_emptyPagerContainer = findViewById(R.id.empty_pager_container);
|
||||
_progressBar = findViewById(R.id.progress_bar);
|
||||
_announcementView = findViewById(R.id.announcement_view)
|
||||
_progressBar.inactiveColor = Color.TRANSPARENT;
|
||||
|
||||
_swipeRefresh = findViewById(R.id.swipe_refresh);
|
||||
@@ -176,10 +172,6 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
||||
_recyclerResults.addOnScrollListener(_scrollListener);
|
||||
}
|
||||
|
||||
protected fun showAnnouncementView() {
|
||||
_announcementView.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
private fun ensureEnoughContentVisible(filteredResults: List<TConverted>) {
|
||||
val canScroll = if (recyclerData.results.isEmpty()) false else {
|
||||
val layoutManager = recyclerData.layoutManager
|
||||
@@ -235,8 +227,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
||||
}
|
||||
|
||||
open fun updateSpanCount() {
|
||||
recyclerData.layoutManager.spanCount =
|
||||
max((resources.configuration.screenWidthDp.toDouble() / resources.getInteger(R.integer.column_width_dp)).toInt(), 1)
|
||||
recyclerData.layoutManager.spanCount = (resources.configuration.screenWidthDp / resources.getDimension(R.dimen.landscape_threshold)).toInt() + 1
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration?) {
|
||||
|
||||
+10
-1
@@ -94,10 +94,20 @@ class HomeFragment : MainFragment() {
|
||||
class HomeView : ContentFeedView<HomeFragment> {
|
||||
override val feedStyle: FeedStyle get() = Settings.instance.home.getHomeFeedStyle();
|
||||
|
||||
private var _announcementsView: AnnouncementView = AnnouncementView(context, null).apply {
|
||||
if(!this.isClosed()) {
|
||||
recyclerData.adapter.viewsToPrepend.add(this)
|
||||
this.onClose.subscribe {
|
||||
recyclerData.adapter.viewsToPrepend.remove(this)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private val _taskGetPager: TaskHandler<Boolean, IPager<IPlatformContent>>;
|
||||
override val shouldShowTimeBar: Boolean get() = Settings.instance.home.progressBar
|
||||
|
||||
constructor(fragment: HomeFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, GridLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData) {
|
||||
|
||||
_taskGetPager = TaskHandler<Boolean, IPager<IPlatformContent>>({ fragment.lifecycleScope }, {
|
||||
StatePlatform.instance.getHomeRefresh(fragment.lifecycleScope)
|
||||
})
|
||||
@@ -128,7 +138,6 @@ class HomeFragment : MainFragment() {
|
||||
};
|
||||
|
||||
setPreviewsEnabled(Settings.instance.home.previewFeedItems);
|
||||
showAnnouncementView()
|
||||
}
|
||||
|
||||
fun onShown() {
|
||||
|
||||
+5
-5
@@ -70,7 +70,7 @@ class PlaylistFragment : MainFragment() {
|
||||
private var _editPlaylistOverlay: SlideUpMenuOverlay? = null;
|
||||
private var _url: String? = null;
|
||||
|
||||
private val _taskLoadPlaylist: TaskHandler<String, Playlist>;
|
||||
private val _taskLoadPlaylist: TaskHandler<String, IPlatformPlaylistDetails>;
|
||||
|
||||
constructor(fragment: PlaylistFragment, inflater: LayoutInflater) : super(inflater) {
|
||||
_fragment = fragment;
|
||||
@@ -137,16 +137,16 @@ class PlaylistFragment : MainFragment() {
|
||||
);
|
||||
};
|
||||
|
||||
_taskLoadPlaylist = TaskHandler<String, Playlist>(
|
||||
_taskLoadPlaylist = TaskHandler<String, IPlatformPlaylistDetails>(
|
||||
StateApp.instance.scopeGetter,
|
||||
{
|
||||
return@TaskHandler StatePlatform.instance.getPlaylist(it).toPlaylist();
|
||||
return@TaskHandler StatePlatform.instance.getPlaylist(it);
|
||||
})
|
||||
.success {
|
||||
setName(it.name);
|
||||
//TODO: Implement support for pagination
|
||||
setVideos(it.videos, false);
|
||||
setVideoCount(it.videos.size);
|
||||
setVideos(it.toPlaylist().videos, false);
|
||||
setVideoCount(it.videoCount);
|
||||
setLoading(false);
|
||||
}
|
||||
.exception<Throwable> {
|
||||
|
||||
+23
-3
@@ -35,6 +35,7 @@ import com.futo.platformplayer.views.ToastView
|
||||
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
|
||||
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||
import com.futo.platformplayer.views.adapters.InsertedViewHolder
|
||||
import com.futo.platformplayer.views.announcements.AnnouncementView
|
||||
import com.futo.platformplayer.views.buttons.BigButton
|
||||
import com.futo.platformplayer.views.subscriptions.SubscriptionBar
|
||||
import kotlinx.coroutines.CancellationException
|
||||
@@ -124,9 +125,6 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
initializeToolbarContent();
|
||||
|
||||
setPreviewsEnabled(Settings.instance.subscriptions.previewFeedItems);
|
||||
if (Settings.instance.tabs.find { it.id == 0 }?.enabled != true) {
|
||||
showAnnouncementView()
|
||||
}
|
||||
}
|
||||
|
||||
fun onShown() {
|
||||
@@ -147,6 +145,26 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
val announcementsView = _announcementsView;
|
||||
val homeTab = Settings.instance.tabs.find { it.id == 0 };
|
||||
val isHomeEnabled = homeTab?.enabled == true;
|
||||
if (announcementsView != null && isHomeEnabled) {
|
||||
recyclerData.adapter.viewsToPrepend.remove(announcementsView)
|
||||
_announcementsView = null
|
||||
}
|
||||
|
||||
if (announcementsView == null && !isHomeEnabled) {
|
||||
val c = context;
|
||||
if (c != null) {
|
||||
_announcementsView = AnnouncementView(c, null).apply {
|
||||
recyclerData.adapter.viewsToPrepend.add(this)
|
||||
this.onClose.subscribe {
|
||||
recyclerData.adapter.viewsToPrepend.remove(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!StateSubscriptions.instance.global.isGlobalUpdating) {
|
||||
finishRefreshLayoutLoader();
|
||||
}
|
||||
@@ -174,6 +192,8 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
|
||||
private var _subscriptionBar: SubscriptionBar? = null;
|
||||
|
||||
private var _announcementsView: AnnouncementView? = null;
|
||||
|
||||
@Serializable
|
||||
class FeedFilterSettings: FragmentedStorageFileJson() {
|
||||
val allowContentTypes: MutableList<ContentType> = mutableListOf(ContentType.MEDIA, ContentType.POST);
|
||||
|
||||
+32
-150
@@ -1,12 +1,11 @@
|
||||
package com.futo.platformplayer.fragment.mainactivity.main
|
||||
|
||||
import android.content.Context
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.OrientationEventListener
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowInsets
|
||||
@@ -29,18 +28,13 @@ import com.futo.platformplayer.models.PlatformVideoWithTime
|
||||
import com.futo.platformplayer.models.UrlVideoWithTime
|
||||
import com.futo.platformplayer.states.StatePlayer
|
||||
import com.futo.platformplayer.views.containers.SingleViewTouchableMotionLayout
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.min
|
||||
|
||||
//region Fragment
|
||||
@UnstableApi
|
||||
class VideoDetailFragment() : MainFragment() {
|
||||
override val isMainView: Boolean = false;
|
||||
class VideoDetailFragment : MainFragment {
|
||||
override val isMainView : Boolean = false;
|
||||
override val hasBottomBar: Boolean = true;
|
||||
override val isOverlay: Boolean = true;
|
||||
override val isOverlay : Boolean = true;
|
||||
override val isHistory: Boolean = false;
|
||||
|
||||
private var _isActive: Boolean = false;
|
||||
@@ -82,10 +76,8 @@ class VideoDetailFragment() : MainFragment() {
|
||||
private var _loadUrlOnCreate: UrlVideoWithTime? = null;
|
||||
private var _leavingPiP = false;
|
||||
|
||||
private var _landscapeOrientationListener: LandscapeOrientationListener? = null
|
||||
private var _portraitOrientationListener: PortraitOrientationListener? = null
|
||||
private var _lastSetOrientation: Int = Configuration.ORIENTATION_UNDEFINED
|
||||
private var _ignoreNextNewOrientation = false
|
||||
//region Fragment
|
||||
constructor() : super()
|
||||
|
||||
fun nextVideo() {
|
||||
_viewDetail?.nextVideo(true, true, true);
|
||||
@@ -99,7 +91,7 @@ class VideoDetailFragment() : MainFragment() {
|
||||
return min(
|
||||
resources.configuration.screenWidthDp,
|
||||
resources.configuration.screenHeightDp
|
||||
) < resources.getInteger(R.integer.column_width_dp) * 2
|
||||
) < resources.getDimension(R.dimen.landscape_threshold)
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
@@ -109,15 +101,6 @@ class VideoDetailFragment() : MainFragment() {
|
||||
|
||||
val isSmallWindow = isSmallWindow()
|
||||
|
||||
val temp = _lastSetOrientation
|
||||
|
||||
if (_ignoreNextNewOrientation) {
|
||||
_ignoreNextNewOrientation = false
|
||||
} else {
|
||||
// the device has rotated so update our state tracking what the physical orientation of the device is
|
||||
_lastSetOrientation = newConfig.orientation
|
||||
}
|
||||
|
||||
if (
|
||||
isSmallWindow
|
||||
&& newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||
@@ -130,7 +113,6 @@ class VideoDetailFragment() : MainFragment() {
|
||||
&& isFullscreen
|
||||
&& !Settings.instance.playback.fullscreenPortrait
|
||||
&& newConfig.orientation == Configuration.ORIENTATION_PORTRAIT
|
||||
&& temp == Configuration.ORIENTATION_LANDSCAPE
|
||||
&& isLandscapeVideo
|
||||
) {
|
||||
_viewDetail?.setFullscreen(false)
|
||||
@@ -161,6 +143,7 @@ class VideoDetailFragment() : MainFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SourceLockedOrientationActivity")
|
||||
fun updateOrientation() {
|
||||
val a = activity ?: return
|
||||
val isFullScreenPortraitAllowed = Settings.instance.playback.fullscreenPortrait
|
||||
@@ -171,52 +154,36 @@ class VideoDetailFragment() : MainFragment() {
|
||||
|
||||
val isSmallWindow = isSmallWindow()
|
||||
|
||||
val autoRotateEnabled = android.provider.Settings.System.getInt(
|
||||
context?.contentResolver,
|
||||
android.provider.Settings.System.ACCELEROMETER_ROTATION, 0
|
||||
) == 1
|
||||
|
||||
// For small windows if the device isn't landscape right now and full screen portrait isn't allowed then we should force landscape
|
||||
if (isSmallWindow && isFullscreen && !isFullScreenPortraitAllowed && _lastSetOrientation != Configuration.ORIENTATION_LANDSCAPE && !rotationLock && isLandscapeVideo) {
|
||||
if (Settings.instance.playback.forceAllowFullScreenRotation) {
|
||||
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|
||||
} else {
|
||||
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
|
||||
}
|
||||
// the next orientation change will not reflect the device because we are manually setting the orientation to landscape
|
||||
_ignoreNextNewOrientation = true
|
||||
if (autoRotateEnabled
|
||||
) {
|
||||
// start listening for the device to rotate to landscape
|
||||
// at which point we'll be able to set requestedOrientation to back to UNSPECIFIED
|
||||
_landscapeOrientationListener?.enableListener()
|
||||
}
|
||||
if (isSmallWindow && isFullscreen && !isFullScreenPortraitAllowed && resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT && !rotationLock && isLandscapeVideo) {
|
||||
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
|
||||
}
|
||||
// For small windows if the device isn't in a portrait orientation and we're in the maximized state then we should force portrait
|
||||
else if (isSmallWindow && !isMinimizingFromFullScreen && !isFullscreen && state == State.MAXIMIZED && _lastSetOrientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
else if (isSmallWindow && !isMinimizingFromFullScreen && !isFullscreen && state == State.MAXIMIZED && resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
|
||||
// the next orientation change will not reflect the device because we are manually setting the orientation to portrait
|
||||
_ignoreNextNewOrientation = true
|
||||
if (autoRotateEnabled
|
||||
) {
|
||||
// start listening for the device to rotate to portrait
|
||||
// at which point we'll be able to set requestedOrientation to back to UNSPECIFIED
|
||||
_portraitOrientationListener?.enableListener()
|
||||
} else {
|
||||
// the rotation state resets to portrait when changing requestedOrientation
|
||||
_lastSetOrientation = Configuration.ORIENTATION_PORTRAIT
|
||||
}
|
||||
} else if (rotationLock) {
|
||||
_portraitOrientationListener?.disableListener()
|
||||
_landscapeOrientationListener?.disableListener()
|
||||
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED
|
||||
} else {
|
||||
_portraitOrientationListener?.disableListener()
|
||||
_landscapeOrientationListener?.disableListener()
|
||||
a.requestedOrientation = if (isReversePortraitAllowed) {
|
||||
ActivityInfo.SCREEN_ORIENTATION_FULL_USER
|
||||
} else {
|
||||
ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||
when (Settings.instance.playback.autoRotate) {
|
||||
0 -> {
|
||||
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED
|
||||
}
|
||||
|
||||
1 -> {
|
||||
a.requestedOrientation = if (isReversePortraitAllowed) {
|
||||
ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR
|
||||
} else {
|
||||
ActivityInfo.SCREEN_ORIENTATION_SENSOR
|
||||
}
|
||||
}
|
||||
|
||||
2 -> {
|
||||
a.requestedOrientation = if (isReversePortraitAllowed) {
|
||||
ActivityInfo.SCREEN_ORIENTATION_FULL_USER
|
||||
} else {
|
||||
ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -387,26 +354,6 @@ class VideoDetailFragment() : MainFragment() {
|
||||
StatePlayer.instance.onRotationLockChanged.subscribe(this) {
|
||||
updateOrientation()
|
||||
}
|
||||
|
||||
_landscapeOrientationListener = LandscapeOrientationListener(requireContext())
|
||||
{
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
// delay to make sure that the system auto rotate updates
|
||||
delay(300)
|
||||
_lastSetOrientation = Configuration.ORIENTATION_LANDSCAPE
|
||||
updateOrientation()
|
||||
}
|
||||
}
|
||||
_portraitOrientationListener = PortraitOrientationListener(requireContext())
|
||||
{
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
// delay to make sure that the system auto rotate updates
|
||||
delay(300)
|
||||
_lastSetOrientation = Configuration.ORIENTATION_PORTRAIT
|
||||
updateOrientation()
|
||||
}
|
||||
}
|
||||
|
||||
return _view!!;
|
||||
}
|
||||
|
||||
@@ -508,9 +455,6 @@ class VideoDetailFragment() : MainFragment() {
|
||||
SettingsActivity.settingsActivityClosed.remove(this)
|
||||
StatePlayer.instance.onRotationLockChanged.remove(this)
|
||||
|
||||
_landscapeOrientationListener?.disableListener()
|
||||
_portraitOrientationListener?.disableListener()
|
||||
|
||||
_viewDetail?.let {
|
||||
_viewDetail = null;
|
||||
it.onDestroy();
|
||||
@@ -603,66 +547,4 @@ class VideoDetailFragment() : MainFragment() {
|
||||
//region View
|
||||
//TODO: Determine if encapsulated would be readable enough
|
||||
//endregion
|
||||
}
|
||||
|
||||
class LandscapeOrientationListener(
|
||||
context: Context,
|
||||
private val onLandscapeDetected: () -> Unit
|
||||
) : OrientationEventListener(context) {
|
||||
|
||||
private var isListening = false
|
||||
|
||||
override fun onOrientationChanged(orientation: Int) {
|
||||
if (!isListening) return
|
||||
|
||||
if (orientation in 60..120 || orientation in 240..300) {
|
||||
onLandscapeDetected()
|
||||
disableListener()
|
||||
}
|
||||
}
|
||||
|
||||
fun enableListener() {
|
||||
if (!isListening) {
|
||||
isListening = true
|
||||
enable()
|
||||
}
|
||||
}
|
||||
|
||||
fun disableListener() {
|
||||
if (isListening) {
|
||||
isListening = false
|
||||
disable()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PortraitOrientationListener(
|
||||
context: Context,
|
||||
private val onPortraitDetected: () -> Unit
|
||||
) : OrientationEventListener(context) {
|
||||
|
||||
private var isListening = false
|
||||
|
||||
override fun onOrientationChanged(orientation: Int) {
|
||||
if (!isListening) return
|
||||
|
||||
if (orientation in 0..30 || orientation in 330..360 || orientation in 150..210) {
|
||||
onPortraitDetected()
|
||||
disableListener()
|
||||
}
|
||||
}
|
||||
|
||||
fun enableListener() {
|
||||
if (!isListening) {
|
||||
isListening = true
|
||||
enable()
|
||||
}
|
||||
}
|
||||
|
||||
fun disableListener() {
|
||||
if (isListening) {
|
||||
isListening = false
|
||||
disable()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
-19
@@ -1322,14 +1322,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
this.video = video;
|
||||
cleanupPlaybackTracker();
|
||||
|
||||
if (video.video.videoSources.isNotEmpty()) {
|
||||
onVideoChanged.emit(
|
||||
video.video.videoSources[0].width,
|
||||
video.video.videoSources[0].height
|
||||
)
|
||||
} else {
|
||||
onVideoChanged.emit(0, 0)
|
||||
}
|
||||
onVideoChanged.emit(video.video.videoSources[0].width, video.video.videoSources[0].height)
|
||||
|
||||
if (video is JSVideoDetails) {
|
||||
val me = this;
|
||||
@@ -1799,13 +1792,8 @@ class VideoDetailView : ConstraintLayout {
|
||||
private fun onSourceChanged(videoSource: IVideoSource?, audioSource: IAudioSource?, resume: Boolean){
|
||||
Logger.i(TAG, "onSourceChanged(videoSource=$videoSource, audioSource=$audioSource, resume=$resume)")
|
||||
|
||||
if((videoSource == null || videoSource is LocalVideoSource) && (audioSource == null || audioSource is LocalAudioSource)) {
|
||||
Logger.i(TAG, "Time since last offline playback toast: " + (System.currentTimeMillis() - _lastOfflinePlaybackToastTime).toString())
|
||||
if (System.currentTimeMillis() - _lastOfflinePlaybackToastTime > 5000) {
|
||||
UIDialogs.toast(context, context.getString(R.string.offline_playback), false);
|
||||
_lastOfflinePlaybackToastTime = System.currentTimeMillis()
|
||||
}
|
||||
}
|
||||
if((videoSource == null || videoSource is LocalVideoSource) && (audioSource == null || audioSource is LocalAudioSource))
|
||||
UIDialogs.toast(context, context.getString(R.string.offline_playback), false);
|
||||
//If LiveStream, set to end
|
||||
if(videoSource is IDashManifestSource || videoSource is IHLSManifestSource) {
|
||||
if (video?.isLive == true) {
|
||||
@@ -2384,8 +2372,8 @@ class VideoDetailView : ConstraintLayout {
|
||||
}
|
||||
|
||||
fun isLandscapeVideo(): Boolean? {
|
||||
val videoSourceWidth = _player.exoPlayer?.player?.videoSize?.width
|
||||
val videoSourceHeight = _player.exoPlayer?.player?.videoSize?.height
|
||||
var videoSourceWidth = _player.exoPlayer?.player?.videoSize?.width
|
||||
var videoSourceHeight = _player.exoPlayer?.player?.videoSize?.height
|
||||
|
||||
return if (videoSourceWidth == null || videoSourceHeight == null || videoSourceWidth == 0 || videoSourceHeight == 0){
|
||||
null
|
||||
@@ -2587,6 +2575,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
_overlayContainer.removeAllViews();
|
||||
_overlay_quality_selector?.hide();
|
||||
|
||||
_player.setFullScreen(true)
|
||||
_player.fillHeight(false)
|
||||
_layoutPlayerContainer.setPadding(0, 0, 0, 0);
|
||||
}
|
||||
@@ -2801,7 +2790,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
if (fragment.state == VideoDetailFragment.State.MINIMIZED) {
|
||||
_player.fillHeight(true)
|
||||
} else if (!fragment.isFullscreen && !fragment.isInPictureInPicture) {
|
||||
} else if (!fragment.isFullscreen) {
|
||||
_player.fitHeight()
|
||||
}
|
||||
}
|
||||
@@ -3034,6 +3023,8 @@ class VideoDetailView : ConstraintLayout {
|
||||
const val TAG_MORE = "MORE";
|
||||
|
||||
private val _buttonPinStore = FragmentedStorage.get<StringArrayStorage>("videoPinnedButtons");
|
||||
private var _lastOfflinePlaybackToastTime: Long = 0
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -55,25 +55,21 @@ class ServiceRecordAggregator {
|
||||
if (_cts != null) throw Exception("Already started.")
|
||||
|
||||
_cts = CoroutineScope(Dispatchers.Default).launch {
|
||||
try {
|
||||
while (isActive) {
|
||||
val now = Date()
|
||||
synchronized(_currentServices) {
|
||||
_cachedAddressRecords.forEach { it.value.removeAll { record -> now.after(record.expirationTime) } }
|
||||
_cachedTxtRecords.entries.removeIf { now.after(it.value.expirationTime) }
|
||||
_cachedSrvRecords.entries.removeIf { now.after(it.value.expirationTime) }
|
||||
_cachedPtrRecords.forEach { it.value.removeAll { record -> now.after(record.expirationTime) } }
|
||||
while (isActive) {
|
||||
val now = Date()
|
||||
synchronized(_currentServices) {
|
||||
_cachedAddressRecords.forEach { it.value.removeAll { record -> now.after(record.expirationTime) } }
|
||||
_cachedTxtRecords.entries.removeIf { now.after(it.value.expirationTime) }
|
||||
_cachedSrvRecords.entries.removeIf { now.after(it.value.expirationTime) }
|
||||
_cachedPtrRecords.forEach { it.value.removeAll { record -> now.after(record.expirationTime) } }
|
||||
|
||||
val newServices = getCurrentServices()
|
||||
_currentServices.clear()
|
||||
_currentServices.addAll(newServices)
|
||||
}
|
||||
|
||||
onServicesUpdated?.invoke(_currentServices.toList())
|
||||
delay(5000)
|
||||
val newServices = getCurrentServices()
|
||||
_currentServices.clear()
|
||||
_currentServices.addAll(newServices)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Unexpected failure in MDNS loop", e)
|
||||
|
||||
onServicesUpdated?.invoke(_currentServices.toList())
|
||||
delay(5000)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -87,7 +83,6 @@ class ServiceRecordAggregator {
|
||||
}
|
||||
|
||||
fun add(packet: DnsPacket) {
|
||||
val currentServices: List<DnsService>
|
||||
val dnsResourceRecords = packet.answers + packet.additionals + packet.authorities
|
||||
val txtRecords = dnsResourceRecords.filter { it.type == ResourceRecordType.TXT.value.toInt() }.map { it to it.getDataReader().readTXTRecord() }
|
||||
val aRecords = dnsResourceRecords.filter { it.type == ResourceRecordType.A.value.toInt() }.map { it to it.getDataReader().readARecord() }
|
||||
@@ -104,33 +99,35 @@ class ServiceRecordAggregator {
|
||||
aaaaRecords.forEach { builder.appendLine("AAAA ${it.first.name} ${it.first.type} ${it.first.clazz} TTL ${it.first.timeToLive}: ${it.second.address}") }
|
||||
Logger.i(TAG, "$builder")*/
|
||||
|
||||
val currentServices: MutableList<DnsService>
|
||||
ptrRecords.forEach { record ->
|
||||
val cachedPtrRecord = _cachedPtrRecords.getOrPut(record.first.name) { mutableListOf() }
|
||||
val newPtrRecord = CachedDnsPtrRecord(Date(System.currentTimeMillis() + record.first.timeToLive.toLong() * 1000L), record.second.domainName)
|
||||
cachedPtrRecord.replaceOrAdd(newPtrRecord) { it.target == record.second.domainName }
|
||||
}
|
||||
|
||||
aRecords.forEach { aRecord ->
|
||||
val cachedARecord = _cachedAddressRecords.getOrPut(aRecord.first.name) { mutableListOf() }
|
||||
val newARecord = CachedDnsAddressRecord(Date(System.currentTimeMillis() + aRecord.first.timeToLive.toLong() * 1000L), aRecord.second.address)
|
||||
cachedARecord.replaceOrAdd(newARecord) { it.address == newARecord.address }
|
||||
}
|
||||
|
||||
aaaaRecords.forEach { aaaaRecord ->
|
||||
val cachedAaaaRecord = _cachedAddressRecords.getOrPut(aaaaRecord.first.name) { mutableListOf() }
|
||||
val newAaaaRecord = CachedDnsAddressRecord(Date(System.currentTimeMillis() + aaaaRecord.first.timeToLive.toLong() * 1000L), aaaaRecord.second.address)
|
||||
cachedAaaaRecord.replaceOrAdd(newAaaaRecord) { it.address == newAaaaRecord.address }
|
||||
}
|
||||
|
||||
txtRecords.forEach { txtRecord ->
|
||||
_cachedTxtRecords[txtRecord.first.name] = CachedDnsTxtRecord(Date(System.currentTimeMillis() + txtRecord.first.timeToLive.toLong() * 1000L), txtRecord.second.texts)
|
||||
}
|
||||
|
||||
srvRecords.forEach { srvRecord ->
|
||||
_cachedSrvRecords[srvRecord.first.name] = CachedDnsSrvRecord(Date(System.currentTimeMillis() + srvRecord.first.timeToLive.toLong() * 1000L), srvRecord.second)
|
||||
}
|
||||
|
||||
//TODO: Maybe this can be debounced?
|
||||
synchronized(this._currentServices) {
|
||||
ptrRecords.forEach { record ->
|
||||
val cachedPtrRecord = _cachedPtrRecords.getOrPut(record.first.name) { mutableListOf() }
|
||||
val newPtrRecord = CachedDnsPtrRecord(Date(System.currentTimeMillis() + record.first.timeToLive.toLong() * 1000L), record.second.domainName)
|
||||
cachedPtrRecord.replaceOrAdd(newPtrRecord) { it.target == record.second.domainName }
|
||||
}
|
||||
|
||||
aRecords.forEach { aRecord ->
|
||||
val cachedARecord = _cachedAddressRecords.getOrPut(aRecord.first.name) { mutableListOf() }
|
||||
val newARecord = CachedDnsAddressRecord(Date(System.currentTimeMillis() + aRecord.first.timeToLive.toLong() * 1000L), aRecord.second.address)
|
||||
cachedARecord.replaceOrAdd(newARecord) { it.address == newARecord.address }
|
||||
}
|
||||
|
||||
aaaaRecords.forEach { aaaaRecord ->
|
||||
val cachedAaaaRecord = _cachedAddressRecords.getOrPut(aaaaRecord.first.name) { mutableListOf() }
|
||||
val newAaaaRecord = CachedDnsAddressRecord(Date(System.currentTimeMillis() + aaaaRecord.first.timeToLive.toLong() * 1000L), aaaaRecord.second.address)
|
||||
cachedAaaaRecord.replaceOrAdd(newAaaaRecord) { it.address == newAaaaRecord.address }
|
||||
}
|
||||
|
||||
txtRecords.forEach { txtRecord ->
|
||||
_cachedTxtRecords[txtRecord.first.name] = CachedDnsTxtRecord(Date(System.currentTimeMillis() + txtRecord.first.timeToLive.toLong() * 1000L), txtRecord.second.texts)
|
||||
}
|
||||
|
||||
srvRecords.forEach { srvRecord ->
|
||||
_cachedSrvRecords[srvRecord.first.name] = CachedDnsSrvRecord(Date(System.currentTimeMillis() + srvRecord.first.timeToLive.toLong() * 1000L), srvRecord.second)
|
||||
}
|
||||
|
||||
currentServices = getCurrentServices()
|
||||
this._currentServices.clear()
|
||||
this._currentServices.addAll(currentServices)
|
||||
|
||||
@@ -12,113 +12,70 @@ import com.futo.platformplayer.activities.MainActivity
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.receivers.MediaControlReceiver
|
||||
import com.futo.platformplayer.timestampRegex
|
||||
import com.futo.platformplayer.views.behavior.NonScrollingTextView
|
||||
import com.futo.platformplayer.views.behavior.NonScrollingTextView.Companion
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
class PlatformLinkMovementMethod(private val _context: Context) : LinkMovementMethod() {
|
||||
class PlatformLinkMovementMethod : LinkMovementMethod {
|
||||
private val _context: Context;
|
||||
|
||||
private var pressedLinks: Array<URLSpan>? = null
|
||||
private var linkPressed = false
|
||||
private var downX = 0f
|
||||
private var downY = 0f
|
||||
private val touchSlop = 20
|
||||
constructor(context: Context) : super() {
|
||||
_context = context;
|
||||
}
|
||||
|
||||
override fun onTouchEvent(widget: TextView, buffer: Spannable, event: MotionEvent): Boolean {
|
||||
val action = event.actionMasked
|
||||
val action = event.action;
|
||||
Logger.i(TAG, "onTouchEvent (action = $action)")
|
||||
if (action == MotionEvent.ACTION_UP) {
|
||||
val x = event.x.toInt() - widget.totalPaddingLeft + widget.scrollX;
|
||||
val y = event.y.toInt() - widget.totalPaddingTop + widget.scrollY;
|
||||
|
||||
when (action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
val links = findLinksAtTouchPosition(widget, buffer, event)
|
||||
if (links.isNotEmpty()) {
|
||||
pressedLinks = links
|
||||
linkPressed = true
|
||||
downX = event.x
|
||||
downY = event.y
|
||||
widget.parent?.requestDisallowInterceptTouchEvent(true)
|
||||
return true
|
||||
} else {
|
||||
linkPressed = false
|
||||
pressedLinks = null
|
||||
}
|
||||
}
|
||||
val layout = widget.layout;
|
||||
val line = layout.getLineForVertical(y);
|
||||
val off = layout.getOffsetForHorizontal(line, x.toFloat());
|
||||
val links = buffer.getSpans(off, off, URLSpan::class.java);
|
||||
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
if (linkPressed) {
|
||||
val dx = event.x - downX
|
||||
val dy = event.y - downY
|
||||
if (Math.abs(dx) > touchSlop || Math.abs(dy) > touchSlop) {
|
||||
linkPressed = false
|
||||
pressedLinks = null
|
||||
widget.parent?.requestDisallowInterceptTouchEvent(false)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
if (links.isNotEmpty()) {
|
||||
runBlocking {
|
||||
for (link in links) {
|
||||
Logger.i(TAG) { "Link clicked '${link.url}'." };
|
||||
|
||||
MotionEvent.ACTION_UP -> {
|
||||
if (linkPressed && pressedLinks != null) {
|
||||
val dx = event.x - downX
|
||||
val dy = event.y - downY
|
||||
if (Math.abs(dx) <= touchSlop && Math.abs(dy) <= touchSlop && isTouchInside(widget, event)) {
|
||||
runBlocking {
|
||||
for (link in pressedLinks!!) {
|
||||
Logger.i(TAG) { "Link clicked '${link.url}'." }
|
||||
if (_context is MainActivity) {
|
||||
if (_context.handleUrl(link.url)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_context is MainActivity) {
|
||||
if (_context.handleUrl(link.url)) continue
|
||||
if (timestampRegex.matches(link.url)) {
|
||||
val tokens = link.url.split(':')
|
||||
var time_s = -1L
|
||||
when (tokens.size) {
|
||||
2 -> time_s = tokens[0].toLong() * 60 + tokens[1].toLong()
|
||||
3 -> time_s = tokens[0].toLong() * 3600 +
|
||||
tokens[1].toLong() * 60 +
|
||||
tokens[2].toLong()
|
||||
}
|
||||
if (timestampRegex.matches(link.url)) {
|
||||
val tokens = link.url.split(':');
|
||||
|
||||
if (time_s != -1L) {
|
||||
MediaControlReceiver.onSeekToReceived.emit(time_s * 1000)
|
||||
continue
|
||||
}
|
||||
}
|
||||
var time_s = -1L;
|
||||
if (tokens.size == 2) {
|
||||
time_s = tokens[0].toLong() * 60 + tokens[1].toLong();
|
||||
} else if (tokens.size == 3) {
|
||||
time_s =
|
||||
tokens[0].toLong() * 60 * 60 + tokens[1].toLong() * 60 + tokens[2].toLong();
|
||||
}
|
||||
|
||||
if (time_s != -1L) {
|
||||
MediaControlReceiver.onSeekToReceived.emit(time_s * 1000);
|
||||
continue;
|
||||
}
|
||||
_context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)))
|
||||
}
|
||||
}
|
||||
pressedLinks = null
|
||||
linkPressed = false
|
||||
return true
|
||||
} else {
|
||||
pressedLinks = null
|
||||
linkPressed = false
|
||||
|
||||
|
||||
_context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_CANCEL -> {
|
||||
linkPressed = false
|
||||
pressedLinks = null
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private fun findLinksAtTouchPosition(widget: TextView, buffer: Spannable, event: MotionEvent): Array<URLSpan> {
|
||||
val x = (event.x - widget.totalPaddingLeft + widget.scrollX).toInt()
|
||||
val y = (event.y - widget.totalPaddingTop + widget.scrollY).toInt()
|
||||
|
||||
val layout = widget.layout ?: return emptyArray()
|
||||
val line = layout.getLineForVertical(y)
|
||||
val off = layout.getOffsetForHorizontal(line, x.toFloat())
|
||||
return buffer.getSpans(off, off, URLSpan::class.java)
|
||||
}
|
||||
|
||||
private fun isTouchInside(widget: TextView, event: MotionEvent): Boolean {
|
||||
return event.x >= 0 && event.x <= widget.width && event.y >= 0 && event.y <= widget.height
|
||||
return super.onTouchEvent(widget, buffer, event);
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "PlatformLinkMovementMethod"
|
||||
val TAG = "PlatformLinkMovementMethod";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package com.futo.platformplayer.receivers
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.view.KeyEvent
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
|
||||
|
||||
class MediaButtonReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
val keyEvent: KeyEvent? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
intent?.getParcelableExtra(Intent.EXTRA_KEY_EVENT, KeyEvent::class.java)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
(intent?.getParcelableExtra(Intent.EXTRA_KEY_EVENT))
|
||||
}
|
||||
|
||||
Logger.i(TAG, "Received media button intent, keyCode: " + keyEvent?.keyCode)
|
||||
if (keyEvent != null && keyEvent.action == KeyEvent.ACTION_DOWN) {
|
||||
when (keyEvent.keyCode) {
|
||||
KeyEvent.KEYCODE_MEDIA_PLAY -> MediaControlReceiver.onPlayReceived.emit()
|
||||
KeyEvent.KEYCODE_MEDIA_PAUSE -> MediaControlReceiver.onPauseReceived.emit()
|
||||
KeyEvent.KEYCODE_MEDIA_NEXT -> MediaControlReceiver.onNextReceived.emit()
|
||||
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> MediaControlReceiver.onPreviousReceived.emit()
|
||||
KeyEvent.KEYCODE_MEDIA_STOP -> MediaControlReceiver.onCloseReceived.emit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = "MediaButtonReceiver"
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,6 @@ import android.support.v4.media.MediaMetadataCompat
|
||||
import android.support.v4.media.session.MediaSessionCompat
|
||||
import android.support.v4.media.session.PlaybackStateCompat
|
||||
import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.request.target.CustomTarget
|
||||
@@ -33,7 +32,6 @@ import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.activities.MainActivity
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.receivers.MediaButtonReceiver
|
||||
import com.futo.platformplayer.receivers.MediaControlReceiver
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.states.StatePlayer
|
||||
@@ -93,7 +91,6 @@ class MediaPlaybackService : Service() {
|
||||
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
fun setupNotificationRequirements() {
|
||||
_audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager;
|
||||
_notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
|
||||
@@ -104,7 +101,6 @@ class MediaPlaybackService : Service() {
|
||||
_notificationManager!!.createNotificationChannel(_notificationChannel!!);
|
||||
|
||||
_mediaSession = MediaSessionCompat(this, "PlayerState");
|
||||
_mediaSession?.isActive = true
|
||||
_mediaSession?.setPlaybackState(PlaybackStateCompat.Builder()
|
||||
.setState(PlaybackStateCompat.STATE_PLAYING, 0, 1f)
|
||||
.build());
|
||||
@@ -147,12 +143,6 @@ class MediaPlaybackService : Service() {
|
||||
MediaControlReceiver.onNextReceived.emit();
|
||||
}
|
||||
});
|
||||
_mediaSession?.setMediaButtonReceiver(PendingIntent.getBroadcast(
|
||||
this@MediaPlaybackService,
|
||||
0,
|
||||
Intent(Intent.ACTION_MEDIA_BUTTON).setClass(this@MediaPlaybackService, MediaButtonReceiver::class.java),
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
))
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
|
||||
@@ -47,7 +47,6 @@ import com.futo.platformplayer.services.DownloadService
|
||||
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 kotlinx.coroutines.*
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
@@ -157,9 +156,12 @@ class StateApp {
|
||||
|
||||
//Files
|
||||
private var _tempDirectory: File? = null;
|
||||
private var _cacheDirectory: File? = null;
|
||||
private var _persistentDirectory: File? = null;
|
||||
|
||||
|
||||
//AutoRotate
|
||||
var systemAutoRotate: Boolean = false;
|
||||
|
||||
//Network
|
||||
private var _lastMeteredState: Boolean = false;
|
||||
private var _connectivityManager: ConnectivityManager? = null;
|
||||
@@ -197,6 +199,17 @@ class StateApp {
|
||||
return File(_persistentDirectory, name);
|
||||
}
|
||||
|
||||
fun getCurrentSystemAutoRotate(): Boolean {
|
||||
_context?.let {
|
||||
systemAutoRotate = android.provider.Settings.System.getInt(
|
||||
it.contentResolver,
|
||||
android.provider.Settings.System.ACCELEROMETER_ROTATION, 0
|
||||
) == 1;
|
||||
};
|
||||
return systemAutoRotate;
|
||||
}
|
||||
|
||||
|
||||
fun isCurrentMetered(): Boolean {
|
||||
ensureConnectivityManager();
|
||||
return _connectivityManager?.isActiveNetworkMetered ?: throw IllegalStateException("Connectivity manager not available");
|
||||
@@ -297,6 +310,9 @@ class StateApp {
|
||||
fun setGlobalContext(context: Context, coroutineScope: CoroutineScope? = null) {
|
||||
_context = context;
|
||||
_scope = coroutineScope
|
||||
|
||||
//System checks
|
||||
systemAutoRotate = getCurrentSystemAutoRotate();
|
||||
}
|
||||
|
||||
fun initializeFiles(force: Boolean = false) {
|
||||
@@ -308,9 +324,6 @@ class StateApp {
|
||||
_tempDirectory?.deleteRecursively();
|
||||
}
|
||||
_tempDirectory?.mkdirs();
|
||||
_cacheDirectory = File(context.filesDir, "cache");
|
||||
if(_cacheDirectory?.exists() == false)
|
||||
_cacheDirectory?.mkdirs();
|
||||
_persistentDirectory = File(context.filesDir, "persist");
|
||||
if(_persistentDirectory?.exists() == false) {
|
||||
_persistentDirectory?.mkdirs();
|
||||
@@ -370,11 +383,6 @@ class StateApp {
|
||||
Logger.i(TAG, "MainApp Starting");
|
||||
initializeFiles(true);
|
||||
|
||||
if(Settings.instance.other.polycentricLocalCache) {
|
||||
Logger.i(TAG, "Initialize Polycentric Disk Cache")
|
||||
_cacheDirectory?.let { ApiMethods.initCache(it) };
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -251,7 +251,7 @@ class StateDownloads {
|
||||
}
|
||||
else {
|
||||
Logger.i(TAG, "New watchlater video ${item.name}");
|
||||
download(VideoDownload(item, playlistDownload.targetPxCount, playlistDownload.targetBitrate, true)
|
||||
download(VideoDownload(item, playlistDownload.targetPxCount, playlistDownload.targetBitrate)
|
||||
.withGroup(VideoDownload.GROUP_WATCHLATER, VideoDownload.GROUP_WATCHLATER), false);
|
||||
hasNew = true;
|
||||
}
|
||||
@@ -296,7 +296,7 @@ class StateDownloads {
|
||||
}
|
||||
else {
|
||||
Logger.i(TAG, "New playlist video ${item.name}");
|
||||
download(VideoDownload(item, playlistDownload.targetPxCount, playlistDownload.targetBitrate, true)
|
||||
download(VideoDownload(item, playlistDownload.targetPxCount, playlistDownload.targetBitrate)
|
||||
.withGroup(VideoDownload.GROUP_PLAYLIST, playlist.id), false);
|
||||
hasNew = true;
|
||||
}
|
||||
|
||||
@@ -59,7 +59,6 @@ class StateHistory {
|
||||
return getHistoryPosition(url) > duration * 0.7;
|
||||
}
|
||||
|
||||
private var _lastHistoryBroadcast = "";
|
||||
fun updateHistoryPosition(liveObj: IPlatformVideo, index: DBHistory.Index, updateExisting: Boolean, position: Long = -1L, date: OffsetDateTime? = null, isUserAction: Boolean = false): Long {
|
||||
val pos = if(position < 0) 0 else position;
|
||||
val historyVideo = index.obj;
|
||||
@@ -83,21 +82,19 @@ class StateHistory {
|
||||
historyVideo.date = date ?: OffsetDateTime.now();
|
||||
_historyDBStore.update(index.id!!, historyVideo);
|
||||
onHistoricVideoChanged.emit(liveObj, pos);
|
||||
}
|
||||
|
||||
|
||||
val historyBroadcastSig = "${historyVideo.position}${historyVideo.video.id.value ?: historyVideo.video.url}"
|
||||
if(isUserAction && _lastHistoryBroadcast != historyBroadcastSig) {
|
||||
_lastHistoryBroadcast = historyBroadcastSig;
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||
if(StateSync.instance.hasAtLeastOneOnlineDevice()) {
|
||||
Logger.i(TAG, "SyncHistory playback broadcasted (${liveObj.name}: ${position})");
|
||||
StateSync.instance.broadcastJsonData(
|
||||
GJSyncOpcodes.syncHistory,
|
||||
listOf(historyVideo)
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
if(isUserAction) {
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||
if(StateSync.instance.hasAtLeastOneOnlineDevice()) {
|
||||
Logger.i(TAG, "SyncHistory playback broadcasted (${liveObj.name}: ${position})");
|
||||
StateSync.instance.broadcastJsonData(
|
||||
GJSyncOpcodes.syncHistory,
|
||||
listOf(historyVideo)
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
return positionBefore;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import com.futo.platformplayer.activities.PolycentricHomeActivity
|
||||
import com.futo.platformplayer.api.media.PlatformID
|
||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||
import com.futo.platformplayer.api.media.models.comments.LazyComment
|
||||
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes
|
||||
@@ -47,7 +46,6 @@ import com.google.protobuf.ByteString
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import userpackage.Protocol
|
||||
@@ -55,7 +53,6 @@ import userpackage.Protocol.Reference
|
||||
import java.time.Instant
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneOffset
|
||||
import java.util.concurrent.ForkJoinPool
|
||||
|
||||
class StatePolycentric {
|
||||
private data class LikeDislikeEntry(val unixMilliseconds: Long, val hasLiked: Boolean, val hasDisliked: Boolean);
|
||||
@@ -66,9 +63,6 @@ class StatePolycentric {
|
||||
private var _transientEnabled = true
|
||||
val enabled get() = _transientEnabled && Settings.instance.other.polycentricEnabled
|
||||
|
||||
private val _commentPool = ForkJoinPool(2);
|
||||
private val _commentPoolDispatcher = _commentPool.asCoroutineDispatcher();
|
||||
|
||||
fun load(context: Context) {
|
||||
if (!enabled) {
|
||||
return
|
||||
@@ -516,7 +510,7 @@ class StatePolycentric {
|
||||
};
|
||||
}
|
||||
|
||||
private suspend fun mapQueryReferences(contextUrl: String, response: Protocol.QueryReferencesResponse): List<IPlatformComment> {
|
||||
private suspend fun mapQueryReferences(contextUrl: String, response: Protocol.QueryReferencesResponse): List<PolycentricPlatformComment> {
|
||||
return response.itemsList.mapNotNull {
|
||||
val sev = SignedEvent.fromProto(it.event);
|
||||
val ev = sev.event;
|
||||
@@ -530,53 +524,49 @@ class StatePolycentric {
|
||||
val dislikes = it.countsList[1];
|
||||
val replies = it.countsList[2];
|
||||
|
||||
val scope = StateApp.instance.scopeOrNull ?: return@mapNotNull null;
|
||||
return@mapNotNull LazyComment(scope.async(_commentPoolDispatcher){
|
||||
Logger.i(TAG, "Fetching comment data for [" + ev.system.key.toBase64() + "]");
|
||||
val profileEvents = ApiMethods.getQueryLatest(
|
||||
PolycentricCache.SERVER,
|
||||
ev.system.toProto(),
|
||||
listOf(
|
||||
ContentType.AVATAR.value,
|
||||
ContentType.USERNAME.value
|
||||
)
|
||||
).eventsList.map { e -> SignedEvent.fromProto(e) }.groupBy { e -> e.event.contentType }
|
||||
.map { (_, events) -> events.maxBy { x -> x.event.unixMilliseconds ?: 0 } };
|
||||
val profileEvents = ApiMethods.getQueryLatest(
|
||||
PolycentricCache.SERVER,
|
||||
ev.system.toProto(),
|
||||
listOf(
|
||||
ContentType.AVATAR.value,
|
||||
ContentType.USERNAME.value
|
||||
)
|
||||
).eventsList.map { e -> SignedEvent.fromProto(e) }.groupBy { e -> e.event.contentType }
|
||||
.map { (_, events) -> events.maxBy { x -> x.event.unixMilliseconds ?: 0 } };
|
||||
|
||||
val nameEvent = profileEvents.firstOrNull { e -> e.event.contentType == ContentType.USERNAME.value };
|
||||
val avatarEvent = profileEvents.firstOrNull { e -> e.event.contentType == ContentType.AVATAR.value };
|
||||
val imageBundle = if (avatarEvent != null) {
|
||||
val lwwElementValue = avatarEvent.event.lwwElement?.value;
|
||||
if (lwwElementValue != null) {
|
||||
Protocol.ImageBundle.parseFrom(lwwElementValue)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val nameEvent = profileEvents.firstOrNull { e -> e.event.contentType == ContentType.USERNAME.value };
|
||||
val avatarEvent = profileEvents.firstOrNull { e -> e.event.contentType == ContentType.AVATAR.value };
|
||||
val imageBundle = if (avatarEvent != null) {
|
||||
val lwwElementValue = avatarEvent.event.lwwElement?.value;
|
||||
if (lwwElementValue != null) {
|
||||
Protocol.ImageBundle.parseFrom(lwwElementValue)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val unixMilliseconds = ev.unixMilliseconds
|
||||
//TODO: Don't use single hardcoded sderver here
|
||||
val systemLinkUrl = ev.system.systemToURLInfoSystemLinkUrl(listOf(PolycentricCache.SERVER));
|
||||
val dp_25 = 25.dp(StateApp.instance.context.resources)
|
||||
return@async PolycentricPlatformComment(
|
||||
contextUrl = contextUrl,
|
||||
author = PlatformAuthorLink(
|
||||
id = PlatformID("polycentric", systemLinkUrl, null, ClaimType.POLYCENTRIC.value.toInt()),
|
||||
name = nameEvent?.event?.lwwElement?.value?.decodeToString() ?: "Unknown",
|
||||
url = systemLinkUrl,
|
||||
thumbnail = imageBundle?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(ev.system.toProto(), img.process, listOf(PolycentricCache.SERVER)) },
|
||||
subscribers = null
|
||||
),
|
||||
msg = if (post.content.count() > PolycentricPlatformComment.MAX_COMMENT_SIZE) post.content.substring(0, PolycentricPlatformComment.MAX_COMMENT_SIZE) else post.content,
|
||||
rating = RatingLikeDislikes(likes, dislikes),
|
||||
date = if (unixMilliseconds != null) Instant.ofEpochMilli(unixMilliseconds).atOffset(ZoneOffset.UTC) else OffsetDateTime.MIN,
|
||||
replyCount = replies.toInt(),
|
||||
eventPointer = sev.toPointer(),
|
||||
parentReference = sev.event.references.getOrNull(0)
|
||||
);
|
||||
});
|
||||
val unixMilliseconds = ev.unixMilliseconds
|
||||
//TODO: Don't use single hardcoded sderver here
|
||||
val systemLinkUrl = ev.system.systemToURLInfoSystemLinkUrl(listOf(PolycentricCache.SERVER));
|
||||
val dp_25 = 25.dp(StateApp.instance.context.resources)
|
||||
return@mapNotNull PolycentricPlatformComment(
|
||||
contextUrl = contextUrl,
|
||||
author = PlatformAuthorLink(
|
||||
id = PlatformID("polycentric", systemLinkUrl, null, ClaimType.POLYCENTRIC.value.toInt()),
|
||||
name = nameEvent?.event?.lwwElement?.value?.decodeToString() ?: "Unknown",
|
||||
url = systemLinkUrl,
|
||||
thumbnail = imageBundle?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(ev.system.toProto(), img.process, listOf(PolycentricCache.SERVER)) },
|
||||
subscribers = null
|
||||
),
|
||||
msg = if (post.content.count() > PolycentricPlatformComment.MAX_COMMENT_SIZE) post.content.substring(0, PolycentricPlatformComment.MAX_COMMENT_SIZE) else post.content,
|
||||
rating = RatingLikeDislikes(likes, dislikes),
|
||||
date = if (unixMilliseconds != null) Instant.ofEpochMilli(unixMilliseconds).atOffset(ZoneOffset.UTC) else OffsetDateTime.MIN,
|
||||
replyCount = replies.toInt(),
|
||||
eventPointer = sev.toPointer(),
|
||||
parentReference = sev.event.references.getOrNull(0)
|
||||
);
|
||||
} catch (e: Throwable) {
|
||||
return@mapNotNull null;
|
||||
}
|
||||
|
||||
@@ -112,23 +112,18 @@ class StateSync {
|
||||
Logger.i(TAG, "Sync key pair initialized (public key = ${publicKey})")
|
||||
|
||||
_thread = Thread {
|
||||
try {
|
||||
val serverSocket = ServerSocket(PORT)
|
||||
_serverSocket = serverSocket
|
||||
val serverSocket = ServerSocket(PORT)
|
||||
_serverSocket = serverSocket
|
||||
|
||||
Log.i(TAG, "Running on port ${PORT} (TCP)")
|
||||
Log.i(TAG, "Running on port ${PORT} (TCP)")
|
||||
|
||||
while (_started) {
|
||||
val socket = serverSocket.accept()
|
||||
val session = createSocketSession(socket, true) { session, socketSession ->
|
||||
while (_started) {
|
||||
val socket = serverSocket.accept()
|
||||
val session = createSocketSession(socket, true) { session, socketSession ->
|
||||
|
||||
}
|
||||
|
||||
session.startAsResponder()
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to bind server socket to port ${PORT}", e)
|
||||
UIDialogs.toast("Failed to start sync, port in use")
|
||||
|
||||
session.startAsResponder()
|
||||
}
|
||||
}.apply { start() }
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||
import com.futo.platformplayer.api.media.models.comments.LazyComment
|
||||
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
|
||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes
|
||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||
@@ -25,7 +24,6 @@ import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StatePolycentric
|
||||
import com.futo.platformplayer.toHumanNowDiffString
|
||||
import com.futo.platformplayer.toHumanNumber
|
||||
import com.futo.platformplayer.views.LoaderView
|
||||
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||
import com.futo.platformplayer.views.pills.PillButton
|
||||
import com.futo.platformplayer.views.pills.PillRatingLikesDislikes
|
||||
@@ -48,9 +46,6 @@ class CommentViewHolder : ViewHolder {
|
||||
private val _layoutComment: ConstraintLayout;
|
||||
private val _buttonDelete: FrameLayout;
|
||||
|
||||
private val _containerComments: ConstraintLayout;
|
||||
private val _loader: LoaderView;
|
||||
|
||||
var onRepliesClick = Event1<IPlatformComment>();
|
||||
var onDelete = Event1<IPlatformComment>();
|
||||
var onAuthorClick = Event1<IPlatformComment>();
|
||||
@@ -72,9 +67,6 @@ class CommentViewHolder : ViewHolder {
|
||||
_pillRatingLikesDislikes = itemView.findViewById(R.id.rating);
|
||||
_buttonDelete = itemView.findViewById(R.id.button_delete);
|
||||
|
||||
_containerComments = itemView.findViewById(R.id.comment_container);
|
||||
_loader = itemView.findViewById(R.id.loader);
|
||||
|
||||
_pillRatingLikesDislikes.onLikeDislikeUpdated.subscribe { args ->
|
||||
val c = comment
|
||||
if (c !is PolycentricPlatformComment) {
|
||||
@@ -131,33 +123,6 @@ class CommentViewHolder : ViewHolder {
|
||||
}
|
||||
|
||||
fun bind(comment: IPlatformComment, readonly: Boolean) {
|
||||
|
||||
if(comment is LazyComment){
|
||||
if(comment.isAvailable)
|
||||
{
|
||||
comment.getUnderlyingComment()?.let {
|
||||
bind(it, readonly);
|
||||
}
|
||||
return;
|
||||
}
|
||||
else {
|
||||
_loader.visibility = View.VISIBLE;
|
||||
_loader.start();
|
||||
_containerComments.visibility = View.GONE;
|
||||
comment.setUIHandler {
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
||||
if (it.isAvailable && it == this@CommentViewHolder.comment)
|
||||
bind(it, readonly);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
_loader.stop();
|
||||
_loader.visibility = View.GONE;
|
||||
_containerComments.visibility = View.VISIBLE;
|
||||
}
|
||||
|
||||
_creatorThumbnail.setThumbnail(comment.author.thumbnail, false);
|
||||
val polycentricComment = if (comment is PolycentricPlatformComment) comment else null
|
||||
_creatorThumbnail.setHarborAvailable(polycentricComment != null,false, polycentricComment?.eventPointer?.system?.toProto());
|
||||
|
||||
@@ -24,7 +24,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class AnnouncementView : LinearLayout {
|
||||
private val _root: FrameLayout;
|
||||
private val _root: ConstraintLayout;
|
||||
private val _textTitle: TextView;
|
||||
private val _textCounter: TextView;
|
||||
private val _textBody: TextView;
|
||||
@@ -45,6 +45,9 @@ class AnnouncementView : LinearLayout {
|
||||
|
||||
_scope = findViewTreeLifecycleOwner()?.lifecycleScope ?: StateApp.instance.scopeOrNull; //TODO: Fetch correct scope
|
||||
|
||||
val dp10 = 10.dp(resources);
|
||||
setPadding(dp10, dp10, dp10, dp10);
|
||||
|
||||
_root = findViewById(R.id.root);
|
||||
_textTitle = findViewById(R.id.text_title);
|
||||
_textCounter = findViewById(R.id.text_counter);
|
||||
@@ -112,12 +115,12 @@ class AnnouncementView : LinearLayout {
|
||||
_currentAnnouncement = announcement;
|
||||
|
||||
if (announcement == null) {
|
||||
_root.visibility = View.GONE
|
||||
visibility = View.GONE
|
||||
onClose.emit()
|
||||
return;
|
||||
}
|
||||
|
||||
_root.visibility = View.VISIBLE
|
||||
visibility = View.VISIBLE
|
||||
|
||||
_textTitle.text = announcement.title;
|
||||
_textBody.text = announcement.msg;
|
||||
|
||||
@@ -16,117 +16,73 @@ import com.futo.platformplayer.timestampRegex
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
class NonScrollingTextView : androidx.appcompat.widget.AppCompatTextView {
|
||||
private var _lastTouchedLinks: Array<URLSpan>? = null
|
||||
private var downX = 0f
|
||||
private var downY = 0f
|
||||
private var linkPressed = false
|
||||
private val touchSlop = 20
|
||||
|
||||
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 scrollTo(x: Int, y: Int) {
|
||||
// do nothing
|
||||
//do nothing
|
||||
}
|
||||
|
||||
override fun onTouchEvent(event: MotionEvent?): Boolean {
|
||||
val action = event?.actionMasked
|
||||
if (event == null) return super.onTouchEvent(event)
|
||||
val action = event?.action
|
||||
Logger.i(TAG, "onTouchEvent (action = $action)");
|
||||
|
||||
when (action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
val x = event.x.toInt()
|
||||
val y = event.y.toInt()
|
||||
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
|
||||
val x = event.x.toInt()
|
||||
val y = event.y.toInt()
|
||||
|
||||
val layout: Layout? = this.layout
|
||||
if (layout != null && this.text is Spannable) {
|
||||
val offset = layout.getOffsetForHorizontal(layout.getLineForVertical(y), x.toFloat())
|
||||
val text = this.text as Spannable
|
||||
val layout: Layout? = this.layout
|
||||
if (layout != null) {
|
||||
val line = layout.getLineForVertical(y)
|
||||
val offset = layout.getOffsetForHorizontal(line, x.toFloat())
|
||||
|
||||
val text = this.text
|
||||
if (text is Spannable) {
|
||||
val links = text.getSpans(offset, offset, URLSpan::class.java)
|
||||
if (links.isNotEmpty()) {
|
||||
parent?.requestDisallowInterceptTouchEvent(true)
|
||||
_lastTouchedLinks = links
|
||||
downX = event.x
|
||||
downY = event.y
|
||||
linkPressed = true
|
||||
return true
|
||||
} else {
|
||||
linkPressed = false
|
||||
_lastTouchedLinks = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
if (linkPressed) {
|
||||
val dx = event.x - downX
|
||||
val dy = event.y - downY
|
||||
if (Math.abs(dx) > touchSlop || Math.abs(dy) > touchSlop) {
|
||||
linkPressed = false
|
||||
_lastTouchedLinks = null
|
||||
parent?.requestDisallowInterceptTouchEvent(false)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_UP -> {
|
||||
if (linkPressed && _lastTouchedLinks != null) {
|
||||
val dx = event.x - downX
|
||||
val dy = event.y - downY
|
||||
if (Math.abs(dx) <= touchSlop && Math.abs(dy) <= touchSlop && isTouchInside(event)) {
|
||||
runBlocking {
|
||||
for (link in _lastTouchedLinks!!) {
|
||||
Logger.i(PlatformLinkMovementMethod.TAG) { "Link clicked '${link.url}'." }
|
||||
val c = context
|
||||
for (link in links) {
|
||||
Logger.i(PlatformLinkMovementMethod.TAG) { "Link clicked '${link.url}'." };
|
||||
|
||||
val c = context;
|
||||
if (c is MainActivity) {
|
||||
if (c.handleUrl(link.url)) continue
|
||||
if (c.handleUrl(link.url)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (timestampRegex.matches(link.url)) {
|
||||
val tokens = link.url.split(':')
|
||||
var time_s = -1L
|
||||
when (tokens.size) {
|
||||
2 -> time_s = tokens[0].toLong() * 60 + tokens[1].toLong()
|
||||
3 -> time_s = tokens[0].toLong() * 3600 +
|
||||
tokens[1].toLong() * 60 +
|
||||
tokens[2].toLong()
|
||||
val tokens = link.url.split(':');
|
||||
|
||||
var time_s = -1L;
|
||||
if (tokens.size == 2) {
|
||||
time_s = tokens[0].toLong() * 60 + tokens[1].toLong();
|
||||
} else if (tokens.size == 3) {
|
||||
time_s = tokens[0].toLong() * 60 * 60 + tokens[1].toLong() * 60 + tokens[2].toLong();
|
||||
}
|
||||
|
||||
if (time_s != -1L) {
|
||||
MediaControlReceiver.onSeekToReceived.emit(time_s * 1000)
|
||||
continue
|
||||
MediaControlReceiver.onSeekToReceived.emit(time_s * 1000);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
c.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)))
|
||||
} else {
|
||||
c.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)))
|
||||
|
||||
c.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)));
|
||||
}
|
||||
}
|
||||
}
|
||||
_lastTouchedLinks = null
|
||||
linkPressed = false
|
||||
|
||||
return true
|
||||
} else {
|
||||
linkPressed = false
|
||||
_lastTouchedLinks = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_CANCEL -> {
|
||||
linkPressed = false
|
||||
_lastTouchedLinks = null
|
||||
}
|
||||
}
|
||||
|
||||
super.onTouchEvent(event)
|
||||
return false
|
||||
}
|
||||
|
||||
private fun isTouchInside(event: MotionEvent): Boolean {
|
||||
return event.x >= 0 && event.x <= width && event.y >= 0 && event.y <= height
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "NonScrollingTextView"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,7 +90,7 @@ class PillRatingLikesDislikes : LinearLayout {
|
||||
setRating(rating, hasLiked, hasDisliked);
|
||||
}
|
||||
is RatingLikes -> {
|
||||
setRating(rating, hasLiked);
|
||||
setRating(rating, hasLiked, hasDisliked);
|
||||
}
|
||||
else -> {
|
||||
throw Exception("Unknown rating type");
|
||||
@@ -98,36 +98,6 @@ class PillRatingLikesDislikes : LinearLayout {
|
||||
}
|
||||
}
|
||||
|
||||
fun setRating(rating: RatingLikeDislikes, hasLiked: Boolean = false, hasDisliked: Boolean = false) {
|
||||
setLoading(false)
|
||||
|
||||
_textLikes.text = rating.likes.toHumanNumber();
|
||||
_textDislikes.text = rating.dislikes.toHumanNumber();
|
||||
_textLikes.visibility = View.VISIBLE;
|
||||
_textDislikes.visibility = View.VISIBLE;
|
||||
_seperator.visibility = View.VISIBLE;
|
||||
_iconDislikes.visibility = View.VISIBLE;
|
||||
_likes = rating.likes;
|
||||
_dislikes = rating.dislikes;
|
||||
_hasLiked = hasLiked;
|
||||
_hasDisliked = hasDisliked;
|
||||
updateColors();
|
||||
}
|
||||
fun setRating(rating: RatingLikes, hasLiked: Boolean = false) {
|
||||
setLoading(false)
|
||||
|
||||
_textLikes.text = rating.likes.toHumanNumber();
|
||||
_textLikes.visibility = View.VISIBLE;
|
||||
_textDislikes.visibility = View.GONE;
|
||||
_seperator.visibility = View.GONE;
|
||||
_iconDislikes.visibility = View.GONE;
|
||||
_likes = rating.likes;
|
||||
_dislikes = 0;
|
||||
_hasLiked = hasLiked;
|
||||
_hasDisliked = false;
|
||||
updateColors();
|
||||
}
|
||||
|
||||
fun like(processHandle: ProcessHandle) {
|
||||
if (_hasDisliked) {
|
||||
_dislikes--;
|
||||
@@ -185,4 +155,34 @@ class PillRatingLikesDislikes : LinearLayout {
|
||||
_iconDislikes.setColorFilter(ContextCompat.getColor(context, R.color.white));
|
||||
}
|
||||
}
|
||||
|
||||
fun setRating(rating: RatingLikeDislikes, hasLiked: Boolean = false, hasDisliked: Boolean = false) {
|
||||
setLoading(false)
|
||||
|
||||
_textLikes.text = rating.likes.toHumanNumber();
|
||||
_textDislikes.text = rating.dislikes.toHumanNumber();
|
||||
_textLikes.visibility = View.VISIBLE;
|
||||
_textDislikes.visibility = View.VISIBLE;
|
||||
_seperator.visibility = View.VISIBLE;
|
||||
_iconDislikes.visibility = View.VISIBLE;
|
||||
_likes = rating.likes;
|
||||
_dislikes = rating.dislikes;
|
||||
_hasLiked = hasLiked;
|
||||
_hasDisliked = hasDisliked;
|
||||
updateColors();
|
||||
}
|
||||
fun setRating(rating: RatingLikes, hasLiked: Boolean = false) {
|
||||
setLoading(false)
|
||||
|
||||
_textLikes.text = rating.likes.toHumanNumber();
|
||||
_textLikes.visibility = View.VISIBLE;
|
||||
_textDislikes.visibility = View.GONE;
|
||||
_seperator.visibility = View.GONE;
|
||||
_iconDislikes.visibility = View.GONE;
|
||||
_likes = rating.likes;
|
||||
_dislikes = 0;
|
||||
_hasLiked = hasLiked;
|
||||
_hasDisliked = false;
|
||||
updateColors();
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,6 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||
import com.futo.platformplayer.api.media.models.comments.LazyComment
|
||||
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||
import com.futo.platformplayer.api.media.structures.IAsyncPager
|
||||
@@ -268,13 +267,9 @@ class CommentsList : ConstraintLayout {
|
||||
}
|
||||
|
||||
fun replaceComment(c: PolycentricPlatformComment, newComment: PolycentricPlatformComment) {
|
||||
val index = _comments.indexOfFirst { it == c || (it is LazyComment && it.getUnderlyingComment() == c) };
|
||||
if (index >= 0) {
|
||||
_comments[index] = newComment;
|
||||
_adapterComments.notifyItemChanged(_adapterComments.childToParentPosition(index));
|
||||
} else {
|
||||
Logger.w(TAG, "Parent comment not found")
|
||||
}
|
||||
val index = _comments.indexOf(c);
|
||||
_comments[index] = newComment;
|
||||
_adapterComments.notifyItemChanged(_adapterComments.childToParentPosition(index));
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -592,11 +592,6 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
fun setFullScreen(fullScreen: Boolean) {
|
||||
// prevent fullscreen before the video has loaded to make sure we know whether it's a vertical or horizontal video
|
||||
if(exoPlayer?.player?.videoSize?.height ?: 0 == 0 && fullScreen){
|
||||
return
|
||||
}
|
||||
|
||||
updateRotateLock()
|
||||
|
||||
if (isFullScreen == fullScreen) {
|
||||
@@ -607,7 +602,6 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
val lp = background.layoutParams as ConstraintLayout.LayoutParams;
|
||||
lp.bottomMargin = 0;
|
||||
background.layoutParams = lp;
|
||||
_videoView.setBackgroundColor(Color.parseColor("#FF000000"))
|
||||
|
||||
gestureControl.hideControls();
|
||||
//videoControlsBar.visibility = View.GONE;
|
||||
@@ -621,7 +615,6 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
val lp = background.layoutParams as ConstraintLayout.LayoutParams;
|
||||
lp.bottomMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 6.0f, Resources.getSystem().displayMetrics).toInt();
|
||||
background.layoutParams = lp;
|
||||
_videoView.setBackgroundColor(Color.parseColor("#00000000"))
|
||||
|
||||
gestureControl.hideControls();
|
||||
//videoControlsBar.visibility = View.VISIBLE;
|
||||
@@ -755,12 +748,12 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
|
||||
if (_lastSourceFit == null || windowWidth != _lastWindowWidth || windowHeight != _lastWindowHeight) {
|
||||
val maxHeight = windowHeight * 0.4f
|
||||
val minHeight = windowHeight * 0.1f
|
||||
|
||||
val determinedHeight = windowWidth / w.toFloat() * h.toFloat()
|
||||
val aspectRatio = h.toFloat() / w
|
||||
val determinedHeight = (aspectRatio * windowWidth)
|
||||
|
||||
_lastSourceFit = determinedHeight
|
||||
_lastSourceFit = _lastSourceFit!!.coerceAtLeast(minHeight)
|
||||
_lastSourceFit = _lastSourceFit!!.coerceAtLeast(220f)
|
||||
_lastSourceFit = _lastSourceFit!!.coerceAtMost(maxHeight)
|
||||
|
||||
_desiredResizeModePortrait = if (_lastSourceFit != determinedHeight)
|
||||
@@ -775,13 +768,14 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
|
||||
val marginBottom =
|
||||
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 7f, resources.displayMetrics)
|
||||
.toInt()
|
||||
val height = TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP,
|
||||
_lastSourceFit!!,
|
||||
resources.displayMetrics
|
||||
)
|
||||
val rootParams = LayoutParams(LayoutParams.MATCH_PARENT, (height + marginBottom).toInt())
|
||||
rootParams.bottomMargin = marginBottom.toInt()
|
||||
rootParams.bottomMargin = marginBottom
|
||||
_root.layoutParams = rootParams
|
||||
isFitMode = true
|
||||
}
|
||||
@@ -813,12 +807,17 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
}
|
||||
|
||||
fun updateRotateLock() {
|
||||
_control_rotate_lock.visibility = View.VISIBLE;
|
||||
_control_rotate_lock_fullscreen.visibility = View.VISIBLE;
|
||||
|
||||
if(Settings.instance.playback.autoRotate == 0) {
|
||||
_control_rotate_lock.visibility = View.GONE;
|
||||
_control_rotate_lock_fullscreen.visibility = View.GONE;
|
||||
}
|
||||
else {
|
||||
_control_rotate_lock.visibility = View.VISIBLE;
|
||||
_control_rotate_lock_fullscreen.visibility = View.VISIBLE;
|
||||
}
|
||||
if(StatePlayer.instance.rotationLock) {
|
||||
_control_rotate_lock_fullscreen.setImageResource(R.drawable.ic_screen_lock_rotation_active);
|
||||
_control_rotate_lock.setImageResource(R.drawable.ic_screen_lock_rotation_active);
|
||||
_control_rotate_lock_fullscreen.setImageResource(R.drawable.ic_screen_rotation);
|
||||
_control_rotate_lock.setImageResource(R.drawable.ic_screen_rotation);
|
||||
}
|
||||
else {
|
||||
_control_rotate_lock_fullscreen.setImageResource(R.drawable.ic_screen_lock_rotation);
|
||||
|
||||
@@ -3,11 +3,14 @@ package com.futo.platformplayer.views.video
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.AttributeSet
|
||||
import android.util.Xml
|
||||
import android.widget.RelativeLayout
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.fragment.app.findFragment
|
||||
import androidx.lifecycle.coroutineScope
|
||||
import androidx.lifecycle.findViewTreeLifecycleOwner
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.C.Encoding
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.PlaybackException
|
||||
import androidx.media3.common.Player
|
||||
@@ -19,9 +22,9 @@ import androidx.media3.datasource.DefaultHttpDataSource
|
||||
import androidx.media3.datasource.HttpDataSource
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.exoplayer.dash.DashMediaSource
|
||||
import androidx.media3.exoplayer.dash.manifest.DashManifest
|
||||
import androidx.media3.exoplayer.dash.manifest.DashManifestParser
|
||||
import androidx.media3.exoplayer.drm.DefaultDrmSessionManager
|
||||
import androidx.media3.exoplayer.drm.HttpMediaDrmCallback
|
||||
import androidx.media3.exoplayer.drm.DefaultDrmSessionManagerProvider
|
||||
import androidx.media3.exoplayer.hls.HlsMediaSource
|
||||
import androidx.media3.exoplayer.source.MediaSource
|
||||
import androidx.media3.exoplayer.source.MergingMediaSource
|
||||
@@ -31,18 +34,18 @@ import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.api.media.models.chapters.IChapter
|
||||
import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.AudioUrlSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlWidevineSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestWidevineSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudioSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlWidevineSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.LocalAudioSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.LocalVideoSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.VideoUrlSource
|
||||
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSAudioUrlRangeSource
|
||||
@@ -52,13 +55,15 @@ import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManif
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSHLSManifestAudioSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSVideoUrlRangeSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSVideoUrlSource
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.engine.dev.V8RemoteObject
|
||||
import com.futo.platformplayer.helpers.VideoHelper
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.video.PlayerManager
|
||||
import com.futo.platformplayer.views.video.datasources.PluginMediaDrmCallback
|
||||
import com.futo.platformplayer.views.video.datasources.JSHttpDataSource
|
||||
import com.google.gson.Gson
|
||||
import getHttpDataSourceFactory
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -412,11 +417,9 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
val didSet = when(videoSource) {
|
||||
is LocalVideoSource -> { swapVideoSourceLocal(videoSource); true; }
|
||||
is JSVideoUrlRangeSource -> { swapVideoSourceUrlRange(videoSource); true; }
|
||||
is IDashManifestWidevineSource -> { swapVideoSourceDashWidevine(videoSource); true }
|
||||
is IDashManifestSource -> { swapVideoSourceDash(videoSource); true;}
|
||||
is JSDashManifestRawSource -> swapVideoSourceDashRaw(videoSource, play, resume);
|
||||
is IHLSManifestSource -> { swapVideoSourceHLS(videoSource); true; }
|
||||
is IVideoUrlWidevineSource -> { swapVideoSourceUrlWidevine(videoSource); true; }
|
||||
is IVideoUrlSource -> { swapVideoSourceUrl(videoSource); true; }
|
||||
null -> { _lastVideoMediaSource = null; true;}
|
||||
else -> throw IllegalArgumentException("Unsupported video source [${videoSource.javaClass.simpleName}]");
|
||||
@@ -481,32 +484,6 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
.createMediaSource(MediaItem.fromUri(videoSource.getVideoUrl()));
|
||||
}
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun swapVideoSourceUrlWidevine(videoSource: IVideoUrlWidevineSource) {
|
||||
Logger.i(TAG, "Loading VideoSource [UrlWidevine]");
|
||||
val dataSource = if(videoSource is JSSource && videoSource.requiresCustomDatasource)
|
||||
videoSource.getHttpDataSourceFactory()
|
||||
else
|
||||
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT)
|
||||
|
||||
val baseCallback = HttpMediaDrmCallback(videoSource.licenseUri, dataSource)
|
||||
|
||||
val callback = if (videoSource.hasLicenseRequestExecutor) {
|
||||
PluginMediaDrmCallback(baseCallback, videoSource.getLicenseRequestExecutor()!!, videoSource.licenseUri)
|
||||
} else {
|
||||
baseCallback
|
||||
}
|
||||
|
||||
_lastVideoMediaSource = ProgressiveMediaSource.Factory(dataSource)
|
||||
.setDrmSessionManagerProvider {
|
||||
DefaultDrmSessionManager.Builder()
|
||||
.setMultiSession(true)
|
||||
.build(callback)
|
||||
}
|
||||
.createMediaSource(
|
||||
MediaItem.fromUri(videoSource.getVideoUrl())
|
||||
)
|
||||
}
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun swapVideoSourceDash(videoSource: IDashManifestSource) {
|
||||
Logger.i(TAG, "Loading VideoSource [Dash]");
|
||||
val dataSource = if(videoSource is JSSource && (videoSource.requiresCustomDatasource))
|
||||
@@ -517,25 +494,6 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
.createMediaSource(MediaItem.fromUri(videoSource.url))
|
||||
}
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun swapVideoSourceDashWidevine(videoSource: IDashManifestWidevineSource) {
|
||||
Logger.i(TAG, "Loading VideoSource [DashWidevine]")
|
||||
val dataSource =
|
||||
if (videoSource is JSSource && (videoSource.requiresCustomDatasource)) videoSource.getHttpDataSourceFactory()
|
||||
else DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT)
|
||||
|
||||
val baseCallback = HttpMediaDrmCallback(videoSource.licenseUri, dataSource)
|
||||
|
||||
val callback = if (videoSource.hasLicenseRequestExecutor) {
|
||||
PluginMediaDrmCallback(baseCallback, videoSource.getLicenseRequestExecutor()!!, videoSource.licenseUri)
|
||||
} else {
|
||||
baseCallback
|
||||
}
|
||||
|
||||
_lastVideoMediaSource = DashMediaSource.Factory(dataSource).setDrmSessionManagerProvider {
|
||||
DefaultDrmSessionManager.Builder().setMultiSession(true).build(callback)
|
||||
}.createMediaSource(MediaItem.fromUri(videoSource.url))
|
||||
}
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun swapVideoSourceDashRaw(videoSource: JSDashManifestRawSource, play: Boolean, resume: Boolean): Boolean {
|
||||
Logger.i(TAG, "Loading VideoSource [Dash]");
|
||||
|
||||
@@ -681,7 +639,6 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun swapAudioSourceUrlWidevine(audioSource: IAudioUrlWidevineSource) {
|
||||
Logger.i(TAG, "Loading AudioSource [UrlWidevine]")
|
||||
@@ -690,22 +647,20 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
else
|
||||
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT)
|
||||
|
||||
val baseCallback = HttpMediaDrmCallback(audioSource.licenseUri, dataSource)
|
||||
|
||||
val callback = if (audioSource.hasLicenseRequestExecutor) {
|
||||
PluginMediaDrmCallback(baseCallback, audioSource.getLicenseRequestExecutor()!!, audioSource.licenseUri)
|
||||
} else {
|
||||
baseCallback
|
||||
}
|
||||
|
||||
val httpRequestHeaders = mapOf("Authorization" to "Bearer " + audioSource.bearerToken)
|
||||
val provider = DefaultDrmSessionManagerProvider()
|
||||
provider.setDrmHttpDataSourceFactory(dataSource)
|
||||
_lastAudioMediaSource = ProgressiveMediaSource.Factory(dataSource)
|
||||
.setDrmSessionManagerProvider {
|
||||
DefaultDrmSessionManager.Builder()
|
||||
.setMultiSession(true)
|
||||
.build(callback)
|
||||
}
|
||||
.setDrmSessionManagerProvider(provider)
|
||||
.createMediaSource(
|
||||
MediaItem.fromUri(audioSource.getAudioUrl())
|
||||
MediaItem.Builder()
|
||||
.setUri(audioSource.getAudioUrl()).setDrmConfiguration(
|
||||
MediaItem.DrmConfiguration.Builder(C.WIDEVINE_UUID)
|
||||
.setLicenseUri(audioSource.licenseUri)
|
||||
.setMultiSession(true)
|
||||
.setLicenseRequestHeaders(httpRequestHeaders)
|
||||
.build()
|
||||
).build()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -360,7 +360,7 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
|
||||
if(executor != null) {
|
||||
try {
|
||||
Logger.Companion.i(TAG, "Executor for " + dataSpec.uri.toString(), null);
|
||||
byte[] data = executor.executeRequest("GET", dataSpec.uri.toString(), null, dataSpec.httpRequestHeaders);
|
||||
byte[] data = executor.executeRequest(dataSpec.uri.toString(), dataSpec.httpRequestHeaders);
|
||||
Logger.Companion.i(TAG, "Executor result for " + dataSpec.uri.toString() + " : " + data.length, null);
|
||||
if (data == null)
|
||||
throw new HttpDataSourceException(
|
||||
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
package com.futo.platformplayer.views.video.datasources
|
||||
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.drm.ExoMediaDrm
|
||||
import androidx.media3.exoplayer.drm.MediaDrmCallback
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
|
||||
import java.util.UUID
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
|
||||
@UnstableApi
|
||||
class PluginMediaDrmCallback(
|
||||
private val delegate: MediaDrmCallback,
|
||||
private val requestExecutor: JSRequestExecutor,
|
||||
private val licenseUrl: String
|
||||
) : MediaDrmCallback by delegate {
|
||||
|
||||
@ExperimentalEncodingApi
|
||||
override fun executeKeyRequest(uuid: UUID, request: ExoMediaDrm.KeyRequest): ByteArray {
|
||||
val pluginResponse = requestExecutor.executeRequest("POST", licenseUrl, request.data, mapOf())
|
||||
|
||||
return pluginResponse
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="48"
|
||||
android:viewportHeight="48">
|
||||
<path
|
||||
android:fillColor="@color/colorPrimary"
|
||||
android:pathData="M35.25,23.3 L36.4,22.2 38.6,24.45Q39.5,25.3 39.5,26.55Q39.5,27.8 38.6,28.65L32.5,34.75Q31.6,35.6 30.375,35.6Q29.15,35.6 28.3,34.75L13.8,20.25Q12.95,19.4 12.95,18.15Q12.95,16.9 13.8,16.1L19.95,9.95Q20.8,9.1 22.05,9.1Q23.3,9.1 24.1,9.95L26.45,12.25L25.3,13.35L22.9,10.95Q22.55,10.6 22.025,10.6Q21.5,10.6 21.15,10.95L14.85,17.25Q14.5,17.6 14.5,18.15Q14.5,18.7 14.85,19.05L29.5,33.75Q29.85,34.1 30.4,34.1Q30.95,34.1 31.25,33.75L37.6,27.4Q37.95,27.05 37.95,26.525Q37.95,26 37.6,25.65ZM26.15,44.45Q21.6,44.45 17.6,42.725Q13.6,41 10.625,38.025Q7.65,35.05 5.925,31.025Q4.2,27 4.2,22.5H5.75Q5.75,26.65 7.325,30.35Q8.9,34.05 11.65,36.825Q14.4,39.6 18.125,41.2Q21.85,42.8 26,42.85L18.55,35.35L19.65,34.25L29.5,44.1Q28.6,44.25 27.8,44.35Q27,44.45 26.15,44.45ZM32.4,18Q31.7,18 31.1,17.4Q30.5,16.8 30.5,16.1V10.85Q30.5,10.15 31.1,9.525Q31.7,8.9 32.4,8.9H32.55V6.85Q32.55,5.4 33.575,4.4Q34.6,3.4 36.05,3.4Q37.55,3.4 38.55,4.4Q39.55,5.4 39.55,6.85V8.9H39.75Q40.45,8.9 41,9.525Q41.55,10.15 41.55,10.85V16.1Q41.55,16.8 40.975,17.4Q40.4,18 39.65,18ZM34.05,8.9H38.05V6.85Q38.05,6 37.475,5.425Q36.9,4.85 36.05,4.85Q35.2,4.85 34.625,5.425Q34.05,6 34.05,6.85ZM26.25,22.35Q26.25,22.35 26.25,22.35Q26.25,22.35 26.25,22.35Q26.25,22.35 26.25,22.35Q26.25,22.35 26.25,22.35Q26.25,22.35 26.25,22.35Q26.25,22.35 26.25,22.35Q26.25,22.35 26.25,22.35Q26.25,22.35 26.25,22.35Q26.25,22.35 26.25,22.35Q26.25,22.35 26.25,22.35Z"/>
|
||||
</vector>
|
||||
@@ -35,12 +35,6 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.futo.platformplayer.views.announcements.AnnouncementView
|
||||
android:id="@+id/announcement_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/container_sort_by"
|
||||
android:layout_width="match_parent"
|
||||
@@ -116,8 +110,7 @@
|
||||
android:visibility="gone"
|
||||
android:id="@+id/empty_pager_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="30dp" />
|
||||
android:layout_height="match_parent" />
|
||||
</FrameLayout>
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
|
||||
@@ -11,179 +11,161 @@
|
||||
android:layout_marginEnd="14dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.futo.platformplayer.views.LoaderView
|
||||
android:id="@+id/loader"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
<com.futo.platformplayer.views.others.CreatorThumbnail
|
||||
android:id="@+id/image_thumbnail"
|
||||
android:layout_width="25dp"
|
||||
android:layout_height="25dp"
|
||||
android:contentDescription="@string/channel_image"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
android:layout_marginTop="50dp"
|
||||
android:layout_marginBottom="50dp" />
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@drawable/placeholder_channel_thumbnail" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/comment_container"
|
||||
android:layout_width="match_parent"
|
||||
<TextView
|
||||
android:id="@+id/text_author"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent">
|
||||
<com.futo.platformplayer.views.others.CreatorThumbnail
|
||||
android:id="@+id/image_thumbnail"
|
||||
android:layout_width="25dp"
|
||||
android:layout_height="25dp"
|
||||
android:contentDescription="@string/channel_image"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@drawable/placeholder_channel_thumbnail" />
|
||||
android:layout_marginStart="10dp"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center_vertical"
|
||||
android:maxLines="1"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintLeft_toRightOf="@id/image_thumbnail"
|
||||
app:layout_constraintTop_toTopOf="@id/image_thumbnail"
|
||||
tools:text="ShortCircuit" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_author"
|
||||
<TextView
|
||||
android:id="@+id/text_metadata"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center_vertical"
|
||||
android:maxLines="1"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:textColor="@color/gray_ac"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/text_author"
|
||||
app:layout_constraintLeft_toRightOf="@id/text_author"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/text_author"
|
||||
tools:text=" • 3 years ago" />
|
||||
|
||||
<com.futo.platformplayer.views.behavior.NonScrollingTextView
|
||||
android:id="@+id/text_body"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginStart="10dp"
|
||||
android:background="@color/transparent"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:isScrollContainer="false"
|
||||
android:textColor="#CCCCCC"
|
||||
android:textSize="13sp"
|
||||
android:maxLines="100"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_metadata"
|
||||
app:layout_constraintLeft_toRightOf="@id/image_thumbnail"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
tools:text="@string/lorem_ipsum" />
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintLeft_toLeftOf="@id/text_body"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_body"
|
||||
android:layout_marginLeft="-10dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<com.futo.platformplayer.views.pills.PillRatingLikesDislikes
|
||||
android:id="@+id/rating"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="10dp"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center_vertical"
|
||||
android:maxLines="1"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintLeft_toRightOf="@id/image_thumbnail"
|
||||
app:layout_constraintTop_toTopOf="@id/image_thumbnail"
|
||||
tools:text="ShortCircuit" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_metadata"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center_vertical"
|
||||
android:maxLines="1"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:textColor="@color/gray_ac"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/text_author"
|
||||
app:layout_constraintLeft_toRightOf="@id/text_author"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/text_author"
|
||||
tools:text=" • 3 years ago" />
|
||||
|
||||
<com.futo.platformplayer.views.behavior.NonScrollingTextView
|
||||
android:id="@+id/text_body"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginStart="10dp"
|
||||
android:background="@color/transparent"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:isScrollContainer="false"
|
||||
android:textColor="#CCCCCC"
|
||||
android:textSize="13sp"
|
||||
android:maxLines="100"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_metadata"
|
||||
app:layout_constraintLeft_toRightOf="@id/image_thumbnail"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
tools:text="@string/lorem_ipsum" />
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:layout_marginStart="9dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="0dp"
|
||||
android:id="@+id/layout_rating"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintLeft_toLeftOf="@id/text_body"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginStart="10dp"
|
||||
app:layout_constraintLeft_toRightOf="@id/image_thumbnail"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_body"
|
||||
android:layout_marginLeft="-10dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<com.futo.platformplayer.views.pills.PillRatingLikesDislikes
|
||||
android:id="@+id/rating"
|
||||
<ImageView
|
||||
android:id="@+id/image_like_icon"
|
||||
android:layout_width="18dp"
|
||||
android:layout_height="18dp"
|
||||
android:contentDescription="@string/cd_image_like_icon"
|
||||
app:srcCompat="@drawable/ic_thumb_up" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_likes"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:layout_marginStart="9dp" />
|
||||
android:layout_height="18dp"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginStart="8dp"
|
||||
tools:text="500K"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="13dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_rating"
|
||||
<ImageView
|
||||
android:id="@+id/image_dislike_icon"
|
||||
android:layout_width="18dp"
|
||||
android:layout_height="18dp"
|
||||
android:contentDescription="@string/cd_image_dislike_icon"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="2dp"
|
||||
app:srcCompat="@drawable/ic_thumb_down" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_dislikes"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginStart="10dp"
|
||||
app:layout_constraintLeft_toRightOf="@id/image_thumbnail"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_body"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image_like_icon"
|
||||
android:layout_width="18dp"
|
||||
android:layout_height="18dp"
|
||||
android:contentDescription="@string/cd_image_like_icon"
|
||||
app:srcCompat="@drawable/ic_thumb_up" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_likes"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="18dp"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginStart="8dp"
|
||||
tools:text="500K"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="13dp" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image_dislike_icon"
|
||||
android:layout_width="18dp"
|
||||
android:layout_height="18dp"
|
||||
android:contentDescription="@string/cd_image_dislike_icon"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="2dp"
|
||||
app:srcCompat="@drawable/ic_thumb_down" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_dislikes"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="18dp"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginStart="8dp"
|
||||
tools:text="500K"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="13dp" />
|
||||
</LinearLayout>
|
||||
<com.futo.platformplayer.views.pills.PillButton
|
||||
android:id="@+id/button_replies"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/cd_button_replies"
|
||||
app:pillIcon="@drawable/ic_forum"
|
||||
app:pillText="55 Replies"
|
||||
android:layout_marginStart="15dp" />
|
||||
|
||||
<Space android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/button_delete"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/background_pill_pred"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:layout_marginStart="12dp">
|
||||
<TextView
|
||||
android:id="@+id/pill_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="13dp"
|
||||
android:gravity="center_vertical"
|
||||
android:fontFamily="@font/inter_light"
|
||||
android:text="@string/delete" />
|
||||
</FrameLayout>
|
||||
android:layout_height="18dp"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginStart="8dp"
|
||||
tools:text="500K"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="13dp" />
|
||||
</LinearLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
<com.futo.platformplayer.views.pills.PillButton
|
||||
android:id="@+id/button_replies"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/cd_button_replies"
|
||||
app:pillIcon="@drawable/ic_forum"
|
||||
app:pillText="55 Replies"
|
||||
android:layout_marginStart="15dp" />
|
||||
|
||||
<Space android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/button_delete"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/background_pill_pred"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:layout_marginStart="12dp">
|
||||
<TextView
|
||||
android:id="@+id/pill_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="13dp"
|
||||
android:gravity="center_vertical"
|
||||
android:fontFamily="@font/inter_light"
|
||||
android:text="@string/delete" />
|
||||
</FrameLayout>
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -1,122 +1,119 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/root">
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical"
|
||||
android:id="@+id/root"
|
||||
android:background="@drawable/background_16_round_4dp"
|
||||
android:paddingLeft="10dp"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingRight="10dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
<TextView android:id="@+id/text_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="Do you know?"
|
||||
android:fontFamily="@font/inter_semibold"
|
||||
android:textSize="15sp"
|
||||
android:textColor="@color/white"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintRight_toLeftOf="@id/text_counter" />
|
||||
|
||||
<TextView android:id="@+id/text_counter"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="1/4"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:textSize="12dp"
|
||||
android:textColor="#585656"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent" />
|
||||
|
||||
<TextView android:id="@+id/text_body"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="Our app now supports dark mode for a better viewing experience. Check it out in your settings. Enjoy the new look!"
|
||||
android:fontFamily="@font/inter_light"
|
||||
android:textSize="14sp"
|
||||
android:textColor="#9D9D9D"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_title"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/background_16_round_4dp"
|
||||
android:paddingLeft="10dp"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingRight="10dp"
|
||||
android:layout_margin="10dp">
|
||||
app:layout_constraintTop_toBottomOf="@id/text_body"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingBottom="10dp">
|
||||
|
||||
<TextView android:id="@+id/text_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="Do you know?"
|
||||
android:fontFamily="@font/inter_semibold"
|
||||
android:textSize="15sp"
|
||||
android:textColor="@color/white"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintRight_toLeftOf="@id/text_counter" />
|
||||
|
||||
<TextView android:id="@+id/text_counter"
|
||||
<TextView android:id="@+id/text_time"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="1/4"
|
||||
tools:text="2022-03-01"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:textSize="12dp"
|
||||
android:textColor="#585656"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent" />
|
||||
|
||||
<TextView android:id="@+id/text_body"
|
||||
<Space android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<TextView android:id="@+id/text_never"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="Our app now supports dark mode for a better viewing experience. Check it out in your settings. Enjoy the new look!"
|
||||
android:fontFamily="@font/inter_light"
|
||||
android:text="@string/never"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:textSize="14sp"
|
||||
android:textColor="#9D9D9D"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_title"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/colorPrimary"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:paddingLeft="20dp"
|
||||
android:paddingRight="20dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_body"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingBottom="10dp">
|
||||
app:layout_constraintRight_toLeftOf="@id/text_close"/>
|
||||
|
||||
<TextView android:id="@+id/text_time"
|
||||
<TextView android:id="@+id/text_close"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/dismiss"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:textSize="14sp"
|
||||
android:textColor="@color/colorPrimary"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:paddingLeft="20dp"
|
||||
android:paddingRight="20dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_body"
|
||||
app:layout_constraintRight_toLeftOf="@id/button_action"/>
|
||||
|
||||
<FrameLayout android:id="@+id/button_action"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/background_button_primary_round_4dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_body"
|
||||
app:layout_constraintRight_toRightOf="parent">
|
||||
|
||||
<TextView android:id="@+id/text_action"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="2022-03-01"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:textSize="12dp"
|
||||
android:textColor="#585656"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent" />
|
||||
|
||||
<Space android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<TextView android:id="@+id/text_never"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/never"
|
||||
tools:text="What's New"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:textSize="14sp"
|
||||
android:textColor="@color/colorPrimary"
|
||||
android:textColor="@color/white"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:paddingLeft="20dp"
|
||||
android:paddingRight="20dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_body"
|
||||
app:layout_constraintRight_toLeftOf="@id/text_close"/>
|
||||
</FrameLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<TextView android:id="@+id/text_close"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/dismiss"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:textSize="14sp"
|
||||
android:textColor="@color/colorPrimary"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:paddingLeft="20dp"
|
||||
android:paddingRight="20dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_body"
|
||||
app:layout_constraintRight_toLeftOf="@id/button_action"/>
|
||||
|
||||
<FrameLayout android:id="@+id/button_action"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/background_button_primary_round_4dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_body"
|
||||
app:layout_constraintRight_toRightOf="parent">
|
||||
|
||||
<TextView android:id="@+id/text_action"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="What's New"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:textSize="14sp"
|
||||
android:textColor="@color/white"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:paddingLeft="20dp"
|
||||
android:paddingRight="20dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_body"
|
||||
app:layout_constraintRight_toLeftOf="@id/text_close"/>
|
||||
</FrameLayout>
|
||||
</LinearLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</FrameLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -233,6 +233,8 @@
|
||||
<string name="announcement">إعلان</string>
|
||||
<string name="attempt_to_utilize_byte_ranges">محاولة استخدام مدى البايت</string>
|
||||
<string name="auto_update">تحديث تلقائي</string>
|
||||
<string name="auto_rotate">تدوير تلقائي</string>
|
||||
<string name="auto_rotate_dead_zone">منطقة ميتة للتدوير التلقائي</string>
|
||||
<string name="automatic_backup">نسخ احتياطي تلقائي</string>
|
||||
<string name="background_behavior">سلوك الخلفية</string>
|
||||
<string name="background_update">تحديث الخلفية</string>
|
||||
@@ -537,6 +539,7 @@
|
||||
<string name="not_yet_available_retrying_in_time_s">لم يصبح متوفراً بعد، إعادة المحاولة في {time}s</string>
|
||||
<string name="failed_to_retry_for_live_stream">فشل في إعادة المحاولة للبث المباشر</string>
|
||||
<string name="this_app_is_in_development_please_submit_bug_reports_and_understand_that_many_features_are_incomplete">هذا التطبيق قيد التطوير. يرجى إرسال تقارير الأخطاء وفهم أن العديد من الميزات غير مكتملة.</string>
|
||||
<string name="please_use_at_least_3_characters">يرجى استخدام 3 أحرف على الأقل</string>
|
||||
<string name="are_you_sure_you_want_to_delete_this_video">هل أنت متأكد من أنك ترغب في حذف هذا الفيديو؟</string>
|
||||
<string name="tap_to_open">انقر للفتح</string>
|
||||
<string name="watching">يشاهد</string>
|
||||
@@ -658,6 +661,11 @@
|
||||
<item>عند التشغيل</item>
|
||||
<item>أبداً</item>
|
||||
</string-array>
|
||||
<string-array name="system_enabled_disabled_array">
|
||||
<item>معطل</item>
|
||||
<item>مفعل</item>
|
||||
<item>كما في النظام</item>
|
||||
</string-array>
|
||||
<string-array name="enabled_disabled_array">
|
||||
<item>معطل</item>
|
||||
<item>مفعل</item>
|
||||
|
||||
@@ -243,6 +243,8 @@
|
||||
<string name="announcement">Ankündigung</string>
|
||||
<string name="attempt_to_utilize_byte_ranges">Versuch, Byte-Bereiche zu nutzen</string>
|
||||
<string name="auto_update">Automatische Aktualisierung</string>
|
||||
<string name="auto_rotate">Automatische Drehung</string>
|
||||
<string name="auto_rotate_dead_zone">Toter Winkel für automatische Drehung</string>
|
||||
<string name="automatic_backup">Automatisches Backup</string>
|
||||
<string name="background_behavior">Hintergrundverhalten</string>
|
||||
<string name="background_update">Hintergrundaktualisierung</string>
|
||||
@@ -540,6 +542,7 @@
|
||||
<string name="not_yet_available_retrying_in_time_s">Noch nicht verfügbar, erneuter Versuch in {time}s</string>
|
||||
<string name="failed_to_retry_for_live_stream">Fehler beim erneuten Versuch für den Live-Stream</string>
|
||||
<string name="this_app_is_in_development_please_submit_bug_reports_and_understand_that_many_features_are_incomplete">Diese App befindet sich in der Entwicklung. Bitte senden Sie Fehlerberichte und verstehen Sie, dass viele Funktionen unvollständig sind.</string>
|
||||
<string name="please_use_at_least_3_characters">Bitte verwenden Sie mindestens 3 Zeichen</string>
|
||||
<string name="are_you_sure_you_want_to_delete_this_video">Sind Sie sicher, dass Sie dieses Video löschen möchten?</string>
|
||||
<string name="tap_to_open">Tippen Sie zum Öffnen</string>
|
||||
<string name="watching">anschauen</string>
|
||||
@@ -658,6 +661,11 @@
|
||||
<item>Beim Start</item>
|
||||
<item>Nie</item>
|
||||
</string-array>
|
||||
<string-array name="system_enabled_disabled_array">
|
||||
<item>Deaktiviert</item>
|
||||
<item>Aktiviert</item>
|
||||
<item>Gleich wie System</item>
|
||||
</string-array>
|
||||
<string-array name="enabled_disabled_array">
|
||||
<item>Deaktiviert</item>
|
||||
<item>Aktiviert</item>
|
||||
|
||||
@@ -217,6 +217,8 @@
|
||||
<string name="announcement">Anuncio</string>
|
||||
<string name="attempt_to_utilize_byte_ranges">Intentar utilizar rangos de bytes</string>
|
||||
<string name="auto_update">Actualización automática</string>
|
||||
<string name="auto_rotate">Auto-rotar</string>
|
||||
<string name="auto_rotate_dead_zone">Zona muerta de auto-rotación</string>
|
||||
<string name="automatic_backup">Copia de seguridad automática</string>
|
||||
<string name="background_behavior">Comportamiento en segundo plano</string>
|
||||
<string name="background_update">Actualización en segundo plano</string>
|
||||
@@ -521,6 +523,7 @@
|
||||
<string name="not_yet_available_retrying_in_time_s">Todavía no está disponible, reintento en {time}s</string>
|
||||
<string name="failed_to_retry_for_live_stream">Error al reintentar la transmisión en vivo</string>
|
||||
<string name="this_app_is_in_development_please_submit_bug_reports_and_understand_that_many_features_are_incomplete">Esta aplicación está en desarrollo. Por favor, envía informes de errores y comprende que muchas características están incompletas.</string>
|
||||
<string name="please_use_at_least_3_characters">Por favor, usa al menos 3 caracteres</string>
|
||||
<string name="are_you_sure_you_want_to_delete_this_video">¿Estás seguro de que deseas eliminar este video?</string>
|
||||
<string name="tap_to_open">Toca para abrir</string>
|
||||
<string name="watching">viendo</string>
|
||||
@@ -668,6 +671,17 @@
|
||||
<item>Al Iniciar</item>
|
||||
<item>Nunca</item>
|
||||
</string-array>
|
||||
<string-array name="system_enabled_disabled_array">
|
||||
<item>Desactivado</item>
|
||||
<item>Activado</item>
|
||||
<item>Mismo que el Sistema</item>
|
||||
</string-array>
|
||||
<string-array name="auto_rotate_dead_zone">
|
||||
<item>0</item>
|
||||
<item>5</item>
|
||||
<item>10</item>
|
||||
<item>20</item>
|
||||
</string-array>
|
||||
<string-array name="enabled_disabled_array">
|
||||
<item>Desactivado</item>
|
||||
<item>Activado</item>
|
||||
|
||||
@@ -256,6 +256,8 @@
|
||||
<string name="announcement">Annonce</string>
|
||||
<string name="attempt_to_utilize_byte_ranges">Tentative d\'utilisation de plages d\'octets</string>
|
||||
<string name="auto_update">Mise à jour automatique</string>
|
||||
<string name="auto_rotate">Rotation automatique</string>
|
||||
<string name="auto_rotate_dead_zone">Zone morte de rotation automatique</string>
|
||||
<string name="automatic_backup">Sauvegarde automatique</string>
|
||||
<string name="background_behavior">Comportement en arrière-plan</string>
|
||||
<string name="background_update">Mise à jour en arrière-plan</string>
|
||||
@@ -560,6 +562,7 @@
|
||||
<string name="not_yet_available_retrying_in_time_s">Pas encore disponible, réessai dans {time}s</string>
|
||||
<string name="failed_to_retry_for_live_stream">Échec de la tentative de réessai pour la diffusion en direct</string>
|
||||
<string name="this_app_is_in_development_please_submit_bug_reports_and_understand_that_many_features_are_incomplete">Cette application est en développement. Veuillez soumettre des rapports de bug et comprendre que de nombreuses fonctionnalités ne sont pas encore complètes.</string>
|
||||
<string name="please_use_at_least_3_characters">Veuillez utiliser au moins 3 caractères</string>
|
||||
<string name="are_you_sure_you_want_to_delete_this_video">Êtes-vous sûr de vouloir supprimer cette vidéo ?</string>
|
||||
<string name="tap_to_open">Appuyez pour ouvrir</string>
|
||||
<string name="watching">en train de regarder</string>
|
||||
@@ -658,6 +661,11 @@
|
||||
<item>Au démarrage</item>
|
||||
<item>Jamais</item>
|
||||
</string-array>
|
||||
<string-array name="system_enabled_disabled_array">
|
||||
<item>Désactivé</item>
|
||||
<item>Activé</item>
|
||||
<item>Même que le système</item>
|
||||
</string-array>
|
||||
<string-array name="enabled_disabled_array">
|
||||
<item>Désactivé</item>
|
||||
<item>Activé</item>
|
||||
|
||||
@@ -220,6 +220,8 @@
|
||||
<string name="announcement">お知らせ</string>
|
||||
<string name="attempt_to_utilize_byte_ranges">バイト範囲を使用する試み</string>
|
||||
<string name="auto_update">自動更新</string>
|
||||
<string name="auto_rotate">自動回転</string>
|
||||
<string name="auto_rotate_dead_zone">自動回転デッドゾーン</string>
|
||||
<string name="automatic_backup">自動バックアップ</string>
|
||||
<string name="background_behavior">バックグラウンドの動作</string>
|
||||
<string name="background_update">バックグラウンド更新</string>
|
||||
@@ -522,6 +524,7 @@
|
||||
<string name="not_yet_available_retrying_in_time_s">{time}秒後に再試行、まだ利用できません</string>
|
||||
<string name="failed_to_retry_for_live_stream">ライブストリームの再試行に失敗しました</string>
|
||||
<string name="this_app_is_in_development_please_submit_bug_reports_and_understand_that_many_features_are_incomplete">このアプリは開発中です。バグレポートを提出し、多くの機能が未完成であることを理解してください。</string>
|
||||
<string name="please_use_at_least_3_characters">少なくとも3文字を使用してください</string>
|
||||
<string name="are_you_sure_you_want_to_delete_this_video">このビデオを削除してもよろしいですか?</string>
|
||||
<string name="tap_to_open">タップして開く</string>
|
||||
<string name="watching">視聴中</string>
|
||||
@@ -658,6 +661,11 @@
|
||||
<item>起動時</item>
|
||||
<item>なし</item>
|
||||
</string-array>
|
||||
<string-array name="system_enabled_disabled_array">
|
||||
<item>無効</item>
|
||||
<item>有効</item>
|
||||
<item>システムと同じ</item>
|
||||
</string-array>
|
||||
<string-array name="enabled_disabled_array">
|
||||
<item>無効</item>
|
||||
<item>有効</item>
|
||||
|
||||
@@ -255,6 +255,8 @@
|
||||
<string name="announcement">공고</string>
|
||||
<string name="attempt_to_utilize_byte_ranges">바이트 범위 사용 시도</string>
|
||||
<string name="auto_update">자동 업데이트</string>
|
||||
<string name="auto_rotate">자동 회전</string>
|
||||
<string name="auto_rotate_dead_zone">자동 회전 데드 존</string>
|
||||
<string name="automatic_backup">자동 백업</string>
|
||||
<string name="background_behavior">백그라운드 동작</string>
|
||||
<string name="background_update">백그라운드 업데이트</string>
|
||||
@@ -559,6 +561,7 @@
|
||||
<string name="not_yet_available_retrying_in_time_s">아직 사용할 수 없습니다, {time}초 후에 다시 시도합니다</string>
|
||||
<string name="failed_to_retry_for_live_stream">라이브 스트림을 다시 시도하지 못했습니다</string>
|
||||
<string name="this_app_is_in_development_please_submit_bug_reports_and_understand_that_many_features_are_incomplete">이 앱은 개발 중입니다. 버그 보고를 제출해 주시고, 많은 기능이 미완성임을 이해해 주세요.</string>
|
||||
<string name="please_use_at_least_3_characters">최소 3자 이상 사용해 주세요</string>
|
||||
<string name="are_you_sure_you_want_to_delete_this_video">이 비디오를 삭제하시겠습니까?</string>
|
||||
<string name="tap_to_open">열려면 탭하세요</string>
|
||||
<string name="watching">시청 중</string>
|
||||
@@ -658,6 +661,11 @@
|
||||
<item>시작할 때</item>
|
||||
<item>안 함</item>
|
||||
</string-array>
|
||||
<string-array name="system_enabled_disabled_array">
|
||||
<item>비활성화</item>
|
||||
<item>활성화</item>
|
||||
<item>시스템과 동일</item>
|
||||
</string-array>
|
||||
<string-array name="enabled_disabled_array">
|
||||
<item>비활성화</item>
|
||||
<item>활성화</item>
|
||||
|
||||
@@ -256,6 +256,8 @@
|
||||
<string name="announcement">Anúncio</string>
|
||||
<string name="attempt_to_utilize_byte_ranges">Tentar utilizar intervalos de bytes</string>
|
||||
<string name="auto_update">Atualização Automática</string>
|
||||
<string name="auto_rotate">Rotação Automática</string>
|
||||
<string name="auto_rotate_dead_zone">Zona Morta de Rotação Automática</string>
|
||||
<string name="automatic_backup">Backup Automático</string>
|
||||
<string name="background_behavior">Comportamento em Segundo Plano</string>
|
||||
<string name="background_update">Atualização em Segundo Plano</string>
|
||||
@@ -555,6 +557,7 @@
|
||||
<string name="not_yet_available_retrying_in_time_s">Ainda não disponível, tentando novamente em {time}s</string>
|
||||
<string name="failed_to_retry_for_live_stream">Falha ao tentar novamente para transmissão ao vivo</string>
|
||||
<string name="this_app_is_in_development_please_submit_bug_reports_and_understand_that_many_features_are_incomplete">Este aplicativo está em desenvolvimento. Envie relatórios de erros e entenda que muitos recursos estão incompletos.</string>
|
||||
<string name="please_use_at_least_3_characters">Use pelo menos 3 caracteres</string>
|
||||
<string name="are_you_sure_you_want_to_delete_this_video">Tem certeza de que deseja excluir este vídeo?</string>
|
||||
<string name="tap_to_open">Toque para abrir</string>
|
||||
<string name="watching">Assistindo</string>
|
||||
@@ -658,6 +661,11 @@
|
||||
<item>Ao Iniciar</item>
|
||||
<item>Nunca</item>
|
||||
</string-array>
|
||||
<string-array name="system_enabled_disabled_array">
|
||||
<item>Desativado</item>
|
||||
<item>Ativado</item>
|
||||
<item>Como no Sistema</item>
|
||||
</string-array>
|
||||
<string-array name="enabled_disabled_array">
|
||||
<item>Desativado</item>
|
||||
<item>Ativado</item>
|
||||
|
||||
@@ -252,6 +252,8 @@
|
||||
<string name="announcement">Объявление</string>
|
||||
<string name="attempt_to_utilize_byte_ranges">Попытка использовать диапазоны байт</string>
|
||||
<string name="auto_update">Автообновление</string>
|
||||
<string name="auto_rotate">Автоповорот</string>
|
||||
<string name="auto_rotate_dead_zone">Мертвая зона автоповорота</string>
|
||||
<string name="automatic_backup">Автоматическое резервное копирование</string>
|
||||
<string name="background_behavior">Поведение в фоновом режиме</string>
|
||||
<string name="background_update">Фоновое обновление</string>
|
||||
@@ -556,6 +558,7 @@
|
||||
<string name="not_yet_available_retrying_in_time_s">Ещё недоступно, повторная попытка через {time}с</string>
|
||||
<string name="failed_to_retry_for_live_stream">Не удалось повторить попытку для прямого эфира</string>
|
||||
<string name="this_app_is_in_development_please_submit_bug_reports_and_understand_that_many_features_are_incomplete">Это приложение находится в стадии разработки. Пожалуйста, отправляйте сообщения об ошибках и поймите, что многие функции незавершены.</string>
|
||||
<string name="please_use_at_least_3_characters">Пожалуйста, используйте хотя бы 3 символа</string>
|
||||
<string name="are_you_sure_you_want_to_delete_this_video">Вы уверены, что хотите удалить это видео?</string>
|
||||
<string name="tap_to_open">Нажмите, чтобы открыть</string>
|
||||
<string name="watching">Смотрят</string>
|
||||
@@ -658,6 +661,11 @@
|
||||
<item>При запуске</item>
|
||||
<item>Никогда</item>
|
||||
</string-array>
|
||||
<string-array name="system_enabled_disabled_array">
|
||||
<item>Отключено</item>
|
||||
<item>Включено</item>
|
||||
<item>Как в системе</item>
|
||||
</string-array>
|
||||
<string-array name="enabled_disabled_array">
|
||||
<item>Отключено</item>
|
||||
<item>Включено</item>
|
||||
|
||||
@@ -256,6 +256,8 @@
|
||||
<string name="announcement">公告</string>
|
||||
<string name="attempt_to_utilize_byte_ranges">尝试使用字节范围</string>
|
||||
<string name="auto_update">自动更新</string>
|
||||
<string name="auto_rotate">自动旋转</string>
|
||||
<string name="auto_rotate_dead_zone">自动旋转死区</string>
|
||||
<string name="automatic_backup">自动备份</string>
|
||||
<string name="background_behavior">后台行为</string>
|
||||
<string name="background_update">后台更新</string>
|
||||
@@ -560,6 +562,7 @@
|
||||
<string name="not_yet_available_retrying_in_time_s">尚未可用,将在{time}s后重试</string>
|
||||
<string name="failed_to_retry_for_live_stream">无法重新尝试直播流</string>
|
||||
<string name="this_app_is_in_development_please_submit_bug_reports_and_understand_that_many_features_are_incomplete">此应用处于开发中。请提交错误报告,并理解许多功能尚未完成。</string>
|
||||
<string name="please_use_at_least_3_characters">请至少使用3个字符</string>
|
||||
<string name="are_you_sure_you_want_to_delete_this_video">您确定要删除此视频吗?</string>
|
||||
<string name="tap_to_open">点击打开</string>
|
||||
<string name="watching">正在观看</string>
|
||||
@@ -658,6 +661,11 @@
|
||||
<item>启动时</item>
|
||||
<item>从不</item>
|
||||
</string-array>
|
||||
<string-array name="system_enabled_disabled_array">
|
||||
<item>已禁用</item>
|
||||
<item>已启用</item>
|
||||
<item>与系统相同</item>
|
||||
</string-array>
|
||||
<string-array name="enabled_disabled_array">
|
||||
<item>已禁用</item>
|
||||
<item>已启用</item>
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
<resources>
|
||||
<dimen name="minimized_player_max_width">500dp</dimen>
|
||||
<dimen name="app_bar_height">200dp</dimen>
|
||||
<integer name="column_width_dp">400</integer>
|
||||
<dimen name="landscape_threshold">300dp</dimen>
|
||||
</resources>
|
||||
|
||||
@@ -199,7 +199,6 @@
|
||||
<string name="previous">Previous</string>
|
||||
<string name="next">Next</string>
|
||||
<string name="comment">Comment</string>
|
||||
<string name="not_empty_close">Comment is not empty, close anyway?</string>
|
||||
<string name="str_import">Import</string>
|
||||
<string name="my_playlist_name">My Playlist Name</string>
|
||||
<string name="do_you_want_to_import_this_store">Do you want to import this store?</string>
|
||||
@@ -287,10 +286,10 @@
|
||||
<string name="planned_content_notifications_description">Schedules discovered planned content as notifications, resulting in more accurate notifications for this content.</string>
|
||||
<string name="attempt_to_utilize_byte_ranges">Attempt to utilize byte ranges</string>
|
||||
<string name="auto_update">Auto Update</string>
|
||||
<string name="force_enable_auto_rotate_in_full_screen">Force Enable Auto-Rotate In Full-Screen Mode</string>
|
||||
<string name="force_enable_auto_rotate_in_full_screen_description">Force enable auto-rotation between the two landscape orientations in full-screen mode, even when you disable auto-rotate at the system level.</string>
|
||||
<string name="auto_rotate">Auto-Rotate</string>
|
||||
<string name="simplify_sources">Simplify sources</string>
|
||||
<string name="simplify_sources_description">Deduplicate sources by resolution so that only more relevant sources are visible.</string>
|
||||
<string name="auto_rotate_dead_zone">Auto-Rotate Dead Zone</string>
|
||||
<string name="automatic_backup">Automatic Backup</string>
|
||||
<string name="background_behavior">Background Behavior</string>
|
||||
<string name="background_update">Background Update</string>
|
||||
@@ -425,8 +424,6 @@
|
||||
<string name="playlist_delete_confirmation">Playlist Delete Confirmation</string>
|
||||
<string name="playlist_delete_confirmation_description">Show confirmation dialog when deleting media from a playlist</string>
|
||||
<string name="enable_polycentric">Enable Polycentric</string>
|
||||
<string name="polycentric_local_cache">Enable Polycentric Local Caching</string>
|
||||
<string name="polycentric_local_cache_description">Caches polycentric results on-device to reduce load times, changing requires app reboot</string>
|
||||
<string name="can_be_disabled_when_you_are_experiencing_issues">Can be disabled when you are experiencing issues</string>
|
||||
<string name="bypass_rotation_prevention_description">Allows for rotation on non-video views.\nWARNING: Not designed for it</string>
|
||||
<string name="bypass_rotation_prevention_warning">This may cause unexpected behavior, and is mostly untested.</string>
|
||||
@@ -928,6 +925,17 @@
|
||||
<item>On Startup</item>
|
||||
<item>Never</item>
|
||||
</string-array>
|
||||
<string-array name="system_enabled_disabled_array">
|
||||
<item>Disabled</item>
|
||||
<item>Enabled</item>
|
||||
<item>Same as System</item>
|
||||
</string-array>
|
||||
<string-array name="auto_rotate_dead_zone" translatable="false">
|
||||
<item>0</item>
|
||||
<item>5</item>
|
||||
<item>10</item>
|
||||
<item>20</item>
|
||||
</string-array>
|
||||
<string-array name="enabled_disabled_array">
|
||||
<item>Disabled</item>
|
||||
<item>Enabled</item>
|
||||
|
||||
Submodule app/src/stable/assets/sources/bilibili updated: 258c71e4f5...9dedbca4f2
Submodule app/src/stable/assets/sources/bitchute updated: 8d7c0e2527...4309c58008
Submodule app/src/stable/assets/sources/odysee updated: 8ddb2e2f15...ba2d99c8e4
Submodule app/src/stable/assets/sources/patreon updated: 9c835e075c...7b66aea99f
Submodule app/src/stable/assets/sources/rumble updated: 6811ff4b41...cbfe372bcc
Submodule app/src/stable/assets/sources/soundcloud updated: 9a10cb8e78...3f7c9f8c94
Submodule app/src/stable/assets/sources/spotify updated: eb231adeae...946bc835e1
Submodule app/src/stable/assets/sources/twitch updated: c3ee73a3e5...543a727d78
Submodule app/src/stable/assets/sources/youtube updated: 59d694b619...e2d896ac45
Submodule app/src/unstable/assets/sources/bilibili updated: 258c71e4f5...9dedbca4f2
Submodule app/src/unstable/assets/sources/bitchute updated: 8d7c0e2527...4309c58008
Submodule app/src/unstable/assets/sources/odysee updated: 8ddb2e2f15...ba2d99c8e4
Submodule app/src/unstable/assets/sources/patreon updated: 9c835e075c...7b66aea99f
Submodule app/src/unstable/assets/sources/rumble updated: 6811ff4b41...cbfe372bcc
Submodule app/src/unstable/assets/sources/soundcloud updated: 9a10cb8e78...3f7c9f8c94
Submodule app/src/unstable/assets/sources/spotify updated: eb231adeae...946bc835e1
Submodule app/src/unstable/assets/sources/twitch updated: c3ee73a3e5...543a727d78
Submodule app/src/unstable/assets/sources/youtube updated: 59d694b619...e2d896ac45
+1
-1
Submodule dep/futopay updated: 829baef9f0...c3f532c660
+1
-1
Submodule dep/polycentricandroid updated: 44edd69ece...f7d58c6ca6
@@ -15,7 +15,6 @@ touch $DOCUMENT_ROOT/maintenance.file
|
||||
|
||||
# Swap over the content
|
||||
echo "Deploying content..."
|
||||
cp ./app/build/outputs/bundle/playstoreRelease/app-playstore-release.aab $DOCUMENT_ROOT/app-playstore-release.aab
|
||||
aws s3 cp ./app/build/outputs/bundle/playstoreRelease/app-playstore-release.aab s3://artifacts-grayjay-app/app-playstore-release.aab
|
||||
|
||||
# Notify Cloudflare to wipe the CDN cache
|
||||
|
||||
Reference in New Issue
Block a user